Skip to content

Commit

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar

Five workstreams built in parallel and merged together (deployed 2026-10-08). Teams and CODEOWNERS - Workspace teams: visible or secret, nested up to 8 levels (children inherit repository roles), maintainers and members, notify-on-mention, review assignment (round robin or load balance, count, skip busy, exclusions). Team repository roles are repo_grants with principal_kind 'team', folded into each person's effective role (highest wins), shown as "via team X". - @workspace/team mentions (inbox reason team_mention) and autocomplete; team review requests (pull.review_requested carries teams and code owners). - CODEOWNERS read from the base branch: .g1t/, .github/, root, docs/, .gitlab/ (first found). gitignore-style patterns, last match wins, people, teams and emails as owners, sections with optional and N-approval rules. Owners are asked to review; errors with line numbers on the blob page, Settings > Branches, a g1t / codeowners status and REST. "Require review from code owners" gates the merge button, API, auto-merge and the queue for people and agents alike; @g1t counts only where the file names it. - Pages /<ws>/-/teams (list, new, members, child teams, repositories, settings); REST, MCP team tool, webhooks and audit log. - identity 0027_teams, work 0026_code_owners. Labels, milestones, pull requests into any branch - Labels per repository (defaults, colours, labels page, pickers, filters), milestones (list, page with progress), a base branch chosen on open or changed later; merge, catch-up, mergeability and @g1t work use it. Branch protection applies to pull requests into the default branch. - 14 REST/MCP operations; issue/pull labeled, unlabeled, milestoned, demilestoned and pull.base_changed events (webhooks and workflows). - work 0027_labels_milestones_bases. Dependency updates: dependabot.yml, version 2 - Read from .g1t/dependabot.yml, then .github/dependabot.yml, unchanged; every option parsed and validated (unknown keys are errors), ecosystems g1t cannot update yet are accepted and listed. Version update pull requests for npm (npm, pnpm, yarn), cargo, gomod and pip (incl. poetry) on each entry's schedule, grouped, limited, with commit-message, branch-name, labels, milestone, target-branch, reviewers and assignees; security updates follow the same file; Dependabot's comment commands addressed to @g1t. Its own Security tab. security 0004_version_updates. Security suite and the Security and quality activation - Custom secret patterns (linear-time regex, limits, dry run), push protection bypass with reasons and delegated approval, validity checks for issuers with a safe read-only endpoint, alert pages. - Code scanning: SARIF 2.1.0 upload, fingerprints, fixed detection, a starter workflow (Bandit, gosec, ESLint + eslint-plugin-security, clippy via clippy-sarif), PR statuses and comments, Fix with g1t. - Dependency graph, SPDX 2.3 SBOM, dependency review on pull requests, workspace security overview, repository Security tabs. - Security and quality activation ($10/month via the price book; free for public repositories and for the free core). REST, MCP security tool, webhooks, inbox notices. security 0005_security_suite, billing 0037_security_activation. Also - Top bar: the New button is a compact "+" Create new menu (with New team); search lives in the sidebar ("Search or jump to", the palette), with a magnifier on phones; docs updated. - repos read_file: the store answers a missing path with NOT_FOUND; it is remembered as absent for 10 minutes instead of failing. - The pull request page reads PullDetail's snake_case fields again (required checks, earlier checks, review pending). - A dependency-review arm that the version-update arm made unreachable is merged into one.

syntaqxcommitted Parenta11c093Browse files
273 files+5375−680/273 viewed
+77−0
8181 ]
8282
8383 [[package]]
84+name = "aho-corasick"
85+version = "1.1.5"
86+source = "registry+https://github.com/rust-lang/crates.io-index"
87+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
88+dependencies = [
89+ "memchr",
90+]
91+
92+[[package]]
8493 name = "android_system_properties"
8594 version = "0.1.6"
8695 source = "registry+https://github.com/rust-lang/crates.io-index"
108117 ]
109118
110119 [[package]]
120+name = "arraydeque"
121+version = "0.5.1"
122+source = "registry+https://github.com/rust-lang/crates.io-index"
123+checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
124+
125+[[package]]
111126 name = "async-trait"
112127 version = "0.1.92"
113128 source = "registry+https://github.com/rust-lang/crates.io-index"
774789 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
775790
776791 [[package]]
792+name = "foldhash"
793+version = "0.2.0"
794+source = "registry+https://github.com/rust-lang/crates.io-index"
795+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
796+
797+[[package]]
777798 name = "form_urlencoded"
778799 version = "1.2.2"
779800 source = "registry+https://github.com/rust-lang/crates.io-index"
10771098 version = "0.1.0"
10781099 dependencies = [
10791100 "miniz_oxide",
1101+ "regex",
10801102 "serde",
10811103 "serde_json",
10821104 "sha1 0.11.0",
11121134 version = "0.1.0"
11131135 dependencies = [
11141136 "futures-util",
1137+ "g1t-actions",
11151138 "g1t-contracts",
11161139 "g1t-kit",
11171140 "g1t-scan",
11191142 "serde",
11201143 "serde_json",
11211144 "serde_yaml",
1145+ "toml",
11221146 "worker",
1147+ "yaml-rust2",
11231148 ]
11241149
11251150 [[package]]
11541179 name = "g1t-work"
11551180 version = "0.1.0"
11561181 dependencies = [
1182+ "base64 0.22.1",
11571183 "futures-util",
11581184 "g1t-contracts",
11591185 "g1t-kit",
12681294 version = "0.17.1"
12691295 source = "registry+https://github.com/rust-lang/crates.io-index"
12701296 checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
1297+dependencies = [
1298+ "foldhash",
1299+]
12711300
12721301 [[package]]
1302+name = "hashlink"
1303+version = "0.12.2"
1304+source = "registry+https://github.com/rust-lang/crates.io-index"
1305+checksum = "a596f1b20ed2cc5ecac41a164aaebc7258057060f06c0cf7a2ba3991ee7990fb"
1306+dependencies = [
1307+ "hashbrown",
1308+]
1309+
1310+[[package]]
12731311 name = "heck"
12741312 version = "0.5.0"
12751313 source = "registry+https://github.com/rust-lang/crates.io-index"
22672305 ]
22682306
22692307 [[package]]
2308+name = "regex"
2309+version = "1.13.1"
2310+source = "registry+https://github.com/rust-lang/crates.io-index"
2311+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
2312+dependencies = [
2313+ "aho-corasick",
2314+ "memchr",
2315+ "regex-automata",
2316+ "regex-syntax",
2317+]
2318+
2319+[[package]]
2320+name = "regex-automata"
2321+version = "0.4.18"
2322+source = "registry+https://github.com/rust-lang/crates.io-index"
2323+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
2324+dependencies = [
2325+ "aho-corasick",
2326+ "memchr",
2327+ "regex-syntax",
2328+]
2329+
2330+[[package]]
2331+name = "regex-syntax"
2332+version = "0.8.11"
2333+source = "registry+https://github.com/rust-lang/crates.io-index"
2334+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
2335+
2336+[[package]]
22702337 name = "reqwest"
22712338 version = "0.13.5"
22722339 source = "registry+https://github.com/rust-lang/crates.io-index"
38693936 checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
38703937
38713938 [[package]]
3939+name = "yaml-rust2"
3940+version = "0.13.0"
3941+source = "registry+https://github.com/rust-lang/crates.io-index"
3942+checksum = "57e5b818a27a4cd30884ea380857a5e56f7ec3ba24a3990a3cc0b95af3238e18"
3943+dependencies = [
3944+ "arraydeque",
3945+ "hashlink",
3946+]
3947+
3948+[[package]]
38723949 name = "yoke"
38733950 version = "0.8.3"
38743951 source = "registry+https://github.com/rust-lang/crates.io-index"
+1−0
1919 mod responses;
2020 mod rest;
2121 mod runners;
22+mod security;
2223 mod tools;
2324
2425 use g1t_contracts::billing::FinishRunArgs;
+2−1
1111 const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
1212
1313 const INSTRUCTIONS: &str = "g1t is a git forge where people and agents work through issues and pull requests. Repositories are named \"owner/name\"; issues and pull requests in one share a sequence of numbers.
14−Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, webhook, access, workspace, account. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs.
14+Tools are resources, each with an `action`: search, repository, issue, pull_request, agent, plan, memory, workflow, secret, security, webhook, access, workspace, account, notifications. The `action` field lists each action and the fields it needs. You see only what your token's scopes allow; a refusal names the scope it needs.
1515 Find a repository: account whoami lists your workspaces; repository list or search finds one.
1616 Work on an issue: issue get (read it and the pull requests already made for it), memory recall, then pull_request create with the issue's number: you get a draft with its own fork to clone and push to. Record your reasoning with pull_request record_session as you go, push, then pull_request ready with a summary. Watch `overlaps` and `behind` on pull_request get, and its checks there: `statuses` from the repository's workflows and `required_checks`, which must pass before it merges. If one fails, read why with workflow get_run and job_logs, push a fix, and the checks run again.
1717 Hand work to g1t's agent: agent delegate opens an issue and starts it in one step; agent assign starts it on an existing issue. Each costs the workspace money.
18+Security: security code_alerts, vulnerability_alerts and secret_alerts show what to fix; fix it in your pull request, which the Code scanning and Dependency review checks judge.
1819 When you learn something the next agent needs, memory remember it (scope project or workspace). Never a secret.";
1920
2021 fn result(id: &Value, value: Value) -> Value {
+2−2
119119 "token_endpoint_auth_methods_supported": ["none"],
120120 // A client may ask for some of these with `scope`; the person
121121 // approving can trim them. Asking for none gives the agent preset.
122− "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()),
122+ "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()).to_vec(),
123123 "service_documentation": "https://docs.g1t.sh/guides/authentication/",
124124 })
125125 }
245245 "authorization_servers": [services.addresses.api],
246246 "bearer_methods_supported": ["header"],
247247 "resource_documentation": "https://docs.g1t.sh/guides/bring-your-own-agent/",
248− "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()),
248+ "scopes_supported": g1t_contracts::scopes::Scope::ALL.map(|scope| scope.as_str()).to_vec(),
249249 }))?
250250 }
251251 ("POST", "/oauth/register") => register(request).await?,
+133−0
88 use serde_json::{Map, Value, json};
99
1010 use crate::operations::Op;
11+use crate::security::SecurityOp;
1112 use crate::rest::{ROUTES, Route};
1213
1314 /// The sections of the API reference: a name, what it covers, and its
8182 Op::GetRepoSettings,
8283 Op::UpdateRepoSettings,
8384 Op::ListCheckNames,
85+ Op::GetCodeownersErrors,
8486 Op::ListEvents,
8587 ],
8688 ),
103105 ],
104106 ),
105107 (
108+ "Teams",
109+ "Groups of a workspace's members: given a role on repositories together, mentioned together as @workspace/team, and asked to review together. Any member may create a team; the workspace's owners and the team's maintainers manage it.",
110+ &[
111+ Op::ListTeams,
112+ Op::CreateTeam,
113+ Op::GetTeam,
114+ Op::UpdateTeam,
115+ Op::DeleteTeam,
116+ Op::ListTeamMembers,
117+ Op::SetTeamMember,
118+ Op::RemoveTeamMember,
119+ Op::ListChildTeams,
120+ Op::ListTeamRepos,
121+ Op::SetTeamRepo,
122+ Op::RemoveTeamRepo,
123+ Op::SetTeamReviewAssignment,
124+ Op::ListUserTeams,
125+ ],
126+ ),
127+ (
106128 "Security",
107129 "Secrets found in what is pushed and in a repository's history, and dependencies with known vulnerabilities: listing the alerts, and dismissing or reopening them.",
108130 &[Op::ListSecurityAlerts, Op::DismissSecurityAlert, Op::ReopenSecurityAlert],
109131 ),
110132 (
133+ "Secret scanning",
134+ "Secrets found in pushes and history, where each one is, pushing past push protection with a reason (and asking for approval when the workspace delegates bypasses), checking with a secret's issuer whether it still works, and custom patterns.",
135+ &[
136+ Op::Security(SecurityOp::ListSecretAlerts),
137+ Op::Security(SecurityOp::GetSecretAlert),
138+ Op::Security(SecurityOp::UpdateSecretAlert),
139+ Op::Security(SecurityOp::ListSecretLocations),
140+ Op::Security(SecurityOp::BypassPushProtection),
141+ Op::Security(SecurityOp::CheckSecretValidity),
142+ Op::Security(SecurityOp::ListBypassRequests),
143+ Op::Security(SecurityOp::ReviewBypassRequest),
144+ Op::Security(SecurityOp::ListCustomPatterns),
145+ Op::Security(SecurityOp::CreateCustomPattern),
146+ Op::Security(SecurityOp::UpdateCustomPattern),
147+ Op::Security(SecurityOp::DeleteCustomPattern),
148+ Op::Security(SecurityOp::DryRunCustomPattern),
149+ ],
150+ ),
151+ (
152+ "Code scanning",
153+ "Results of static analysis tools, uploaded as SARIF: alerts on the default branch, the analyses that made them, uploads, and putting g1t on an alert to fix it.",
154+ &[
155+ Op::Security(SecurityOp::ListCodeAlerts),
156+ Op::Security(SecurityOp::GetCodeAlert),
157+ Op::Security(SecurityOp::UpdateCodeAlert),
158+ Op::Security(SecurityOp::ListAnalyses),
159+ Op::Security(SecurityOp::UploadSarif),
160+ Op::Security(SecurityOp::GetSarifUpload),
161+ Op::Security(SecurityOp::FixAlert),
162+ ],
163+ ),
164+ (
165+ "Supply chain",
166+ "What a repository depends on: vulnerability alerts, the dependency graph, an SPDX SBOM of it, and comparing two commits' dependencies as dependency review does.",
167+ &[
168+ Op::Security(SecurityOp::ListVulnerabilityAlerts),
169+ Op::Security(SecurityOp::GetVulnerabilityAlert),
170+ Op::Security(SecurityOp::UpdateVulnerabilityAlert),
171+ Op::Security(SecurityOp::GetDependencyGraph),
172+ Op::Security(SecurityOp::GetSbom),
173+ Op::Security(SecurityOp::CompareDependencies),
174+ ],
175+ ),
176+ (
177+ "Security settings",
178+ "When pull request checks fail, dependency review's policy, delegated bypass and validity checks, and a workspace's security overview.",
179+ &[
180+ Op::Security(SecurityOp::GetSettings),
181+ Op::Security(SecurityOp::UpdateSettings),
182+ Op::Security(SecurityOp::GetWorkspaceSettings),
183+ Op::Security(SecurityOp::UpdateWorkspaceSettings),
184+ Op::Security(SecurityOp::GetOverview),
185+ ],
186+ ),
187+ (
111188 "Issues",
112189 "What should change in a repository, with labels and comments. Issues and pull requests share one sequence of numbers.",
113190 &[
120197 Op::AssignIssue,
121198 Op::Delegate,
122199 Op::AddComment,
200+ Op::ListIssueLabels,
201+ Op::AddIssueLabels,
202+ Op::SetIssueLabels,
203+ Op::RemoveIssueLabels,
204+ ],
205+ ),
206+ (
207+ "Labels and milestones",
208+ "A repository's labels, which issues and pull requests carry by name, and its milestones, which gather them under a goal and a due date.",
209+ &[
123210 Op::ListLabels,
211+ Op::CreateLabel,
212+ Op::UpdateLabel,
213+ Op::DeleteLabel,
214+ Op::AddDefaultLabels,
215+ Op::ListMilestones,
216+ Op::CreateMilestone,
217+ Op::GetMilestone,
218+ Op::UpdateMilestone,
219+ Op::DeleteMilestone,
124220 ],
125221 ),
126222 (
135231 Op::ListPullRequests,
136232 Op::CreatePullRequest,
137233 Op::GetPullRequest,
234+ Op::UpdatePullRequest,
138235 Op::GetPullRequestChanges,
139236 Op::MarkPullRequestReady,
237+ Op::RequestReviewers,
238+ Op::RemoveRequestedReviewers,
140239 Op::ReviewPullRequest,
141240 Op::MergePullRequest,
142241 Op::ClosePullRequest,
299398 Op::GetPlan => "Get a plan",
300399 Op::ApplyPlan => "Apply a plan",
301400 Op::ListLabels => "List labels",
401+ Op::CreateLabel => "Create a label",
402+ Op::UpdateLabel => "Update a label",
403+ Op::DeleteLabel => "Delete a label",
404+ Op::AddDefaultLabels => "Add the default labels",
405+ Op::ListIssueLabels => "List an issue's labels",
406+ Op::AddIssueLabels => "Add labels to an issue",
407+ Op::SetIssueLabels => "Set an issue's labels",
408+ Op::RemoveIssueLabels => "Remove labels from an issue",
409+ Op::ListMilestones => "List milestones",
410+ Op::GetMilestone => "Get a milestone",
411+ Op::CreateMilestone => "Create a milestone",
412+ Op::UpdateMilestone => "Update a milestone",
413+ Op::DeleteMilestone => "Delete a milestone",
414+ Op::UpdatePullRequest => "Update a pull request",
302415 Op::AddComment => "Add a comment",
303416 Op::ReviewPullRequest => "Review a pull request",
304417 Op::ListPullRequests => "List pull requests",
382495 Op::PinProject => "Pin a project",
383496 Op::UnpinProject => "Unpin a project",
384497 Op::ReorderPinnedProjects => "Reorder your pinned projects",
498+ Op::ListTeams => "List teams",
499+ Op::GetTeam => "Get a team",
500+ Op::CreateTeam => "Create a team",
501+ Op::UpdateTeam => "Update a team",
502+ Op::DeleteTeam => "Delete a team",
503+ Op::ListTeamMembers => "List a team's members",
504+ Op::SetTeamMember => "Add or change a team member",
505+ Op::RemoveTeamMember => "Remove a team member",
506+ Op::ListChildTeams => "List child teams",
507+ Op::ListTeamRepos => "List a team's repositories",
508+ Op::SetTeamRepo => "Give a team a role on a repository",
509+ Op::RemoveTeamRepo => "Remove a team from a repository",
510+ Op::SetTeamReviewAssignment => "Set a team's review assignment",
511+ Op::ListUserTeams => "List someone's teams",
512+ Op::RequestReviewers => "Request reviewers",
513+ Op::RemoveRequestedReviewers => "Remove requested reviewers",
514+ Op::GetCodeownersErrors => "List CODEOWNERS errors",
515+ Op::Security(op) => op.title(),
385516 }
386517 }
387518
476607 "PUT" => "set_issue_subscription".to_owned(),
477608 _ => "delete_issue_subscription".to_owned(),
478609 },
610+ // One label off an issue, by its name in the path.
611+ ("DELETE", ":label") if route.path.contains("/issues/:number/") => "remove_issue_label".to_owned(),
479612 ("DELETE", "saved") => "unsave_thread".to_owned(),
480613 ("DELETE", "snooze") => "unsnooze_thread".to_owned(),
481614 _ => op.name().to_owned(),
+1225−17
99 OutsideCollaboratorsArgs, RemoveCollaboratorArgs, RepoAccess, RepoAccessArgs, RepoInvitation, RepoRole,
1010 RespondRepoInvitationArgs, RevokeRepoInvitationArgs, SetBasePermissionArgs, SetCollaboratorRoleArgs,
1111 };
12+use g1t_contracts::codeowners::CodeOwnersErrorsArgs;
1213 use g1t_contracts::identity::AgentScope;
1314 use g1t_contracts::events::{Event, ListArgs as ListEventsArgs};
1415 use g1t_contracts::identity::{CreateWorkspaceArgs, UpdateWorkspaceArgs, Workspace};
1516 use g1t_contracts::repos::{CreateArgs, GetArgs, ListArgs as ListReposArgs, Repo, RepoPath};
17+use g1t_contracts::teams::{
18+ CreateTeamArgs, DeleteTeamArgs, ListTeamsArgs, RemoveTeamMemberArgs, RemoveTeamRepoArgs, ReviewAlgorithm,
19+ ReviewAssignment, SetTeamMemberArgs, SetTeamRepoArgs, Team, TeamArgs, TeamRole, TeamVisibility, UpdateTeamArgs,
20+ UserTeamsArgs,
21+};
1622 use g1t_contracts::security::{
1723 AlertChange, AlertState, DismissArgs, DismissReason, OverviewArgs as SecurityOverviewArgs, ReopenArgs,
1824 SecurityOverview,
1925 };
2026
2127 use crate::alerts::{AlertKind, SecurityAlert};
28+use crate::security::SecurityOp;
2229 use g1t_contracts::inbox::{Reason, Severity, WATCH_EVENTS, WatchLevel};
2330 use g1t_contracts::work::*;
2431 use g1t_contracts::{FailureCode, Outcome, Viewer};
131138 GetPlan,
132139 ApplyPlan,
133140 ListLabels,
141+ CreateLabel,
142+ UpdateLabel,
143+ DeleteLabel,
144+ AddDefaultLabels,
145+ ListIssueLabels,
146+ AddIssueLabels,
147+ SetIssueLabels,
148+ RemoveIssueLabels,
149+ ListMilestones,
150+ GetMilestone,
151+ CreateMilestone,
152+ UpdateMilestone,
153+ DeleteMilestone,
134154 AddComment,
135155 ReviewPullRequest,
136156 ListPullRequests,
137157 GetPullRequest,
138158 CreatePullRequest,
159+ UpdatePullRequest,
139160 RecordSession,
140161 ReadSession,
141162 MarkPullRequestReady,
214235 PinProject,
215236 UnpinProject,
216237 ReorderPinnedProjects,
238+ ListTeams,
239+ GetTeam,
240+ CreateTeam,
241+ UpdateTeam,
242+ DeleteTeam,
243+ ListTeamMembers,
244+ SetTeamMember,
245+ RemoveTeamMember,
246+ ListChildTeams,
247+ ListTeamRepos,
248+ SetTeamRepo,
249+ RemoveTeamRepo,
250+ SetTeamReviewAssignment,
251+ ListUserTeams,
252+ RequestReviewers,
253+ RemoveRequestedReviewers,
254+ GetCodeownersErrors,
255+ /// The security suite's operations: see [`crate::security`].
256+ Security(SecurityOp),
217257 }
218258
219259 fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
415455 g1t_contracts::webhooks::EVENT_TYPES.to_vec()
416456 }
417457
458+fn label_schema() -> Value {
459+ json!({ "type": "string", "description": "The label's name, e.g. \"good first issue\". URL-encode spaces in the path." })
460+}
461+
462+fn milestone_schema() -> Value {
463+ json!({ "type": "integer", "description": "The milestone's number, from list_milestones." })
464+}
465+
466+/// A milestone given as a number, or as null or 0 for none: `Some(0)` for
467+/// none, `None` when it was not given.
468+fn milestone_input(input: &Value) -> Option<u32> {
469+ match input.get("milestone") {
470+ None => None,
471+ Some(Value::Null) => Some(0),
472+ Some(_) => integer(input, "milestone"),
473+ }
474+}
475+
418476 fn repo_schema() -> Value {
419477 json!({
420478 "type": "string",
435493 })
436494 }
437495
496+fn team_schema() -> Value {
497+ json!({
498+ "type": "string",
499+ "description": "The team's slug, as in its mention @workspace/slug, e.g. \"backend\".",
500+ })
501+}
502+
503+/// A person's place in a team.
504+fn team_role_schema() -> Value {
505+ json!({
506+ "type": "string",
507+ "enum": [TeamRole::Member.as_str(), TeamRole::Maintainer.as_str()],
508+ "description": "member, or maintainer: also manages the team's people and settings. Defaults to member.",
509+ })
510+}
511+
512+fn team_visibility_schema() -> Value {
513+ json!({
514+ "type": "string",
515+ "enum": [TeamVisibility::Visible.as_str(), TeamVisibility::Secret.as_str()],
516+ "description": "visible: every member of the workspace sees it. secret: only its own people and the workspace's owners.",
517+ })
518+}
519+
520+fn include_child_teams_schema() -> Value {
521+ json!({
522+ "type": "boolean",
523+ "description": "Also the people of its child teams: listed with list_members, picked from with review assignment.",
524+ })
525+}
526+
527+/// The fields of a team's review assignment, each optional.
528+fn review_assignment_properties() -> Value {
529+ json!({
530+ "enabled": {
531+ "type": "boolean",
532+ "description": "On: g1t picks count people from the team to ask. Off: everyone in it is asked.",
533+ },
534+ "algorithm": {
535+ "type": "string",
536+ "enum": [ReviewAlgorithm::RoundRobin.as_str(), ReviewAlgorithm::LoadBalance.as_str()],
537+ "description": "round_robin: whoever this team asked least recently. load_balance: whoever has the fewest pull requests waiting on their review.",
538+ },
539+ "count": {
540+ "type": "integer",
541+ "minimum": 1,
542+ "maximum": g1t_contracts::teams::MAX_ASSIGNED,
543+ "description": "How many people to pick, 1 to 10. People from the team already asked count towards it.",
544+ },
545+ "skip_busy": {
546+ "type": "boolean",
547+ "description": "Leave out anyone with busy_at or more pull requests waiting on their review.",
548+ },
549+ "busy_at": {
550+ "type": "integer",
551+ "minimum": 1,
552+ "maximum": 100,
553+ "description": "With skip_busy: how many waiting reviews make someone busy, 1 to 100.",
554+ },
555+ "include_child_teams": include_child_teams_schema(),
556+ "excluded": {
557+ "type": "array",
558+ "items": { "type": "string" },
559+ "description": "Usernames never picked. Replaces the whole list.",
560+ },
561+ "notify_team": {
562+ "type": "boolean",
563+ "description": "Also tell the rest of the team when people are picked.",
564+ },
565+ })
566+}
567+
568+/// The inputs naming a team, with `more` added.
569+fn team_target(more: Value) -> Value {
570+ let mut properties = json!({ "workspace": workspace_schema(), "team": team_schema() });
571+ if let (Some(all), Value::Object(more)) = (properties.as_object_mut(), more) {
572+ all.extend(more);
573+ }
574+ properties
575+}
576+
577+/// The people and teams to ask, or stop asking, to review a pull request.
578+fn requested_reviewers_properties() -> Value {
579+ numbered(json!({
580+ "reviewers": {
581+ "type": "array",
582+ "items": { "type": "string" },
583+ "description": "Usernames. g1t asks a g1t agent.",
584+ },
585+ "team_reviewers": {
586+ "type": "array",
587+ "items": { "type": "string" },
588+ "description": "Teams, as \"workspace/team\", or the team's slug in the repository's workspace.",
589+ },
590+ }))
591+}
592+
438593 fn thread_id_schema() -> Value {
439594 json!({ "type": "string", "description": "The thread's id, from list_notifications." })
440595 }
461616 }
462617
463618 impl Op {
464− pub const ALL: [Op; 135] = [
619+ pub const ALL: [Op; 197] = [
465620 Op::Whoami,
466621 Op::CreateWorkspace,
467622 Op::DeleteWorkspace,
514669 Op::GetPlan,
515670 Op::ApplyPlan,
516671 Op::ListLabels,
672+ Op::CreateLabel,
673+ Op::UpdateLabel,
674+ Op::DeleteLabel,
675+ Op::AddDefaultLabels,
676+ Op::ListIssueLabels,
677+ Op::AddIssueLabels,
678+ Op::SetIssueLabels,
679+ Op::RemoveIssueLabels,
680+ Op::ListMilestones,
681+ Op::GetMilestone,
682+ Op::CreateMilestone,
683+ Op::UpdateMilestone,
684+ Op::DeleteMilestone,
517685 Op::AddComment,
518686 Op::ReviewPullRequest,
519687 Op::ListPullRequests,
520688 Op::GetPullRequest,
521689 Op::CreatePullRequest,
690+ Op::UpdatePullRequest,
522691 Op::RecordSession,
523692 Op::ReadSession,
524693 Op::MarkPullRequestReady,
597766 Op::PinProject,
598767 Op::UnpinProject,
599768 Op::ReorderPinnedProjects,
769+ Op::ListTeams,
770+ Op::GetTeam,
771+ Op::CreateTeam,
772+ Op::UpdateTeam,
773+ Op::DeleteTeam,
774+ Op::ListTeamMembers,
775+ Op::SetTeamMember,
776+ Op::RemoveTeamMember,
777+ Op::ListChildTeams,
778+ Op::ListTeamRepos,
779+ Op::SetTeamRepo,
780+ Op::RemoveTeamRepo,
781+ Op::SetTeamReviewAssignment,
782+ Op::ListUserTeams,
783+ Op::RequestReviewers,
784+ Op::RemoveRequestedReviewers,
785+ Op::GetCodeownersErrors,
786+ Op::Security(SecurityOp::ListSecretAlerts),
787+ Op::Security(SecurityOp::GetSecretAlert),
788+ Op::Security(SecurityOp::UpdateSecretAlert),
789+ Op::Security(SecurityOp::ListSecretLocations),
790+ Op::Security(SecurityOp::BypassPushProtection),
791+ Op::Security(SecurityOp::CheckSecretValidity),
792+ Op::Security(SecurityOp::ListBypassRequests),
793+ Op::Security(SecurityOp::ReviewBypassRequest),
794+ Op::Security(SecurityOp::ListCustomPatterns),
795+ Op::Security(SecurityOp::CreateCustomPattern),
796+ Op::Security(SecurityOp::UpdateCustomPattern),
797+ Op::Security(SecurityOp::DeleteCustomPattern),
798+ Op::Security(SecurityOp::DryRunCustomPattern),
799+ Op::Security(SecurityOp::ListCodeAlerts),
800+ Op::Security(SecurityOp::GetCodeAlert),
801+ Op::Security(SecurityOp::UpdateCodeAlert),
802+ Op::Security(SecurityOp::ListAnalyses),
803+ Op::Security(SecurityOp::UploadSarif),
804+ Op::Security(SecurityOp::GetSarifUpload),
805+ Op::Security(SecurityOp::ListVulnerabilityAlerts),
806+ Op::Security(SecurityOp::GetVulnerabilityAlert),
807+ Op::Security(SecurityOp::UpdateVulnerabilityAlert),
808+ Op::Security(SecurityOp::FixAlert),
809+ Op::Security(SecurityOp::GetDependencyGraph),
810+ Op::Security(SecurityOp::GetSbom),
811+ Op::Security(SecurityOp::CompareDependencies),
812+ Op::Security(SecurityOp::GetSettings),
813+ Op::Security(SecurityOp::UpdateSettings),
814+ Op::Security(SecurityOp::GetWorkspaceSettings),
815+ Op::Security(SecurityOp::UpdateWorkspaceSettings),
816+ Op::Security(SecurityOp::GetOverview),
600817 ];
601818
602819 pub fn by_name(name: &str) -> Option<Op> {
658875 Op::GetPlan => "get_plan",
659876 Op::ApplyPlan => "apply_plan",
660877 Op::ListLabels => "list_labels",
878+ Op::CreateLabel => "create_label",
879+ Op::UpdateLabel => "update_label",
880+ Op::DeleteLabel => "delete_label",
881+ Op::AddDefaultLabels => "add_default_labels",
882+ Op::ListIssueLabels => "list_issue_labels",
883+ Op::AddIssueLabels => "add_issue_labels",
884+ Op::SetIssueLabels => "set_issue_labels",
885+ Op::RemoveIssueLabels => "remove_issue_labels",
886+ Op::ListMilestones => "list_milestones",
887+ Op::GetMilestone => "get_milestone",
888+ Op::CreateMilestone => "create_milestone",
889+ Op::UpdateMilestone => "update_milestone",
890+ Op::DeleteMilestone => "delete_milestone",
661891 Op::AddComment => "add_comment",
662892 Op::ReviewPullRequest => "review_pull_request",
663893 Op::ListPullRequests => "list_pull_requests",
664894 Op::GetPullRequest => "get_pull_request",
665895 Op::CreatePullRequest => "create_pull_request",
896+ Op::UpdatePullRequest => "update_pull_request",
666897 Op::RecordSession => "record_session",
667898 Op::ReadSession => "read_session",
668899 Op::MarkPullRequestReady => "mark_pull_request_ready",
741972 Op::PinProject => "pin_project",
742973 Op::UnpinProject => "unpin_project",
743974 Op::ReorderPinnedProjects => "reorder_pinned_projects",
975+ Op::ListTeams => "list_teams",
976+ Op::GetTeam => "get_team",
977+ Op::CreateTeam => "create_team",
978+ Op::UpdateTeam => "update_team",
979+ Op::DeleteTeam => "delete_team",
980+ Op::ListTeamMembers => "list_team_members",
981+ Op::SetTeamMember => "set_team_member",
982+ Op::RemoveTeamMember => "remove_team_member",
983+ Op::ListChildTeams => "list_child_teams",
984+ Op::ListTeamRepos => "list_team_repos",
985+ Op::SetTeamRepo => "set_team_repo",
986+ Op::RemoveTeamRepo => "remove_team_repo",
987+ Op::SetTeamReviewAssignment => "set_team_review_assignment",
988+ Op::ListUserTeams => "list_user_teams",
989+ Op::RequestReviewers => "request_reviewers",
990+ Op::RemoveRequestedReviewers => "remove_requested_reviewers",
991+ Op::GetCodeownersErrors => "get_codeowners_errors",
992+ Op::Security(op) => op.name(),
744993 }
745994 }
746995
8221071 "Move a repository to another workspace, keeping its name. You must own both workspaces, and the destination must not already have a repository of that name; a free destination takes a private repository only if its private storage has room. Everything moves with it: git data, issues, pull requests, comments, labels, workflow runs, deployments, its project, and its own secrets, variables and webhooks. Its old address keeps working: web pages, git remotes and API calls redirect to the new one until a repository is made at the old address. Usage from now on is charged to the new workspace."
8231072 }
8241073 Op::GetRepoSettings => {
825− "How a repository handles pull requests, as its default branch's protection: the checks that must pass (required_checks), the approvals a merge needs, whether required checks can be bypassed, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged. The same rules hold for a person's pull request and an agent's."
1074+ "How a repository handles pull requests, as its default branch's protection: the checks that must pass (required_checks), the approvals a merge needs, whether its code owners must approve (`require_code_owner_review`), whether required checks can be bypassed, whether a pull request must be up to date, and how g1t's agents are reviewed, revised and merged. The same rules hold for a person's pull request and an agent's."
8261075 }
8271076 Op::UpdateRepoSettings => {
828− "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. Needs the Maintain role or higher."
1077+ "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. With `require_code_owner_review`, a pull request merges only once the code owners of every file it changes, as the CODEOWNERS file of the branch it merges into names them, have approved it. Needs the Maintain role or higher."
8291078 }
8301079 Op::ListCheckNames => {
8311080 "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)."
8611110 "Create a repository in one of your workspaces, empty or as a copy of a public git repository elsewhere."
8621111 }
8631112 Op::ListIssues => {
864− "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it."
1113+ "Issues on a repository, newest first. An issue is something that should change: a bug, a feature, a question. Pull requests are made against it. Filter by state, by a label's name, or by a milestone's number."
8651114 }
8661115 Op::GetIssue => {
8671116 "An issue: its description (which may say what done means, under \"Definition of done\"), labels, its comments, and every pull request made against it with its status. If the issue is closed, resolved_by is the number of the pull request that was merged for it. Read this before opening a pull request, to see what others have already tried."
8681117 }
8691118 Op::CreateIssue => {
870− "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request."
1119+ "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request. labels are the repository's labels by name; a name it does not have yet is created when you have the Triage role or higher, and refused otherwise. milestone, a milestone's number, needs the Triage role."
8711120 }
8721121 Op::UpdateIssue => {
873− "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher."
1122+ "Change an issue's title, body, labels, milestone or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set, and milestone null or 0 takes it out of its milestone. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher, and so does the milestone. Each label added or removed is an issue.labeled or issue.unlabeled event."
8741123 }
8751124 Op::CloseIssue => {
8761125 "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher."
8911140 Op::Delegate => {
8921141 "Put an agent on something in one step: open an issue and assign it to g1t at once. Say what you want done in plain words, with what done means if you know it. What must pass before its pull request merges is the default branch's required checks. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose."
8931142 }
894− Op::ListLabels => "The labels available on a repository's issues.",
1143+ Op::ListLabels => {
1144+ "A repository's labels, by name: each one's color (six hex digits), description, and how many issues and pull requests carry it. A new repository starts with bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security."
1145+ }
1146+ Op::CreateLabel => {
1147+ "Create a label, named by label. Names are lowercase and unique in a repository, at most 50 characters; color is six hex digits (one is chosen from the name when left out), description at most 100 characters. Needs the Triage role or higher."
1148+ }
1149+ Op::UpdateLabel => {
1150+ "Change a label's name, color or description; only the fields given change. Renaming it renames it on every issue and pull request that carries it. Needs the Triage role or higher."
1151+ }
1152+ Op::DeleteLabel => {
1153+ "Delete a label. It is taken off every issue and pull request that carries it, without events for each. Needs the Triage role or higher."
1154+ }
1155+ Op::AddDefaultLabels => {
1156+ "Add the default labels a repository does not have yet: bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security. Labels it has already are left as they are. Returns every label it has now. Needs the Triage role or higher."
1157+ }
1158+ Op::ListIssueLabels => {
1159+ "The labels an issue or a pull request carries, with their colors and descriptions. Issues and pull requests share numbers."
1160+ }
1161+ Op::AddIssueLabels => {
1162+ "Add labels to an issue or a pull request, keeping the ones it has. A name the repository does not have yet is created when you have the Triage role or higher; without it, you may use the repository's labels on what you opened. Each label added is an issue.labeled or pull.labeled event. Returns its labels now, at most 20."
1163+ }
1164+ Op::SetIssueLabels => {
1165+ "Replace the labels of an issue or a pull request with these; an empty list takes them all off. The same rules as add_issue_labels. Returns its labels now."
1166+ }
1167+ Op::RemoveIssueLabels => {
1168+ "Take labels off an issue or a pull request: label for one, labels for several, or neither for all of them. The labels stay on the repository. Returns its labels now."
1169+ }
1170+ Op::ListMilestones => {
1171+ "A repository's milestones: open ones soonest due first (those without a due date after), then closed ones, most recently closed first. Each has its number, title, description, due_on (YYYY-MM-DD), state, and open_items and closed_items: its issues and pull requests, a merged pull request counting as closed."
1172+ }
1173+ Op::GetMilestone => "A milestone, with every issue and pull request in it, newest first.",
1174+ Op::CreateMilestone => {
1175+ "Create a milestone: a title, unique in the repository, at most 100 characters; a description in Markdown; and a due_on day (YYYY-MM-DD). Milestones are numbered from 1 in each repository, apart from issues. Needs the Triage role or higher."
1176+ }
1177+ Op::UpdateMilestone => {
1178+ "Change a milestone's title, description, due date or state (open or closed); only the fields given change, and due_on \"\" clears its due date. Needs the Triage role or higher."
1179+ }
1180+ Op::DeleteMilestone => {
1181+ "Delete a milestone. The issues and pull requests in it are in no milestone afterwards. Needs the Triage role or higher."
1182+ }
8951183 Op::AddComment => {
8961184 "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change."
8971185 }
8991187 "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened, or one g1t made for you (you are its requested_by)."
9001188 }
9011189 Op::ListPullRequests => {
902− "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed."
1190+ "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed. Filter by a label's name, a milestone's number, or base, the branch they merge into."
9031191 }
9041192 Op::GetPullRequest => {
905− "A pull request's status, head commit, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the default branch requires, as success, failure, pending or expected when nothing has reported it yet), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files."
1193+ "A pull request's status, base (the branch it merges into), head commit, labels, milestone, comments and reviews, the issue it is for, its checks (statuses: what each workflow run reported on its head, with a link to the run; get_workflow_run and get_job_logs say why one failed), required_checks (each check the default branch requires, as success, failure, pending or expected when nothing has reported it yet; empty for a pull request into another branch, which the default branch's protection does not cover), whether it is behind the branch it would merge into, and overlaps: other pull requests in progress that change the same files. An overlap with a pull request for a different issue means the two will conflict; say so, or keep clear of those files. `pull.reviewers` lists the people asked to review it and `pull.team_reviewers` the teams, as `workspace/team`. `code_owners` is there when the branch it merges into has a CODEOWNERS file: its `path`, whether code owners' approval is `required`, `reviews` (one per section and rule that owns a changed file, with its `section`, `line`, `pattern`, `owners`, `files`, whether it is `optional`, the approvals `required`, who it was `approved_by` and `changes_requested_by`, and whether it is `satisfied`), what is still `missing`, and how many `errors` the file has (get_codeowners_errors lists them)."
9061194 }
9071195 Op::CreatePullRequest => {
908− "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once."
1196+ "Start a change. Opens a draft pull request with its own fork of the repository and returns the fork's git remote. Clone it, commit your work there, push, record your session as you go, then call mark_pull_request_ready. Give the issue it is for whenever there is one. If the change is already on a branch pushed to the repository, give that branch instead: no fork is made and the pull request is ready for review at once. It merges into the default branch unless base names another existing branch; leave base out unless you were asked for another."
1197+ }
1198+ Op::UpdatePullRequest => {
1199+ "Change an open pull request: base, the branch it merges into (an existing branch; needs the Write role or higher); its labels (replacing the set, as set_issue_labels does); its milestone (a number, or null or 0 for none; needs the Triage role); and assignees and reviewers (each replacing the set). Only the fields given change. Its author, or whoever asked g1t for it, may change it; anyone else needs the Triage role or higher. A new base is a pull.base_changed event: it leaves the merge queue, and whether it is behind, merges cleanly and has the checks it needs is worked out against the new base."
9091200 }
9101201 Op::RecordSession => {
9111202 "Append entries to a pull request's session: the prompt you were given, your reasoning, the tools you ran. This is how people later see why a change was made, so record as you work, not only at the end."
9191210 "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue."
9201211 }
9211212 Op::MergePullRequest => {
922− "Land a pull request on the repository's main branch. Merging needs the Write role or higher, and only once it is marked ready and every check the default branch requires has passed on its head (see required_checks on get_pull_request); with ignore_checks, someone who may merge can bypass them where the repository allows it. Merging resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded. Where the repository has a merge queue, it joins the queue instead of landing at once. If main has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests to be up to date refuses instead, so pull main into its fork or branch, push, and merge again. Check status in the result to see whether it has landed."
1213+ "Land a pull request on its base, the branch it merges into (the default branch unless it names another). Merging needs the Write role or higher, and only once it is marked ready and, into the default branch, every check the default branch requires has passed on its head (see required_checks on get_pull_request); with ignore_checks, someone who may merge can bypass them where the repository allows it. Merging into the default branch resolves the issue it was made for: the issue closes recording this pull request, and the other pull requests still in progress for that issue close as superseded; merging into another branch leaves the issue open. Where the repository has a merge queue, a pull request into the default branch joins the queue instead of landing at once. If its base has moved since the pull request was opened, it is brought up to date first and lands when that is done; a repository that requires pull requests into its default branch to be up to date refuses instead, so pull the base into its fork or branch, push, and merge again. Check status in the result to see whether it has landed."
9231214 }
9241215 Op::ListEvents => {
9251216 "The timeline of a repository: pushes, issues, pull requests, comments and session activity, newest first."
11171408 Op::ReorderPinnedProjects => {
11181409 "Put your pins in a workspace in a new order: `projects` names every pinned project's slug, once, in the order you want them. Returns your pins, in order."
11191410 }
1411+ Op::ListTeams => {
1412+ "A workspace's teams that you can see, yours first, then by name. A team is a group of the workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. A secret team is seen only by its own people and the workspace's owners. Each team has its `slug`, `name`, `description`, `visibility` (`visible` or `secret`), `parent`, whether its people are notified when it is mentioned (`notify`), its `review_assignment`, how many people, repositories and child teams it has (`members_count`, `repos_count`, `child_teams_count`), your own `viewer_role` in it, and whether you may change it (`can_manage`). `query` narrows them by name or slug. Members of the workspace only."
1413+ }
1414+ Op::GetTeam => {
1415+ "One team, by its slug, as list_teams describes it. A secret team is found only by its own people and the workspace's owners; anyone else is told it does not exist. Members of the workspace only."
1416+ }
1417+ Op::CreateTeam => {
1418+ "Create a team in a workspace. Any member may create one, and becomes its first maintainer; `members` adds more people by username, each a member of the workspace. `slug` is made from the name unless you give one: lowercase letters, digits and single hyphens. `visibility` is `visible` (the default: every member sees it) or `secret` (only its people and the owners). A team under a `parent` inherits the parent's roles on repositories, and a mention or review request for the parent reaches it too; giving it a parent needs an owner, or a maintainer of the parent. Secret teams cannot be nested. People only, signed in or with a personal access token. Returns the team."
1419+ }
1420+ Op::UpdateTeam => {
1421+ "Change a team's `name`, `slug`, `description`, `visibility`, `parent` (an empty string takes it out from under its parent), `notify` or `review_assignment`. Only the fields given change; give at least one. A new slug changes how it is mentioned, @workspace/slug. Owners of the workspace and the team's maintainers. People only. Returns the team as it is now."
1422+ }
1423+ Op::DeleteTeam => {
1424+ "Delete a team. Its child teams move up to its parent, and the roles it gave on repositories go with it: its people keep only what they have otherwise. Owners of the workspace and the team's maintainers. People only. Returns true."
1425+ }
1426+ Op::ListTeamMembers => {
1427+ "The people in a team, each with their `username`, `name`, `avatar` and `role` in it (`member` or `maintainer`). With `include_child_teams`, the people of its child teams are listed too, each with `via`, the child team they are in. Anyone who can see the team."
1428+ }
1429+ Op::SetTeamMember => {
1430+ "Add a member of the workspace to a team, or change their role in it: `member` (the default) or `maintainer`, who manages the team's people and settings. Someone who is not a member of the workspace must join it first. Owners of the workspace and the team's maintainers. People only. Returns the person as list_team_members lists them."
1431+ }
1432+ Op::RemoveTeamMember => {
1433+ "Take someone out of a team. They lose the roles the team gave them on repositories, unless they have them otherwise. Owners of the workspace and the team's maintainers; anyone may leave a team themselves. People only. Returns true."
1434+ }
1435+ Op::ListChildTeams => {
1436+ "The teams nested directly under a team, as list_teams describes them. Anyone who can see the team."
1437+ }
1438+ Op::ListTeamRepos => {
1439+ "The repositories a team has a role on: each one's `repo` (`workspace/name`), the team's `role` there (read, triage, write, maintain or admin), and `inherited_from`, the parent team it comes from when the team inherits it, or null for its own. Everyone in the team gets the role; where someone has a higher one otherwise, the higher one counts. Anyone who can see the team."
1440+ }
1441+ Op::SetTeamRepo => {
1442+ "Give a team a role on a repository in its workspace, or change it: read, triage, write, maintain or admin. Everyone in the team and in its child teams gets the role. Needs the Admin role on the repository. People only. Returns the repository as list_team_repos lists it."
1443+ }
1444+ Op::RemoveTeamRepo => {
1445+ "Take a team's role on a repository away. Its people keep only the roles they have otherwise. Needs the Admin role on the repository, or to be an owner or one of the team's maintainers. People only. Returns true."
1446+ }
1447+ Op::SetTeamReviewAssignment => {
1448+ "Choose what happens when a team is asked to review a pull request. Off, everyone in it is asked. On (`enabled`), g1t picks `count` people from it (1 to 10, never the pull request's author) and asks them, and the team stays shown as asked beside them: `round_robin` picks whoever this team asked least recently, `load_balance` whoever has the fewest pull requests waiting on their review. `skip_busy` leaves out anyone with `busy_at` or more waiting; `include_child_teams` also picks from its child teams' people; `excluded` lists usernames never picked; `notify_team` also tells the rest of the team. Fields left out keep their current value. Owners of the workspace and the team's maintainers. People only. Returns the team."
1449+ }
1450+ Op::ListUserTeams => {
1451+ "The teams someone is in within a workspace, as list_teams describes them, leaving out secret teams you cannot see. Members of the workspace only."
1452+ }
1453+ Op::RequestReviewers => {
1454+ "Ask more people or teams to review a pull request. `reviewers` are usernames, and may include `g1t` to ask a g1t agent; `team_reviewers` are teams, as `workspace/team` or the team's slug in the repository's workspace. They are added to whoever is asked already. Asking a team asks everyone in it, or with its review assignment on, the people it picks. Nobody is asked to review their own pull request, and a team must be one you can see. Whoever opened the pull request, or anyone with the Triage role or higher, while it is open. Returns the pull request, with `reviewers` and `team_reviewers` as they are now."
1455+ }
1456+ Op::RemoveRequestedReviewers => {
1457+ "Stop asking people or teams to review a pull request: `reviewers` by username and `team_reviewers` as `workspace/team` or the team's slug. Reviews they already gave stay. The same people may do this as may ask. Returns the pull request, with `reviewers` and `team_reviewers` as they are now."
1458+ }
1459+ Op::GetCodeownersErrors => {
1460+ "Check a repository's CODEOWNERS file as a linter would. g1t reads it from one branch (`ref`, the default branch unless you say): the first of `.g1t/CODEOWNERS`, `.github/CODEOWNERS`, `CODEOWNERS`, `docs/CODEOWNERS` and `.gitlab/CODEOWNERS` that exists. Returns its `path` (null when there is none), the `ref` read, its `size`, how many `rules` it has, its `sections`, and `errors`: each with its `line` (0 for the file as a whole), `kind`, the `token` at fault and a `message` saying how to fix it. `kind` is `too_large`, `negation`, `character_range`, `bad_pattern`, `bad_owner`, `bad_section`, `unknown_user`, `unknown_team`, `unknown_email`, `no_write_access` or `team_no_access`. Needs the Read role; a public repository's is open to anyone."
1461+ }
1462+ Op::Security(op) => op.description(),
11201463 }
11211464 }
11221465
12491592 }),
12501593 &["repo", "to"],
12511594 ),
1252− Op::GetRepo | Op::ListLabels => repo_only(),
1595+ Op::GetRepo | Op::ListLabels | Op::AddDefaultLabels => repo_only(),
1596+ Op::CreateLabel => object(
1597+ json!({
1598+ "repo": repo_schema(),
1599+ "label": { "type": "string", "description": "Its name: lowercase, at most 50 characters, e.g. \"good first issue\"." },
1600+ "color": { "type": "string", "description": "Six hex digits, with or without #, e.g. \"d73a4a\". Chosen from the name when left out." },
1601+ "description": { "type": "string", "description": "What it means, at most 100 characters." },
1602+ }),
1603+ &["repo", "label"],
1604+ ),
1605+ Op::UpdateLabel => object(
1606+ json!({
1607+ "repo": repo_schema(),
1608+ "label": label_schema(),
1609+ "new_name": { "type": "string", "description": "Rename it, on everything that carries it." },
1610+ "color": { "type": "string", "description": "Six hex digits." },
1611+ "description": { "type": "string", "description": "An empty string clears it." },
1612+ }),
1613+ &["repo", "label"],
1614+ ),
1615+ Op::DeleteLabel => object(json!({ "repo": repo_schema(), "label": label_schema() }), &["repo", "label"]),
1616+ Op::ListIssueLabels => just_numbered(),
1617+ Op::AddIssueLabels | Op::SetIssueLabels => object(
1618+ numbered(json!({
1619+ "labels": {
1620+ "type": "array",
1621+ "items": { "type": "string" },
1622+ "description": "Label names, e.g. [\"bug\", \"help wanted\"]. Names the repository does not have yet are created for someone with the Triage role.",
1623+ },
1624+ })),
1625+ &["repo", "number", "labels"],
1626+ ),
1627+ Op::RemoveIssueLabels => object(
1628+ numbered(json!({
1629+ "label": label_schema(),
1630+ "labels": {
1631+ "type": "array",
1632+ "items": { "type": "string" },
1633+ "description": "Instead of label: several to take off. With neither, all of them.",
1634+ },
1635+ })),
1636+ &["repo", "number"],
1637+ ),
1638+ Op::ListMilestones => object(
1639+ json!({ "repo": repo_schema(), "state": states }),
1640+ &["repo"],
1641+ ),
1642+ Op::GetMilestone | Op::DeleteMilestone => {
1643+ object(json!({ "repo": repo_schema(), "milestone": milestone_schema() }), &["repo", "milestone"])
1644+ }
1645+ Op::CreateMilestone | Op::UpdateMilestone => {
1646+ let mut properties = json!({
1647+ "repo": repo_schema(),
1648+ "title": { "type": "string", "description": "Unique in the repository, at most 100 characters." },
1649+ "description": { "type": "string", "description": "Markdown." },
1650+ "due_on": { "type": "string", "description": "The day it is due, YYYY-MM-DD. On update, \"\" clears it." },
1651+ "state": states,
1652+ });
1653+ if self == Op::UpdateMilestone {
1654+ properties["milestone"] = milestone_schema();
1655+ object(properties, &["repo", "milestone"])
1656+ } else {
1657+ object(properties, &["repo", "title"])
1658+ }
1659+ }
12531660 Op::UpdateRepo => object(
12541661 json!({
12551662 "repo": repo_schema(),
14651872 "type": "boolean",
14661873 "description": "Ask a person before merging a g1t agent's change whose confidence is low: auto-merge and the merge queue leave it until a person approves it. On by default.",
14671874 },
1875+ "require_code_owner_review": {
1876+ "type": "boolean",
1877+ "description": "Refuse to merge until the code owners of every file a pull request changes, as the CODEOWNERS file of the branch it merges into names them, have approved it, as many as each section asks. Only people's approvals count, and g1t's only where the file names @g1t.",
1878+ },
14681879 }),
14691880 &["repo"],
14701881 ),
14891900 "repo": repo_schema(),
14901901 "state": states,
14911902 "label": { "type": "string", "description": "Only issues carrying this label." },
1903+ "milestone": { "type": "integer", "description": "Only issues in the milestone of this number." },
14921904 }),
14931905 &["repo"],
14941906 ),
15081920 "labels": {
15091921 "type": "array",
15101922 "items": { "type": "string" },
1511− "description": "What kind of issue this is, e.g. \"bug\" or \"feature\". list_labels shows the labels in use; a new name creates a new label.",
1923+ "description": "What kind of issue this is, e.g. \"bug\" or \"enhancement\": the repository's labels, as list_labels gives them. A name it does not have yet is created for someone with the Triage role.",
15121924 },
15131925 "checks": {
15141926 "type": "array",
15161928 "deprecated": true,
15171929 "description": "Deprecated. Commands are added to the body under \"Definition of done\", and the response says so in deprecation. What must pass before a pull request merges is the default branch's required checks.",
15181930 },
1931+ "milestone": { "type": "integer", "description": "The number of the milestone to put it in. Needs the Triage role." },
15191932 }),
15201933 &["repo", "title"],
15211934 ),
15231936 numbered(json!({
15241937 "title": { "type": "string" },
15251938 "body": { "type": "string" },
1526− "labels": { "type": "array", "items": { "type": "string" } },
1939+ "labels": {
1940+ "type": "array",
1941+ "items": { "type": "string" },
1942+ "description": "Replaces the whole set. Names the repository does not have yet are created for someone with the Triage role.",
1943+ },
1944+ "milestone": {
1945+ "type": ["integer", "null"],
1946+ "description": "The number of the milestone to put it in; null or 0 takes it out. Needs the Triage role.",
1947+ },
15271948 "assignees": {
15281949 "type": "array",
15291950 "items": { "type": "string" },
16302051 })),
16312052 &["repo", "number", "verdict"],
16322053 ),
1633− Op::ListPullRequests => {
1634− object(json!({ "repo": repo_schema(), "state": states }), &["repo"])
1635− }
2054+ Op::ListPullRequests => object(
2055+ json!({
2056+ "repo": repo_schema(),
2057+ "state": states,
2058+ "label": { "type": "string", "description": "Only pull requests carrying this label." },
2059+ "milestone": { "type": "integer", "description": "Only pull requests in the milestone of this number." },
2060+ "base": { "type": "string", "description": "Only pull requests into this branch." },
2061+ }),
2062+ &["repo"],
2063+ ),
2064+ Op::UpdatePullRequest => object(
2065+ numbered(json!({
2066+ "base": {
2067+ "type": "string",
2068+ "description": "The branch it merges into: an existing branch other than its own. Needs the Write role.",
2069+ },
2070+ "labels": {
2071+ "type": "array",
2072+ "items": { "type": "string" },
2073+ "description": "Replaces the whole set.",
2074+ },
2075+ "milestone": {
2076+ "type": ["integer", "null"],
2077+ "description": "The number of the milestone to put it in; null or 0 takes it out. Needs the Triage role.",
2078+ },
2079+ "assignees": {
2080+ "type": "array",
2081+ "items": { "type": "string" },
2082+ "description": "Usernames; replaces the whole set.",
2083+ },
2084+ "reviewers": {
2085+ "type": "array",
2086+ "items": { "type": "string" },
2087+ "description": "Usernames whose review is asked for, and g1t for a g1t agent's; replaces the whole set.",
2088+ },
2089+ })),
2090+ &["repo", "number"],
2091+ ),
16362092 Op::CreatePullRequest => object(
16372093 json!({
16382094 "repo": repo_schema(),
16532109 "type": "string",
16542110 "description": "A label for the agent doing the work, e.g. \"claude-code\".",
16552111 },
2112+ "base": {
2113+ "type": "string",
2114+ "description": "The branch it merges into: the default branch when left out. Name another existing branch only when asked to.",
2115+ },
16562116 }),
16572117 &["repo"],
16582118 ),
21342594 }),
21352595 &["workspace", "projects"],
21362596 ),
2597+ Op::ListTeams => object(
2598+ json!({
2599+ "workspace": workspace_schema(),
2600+ "query": { "type": "string", "description": "Only teams whose name or slug has these letters." },
2601+ }),
2602+ &["workspace"],
2603+ ),
2604+ Op::GetTeam | Op::DeleteTeam | Op::ListChildTeams | Op::ListTeamRepos => {
2605+ object(team_target(json!({})), &["workspace", "team"])
2606+ }
2607+ Op::CreateTeam => object(
2608+ json!({
2609+ "workspace": workspace_schema(),
2610+ "name": { "type": "string", "description": "Its display name, at most 80 characters." },
2611+ "slug": {
2612+ "type": "string",
2613+ "description": "Its name in mentions and URLs: lowercase letters, digits and single hyphens. Made from the name if left out.",
2614+ },
2615+ "description": { "type": "string", "description": "What it is for, at most 280 characters." },
2616+ "visibility": team_visibility_schema(),
2617+ "parent": { "type": "string", "description": "The slug of the team to nest it under." },
2618+ "notify": {
2619+ "type": "boolean",
2620+ "description": "Whether its people are notified when it is mentioned. On unless you say.",
2621+ },
2622+ "members": {
2623+ "type": "array",
2624+ "items": { "type": "string" },
2625+ "description": "Usernames of members of the workspace to add, besides you.",
2626+ },
2627+ }),
2628+ &["workspace", "name"],
2629+ ),
2630+ Op::UpdateTeam => object(
2631+ team_target(json!({
2632+ "name": { "type": "string", "description": "A new display name." },
2633+ "slug": { "type": "string", "description": "A new slug, which changes its mention." },
2634+ "description": { "type": "string", "description": "A new description; an empty string clears it." },
2635+ "visibility": team_visibility_schema(),
2636+ "parent": {
2637+ "type": "string",
2638+ "description": "The slug of the team to nest it under; an empty string for none.",
2639+ },
2640+ "notify": { "type": "boolean", "description": "Whether its people are notified when it is mentioned." },
2641+ "review_assignment": {
2642+ "type": "object",
2643+ "properties": review_assignment_properties(),
2644+ "description": "What happens when it is asked to review; fields left out keep their value. See set_team_review_assignment.",
2645+ },
2646+ })),
2647+ &["workspace", "team"],
2648+ ),
2649+ Op::ListTeamMembers => object(
2650+ team_target(json!({ "include_child_teams": include_child_teams_schema() })),
2651+ &["workspace", "team"],
2652+ ),
2653+ Op::SetTeamMember => object(
2654+ team_target(json!({ "username": username_schema(), "role": team_role_schema() })),
2655+ &["workspace", "team", "username"],
2656+ ),
2657+ Op::RemoveTeamMember => object(
2658+ team_target(json!({ "username": username_schema() })),
2659+ &["workspace", "team", "username"],
2660+ ),
2661+ Op::SetTeamRepo | Op::RemoveTeamRepo => {
2662+ let mut properties = team_target(json!({
2663+ "repo": {
2664+ "type": "string",
2665+ "description": "The repository, in the team's workspace: its name, or \"owner/name\".",
2666+ },
2667+ }));
2668+ let mut required = vec!["workspace", "team", "repo"];
2669+ if self == Op::SetTeamRepo {
2670+ properties["role"] = role_schema();
2671+ required.push("role");
2672+ }
2673+ object(properties, &required)
2674+ }
2675+ Op::SetTeamReviewAssignment => object(team_target(review_assignment_properties()), &["workspace", "team"]),
2676+ Op::ListUserTeams => object(
2677+ json!({ "workspace": workspace_schema(), "username": username_schema() }),
2678+ &["workspace", "username"],
2679+ ),
2680+ Op::RequestReviewers | Op::RemoveRequestedReviewers => {
2681+ object(requested_reviewers_properties(), &["repo", "number"])
2682+ }
2683+ Op::GetCodeownersErrors => object(
2684+ json!({
2685+ "repo": repo_schema(),
2686+ "ref": {
2687+ "type": "string",
2688+ "description": "The branch, tag or commit to read the file from. The default branch if left out.",
2689+ },
2690+ }),
2691+ &["repo"],
2692+ ),
2693+ Op::Security(op) => op.input(),
21372694 }
21382695 }
21392696
21472704 | Op::ListIssues
21482705 | Op::GetIssue
21492706 | Op::ListLabels
2707+ | Op::ListIssueLabels
2708+ | Op::ListMilestones
2709+ | Op::GetMilestone
21502710 | Op::ListPullRequests
21512711 | Op::GetPullRequest
21522712 | Op::ReadSession
21552715 | Op::GetRepoSettings
21562716 | Op::ListCheckNames
21572717 | Op::GetMergeQueue
2718+ | Op::GetCodeownersErrors
21582719 )
21592720 }
21602721
21652726
21662727 /// Whether the operation is about one repository, named by `repo`.
21672728 pub(crate) fn needs_repo(self) -> bool {
2729+ if let Op::Security(op) = self {
2730+ return op.needs_repo();
2731+ }
21682732 !matches!(
21692733 self,
21702734 Op::Whoami
22352799 | Op::PinProject
22362800 | Op::UnpinProject
22372801 | Op::ReorderPinnedProjects
2802+ | Op::ListTeams
2803+ | Op::GetTeam
2804+ | Op::CreateTeam
2805+ | Op::UpdateTeam
2806+ | Op::DeleteTeam
2807+ | Op::ListTeamMembers
2808+ | Op::SetTeamMember
2809+ | Op::RemoveTeamMember
2810+ | Op::ListChildTeams
2811+ | Op::ListTeamRepos
2812+ | Op::SetTeamRepo
2813+ | Op::RemoveTeamRepo
2814+ | Op::SetTeamReviewAssignment
2815+ | Op::ListUserTeams
22382816 )
22392817 }
22402818
29023480 max_revisions: integer(input, "max_revisions").unwrap_or(current.max_revisions),
29033481 merge_queue: flag("merge_queue", current.merge_queue),
29043482 hold_low_confidence: flag("hold_low_confidence", current.hold_low_confidence),
3483+ require_code_owner_review: flag(
3484+ "require_code_owner_review",
3485+ current.require_code_owner_review,
3486+ ),
29053487 ..current
29063488 };
29073489 pass(
29483530 viewer: viewer.clone(),
29493531 state: state(input),
29503532 label: optional_text(input, "label"),
3533+ milestone: integer(input, "milestone"),
29513534 },
29523535 )
29533536 .await
29653548 body: text(input, "body"),
29663549 labels: strings(input, "labels").unwrap_or_default(),
29673550 checks: checks.clone(),
3551+ milestone: integer(input, "milestone"),
29683552 },
29693553 )
29703554 .await?;
29823566 body: input["body"].as_str().map(str::to_owned),
29833567 labels: strings(input, "labels"),
29843568 assignees: strings(input, "assignees"),
3569+ milestone: milestone_input(input),
29853570 },
29863571 )
29873572 .await
30733658 .await
30743659 }
30753660 Op::ListLabels => pass(work, "list_labels", &view()).await,
3661+ Op::CreateLabel | Op::UpdateLabel => {
3662+ let creating = self == Op::CreateLabel;
3663+ pass(
3664+ work,
3665+ "save_label",
3666+ &SaveLabelArgs {
3667+ actor: actor(),
3668+ repo,
3669+ name: (!creating).then(|| text(input, "label")),
3670+ new_name: if creating { Some(text(input, "label")) } else { optional_text(input, "new_name") },
3671+ color: optional_text(input, "color"),
3672+ description: input["description"].as_str().map(str::to_owned),
3673+ },
3674+ )
3675+ .await
3676+ }
3677+ Op::DeleteLabel => {
3678+ pass(work, "delete_label", &DeleteLabelArgs { actor: actor(), repo, name: text(input, "label") }).await
3679+ }
3680+ Op::AddDefaultLabels => pass(work, "add_default_labels", &RepoActorArgs { actor: actor(), repo }).await,
3681+ Op::ListIssueLabels => {
3682+ // The item's names, with each label's color and description.
3683+ let labels = call::<_, Vec<Label>>(work, "list_labels", &view()).await?;
3684+ let item = call::<_, IssueDetail>(work, "get_issue", &view()).await?;
3685+ let names = match item {
3686+ Outcome::Ok(detail) => detail.issue.labels,
3687+ Outcome::Fail(_) => match call::<_, PullDetail>(work, "get_pull", &view()).await? {
3688+ Outcome::Ok(detail) => detail.pull.labels,
3689+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
3690+ },
3691+ };
3692+ let labels = match labels {
3693+ Outcome::Ok(labels) => labels,
3694+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
3695+ };
3696+ ok(&names
3697+ .iter()
3698+ .filter_map(|name| labels.iter().find(|label| label.name == *name))
3699+ .collect::<Vec<_>>())
3700+ }
3701+ Op::AddIssueLabels | Op::SetIssueLabels | Op::RemoveIssueLabels => {
3702+ let (change, labels) = match self {
3703+ Op::AddIssueLabels => (LabelChange::Add, strings(input, "labels").unwrap_or_default()),
3704+ Op::SetIssueLabels => (LabelChange::Set, strings(input, "labels").unwrap_or_default()),
3705+ // One, several, or with neither, all of them.
3706+ _ => match (optional_text(input, "label"), strings(input, "labels")) {
3707+ (Some(one), _) => (LabelChange::Remove, vec![one]),
3708+ (None, Some(several)) => (LabelChange::Remove, several),
3709+ (None, None) => (LabelChange::Set, Vec::new()),
3710+ },
3711+ };
3712+ pass(work, "set_labels", &SetLabelsArgs { actor: actor(), repo, number, labels, change }).await
3713+ }
3714+ Op::ListMilestones => {
3715+ pass(work, "list_milestones", &ListMilestonesArgs { repo, viewer: viewer.clone(), state: state(input) }).await
3716+ }
3717+ Op::GetMilestone => {
3718+ let asked = ViewArgs { number: integer(input, "milestone").unwrap_or_default(), ..view() };
3719+ pass(work, "get_milestone", &asked).await
3720+ }
3721+ Op::CreateMilestone | Op::UpdateMilestone => {
3722+ pass(
3723+ work,
3724+ "save_milestone",
3725+ &SaveMilestoneArgs {
3726+ actor: actor(),
3727+ repo,
3728+ number: (self == Op::UpdateMilestone).then(|| integer(input, "milestone").unwrap_or_default()),
3729+ title: input["title"].as_str().map(str::to_owned),
3730+ description: input["description"].as_str().map(str::to_owned),
3731+ due_on: input["due_on"].as_str().map(str::to_owned),
3732+ state: state(input),
3733+ },
3734+ )
3735+ .await
3736+ }
3737+ Op::DeleteMilestone => {
3738+ pass(
3739+ work,
3740+ "delete_milestone",
3741+ &DeleteMilestoneArgs { actor: actor(), repo, number: integer(input, "milestone").unwrap_or_default() },
3742+ )
3743+ .await
3744+ }
3745+ Op::UpdatePullRequest => {
3746+ pass(
3747+ work,
3748+ "update_pull",
3749+ &UpdatePullArgs {
3750+ actor: actor(),
3751+ repo,
3752+ number,
3753+ assignees: strings(input, "assignees"),
3754+ reviewers: strings(input, "reviewers"),
3755+ labels: strings(input, "labels"),
3756+ milestone: milestone_input(input),
3757+ base: optional_text(input, "base"),
3758+ },
3759+ )
3760+ .await
3761+ }
30763762 Op::AddComment | Op::ReviewPullRequest => {
30773763 let verdict = match (self, input["verdict"].as_str()) {
30783764 (Op::AddComment, _) => None,
31083794 repo,
31093795 viewer: viewer.clone(),
31103796 state: state(input),
3797+ label: optional_text(input, "label"),
3798+ milestone: integer(input, "milestone"),
3799+ base: optional_text(input, "base"),
31113800 },
31123801 )
31133802 .await
31273816 branch: optional_text(input, "branch"),
31283817 agent: optional_text(input, "agent").unwrap_or_else(|| "agent".into()),
31293818 runtime: Runtime::External,
3819+ base: optional_text(input, "base"),
31303820 },
31313821 )
31323822 .await?;
36654355 .await?;
36664356 changed_alert(changed)
36674357 }
4358+ // Teams: identity decides who may see and change each, and
4359+ // refuses every token but a person's for changes. See
4360+ // g1t_contracts::teams.
4361+ Op::ListTeams => {
4362+ pass(
4363+ identity,
4364+ "list_teams",
4365+ &ListTeamsArgs { viewer: viewer.clone(), workspace: workspace(), query: optional_text(input, "query") },
4366+ )
4367+ .await
4368+ }
4369+ Op::GetTeam | Op::ListChildTeams | Op::ListTeamRepos | Op::ListTeamMembers => {
4370+ let method = match self {
4371+ Op::GetTeam => "get_team",
4372+ Op::ListChildTeams => "child_teams",
4373+ Op::ListTeamRepos => "team_repos",
4374+ _ => "team_members",
4375+ };
4376+ pass(
4377+ identity,
4378+ method,
4379+ &TeamArgs {
4380+ viewer: viewer.clone(),
4381+ workspace: workspace(),
4382+ team: team_slug(input),
4383+ include_child_teams: self == Op::ListTeamMembers && yes(input, "include_child_teams") == Some(true),
4384+ },
4385+ )
4386+ .await
4387+ }
4388+ Op::CreateTeam => {
4389+ let visibility = match team_visibility(input) {
4390+ Ok(visibility) => visibility,
4391+ Err(message) => return failed(FailureCode::Invalid, &message),
4392+ };
4393+ pass(
4394+ identity,
4395+ "create_team",
4396+ &CreateTeamArgs {
4397+ actor: actor(),
4398+ workspace: workspace(),
4399+ name: text(input, "name").trim().to_owned(),
4400+ slug: optional_text(input, "slug"),
4401+ description: optional_text(input, "description"),
4402+ visibility,
4403+ parent: optional_text(input, "parent"),
4404+ notify: yes(input, "notify"),
4405+ members: strings(input, "members").unwrap_or_default(),
4406+ surface: Some(services.audit.surface),
4407+ },
4408+ )
4409+ .await
4410+ }
4411+ Op::UpdateTeam | Op::SetTeamReviewAssignment => {
4412+ let visibility = match team_visibility(input) {
4413+ Ok(visibility) if self == Op::UpdateTeam => visibility,
4414+ Ok(_) => None,
4415+ Err(message) => return failed(FailureCode::Invalid, &message),
4416+ };
4417+ // The review assignment's fields: in `review_assignment` to
4418+ // update a team, or at the top level to set it.
4419+ let given = match self {
4420+ Op::UpdateTeam => input.get("review_assignment").filter(|value| !value.is_null()),
4421+ _ => Some(input),
4422+ };
4423+ if given.is_some_and(|given| !given.is_object()) {
4424+ return failed(FailureCode::Invalid, "review_assignment is an object, such as {\"enabled\": true, \"count\": 2}.");
4425+ }
4426+ let review = match given {
4427+ None => None,
4428+ Some(given) => {
4429+ if !REVIEW_ASSIGNMENT_FIELDS.iter().any(|key| given.get(*key).is_some_and(|value| !value.is_null())) {
4430+ return failed(
4431+ FailureCode::Invalid,
4432+ &format!("Give the review assignment to change: {}.", REVIEW_ASSIGNMENT_FIELDS.join(", ")),
4433+ );
4434+ }
4435+ // What is not given stays as it is.
4436+ let current: Outcome<Team> = call(
4437+ identity,
4438+ "get_team",
4439+ &TeamArgs { viewer: viewer.clone(), workspace: workspace(), team: team_slug(input), include_child_teams: false },
4440+ )
4441+ .await?;
4442+ let current = match current {
4443+ Outcome::Ok(team) => team.review_assignment,
4444+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
4445+ };
4446+ match review_assignment(given, current) {
4447+ Ok(review) => Some(review),
4448+ Err(message) => return failed(FailureCode::Invalid, &message),
4449+ }
4450+ }
4451+ };
4452+ let words = |key: &str| match self {
4453+ Op::UpdateTeam => input[key].as_str().map(str::to_owned),
4454+ _ => None,
4455+ };
4456+ let args = UpdateTeamArgs {
4457+ actor: actor(),
4458+ workspace: workspace(),
4459+ team: team_slug(input),
4460+ name: words("name"),
4461+ slug: words("slug"),
4462+ description: words("description"),
4463+ visibility,
4464+ parent: words("parent"),
4465+ notify: if self == Op::UpdateTeam { yes(input, "notify") } else { None },
4466+ review_assignment: review,
4467+ surface: Some(services.audit.surface),
4468+ };
4469+ if args.name.is_none()
4470+ && args.slug.is_none()
4471+ && args.description.is_none()
4472+ && args.visibility.is_none()
4473+ && args.parent.is_none()
4474+ && args.notify.is_none()
4475+ && args.review_assignment.is_none()
4476+ {
4477+ return failed(
4478+ FailureCode::Invalid,
4479+ "Give name, slug, description, visibility, parent, notify or review_assignment to change.",
4480+ );
4481+ }
4482+ pass(identity, "update_team", &args).await
4483+ }
4484+ Op::DeleteTeam => {
4485+ pass(
4486+ identity,
4487+ "delete_team",
4488+ &DeleteTeamArgs {
4489+ actor: actor(),
4490+ workspace: workspace(),
4491+ team: team_slug(input),
4492+ surface: Some(services.audit.surface),
4493+ },
4494+ )
4495+ .await
4496+ }
4497+ Op::SetTeamMember => {
4498+ let role = match team_role(input) {
4499+ Ok(role) => role,
4500+ Err(message) => return failed(FailureCode::Invalid, &message),
4501+ };
4502+ pass(
4503+ identity,
4504+ "set_team_member",
4505+ &SetTeamMemberArgs {
4506+ actor: actor(),
4507+ workspace: workspace(),
4508+ team: team_slug(input),
4509+ username: text(input, "username").trim().trim_start_matches('@').to_owned(),
4510+ role,
4511+ surface: Some(services.audit.surface),
4512+ },
4513+ )
4514+ .await
4515+ }
4516+ Op::RemoveTeamMember => {
4517+ pass(
4518+ identity,
4519+ "remove_team_member",
4520+ &RemoveTeamMemberArgs {
4521+ actor: actor(),
4522+ workspace: workspace(),
4523+ team: team_slug(input),
4524+ username: text(input, "username").trim().trim_start_matches('@').to_owned(),
4525+ surface: Some(services.audit.surface),
4526+ },
4527+ )
4528+ .await
4529+ }
4530+ Op::SetTeamRepo | Op::RemoveTeamRepo => {
4531+ let Some(path) = team_repo(input, &workspace()) else {
4532+ return failed(
4533+ FailureCode::Invalid,
4534+ "Give the repository: its name in the team's workspace, or \"owner/name\".",
4535+ );
4536+ };
4537+ if self == Op::RemoveTeamRepo {
4538+ return pass(
4539+ identity,
4540+ "remove_team_repo",
4541+ &RemoveTeamRepoArgs {
4542+ actor: actor(),
4543+ workspace: workspace(),
4544+ team: team_slug(input),
4545+ repo: path,
4546+ surface: Some(services.audit.surface),
4547+ },
4548+ )
4549+ .await;
4550+ }
4551+ let Some(role) = repo_role(input) else {
4552+ return failed(FailureCode::Invalid, ROLE_NEEDED);
4553+ };
4554+ pass(
4555+ identity,
4556+ "set_team_repo",
4557+ &SetTeamRepoArgs {
4558+ actor: actor(),
4559+ workspace: workspace(),
4560+ team: team_slug(input),
4561+ repo: path,
4562+ role,
4563+ surface: Some(services.audit.surface),
4564+ },
4565+ )
4566+ .await
4567+ }
4568+ Op::ListUserTeams => {
4569+ pass(
4570+ identity,
4571+ "user_teams",
4572+ &UserTeamsArgs {
4573+ viewer: viewer.clone(),
4574+ workspace: workspace(),
4575+ username: text(input, "username").trim().trim_start_matches('@').to_owned(),
4576+ },
4577+ )
4578+ .await
4579+ }
4580+ // Who is asked to review: the whole list, people and teams,
4581+ // replaces who is asked, so read it and change it.
4582+ Op::RequestReviewers | Op::RemoveRequestedReviewers => {
4583+ let (people, teams) = reviewer_names(input, &repo.namespace);
4584+ if people.is_empty() && teams.is_empty() {
4585+ return failed(
4586+ FailureCode::Invalid,
4587+ "Give reviewers (usernames) or team_reviewers (\"workspace/team\").",
4588+ );
4589+ }
4590+ let found: Outcome<PullDetail> = call(work, "get_pull", &view()).await?;
4591+ let pull = match found {
4592+ Outcome::Ok(detail) => detail.pull,
4593+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
4594+ };
4595+ let reviewers = reviewers_after(
4596+ &pull.reviewers,
4597+ &pull.team_reviewers,
4598+ &people,
4599+ &teams,
4600+ self == Op::RequestReviewers,
4601+ );
4602+ pass(
4603+ work,
4604+ "update_pull",
4605+ &UpdatePullArgs { actor: actor(), repo: repo.clone(), number, assignees: None, reviewers: Some(reviewers), labels: None, milestone: None, base: None },
4606+ )
4607+ .await
4608+ }
4609+ Op::GetCodeownersErrors => {
4610+ pass(
4611+ work,
4612+ "codeowners_errors",
4613+ &CodeOwnersErrorsArgs { viewer: viewer.clone(), repo, git_ref: optional_text(input, "ref") },
4614+ )
4615+ .await
4616+ }
36684617 // A person's own inbox: the events service keeps it.
36694618 Op::ListNotifications
36704619 | Op::MarkNotificationsRead
36844633 Op::ListPinnedProjects | Op::PinProject | Op::UnpinProject | Op::ReorderPinnedProjects => {
36854634 crate::pins::run(self, services, viewer, input).await
36864635 }
4636+ // The security suite: the security service decides, this gives
4637+ // each answer its public shape.
4638+ Op::Security(op) => crate::security::run(op, services, viewer, input).await,
36874639 Op::ReopenSecurityAlert => {
36884640 let changed: Outcome<AlertChange> = call(
36894641 &services.security,
37574709 input["role"].as_str().and_then(RepoRole::parse)
37584710 }
37594711
4712+/// A yes or no, given as a boolean or, in a URL, as text.
4713+fn yes(input: &Value, key: &str) -> Option<bool> {
4714+ match &input[key] {
4715+ Value::Bool(value) => Some(*value),
4716+ Value::String(text) => match text.trim().to_ascii_lowercase().as_str() {
4717+ "true" | "1" | "yes" => Some(true),
4718+ "false" | "0" | "no" => Some(false),
4719+ _ => None,
4720+ },
4721+ _ => None,
4722+ }
4723+}
4724+
4725+/// The team named by `team`, by its slug.
4726+fn team_slug(input: &Value) -> String {
4727+ text(input, "team").trim().trim_start_matches('@').to_lowercase()
4728+}
4729+
4730+/// `visibility`, when it is given.
4731+fn team_visibility(input: &Value) -> std::result::Result<Option<TeamVisibility>, String> {
4732+ match input.get("visibility").filter(|value| !value.is_null()) {
4733+ None => Ok(None),
4734+ Some(value) => value
4735+ .as_str()
4736+ .and_then(TeamVisibility::parse)
4737+ .map(Some)
4738+ .ok_or_else(|| "visibility is visible or secret.".to_owned()),
4739+ }
4740+}
4741+
4742+/// A person's `role` in a team: member when it is left out.
4743+fn team_role(input: &Value) -> std::result::Result<TeamRole, String> {
4744+ match input.get("role").filter(|value| !value.is_null()) {
4745+ None => Ok(TeamRole::Member),
4746+ Some(value) => value
4747+ .as_str()
4748+ .and_then(TeamRole::parse)
4749+ .ok_or_else(|| "role is member or maintainer.".to_owned()),
4750+ }
4751+}
4752+
4753+/// The fields of a team's review assignment, as inputs name them.
4754+const REVIEW_ASSIGNMENT_FIELDS: [&str; 8] =
4755+ ["enabled", "algorithm", "count", "skip_busy", "busy_at", "include_child_teams", "excluded", "notify_team"];
4756+
4757+/// `current` with the fields `given` has changed, each checked.
4758+fn review_assignment(given: &Value, current: ReviewAssignment) -> std::result::Result<ReviewAssignment, String> {
4759+ let mut next = current;
4760+ let present = |key: &str| given.get(key).is_some_and(|value| !value.is_null());
4761+ let boolean = |key: &str, now: bool| -> std::result::Result<bool, String> {
4762+ if !present(key) {
4763+ return Ok(now);
4764+ }
4765+ yes(given, key).ok_or_else(|| format!("{key} is true or false."))
4766+ };
4767+ let within = |key: &str, now: u32, most: u32| -> std::result::Result<u32, String> {
4768+ if !present(key) {
4769+ return Ok(now);
4770+ }
4771+ integer(given, key)
4772+ .filter(|n| (1..=most).contains(n))
4773+ .ok_or_else(|| format!("{key} is a whole number from 1 to {most}."))
4774+ };
4775+ next.enabled = boolean("enabled", next.enabled)?;
4776+ if present("algorithm") {
4777+ next.algorithm = given["algorithm"]
4778+ .as_str()
4779+ .and_then(ReviewAlgorithm::parse)
4780+ .ok_or_else(|| "algorithm is round_robin or load_balance.".to_owned())?;
4781+ }
4782+ next.count = within("count", next.count, g1t_contracts::teams::MAX_ASSIGNED)?;
4783+ next.skip_busy = boolean("skip_busy", next.skip_busy)?;
4784+ next.busy_at = within("busy_at", next.busy_at, 100)?;
4785+ next.include_child_teams = boolean("include_child_teams", next.include_child_teams)?;
4786+ if present("excluded") {
4787+ next.excluded = strings(given, "excluded").ok_or_else(|| "excluded is a list of usernames.".to_owned())?;
4788+ }
4789+ next.notify_team = boolean("notify_team", next.notify_team)?;
4790+ Ok(next)
4791+}
4792+
4793+/// The repository `repo` names for a team of `workspace`: `owner/name`, or
4794+/// a name in the workspace.
4795+fn team_repo(input: &Value, workspace: &str) -> Option<RepoPath> {
4796+ repo_path(input).or_else(|| {
4797+ let name = input["repo"].as_str()?.trim();
4798+ (!name.is_empty() && !name.contains('/')).then(|| RepoPath {
4799+ namespace: workspace.to_owned(),
4800+ name: name.to_owned(),
4801+ })
4802+ })
4803+}
4804+
4805+/// The people (`reviewers`) and teams (`team_reviewers`) a call names, each
4806+/// once, lowercase; a team as `workspace/team`, a bare slug being one of
4807+/// `workspace`'s. A name in `reviewers` with a `/` is a team too.
4808+fn reviewer_names(input: &Value, workspace: &str) -> (Vec<String>, Vec<String>) {
4809+ let (mut people, mut teams): (Vec<String>, Vec<String>) = (Vec::new(), Vec::new());
4810+ let clean = |name: &str| name.trim().trim_start_matches('@').to_lowercase();
4811+ for name in strings(input, "reviewers").unwrap_or_default() {
4812+ let name = clean(&name);
4813+ let list = if name.contains('/') { &mut teams } else { &mut people };
4814+ if !name.is_empty() && !list.contains(&name) {
4815+ list.push(name);
4816+ }
4817+ }
4818+ for name in strings(input, "team_reviewers").unwrap_or_default() {
4819+ let name = clean(&name);
4820+ if name.is_empty() {
4821+ continue;
4822+ }
4823+ let name = if name.contains('/') { name } else { format!("{}/{name}", workspace.to_lowercase()) };
4824+ if !teams.contains(&name) {
4825+ teams.push(name);
4826+ }
4827+ }
4828+ (people, teams)
4829+}
4830+
4831+/// Who is asked to review once `people` and `teams` are added (or, with
4832+/// `add` false, taken away), as update_pull takes it: people, then teams.
4833+fn reviewers_after(
4834+ current_people: &[String],
4835+ current_teams: &[String],
4836+ people: &[String],
4837+ teams: &[String],
4838+ add: bool,
4839+) -> Vec<String> {
4840+ let has = |list: &[String], name: &str| list.iter().any(|item| item.eq_ignore_ascii_case(name));
4841+ let mut out = Vec::new();
4842+ for (current, change) in [(current_people, people), (current_teams, teams)] {
4843+ let mut kept: Vec<String> = current.iter().filter(|name| add || !has(change, name)).cloned().collect();
4844+ if add {
4845+ for name in change {
4846+ if !has(&kept, name) {
4847+ kept.push(name.clone());
4848+ }
4849+ }
4850+ }
4851+ out.extend(kept);
4852+ }
4853+ out
4854+}
4855+
37604856 impl Op {
37614857 /// The properties of the operation's input schema.
37624858 pub fn properties(self) -> Map<String, Value> {
39205016 }
39215017 assert!(!NEVER.contains(&Op::ListSecurityAlerts.name()));
39225018 }
5019+
5020+ const TEAMS: [Op; 14] = [
5021+ Op::ListTeams,
5022+ Op::GetTeam,
5023+ Op::CreateTeam,
5024+ Op::UpdateTeam,
5025+ Op::DeleteTeam,
5026+ Op::ListTeamMembers,
5027+ Op::SetTeamMember,
5028+ Op::RemoveTeamMember,
5029+ Op::ListChildTeams,
5030+ Op::ListTeamRepos,
5031+ Op::SetTeamRepo,
5032+ Op::RemoveTeamRepo,
5033+ Op::SetTeamReviewAssignment,
5034+ Op::ListUserTeams,
5035+ ];
5036+
5037+ /// A team belongs to a workspace: its operations name the workspace,
5038+ /// never need a repository, and need someone signed in.
5039+ #[test]
5040+ fn team_operations_name_a_workspace() {
5041+ for op in TEAMS {
5042+ assert!(!op.needs_repo(), "{}", op.name());
5043+ assert!(op.needs_user(), "{}", op.name());
5044+ assert!(op.required().contains(&"workspace".to_owned()), "{}", op.name());
5045+ }
5046+ for op in [Op::RequestReviewers, Op::RemoveRequestedReviewers, Op::GetCodeownersErrors] {
5047+ assert!(op.needs_repo(), "{}", op.name());
5048+ }
5049+ // A public repository's CODEOWNERS file is anyone's to check.
5050+ assert!(!Op::GetCodeownersErrors.needs_user());
5051+ }
5052+
5053+ #[test]
5054+ fn team_words_are_checked() {
5055+ assert_eq!(team_visibility(&json!({})), Ok(None));
5056+ assert_eq!(team_visibility(&json!({ "visibility": "Secret" })), Ok(Some(TeamVisibility::Secret)));
5057+ assert!(team_visibility(&json!({ "visibility": "hidden" })).is_err());
5058+ assert_eq!(team_role(&json!({})), Ok(TeamRole::Member));
5059+ assert_eq!(team_role(&json!({ "role": "maintainer" })), Ok(TeamRole::Maintainer));
5060+ assert!(team_role(&json!({ "role": "admin" })).is_err());
5061+ assert_eq!(Op::SetTeamMember.input()["properties"]["role"]["enum"], json!(["member", "maintainer"]));
5062+ assert_eq!(Op::CreateTeam.input()["properties"]["visibility"]["enum"], json!(["visible", "secret"]));
5063+ assert_eq!(
5064+ Op::SetTeamRepo.input()["properties"]["role"]["enum"],
5065+ json!(["read", "triage", "write", "maintain", "admin"])
5066+ );
5067+ assert_eq!(
5068+ Op::SetTeamReviewAssignment.input()["properties"]["algorithm"]["enum"],
5069+ json!(["round_robin", "load_balance"])
5070+ );
5071+ assert_eq!(yes(&json!({ "a": "true" }), "a"), Some(true));
5072+ assert_eq!(yes(&json!({ "a": false }), "a"), Some(false));
5073+ assert_eq!(yes(&json!({ "a": "maybe" }), "a"), None);
5074+ assert_eq!(team_slug(&json!({ "team": " @Backend " })), "backend");
5075+ }
5076+
5077+ /// Fields left out keep their value; a bad one is refused before
5078+ /// identity is asked.
5079+ #[test]
5080+ fn review_assignment_changes_only_what_is_given() {
5081+ let current = ReviewAssignment { count: 2, excluded: vec!["bo".into()], ..ReviewAssignment::default() };
5082+ let next = review_assignment(&json!({ "enabled": true, "algorithm": "load_balance" }), current.clone()).unwrap();
5083+ assert!(next.enabled);
5084+ assert_eq!(next.algorithm, ReviewAlgorithm::LoadBalance);
5085+ assert_eq!((next.count, next.excluded.clone()), (2, vec!["bo".to_owned()]));
5086+ let next = review_assignment(&json!({ "count": "3", "excluded": [], "skip_busy": "true", "busy_at": 4 }), current.clone()).unwrap();
5087+ assert_eq!((next.count, next.busy_at, next.skip_busy), (3, 4, true));
5088+ assert!(next.excluded.is_empty());
5089+ for bad in [
5090+ json!({ "algorithm": "random" }),
5091+ json!({ "count": 0 }),
5092+ json!({ "count": 11 }),
5093+ json!({ "busy_at": 101 }),
5094+ json!({ "enabled": "sometimes" }),
5095+ json!({ "excluded": "ana" }),
5096+ ] {
5097+ assert!(review_assignment(&bad, current.clone()).is_err(), "{bad}");
5098+ }
5099+ }
5100+
5101+ #[test]
5102+ fn a_team_names_a_repository_by_itself_or_in_full() {
5103+ let path = team_repo(&json!({ "repo": "rocket" }), "acme").unwrap();
5104+ assert_eq!((path.namespace.as_str(), path.name.as_str()), ("acme", "rocket"));
5105+ let path = team_repo(&json!({ "repo": "acme/rocket" }), "other").unwrap();
5106+ assert_eq!((path.namespace.as_str(), path.name.as_str()), ("acme", "rocket"));
5107+ assert!(team_repo(&json!({ "repo": "" }), "acme").is_none());
5108+ assert!(team_repo(&json!({}), "acme").is_none());
5109+ }
5110+
5111+ /// Requested reviewers are added to, or taken from, who is asked; a
5112+ /// team's bare slug is one of the repository's workspace.
5113+ #[test]
5114+ fn requested_reviewers_change_the_whole_list() {
5115+ let input = json!({ "reviewers": ["@Ana", "g1t", "acme/web"], "team_reviewers": ["Backend", "acme/web"] });
5116+ let (people, teams) = reviewer_names(&input, "Acme");
5117+ assert_eq!(people, vec!["ana", "g1t"]);
5118+ assert_eq!(teams, vec!["acme/web", "acme/backend"]);
5119+ let current_people = vec!["bo".to_owned(), "ana".to_owned()];
5120+ let current_teams = vec!["acme/web".to_owned()];
5121+ assert_eq!(
5122+ reviewers_after(&current_people, &current_teams, &people, &teams, true),
5123+ vec!["bo", "ana", "g1t", "acme/web", "acme/backend"]
5124+ );
5125+ assert_eq!(
5126+ reviewers_after(&current_people, &current_teams, &["ANA".to_owned()], &["acme/web".to_owned()], false),
5127+ vec!["bo"]
5128+ );
5129+ assert_eq!(reviewer_names(&json!({}), "acme"), (vec![], vec![]));
5130+ }
39235131 }
+2408−22
726726 "max_revisions": 2,
727727 "merge_queue": false,
728728 "hold_low_confidence": true,
729+ "require_code_owner_review": false,
729730 "updated_by": null,
730731 "updated_at": null
731732 }
738739 ],
739740 "required_approvals": 1,
740741 "count_agent_approvals": false,
741− "merge_queue": true
742+ "merge_queue": true,
743+ "require_code_owner_review": true
742744 },
743745 "response": {
744746 "auto_merge": false,
754756 "max_revisions": 2,
755757 "merge_queue": true,
756758 "hold_low_confidence": true,
759+ "require_code_owner_review": true,
757760 "updated_by": "syntaqx",
758761 "updated_at": "2026-10-04T16:20:37.508Z"
759762 },
760− "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](/reference/api/repositories/list-check-names/) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/working-with-g1t/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/working-with-g1t/#how-sure-the-agent-is)."
763+ "notes": "`required_checks` replaces the whole list: at most 20 names, each a workflow's name or another status's context, as [`list_check_names`](/reference/api/repositories/list-check-names/) gives them. A pull request merges only once each passes on its head; one nothing has reported holds it too. With the merge queue on, each must also pass on the queued state, so the workflows behind them need `merge_group` in their `on:`. `required_approvals` is at most 6 and `max_revisions` at most 5. See [what a repository can ask for](/guides/working-with-g1t/#what-a-repository-can-ask-for). `hold_low_confidence` is on unless turned off: see [confidence](/guides/working-with-g1t/#how-sure-the-agent-is). With `require_code_owner_review`, a pull request into the default branch merges only once the code owners of every file it changes have approved it, as many as each section of its CODEOWNERS file asks: see [Code owners](/guides/codeowners/)."
761764 },
762765 "list_check_names": {
763766 "response": [
778781 ],
779782 "notes": "The names reported on the repository's commits in the last 30 days, most recent first. A workflow reports a check named after it (`CI`), for each event it ran for; another tool, such as a deployment, reports its own name (`g1t / deploy`). Empty until something has reported on a commit: add a workflow in `.g1t/workflows` first."
780783 },
784+ "get_codeowners_errors": {
785+ "params": {
786+ "owner": "flagon-io",
787+ "name": "hello"
788+ },
789+ "query": {
790+ "ref": "main"
791+ },
792+ "response": {
793+ "path": ".github/CODEOWNERS",
794+ "ref": "main",
795+ "size": 412,
796+ "rules": 9,
797+ "sections": [
798+ "Docs"
799+ ],
800+ "errors": [
801+ {
802+ "line": 4,
803+ "kind": "unknown_team",
804+ "token": "@flagon-io/platform",
805+ "message": "@flagon-io/platform is not a team of flagon-io."
806+ },
807+ {
808+ "line": 7,
809+ "kind": "negation",
810+ "token": "!docs/internal/",
811+ "message": "!docs/internal/ starts with !, and negation is not supported; this line is skipped. Give the path a later rule with no owners instead."
812+ }
813+ ]
814+ },
815+ "notes": "The file is the first of `.g1t/CODEOWNERS`, `.github/CODEOWNERS`, `CODEOWNERS`, `docs/CODEOWNERS` and `.gitlab/CODEOWNERS` found on `ref` (the default branch when left out); `path` is null when there is none, with no errors. Each error has its `line` (0 for the file as a whole), the `token` at fault, a `message`, and a `kind`:\n\n| `kind` | |\n| --- | --- |\n| `too_large` | The file is over 3 MB and was ignored. |\n| `negation` | A pattern starting with `!`; the line was skipped. |\n| `character_range` | A pattern with `[` or `]`; the line was skipped. |\n| `bad_pattern` | A pattern that names no path; the line was skipped. |\n| `bad_owner` | An owner that is not `@user`, `@workspace/team` or an email address. |\n| `bad_section` | A section header that could not be read. |\n| `unknown_user` | No account has that username. |\n| `unknown_team` | The workspace has no team of that slug. |\n| `unknown_email` | No account has confirmed that address. |\n| `no_write_access` | The person cannot write to the repository. |\n| `team_no_access` | The team has no write access to the repository. |\n\nSee [Code owners](/guides/codeowners/)."
816+ },
781817 "list_events": {
782818 "query": {
783819 "before": "evt_01m43t2a6c9e3g7j1m5q9t3x7b"
849885 "assignees": [],
850886 "blocked_by": [],
851887 "queued": false,
852− "agent": "claude-code"
888+ "agent": "claude-code",
889+ "milestone": null
853890 }
854891 ],
855892 "notes": "Returns at most 100 issues, newest first. `comment_count` counts comments, not events such as \"opened #14 for this\"."
890927 "assignees": [],
891928 "blocked_by": [],
892929 "queued": false,
893− "agent": null
930+ "agent": null,
931+ "milestone": null
894932 },
895933 "notes": "Labels are lowercased. A label not used before is created."
896934 },
924962 "assignees": [],
925963 "blocked_by": [],
926964 "queued": false,
927− "agent": null
965+ "agent": null,
966+ "milestone": null
928967 },
929968 "pulls": [
930969 {
952991 "files": [],
953992 "assignees": [],
954993 "reviewers": [],
994+ "labels": [],
995+ "milestone": null,
996+ "base": "main",
955997 "author": {
956998 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
957999 "username": "syntaqx",
9961038 "reviewers": [
9971039 "ana"
9981040 ],
1041+ "labels": [],
1042+ "milestone": null,
1043+ "base": "main",
9991044 "author": {
10001045 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
10011046 "username": "syntaqx",
10371082 ],
10381083 "assignees": [
10391084 "syntaqx"
1040− ]
1085+ ],
1086+ "milestone": 3
10411087 },
10421088 "response": {
10431089 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
10701116 ],
10711117 "blocked_by": [],
10721118 "queued": false,
1073− "agent": null
1119+ "agent": null,
1120+ "milestone": {
1121+ "number": 3,
1122+ "title": "Launch"
1123+ }
10741124 }
10751125 },
10761126 "close_issue": {
11051155 "assignees": [],
11061156 "blocked_by": [],
11071157 "queued": false,
1108− "agent": null
1158+ "agent": null,
1159+ "milestone": null
11091160 }
11101161 },
11111162 "reopen_issue": {
11371188 "assignees": [],
11381189 "blocked_by": [],
11391190 "queued": false,
1140− "agent": null
1191+ "agent": null,
1192+ "milestone": null
11411193 }
11421194 },
11431195 "assign_issue": {
11691221 "files": [],
11701222 "assignees": [],
11711223 "reviewers": [],
1224+ "labels": [],
1225+ "milestone": null,
1226+ "base": "main",
11721227 "author": {
11731228 "id": "usr_g1t_agent",
11741229 "username": "g1t",
12211276 "assignees": [],
12221277 "blocked_by": [],
12231278 "queued": false,
1224− "agent": "g1t"
1279+ "agent": "g1t",
1280+ "milestone": null
12251281 },
12261282 "pull": {
12271283 "id": "pr_01m4a2c9b6e0h4m8q2t6x0a4d8",
12481304 "files": [],
12491305 "assignees": [],
12501306 "reviewers": [],
1307+ "labels": [],
1308+ "milestone": null,
1309+ "base": "main",
12511310 "author": {
12521311 "id": "usr_g1t_agent",
12531312 "username": "g1t",
13031362 },
13041363 "list_labels": {
13051364 "response": [
1306− "bug",
1307− "feature",
1308− "docs",
1309− "chore",
1310− "question",
1311− "good first issue"
1365+ {
1366+ "name": "bug",
1367+ "color": "d73a4a",
1368+ "description": "Something isn't working",
1369+ "issues": 4,
1370+ "pulls": 1
1371+ },
1372+ {
1373+ "name": "dependencies",
1374+ "color": "0366d6",
1375+ "description": "Updates a dependency",
1376+ "issues": 0,
1377+ "pulls": 6
1378+ },
1379+ {
1380+ "name": "good first issue",
1381+ "color": "7057ff",
1382+ "description": "Good for newcomers",
1383+ "issues": 2,
1384+ "pulls": 0
1385+ }
13121386 ],
1313− "notes": "The five default labels come first, then the others used on the repository's issues, alphabetically."
1387+ "notes": "By name. A new repository starts with the default labels; add_default_labels adds those an older one is missing."
13141388 },
13151389 "plan_work": {
13161390 "request": {
14791553 "reviewers": [
14801554 "ana"
14811555 ],
1556+ "team_reviewers": [],
14821557 "author": {
14831558 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
14841559 "username": "syntaqx",
14881563 },
14891564 "requested_by": null,
14901565 "created_at": "2026-10-01T18:20:02.117Z",
1491− "updated_at": "2026-10-01T18:35:44.902Z"
1566+ "updated_at": "2026-10-01T18:35:44.902Z",
1567+ "labels": [],
1568+ "milestone": null,
1569+ "base": "main"
14921570 }
14931571 ],
14941572 "notes": "Returns at most 100 pull requests, newest first. `check_status` is `queued`, `running`, `passed`, `failed`, `errored`, or `null` when no checks have run against the head."
15241602 "files": [],
15251603 "assignees": [],
15261604 "reviewers": [],
1605+ "labels": [],
1606+ "milestone": null,
1607+ "base": "main",
15271608 "author": {
15281609 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
15291610 "username": "syntaqx",
15781659 "reviewers": [
15791660 "ana"
15801661 ],
1662+ "team_reviewers": [
1663+ "flagon-io/backend"
1664+ ],
15811665 "author": {
15821666 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
15831667 "username": "syntaqx",
15881672 "requested_by": null,
15891673 "created_at": "2026-10-01T18:20:02.117Z",
15901674 "updated_at": "2026-10-01T18:35:44.902Z",
1591− "confidence": null
1675+ "confidence": null,
1676+ "labels": [],
1677+ "milestone": null,
1678+ "base": "main"
15921679 },
15931680 "issue": {
15941681 "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
16181705 "assignees": [],
16191706 "blocked_by": [],
16201707 "queued": false,
1621− "agent": "claude-code"
1708+ "agent": "claude-code",
1709+ "milestone": null
16221710 },
16231711 "comments": [
16241712 {
16751763 "description": "CI passed",
16761764 "target_url": "https://g1t.sh/syntaqx/hello/actions/runs/run_01m43sw8e2g6j0m4q8t2x6a0c4"
16771765 }
1678− ]
1766+ ],
1767+ "code_owners": {
1768+ "path": ".github/CODEOWNERS",
1769+ "required": true,
1770+ "reviews": [
1771+ {
1772+ "section": null,
1773+ "line": 3,
1774+ "pattern": "/src/",
1775+ "owners": [
1776+ "@flagon-io/backend"
1777+ ],
1778+ "files": [
1779+ "src/main.rs"
1780+ ],
1781+ "optional": false,
1782+ "required": 1,
1783+ "approved_by": [],
1784+ "changes_requested_by": [],
1785+ "satisfied": false
1786+ }
1787+ ],
1788+ "missing": "Code owners have not approved: @flagon-io/backend for /src/.",
1789+ "errors": 0
1790+ }
16791791 },
1680− "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](/reference/api/repositories/update-repo-settings/)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request g1t is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have g1t do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](/reference/api/actions/get-workflow-run/) and [`get_job_logs`](/reference/api/actions/get-job-logs/)."
1792+ "notes": "| Field | |\n| --- | --- |\n| `pull.files` | The files it changes, with lines added and removed. |\n| `statuses` | Its checks: what each workflow run (or another tool, such as a deployment) reported on its head, as `pending`, `success`, `failure` or `error`, with a link to the run. |\n| `required_checks` | Each check the default branch requires (see [`update_repo_settings`](/reference/api/repositories/update-repo-settings/)), as it stands on the head: `success`, `failure`, `pending`, or `expected` when nothing has reported it yet. It merges only once all are `success`. |\n| `checks` | Set when the merge queue took it out, with why in `error`. Older pull requests may show a run of commands from their issue here, from before checks were workflows. |\n| `overlaps` | Other pull requests in progress that change the same files. |\n| `behind` | Whether the default branch has moved since it was made. |\n| `landing` | Whether it is being brought up to date to land. |\n| `lifecycle` | For a pull request g1t is seeing through: its stage, such as `working`, `checking`, `reviewing` or `needs_you`. |\n| `comments` | Comments, reviews and events, with `path`, `line` and `verdict`. |\n| `messages` | Messages sent to the agent working on it. |\n| `pull.reviewers`, `pull.team_reviewers` | The people asked to review it, `g1t` among them when a g1t agent was, and the teams, as `workspace/team`. Change them with [`request_reviewers`](/reference/api/pull-requests/request-reviewers/). |\n| `code_owners` | Present when the branch it merges into has a CODEOWNERS file: its `path`, whether code owners' approval is `required` there, `reviews` (one per section and rule that owns a changed file: `section`, `line`, `pattern`, `owners`, `files`, `optional`, the approvals `required`, `approved_by`, `changes_requested_by` and `satisfied`), what is still `missing`, as the merge box says it, and how many `errors` the file has; [`get_codeowners_errors`](/reference/api/repositories/get-codeowners-errors/) lists them. |\n| `mergeable` | Whether it merges cleanly into its target: `clean`, `conflicting`, `checking` (being worked out) or `unknown`. Worked out ahead of time whenever it or its target moves. |\n| `conflicts` | When `mergeable` is `conflicting`, the files that conflict. Resolve them by merging the target in, or have g1t do it. |\n| `earlier_checks` | Earlier records like `checks`, newest first. |\n\nChecks are the repository's workflows: they run on `pull_request` events when it is opened, marked ready and pushed to. Read why one failed with [`get_workflow_run`](/reference/api/actions/get-workflow-run/) and [`get_job_logs`](/reference/api/actions/get-job-logs/)."
16811793 },
16821794 "get_pull_request_changes": {
16831795 "response": {
17611873 ],
17621874 "assignees": [],
17631875 "reviewers": [],
1876+ "labels": [],
1877+ "milestone": null,
1878+ "base": "main",
17641879 "author": {
17651880 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
17661881 "username": "syntaqx",
18001915 "created_at": "2026-10-01T18:40:05.019Z"
18011916 }
18021917 },
1918+ "request_reviewers": {
1919+ "params": {
1920+ "owner": "flagon-io",
1921+ "name": "hello",
1922+ "number": 14
1923+ },
1924+ "request": {
1925+ "reviewers": [
1926+ "bo"
1927+ ],
1928+ "team_reviewers": [
1929+ "backend"
1930+ ]
1931+ },
1932+ "response": {
1933+ "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
1934+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1935+ "number": 14,
1936+ "issue": 12,
1937+ "title": "Greeting should name the caller",
1938+ "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".",
1939+ "agent": "claude-code",
1940+ "runtime": "external",
1941+ "status": "open",
1942+ "fork": {
1943+ "namespace": "pulls",
1944+ "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
1945+ },
1946+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
1947+ "branch": null,
1948+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
1949+ "merge_base": null,
1950+ "merged_by": null,
1951+ "merged_at": null,
1952+ "superseded_by": null,
1953+ "check_status": "passed",
1954+ "files": [
1955+ {
1956+ "path": "src/main.rs",
1957+ "additions": 6,
1958+ "deletions": 2
1959+ }
1960+ ],
1961+ "assignees": [],
1962+ "reviewers": [
1963+ "ana",
1964+ "bo"
1965+ ],
1966+ "team_reviewers": [
1967+ "flagon-io/backend"
1968+ ],
1969+ "author": {
1970+ "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
1971+ "username": "syntaqx",
1972+ "kind": "user",
1973+ "verified": false,
1974+ "workspaces": []
1975+ },
1976+ "requested_by": null,
1977+ "created_at": "2026-10-01T18:20:02.117Z",
1978+ "updated_at": "2026-10-01T18:40:12.331Z"
1979+ },
1980+ "notes": "Adds to who is asked: `reviewers` by username (`g1t` asks a g1t agent), `team_reviewers` as `workspace/team`, or a team's slug in the repository's workspace. A team with review assignment on has the people it picks added to `reviewers`, and stays in `team_reviewers`. Each is told in their inbox. Nobody is asked to review their own pull request. `422` for someone who is not an account, or a team that does not exist or that you cannot see. Whoever opened it, or the Triage role or higher, while it is open. See [Pull requests](/guides/pull-requests/) and [Teams](/guides/teams/)."
1981+ },
1982+ "remove_requested_reviewers": {
1983+ "params": {
1984+ "owner": "flagon-io",
1985+ "name": "hello",
1986+ "number": 14
1987+ },
1988+ "request": {
1989+ "reviewers": [
1990+ "bo"
1991+ ],
1992+ "team_reviewers": [
1993+ "flagon-io/backend"
1994+ ]
1995+ },
1996+ "response": {
1997+ "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
1998+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
1999+ "number": 14,
2000+ "issue": 12,
2001+ "title": "Greeting should name the caller",
2002+ "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".",
2003+ "agent": "claude-code",
2004+ "runtime": "external",
2005+ "status": "open",
2006+ "fork": {
2007+ "namespace": "pulls",
2008+ "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
2009+ },
2010+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
2011+ "branch": null,
2012+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
2013+ "merge_base": null,
2014+ "merged_by": null,
2015+ "merged_at": null,
2016+ "superseded_by": null,
2017+ "check_status": "passed",
2018+ "files": [
2019+ {
2020+ "path": "src/main.rs",
2021+ "additions": 6,
2022+ "deletions": 2
2023+ }
2024+ ],
2025+ "assignees": [],
2026+ "reviewers": [
2027+ "ana"
2028+ ],
2029+ "team_reviewers": [],
2030+ "author": {
2031+ "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
2032+ "username": "syntaqx",
2033+ "kind": "user",
2034+ "verified": false,
2035+ "workspaces": []
2036+ },
2037+ "requested_by": null,
2038+ "created_at": "2026-10-01T18:20:02.117Z",
2039+ "updated_at": "2026-10-01T18:41:30.904Z"
2040+ },
2041+ "notes": "Takes them off who is asked; anyone not asked is ignored. Reviews they already gave stay, as do the people a team's review assignment picked: remove them by username."
2042+ },
18032043 "merge_pull_request": {
18042044 "request": {
18052045 "keep_issue_open": false
18372077 "reviewers": [
18382078 "ana"
18392079 ],
2080+ "labels": [],
2081+ "milestone": null,
2082+ "base": "main",
18402083 "author": {
18412084 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
18422085 "username": "syntaqx",
18822125 ],
18832126 "assignees": [],
18842127 "reviewers": [],
2128+ "labels": [],
2129+ "milestone": null,
2130+ "base": "main",
18852131 "author": {
18862132 "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
18872133 "username": "syntaqx",
41114357 ],
41124358 "notes": "By username, each with the repositories they have a role on. Refused with `403` for anyone but an owner. See [Access and roles](/guides/access-and-roles/)."
41134359 },
4360+ "list_teams": {
4361+ "params": {
4362+ "workspace": "flagon-io"
4363+ },
4364+ "query": {
4365+ "q": "back"
4366+ },
4367+ "response": [
4368+ {
4369+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4370+ "workspace": "flagon-io",
4371+ "slug": "backend",
4372+ "name": "Backend",
4373+ "description": "The API and the services behind it.",
4374+ "visibility": "visible",
4375+ "parent": {
4376+ "slug": "engineering",
4377+ "name": "Engineering"
4378+ },
4379+ "notify": true,
4380+ "review_assignment": {
4381+ "enabled": false,
4382+ "algorithm": "round_robin",
4383+ "count": 1,
4384+ "skip_busy": false,
4385+ "busy_at": 5,
4386+ "include_child_teams": false,
4387+ "excluded": [],
4388+ "notify_team": false
4389+ },
4390+ "members_count": 4,
4391+ "repos_count": 2,
4392+ "child_teams_count": 1,
4393+ "viewer_role": "maintainer",
4394+ "can_manage": true,
4395+ "created_at": "2026-10-06T15:02:11.480Z",
4396+ "updated_at": "2026-10-06T15:02:11.480Z"
4397+ }
4398+ ],
4399+ "notes": "Teams you are in come first, then the rest by name. A secret team is listed only for its own people and the workspace's owners. `review_assignment` is what happens when the team is asked to review: see [`set_team_review_assignment`](/reference/api/teams/set-team-review-assignment/). `403` for anyone who is not a member of the workspace. See [Teams](/guides/teams/)."
4400+ },
4401+ "create_team": {
4402+ "params": {
4403+ "workspace": "flagon-io"
4404+ },
4405+ "request": {
4406+ "name": "Backend",
4407+ "description": "The API and the services behind it.",
4408+ "visibility": "visible",
4409+ "parent": "engineering",
4410+ "members": [
4411+ "ana"
4412+ ]
4413+ },
4414+ "response": {
4415+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4416+ "workspace": "flagon-io",
4417+ "slug": "backend",
4418+ "name": "Backend",
4419+ "description": "The API and the services behind it.",
4420+ "visibility": "visible",
4421+ "parent": {
4422+ "slug": "engineering",
4423+ "name": "Engineering"
4424+ },
4425+ "notify": true,
4426+ "review_assignment": {
4427+ "enabled": false,
4428+ "algorithm": "round_robin",
4429+ "count": 1,
4430+ "skip_busy": false,
4431+ "busy_at": 5,
4432+ "include_child_teams": false,
4433+ "excluded": [],
4434+ "notify_team": false
4435+ },
4436+ "members_count": 2,
4437+ "repos_count": 0,
4438+ "child_teams_count": 0,
4439+ "viewer_role": "maintainer",
4440+ "can_manage": true,
4441+ "created_at": "2026-10-06T15:02:11.480Z",
4442+ "updated_at": "2026-10-06T15:02:11.480Z"
4443+ },
4444+ "notes": "You become the team's maintainer. `slug` is made from `name` when left out (`Web & Mobile` becomes `web-mobile`), and `409` says one is taken. A workspace has at most 500 teams, nested at most 8 deep; `name` is at most 80 characters and `description` 280. A `secret` team cannot have a parent or child teams. Refused with `403` for an agent's or a workspace's token. See [Teams](/guides/teams/)."
4445+ },
4446+ "get_team": {
4447+ "params": {
4448+ "workspace": "flagon-io",
4449+ "team": "backend"
4450+ },
4451+ "response": {
4452+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4453+ "workspace": "flagon-io",
4454+ "slug": "backend",
4455+ "name": "Backend",
4456+ "description": "The API and the services behind it.",
4457+ "visibility": "visible",
4458+ "parent": {
4459+ "slug": "engineering",
4460+ "name": "Engineering"
4461+ },
4462+ "notify": true,
4463+ "review_assignment": {
4464+ "enabled": false,
4465+ "algorithm": "round_robin",
4466+ "count": 1,
4467+ "skip_busy": false,
4468+ "busy_at": 5,
4469+ "include_child_teams": false,
4470+ "excluded": [],
4471+ "notify_team": false
4472+ },
4473+ "members_count": 4,
4474+ "repos_count": 2,
4475+ "child_teams_count": 1,
4476+ "viewer_role": "maintainer",
4477+ "can_manage": true,
4478+ "created_at": "2026-10-06T15:02:11.480Z",
4479+ "updated_at": "2026-10-06T15:02:11.480Z"
4480+ },
4481+ "notes": "`404` for a team that does not exist, or a secret one you are not in, unless you are an owner."
4482+ },
4483+ "update_team": {
4484+ "params": {
4485+ "workspace": "flagon-io",
4486+ "team": "backend"
4487+ },
4488+ "request": {
4489+ "description": "The API, the services behind it, and their on-call.",
4490+ "visibility": "secret",
4491+ "parent": ""
4492+ },
4493+ "response": {
4494+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4495+ "workspace": "flagon-io",
4496+ "slug": "backend",
4497+ "name": "Backend",
4498+ "description": "The API, the services behind it, and their on-call.",
4499+ "visibility": "secret",
4500+ "parent": null,
4501+ "notify": true,
4502+ "review_assignment": {
4503+ "enabled": false,
4504+ "algorithm": "round_robin",
4505+ "count": 1,
4506+ "skip_busy": false,
4507+ "busy_at": 5,
4508+ "include_child_teams": false,
4509+ "excluded": [],
4510+ "notify_team": false
4511+ },
4512+ "members_count": 4,
4513+ "repos_count": 2,
4514+ "child_teams_count": 1,
4515+ "viewer_role": "maintainer",
4516+ "can_manage": true,
4517+ "created_at": "2026-10-06T15:02:11.480Z",
4518+ "updated_at": "2026-10-07T09:41:52.006Z"
4519+ },
4520+ "notes": "Only the fields given change. `parent` set to `\"\"` takes the team out from under its parent; a team cannot be nested under itself or one of its own child teams. A new `slug` changes how it is mentioned: `@flagon-io/backend` no longer reaches it. `review_assignment` takes the fields [`set_team_review_assignment`](/reference/api/teams/set-team-review-assignment/) does. Owners of the workspace and the team's maintainers (`403` otherwise)."
4521+ },
4522+ "delete_team": {
4523+ "params": {
4524+ "workspace": "flagon-io",
4525+ "team": "backend"
4526+ },
4527+ "response": true,
4528+ "notes": "Its child teams move up to its parent, or to the top when it has none. The roles it gave on repositories are taken away, and it is no longer asked to review. Owners of the workspace and the team's maintainers (`403` otherwise)."
4529+ },
4530+ "list_team_members": {
4531+ "params": {
4532+ "workspace": "flagon-io",
4533+ "team": "backend"
4534+ },
4535+ "query": {
4536+ "include_child_teams": "true"
4537+ },
4538+ "response": [
4539+ {
4540+ "username": "syntaqx",
4541+ "name": "Chase Pierce",
4542+ "avatar": null,
4543+ "role": "maintainer",
4544+ "via": null
4545+ },
4546+ {
4547+ "username": "ana",
4548+ "name": "Ana Lima",
4549+ "avatar": null,
4550+ "role": "member",
4551+ "via": null
4552+ },
4553+ {
4554+ "username": "bo",
4555+ "name": null,
4556+ "avatar": null,
4557+ "role": "member",
4558+ "via": "payments"
4559+ }
4560+ ],
4561+ "notes": "Maintainers come first, then members, each by username. With `include_child_teams`, the people of its child teams (and theirs) follow, each with `via`, the child team they are in; someone in both is listed once, as the team's own."
4562+ },
4563+ "set_team_member": {
4564+ "params": {
4565+ "workspace": "flagon-io",
4566+ "team": "backend",
4567+ "username": "ana"
4568+ },
4569+ "request": {
4570+ "role": "maintainer"
4571+ },
4572+ "response": {
4573+ "username": "ana",
4574+ "name": "Ana Lima",
4575+ "avatar": null,
4576+ "role": "maintainer",
4577+ "via": null
4578+ },
4579+ "notes": "`role` is `member` (the default) or `maintainer`. `422` when they are not a member of the workspace: add them to it first. Owners of the workspace and the team's maintainers (`403` otherwise)."
4580+ },
4581+ "remove_team_member": {
4582+ "params": {
4583+ "workspace": "flagon-io",
4584+ "team": "backend",
4585+ "username": "ana"
4586+ },
4587+ "response": true,
4588+ "notes": "Anyone may take themselves out of a team. Leaving the workspace takes a person out of all its teams."
4589+ },
4590+ "list_child_teams": {
4591+ "params": {
4592+ "workspace": "flagon-io",
4593+ "team": "engineering"
4594+ },
4595+ "response": [
4596+ {
4597+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4598+ "workspace": "flagon-io",
4599+ "slug": "backend",
4600+ "name": "Backend",
4601+ "description": "The API and the services behind it.",
4602+ "visibility": "visible",
4603+ "parent": {
4604+ "slug": "engineering",
4605+ "name": "Engineering"
4606+ },
4607+ "notify": true,
4608+ "review_assignment": {
4609+ "enabled": false,
4610+ "algorithm": "round_robin",
4611+ "count": 1,
4612+ "skip_busy": false,
4613+ "busy_at": 5,
4614+ "include_child_teams": false,
4615+ "excluded": [],
4616+ "notify_team": false
4617+ },
4618+ "members_count": 4,
4619+ "repos_count": 2,
4620+ "child_teams_count": 0,
4621+ "viewer_role": "maintainer",
4622+ "can_manage": true,
4623+ "created_at": "2026-10-06T15:02:11.480Z",
4624+ "updated_at": "2026-10-06T15:02:11.480Z"
4625+ }
4626+ ],
4627+ "notes": "Only the teams directly under it; read each one's own with this again. A child team inherits its parent's roles on repositories, and a mention or review request for the parent reaches its people too."
4628+ },
4629+ "list_team_repos": {
4630+ "params": {
4631+ "workspace": "flagon-io",
4632+ "team": "backend"
4633+ },
4634+ "response": [
4635+ {
4636+ "repo": "flagon-io/hello",
4637+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
4638+ "role": "write",
4639+ "inherited_from": null
4640+ },
4641+ {
4642+ "repo": "flagon-io/docs",
4643+ "repo_id": "rep_01m3m5r2a8c4e6g8j0m2p4r6t8",
4644+ "role": "read",
4645+ "inherited_from": "engineering"
4646+ }
4647+ ],
4648+ "notes": "A role inherited from a parent team names it in `inherited_from`. Where the team has a role of its own on the same repository, the higher one is listed. Only repositories you can see are listed."
4649+ },
4650+ "set_team_repo": {
4651+ "params": {
4652+ "workspace": "flagon-io",
4653+ "team": "backend",
4654+ "repo": "hello"
4655+ },
4656+ "request": {
4657+ "role": "maintain"
4658+ },
4659+ "response": {
4660+ "repo": "flagon-io/hello",
4661+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
4662+ "role": "maintain",
4663+ "inherited_from": null
4664+ },
4665+ "notes": "`repo` in the path is the repository's name in the team's workspace; a team has roles only on its own workspace's repositories. `role` is `read`, `triage`, `write`, `maintain` or `admin`. Everyone in the team and its child teams gets it; someone with a higher role otherwise keeps that. Needs the Admin role on the repository (`403` otherwise), and the `access:admin` scope. See [Access and roles](/guides/access-and-roles/)."
4666+ },
4667+ "remove_team_repo": {
4668+ "params": {
4669+ "workspace": "flagon-io",
4670+ "team": "backend",
4671+ "repo": "hello"
4672+ },
4673+ "response": true,
4674+ "notes": "A role the team inherits from a parent is taken away on the parent. Needs the Admin role on the repository, or to be an owner or one of the team's maintainers."
4675+ },
4676+ "set_team_review_assignment": {
4677+ "params": {
4678+ "workspace": "flagon-io",
4679+ "team": "backend"
4680+ },
4681+ "request": {
4682+ "enabled": true,
4683+ "algorithm": "load_balance",
4684+ "count": 2,
4685+ "skip_busy": true,
4686+ "busy_at": 5,
4687+ "excluded": [
4688+ "syntaqx"
4689+ ]
4690+ },
4691+ "response": {
4692+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4693+ "workspace": "flagon-io",
4694+ "slug": "backend",
4695+ "name": "Backend",
4696+ "description": "The API and the services behind it.",
4697+ "visibility": "visible",
4698+ "parent": {
4699+ "slug": "engineering",
4700+ "name": "Engineering"
4701+ },
4702+ "notify": true,
4703+ "review_assignment": {
4704+ "enabled": true,
4705+ "algorithm": "load_balance",
4706+ "count": 2,
4707+ "skip_busy": true,
4708+ "busy_at": 5,
4709+ "include_child_teams": false,
4710+ "excluded": [
4711+ "syntaqx"
4712+ ],
4713+ "notify_team": false
4714+ },
4715+ "members_count": 4,
4716+ "repos_count": 2,
4717+ "child_teams_count": 1,
4718+ "viewer_role": "maintainer",
4719+ "can_manage": true,
4720+ "created_at": "2026-10-06T15:02:11.480Z",
4721+ "updated_at": "2026-10-07T09:44:03.512Z"
4722+ },
4723+ "notes": "| Field | |\n| --- | --- |\n| `enabled` | Off, everyone in the team is asked. On, `count` people are picked and asked, and the team stays shown as asked beside them. |\n| `algorithm` | `round_robin`: whoever this team asked least recently. `load_balance`: whoever has the fewest pull requests waiting on their review. |\n| `count` | How many to pick, 1 to 10. People from the team already asked count towards it. |\n| `skip_busy`, `busy_at` | Leave out anyone with `busy_at` (1 to 100) or more pull requests waiting on their review. |\n| `include_child_teams` | Also pick from its child teams' people. |\n| `excluded` | Usernames never picked. Replaces the whole list. |\n| `notify_team` | Also tell the rest of the team when people are picked. |\n\nFields left out keep their value. The pull request's author is never picked. See [Teams](/guides/teams/)."
4724+ },
4725+ "list_user_teams": {
4726+ "params": {
4727+ "workspace": "flagon-io",
4728+ "username": "ana"
4729+ },
4730+ "response": [
4731+ {
4732+ "id": "team_01m52k8d3f7h1k5n9r3v7z1c5g",
4733+ "workspace": "flagon-io",
4734+ "slug": "backend",
4735+ "name": "Backend",
4736+ "description": "The API and the services behind it.",
4737+ "visibility": "visible",
4738+ "parent": {
4739+ "slug": "engineering",
4740+ "name": "Engineering"
4741+ },
4742+ "notify": true,
4743+ "review_assignment": {
4744+ "enabled": false,
4745+ "algorithm": "round_robin",
4746+ "count": 1,
4747+ "skip_busy": false,
4748+ "busy_at": 5,
4749+ "include_child_teams": false,
4750+ "excluded": [],
4751+ "notify_team": false
4752+ },
4753+ "members_count": 4,
4754+ "repos_count": 2,
4755+ "child_teams_count": 1,
4756+ "viewer_role": null,
4757+ "can_manage": false,
4758+ "created_at": "2026-10-06T15:02:11.480Z",
4759+ "updated_at": "2026-10-06T15:02:11.480Z"
4760+ }
4761+ ],
4762+ "notes": "Only the teams they are in themselves, not the parents those teams are under. Secret teams you are not in are left out unless you are an owner. `403` for anyone who is not a member of the workspace."
4763+ },
41144764 "list_security_alerts": {
41154765 "params": {
41164766 "owner": "flagon-io",
51805830 }
51815831 ],
51825832 "notes": "Name every pinned project once; anything else is refused with `422 invalid`."
5833+ },
5834+ "list_secret_scanning_alerts": {
5835+ "params": {
5836+ "owner": "flagon-io",
5837+ "name": "hello"
5838+ },
5839+ "query": {
5840+ "state": "open"
5841+ },
5842+ "response": [
5843+ {
5844+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
5845+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
5846+ "kind": "github_token",
5847+ "label": "a GitHub token",
5848+ "path": "scripts/release.sh",
5849+ "line": 12,
5850+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
5851+ "preview": "ghp_X7…",
5852+ "status": "open",
5853+ "source": "push",
5854+ "found_by": "syntaqx",
5855+ "found_at": "2026-10-06T09:14:02.118Z",
5856+ "decided_by": null,
5857+ "reason": null,
5858+ "decided_at": null,
5859+ "dismissed_reason": null,
5860+ "test_value": null,
5861+ "state": "open",
5862+ "validity": "active",
5863+ "validity_checked_at": "2026-10-06T09:20:41.502Z",
5864+ "bypass": {
5865+ "reason": "will_fix_later",
5866+ "comment": "Rotating it this afternoon.",
5867+ "by": "syntaqx",
5868+ "at": "2026-10-06T09:14:02.118Z",
5869+ "approved_by": null
5870+ },
5871+ "pattern_id": null,
5872+ "pattern_name": null,
5873+ "locations": 1
5874+ }
5875+ ],
5876+ "notes": "| Filter | Values |\n| --- | --- |\n| `state` | `open` (in the history, or blocked at a push), `dismissed`, `fixed` |\n| `secret_type` | `aws_access_key`, `github_token`, `custom_pattern`, … |\n| `validity` | `active`, `inactive`, `unknown`, `unsupported` |\n| `bypassed` | `true` or `false` |\n\nThe secret itself is never returned: `preview` is enough to recognise it. `status` says where it stands: `open`, `blocked` (stopped at a push, never landed), `allowed` or `resolved`."
5877+ },
5878+ "list_secret_scanning_alerts_for_workspace": {
5879+ "params": {
5880+ "workspace": "flagon-io"
5881+ },
5882+ "query": {
5883+ "state": "open"
5884+ },
5885+ "response": [
5886+ {
5887+ "repo": "hello",
5888+ "secret": {
5889+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
5890+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
5891+ "kind": "github_token",
5892+ "label": "a GitHub token",
5893+ "path": "scripts/release.sh",
5894+ "line": 12,
5895+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
5896+ "preview": "ghp_X7…",
5897+ "status": "open",
5898+ "source": "push",
5899+ "found_by": "syntaqx",
5900+ "found_at": "2026-10-06T09:14:02.118Z",
5901+ "decided_by": null,
5902+ "reason": null,
5903+ "decided_at": null,
5904+ "dismissed_reason": null,
5905+ "test_value": null,
5906+ "state": "open",
5907+ "validity": "active",
5908+ "validity_checked_at": "2026-10-06T09:20:41.502Z",
5909+ "bypass": {
5910+ "reason": "will_fix_later",
5911+ "comment": "Rotating it this afternoon.",
5912+ "by": "syntaqx",
5913+ "at": "2026-10-06T09:14:02.118Z",
5914+ "approved_by": null
5915+ },
5916+ "pattern_id": null,
5917+ "pattern_name": null,
5918+ "locations": 1
5919+ }
5920+ }
5921+ ],
5922+ "notes": "Every repository whose findings you may see, each alert with its repository's name."
5923+ },
5924+ "get_secret_scanning_alert": {
5925+ "params": {
5926+ "owner": "flagon-io",
5927+ "name": "hello",
5928+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p"
5929+ },
5930+ "response": {
5931+ "secret": {
5932+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
5933+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
5934+ "kind": "github_token",
5935+ "label": "a GitHub token",
5936+ "path": "scripts/release.sh",
5937+ "line": 12,
5938+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
5939+ "preview": "ghp_X7…",
5940+ "status": "blocked",
5941+ "source": "push",
5942+ "found_by": "syntaqx",
5943+ "found_at": "2026-10-06T09:14:02.118Z",
5944+ "decided_by": null,
5945+ "reason": null,
5946+ "decided_at": null,
5947+ "dismissed_reason": null,
5948+ "test_value": null,
5949+ "state": "open",
5950+ "validity": null,
5951+ "validity_checked_at": null,
5952+ "bypass": null,
5953+ "pattern_id": null,
5954+ "pattern_name": null,
5955+ "locations": 1
5956+ },
5957+ "locations": [
5958+ {
5959+ "path": "scripts/release.sh",
5960+ "line": 12,
5961+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
5962+ "source": "push",
5963+ "found_at": "2026-10-06T09:14:02.118Z"
5964+ }
5965+ ],
5966+ "activity": [],
5967+ "requests": [],
5968+ "checkable": true,
5969+ "can_bypass": true,
5970+ "can_request_bypass": false
5971+ }
5972+ },
5973+ "update_secret_scanning_alert": {
5974+ "params": {
5975+ "owner": "flagon-io",
5976+ "name": "hello",
5977+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p"
5978+ },
5979+ "request": {
5980+ "state": "dismissed",
5981+ "reason": "revoked",
5982+ "comment": "Rotated in the issuer's settings."
5983+ },
5984+ "response": {
5985+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
5986+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
5987+ "kind": "github_token",
5988+ "label": "a GitHub token",
5989+ "path": "scripts/release.sh",
5990+ "line": 12,
5991+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
5992+ "preview": "ghp_X7…",
5993+ "status": "resolved",
5994+ "source": "push",
5995+ "found_by": "syntaqx",
5996+ "found_at": "2026-10-06T09:14:02.118Z",
5997+ "decided_by": "syntaqx",
5998+ "reason": "Rotated in the issuer's settings.",
5999+ "decided_at": "2026-10-06T09:20:41.502Z",
6000+ "dismissed_reason": "revoked",
6001+ "test_value": null,
6002+ "state": "fixed",
6003+ "validity": "active",
6004+ "validity_checked_at": "2026-10-06T09:20:41.502Z",
6005+ "bypass": {
6006+ "reason": "will_fix_later",
6007+ "comment": "Rotating it this afternoon.",
6008+ "by": "syntaqx",
6009+ "at": "2026-10-06T09:14:02.118Z",
6010+ "approved_by": null
6011+ },
6012+ "pattern_id": null,
6013+ "pattern_name": null,
6014+ "locations": 1
6015+ },
6016+ "notes": "Takes the Admin role: a dismissed secret is let through push protection, unless it was `revoked`, which marks it fixed. `state` `open` reopens it."
6017+ },
6018+ "list_secret_scanning_locations": {
6019+ "params": {
6020+ "owner": "flagon-io",
6021+ "name": "hello",
6022+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p"
6023+ },
6024+ "response": [
6025+ {
6026+ "path": "scripts/release.sh",
6027+ "line": 12,
6028+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6029+ "source": "push",
6030+ "found_at": "2026-10-06T09:14:02.118Z"
6031+ }
6032+ ]
6033+ },
6034+ "bypass_push_protection": {
6035+ "params": {
6036+ "owner": "flagon-io",
6037+ "name": "hello",
6038+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p"
6039+ },
6040+ "request": {
6041+ "reason": "will_fix_later",
6042+ "comment": "Rotating it this afternoon."
6043+ },
6044+ "response": {
6045+ "secret": {
6046+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
6047+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6048+ "kind": "github_token",
6049+ "label": "a GitHub token",
6050+ "path": "scripts/release.sh",
6051+ "line": 12,
6052+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6053+ "preview": "ghp_X7…",
6054+ "status": "open",
6055+ "source": "push",
6056+ "found_by": "syntaqx",
6057+ "found_at": "2026-10-06T09:14:02.118Z",
6058+ "decided_by": null,
6059+ "reason": null,
6060+ "decided_at": null,
6061+ "dismissed_reason": null,
6062+ "test_value": null,
6063+ "state": "open",
6064+ "validity": null,
6065+ "validity_checked_at": null,
6066+ "bypass": {
6067+ "reason": "will_fix_later",
6068+ "comment": "Rotating it this afternoon.",
6069+ "by": "syntaqx",
6070+ "at": "2026-10-06T09:14:02.118Z",
6071+ "approved_by": null
6072+ },
6073+ "pattern_id": null,
6074+ "pattern_name": null,
6075+ "locations": 1
6076+ },
6077+ "request": null
6078+ },
6079+ "notes": "| Reason | The alert |\n| --- | --- |\n| `false_positive` | Closed as a false positive |\n| `used_in_tests` | Closed as used in tests |\n| `will_fix_later` | Stays open, to be rotated |\n\nWith delegated bypass on, a call from someone who does not review bypasses makes a request instead: `request` is set and the secret is still blocked until an owner or admin approves it. Push again once it is bypassed."
6080+ },
6081+ "check_secret_validity": {
6082+ "params": {
6083+ "owner": "flagon-io",
6084+ "name": "hello",
6085+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p"
6086+ },
6087+ "response": {
6088+ "id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
6089+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6090+ "kind": "github_token",
6091+ "label": "a GitHub token",
6092+ "path": "scripts/release.sh",
6093+ "line": 12,
6094+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6095+ "preview": "ghp_X7…",
6096+ "status": "open",
6097+ "source": "push",
6098+ "found_by": "syntaqx",
6099+ "found_at": "2026-10-06T09:14:02.118Z",
6100+ "decided_by": null,
6101+ "reason": null,
6102+ "decided_at": null,
6103+ "dismissed_reason": null,
6104+ "test_value": null,
6105+ "state": "open",
6106+ "validity": "active",
6107+ "validity_checked_at": "2026-10-06T09:20:41.502Z",
6108+ "bypass": {
6109+ "reason": "will_fix_later",
6110+ "comment": "Rotating it this afternoon.",
6111+ "by": "syntaqx",
6112+ "at": "2026-10-06T09:14:02.118Z",
6113+ "approved_by": null
6114+ },
6115+ "pattern_id": null,
6116+ "pattern_name": null,
6117+ "locations": 1
6118+ },
6119+ "notes": "Made for GitHub, GitLab, Stripe, Slack, npm, OpenAI, Anthropic and SendGrid tokens, with the issuer's own read-only call. Other formats answer `unsupported`; a secret that never landed answers `unknown`."
6120+ },
6121+ "list_bypass_requests": {
6122+ "params": {
6123+ "workspace": "flagon-io"
6124+ },
6125+ "query": {
6126+ "state": "pending"
6127+ },
6128+ "response": [
6129+ {
6130+ "id": "byp_01kq2n8e5f6g7h8j9k0m1n2p3q",
6131+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6132+ "workspace": "flagon-io",
6133+ "repo": "hello",
6134+ "secret_id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
6135+ "label": "a GitHub token",
6136+ "path": "scripts/release.sh",
6137+ "line": 12,
6138+ "preview": "ghp_X7…",
6139+ "requester": "ana",
6140+ "reason": "used_in_tests",
6141+ "comment": "A token from the test fixtures, never issued.",
6142+ "state": "pending",
6143+ "reviewer": null,
6144+ "review_comment": null,
6145+ "created_at": "2026-10-06T09:14:02.118Z",
6146+ "reviewed_at": null
6147+ }
6148+ ]
6149+ },
6150+ "review_bypass_request": {
6151+ "params": {
6152+ "workspace": "flagon-io",
6153+ "id": "byp_01kq2n8e5f6g7h8j9k0m1n2p3q"
6154+ },
6155+ "request": {
6156+ "decision": "approve",
6157+ "comment": "A fixture."
6158+ },
6159+ "response": {
6160+ "id": "byp_01kq2n8e5f6g7h8j9k0m1n2p3q",
6161+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6162+ "workspace": "flagon-io",
6163+ "repo": "hello",
6164+ "secret_id": "sec_01kq2m7d4e5f6g7h8j9k0m1n2p",
6165+ "label": "a GitHub token",
6166+ "path": "scripts/release.sh",
6167+ "line": 12,
6168+ "preview": "ghp_X7…",
6169+ "requester": "ana",
6170+ "reason": "used_in_tests",
6171+ "comment": "A token from the test fixtures, never issued.",
6172+ "state": "approved",
6173+ "reviewer": "syntaqx",
6174+ "review_comment": "A fixture.",
6175+ "created_at": "2026-10-06T09:14:02.118Z",
6176+ "reviewed_at": "2026-10-06T09:20:41.502Z"
6177+ }
6178+ },
6179+ "list_custom_patterns": {
6180+ "params": {
6181+ "owner": "flagon-io",
6182+ "name": "hello"
6183+ },
6184+ "response": {
6185+ "patterns": [
6186+ {
6187+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r",
6188+ "scope": "repository",
6189+ "workspace": "flagon-io",
6190+ "repo": "hello",
6191+ "name": "Acme API key",
6192+ "pattern": "acme_[a-z0-9]{32}",
6193+ "before": null,
6194+ "after": null,
6195+ "test_strings": [
6196+ "ACME_KEY=acme_0123456789abcdef0123456789abcdef"
6197+ ],
6198+ "state": "published",
6199+ "created_by": "syntaqx",
6200+ "created_at": "2026-10-06T09:14:02.118Z",
6201+ "updated_by": "syntaqx",
6202+ "updated_at": "2026-10-06T09:14:02.118Z",
6203+ "open_alerts": 0
6204+ }
6205+ ],
6206+ "entitled": true
6207+ },
6208+ "notes": "A repository's list includes its workspace's patterns, with `scope` `workspace`. `entitled` says whether they run here: always on a public repository, and on a private one with the Security and quality activation."
6209+ },
6210+ "list_custom_patterns_for_workspace": {
6211+ "params": {
6212+ "workspace": "flagon-io"
6213+ },
6214+ "response": {
6215+ "patterns": [
6216+ {
6217+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r",
6218+ "scope": "workspace",
6219+ "workspace": "flagon-io",
6220+ "repo": null,
6221+ "name": "Acme API key",
6222+ "pattern": "acme_[a-z0-9]{32}",
6223+ "before": null,
6224+ "after": null,
6225+ "test_strings": [
6226+ "ACME_KEY=acme_0123456789abcdef0123456789abcdef"
6227+ ],
6228+ "state": "published",
6229+ "created_by": "syntaqx",
6230+ "created_at": "2026-10-06T09:14:02.118Z",
6231+ "updated_by": "syntaqx",
6232+ "updated_at": "2026-10-06T09:14:02.118Z",
6233+ "open_alerts": 0
6234+ }
6235+ ],
6236+ "entitled": true
6237+ }
6238+ },
6239+ "create_custom_pattern": {
6240+ "params": {
6241+ "owner": "flagon-io",
6242+ "name": "hello"
6243+ },
6244+ "request": {
6245+ "pattern_name": "Acme API key",
6246+ "pattern": "acme_[a-z0-9]{32}",
6247+ "test_strings": [
6248+ "ACME_KEY=acme_0123456789abcdef0123456789abcdef"
6249+ ],
6250+ "publish": true
6251+ },
6252+ "response": {
6253+ "pattern": {
6254+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r",
6255+ "scope": "repository",
6256+ "workspace": "flagon-io",
6257+ "repo": "hello",
6258+ "name": "Acme API key",
6259+ "pattern": "acme_[a-z0-9]{32}",
6260+ "before": null,
6261+ "after": null,
6262+ "test_strings": [
6263+ "ACME_KEY=acme_0123456789abcdef0123456789abcdef"
6264+ ],
6265+ "state": "published",
6266+ "created_by": "syntaqx",
6267+ "created_at": "2026-10-06T09:14:02.118Z",
6268+ "updated_by": "syntaqx",
6269+ "updated_at": "2026-10-06T09:14:02.118Z",
6270+ "open_alerts": 0
6271+ },
6272+ "tests": [
6273+ [
6274+ 9,
6275+ 46
6276+ ]
6277+ ]
6278+ },
6279+ "notes": "`tests` gives, for each test string, where the pattern matched (start and end, in characters), or `null`. A pattern that does not compile, matches an empty string, or is too complex is refused with `422` and says why."
6280+ },
6281+ "create_custom_pattern_for_workspace": {
6282+ "params": {
6283+ "workspace": "flagon-io"
6284+ },
6285+ "request": {
6286+ "pattern_name": "Acme API key",
6287+ "pattern": "acme_[a-z0-9]{32}",
6288+ "publish": false
6289+ },
6290+ "response": {
6291+ "pattern": {
6292+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r",
6293+ "scope": "workspace",
6294+ "workspace": "flagon-io",
6295+ "repo": null,
6296+ "name": "Acme API key",
6297+ "pattern": "acme_[a-z0-9]{32}",
6298+ "before": null,
6299+ "after": null,
6300+ "test_strings": [],
6301+ "state": "draft",
6302+ "created_by": "syntaqx",
6303+ "created_at": "2026-10-06T09:14:02.118Z",
6304+ "updated_by": "syntaqx",
6305+ "updated_at": "2026-10-06T09:14:02.118Z",
6306+ "open_alerts": 0
6307+ },
6308+ "tests": []
6309+ }
6310+ },
6311+ "update_custom_pattern": {
6312+ "params": {
6313+ "owner": "flagon-io",
6314+ "name": "hello",
6315+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r"
6316+ },
6317+ "request": {
6318+ "pattern_name": "Acme API key",
6319+ "pattern": "acme_[a-z0-9]{32,40}",
6320+ "publish": true
6321+ },
6322+ "response": {
6323+ "pattern": {
6324+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r",
6325+ "scope": "repository",
6326+ "workspace": "flagon-io",
6327+ "repo": "hello",
6328+ "name": "Acme API key",
6329+ "pattern": "acme_[a-z0-9]{32,40}",
6330+ "before": null,
6331+ "after": null,
6332+ "test_strings": [
6333+ "ACME_KEY=acme_0123456789abcdef0123456789abcdef"
6334+ ],
6335+ "state": "published",
6336+ "created_by": "syntaqx",
6337+ "created_at": "2026-10-06T09:14:02.118Z",
6338+ "updated_by": "syntaqx",
6339+ "updated_at": "2026-10-06T09:14:02.118Z",
6340+ "open_alerts": 0
6341+ },
6342+ "tests": [
6343+ [
6344+ 9,
6345+ 46
6346+ ]
6347+ ]
6348+ }
6349+ },
6350+ "update_custom_pattern_for_workspace": {
6351+ "params": {
6352+ "workspace": "flagon-io",
6353+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r"
6354+ },
6355+ "request": {
6356+ "pattern_name": "Acme API key",
6357+ "pattern": "acme_[a-z0-9]{32}",
6358+ "publish": true
6359+ },
6360+ "response": {
6361+ "pattern": {
6362+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r",
6363+ "scope": "workspace",
6364+ "workspace": "flagon-io",
6365+ "repo": null,
6366+ "name": "Acme API key",
6367+ "pattern": "acme_[a-z0-9]{32}",
6368+ "before": null,
6369+ "after": null,
6370+ "test_strings": [
6371+ "ACME_KEY=acme_0123456789abcdef0123456789abcdef"
6372+ ],
6373+ "state": "published",
6374+ "created_by": "syntaqx",
6375+ "created_at": "2026-10-06T09:14:02.118Z",
6376+ "updated_by": "syntaqx",
6377+ "updated_at": "2026-10-06T09:14:02.118Z",
6378+ "open_alerts": 0
6379+ },
6380+ "tests": [
6381+ [
6382+ 9,
6383+ 46
6384+ ]
6385+ ]
6386+ }
6387+ },
6388+ "delete_custom_pattern": {
6389+ "params": {
6390+ "owner": "flagon-io",
6391+ "name": "hello",
6392+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r"
6393+ },
6394+ "response": {
6395+ "deleted": true
6396+ }
6397+ },
6398+ "delete_custom_pattern_for_workspace": {
6399+ "params": {
6400+ "workspace": "flagon-io",
6401+ "id": "pat_01kq2p9f6g7h8j9k0m1n2p3q4r"
6402+ },
6403+ "response": {
6404+ "deleted": true
6405+ }
6406+ },
6407+ "dry_run_custom_pattern": {
6408+ "params": {
6409+ "owner": "flagon-io",
6410+ "name": "hello"
6411+ },
6412+ "request": {
6413+ "pattern": "acme_[a-z0-9]{32}"
6414+ },
6415+ "response": {
6416+ "repos": [
6417+ {
6418+ "name": "hello",
6419+ "files_scanned": 214,
6420+ "matches": [
6421+ {
6422+ "path": "config/dev.env",
6423+ "line": 3,
6424+ "preview": "ACME_KEY=acme_01••••••••••••••••••••••••"
6425+ }
6426+ ],
6427+ "truncated": false,
6428+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291"
6429+ }
6430+ ]
6431+ },
6432+ "notes": "Reads up to 2,000 files and 20 MB of the default branch, skipping what secret scanning skips. Nothing is recorded."
6433+ },
6434+ "dry_run_custom_pattern_for_workspace": {
6435+ "params": {
6436+ "workspace": "flagon-io"
6437+ },
6438+ "request": {
6439+ "pattern": "acme_[a-z0-9]{32}",
6440+ "repos": [
6441+ "hello"
6442+ ]
6443+ },
6444+ "response": {
6445+ "repos": [
6446+ {
6447+ "name": "hello",
6448+ "files_scanned": 214,
6449+ "matches": [],
6450+ "truncated": false,
6451+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291"
6452+ }
6453+ ]
6454+ }
6455+ },
6456+ "list_code_scanning_alerts": {
6457+ "params": {
6458+ "owner": "flagon-io",
6459+ "name": "hello"
6460+ },
6461+ "query": {
6462+ "state": "open",
6463+ "severity": "high"
6464+ },
6465+ "response": [
6466+ {
6467+ "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s",
6468+ "number": 4,
6469+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6470+ "tool": "Semgrep OSS",
6471+ "category": "Semgrep OSS",
6472+ "rule_id": "javascript.lang.security.detect-child-process.detect-child-process",
6473+ "rule_name": "javascript.lang.security.detect-child-process.detect-child-process",
6474+ "rule_description": "Detected calls to child_process from a function argument.",
6475+ "help": null,
6476+ "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process",
6477+ "tags": [
6478+ "security",
6479+ "CWE-78"
6480+ ],
6481+ "level": "error",
6482+ "security_severity": null,
6483+ "severity": "high",
6484+ "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.",
6485+ "path": "src/server.js",
6486+ "start_line": 6,
6487+ "end_line": 6,
6488+ "start_column": 3,
6489+ "end_column": 60,
6490+ "state": "open",
6491+ "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6",
6492+ "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6493+ "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6494+ "created_at": "2026-10-06T09:14:02.118Z",
6495+ "updated_at": "2026-10-06T09:14:02.118Z",
6496+ "fixed_at": null,
6497+ "dismissed_by": null,
6498+ "dismissed_reason": null,
6499+ "dismissed_comment": null,
6500+ "dismissed_at": null,
6501+ "issue": null
6502+ }
6503+ ],
6504+ "notes": "`severity` is the rule's security severity when it has one (from its `security-severity` score), else from the result's level: `error` high, `warning` medium, `note` low."
6505+ },
6506+ "list_code_scanning_alerts_for_workspace": {
6507+ "params": {
6508+ "workspace": "flagon-io"
6509+ },
6510+ "query": {
6511+ "state": "open"
6512+ },
6513+ "response": [
6514+ {
6515+ "repo": "hello",
6516+ "code": {
6517+ "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s",
6518+ "number": 4,
6519+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6520+ "tool": "Semgrep OSS",
6521+ "category": "Semgrep OSS",
6522+ "rule_id": "javascript.lang.security.detect-child-process.detect-child-process",
6523+ "rule_name": "javascript.lang.security.detect-child-process.detect-child-process",
6524+ "rule_description": "Detected calls to child_process from a function argument.",
6525+ "help": null,
6526+ "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process",
6527+ "tags": [
6528+ "security",
6529+ "CWE-78"
6530+ ],
6531+ "level": "error",
6532+ "security_severity": null,
6533+ "severity": "high",
6534+ "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.",
6535+ "path": "src/server.js",
6536+ "start_line": 6,
6537+ "end_line": 6,
6538+ "start_column": 3,
6539+ "end_column": 60,
6540+ "state": "open",
6541+ "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6",
6542+ "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6543+ "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6544+ "created_at": "2026-10-06T09:14:02.118Z",
6545+ "updated_at": "2026-10-06T09:14:02.118Z",
6546+ "fixed_at": null,
6547+ "dismissed_by": null,
6548+ "dismissed_reason": null,
6549+ "dismissed_comment": null,
6550+ "dismissed_at": null,
6551+ "issue": null
6552+ }
6553+ }
6554+ ]
6555+ },
6556+ "get_code_scanning_alert": {
6557+ "params": {
6558+ "owner": "flagon-io",
6559+ "name": "hello",
6560+ "number": 4
6561+ },
6562+ "response": {
6563+ "alert": {
6564+ "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s",
6565+ "number": 4,
6566+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6567+ "tool": "Semgrep OSS",
6568+ "category": "Semgrep OSS",
6569+ "rule_id": "javascript.lang.security.detect-child-process.detect-child-process",
6570+ "rule_name": "javascript.lang.security.detect-child-process.detect-child-process",
6571+ "rule_description": "Detected calls to child_process from a function argument.",
6572+ "help": null,
6573+ "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process",
6574+ "tags": [
6575+ "security",
6576+ "CWE-78"
6577+ ],
6578+ "level": "error",
6579+ "security_severity": null,
6580+ "severity": "high",
6581+ "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.",
6582+ "path": "src/server.js",
6583+ "start_line": 6,
6584+ "end_line": 6,
6585+ "start_column": 3,
6586+ "end_column": 60,
6587+ "state": "open",
6588+ "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6",
6589+ "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6590+ "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6591+ "created_at": "2026-10-06T09:14:02.118Z",
6592+ "updated_at": "2026-10-06T09:14:02.118Z",
6593+ "fixed_at": null,
6594+ "dismissed_by": null,
6595+ "dismissed_reason": null,
6596+ "dismissed_comment": null,
6597+ "dismissed_at": null,
6598+ "issue": null
6599+ },
6600+ "activity": [],
6601+ "analyses": [
6602+ {
6603+ "id": "ana_01kq2rbh8j9k0m1n2p3q4r5s6t",
6604+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6605+ "sarif_id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v",
6606+ "tool": "Semgrep OSS",
6607+ "tool_version": "1.140.0",
6608+ "category": "Semgrep OSS",
6609+ "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291",
6610+ "git_ref": "refs/heads/main",
6611+ "pull": null,
6612+ "results": 7,
6613+ "new_alerts": 2,
6614+ "fixed_alerts": 1,
6615+ "dropped": 0,
6616+ "created_at": "2026-10-06T09:14:02.118Z"
6617+ }
6618+ ]
6619+ }
6620+ },
6621+ "update_code_scanning_alert": {
6622+ "params": {
6623+ "owner": "flagon-io",
6624+ "name": "hello",
6625+ "number": 4
6626+ },
6627+ "request": {
6628+ "state": "dismissed",
6629+ "dismissed_reason": "false_positive",
6630+ "dismissed_comment": "The argument is a constant."
6631+ },
6632+ "response": {
6633+ "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s",
6634+ "number": 4,
6635+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6636+ "tool": "Semgrep OSS",
6637+ "category": "Semgrep OSS",
6638+ "rule_id": "javascript.lang.security.detect-child-process.detect-child-process",
6639+ "rule_name": "javascript.lang.security.detect-child-process.detect-child-process",
6640+ "rule_description": "Detected calls to child_process from a function argument.",
6641+ "help": null,
6642+ "help_uri": "https://semgrep.dev/r/javascript.lang.security.detect-child-process.detect-child-process",
6643+ "tags": [
6644+ "security",
6645+ "CWE-78"
6646+ ],
6647+ "level": "error",
6648+ "security_severity": null,
6649+ "severity": "high",
6650+ "message": "Detected calls to child_process from a function argument `req`. This could lead to a command injection.",
6651+ "path": "src/server.js",
6652+ "start_line": 6,
6653+ "end_line": 6,
6654+ "start_column": 3,
6655+ "end_column": 60,
6656+ "state": "dismissed",
6657+ "fingerprint": "3f1c9a0e7b2d4c5e6f708192a3b4c5d6",
6658+ "first_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6659+ "last_commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6660+ "created_at": "2026-10-06T09:14:02.118Z",
6661+ "updated_at": "2026-10-06T09:14:02.118Z",
6662+ "fixed_at": null,
6663+ "dismissed_by": "syntaqx",
6664+ "dismissed_reason": "false_positive",
6665+ "dismissed_comment": "The argument is a constant.",
6666+ "dismissed_at": "2026-10-06T09:20:41.502Z",
6667+ "issue": null
6668+ }
6669+ },
6670+ "list_code_scanning_analyses": {
6671+ "params": {
6672+ "owner": "flagon-io",
6673+ "name": "hello"
6674+ },
6675+ "response": [
6676+ {
6677+ "id": "ana_01kq2rbh8j9k0m1n2p3q4r5s6t",
6678+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6679+ "sarif_id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v",
6680+ "tool": "Semgrep OSS",
6681+ "tool_version": "1.140.0",
6682+ "category": "Semgrep OSS",
6683+ "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291",
6684+ "git_ref": "refs/heads/main",
6685+ "pull": null,
6686+ "results": 7,
6687+ "new_alerts": 2,
6688+ "fixed_alerts": 1,
6689+ "dropped": 0,
6690+ "created_at": "2026-10-06T09:14:02.118Z"
6691+ }
6692+ ]
6693+ },
6694+ "upload_sarif": {
6695+ "params": {
6696+ "owner": "flagon-io",
6697+ "name": "hello"
6698+ },
6699+ "request": {
6700+ "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291",
6701+ "ref": "refs/heads/main",
6702+ "sarif": "H4sIAAAAAAAA…",
6703+ "checkout_uri": "file:///home/runner/work/repo"
6704+ },
6705+ "response": {
6706+ "id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v",
6707+ "processing_status": "complete",
6708+ "analyses": [
6709+ "ana_01kq2rbh8j9k0m1n2p3q4r5s6t"
6710+ ],
6711+ "errors": [],
6712+ "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291",
6713+ "git_ref": "refs/heads/main",
6714+ "created_at": "2026-10-06T09:14:02.118Z"
6715+ },
6716+ "notes": "`sarif` is the SARIF 2.1.0 file gzipped, then base64-encoded: `gzip -c results.sarif | base64 -w0`. The upload is read at once: `processing_status` is `complete` or `failed`, with `errors` saying why. For `refs/pull/<number>/head`, the results become the pull request's `Code scanning` check instead of alerts."
6717+ },
6718+ "get_sarif_upload": {
6719+ "params": {
6720+ "owner": "flagon-io",
6721+ "name": "hello",
6722+ "id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v"
6723+ },
6724+ "response": {
6725+ "id": "sar_01kq2scj9k0m1n2p3q4r5s6t7v",
6726+ "processing_status": "complete",
6727+ "analyses": [
6728+ "ana_01kq2rbh8j9k0m1n2p3q4r5s6t"
6729+ ],
6730+ "errors": [],
6731+ "commit_sha": "4807077b296e6edbf410d55e72749d3e1170c291",
6732+ "git_ref": "refs/heads/main",
6733+ "created_at": "2026-10-06T09:14:02.118Z"
6734+ }
6735+ },
6736+ "list_vulnerability_alerts": {
6737+ "params": {
6738+ "owner": "flagon-io",
6739+ "name": "hello"
6740+ },
6741+ "query": {
6742+ "state": "open"
6743+ },
6744+ "response": [
6745+ {
6746+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n",
6747+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6748+ "ecosystem": "npm",
6749+ "package": "lodash",
6750+ "version": "4.17.20",
6751+ "manifest": "package-lock.json",
6752+ "advisory": "GHSA-35jh-r3h4-6jhm",
6753+ "osv_id": "GHSA-35jh-r3h4-6jhm",
6754+ "summary": "Command Injection in lodash",
6755+ "severity": "high",
6756+ "fixed_version": "4.17.21",
6757+ "status": "open",
6758+ "issue": null,
6759+ "found_at": "2026-10-06T09:14:02.118Z",
6760+ "fixed_at": null,
6761+ "state": "open",
6762+ "dismissed_by": null,
6763+ "dismissed_reason": null,
6764+ "dismissed_comment": null,
6765+ "dismissed_at": null,
6766+ "update": null
6767+ }
6768+ ]
6769+ },
6770+ "list_vulnerability_alerts_for_workspace": {
6771+ "params": {
6772+ "workspace": "flagon-io"
6773+ },
6774+ "query": {
6775+ "severity": "critical"
6776+ },
6777+ "response": [
6778+ {
6779+ "repo": "hello",
6780+ "vulnerability": {
6781+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n",
6782+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6783+ "ecosystem": "npm",
6784+ "package": "lodash",
6785+ "version": "4.17.20",
6786+ "manifest": "package-lock.json",
6787+ "advisory": "GHSA-35jh-r3h4-6jhm",
6788+ "osv_id": "GHSA-35jh-r3h4-6jhm",
6789+ "summary": "Command Injection in lodash",
6790+ "severity": "high",
6791+ "fixed_version": "4.17.21",
6792+ "status": "open",
6793+ "issue": null,
6794+ "found_at": "2026-10-06T09:14:02.118Z",
6795+ "fixed_at": null,
6796+ "state": "open",
6797+ "dismissed_by": null,
6798+ "dismissed_reason": null,
6799+ "dismissed_comment": null,
6800+ "dismissed_at": null,
6801+ "update": null
6802+ }
6803+ }
6804+ ]
6805+ },
6806+ "get_vulnerability_alert": {
6807+ "params": {
6808+ "owner": "flagon-io",
6809+ "name": "hello",
6810+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n"
6811+ },
6812+ "response": {
6813+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n",
6814+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6815+ "ecosystem": "npm",
6816+ "package": "lodash",
6817+ "version": "4.17.20",
6818+ "manifest": "package-lock.json",
6819+ "advisory": "GHSA-35jh-r3h4-6jhm",
6820+ "osv_id": "GHSA-35jh-r3h4-6jhm",
6821+ "summary": "Command Injection in lodash",
6822+ "severity": "high",
6823+ "fixed_version": "4.17.21",
6824+ "status": "open",
6825+ "issue": null,
6826+ "found_at": "2026-10-06T09:14:02.118Z",
6827+ "fixed_at": null,
6828+ "state": "open",
6829+ "dismissed_by": null,
6830+ "dismissed_reason": null,
6831+ "dismissed_comment": null,
6832+ "dismissed_at": null,
6833+ "update": null
6834+ }
6835+ },
6836+ "update_vulnerability_alert": {
6837+ "params": {
6838+ "owner": "flagon-io",
6839+ "name": "hello",
6840+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n"
6841+ },
6842+ "request": {
6843+ "state": "dismissed",
6844+ "reason": "tolerable_risk",
6845+ "comment": "Only the build uses it."
6846+ },
6847+ "response": {
6848+ "id": "vul_01kp4b8c3d4e5f6g7h8j9k0m1n",
6849+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
6850+ "ecosystem": "npm",
6851+ "package": "lodash",
6852+ "version": "4.17.20",
6853+ "manifest": "package-lock.json",
6854+ "advisory": "GHSA-35jh-r3h4-6jhm",
6855+ "osv_id": "GHSA-35jh-r3h4-6jhm",
6856+ "summary": "Command Injection in lodash",
6857+ "severity": "high",
6858+ "fixed_version": "4.17.21",
6859+ "status": "dismissed",
6860+ "issue": null,
6861+ "found_at": "2026-10-06T09:14:02.118Z",
6862+ "fixed_at": null,
6863+ "state": "dismissed",
6864+ "dismissed_by": "syntaqx",
6865+ "dismissed_reason": "tolerable_risk",
6866+ "dismissed_comment": "Only the build uses it.",
6867+ "dismissed_at": "2026-10-06T09:20:41.502Z",
6868+ "update": null
6869+ }
6870+ },
6871+ "fix_security_alert": {
6872+ "params": {
6873+ "owner": "flagon-io",
6874+ "name": "hello",
6875+ "id": "cod_01kq2qag7h8j9k0m1n2p3q4r5s"
6876+ },
6877+ "response": {
6878+ "issue": 57,
6879+ "started": true,
6880+ "message": null
6881+ },
6882+ "notes": "Opens an issue with what is wrong and what done means, and puts g1t on it as you. Its run is charged as agent usage, and its pull request lands through the repository's required checks. Asking again while the issue is open returns it."
6883+ },
6884+ "get_dependency_graph": {
6885+ "params": {
6886+ "owner": "flagon-io",
6887+ "name": "hello"
6888+ },
6889+ "response": {
6890+ "commit": "4807077b296e6edbf410d55e72749d3e1170c291",
6891+ "manifests": [
6892+ {
6893+ "path": "package-lock.json",
6894+ "ecosystem": "npm",
6895+ "dependencies": 2,
6896+ "direct": 1
6897+ }
6898+ ],
6899+ "dependencies": [
6900+ {
6901+ "ecosystem": "npm",
6902+ "name": "lodash",
6903+ "version": "4.17.20",
6904+ "manifest": "package-lock.json",
6905+ "relationship": "direct",
6906+ "development": false,
6907+ "license": "MIT",
6908+ "purl": "pkg:npm/lodash@4.17.20",
6909+ "vulnerabilities": 1
6910+ },
6911+ {
6912+ "ecosystem": "npm",
6913+ "name": "ms",
6914+ "version": "2.1.3",
6915+ "manifest": "package-lock.json",
6916+ "relationship": "transitive",
6917+ "development": false,
6918+ "license": "MIT",
6919+ "purl": "pkg:npm/ms@2.1.3",
6920+ "vulnerabilities": 0
6921+ }
6922+ ]
6923+ }
6924+ },
6925+ "get_sbom": {
6926+ "params": {
6927+ "owner": "flagon-io",
6928+ "name": "hello"
6929+ },
6930+ "response": {
6931+ "sbom": {
6932+ "spdxVersion": "SPDX-2.3",
6933+ "dataLicense": "CC0-1.0",
6934+ "SPDXID": "SPDXRef-DOCUMENT",
6935+ "name": "flagon-io/hello dependency graph",
6936+ "documentNamespace": "https://g1t.sh/flagon-io/hello/sbom/sbom_01kq2tdk0m1n2p3q4r5s6t7v8w",
6937+ "creationInfo": {
6938+ "created": "2026-10-06T09:14:02Z",
6939+ "creators": [
6940+ "Tool: g1t",
6941+ "Organization: g1t"
6942+ ],
6943+ "comment": "Read from the repository's lockfiles on its default branch."
6944+ },
6945+ "documentDescribes": [
6946+ "SPDXRef-Repository-flagon-io-hello"
6947+ ],
6948+ "packages": [
6949+ {
6950+ "SPDXID": "SPDXRef-Repository-flagon-io-hello",
6951+ "name": "flagon-io/hello",
6952+ "versionInfo": "4807077b296e6edbf410d55e72749d3e1170c291",
6953+ "downloadLocation": "git+https://g1t.sh/flagon-io/hello.git",
6954+ "filesAnalyzed": false,
6955+ "licenseConcluded": "NOASSERTION",
6956+ "licenseDeclared": "NOASSERTION",
6957+ "copyrightText": "NOASSERTION",
6958+ "primaryPackagePurpose": "SOURCE",
6959+ "externalRefs": []
6960+ },
6961+ {
6962+ "SPDXID": "SPDXRef-Package-npm-lodash-4.17.20",
6963+ "name": "lodash",
6964+ "versionInfo": "4.17.20",
6965+ "downloadLocation": "NOASSERTION",
6966+ "filesAnalyzed": false,
6967+ "licenseConcluded": "NOASSERTION",
6968+ "licenseDeclared": "MIT",
6969+ "copyrightText": "NOASSERTION",
6970+ "primaryPackagePurpose": "LIBRARY",
6971+ "comment": "Resolved by package-lock.json (direct dependency).",
6972+ "externalRefs": [
6973+ {
6974+ "referenceCategory": "PACKAGE-MANAGER",
6975+ "referenceType": "purl",
6976+ "referenceLocator": "pkg:npm/lodash@4.17.20"
6977+ }
6978+ ]
6979+ }
6980+ ],
6981+ "relationships": [
6982+ {
6983+ "spdxElementId": "SPDXRef-DOCUMENT",
6984+ "relationshipType": "DESCRIBES",
6985+ "relatedSpdxElement": "SPDXRef-Repository-flagon-io-hello"
6986+ },
6987+ {
6988+ "spdxElementId": "SPDXRef-Repository-flagon-io-hello",
6989+ "relationshipType": "DEPENDS_ON",
6990+ "relatedSpdxElement": "SPDXRef-Package-npm-lodash-4.17.20"
6991+ }
6992+ ]
6993+ }
6994+ },
6995+ "notes": "`sbom` is an SPDX 2.3 JSON document, sent as SPDX spells it. Save it with `jq .sbom`."
6996+ },
6997+ "compare_dependencies": {
6998+ "params": {
6999+ "owner": "flagon-io",
7000+ "name": "hello",
7001+ "basehead": "main...upgrade-deps"
7002+ },
7003+ "response": {
7004+ "base": "main",
7005+ "head": "upgrade-deps",
7006+ "changes": [
7007+ {
7008+ "change_type": "added",
7009+ "manifest": "package-lock.json",
7010+ "ecosystem": "npm",
7011+ "name": "lodash",
7012+ "version": "4.17.20",
7013+ "relationship": "direct",
7014+ "development": false,
7015+ "license": "MIT",
7016+ "purl": "pkg:npm/lodash@4.17.20",
7017+ "vulnerabilities": [
7018+ {
7019+ "advisory": "GHSA-35jh-r3h4-6jhm",
7020+ "osv_id": "GHSA-35jh-r3h4-6jhm",
7021+ "summary": "Command Injection in lodash",
7022+ "severity": "high",
7023+ "fixed_version": "4.17.21",
7024+ "url": "https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm"
7025+ }
7026+ ],
7027+ "denied_license": false,
7028+ "failing": true
7029+ },
7030+ {
7031+ "change_type": "removed",
7032+ "manifest": "package-lock.json",
7033+ "ecosystem": "npm",
7034+ "name": "lodash",
7035+ "version": "4.17.21",
7036+ "relationship": "direct",
7037+ "development": false,
7038+ "license": "MIT",
7039+ "purl": "pkg:npm/lodash@4.17.21",
7040+ "vulnerabilities": [],
7041+ "denied_license": false,
7042+ "failing": false
7043+ }
7044+ ],
7045+ "passed": false,
7046+ "headline": "Adds 1 vulnerable package",
7047+ "fail_on": "high",
7048+ "deny_licenses": []
7049+ }
7050+ },
7051+ "get_security_settings": {
7052+ "params": {
7053+ "owner": "flagon-io",
7054+ "name": "hello"
7055+ },
7056+ "response": {
7057+ "settings": {
7058+ "code_scanning_gate": "high",
7059+ "dependency_review": true,
7060+ "review_fail_on": "high",
7061+ "review_deny_licenses": [
7062+ "AGPL-3.0-only"
7063+ ],
7064+ "review_comment": true
7065+ },
7066+ "workspace": {
7067+ "delegated_bypass": true,
7068+ "validity_checks": true
7069+ },
7070+ "private": true,
7071+ "entitled": true,
7072+ "upkeep": true
7073+ }
7074+ },
7075+ "update_security_settings": {
7076+ "params": {
7077+ "owner": "flagon-io",
7078+ "name": "hello"
7079+ },
7080+ "request": {
7081+ "code_scanning_gate": "high",
7082+ "review_deny_licenses": [
7083+ "AGPL-3.0-only"
7084+ ]
7085+ },
7086+ "response": {
7087+ "settings": {
7088+ "code_scanning_gate": "high",
7089+ "dependency_review": true,
7090+ "review_fail_on": "high",
7091+ "review_deny_licenses": [
7092+ "AGPL-3.0-only"
7093+ ],
7094+ "review_comment": true
7095+ },
7096+ "workspace": {
7097+ "delegated_bypass": true,
7098+ "validity_checks": true
7099+ },
7100+ "private": true,
7101+ "entitled": true,
7102+ "upkeep": true
7103+ },
7104+ "notes": "Only what you send changes. The `Code scanning` and `Dependency review` checks gate merges once you require them in branch protection."
7105+ },
7106+ "get_workspace_security_settings": {
7107+ "params": {
7108+ "workspace": "flagon-io"
7109+ },
7110+ "response": {
7111+ "settings": {
7112+ "delegated_bypass": true,
7113+ "validity_checks": true
7114+ },
7115+ "activated": true
7116+ }
7117+ },
7118+ "update_workspace_security_settings": {
7119+ "params": {
7120+ "workspace": "flagon-io"
7121+ },
7122+ "request": {
7123+ "delegated_bypass": true
7124+ },
7125+ "response": {
7126+ "settings": {
7127+ "delegated_bypass": true,
7128+ "validity_checks": true
7129+ },
7130+ "activated": true
7131+ }
7132+ },
7133+ "get_security_overview": {
7134+ "params": {
7135+ "workspace": "flagon-io"
7136+ },
7137+ "query": {
7138+ "days": "30"
7139+ },
7140+ "response": {
7141+ "activated": true,
7142+ "private_hidden": 0,
7143+ "totals": [
7144+ {
7145+ "alert_type": "secret_scanning",
7146+ "open": {
7147+ "critical": 1,
7148+ "high": 0,
7149+ "medium": 0,
7150+ "low": 0,
7151+ "unknown": 0
7152+ },
7153+ "opened": 2,
7154+ "closed": 1
7155+ },
7156+ {
7157+ "alert_type": "code_scanning",
7158+ "open": {
7159+ "critical": 0,
7160+ "high": 3,
7161+ "medium": 4,
7162+ "low": 0,
7163+ "unknown": 0
7164+ },
7165+ "opened": 7,
7166+ "closed": 2
7167+ },
7168+ {
7169+ "alert_type": "vulnerability",
7170+ "open": {
7171+ "critical": 0,
7172+ "high": 1,
7173+ "medium": 2,
7174+ "low": 1,
7175+ "unknown": 0
7176+ },
7177+ "opened": 3,
7178+ "closed": 5
7179+ }
7180+ ],
7181+ "trend": [
7182+ {
7183+ "day": "2026-10-05",
7184+ "secret_scanning": 1,
7185+ "code_scanning": 8,
7186+ "vulnerability": 6
7187+ },
7188+ {
7189+ "day": "2026-10-06",
7190+ "secret_scanning": 1,
7191+ "code_scanning": 7,
7192+ "vulnerability": 4
7193+ }
7194+ ],
7195+ "repos": [
7196+ {
7197+ "repo_id": "rep_01kp0a1b2c3d4e5f6g7h8j9k0m",
7198+ "name": "hello",
7199+ "private": true,
7200+ "custom_patterns": 1,
7201+ "validity_checks": true,
7202+ "code_scanning_at": "2026-10-06T09:14:02.118Z",
7203+ "dependency_review": true,
7204+ "security_updates": true,
7205+ "lockfiles": 1,
7206+ "secrets": {
7207+ "critical": 1,
7208+ "high": 0,
7209+ "medium": 0,
7210+ "low": 0,
7211+ "unknown": 0
7212+ },
7213+ "code": {
7214+ "critical": 0,
7215+ "high": 3,
7216+ "medium": 4,
7217+ "low": 0,
7218+ "unknown": 0
7219+ },
7220+ "vulnerabilities": {
7221+ "critical": 0,
7222+ "high": 1,
7223+ "medium": 2,
7224+ "low": 1,
7225+ "unknown": 0
7226+ }
7227+ }
7228+ ]
7229+ }
7230+ },
7231+ "create_label": {
7232+ "request": {
7233+ "label": "area: cli",
7234+ "color": "1d76db",
7235+ "description": "The command-line tool"
7236+ },
7237+ "response": {
7238+ "name": "area: cli",
7239+ "color": "1d76db",
7240+ "description": "The command-line tool",
7241+ "issues": 0,
7242+ "pulls": 0
7243+ }
7244+ },
7245+ "update_label": {
7246+ "params": {
7247+ "label": "area: cli"
7248+ },
7249+ "request": {
7250+ "new_name": "cli",
7251+ "color": "0052cc"
7252+ },
7253+ "response": {
7254+ "name": "cli",
7255+ "color": "0052cc",
7256+ "description": "The command-line tool",
7257+ "issues": 3,
7258+ "pulls": 1
7259+ },
7260+ "notes": "Renaming a label renames it on every issue and pull request that carries it."
7261+ },
7262+ "delete_label": {
7263+ "params": {
7264+ "label": "wontfix"
7265+ },
7266+ "response": true
7267+ },
7268+ "add_default_labels": {
7269+ "response": [
7270+ {
7271+ "name": "bug",
7272+ "color": "d73a4a",
7273+ "description": "Something isn't working",
7274+ "issues": 4,
7275+ "pulls": 1
7276+ },
7277+ {
7278+ "name": "documentation",
7279+ "color": "0075ca",
7280+ "description": "Improvements or additions to documentation",
7281+ "issues": 0,
7282+ "pulls": 0
7283+ }
7284+ ],
7285+ "notes": "Every label the repository has afterwards, shortened here. Labels it already had are left as they were."
7286+ },
7287+ "list_issue_labels": {
7288+ "response": [
7289+ {
7290+ "name": "bug",
7291+ "color": "d73a4a",
7292+ "description": "Something isn't working",
7293+ "issues": 4,
7294+ "pulls": 1
7295+ },
7296+ {
7297+ "name": "help wanted",
7298+ "color": "008672",
7299+ "description": "Extra attention is needed",
7300+ "issues": 1,
7301+ "pulls": 0
7302+ }
7303+ ]
7304+ },
7305+ "add_issue_labels": {
7306+ "request": {
7307+ "labels": [
7308+ "help wanted"
7309+ ]
7310+ },
7311+ "response": [
7312+ "bug",
7313+ "help wanted"
7314+ ],
7315+ "notes": "Returns its labels now, by name."
7316+ },
7317+ "set_issue_labels": {
7318+ "request": {
7319+ "labels": [
7320+ "bug"
7321+ ]
7322+ },
7323+ "response": [
7324+ "bug"
7325+ ]
7326+ },
7327+ "remove_issue_labels": {
7328+ "response": []
7329+ },
7330+ "remove_issue_label": {
7331+ "params": {
7332+ "label": "help wanted"
7333+ },
7334+ "response": [
7335+ "bug"
7336+ ]
7337+ },
7338+ "list_milestones": {
7339+ "response": [
7340+ {
7341+ "number": 3,
7342+ "title": "Launch",
7343+ "description": "Everything that has to land before the launch on October 14.",
7344+ "due_on": "2026-10-14",
7345+ "state": "open",
7346+ "open_items": 5,
7347+ "closed_items": 12,
7348+ "created_at": "2026-09-20T09:00:00.000Z",
7349+ "updated_at": "2026-10-06T16:12:40.118Z",
7350+ "closed_at": null
7351+ }
7352+ ],
7353+ "notes": "Open milestones soonest due first, then closed ones. Progress is closed_items out of open_items plus closed_items."
7354+ },
7355+ "create_milestone": {
7356+ "request": {
7357+ "title": "Launch",
7358+ "description": "Everything that has to land before the launch on October 14.",
7359+ "due_on": "2026-10-14"
7360+ },
7361+ "response": {
7362+ "number": 3,
7363+ "title": "Launch",
7364+ "description": "Everything that has to land before the launch on October 14.",
7365+ "due_on": "2026-10-14",
7366+ "state": "open",
7367+ "open_items": 0,
7368+ "closed_items": 0,
7369+ "created_at": "2026-09-20T09:00:00.000Z",
7370+ "updated_at": "2026-09-20T09:00:00.000Z",
7371+ "closed_at": null
7372+ }
7373+ },
7374+ "update_milestone": {
7375+ "params": {
7376+ "milestone": 3
7377+ },
7378+ "request": {
7379+ "state": "closed"
7380+ },
7381+ "response": {
7382+ "number": 3,
7383+ "title": "Launch",
7384+ "description": "Everything that has to land before the launch on October 14.",
7385+ "due_on": "2026-10-14",
7386+ "state": "closed",
7387+ "open_items": 0,
7388+ "closed_items": 17,
7389+ "created_at": "2026-09-20T09:00:00.000Z",
7390+ "updated_at": "2026-10-14T18:00:00.000Z",
7391+ "closed_at": "2026-10-14T18:00:00.000Z"
7392+ }
7393+ },
7394+ "delete_milestone": {
7395+ "params": {
7396+ "milestone": 3
7397+ },
7398+ "response": true
7399+ },
7400+ "get_milestone": {
7401+ "params": {
7402+ "milestone": 3
7403+ },
7404+ "response": {
7405+ "milestone": {
7406+ "number": 3,
7407+ "title": "Launch",
7408+ "description": "Everything that has to land before the launch on October 14.",
7409+ "due_on": "2026-10-14",
7410+ "state": "open",
7411+ "open_items": 5,
7412+ "closed_items": 12,
7413+ "created_at": "2026-09-20T09:00:00.000Z",
7414+ "updated_at": "2026-10-06T16:12:40.118Z",
7415+ "closed_at": null
7416+ },
7417+ "issues": [
7418+ {
7419+ "id": "iss_01m43shrzpfe49x74ga7sj1c6v",
7420+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
7421+ "number": 12,
7422+ "title": "Greeting should name the caller",
7423+ "body": "Take a name from the first argument; fall back to world.",
7424+ "labels": [
7425+ "feature",
7426+ "good first issue"
7427+ ],
7428+ "state": "open",
7429+ "reason": null,
7430+ "resolved_by": null,
7431+ "author": {
7432+ "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
7433+ "username": "syntaqx",
7434+ "kind": "user",
7435+ "verified": false,
7436+ "workspaces": []
7437+ },
7438+ "requested_by": null,
7439+ "created_at": "2026-10-01T18:04:11.482Z",
7440+ "updated_at": "2026-10-01T18:09:47.305Z",
7441+ "closed_at": null,
7442+ "pull_count": 0,
7443+ "comment_count": 0,
7444+ "assignees": [
7445+ "syntaqx"
7446+ ],
7447+ "blocked_by": [],
7448+ "queued": false,
7449+ "agent": null,
7450+ "milestone": {
7451+ "number": 3,
7452+ "title": "Launch"
7453+ }
7454+ }
7455+ ],
7456+ "pulls": [
7457+ {
7458+ "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
7459+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
7460+ "number": 14,
7461+ "issue": 12,
7462+ "title": "Greeting should name the caller",
7463+ "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".",
7464+ "agent": "claude-code",
7465+ "runtime": "external",
7466+ "status": "open",
7467+ "fork": {
7468+ "namespace": "pulls",
7469+ "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
7470+ },
7471+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
7472+ "branch": null,
7473+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
7474+ "merge_base": null,
7475+ "merged_by": null,
7476+ "merged_at": null,
7477+ "superseded_by": null,
7478+ "check_status": "passed",
7479+ "files": [
7480+ {
7481+ "path": "src/main.rs",
7482+ "additions": 6,
7483+ "deletions": 2
7484+ }
7485+ ],
7486+ "assignees": [],
7487+ "reviewers": [
7488+ "ana"
7489+ ],
7490+ "labels": [],
7491+ "milestone": {
7492+ "number": 3,
7493+ "title": "Launch"
7494+ },
7495+ "base": "main",
7496+ "author": {
7497+ "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
7498+ "username": "syntaqx",
7499+ "kind": "user",
7500+ "verified": false,
7501+ "workspaces": []
7502+ },
7503+ "requested_by": null,
7504+ "created_at": "2026-10-01T18:20:02.117Z",
7505+ "updated_at": "2026-10-01T18:35:44.902Z",
7506+ "confidence": null
7507+ }
7508+ ]
7509+ }
7510+ },
7511+ "update_pull_request": {
7512+ "request": {
7513+ "base": "release/1.x",
7514+ "labels": [
7515+ "bug"
7516+ ]
7517+ },
7518+ "response": {
7519+ "id": "pr_01m43smh3vexsr5pmp60qwv0vs",
7520+ "repo_id": "rep_01m3m5q6p0e2qaw6mmjahk0qrr",
7521+ "number": 14,
7522+ "issue": 12,
7523+ "title": "Greeting should name the caller",
7524+ "body": "Reads a name from the first argument. `hello ana` prints \"Hello, ana!\".",
7525+ "agent": "claude-code",
7526+ "runtime": "external",
7527+ "status": "open",
7528+ "fork": {
7529+ "namespace": "pulls",
7530+ "name": "pr_01m43smh3vexsr5pmp60qwv0vs"
7531+ },
7532+ "fork_repo_id": "rep_01m43smh5xd7aw2kq9tv0b4c8e",
7533+ "branch": null,
7534+ "head_commit": "9f2c4e1a7b3d5f60812a4c6e8b0d2f4a6c8e0b13",
7535+ "merge_base": null,
7536+ "merged_by": null,
7537+ "merged_at": null,
7538+ "superseded_by": null,
7539+ "check_status": "passed",
7540+ "files": [
7541+ {
7542+ "path": "src/main.rs",
7543+ "additions": 6,
7544+ "deletions": 2
7545+ }
7546+ ],
7547+ "assignees": [],
7548+ "reviewers": [
7549+ "ana"
7550+ ],
7551+ "labels": [
7552+ "bug"
7553+ ],
7554+ "milestone": null,
7555+ "base": "release/1.x",
7556+ "author": {
7557+ "id": "usr_01kkntcg1eeb98j62xjm7eh09p",
7558+ "username": "syntaqx",
7559+ "kind": "user",
7560+ "verified": false,
7561+ "workspaces": []
7562+ },
7563+ "requested_by": null,
7564+ "created_at": "2026-10-01T18:20:02.117Z",
7565+ "updated_at": "2026-10-01T18:35:44.902Z",
7566+ "confidence": null
7567+ },
7568+ "notes": "A new base takes it out of the merge queue, and whether it is behind, merges cleanly and has the checks it needs is worked out against the new base."
51837569 }
51847570 }
+31−3
77 //! encoded again, so that every field the type has is sent, not only the
88 //! ones an example shows.
99
10−use g1t_contracts::{access, actions, integrations, repos, search, webhooks, work};
10+use g1t_contracts::{access, actions, codeowners, integrations, repos, search, teams, webhooks, work};
1111 use g1t_kit::wire::{self, USER_KEYED};
1212 use serde::Serialize;
1313 use serde::de::DeserializeOwned;
7979 return through::<access::RepoInvitation>(op, as_is);
8080 }
8181 Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is),
82+ // Teams and code owners, also `snake_case`.
83+ Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is),
84+ Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => {
85+ return through::<teams::Team>(op, as_is);
86+ }
87+ Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is),
88+ Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is),
89+ Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is),
90+ Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is),
91+ Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is),
92+ Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is),
8293 // Built by the API itself, in `snake_case`.
8394 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
8495 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
8697 }
8798 _ => {}
8899 }
89− let sent = as_services_send(example);
100+ let mut sent = as_services_send(example);
101+ // A pull request's code owners are `snake_case` inside it.
102+ if op == Op::GetPullRequest
103+ && let Some(code_owners) = example.get("code_owners")
104+ {
105+ sent["codeOwners"] = code_owners.clone();
106+ }
90107 match op {
91108 Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
92109 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
113130 Op::GetIssue => through::<work::IssueDetail>(op, sent),
114131 Op::Delegate => through::<work::Delegated>(op, sent),
115132 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
133+ Op::UpdatePullRequest => through::<work::Pull>(op, sent),
134+ Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
135+ Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
136+ Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
137+ Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
138+ Op::GetMilestone => through::<work::MilestoneDetail>(op, sent),
116139 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
117− Op::MarkPullRequestReady | Op::ClosePullRequest | Op::MergePullRequest | Op::AssignIssue => {
140+ Op::MarkPullRequestReady
141+ | Op::ClosePullRequest
142+ | Op::MergePullRequest
143+ | Op::AssignIssue
144+ | Op::RequestReviewers
145+ | Op::RemoveRequestedReviewers => {
118146 through::<work::Pull>(op, sent)
119147 }
120148 Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
+193−5
33 use serde_json::{Map, Value};
44
55 use crate::operations::Op;
6+use crate::security::SecurityOp;
67
78 pub struct Route {
89 pub method: &'static str,
9495 Op::ListOutsideCollaborators,
9596 &[],
9697 ),
98+ // Teams: a workspace's groups of members, with roles on repositories.
99+ route("GET", "/workspaces/:workspace/teams", Op::ListTeams, &[("q", "query")]),
100+ route("POST", "/workspaces/:workspace/teams", Op::CreateTeam, &[]),
101+ route("GET", "/workspaces/:workspace/teams/:team", Op::GetTeam, &[]),
102+ route("PATCH", "/workspaces/:workspace/teams/:team", Op::UpdateTeam, &[]),
103+ route("DELETE", "/workspaces/:workspace/teams/:team", Op::DeleteTeam, &[]),
104+ route(
105+ "GET",
106+ "/workspaces/:workspace/teams/:team/members",
107+ Op::ListTeamMembers,
108+ &[("include_child_teams", "include_child_teams")],
109+ ),
110+ route("PUT", "/workspaces/:workspace/teams/:team/members/:username", Op::SetTeamMember, &[]),
111+ route("DELETE", "/workspaces/:workspace/teams/:team/members/:username", Op::RemoveTeamMember, &[]),
112+ route("GET", "/workspaces/:workspace/teams/:team/teams", Op::ListChildTeams, &[]),
113+ route("GET", "/workspaces/:workspace/teams/:team/repos", Op::ListTeamRepos, &[]),
114+ route("PUT", "/workspaces/:workspace/teams/:team/repos/:repo", Op::SetTeamRepo, &[]),
115+ route("DELETE", "/workspaces/:workspace/teams/:team/repos/:repo", Op::RemoveTeamRepo, &[]),
116+ route(
117+ "PUT",
118+ "/workspaces/:workspace/teams/:team/review_assignment",
119+ Op::SetTeamReviewAssignment,
120+ &[],
121+ ),
122+ route("GET", "/workspaces/:workspace/members/:username/teams", Op::ListUserTeams, &[]),
123+ // Code owners: the CODEOWNERS file, checked.
124+ route("GET", "/repos/:owner/:name/codeowners/errors", Op::GetCodeownersErrors, &[("ref", "ref")]),
97125 // Security alerts: secrets and vulnerable dependencies.
98126 route(
99127 "GET",
103131 ),
104132 route("POST", "/repos/:owner/:name/security/alerts/:id/dismiss", Op::DismissSecurityAlert, &[]),
105133 route("POST", "/repos/:owner/:name/security/alerts/:id/reopen", Op::ReopenSecurityAlert, &[]),
134+ // The security suite: secret scanning, code scanning, vulnerability
135+ // alerts and the supply chain, at the common addresses.
136+ route("GET", "/repos/:owner/:name/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
137+ route("GET", "/workspaces/:workspace/secret-scanning/alerts", Op::Security(SecurityOp::ListSecretAlerts), &[("state", "state"), ("secret_type", "secret_type"), ("validity", "validity"), ("bypassed", "bypassed")]),
138+ route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::GetSecretAlert), &[]),
139+ route("PATCH", "/repos/:owner/:name/secret-scanning/alerts/:id", Op::Security(SecurityOp::UpdateSecretAlert), &[]),
140+ route("GET", "/repos/:owner/:name/secret-scanning/alerts/:id/locations", Op::Security(SecurityOp::ListSecretLocations), &[]),
141+ route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/bypass", Op::Security(SecurityOp::BypassPushProtection), &[]),
142+ route("POST", "/repos/:owner/:name/secret-scanning/alerts/:id/validity", Op::Security(SecurityOp::CheckSecretValidity), &[]),
143+ route("GET", "/workspaces/:workspace/secret-scanning/bypass-requests", Op::Security(SecurityOp::ListBypassRequests), &[("state", "state"), ("repo", "repo")]),
144+ route("PATCH", "/workspaces/:workspace/secret-scanning/bypass-requests/:id", Op::Security(SecurityOp::ReviewBypassRequest), &[]),
145+ route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
146+ route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns/dry-run", Op::Security(SecurityOp::DryRunCustomPattern), &[]),
147+ route("GET", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
148+ route("GET", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::ListCustomPatterns), &[]),
149+ route("POST", "/repos/:owner/:name/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
150+ route("POST", "/workspaces/:workspace/secret-scanning/custom-patterns", Op::Security(SecurityOp::CreateCustomPattern), &[]),
151+ route("PATCH", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
152+ route("PATCH", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::UpdateCustomPattern), &[]),
153+ route("DELETE", "/repos/:owner/:name/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
154+ route("DELETE", "/workspaces/:workspace/secret-scanning/custom-patterns/:id", Op::Security(SecurityOp::DeleteCustomPattern), &[]),
155+ route("GET", "/repos/:owner/:name/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
156+ route("GET", "/workspaces/:workspace/code-scanning/alerts", Op::Security(SecurityOp::ListCodeAlerts), &[("state", "state"), ("severity", "severity"), ("tool", "tool"), ("rule_id", "rule_id")]),
157+ route("GET", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::GetCodeAlert), &[]),
158+ route("PATCH", "/repos/:owner/:name/code-scanning/alerts/:number", Op::Security(SecurityOp::UpdateCodeAlert), &[]),
159+ route("GET", "/repos/:owner/:name/code-scanning/analyses", Op::Security(SecurityOp::ListAnalyses), &[]),
160+ route("POST", "/repos/:owner/:name/code-scanning/sarifs", Op::Security(SecurityOp::UploadSarif), &[]),
161+ route("GET", "/repos/:owner/:name/code-scanning/sarifs/:id", Op::Security(SecurityOp::GetSarifUpload), &[]),
162+ route("GET", "/repos/:owner/:name/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
163+ route("GET", "/workspaces/:workspace/vulnerability-alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), &[("state", "state"), ("severity", "severity"), ("ecosystem", "ecosystem"), ("package", "package")]),
164+ route("GET", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::GetVulnerabilityAlert), &[]),
165+ route("PATCH", "/repos/:owner/:name/vulnerability-alerts/:id", Op::Security(SecurityOp::UpdateVulnerabilityAlert), &[]),
166+ route("POST", "/repos/:owner/:name/security/alerts/:id/fix", Op::Security(SecurityOp::FixAlert), &[]),
167+ route("GET", "/repos/:owner/:name/dependency-graph", Op::Security(SecurityOp::GetDependencyGraph), &[]),
168+ route("GET", "/repos/:owner/:name/dependency-graph/sbom", Op::Security(SecurityOp::GetSbom), &[]),
169+ route("GET", "/repos/:owner/:name/dependency-graph/compare/:basehead", Op::Security(SecurityOp::CompareDependencies), &[]),
170+ route("GET", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::GetSettings), &[]),
171+ route("PATCH", "/repos/:owner/:name/security/settings", Op::Security(SecurityOp::UpdateSettings), &[]),
172+ route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
173+ route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
174+ route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
106175 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
107176 route(
108177 "GET",
184253 &[("before", "before")],
185254 ),
186255 route("GET", "/repos/:owner/:name/labels", Op::ListLabels, &[]),
256+ route("POST", "/repos/:owner/:name/labels", Op::CreateLabel, &[]),
257+ route("POST", "/repos/:owner/:name/labels/defaults", Op::AddDefaultLabels, &[]),
258+ route("PATCH", "/repos/:owner/:name/labels/:label", Op::UpdateLabel, &[]),
259+ route("DELETE", "/repos/:owner/:name/labels/:label", Op::DeleteLabel, &[]),
260+ route("GET", "/repos/:owner/:name/issues/:number/labels", Op::ListIssueLabels, &[]),
261+ route("POST", "/repos/:owner/:name/issues/:number/labels", Op::AddIssueLabels, &[]),
262+ route("PUT", "/repos/:owner/:name/issues/:number/labels", Op::SetIssueLabels, &[]),
263+ route("DELETE", "/repos/:owner/:name/issues/:number/labels", Op::RemoveIssueLabels, &[]),
264+ route("DELETE", "/repos/:owner/:name/issues/:number/labels/:label", Op::RemoveIssueLabels, &[]),
265+ route("GET", "/repos/:owner/:name/milestones", Op::ListMilestones, &[("state", "state")]),
266+ route("POST", "/repos/:owner/:name/milestones", Op::CreateMilestone, &[]),
267+ route("GET", "/repos/:owner/:name/milestones/:milestone", Op::GetMilestone, &[]),
268+ route("PATCH", "/repos/:owner/:name/milestones/:milestone", Op::UpdateMilestone, &[]),
269+ route("DELETE", "/repos/:owner/:name/milestones/:milestone", Op::DeleteMilestone, &[]),
187270 route(
188271 "GET",
189272 "/repos/:owner/:name/issues",
190273 Op::ListIssues,
191− &[("state", "state"), ("label", "label")],
274+ &[("state", "state"), ("label", "label"), ("milestone", "milestone")],
192275 ),
193276 route("POST", "/repos/:owner/:name/issues", Op::CreateIssue, &[]),
194277 route(
560643 "GET",
561644 "/repos/:owner/:name/pulls",
562645 Op::ListPullRequests,
563− &[("state", "state")],
646+ &[("state", "state"), ("label", "label"), ("milestone", "milestone"), ("base", "base")],
564647 ),
565648 route(
566649 "POST",
575658 &[],
576659 ),
577660 route(
661+ "PATCH",
662+ "/repos/:owner/:name/pulls/:number",
663+ Op::UpdatePullRequest,
664+ &[],
665+ ),
666+ route(
578667 "GET",
579668 "/repos/:owner/:name/pulls/:number/changes",
580669 Op::GetPullRequestChanges,
587676 &[],
588677 ),
589678 route(
679+ "POST",
680+ "/repos/:owner/:name/pulls/:number/requested_reviewers",
681+ Op::RequestReviewers,
682+ &[],
683+ ),
684+ route(
685+ "DELETE",
686+ "/repos/:owner/:name/pulls/:number/requested_reviewers",
687+ Op::RemoveRequestedReviewers,
688+ &[],
689+ ),
690+ route(
590691 "GET",
591692 "/repos/:owner/:name/pulls/:number/session",
592693 Op::ReadSession,
673774 ///
674775 /// The input is the JSON body, overlaid with the query parameters the route
675776 /// reads and then with what the path names: `owner` and `name` become
676−/// `repo`, and `number` becomes an integer.
777+/// `repo`, as does a team's `repo` with its `workspace`, and `number`
778+/// becomes an integer.
677779 pub fn resolve(
678780 method: &str,
679781 path: &str,
703805 if let (Some(owner), Some(name)) = (param("owner"), param("name")) {
704806 input.insert("repo".to_owned(), Value::String(format!("{owner}/{name}")));
705807 }
706− for key in ["plan", "id", "workspace", "delivery", "workflow", "job", "setting", "username"] {
808+ for key in ["plan", "id", "workspace", "delivery", "workflow", "job", "setting", "username", "team", "basehead"] {
707809 if let Some(value) = param(key) {
708810 input.insert(key.to_owned(), Value::String(value.to_owned()));
709811 }
710812 }
711− // A branch name may hold slashes, sent URL-encoded as one segment.
813+ // A repository of a team's workspace, named by itself.
814+ if let (Some(workspace), Some(name)) = (param("workspace"), param("repo")) {
815+ input.insert("repo".to_owned(), Value::String(format!("{workspace}/{name}")));
816+ }
817+ // A branch name may hold slashes, sent URL-encoded as one segment, and
818+ // a label's name spaces.
712819 if let Some(branch) = param("branch") {
713820 input.insert("branch".to_owned(), Value::String(percent_decoded(branch)));
714821 }
822+ if let Some(label) = param("label") {
823+ input.insert("label".to_owned(), Value::String(percent_decoded(label)));
824+ }
825+ if let Some(milestone) = param("milestone") {
826+ // Not a number: zero, which no milestone has.
827+ input.insert("milestone".to_owned(), milestone.parse::<u32>().unwrap_or(0).into());
828+ }
715829 // GitHub says some things with the path alone.
716830 if route.path.ends_with("/enable") || route.path.ends_with("/disable") {
717831 input.insert("enabled".to_owned(), Value::Bool(route.path.ends_with("/enable")));
794908 }
795909
796910 #[test]
911+ fn teams_are_addressed_by_workspace_and_slug() {
912+ let query = [("q".to_owned(), "back".to_owned())];
913+ let (route, input) = resolve("GET", "/workspaces/acme/teams", &query, Value::Null).unwrap();
914+ assert_eq!(route.op, Op::ListTeams);
915+ assert_eq!(input, json!({ "query": "back", "workspace": "acme" }));
916+ let (route, input) = resolve("PATCH", "/workspaces/acme/teams/backend", &[], json!({ "name": "Back end" })).unwrap();
917+ assert_eq!(route.op, Op::UpdateTeam);
918+ assert_eq!(input, json!({ "name": "Back end", "workspace": "acme", "team": "backend" }));
919+ let (route, input) =
920+ resolve("PUT", "/workspaces/acme/teams/backend/members/ana", &[], json!({ "role": "maintainer" })).unwrap();
921+ assert_eq!(route.op, Op::SetTeamMember);
922+ assert_eq!(input, json!({ "role": "maintainer", "workspace": "acme", "team": "backend", "username": "ana" }));
923+ let query = [("include_child_teams".to_owned(), "true".to_owned())];
924+ let (route, input) = resolve("GET", "/workspaces/acme/teams/backend/members", &query, Value::Null).unwrap();
925+ assert_eq!(route.op, Op::ListTeamMembers);
926+ assert_eq!(input, json!({ "include_child_teams": "true", "workspace": "acme", "team": "backend" }));
927+ // A repository is named by itself, in the team's workspace.
928+ let (route, input) =
929+ resolve("PUT", "/workspaces/acme/teams/backend/repos/rocket", &[], json!({ "role": "write" })).unwrap();
930+ assert_eq!(route.op, Op::SetTeamRepo);
931+ assert_eq!(input, json!({ "role": "write", "workspace": "acme", "team": "backend", "repo": "acme/rocket" }));
932+ let op = |method: &str, path: &str| resolve(method, path, &[], Value::Null).unwrap().0.op;
933+ assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/repos/rocket"), Op::RemoveTeamRepo);
934+ assert_eq!(op("GET", "/workspaces/acme/teams/backend/teams"), Op::ListChildTeams);
935+ assert_eq!(op("GET", "/workspaces/acme/teams/backend/repos"), Op::ListTeamRepos);
936+ assert_eq!(op("PUT", "/workspaces/acme/teams/backend/review_assignment"), Op::SetTeamReviewAssignment);
937+ assert_eq!(op("DELETE", "/workspaces/acme/teams/backend"), Op::DeleteTeam);
938+ assert_eq!(op("POST", "/workspaces/acme/teams"), Op::CreateTeam);
939+ assert_eq!(op("GET", "/workspaces/acme/teams/backend"), Op::GetTeam);
940+ assert_eq!(op("DELETE", "/workspaces/acme/teams/backend/members/ana"), Op::RemoveTeamMember);
941+ let (route, input) = resolve("GET", "/workspaces/acme/members/ana/teams", &[], Value::Null).unwrap();
942+ assert_eq!(route.op, Op::ListUserTeams);
943+ assert_eq!(input, json!({ "workspace": "acme", "username": "ana" }));
944+ }
945+
946+ #[test]
947+ fn reviewers_are_requested_and_code_owners_checked_on_a_repository() {
948+ let body = json!({ "reviewers": ["ana"], "team_reviewers": ["backend"] });
949+ let (route, input) = resolve("POST", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body.clone()).unwrap();
950+ assert_eq!(route.op, Op::RequestReviewers);
951+ assert_eq!(
952+ input,
953+ json!({ "reviewers": ["ana"], "team_reviewers": ["backend"], "repo": "acme/rocket", "number": 7 })
954+ );
955+ let (route, _) = resolve("DELETE", "/repos/acme/rocket/pulls/7/requested_reviewers", &[], body).unwrap();
956+ assert_eq!(route.op, Op::RemoveRequestedReviewers);
957+ let query = [("ref".to_owned(), "main".to_owned())];
958+ let (route, input) = resolve("GET", "/repos/acme/rocket/codeowners/errors", &query, Value::Null).unwrap();
959+ assert_eq!(route.op, Op::GetCodeownersErrors);
960+ assert_eq!(input, json!({ "ref": "main", "repo": "acme/rocket" }));
961+ }
962+
963+ #[test]
797964 fn notifications_are_addressed_as_threads_and_by_issue() {
798965 let (route, input) = resolve("DELETE", "/notifications/threads/ntf_1", &[], Value::Null).unwrap();
799966 assert_eq!(route.op, Op::MarkThreadDone);
815982 }
816983
817984 #[test]
985+ fn labels_and_milestones_are_named_in_the_path() {
986+ let (route, input) = resolve("PATCH", "/repos/acme/web/labels/good%20first%20issue", &[], json!({ "color": "7057ff" })).unwrap();
987+ assert_eq!(route.op, Op::UpdateLabel);
988+ assert_eq!(input, json!({ "color": "7057ff", "label": "good first issue", "repo": "acme/web" }));
989+ let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels/bug", &[], Value::Null).unwrap();
990+ assert_eq!(route.op, Op::RemoveIssueLabels);
991+ assert_eq!(input, json!({ "label": "bug", "number": 7, "repo": "acme/web" }));
992+ let (route, input) = resolve("DELETE", "/repos/acme/web/issues/7/labels", &[], Value::Null).unwrap();
993+ assert_eq!(route.op, Op::RemoveIssueLabels);
994+ assert_eq!(input, json!({ "number": 7, "repo": "acme/web" }));
995+ let (route, _) = resolve("POST", "/repos/acme/web/labels/defaults", &[], Value::Null).unwrap();
996+ assert_eq!(route.op, Op::AddDefaultLabels);
997+ let (route, input) = resolve("PATCH", "/repos/acme/web/milestones/3", &[], json!({ "state": "closed" })).unwrap();
998+ assert_eq!(route.op, Op::UpdateMilestone);
999+ assert_eq!(input, json!({ "state": "closed", "milestone": 3, "repo": "acme/web" }));
1000+ let (route, input) = resolve("PATCH", "/repos/acme/web/pulls/9", &[], json!({ "base": "release" })).unwrap();
1001+ assert_eq!(route.op, Op::UpdatePullRequest);
1002+ assert_eq!(input, json!({ "base": "release", "number": 9, "repo": "acme/web" }));
1003+ }
1004+
1005+ #[test]
8181006 fn query_parameters_are_renamed() {
8191007 let query = [
8201008 ("q".to_owned(), "parser".to_owned()),
+1067−0
1+//! The security suite over REST and MCP: secret scanning (alerts, where
2+//! each secret is, push protection bypasses and their review, validity
3+//! checks, custom patterns), code scanning (alerts, analyses, SARIF
4+//! uploads), vulnerability alerts, the dependency graph with its SBOM and
5+//! dependency review, "Fix with g1t", settings, and the workspace's
6+//! overview.
7+//!
8+//! The addresses follow the common ones (`/repos/{owner}/{name}/secret-
9+//! scanning/alerts`, `/code-scanning/sarifs`, `/dependency-graph/sbom`),
10+//! in g1t's spelling: no version prefix, `snake_case` throughout. The
11+//! security service decides who may see and change what, and which parts
12+//! need the Security and quality activation (a 402 says so); this module
13+//! reads the input and gives each answer its public shape.
14+
15+use g1t_contracts::repos::RepoPath;
16+use g1t_contracts::security::{AlertChange, AlertState, DismissArgs, DismissReason, ReopenArgs, SecretFinding, SecurityOverview, Vulnerability};
17+use g1t_contracts::security_suite::*;
18+use g1t_contracts::{FailureCode, Outcome, Viewer};
19+use serde::Serialize;
20+use serde::de::DeserializeOwned;
21+use serde_json::{Value, json};
22+use worker::Result;
23+
24+use crate::operations::Services;
25+
26+/// One operation of the suite.
27+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28+pub enum SecurityOp {
29+ ListSecretAlerts,
30+ GetSecretAlert,
31+ UpdateSecretAlert,
32+ ListSecretLocations,
33+ BypassPushProtection,
34+ CheckSecretValidity,
35+ ListBypassRequests,
36+ ReviewBypassRequest,
37+ ListCustomPatterns,
38+ CreateCustomPattern,
39+ UpdateCustomPattern,
40+ DeleteCustomPattern,
41+ DryRunCustomPattern,
42+ ListCodeAlerts,
43+ GetCodeAlert,
44+ UpdateCodeAlert,
45+ ListAnalyses,
46+ UploadSarif,
47+ GetSarifUpload,
48+ ListVulnerabilityAlerts,
49+ GetVulnerabilityAlert,
50+ UpdateVulnerabilityAlert,
51+ FixAlert,
52+ GetDependencyGraph,
53+ GetSbom,
54+ CompareDependencies,
55+ GetSettings,
56+ UpdateSettings,
57+ GetWorkspaceSettings,
58+ UpdateWorkspaceSettings,
59+ GetOverview,
60+}
61+
62+impl SecurityOp {
63+ /// Every one: `Op::ALL` lists each as `Op::Security(…)`, which a test
64+ /// checks against this.
65+ #[cfg(test)]
66+ pub const ALL: [SecurityOp; 31] = [
67+ SecurityOp::ListSecretAlerts,
68+ SecurityOp::GetSecretAlert,
69+ SecurityOp::UpdateSecretAlert,
70+ SecurityOp::ListSecretLocations,
71+ SecurityOp::BypassPushProtection,
72+ SecurityOp::CheckSecretValidity,
73+ SecurityOp::ListBypassRequests,
74+ SecurityOp::ReviewBypassRequest,
75+ SecurityOp::ListCustomPatterns,
76+ SecurityOp::CreateCustomPattern,
77+ SecurityOp::UpdateCustomPattern,
78+ SecurityOp::DeleteCustomPattern,
79+ SecurityOp::DryRunCustomPattern,
80+ SecurityOp::ListCodeAlerts,
81+ SecurityOp::GetCodeAlert,
82+ SecurityOp::UpdateCodeAlert,
83+ SecurityOp::ListAnalyses,
84+ SecurityOp::UploadSarif,
85+ SecurityOp::GetSarifUpload,
86+ SecurityOp::ListVulnerabilityAlerts,
87+ SecurityOp::GetVulnerabilityAlert,
88+ SecurityOp::UpdateVulnerabilityAlert,
89+ SecurityOp::FixAlert,
90+ SecurityOp::GetDependencyGraph,
91+ SecurityOp::GetSbom,
92+ SecurityOp::CompareDependencies,
93+ SecurityOp::GetSettings,
94+ SecurityOp::UpdateSettings,
95+ SecurityOp::GetWorkspaceSettings,
96+ SecurityOp::UpdateWorkspaceSettings,
97+ SecurityOp::GetOverview,
98+ ];
99+
100+ pub fn name(self) -> &'static str {
101+ match self {
102+ SecurityOp::ListSecretAlerts => "list_secret_scanning_alerts",
103+ SecurityOp::GetSecretAlert => "get_secret_scanning_alert",
104+ SecurityOp::UpdateSecretAlert => "update_secret_scanning_alert",
105+ SecurityOp::ListSecretLocations => "list_secret_scanning_locations",
106+ SecurityOp::BypassPushProtection => "bypass_push_protection",
107+ SecurityOp::CheckSecretValidity => "check_secret_validity",
108+ SecurityOp::ListBypassRequests => "list_bypass_requests",
109+ SecurityOp::ReviewBypassRequest => "review_bypass_request",
110+ SecurityOp::ListCustomPatterns => "list_custom_patterns",
111+ SecurityOp::CreateCustomPattern => "create_custom_pattern",
112+ SecurityOp::UpdateCustomPattern => "update_custom_pattern",
113+ SecurityOp::DeleteCustomPattern => "delete_custom_pattern",
114+ SecurityOp::DryRunCustomPattern => "dry_run_custom_pattern",
115+ SecurityOp::ListCodeAlerts => "list_code_scanning_alerts",
116+ SecurityOp::GetCodeAlert => "get_code_scanning_alert",
117+ SecurityOp::UpdateCodeAlert => "update_code_scanning_alert",
118+ SecurityOp::ListAnalyses => "list_code_scanning_analyses",
119+ SecurityOp::UploadSarif => "upload_sarif",
120+ SecurityOp::GetSarifUpload => "get_sarif_upload",
121+ SecurityOp::ListVulnerabilityAlerts => "list_vulnerability_alerts",
122+ SecurityOp::GetVulnerabilityAlert => "get_vulnerability_alert",
123+ SecurityOp::UpdateVulnerabilityAlert => "update_vulnerability_alert",
124+ SecurityOp::FixAlert => "fix_security_alert",
125+ SecurityOp::GetDependencyGraph => "get_dependency_graph",
126+ SecurityOp::GetSbom => "get_sbom",
127+ SecurityOp::CompareDependencies => "compare_dependencies",
128+ SecurityOp::GetSettings => "get_security_settings",
129+ SecurityOp::UpdateSettings => "update_security_settings",
130+ SecurityOp::GetWorkspaceSettings => "get_workspace_security_settings",
131+ SecurityOp::UpdateWorkspaceSettings => "update_workspace_security_settings",
132+ SecurityOp::GetOverview => "get_security_overview",
133+ }
134+ }
135+
136+ /// For the API reference: "List secret scanning alerts".
137+ pub fn title(self) -> &'static str {
138+ match self {
139+ SecurityOp::ListSecretAlerts => "List secret scanning alerts",
140+ SecurityOp::GetSecretAlert => "Get a secret scanning alert",
141+ SecurityOp::UpdateSecretAlert => "Dismiss or reopen a secret scanning alert",
142+ SecurityOp::ListSecretLocations => "List where a secret was found",
143+ SecurityOp::BypassPushProtection => "Bypass push protection",
144+ SecurityOp::CheckSecretValidity => "Check whether a secret still works",
145+ SecurityOp::ListBypassRequests => "List push protection bypass requests",
146+ SecurityOp::ReviewBypassRequest => "Review a bypass request",
147+ SecurityOp::ListCustomPatterns => "List custom patterns",
148+ SecurityOp::CreateCustomPattern => "Create a custom pattern",
149+ SecurityOp::UpdateCustomPattern => "Update a custom pattern",
150+ SecurityOp::DeleteCustomPattern => "Delete a custom pattern",
151+ SecurityOp::DryRunCustomPattern => "Dry-run a custom pattern",
152+ SecurityOp::ListCodeAlerts => "List code scanning alerts",
153+ SecurityOp::GetCodeAlert => "Get a code scanning alert",
154+ SecurityOp::UpdateCodeAlert => "Dismiss or reopen a code scanning alert",
155+ SecurityOp::ListAnalyses => "List code scanning analyses",
156+ SecurityOp::UploadSarif => "Upload a SARIF file",
157+ SecurityOp::GetSarifUpload => "Get a SARIF upload",
158+ SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts",
159+ SecurityOp::GetVulnerabilityAlert => "Get a vulnerability alert",
160+ SecurityOp::UpdateVulnerabilityAlert => "Dismiss or reopen a vulnerability alert",
161+ SecurityOp::FixAlert => "Fix an alert with g1t",
162+ SecurityOp::GetDependencyGraph => "Get the dependency graph",
163+ SecurityOp::GetSbom => "Export an SBOM",
164+ SecurityOp::CompareDependencies => "Compare dependencies",
165+ SecurityOp::GetSettings => "Get a repository's security settings",
166+ SecurityOp::UpdateSettings => "Update a repository's security settings",
167+ SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings",
168+ SecurityOp::UpdateWorkspaceSettings => "Update a workspace's security settings",
169+ SecurityOp::GetOverview => "Get the security overview",
170+ }
171+ }
172+
173+ pub fn description(self) -> &'static str {
174+ match self {
175+ SecurityOp::ListSecretAlerts => "List secret scanning alerts: secrets found in pushes (blocked) and in history (open), newest first, in a repository or (with workspace) across a workspace. Filter by state (open, dismissed, fixed), secret_type, validity (active, inactive, unknown, unsupported) and bypassed. The secret itself is never returned: a preview and a fingerprint-based id only.",
176+ SecurityOp::GetSecretAlert => "Get one secret scanning alert by id (sec_…), with every place it was found, its activity, its bypass requests, and whether you may bypass it or only ask to.",
177+ SecurityOp::UpdateSecretAlert => "Dismiss a secret scanning alert (state dismissed, with a reason: false_positive, used_in_tests, revoked or wont_fix, and an optional comment) or reopen it (state open). Revoked marks it fixed; the others let pushes carrying it through. Takes the Admin role.",
178+ SecurityOp::ListSecretLocations => "List every place a secret was found: file, line, commit and whether a push or the history scan found it.",
179+ SecurityOp::BypassPushProtection => "Push past push protection for a blocked secret, with a reason: false_positive or used_in_tests (the alert is closed with that reason) or will_fix_later (it stays open, to be rotated). Recorded on the alert and in the audit log. With delegated bypass on, someone who does not review bypasses makes a request instead, which owners and the repository's admins approve or deny; the answer says which happened. Push again once it is bypassed or approved.",
180+ SecurityOp::CheckSecretValidity => "Ask a landed secret's issuer whether it still works, and mark the alert active or inactive. The check is the issuer's own read-only identity call over HTTPS; the secret goes nowhere else. Needs validity checks on for the workspace (and the Security and quality activation on a private repository). Formats with no safe check answer unsupported.",
181+ SecurityOp::ListBypassRequests => "List a workspace's push protection bypass requests, pending first. Owners and repository admins see every request; anyone else their own. Filter by state (pending, approved, denied, cancelled) or repo.",
182+ SecurityOp::ReviewBypassRequest => "Approve or deny a bypass request (owners and the repository's admins, never your own), or cancel your own. An approved request bypasses push protection for that secret, as its requester asked.",
183+ SecurityOp::ListCustomPatterns => "List custom secret patterns: a repository's own and the ones it inherits from its workspace (with repo), or a workspace's (with workspace).",
184+ SecurityOp::CreateCustomPattern => "Create a custom secret pattern: a name, a regular expression for the secret, optional regular expressions for what comes right before and after it, and test strings. Patterns run in linear time (no look-around or back-references) and within size limits. With publish true, push protection and scans use it at once and the history is scanned again for it; otherwise it is a draft. A repository's takes Admin; a workspace's, an owner. On a private repository it needs the Security and quality activation.",
185+ SecurityOp::UpdateCustomPattern => "Change a custom pattern, publish it, or turn it back into a draft (publish false). Returns where it matched each test string.",
186+ SecurityOp::DeleteCustomPattern => "Delete a custom pattern. Alerts it found stay.",
187+ SecurityOp::DryRunCustomPattern => "Run a pattern over the default branch without saving it: of the repository, or (with workspace) of up to ten of its repositories, or those named in repos. Returns the files read and up to fifty matches each, masked.",
188+ SecurityOp::ListCodeAlerts => "List code scanning alerts: problems a tool reported on the default branch, one per tool, category and fingerprint, open first and worst first. In a repository, or (with workspace) across a workspace. Filter by state, severity, tool and rule_id.",
189+ SecurityOp::GetCodeAlert => "Get one code scanning alert by number, with its rule, location, activity and the analyses that reported it.",
190+ SecurityOp::UpdateCodeAlert => "Dismiss a code scanning alert (state dismissed, dismissed_reason false_positive, wont_fix or used_in_tests, optional dismissed_comment) or reopen it (state open). A fixed alert reopens by itself when an analysis reports it again.",
191+ SecurityOp::ListAnalyses => "List code scanning analyses, newest first: each upload's run of one tool on one commit, with how many results it had and the alerts it opened and fixed.",
192+ SecurityOp::UploadSarif => "Upload a SARIF 2.1.0 file: sarif is the file gzipped and base64-encoded; commit_sha the full commit; ref refs/heads/<branch> or refs/pull/<number>/head. For the default branch, new results open alerts and results no longer reported fix theirs. For a pull request, its results new to it on lines it changes become review comments and the Code scanning check, which fails at the repository's threshold. Read at once; the answer says complete or failed and why. Needs the Security and quality activation on a private repository.",
193+ SecurityOp::GetSarifUpload => "Get a SARIF upload by id (sar_…): whether it was read, the analyses it made, and what was wrong.",
194+ SecurityOp::ListVulnerabilityAlerts => "List vulnerability alerts: a package a lockfile resolves with a known advisory, open first and worst first, with the security update g1t opened for it. In a repository, or (with workspace) across a workspace. Filter by state, severity, ecosystem and package.",
195+ SecurityOp::GetVulnerabilityAlert => "Get one vulnerability alert by id (vul_…).",
196+ SecurityOp::UpdateVulnerabilityAlert => "Dismiss a vulnerability alert (state dismissed, with a reason: fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used, and an optional comment) or reopen it (state open).",
197+ SecurityOp::FixAlert => "Put g1t on an issue to fix an alert: a code scanning alert (cod_…), a vulnerable dependency (vul_…) or a secret in the code (sec_…; rotating it stays with you). Its pull request lands through the repository's required checks. The agent's run is charged as agent usage. Returns the issue, and whether the agent started.",
198+ SecurityOp::GetDependencyGraph => "Get the dependency graph: every package the lockfiles on the default branch resolve, per lockfile, with whether it is direct or transitive (where the lockfile says), for development, its license when recorded, its package URL and its open vulnerability alerts.",
199+ SecurityOp::GetSbom => "Export the dependency graph as an SPDX 2.3 JSON document, in sbom. Every package is named by its package URL.",
200+ SecurityOp::CompareDependencies => "Compare the dependencies at two commits, branches or tags (basehead, as base...head): what was added and removed per lockfile, with the known vulnerabilities of what was added and whether it passes the repository's dependency review policy. Needs the Security and quality activation on a private repository.",
201+ SecurityOp::GetSettings => "Get a repository's security settings: when the Code scanning check fails, dependency review and its policy, its workspace's settings, and whether the paid features are on for it.",
202+ SecurityOp::UpdateSettings => "Change a repository's security settings: code_scanning_gate (none, errors, critical, high, medium or any), dependency_review, review_fail_on (critical, high, medium, low or none), review_deny_licenses (SPDX ids) and review_comment. Takes the Maintain role. Require the Code scanning and Dependency review checks in branch protection to gate merges on them.",
203+ SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings (delegated bypass, validity checks) and whether it has the Security and quality activation.",
204+ SecurityOp::UpdateWorkspaceSettings => "Turn delegated bypass and validity checks on or off for a workspace. Owners only.",
205+ SecurityOp::GetOverview => "Get a workspace's security overview: open alerts by type and severity, how many opened and closed in the last days (7 to 90, 30 by default), a daily trend, and for each repository which features are on and what is open, most in need first. Private repositories count with the Security and quality activation only.",
206+ }
207+ }
208+
209+ /// Whether the operation is about one repository named by `repo`
210+ /// (rather than a workspace, or either).
211+ pub fn needs_repo(self) -> bool {
212+ !matches!(
213+ self,
214+ SecurityOp::ListSecretAlerts
215+ | SecurityOp::ListCodeAlerts
216+ | SecurityOp::ListVulnerabilityAlerts
217+ | SecurityOp::ListBypassRequests
218+ | SecurityOp::ReviewBypassRequest
219+ | SecurityOp::ListCustomPatterns
220+ | SecurityOp::CreateCustomPattern
221+ | SecurityOp::UpdateCustomPattern
222+ | SecurityOp::DeleteCustomPattern
223+ | SecurityOp::DryRunCustomPattern
224+ | SecurityOp::GetWorkspaceSettings
225+ | SecurityOp::UpdateWorkspaceSettings
226+ | SecurityOp::GetOverview
227+ )
228+ }
229+
230+ pub fn input(self) -> Value {
231+ let repo = || json!({ "type": "string", "description": "Repository as \"owner/name\", e.g. \"flagon-io/hello\"." });
232+ let either = |mut properties: Value| {
233+ properties["repo"] = json!({ "type": "string", "description": "Repository as \"owner/name\". Or give workspace." });
234+ properties["workspace"] = json!({ "type": "string", "description": "Instead of repo: the workspace's slug, for all of it (or its own, for patterns)." });
235+ properties
236+ };
237+ let secret_id = || json!({ "type": "string", "description": "The alert's id: sec_…" });
238+ let number = || json!({ "type": "integer", "description": "The code scanning alert's number." });
239+ let state = || json!({ "type": "string", "enum": ["open", "dismissed", "fixed"], "description": "Only alerts in this state." });
240+ let severity = || json!({ "type": "string", "enum": ["critical", "high", "medium", "low", "unknown"], "description": "Only alerts of this severity." });
241+ let set_state = || json!({ "type": "string", "enum": ["open", "dismissed"], "description": "dismissed, with a reason, or open to reopen." });
242+ let comment = || json!({ "type": "string", "description": "Why, in a sentence; kept with the alert. At most 500 characters." });
243+ let pattern_fields = |mut properties: Value| {
244+ properties["pattern_name"] = json!({ "type": "string", "description": "What people call it: \"Acme API key\"." });
245+ properties["pattern"] = json!({ "type": "string", "description": "The secret's format, as a regular expression (the regex crate's syntax: no look-around or back-references). At most 1,000 characters; it may not match an empty string." });
246+ properties["before"] = json!({ "type": "string", "description": "What must come right before the secret, as a regular expression. Default: the start of the line or a character that is not a letter or digit." });
247+ properties["after"] = json!({ "type": "string", "description": "What must come right after it. Default: the end of the line or a character that is not a letter or digit." });
248+ properties
249+ };
250+ let workspace = || json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." });
251+ let (properties, required): (Value, &[&str]) = match self {
252+ SecurityOp::ListSecretAlerts => (
253+ either(json!({
254+ "state": state(),
255+ "secret_type": { "type": "string", "description": "Only this kind of secret: aws_access_key, github_token, custom_pattern, …" },
256+ "validity": { "type": "string", "enum": ["active", "inactive", "unknown", "unsupported"], "description": "Only alerts whose issuer said this when last asked." },
257+ "bypassed": { "type": "boolean", "description": "Only alerts someone bypassed push protection for (true), or not (false)." },
258+ })),
259+ &[],
260+ ),
261+ SecurityOp::GetSecretAlert | SecurityOp::ListSecretLocations | SecurityOp::CheckSecretValidity => {
262+ (json!({ "repo": repo(), "id": secret_id() }), &["repo", "id"])
263+ }
264+ SecurityOp::UpdateSecretAlert => (
265+ json!({
266+ "repo": repo(),
267+ "id": secret_id(),
268+ "state": set_state(),
269+ "reason": { "type": "string", "enum": ["false_positive", "used_in_tests", "revoked", "wont_fix"], "description": "Why it is dismissed. revoked marks it fixed." },
270+ "comment": comment(),
271+ }),
272+ &["repo", "id", "state"],
273+ ),
274+ SecurityOp::BypassPushProtection => (
275+ json!({
276+ "repo": repo(),
277+ "id": secret_id(),
278+ "reason": { "type": "string", "enum": BypassReason::ALL.map(BypassReason::as_str), "description": "false_positive: not a secret. used_in_tests: a value for tests. will_fix_later: real, to be rotated (the alert stays open)." },
279+ "comment": comment(),
280+ }),
281+ &["repo", "id", "reason"],
282+ ),
283+ SecurityOp::ListBypassRequests => (
284+ json!({
285+ "workspace": workspace(),
286+ "repo": { "type": "string", "description": "Only this repository's, as \"owner/name\"." },
287+ "state": { "type": "string", "enum": ["pending", "approved", "denied", "cancelled"], "description": "Only requests in this state." },
288+ }),
289+ &["workspace"],
290+ ),
291+ SecurityOp::ReviewBypassRequest => (
292+ json!({
293+ "workspace": workspace(),
294+ "id": { "type": "string", "description": "The request's id: byp_…" },
295+ "decision": { "type": "string", "enum": ["approve", "deny", "cancel"], "description": "approve or deny (reviewers), or cancel (your own)." },
296+ "comment": comment(),
297+ }),
298+ &["workspace", "id", "decision"],
299+ ),
300+ SecurityOp::ListCustomPatterns => (either(json!({})), &[]),
301+ SecurityOp::CreateCustomPattern => (
302+ either(pattern_fields(json!({
303+ "test_strings": { "type": "array", "items": { "type": "string" }, "description": "Up to 20 strings to show the pattern working on." },
304+ "publish": { "type": "boolean", "description": "Use it in push protection and scans now (true), or keep a draft (false, the default)." },
305+ }))),
306+ &["pattern_name", "pattern"],
307+ ),
308+ SecurityOp::UpdateCustomPattern => (
309+ either(pattern_fields(json!({
310+ "id": { "type": "string", "description": "The pattern's id: pat_…" },
311+ "test_strings": { "type": "array", "items": { "type": "string" }, "description": "Up to 20 strings to show the pattern working on." },
312+ "publish": { "type": "boolean", "description": "Published (true) or a draft (false)." },
313+ }))),
314+ &["id", "pattern_name", "pattern"],
315+ ),
316+ SecurityOp::DeleteCustomPattern => (either(json!({ "id": { "type": "string", "description": "The pattern's id: pat_…" } })), &["id"]),
317+ SecurityOp::DryRunCustomPattern => (
318+ either(pattern_fields(json!({
319+ "repos": { "type": "array", "items": { "type": "string" }, "description": "With workspace: repository names to run it on; the first ten when empty." },
320+ }))),
321+ &["pattern"],
322+ ),
323+ SecurityOp::ListCodeAlerts => (
324+ either(json!({
325+ "state": state(),
326+ "severity": severity(),
327+ "tool": { "type": "string", "description": "Only this tool's: \"ESLint\"." },
328+ "rule_id": { "type": "string", "description": "Only this rule's." },
329+ })),
330+ &[],
331+ ),
332+ SecurityOp::GetCodeAlert => (json!({ "repo": repo(), "number": number() }), &["repo", "number"]),
333+ SecurityOp::UpdateCodeAlert => (
334+ json!({
335+ "repo": repo(),
336+ "number": number(),
337+ "state": set_state(),
338+ "dismissed_reason": { "type": "string", "enum": ["false_positive", "wont_fix", "used_in_tests"], "description": "Why it is dismissed." },
339+ "dismissed_comment": comment(),
340+ }),
341+ &["repo", "number", "state"],
342+ ),
343+ SecurityOp::ListAnalyses => (json!({ "repo": repo() }), &["repo"]),
344+ SecurityOp::UploadSarif => (
345+ json!({
346+ "repo": repo(),
347+ "commit_sha": { "type": "string", "description": "The full hash of the commit analysed." },
348+ "ref": { "type": "string", "description": "refs/heads/<branch>, or refs/pull/<number>/head (or /merge) for a pull request." },
349+ "sarif": { "type": "string", "description": "The SARIF 2.1.0 file, gzipped, then base64-encoded. At most 10 MB encoded and 40 MB unzipped." },
350+ "tool_name": { "type": "string", "description": "The tool's name, when the file has one run and you want another name for it." },
351+ "category": { "type": "string", "description": "Which analysis this is, when a repository runs several of one tool. Default: the run's automationDetails.id, or the tool's name." },
352+ "checkout_uri": { "type": "string", "description": "Where the files were checked out (file:///home/runner/work/repo), so absolute paths become repository paths." },
353+ }),
354+ &["repo", "commit_sha", "ref", "sarif"],
355+ ),
356+ SecurityOp::GetSarifUpload => (json!({ "repo": repo(), "id": { "type": "string", "description": "The upload's id: sar_…" } }), &["repo", "id"]),
357+ SecurityOp::ListVulnerabilityAlerts => (
358+ either(json!({
359+ "state": state(),
360+ "severity": severity(),
361+ "ecosystem": { "type": "string", "description": "Only this ecosystem's: npm, crates.io, Go or PyPI." },
362+ "package": { "type": "string", "description": "Only this package's." },
363+ })),
364+ &[],
365+ ),
366+ SecurityOp::GetVulnerabilityAlert => (json!({ "repo": repo(), "id": { "type": "string", "description": "The alert's id: vul_…" } }), &["repo", "id"]),
367+ SecurityOp::UpdateVulnerabilityAlert => (
368+ json!({
369+ "repo": repo(),
370+ "id": { "type": "string", "description": "The alert's id: vul_…" },
371+ "state": set_state(),
372+ "reason": { "type": "string", "enum": ["fix_started", "no_bandwidth", "tolerable_risk", "inaccurate", "not_used"], "description": "Why it is dismissed." },
373+ "comment": comment(),
374+ }),
375+ &["repo", "id", "state"],
376+ ),
377+ SecurityOp::FixAlert => (
378+ json!({ "repo": repo(), "id": { "type": "string", "description": "The alert's id: cod_…, vul_… or sec_…" } }),
379+ &["repo", "id"],
380+ ),
381+ SecurityOp::GetDependencyGraph | SecurityOp::GetSbom | SecurityOp::GetSettings => (json!({ "repo": repo() }), &["repo"]),
382+ SecurityOp::CompareDependencies => (
383+ json!({
384+ "repo": repo(),
385+ "basehead": { "type": "string", "description": "base...head: two commits, branches or tags, e.g. main...my-branch." },
386+ }),
387+ &["repo", "basehead"],
388+ ),
389+ SecurityOp::UpdateSettings => (
390+ json!({
391+ "repo": repo(),
392+ "code_scanning_gate": { "type": "string", "enum": ["none", "errors", "critical", "high", "medium", "any"], "description": "When a pull request's Code scanning check fails: never, on errors, or on new results of this security severity or worse (and errors)." },
393+ "dependency_review": { "type": "boolean", "description": "Whether pull requests get the Dependency review check." },
394+ "review_fail_on": { "type": "string", "enum": ["critical", "high", "medium", "low", "none"], "description": "The lowest severity of a known vulnerability in an added package that fails the review." },
395+ "review_deny_licenses": { "type": "array", "items": { "type": "string" }, "description": "SPDX license ids an added package may not have." },
396+ "review_comment": { "type": "boolean", "description": "Whether the review comments its summary on the pull request." },
397+ }),
398+ &["repo"],
399+ ),
400+ SecurityOp::GetWorkspaceSettings => (json!({ "workspace": workspace() }), &["workspace"]),
401+ SecurityOp::UpdateWorkspaceSettings => (
402+ json!({
403+ "workspace": workspace(),
404+ "delegated_bypass": { "type": "boolean", "description": "Bypasses need an owner's or the repository's admins' approval." },
405+ "validity_checks": { "type": "boolean", "description": "Ask issuers whether secrets still work, where that can be done safely." },
406+ }),
407+ &["workspace"],
408+ ),
409+ SecurityOp::GetOverview => (
410+ json!({ "workspace": workspace(), "days": { "type": "integer", "description": "Days of trend, 7 to 90. Default 30." } }),
411+ &["workspace"],
412+ ),
413+ };
414+ let mut schema = json!({ "type": "object", "properties": properties });
415+ if !required.is_empty() {
416+ schema["required"] = json!(required);
417+ }
418+ schema
419+ }
420+}
421+
422+fn failed(code: FailureCode, message: &str) -> Result<Outcome<Value>> {
423+ Ok(Outcome::fail(code, message))
424+}
425+
426+fn ok<T: Serialize>(value: &T) -> Result<Outcome<Value>> {
427+ Ok(Outcome::Ok(serde_json::to_value(value)?))
428+}
429+
430+fn text(input: &Value, key: &str) -> Option<String> {
431+ input[key].as_str().map(str::trim).filter(|value| !value.is_empty()).map(str::to_owned)
432+}
433+
434+fn flag(input: &Value, key: &str) -> Option<bool> {
435+ match &input[key] {
436+ Value::Bool(value) => Some(*value),
437+ Value::String(text) => match text.trim() {
438+ "true" | "1" => Some(true),
439+ "false" | "0" => Some(false),
440+ _ => None,
441+ },
442+ _ => None,
443+ }
444+}
445+
446+fn whole(input: &Value, key: &str) -> Option<u32> {
447+ match &input[key] {
448+ Value::Number(number) => number.as_u64().and_then(|n| u32::try_from(n).ok()),
449+ Value::String(digits) => digits.trim().parse().ok(),
450+ _ => None,
451+ }
452+}
453+
454+fn strings(input: &Value, key: &str) -> Vec<String> {
455+ input[key].as_array().map(|items| items.iter().filter_map(|item| item.as_str().map(str::to_owned)).collect()).unwrap_or_default()
456+}
457+
458+/// One of `allowed`, or why not.
459+fn one_of(input: &Value, key: &str, allowed: &[&str]) -> std::result::Result<Option<String>, String> {
460+ match text(input, key) {
461+ None => Ok(None),
462+ Some(given) => {
463+ let lower = given.to_lowercase();
464+ if allowed.contains(&lower.as_str()) {
465+ Ok(Some(lower))
466+ } else {
467+ Err(format!("{key} is {}, not {given}.", allowed.join(", ")))
468+ }
469+ }
470+ }
471+}
472+
473+/// Filters for secret scanning alerts.
474+#[derive(Debug, Default, PartialEq)]
475+pub(crate) struct SecretFilters {
476+ pub state: Option<AlertState>,
477+ pub secret_type: Option<String>,
478+ pub validity: Option<String>,
479+ pub bypassed: Option<bool>,
480+}
481+
482+pub(crate) fn secret_filters(input: &Value) -> std::result::Result<SecretFilters, String> {
483+ Ok(SecretFilters {
484+ state: one_of(input, "state", &["open", "dismissed", "fixed"])?.and_then(|state| AlertState::parse(&state)),
485+ secret_type: text(input, "secret_type"),
486+ validity: one_of(input, "validity", &["active", "inactive", "unknown", "unsupported"])?,
487+ bypassed: match &input["bypassed"] {
488+ Value::Null => None,
489+ _ => Some(flag(input, "bypassed").ok_or("bypassed is true or false.")?),
490+ },
491+ })
492+}
493+
494+impl SecretFilters {
495+ pub(crate) fn keeps(&self, secret: &SecretFinding) -> bool {
496+ self.state.is_none_or(|state| secret.state == state)
497+ && self.secret_type.as_deref().is_none_or(|kind| secret.kind == kind)
498+ && self.validity.as_deref().is_none_or(|validity| secret.validity.as_deref().unwrap_or("unknown") == validity)
499+ && self.bypassed.is_none_or(|bypassed| secret.bypass.is_some() == bypassed)
500+ }
501+}
502+
503+/// Filters for code scanning alerts.
504+#[derive(Debug, Default, PartialEq)]
505+pub(crate) struct CodeFilters {
506+ pub state: Option<AlertState>,
507+ pub severity: Option<String>,
508+ pub tool: Option<String>,
509+ pub rule_id: Option<String>,
510+}
511+
512+pub(crate) fn code_filters(input: &Value) -> std::result::Result<CodeFilters, String> {
513+ Ok(CodeFilters {
514+ state: one_of(input, "state", &["open", "dismissed", "fixed"])?.and_then(|state| AlertState::parse(&state)),
515+ severity: one_of(input, "severity", &["critical", "high", "medium", "low", "unknown"])?,
516+ tool: text(input, "tool"),
517+ rule_id: text(input, "rule_id"),
518+ })
519+}
520+
521+impl CodeFilters {
522+ pub(crate) fn keeps(&self, alert: &CodeAlert) -> bool {
523+ self.state.is_none_or(|state| alert.state == state)
524+ && self.severity.as_deref().is_none_or(|severity| alert.severity == severity)
525+ && self.tool.as_deref().is_none_or(|tool| alert.tool.eq_ignore_ascii_case(tool))
526+ && self.rule_id.as_deref().is_none_or(|rule| alert.rule_id == rule)
527+ }
528+}
529+
530+/// Filters for vulnerability alerts.
531+#[derive(Debug, Default, PartialEq)]
532+pub(crate) struct VulnerabilityFilters {
533+ pub state: Option<AlertState>,
534+ pub severity: Option<String>,
535+ pub ecosystem: Option<String>,
536+ pub package: Option<String>,
537+}
538+
539+pub(crate) fn vulnerability_filters(input: &Value) -> std::result::Result<VulnerabilityFilters, String> {
540+ Ok(VulnerabilityFilters {
541+ state: one_of(input, "state", &["open", "dismissed", "fixed"])?.and_then(|state| AlertState::parse(&state)),
542+ severity: one_of(input, "severity", &["critical", "high", "medium", "low", "unknown"])?,
543+ ecosystem: text(input, "ecosystem"),
544+ package: text(input, "package"),
545+ })
546+}
547+
548+impl VulnerabilityFilters {
549+ pub(crate) fn keeps(&self, vuln: &Vulnerability) -> bool {
550+ self.state.is_none_or(|state| vuln.state == state)
551+ && self.severity.as_deref().is_none_or(|severity| vuln.severity == severity)
552+ && self.ecosystem.as_deref().is_none_or(|ecosystem| vuln.ecosystem.eq_ignore_ascii_case(ecosystem))
553+ && self.package.as_deref().is_none_or(|package| vuln.package == package)
554+ }
555+}
556+
557+/// `base...head` (or `base..head`), as compare_dependencies reads it.
558+pub(crate) fn base_head(text: &str) -> Option<(String, String)> {
559+ let (base, head) = text.split_once("...").or_else(|| text.split_once(".."))?;
560+ let (base, head) = (base.trim(), head.trim());
561+ (!base.is_empty() && !head.is_empty()).then(|| (base.to_owned(), head.to_owned()))
562+}
563+
564+/// What dismissing or reopening an alert of `kind` asks: the reason, if
565+/// dismissing, checked against the reasons that kind takes.
566+pub(crate) fn state_change(input: &Value, reason_key: &str, reasons: &[DismissReason]) -> std::result::Result<Option<DismissReason>, String> {
567+ match text(input, "state").as_deref() {
568+ Some("open") => Ok(None),
569+ Some("dismissed") => {
570+ let names: Vec<&str> = reasons.iter().map(|reason| reason.as_str()).collect();
571+ let given = text(input, reason_key).ok_or_else(|| format!("Give {reason_key}: one of {}.", names.join(", ")))?;
572+ DismissReason::parse(&given)
573+ .filter(|reason| reasons.contains(reason))
574+ .map(Some)
575+ .ok_or_else(|| format!("{reason_key} is {}, not {given}.", names.join(", ")))
576+ }
577+ _ => Err("state is open or dismissed.".to_owned()),
578+ }
579+}
580+
581+const SECRET_REASONS: [DismissReason; 4] = [DismissReason::FalsePositive, DismissReason::UsedInTests, DismissReason::Revoked, DismissReason::WontFix];
582+const CODE_REASONS: [DismissReason; 3] = [DismissReason::FalsePositive, DismissReason::WontFix, DismissReason::UsedInTests];
583+const DEPENDENCY_REASONS: [DismissReason; 5] = [
584+ DismissReason::FixStarted,
585+ DismissReason::NoBandwidth,
586+ DismissReason::TolerableRisk,
587+ DismissReason::Inaccurate,
588+ DismissReason::NotUsed,
589+];
590+
591+async fn call<A: Serialize, T: DeserializeOwned>(services: &Services, method: &str, args: &A) -> Result<Outcome<T>> {
592+ g1t_kit::call(&services.security, method, args).await
593+}
594+
595+/// Passes a service's outcome through as it is.
596+async fn pass<A: Serialize>(services: &Services, method: &str, args: &A) -> Result<Outcome<Value>> {
597+ call(services, method, args).await
598+}
599+
600+fn path_of(input: &Value) -> Option<RepoPath> {
601+ crate::operations::repo_path(input)
602+}
603+
604+pub async fn run(op: SecurityOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
605+ let actor = || viewer.clone().unwrap_or_default();
606+ let repo = path_of(input);
607+ let workspace = text(input, "workspace").map(|slug| slug.to_lowercase());
608+ let need_repo = || "Give the repository as \"owner/name\".".to_owned();
609+ // Operations on a repository or a workspace: which.
610+ let scope_repo = repo.clone();
611+ let scope_workspace = || workspace.clone().or_else(|| repo.as_ref().map(|repo| repo.namespace.to_lowercase()));
612+ match op {
613+ SecurityOp::ListSecretAlerts => {
614+ let filters = match secret_filters(input) {
615+ Ok(filters) => filters,
616+ Err(message) => return failed(FailureCode::Invalid, &message),
617+ };
618+ match (scope_repo, workspace) {
619+ (Some(repo), _) => {
620+ let overview: Outcome<SecurityOverview> =
621+ call(services, "overview", &g1t_contracts::security::OverviewArgs { repo, viewer: viewer.clone() }).await?;
622+ match overview {
623+ Outcome::Ok(overview) => {
624+ let alerts: Vec<SecretFinding> = overview.secrets.into_iter().filter(|secret| filters.keeps(secret)).collect();
625+ ok(&alerts)
626+ }
627+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
628+ }
629+ }
630+ (None, Some(workspace)) => {
631+ let found: Outcome<Vec<WorkspaceAlert>> = call(
632+ services,
633+ "workspace_alerts",
634+ &WorkspaceAlertsArgs { viewer: viewer.clone(), workspace, alert_type: AlertType::SecretScanning },
635+ )
636+ .await?;
637+ match found {
638+ Outcome::Ok(found) => {
639+ let alerts: Vec<WorkspaceAlert> =
640+ found.into_iter().filter(|alert| alert.secret.as_ref().is_some_and(|secret| filters.keeps(secret))).collect();
641+ ok(&alerts)
642+ }
643+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
644+ }
645+ }
646+ (None, None) => failed(FailureCode::Invalid, "Give repo, or workspace for all of one."),
647+ }
648+ }
649+ SecurityOp::GetSecretAlert | SecurityOp::ListSecretLocations => {
650+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
651+ let id = text(input, "id").unwrap_or_default();
652+ let detail: Outcome<SecretAlertDetail> = call(services, "secret_alert", &SecretAlertArgs { viewer: viewer.clone(), repo, id }).await?;
653+ match (detail, op) {
654+ (Outcome::Ok(detail), SecurityOp::ListSecretLocations) => ok(&detail.locations),
655+ (Outcome::Ok(detail), _) => ok(&detail),
656+ (Outcome::Fail(failure), _) => Ok(Outcome::Fail(failure)),
657+ }
658+ }
659+ SecurityOp::UpdateSecretAlert | SecurityOp::UpdateVulnerabilityAlert => {
660+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
661+ let id = text(input, "id").unwrap_or_default();
662+ let (reasons, wants): (&[DismissReason], &str) = match op {
663+ SecurityOp::UpdateSecretAlert => (&SECRET_REASONS, "sec_"),
664+ _ => (&DEPENDENCY_REASONS, "vul_"),
665+ };
666+ if !id.starts_with(wants) {
667+ return failed(FailureCode::NotFound, "No such alert.");
668+ }
669+ let reason = match state_change(input, "reason", reasons) {
670+ Ok(reason) => reason,
671+ Err(message) => return failed(FailureCode::Invalid, &message),
672+ };
673+ let changed: Outcome<AlertChange> = match reason {
674+ Some(reason) => {
675+ let comment = text(input, "comment").unwrap_or_default();
676+ call(services, "dismiss", &DismissArgs { actor: actor(), repo, id, reason, comment }).await?
677+ }
678+ None => call(services, "reopen", &ReopenArgs { actor: actor(), repo, id }).await?,
679+ };
680+ match changed {
681+ Outcome::Ok(AlertChange { secret: Some(secret), .. }) => ok(&secret),
682+ Outcome::Ok(AlertChange { vulnerability: Some(vuln), .. }) => ok(&vuln),
683+ Outcome::Ok(_) => failed(FailureCode::NotFound, "No such alert."),
684+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
685+ }
686+ }
687+ SecurityOp::BypassPushProtection => {
688+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
689+ let Some(reason) = text(input, "reason").as_deref().and_then(BypassReason::parse) else {
690+ return failed(FailureCode::Invalid, "reason is false_positive, used_in_tests or will_fix_later.");
691+ };
692+ let args = BypassArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default(), reason, comment: text(input, "comment").unwrap_or_default() };
693+ pass(services, "bypass", &args).await
694+ }
695+ SecurityOp::CheckSecretValidity => {
696+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
697+ pass(services, "check_validity", &CheckValidityArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default() }).await
698+ }
699+ SecurityOp::ListBypassRequests => {
700+ let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") };
701+ let state = match one_of(input, "state", &["pending", "approved", "denied", "cancelled"]) {
702+ Ok(state) => state,
703+ Err(message) => return failed(FailureCode::Invalid, &message),
704+ };
705+ pass(services, "bypass_requests", &BypassRequestsArgs { viewer: viewer.clone(), workspace, repo, state }).await
706+ }
707+ SecurityOp::ReviewBypassRequest => {
708+ let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") };
709+ let decision = match one_of(input, "decision", &["approve", "deny", "cancel"]) {
710+ Ok(Some(decision)) => decision,
711+ Ok(None) => return failed(FailureCode::Invalid, "decision is approve, deny or cancel."),
712+ Err(message) => return failed(FailureCode::Invalid, &message),
713+ };
714+ let args = ReviewBypassArgs {
715+ actor: actor(),
716+ workspace,
717+ id: text(input, "id").unwrap_or_default(),
718+ decision,
719+ comment: text(input, "comment").unwrap_or_default(),
720+ };
721+ pass(services, "review_bypass", &args).await
722+ }
723+ SecurityOp::ListCustomPatterns => {
724+ let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") };
725+ pass(services, "custom_patterns", &CustomPatternsArgs { viewer: viewer.clone(), workspace, repo: scope_repo }).await
726+ }
727+ SecurityOp::CreateCustomPattern | SecurityOp::UpdateCustomPattern => {
728+ let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") };
729+ let args = SaveCustomPatternArgs {
730+ actor: actor(),
731+ workspace,
732+ repo: scope_repo,
733+ id: if op == SecurityOp::UpdateCustomPattern { text(input, "id") } else { None },
734+ name: text(input, "pattern_name").unwrap_or_default(),
735+ pattern: input["pattern"].as_str().unwrap_or_default().to_owned(),
736+ before: text(input, "before"),
737+ after: text(input, "after"),
738+ test_strings: strings(input, "test_strings"),
739+ publish: flag(input, "publish").unwrap_or(false),
740+ };
741+ pass(services, "save_custom_pattern", &args).await
742+ }
743+ SecurityOp::DeleteCustomPattern => {
744+ let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") };
745+ let args = DeleteCustomPatternArgs { actor: actor(), workspace, repo: scope_repo, id: text(input, "id").unwrap_or_default() };
746+ match call::<_, bool>(services, "delete_custom_pattern", &args).await? {
747+ Outcome::Ok(_) => ok(&json!({ "deleted": true })),
748+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
749+ }
750+ }
751+ SecurityOp::DryRunCustomPattern => {
752+ let Some(workspace) = scope_workspace() else { return failed(FailureCode::Invalid, "Give repo or workspace.") };
753+ let args = DryRunPatternArgs {
754+ actor: actor(),
755+ workspace,
756+ repo: scope_repo,
757+ repos: strings(input, "repos"),
758+ pattern: input["pattern"].as_str().unwrap_or_default().to_owned(),
759+ before: text(input, "before"),
760+ after: text(input, "after"),
761+ };
762+ pass(services, "dry_run_pattern", &args).await
763+ }
764+ SecurityOp::ListCodeAlerts => {
765+ let filters = match code_filters(input) {
766+ Ok(filters) => filters,
767+ Err(message) => return failed(FailureCode::Invalid, &message),
768+ };
769+ match (scope_repo, workspace) {
770+ (Some(repo), _) => match call::<_, CodeScanning>(services, "code_scanning", &CodeScanningArgs { viewer: viewer.clone(), repo }).await? {
771+ Outcome::Ok(scanning) => {
772+ let alerts: Vec<CodeAlert> = scanning.alerts.into_iter().filter(|alert| filters.keeps(alert)).collect();
773+ ok(&alerts)
774+ }
775+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
776+ },
777+ (None, Some(workspace)) => {
778+ let found: Outcome<Vec<WorkspaceAlert>> = call(
779+ services,
780+ "workspace_alerts",
781+ &WorkspaceAlertsArgs { viewer: viewer.clone(), workspace, alert_type: AlertType::CodeScanning },
782+ )
783+ .await?;
784+ match found {
785+ Outcome::Ok(found) => {
786+ let alerts: Vec<WorkspaceAlert> =
787+ found.into_iter().filter(|alert| alert.code.as_ref().is_some_and(|code| filters.keeps(code))).collect();
788+ ok(&alerts)
789+ }
790+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
791+ }
792+ }
793+ (None, None) => failed(FailureCode::Invalid, "Give repo, or workspace for all of one."),
794+ }
795+ }
796+ SecurityOp::GetCodeAlert => {
797+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
798+ pass(services, "code_alert", &CodeAlertArgs { viewer: viewer.clone(), repo, number: whole(input, "number").unwrap_or(0) }).await
799+ }
800+ SecurityOp::UpdateCodeAlert => {
801+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
802+ let reason = match state_change(input, "dismissed_reason", &CODE_REASONS) {
803+ Ok(reason) => reason,
804+ Err(message) => return failed(FailureCode::Invalid, &message),
805+ };
806+ let args = SetCodeAlertStateArgs {
807+ actor: actor(),
808+ repo,
809+ number: whole(input, "number").unwrap_or(0),
810+ state: if reason.is_some() { AlertState::Dismissed } else { AlertState::Open },
811+ reason,
812+ comment: text(input, "dismissed_comment").unwrap_or_default(),
813+ };
814+ pass(services, "set_code_alert_state", &args).await
815+ }
816+ SecurityOp::ListAnalyses => {
817+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
818+ match call::<_, CodeScanning>(services, "code_scanning", &CodeScanningArgs { viewer: viewer.clone(), repo }).await? {
819+ Outcome::Ok(scanning) => ok(&scanning.analyses),
820+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
821+ }
822+ }
823+ SecurityOp::UploadSarif => {
824+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
825+ let (Some(commit_sha), Some(git_ref), Some(sarif)) = (text(input, "commit_sha"), text(input, "ref"), text(input, "sarif")) else {
826+ return failed(FailureCode::Invalid, "Give commit_sha, ref and sarif (the file gzipped and base64-encoded).");
827+ };
828+ if sarif.len() > g1t_scan_limits::MAX_UPLOAD_BYTES {
829+ return failed(FailureCode::Invalid, "The upload is larger than 10 MB.");
830+ }
831+ let args = UploadSarifArgs {
832+ actor: actor(),
833+ repo,
834+ commit_sha,
835+ git_ref,
836+ sarif,
837+ tool_name: text(input, "tool_name"),
838+ category: text(input, "category"),
839+ checkout_uri: text(input, "checkout_uri"),
840+ };
841+ pass(services, "upload_sarif", &args).await
842+ }
843+ SecurityOp::GetSarifUpload => {
844+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
845+ pass(services, "sarif_status", &SarifStatusArgs { viewer: viewer.clone(), repo, id: text(input, "id").unwrap_or_default() }).await
846+ }
847+ SecurityOp::ListVulnerabilityAlerts => {
848+ let filters = match vulnerability_filters(input) {
849+ Ok(filters) => filters,
850+ Err(message) => return failed(FailureCode::Invalid, &message),
851+ };
852+ match (scope_repo, workspace) {
853+ (Some(repo), _) => {
854+ let overview: Outcome<SecurityOverview> =
855+ call(services, "overview", &g1t_contracts::security::OverviewArgs { repo, viewer: viewer.clone() }).await?;
856+ match overview {
857+ Outcome::Ok(overview) => {
858+ let alerts: Vec<Vulnerability> = overview.vulnerabilities.into_iter().filter(|vuln| filters.keeps(vuln)).collect();
859+ ok(&alerts)
860+ }
861+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
862+ }
863+ }
864+ (None, Some(workspace)) => {
865+ let found: Outcome<Vec<WorkspaceAlert>> = call(
866+ services,
867+ "workspace_alerts",
868+ &WorkspaceAlertsArgs { viewer: viewer.clone(), workspace, alert_type: AlertType::Vulnerability },
869+ )
870+ .await?;
871+ match found {
872+ Outcome::Ok(found) => {
873+ let alerts: Vec<WorkspaceAlert> =
874+ found.into_iter().filter(|alert| alert.vulnerability.as_ref().is_some_and(|vuln| filters.keeps(vuln))).collect();
875+ ok(&alerts)
876+ }
877+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
878+ }
879+ }
880+ (None, None) => failed(FailureCode::Invalid, "Give repo, or workspace for all of one."),
881+ }
882+ }
883+ SecurityOp::GetVulnerabilityAlert => {
884+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
885+ let id = text(input, "id").unwrap_or_default();
886+ let overview: Outcome<SecurityOverview> =
887+ call(services, "overview", &g1t_contracts::security::OverviewArgs { repo, viewer: viewer.clone() }).await?;
888+ match overview {
889+ Outcome::Ok(overview) => match overview.vulnerabilities.into_iter().find(|vuln| vuln.id == id) {
890+ Some(vuln) => ok(&vuln),
891+ None => failed(FailureCode::NotFound, "No such alert."),
892+ },
893+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
894+ }
895+ }
896+ SecurityOp::FixAlert => {
897+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
898+ pass(services, "fix_alert", &FixAlertArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default() }).await
899+ }
900+ SecurityOp::GetDependencyGraph => {
901+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
902+ pass(services, "dependency_graph", &DependencyGraphArgs { viewer: viewer.clone(), repo }).await
903+ }
904+ SecurityOp::GetSbom => {
905+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
906+ // The document goes out as SPDX spells it: `sbom` is passed
907+ // through untouched (g1t_kit::wire::USER_KEYED).
908+ match call::<_, Value>(services, "sbom", &SbomArgs { viewer: viewer.clone(), repo }).await? {
909+ Outcome::Ok(document) => ok(&json!({ "sbom": document })),
910+ Outcome::Fail(failure) => Ok(Outcome::Fail(failure)),
911+ }
912+ }
913+ SecurityOp::CompareDependencies => {
914+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
915+ let Some((base, head)) = text(input, "basehead").as_deref().and_then(base_head) else {
916+ return failed(FailureCode::Invalid, "basehead is base...head, e.g. main...my-branch.");
917+ };
918+ pass(services, "dependency_review", &DependencyReviewArgs { viewer: viewer.clone(), repo, base, head }).await
919+ }
920+ SecurityOp::GetSettings => {
921+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
922+ pass(services, "security_settings", &SecuritySettingsArgs { viewer: viewer.clone(), repo }).await
923+ }
924+ SecurityOp::UpdateSettings => {
925+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
926+ // What is not given stays as it is.
927+ let current: Outcome<SecuritySettingsView> =
928+ call(services, "security_settings", &SecuritySettingsArgs { viewer: viewer.clone(), repo: repo.clone() }).await?;
929+ let mut settings = match current {
930+ Outcome::Ok(view) => view.settings,
931+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
932+ };
933+ if let Some(gate) = text(input, "code_scanning_gate") {
934+ settings.code_scanning_gate = gate.to_lowercase();
935+ }
936+ if let Some(on) = flag(input, "dependency_review") {
937+ settings.dependency_review = on;
938+ }
939+ if let Some(fail_on) = text(input, "review_fail_on") {
940+ settings.review_fail_on = fail_on.to_lowercase();
941+ }
942+ if input["review_deny_licenses"].is_array() {
943+ settings.review_deny_licenses = strings(input, "review_deny_licenses");
944+ }
945+ if let Some(on) = flag(input, "review_comment") {
946+ settings.review_comment = on;
947+ }
948+ pass(services, "set_security_settings", &SetSecuritySettingsArgs { actor: actor(), repo, settings }).await
949+ }
950+ SecurityOp::GetWorkspaceSettings => {
951+ let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") };
952+ pass(services, "workspace_security_settings", &WorkspaceSecuritySettingsArgs { viewer: viewer.clone(), workspace }).await
953+ }
954+ SecurityOp::UpdateWorkspaceSettings => {
955+ let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") };
956+ let current: Outcome<WorkspaceSecurityView> =
957+ call(services, "workspace_security_settings", &WorkspaceSecuritySettingsArgs { viewer: viewer.clone(), workspace: workspace.clone() }).await?;
958+ let mut settings = match current {
959+ Outcome::Ok(view) => view.settings,
960+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
961+ };
962+ if let Some(on) = flag(input, "delegated_bypass") {
963+ settings.delegated_bypass = on;
964+ }
965+ if let Some(on) = flag(input, "validity_checks") {
966+ settings.validity_checks = on;
967+ }
968+ pass(services, "set_workspace_security_settings", &SetWorkspaceSecuritySettingsArgs { actor: actor(), workspace, settings }).await
969+ }
970+ SecurityOp::GetOverview => {
971+ let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") };
972+ pass(services, "security_overview", &WorkspaceOverviewArgs { viewer: viewer.clone(), workspace, days: whole(input, "days") }).await
973+ }
974+ }
975+}
976+
977+/// Limits the API checks before passing an upload on.
978+mod g1t_scan_limits {
979+ /// As the security service's: 10 MB gzipped and base64-encoded.
980+ pub const MAX_UPLOAD_BYTES: usize = 10 * 1024 * 1024;
981+}
982+
983+#[cfg(test)]
984+mod tests {
985+ use super::*;
986+
987+ #[test]
988+ fn every_one_is_an_operation() {
989+ for op in SecurityOp::ALL {
990+ assert!(crate::operations::Op::ALL.contains(&crate::operations::Op::Security(op)), "{}", op.name());
991+ }
992+ }
993+
994+ #[test]
995+ fn names_are_unique_and_found_again() {
996+ let mut names: Vec<&str> = SecurityOp::ALL.iter().map(|op| op.name()).collect();
997+ names.sort();
998+ names.dedup();
999+ assert_eq!(names.len(), SecurityOp::ALL.len());
1000+ }
1001+
1002+ #[test]
1003+ fn secret_filters_are_read_as_words() {
1004+ let filters = secret_filters(&json!({ "state": "OPEN", "validity": "active", "bypassed": "true", "secret_type": "github_token" })).unwrap();
1005+ assert_eq!(filters.state, Some(AlertState::Open));
1006+ assert_eq!(filters.validity.as_deref(), Some("active"));
1007+ assert_eq!(filters.bypassed, Some(true));
1008+ assert!(secret_filters(&json!({ "validity": "maybe" })).unwrap_err().contains("validity is active, inactive"));
1009+ assert!(secret_filters(&json!({ "bypassed": "perhaps" })).is_err());
1010+ assert_eq!(secret_filters(&json!({})).unwrap(), SecretFilters::default());
1011+ }
1012+
1013+ #[test]
1014+ fn a_state_change_needs_a_reason_its_kind_takes() {
1015+ assert_eq!(state_change(&json!({ "state": "open" }), "reason", &SECRET_REASONS), Ok(None));
1016+ assert_eq!(
1017+ state_change(&json!({ "state": "dismissed", "reason": "revoked" }), "reason", &SECRET_REASONS),
1018+ Ok(Some(DismissReason::Revoked))
1019+ );
1020+ assert!(state_change(&json!({ "state": "dismissed", "reason": "not_used" }), "reason", &SECRET_REASONS).unwrap_err().contains("reason is false_positive"));
1021+ assert!(state_change(&json!({ "state": "dismissed" }), "dismissed_reason", &CODE_REASONS).unwrap_err().starts_with("Give dismissed_reason"));
1022+ assert!(state_change(&json!({ "state": "fixed" }), "reason", &CODE_REASONS).is_err());
1023+ }
1024+
1025+ #[test]
1026+ fn base_and_head_are_split_at_the_dots() {
1027+ assert_eq!(base_head("main...feature/x"), Some(("main".into(), "feature/x".into())));
1028+ assert_eq!(base_head("v1.0..v1.1"), Some(("v1.0".into(), "v1.1".into())));
1029+ assert_eq!(base_head("main"), None);
1030+ assert_eq!(base_head("...head"), None);
1031+ }
1032+
1033+ #[test]
1034+ fn code_and_vulnerability_filters_check_their_words() {
1035+ assert!(code_filters(&json!({ "severity": "severe" })).unwrap_err().contains("severity is critical"));
1036+ let filters = vulnerability_filters(&json!({ "ecosystem": "npm", "state": "dismissed" })).unwrap();
1037+ assert_eq!(filters.state, Some(AlertState::Dismissed));
1038+ }
1039+
1040+ #[test]
1041+ fn a_read_only_token_sees_only_the_security_reads() {
1042+ use g1t_contracts::scopes::{Scope, scope_for};
1043+ for op in SecurityOp::ALL {
1044+ let scope = scope_for(op.name()).unwrap_or_else(|| panic!("{} has no scope", op.name()));
1045+ assert!(matches!(scope, Scope::SecurityRead | Scope::SecurityWrite), "{}", op.name());
1046+ }
1047+ assert_eq!(scope_for("get_sbom"), Some(Scope::SecurityRead));
1048+ assert_eq!(scope_for("upload_sarif"), Some(Scope::SecurityWrite));
1049+ // Fixing an alert also opens an issue and spends agent time.
1050+ let needed = g1t_contracts::scopes::needed("fix_security_alert", &json!({}));
1051+ assert_eq!(needed, [Scope::SecurityWrite, Scope::IssuesWrite, Scope::AgentsRun]);
1052+ // No agent decides about security.
1053+ for name in ["bypass_push_protection", "review_bypass_request", "update_security_settings", "fix_security_alert"] {
1054+ assert!(g1t_contracts::credentials::NEVER.contains(&name), "{name}");
1055+ }
1056+ }
1057+
1058+ #[test]
1059+ fn workspace_wide_operations_do_not_need_a_repository() {
1060+ for op in [SecurityOp::GetOverview, SecurityOp::ListBypassRequests, SecurityOp::ListCustomPatterns] {
1061+ assert!(!op.needs_repo());
1062+ }
1063+ assert!(SecurityOp::UploadSarif.needs_repo());
1064+ let required = SecurityOp::UploadSarif.input()["required"].clone();
1065+ assert_eq!(required, json!(["repo", "commit_sha", "ref", "sarif"]));
1066+ }
1067+}
+157−7
1919 use serde_json::{Map, Value, json};
2020
2121 use crate::operations::Op;
22+use crate::security::SecurityOp;
2223
2324 pub struct Action {
2425 pub name: &'static str,
5758 Tool {
5859 name: "repository",
5960 title: "Repositories",
60− description: "Repositories: find, read and create them, change their settings, and see and dismiss their security alerts (secrets and vulnerable dependencies). Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
61+ description: "Repositories: find, read and create them, change their settings, check their CODEOWNERS file, manage their labels and milestones, and see and dismiss their security alerts (secrets and vulnerable dependencies). Name one as \"owner/name\". Deleting, transferring and changing visibility need `confirm`.",
6162 default_action: None,
6263 actions: &[
6364 a("list", Op::ListRepos, "Repositories you can see"),
6768 a("get_settings", Op::GetRepoSettings, "Branch protection: required checks, approvals, how pull requests merge"),
6869 a("update_settings", Op::UpdateRepoSettings, "Change branch protection and how pull requests merge"),
6970 a("check_names", Op::ListCheckNames, "Check names reported lately, to require on the default branch"),
70− a("list_labels", Op::ListLabels, "Labels in use"),
71+ a("codeowners", Op::GetCodeownersErrors, "Problems in its CODEOWNERS file, by line"),
72+ a("list_labels", Op::ListLabels, "Labels, with colors and how many issues and pull requests carry each"),
73+ a("create_label", Op::CreateLabel, "Create a label"),
74+ a("update_label", Op::UpdateLabel, "Rename a label or change its color or description"),
75+ a("delete_label", Op::DeleteLabel, "Delete a label, from everything that carries it"),
76+ a("add_default_labels", Op::AddDefaultLabels, "Add the default labels it is missing"),
77+ a("list_milestones", Op::ListMilestones, "Milestones, with progress and due dates"),
78+ a("get_milestone", Op::GetMilestone, "One milestone with its issues and pull requests"),
79+ a("create_milestone", Op::CreateMilestone, "Create a milestone"),
80+ a("update_milestone", Op::UpdateMilestone, "Change a milestone's title, description, due date or state"),
81+ a("delete_milestone", Op::DeleteMilestone, "Delete a milestone"),
7182 a("list_events", Op::ListEvents, "Timeline: pushes, issues, pull requests, comments"),
7283 a("rename_branch", Op::RenameBranch, "Rename a branch"),
7384 a("rename", Op::RenameRepo, "Rename it; old addresses redirect"),
93104 a("list", Op::ListIssues, "Issues on a repository, newest first"),
94105 a("get", Op::GetIssue, "One issue with comments and its pull requests"),
95106 a("create", Op::CreateIssue, "Open an issue"),
96− a("update", Op::UpdateIssue, "Change title, body, labels or assignees"),
107+ a("update", Op::UpdateIssue, "Change title, body, labels, milestone or assignees"),
108+ a("labels", Op::ListIssueLabels, "The labels an issue or pull request carries"),
109+ a("add_labels", Op::AddIssueLabels, "Add labels to an issue or pull request"),
110+ a("set_labels", Op::SetIssueLabels, "Replace the labels of an issue or pull request"),
111+ a("remove_labels", Op::RemoveIssueLabels, "Take labels off an issue or pull request"),
97112 a("close", Op::CloseIssue, "Close it without a pull request"),
98113 a("reopen", Op::ReopenIssue, "Reopen it"),
99114 a("comment", Op::AddComment, "Comment on an issue or pull request; path and line for one line of a change"),
103118 Tool {
104119 name: "pull_request",
105120 title: "Pull requests",
106− description: "Pull requests: start a change for an issue, record your session, mark it ready, review and merge. Read `overlaps` and `behind` on `get` before going far.",
121+ description: "Pull requests: start a change for an issue, record your session, mark it ready, ask people and teams to review, review and merge. Read `overlaps` and `behind` on `get` before going far, and `code_owners` for whose approval it needs.",
107122 default_action: None,
108123 actions: &[
109124 a("list", Op::ListPullRequests, "Pull requests on a repository, newest first"),
110125 a("get", Op::GetPullRequest, "Status, checks and required checks, reviews, overlaps, whether it is behind"),
111126 a("changes", Op::GetPullRequestChanges, "Files and line-by-line diff"),
112127 a("create", Op::CreatePullRequest, "Start a draft with its own fork to push to, or open one from a pushed branch"),
128+ a("update", Op::UpdatePullRequest, "Change its base branch, labels, milestone, assignees or reviewers"),
113129 a("record_session", Op::RecordSession, "Append prompt, reasoning and tool entries to its session"),
114130 a("read_session", Op::ReadSession, "Its recorded session"),
115131 a("ready", Op::MarkPullRequestReady, "Mark a draft ready, with a summary"),
132+ a("request_reviewers", Op::RequestReviewers, "Ask people or teams to review it"),
133+ a("remove_requested_reviewers", Op::RemoveRequestedReviewers, "Stop asking people or teams to review it"),
116134 a("review", Op::ReviewPullRequest, "Approve or request changes"),
117135 a("close", Op::ClosePullRequest, "Close without merging"),
118136 a("merge", Op::MergePullRequest, "Land it, or join the merge queue"),
225243 ],
226244 },
227245 Tool {
246+ name: "team",
247+ title: "Teams",
248+ description: "Teams: groups of a workspace's members, given roles on repositories together, mentioned as @workspace/team and asked to review together. Name one by `workspace` and its slug (`team`). Any member may create a team; the workspace's owners and the team's maintainers manage it. A secret team is seen only by its people and the owners.",
249+ default_action: None,
250+ actions: &[
251+ a("list", Op::ListTeams, "A workspace's teams you can see"),
252+ a("get", Op::GetTeam, "One team"),
253+ a("create", Op::CreateTeam, "Create a team; you become its maintainer"),
254+ a("update", Op::UpdateTeam, "Change its name, slug, description, visibility, parent or notifications"),
255+ a("delete", Op::DeleteTeam, "Delete it; its child teams move up"),
256+ a("list_members", Op::ListTeamMembers, "Its people and their roles, child teams' with include_child_teams"),
257+ a("set_member", Op::SetTeamMember, "Add a member of the workspace, or change their role"),
258+ a("remove_member", Op::RemoveTeamMember, "Take someone out of it"),
259+ a("list_child_teams", Op::ListChildTeams, "The teams nested under it"),
260+ a("list_repos", Op::ListTeamRepos, "The repositories it has a role on"),
261+ a("set_repo", Op::SetTeamRepo, "Give it a role on a repository"),
262+ a("remove_repo", Op::RemoveTeamRepo, "Take its role on a repository away"),
263+ a("set_review_assignment", Op::SetTeamReviewAssignment, "Whom it picks when asked to review"),
264+ a("list_user_teams", Op::ListUserTeams, "The teams someone is in"),
265+ ],
266+ },
267+ Tool {
228268 name: "workspace",
229269 title: "Workspaces",
230270 description: "Workspaces own repositories (g1t.sh/{workspace}/{repo}): create, update or delete one, invite members, connect integrations and model providers, and keep your own pinned projects at the top of its sidebar.",
249289 ],
250290 },
251291 Tool {
292+ name: "security",
293+ title: "Security",
294+ description: "A repository's security: secret scanning alerts and push protection bypasses, custom secret patterns, code scanning alerts and SARIF uploads, vulnerability alerts, the dependency graph and its SBOM, dependency review, settings, and a workspace's overview. Fix an alert with g1t. Findings are shown to those who can change the code only. Give `repo` (owner/name), or `workspace` for lists across one.",
295+ default_action: Some("secret_alerts"),
296+ actions: &[
297+ a("secret_alerts", Op::Security(SecurityOp::ListSecretAlerts), "Secret scanning alerts; by state, secret_type, validity, bypassed"),
298+ a("secret_alert", Op::Security(SecurityOp::GetSecretAlert), "One secret alert, with where it was found and its bypass requests"),
299+ a("update_secret_alert", Op::Security(SecurityOp::UpdateSecretAlert), "Dismiss a secret alert with a reason, or reopen it"),
300+ a("secret_locations", Op::Security(SecurityOp::ListSecretLocations), "Every file, line and commit a secret is in"),
301+ a("bypass", Op::Security(SecurityOp::BypassPushProtection), "Push past push protection with a reason, or ask to"),
302+ a("check_validity", Op::Security(SecurityOp::CheckSecretValidity), "Ask a secret's issuer whether it still works"),
303+ a("bypass_requests", Op::Security(SecurityOp::ListBypassRequests), "A workspace's push protection bypass requests"),
304+ a("review_bypass", Op::Security(SecurityOp::ReviewBypassRequest), "Approve, deny or cancel a bypass request"),
305+ a("patterns", Op::Security(SecurityOp::ListCustomPatterns), "Custom secret patterns of a repository or workspace"),
306+ a("create_pattern", Op::Security(SecurityOp::CreateCustomPattern), "Create a custom secret pattern, as a draft or published"),
307+ a("update_pattern", Op::Security(SecurityOp::UpdateCustomPattern), "Change, publish or unpublish a custom pattern"),
308+ a("delete_pattern", Op::Security(SecurityOp::DeleteCustomPattern), "Delete a custom pattern"),
309+ a("dry_run_pattern", Op::Security(SecurityOp::DryRunCustomPattern), "Run a pattern over the default branch without saving it"),
310+ a("code_alerts", Op::Security(SecurityOp::ListCodeAlerts), "Code scanning alerts; by state, severity, tool, rule_id"),
311+ a("code_alert", Op::Security(SecurityOp::GetCodeAlert), "One code scanning alert by number"),
312+ a("update_code_alert", Op::Security(SecurityOp::UpdateCodeAlert), "Dismiss a code scanning alert with a reason, or reopen it"),
313+ a("analyses", Op::Security(SecurityOp::ListAnalyses), "Code scanning analyses, newest first"),
314+ a("upload_sarif", Op::Security(SecurityOp::UploadSarif), "Upload a SARIF file, gzipped and base64-encoded"),
315+ a("sarif_upload", Op::Security(SecurityOp::GetSarifUpload), "Whether a SARIF upload was read, and its analyses"),
316+ a("vulnerability_alerts", Op::Security(SecurityOp::ListVulnerabilityAlerts), "Vulnerable dependencies; by state, severity, ecosystem, package"),
317+ a("vulnerability_alert", Op::Security(SecurityOp::GetVulnerabilityAlert), "One vulnerability alert"),
318+ a("update_vulnerability_alert", Op::Security(SecurityOp::UpdateVulnerabilityAlert), "Dismiss a vulnerability alert with a reason, or reopen it"),
319+ a("fix", Op::Security(SecurityOp::FixAlert), "Put g1t on an issue to fix an alert"),
320+ a("dependency_graph", Op::Security(SecurityOp::GetDependencyGraph), "Every package the lockfiles resolve, direct or transitive"),
321+ a("sbom", Op::Security(SecurityOp::GetSbom), "The dependency graph as an SPDX 2.3 document"),
322+ a("compare_dependencies", Op::Security(SecurityOp::CompareDependencies), "What changes in dependencies between base...head"),
323+ a("settings", Op::Security(SecurityOp::GetSettings), "A repository's security settings"),
324+ a("update_settings", Op::Security(SecurityOp::UpdateSettings), "Change when checks fail and dependency review's policy"),
325+ a("workspace_settings", Op::Security(SecurityOp::GetWorkspaceSettings), "A workspace's delegated bypass and validity checks"),
326+ a("update_workspace_settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), "Turn delegated bypass or validity checks on or off"),
327+ a("overview", Op::Security(SecurityOp::GetOverview), "A workspace's alerts, trends and coverage"),
328+ ],
329+ },
330+ Tool {
252331 name: "notifications",
253332 title: "Notifications",
254333 description: "Your inbox: what needs you, and what you follow. One thread per issue, pull request, workflow or deployment, with why you were told (`reason`): an agent waiting on you, a review asked of you, an assignment, a mention, your work's checks, or what you subscribe to and watch. Mark threads read or done once handled, and choose what you hear of with subscribe, unsubscribe and watch. Your own: a personal token.",
296375 fn destructive(op: Op) -> bool {
297376 matches!(
298377 op,
299− Op::DeleteWorkspace
378+ Op::Security(SecurityOp::DeleteCustomPattern | SecurityOp::BypassPushProtection)
379+ | Op::DeleteWorkspace
300380 | Op::UpdateWorkspace
301381 | Op::DeleteRepo
302382 | Op::PurgeRepo
312392 | Op::SetActionsVariable
313393 | Op::SetModelRoutes
314394 | Op::SetBasePermission
395+ | Op::DeleteTeam
396+ | Op::RemoveTeamRepo
315397 | Op::MergePullRequest
316398 | Op::RemoveRunner
317399 | Op::DeleteRunnerGroup
629711 assert_eq!(tool["annotations"]["destructiveHint"], false);
630712 }
631713 let issue = tools.iter().find(|tool| tool["name"] == "issue").unwrap();
632− assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get"]));
714+ assert_eq!(issue["inputSchema"]["properties"]["action"]["enum"], json!(["list", "get", "labels"]));
633715 // Nothing of the agent tool is a read.
634716 assert!(!tools.iter().any(|tool| tool["name"] == "agent"));
635717 }
638720 fn a_narrow_token_sees_only_its_tools() {
639721 let access = token(Some(vec![Scope::IssuesWrite]));
640722 let names: Vec<Value> = listed(&Gate::Token(&access)).into_iter().map(|tool| tool["name"].clone()).collect();
641− assert_eq!(names, vec![json!("issue"), json!("plan"), json!("account")]);
723+ // Labels and milestones are the repository's, managed with issues:write.
724+ assert_eq!(names, vec![json!("repository"), json!("issue"), json!("plan"), json!("account")]);
642725 // Notifications are a resource of their own: reading them lists
643726 // only what reads.
644727 let reader = token(Some(vec![Scope::NotificationsRead]));
677760 assert_eq!(resolve(account, &json!({})), Ok(Op::Whoami));
678761 }
679762
763+ #[test]
764+ fn teams_are_one_tool_and_a_workspace_reader_sees_only_its_reads() {
765+ let team = Tool::by_name("team").unwrap();
766+ let names: Vec<&str> = team.actions.iter().map(|action| action.name).collect();
767+ assert_eq!(
768+ names,
769+ [
770+ "list",
771+ "get",
772+ "create",
773+ "update",
774+ "delete",
775+ "list_members",
776+ "set_member",
777+ "remove_member",
778+ "list_child_teams",
779+ "list_repos",
780+ "set_repo",
781+ "remove_repo",
782+ "set_review_assignment",
783+ "list_user_teams",
784+ ]
785+ );
786+ let reader = token(Some(vec![Scope::WorkspaceRead]));
787+ let tools = listed(&Gate::Token(&reader));
788+ let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap();
789+ assert_eq!(
790+ listed_team["inputSchema"]["properties"]["action"]["enum"],
791+ json!(["list", "get", "list_members", "list_child_teams", "list_repos", "list_user_teams"])
792+ );
793+ assert_eq!(listed_team["annotations"]["readOnlyHint"], true);
794+ // A team's role on a repository is who has access.
795+ let admin = token(Some(vec![Scope::WorkspaceAdmin]));
796+ let tools = listed(&Gate::Token(&admin));
797+ let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap();
798+ let actions = listed_team["inputSchema"]["properties"]["action"]["enum"].as_array().unwrap();
799+ assert!(actions.contains(&json!("set_review_assignment")) && !actions.contains(&json!("set_repo")));
800+ let access = token(Some(vec![Scope::AccessAdmin]));
801+ let tools = listed(&Gate::Token(&access));
802+ let listed_team = tools.iter().find(|tool| tool["name"] == "team").unwrap();
803+ assert_eq!(listed_team["inputSchema"]["properties"]["action"]["enum"], json!(["set_repo", "remove_repo"]));
804+ // Both kinds of role a schema names are offered.
805+ let roles = &listed(&Gate::Everything).into_iter().find(|tool| tool["name"] == "team").unwrap()["inputSchema"]
806+ ["properties"]["role"]["enum"];
807+ for role in ["member", "maintainer", "read", "admin"] {
808+ assert!(roles.as_array().unwrap().contains(&json!(role)), "{role}");
809+ }
810+ assert_eq!(
811+ resolve(team, &json!({ "action": "set_repo", "workspace": "acme", "team": "backend", "repo": "rocket" })),
812+ Err("team.set_repo needs role.".to_owned())
813+ );
814+ }
815+
816+ #[test]
817+ fn reviewers_and_code_owners_are_actions_of_their_tools() {
818+ let pull = Tool::by_name("pull_request").unwrap();
819+ assert_eq!(
820+ resolve(pull, &json!({ "action": "request_reviewers", "repo": "a/b", "number": 1, "team_reviewers": ["backend"] })),
821+ Ok(Op::RequestReviewers)
822+ );
823+ assert_eq!(pull.action("remove_requested_reviewers").map(|action| action.op), Some(Op::RemoveRequestedReviewers));
824+ let repository = Tool::by_name("repository").unwrap();
825+ assert_eq!(resolve(repository, &json!({ "action": "codeowners", "repo": "a/b" })), Ok(Op::GetCodeownersErrors));
826+ assert!(reads_only(Op::GetCodeownersErrors));
827+ assert!(!reads_only(Op::RequestReviewers));
828+ }
829+
680830 /// How much smaller `tools/list` is than one tool per operation. Run
681831 /// with `--nocapture` to see the numbers.
682832 #[test]
+16−0
8484 { label: 'RubyGems', slug: 'guides/rubygems' },
8585 { label: 'Go modules', slug: 'guides/go' },
8686 { label: 'Secrets and variables', slug: 'guides/secrets-and-variables' },
87+ ],
88+ },
89+ {
90+ label: 'Security',
91+ items: [
8792 { label: 'Security', slug: 'guides/security' },
93+ { label: 'Dependency updates', slug: 'guides/dependency-updates' },
94+ { label: 'Secret protection', slug: 'guides/security/secret-protection' },
95+ { label: 'Code scanning', slug: 'guides/security/code-scanning' },
96+ { label: 'Supply chain', slug: 'guides/security/supply-chain' },
97+ { label: 'Security overview', slug: 'guides/security/security-overview' },
98+ { label: "What's free and what's paid", slug: 'guides/security/pricing' },
8899 ],
89100 },
90101 {
113124 label: 'Landing changes',
114125 items: [
115126 { label: 'Pull requests and checks', slug: 'guides/pull-requests' },
127+ { label: 'Pull requests into other branches', slug: 'guides/base-branches' },
128+ { label: 'Labels', slug: 'guides/labels' },
129+ { label: 'Milestones', slug: 'guides/milestones' },
116130 { label: 'The merge queue', slug: 'guides/merge-queue' },
131+ { label: 'CODEOWNERS', slug: 'guides/codeowners' },
117132 { label: 'Sessions and why-blame', slug: 'guides/why-blame' },
118133 { label: 'Forks and branches', slug: 'concepts/forks' },
119134 ],
125140 { label: 'GitHub', slug: 'guides/github' },
126141 { label: 'Workspaces and tokens', slug: 'guides/workspaces' },
127142 { label: 'Access and roles', slug: 'guides/access-and-roles' },
143+ { label: 'Teams', slug: 'guides/teams' },
128144 { label: 'Managing a repository', slug: 'guides/managing-repositories' },
129145 { label: 'Transferring a repository', slug: 'guides/transferring-repositories' },
130146 { label: 'Audit log', slug: 'guides/audit-log' },
+18−1
157157 to a branch.
158158 - **Status.** Clear fork storage rules depend on Cloudflare.
159159
160+### Some dependency update options are not applied yet
161+
162+g1t reads and checks every option of a `dependabot.yml` file, but does not
163+act on all of them yet:
164+
165+- Version update pull requests are opened for npm, Cargo, Go and pip only.
166+ Entries for other ecosystems are checked and listed, and open nothing.
167+
168+- A multi-ecosystem group opens one pull request per ecosystem, not one
169+ for the group.
170+- Registries that sign in with OIDC are not used.
171+
172+- **Instead.** Keep a separate entry per ecosystem and directory, and
173+ check the Security page, which lists what each entry reads but does not
174+ act on. See [Dependency updates](/guides/dependency-updates/#options).
175+- **Status.** Planned.
176+
160177 ### No conflict resolution in the browser
161178
162179 You can't resolve a merge conflict on the pull request's page.
349366
350367 ### Not built yet
351368
352−- **Milestones.** Planned.
369+
353370 - **Releases and package registries.** Planned.
354371 - **Wikis.** Not scheduled. Keep docs in the repository.
+31−10
11 ---
22 title: Access and roles
3−description: The five repository roles and what each can do, the base permission members get, outside collaborators and invitations, and what agents may do on a person's behalf.
3+description: The five repository roles and what each can do, the base permission members get, roles through teams, outside collaborators and invitations, and what agents may do on a person's behalf.
44 ---
55
66 Everyone who can work in a repository has a role on it. The role says what
77 they can do there, from reading it to managing who else has access. A
8−workspace gives its members a role on every one of its repositories, and a
8+workspace gives its members a role on every one of its repositories, a
99 repository can give anyone a role of their own: a member who needs more
10−there, or someone outside the workspace.
10+there, or someone outside the workspace, and it can give a
11+[team](/guides/teams/) a role that everyone in the team has.
1112
1213 ## The roles
1314
5758 in it.
5859 3. **A role given to you on that repository.** See
5960 [add someone to a repository](#add-someone-to-a-repository).
60−4. **Public.** Anyone, signed in or not, can read a public repository.
61+4. **Your teams.** The role each [team](/guides/teams/) you are in has on
62+ that repository, and the roles of that team's parent teams, which child
63+ teams inherit. See [repository access](/guides/teams/#repository-access).
64+5. **Public.** Anyone, signed in or not, can read a public repository.
6165
6266 The highest wins. A member whose base permission is Read and who is given
6367 Maintain on one repository has Maintain there and Read everywhere else. A
64−role lower than what you already have changes nothing.
68+role lower than what you already have changes nothing. When a role given to
69+you and a team's role are the same, the one given to you is shown as where
70+it comes from.
6571
6672 A workspace's own [access token](/guides/workspaces/#workspace-access-tokens)
6773 has Admin on its workspace's repositories, and none on any other.
136142 3. Pick their role and choose **Add**.
137143
138144 Everyone with access is listed under **People with access**, with their
139−role and where it comes from. People with Write or Maintain can see the
140−list; changing it needs Admin.
145+role and where it comes from: owner, the base permission, a role given to
146+them, or **Through team** and the team's slug. **Teams with access** lists the
147+teams given a role on it, with how many people each has. People with Write
148+or Maintain can see the lists; changing them needs Admin.
149+
150+Someone with Admin can give a team a role under **Teams with access**:
151+pick the team and its role, and add it. Only the workspace's own teams can
152+be added. See [teams](/guides/teams/#repository-access).
141153
142154 What happens depends on who they are:
143155
153165
154166 To change someone's role, pick another beside their name. To take it away,
155167 choose **Remove**. Removing takes away only the role given on this
156−repository: an owner's Admin and a member's base permission stay. Anyone
168+repository: an owner's Admin, a member's base permission and what their
169+teams give them stay. Anyone
157170 can remove their own role from a repository. Each change is confirmed
158171 under the list; one that is refused says why on that person's row.
159172
184197 roles they have stay, and the base permission adds to them.
185198
186199 Removing a member from a workspace also removes the roles they were given
187−on its repositories.
200+on its repositories, and takes them out of its teams.
188201
189202 ## Invitations
190203
258271 }
259272 ```
260273
261−`source` is `owner`, `base` or `direct`.
274+`source` is `owner`, `base`, `direct` or `team`. In the list from
275+`list_collaborators`, each person also has `direct`, the role given to them
276+on the repository if any, and `team_role` and `team`: the highest role a
277+team gives them there, and that team's slug. A team's own roles are managed
278+through the [teams API](/guides/teams/#through-the-api).
262279
263280 ## Webhooks and the audit log
264281
276293 The workspace's [audit log](/guides/audit-log/) records the same changes
277294 under those names, and also `repo.invitation_created`,
278295 `repo.invitation_revoked` and `workspace.base_permission_changed`.
296+
297+A team's role on a repository changing is sent as `team.repo_added`,
298+`team.repo_role_changed` or `team.repo_removed`; see
299+[teams](/guides/teams/#webhooks-and-the-audit-log).
+11−0
184184 marks it ready, which on g1t is when it first has code. Each head runs
185185 each workflow once.
186186
187+They also start on the activity types `labeled`, `unlabeled`,
188+`milestoned`, `demilestoned`, `assigned`, `review_requested` and
189+`closed`, and `edited` when the branch a pull request merges into
190+changes; `issues` workflows on `labeled`, `unlabeled`, `milestoned` and
191+`demilestoned` too. List them under `types:` to run on them. For
192+`labeled` and `unlabeled`, `github.event.label` names the label. A pull
193+request's `branches` filter, `github.base_ref` and
194+`pull_request.base.ref` are the branch it merges into, which is not
195+always the default branch: see
196+[pull requests into other branches](/guides/base-branches/).
197+
187198 `github.event.pull_request` reads as it does on GitHub. For a pull request
188199 g1t made, `pull_request.user` is g1t (`login` `g1t`, `type` `Bot`), and
189200 `pull_request.requested_by` names the person who asked for it; it is `null`
+3−0
2929 | `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had it changed, or lost it. See [access and roles](/guides/access-and-roles/). |
3030 | `repo.invitation_created`, `repo.invitation_revoked` | Someone was invited to it, or an invitation was withdrawn. |
3131 | `workspace.base_permission_changed` | An owner changed what members get on every repository. |
32+| `team.created`, `team.edited`, `team.deleted` | A [team](/guides/teams/) was created, changed or deleted. |
33+| `team.member_added`, `team.member_role_changed`, `team.member_removed` | Someone was added to a team, made its maintainer or a member, or taken out of it. |
34+| `team.repo_added`, `team.repo_role_changed`, `team.repo_removed` | A team was given a role on a repository, had it changed, or lost it. |
3235 | `workspace.residency_changed` | An owner changed where the workspace's new repositories are stored. See [data residency](/guides/workspaces/#data-residency). |
3336 | `workspace.deleted`, `workspace.restored`, `workspace.purged` | An owner deleted the workspace, g1t's support restored it, or it was removed for good. See [deleting a workspace](/guides/workspaces/#delete-a-workspace). |
3437
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.