Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 1 | //! Run credentials: the least-privilege tokens a sandbox works with. |
| 2 | //! | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 3 | //! Every sandbox run (checks, the merge queue, a merge check, a deployment, |
| 4 | //! a security update's bump) gets its own tokens, bound to the run, its | |
| 5 | //! repository, what that kind of run needs to do, and an expiry no later | |
| 6 | //! than the run's timeout. Each carries a composite identity: g1t acting on | |
| 7 | //! behalf of the person who started the work. What it may do is the | |
| 8 | //! intersection of the two: the run's scope, and what that person may do | |
| 9 | //! right now. | |
| 10 | //! | |
| 11 | //! g1t's retired hosted agents had run credentials too (`implement`, | |
| 12 | //! `revise`, `review`, `answer`, `update`, `plan`). Those kinds still parse, | |
| 13 | //! so stored tokens and the history they wrote read as before, but no | |
| 14 | //! credential is made for them and they grant nothing | |
| 15 | //! ([`RunCredentialKind::retired`]). | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 16 | //! |
| 17 | //! The policy lives here, as pure functions, so that identity (which mints | |
| 18 | //! the tokens), the API (which serves REST and MCP) and repos (which serves | |
| 19 | //! git) all enforce the same rules, and so the rules can be tested. | |
| 20 | ||
| 21 | use serde::{Deserialize, Serialize}; | |
| 22 | ||
| 23 | use crate::identity::AgentScope; | |
| 24 | use crate::repos::RepoPath; | |
| 25 | use crate::access::{BasePermission, RepoGrant, RepoRole}; | |
| 26 | use crate::{Membership, PrincipalKind, Role, User}; | |
| 27 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 28 | /// What a run does, as far as its credentials are concerned. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 29 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 30 | #[serde(rename_all = "snake_case")] | |
| 31 | pub enum RunCredentialKind { | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 32 | /// Retired with g1t's hosted agents, as are `revise`, `review`, |
| 33 | /// `answer`, `update` and `plan`: kept so stored tokens parse; never | |
| 34 | /// issued, and they grant nothing. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 35 | Implement, |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 36 | /// Retired: see [`RunCredentialKind::Implement`]. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 37 | Revise, |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 38 | /// Retired: see [`RunCredentialKind::Implement`]. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 39 | Review, |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 40 | /// Retired: see [`RunCredentialKind::Implement`]. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 41 | Answer, |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 42 | /// Retired: see [`RunCredentialKind::Implement`]. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 43 | Update, |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 44 | /// Retired: see [`RunCredentialKind::Implement`]. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 45 | Plan, |
| 46 | Checks, | |
| 47 | Queue, | |
| 48 | Mergecheck, | |
| 49 | Deploy, | |
| 50 | /// A security update: raising one package's version in its lockfiles | |
| 51 | /// and pushing that to a branch of its own. Not an agent. | |
| 52 | Bump, | |
| 53 | } | |
| 54 | ||
| 55 | impl RunCredentialKind { | |
| 56 | pub const ALL: [RunCredentialKind; 11] = [ | |
| 57 | RunCredentialKind::Implement, | |
| 58 | RunCredentialKind::Revise, | |
| 59 | RunCredentialKind::Review, | |
| 60 | RunCredentialKind::Answer, | |
| 61 | RunCredentialKind::Update, | |
| 62 | RunCredentialKind::Plan, | |
| 63 | RunCredentialKind::Checks, | |
| 64 | RunCredentialKind::Queue, | |
| 65 | RunCredentialKind::Mergecheck, | |
| 66 | RunCredentialKind::Deploy, | |
| 67 | RunCredentialKind::Bump, | |
| 68 | ]; | |
| 69 | ||
| 70 | pub fn as_str(self) -> &'static str { | |
| 71 | match self { | |
| 72 | RunCredentialKind::Implement => "implement", | |
| 73 | RunCredentialKind::Revise => "revise", | |
| 74 | RunCredentialKind::Review => "review", | |
| 75 | RunCredentialKind::Answer => "answer", | |
| 76 | RunCredentialKind::Update => "update", | |
| 77 | RunCredentialKind::Plan => "plan", | |
| 78 | RunCredentialKind::Checks => "checks", | |
| 79 | RunCredentialKind::Queue => "queue", | |
| 80 | RunCredentialKind::Mergecheck => "mergecheck", | |
| 81 | RunCredentialKind::Deploy => "deploy", | |
| 82 | RunCredentialKind::Bump => "bump", | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 86 | /// Whether this kind was one of g1t's retired hosted agents' runs: it |
| 87 | /// still parses, but no credential is made for it and it grants | |
| 88 | /// nothing. | |
| 89 | pub fn retired(self) -> bool { | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 90 | matches!( |
| 91 | self, | |
| 92 | RunCredentialKind::Implement | |
| 93 | | RunCredentialKind::Revise | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 94 | | RunCredentialKind::Review |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 95 | | RunCredentialKind::Answer |
| 96 | | RunCredentialKind::Update | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 97 | | RunCredentialKind::Plan |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 98 | ) |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | /// Which part of a sandbox a credential is for. | |
| 103 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 104 | #[serde(rename_all = "snake_case")] | |
| 105 | pub enum CredentialUse { | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 106 | /// g1t's runner: cloning, fetching and pushing what the run needs. It |
| 107 | /// acts as the person downstream, so that what it pushes is theirs, | |
| 108 | /// within the run's scope. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 109 | Runner, |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 110 | /// A retired hosted agent's own tools, over MCP. Kept so stored tokens |
| 111 | /// parse; never issued. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 112 | Tools, |
| 113 | } | |
| 114 | ||
| 115 | impl CredentialUse { | |
| 116 | pub fn as_str(self) -> &'static str { | |
| 117 | match self { | |
| 118 | CredentialUse::Runner => "runner", | |
| 119 | CredentialUse::Tools => "tools", | |
| 120 | } | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | /// A repository a run may push to, and the one branch, if only one. | |
| 125 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 126 | pub struct GitGrant { | |
| 127 | pub repo: RepoPath, | |
| 128 | /// Null: any branch. A pull request's fork is its own repository, so | |
| 129 | /// the whole of it is the pull request's. | |
| 130 | #[serde(default)] | |
| 131 | pub branch: Option<String>, | |
| 132 | } | |
| 133 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 134 | /// What binds a token to one run. Absent on tokens g1t's retired hosted |
| 135 | /// agents were issued before run credentials. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 136 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] |
| 137 | #[serde(rename_all = "camelCase")] | |
| 138 | pub struct RunBinding { | |
| 139 | pub kind: RunCredentialKind, | |
| 140 | #[serde(rename = "use")] | |
| 141 | pub usage: CredentialUse, | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 142 | /// The run, once the sandbox has recorded it. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 143 | #[serde(default)] |
| 144 | pub run_id: Option<String>, | |
| 145 | /// The pull request the run works on, for the kinds that work on one. | |
| 146 | #[serde(default)] | |
| 147 | pub number: Option<u32>, | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 148 | /// Who acts, as the audit log names it: `g1t`. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 149 | pub agent: String, |
| 150 | /// Repositories it may clone and fetch, besides those it may push to. | |
| 151 | #[serde(default)] | |
| 152 | pub read: Vec<RepoPath>, | |
| 153 | /// Where it may push. | |
| 154 | #[serde(default)] | |
| 155 | pub push: Vec<GitGrant>, | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 156 | /// g1t's own run (a security update): the |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 157 | /// credential belongs to the workspace, and acts on behalf of g1t |
| 158 | /// (`system::ID`), so what it does is g1t's, and the pull request g1t | |
| 159 | /// opened, and its working copy, are its own. | |
| 160 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 161 | pub system: bool, | |
| 162 | } | |
| 163 | ||
| 164 | /// A person, by id and name. | |
| 165 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 166 | pub struct Principal { | |
| 167 | pub id: String, | |
| 168 | pub username: String, | |
| 169 | } | |
| 170 | ||
| 171 | impl From<&User> for Principal { | |
| 172 | fn from(user: &User) -> Self { | |
| 173 | Principal { | |
| 174 | id: user.id.clone(), | |
| 175 | username: user.username.clone(), | |
| 176 | } | |
| 177 | } | |
| 178 | } | |
| 179 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 180 | /// Set on a [`User`] resolved from a run credential: the composite |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 181 | /// identity, "g1t on behalf of syntaqx", and what it may do. |
| 182 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 183 | #[serde(rename_all = "camelCase")] | |
| 184 | pub struct Acting { | |
| 185 | /// The token's id, as audit entries name it. | |
| 186 | pub credential_id: String, | |
| 187 | pub agent: String, | |
| 188 | pub on_behalf_of: Principal, | |
| 189 | pub scope: AgentScope, | |
| 190 | } | |
| 191 | ||
| 192 | impl Acting { | |
| 193 | pub fn run(&self) -> Option<&RunBinding> { | |
| 194 | self.scope.run.as_ref() | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 198 | /// `create_run_credential`: a token for one sandbox run. It acts as g1t on |
| 199 | /// behalf of `on_behalf_of`, can do only what `kind` and `usage` allow in | |
| 200 | /// `repo`, and expires after `ttl_seconds`, which should be the run's | |
| 201 | /// timeout. A [retired](RunCredentialKind::retired) kind is refused. | |
| 202 | /// Returns `CreatedAccessToken`. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 203 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 204 | #[serde(rename_all = "camelCase")] | |
| 205 | pub struct CreateRunCredentialArgs { | |
| 206 | pub on_behalf_of: User, | |
| 207 | pub repo: RepoPath, | |
| 208 | pub kind: RunCredentialKind, | |
| 209 | #[serde(rename = "use")] | |
| 210 | pub usage: CredentialUse, | |
| 211 | #[serde(default)] | |
| 212 | pub number: Option<u32>, | |
| 213 | #[serde(default)] | |
| 214 | pub read: Vec<RepoPath>, | |
| 215 | #[serde(default)] | |
| 216 | pub push: Vec<GitGrant>, | |
| 217 | pub ttl_seconds: u64, | |
| 218 | } | |
| 219 | ||
| 220 | /// `bind_run_credentials`: ties tokens, named by the SHA-256 of their | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 221 | /// text in hex, to the run their sandbox recorded. Returns how many. |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 222 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 223 | #[serde(rename_all = "camelCase")] | |
| 224 | pub struct BindRunCredentialsArgs { | |
| 225 | pub token_hashes: Vec<String>, | |
| 226 | pub run_id: String, | |
| 227 | } | |
| 228 | ||
| 229 | /// `revoke_run_credentials`: ends tokens when their sandbox stops, by hash | |
| 230 | /// or by run. Only run credentials are touched, never a token a person | |
| 231 | /// made. Returns how many. | |
| 232 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 233 | #[serde(rename_all = "camelCase")] | |
| 234 | pub struct RevokeRunCredentialsArgs { | |
| 235 | #[serde(default)] | |
| 236 | pub token_hashes: Vec<String>, | |
| 237 | #[serde(default)] | |
| 238 | pub run_id: Option<String>, | |
| 239 | } | |
| 240 | ||
| 241 | // --- Policy -------------------------------------------------------------- | |
| 242 | ||
| 243 | /// Operations that only read. | |
| 244 | pub const READ_OPERATIONS: &[&str] = &[ | |
| 245 | "whoami", | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 246 | "get_usage", |
| 247 | "get_budget", | |
| 248 | "list_invoices", | |
| 249 | "get_billing_details", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 250 | "list_repos", |
| 251 | "get_repo", | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 252 | "list_projects", |
| 253 | "get_project", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 254 | "list_deleted_repos", |
| 255 | "list_collaborators", | |
| 256 | "get_collaborator_permission", | |
| 257 | "list_repo_invitations", | |
| 258 | "list_my_repo_invitations", | |
| 259 | "list_outside_collaborators", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 260 | "list_teams", |
| 261 | "get_team", | |
| 262 | "list_team_members", | |
| 263 | "list_child_teams", | |
| 264 | "list_team_repos", | |
| 265 | "list_user_teams", | |
| 266 | "get_codeowners_errors", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 267 | "get_repo_settings", |
| 268 | "list_check_names", | |
| 269 | "get_merge_queue", | |
| 270 | "recall", | |
| 271 | "list_issues", | |
| 272 | "get_issue", | |
| 273 | "list_labels", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 274 | "list_issue_labels", |
| 275 | "list_milestones", | |
| 276 | "get_milestone", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 277 | "list_pull_requests", |
| 278 | "get_pull_request", | |
| 279 | "read_session", | |
| The merge queue works out who answers for a failed combination and tells the work: a state that fails while an entry ahead of it is still being tested is held until those ahead settle, so a broken change is blamed on itself and the entries tested with it are tested again instead of being taken out; an entry whose own checks pass but which fails with several entries ahead has smaller combinations (the default branch, half of the suspects and the entry) built and checked by the merge_group workflows, halving the suspects within three extra builds, and never landed. The result goes on the pull request's and its issue's timelines and into its session, as an integration_failure decision record (tested), and as integration.failed with the pull request, its issue, the combination, the failing check's name, conclusion and link, whether it was bisected, the suspects and the other pull requests; a combination that passes is an integration_passed decision and integration.passed. An entry the queue took out keeps its merge request: once a new commit's checks all pass, g1t merges it again as whoever queued it, which puts it back in the queue under the branch's rules as they are then. | 280 | "list_decisions", |
| 281 | "why", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 282 | "get_pull_request_changes", |
| 283 | "list_events", | |
| 284 | "get_context", | |
| 285 | "search", | |
| 286 | "list_workflows", | |
| 287 | "list_workflow_runs", | |
| 288 | "get_workflow_run", | |
| 289 | "get_job_logs", | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 290 | "get_pending_deployments", |
| 291 | "get_workflow_permissions", | |
| 292 | "get_fork_pr_approval", | |
| Merge checks: statuses and check runs on every commit | 293 | "list_commit_statuses", |
| 294 | "get_combined_status", | |
| 295 | "list_check_runs_for_ref", | |
| 296 | "get_check_run", | |
| 297 | "list_check_run_annotations", | |
| 298 | "list_check_suites_for_ref", | |
| 299 | "get_check_suite", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 300 | "list_integrations", |
| 301 | "list_webhooks", | |
| 302 | "list_webhook_deliveries", | |
| 303 | "list_actions_secrets", | |
| 304 | "list_actions_variables", | |
| 305 | "list_security_alerts", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 306 | "list_secret_scanning_alerts", |
| 307 | "get_secret_scanning_alert", | |
| 308 | "list_secret_scanning_locations", | |
| 309 | "list_bypass_requests", | |
| 310 | "list_custom_patterns", | |
| 311 | "list_code_scanning_alerts", | |
| 312 | "get_code_scanning_alert", | |
| 313 | "list_code_scanning_analyses", | |
| 314 | "get_sarif_upload", | |
| 315 | "list_vulnerability_alerts", | |
| 316 | "get_vulnerability_alert", | |
| 317 | "get_dependency_graph", | |
| 318 | "get_sbom", | |
| 319 | "compare_dependencies", | |
| 320 | "get_security_settings", | |
| 321 | "get_workspace_security_settings", | |
| 322 | "get_security_overview", | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 323 | "list_security_advisories", |
| 324 | "get_security_advisory", | |
| 325 | "list_vulnerability_reports", | |
| API: notifications over REST and MCP, with notifications scopes | 326 | "list_notifications", |
| 327 | "get_notification_thread", | |
| 328 | "get_thread_subscription", | |
| 329 | "get_repo_subscription", | |
| 330 | "list_watched_repos", | |
| API: pinned projects over REST and MCP | 331 | "list_pinned_projects", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 332 | ]; |
| 333 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 334 | /// What no run credential may ever do, whatever its scope says: |
| 335 | /// workspaces, repositories' settings, members, tokens, billing, | |
| 336 | /// integrations, webhooks, secrets, workflows' controls and merging. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 337 | pub const NEVER: &[&str] = &[ |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 338 | // Billing is people's: runs never spend or change it. |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 339 | "set_budget", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 340 | "create_workspace", |
| 341 | "delete_workspace", | |
| 342 | "update_workspace", | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 343 | // So is who belongs to a workspace and who owns it. |
| 344 | "list_members", | |
| 345 | "update_member", | |
| 346 | "remove_member", | |
| 347 | "transfer_ownership", | |
| 348 | "leave_workspace", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 349 | "transfer_repo", |
| 350 | "create_repo", | |
| 351 | "update_repo", | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 352 | "update_project", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 353 | "delete_repo", |
| 354 | "list_deleted_repos", | |
| 355 | "restore_repo", | |
| 356 | "purge_repo", | |
| 357 | "rename_repo", | |
| 358 | "archive_repo", | |
| 359 | "unarchive_repo", | |
| 360 | "set_repo_visibility", | |
| 361 | "rename_branch", | |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 362 | "delete_branch", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 363 | "update_repo_settings", |
| 364 | "merge_pull_request", | |
| 365 | "import_issue", | |
| 366 | "list_integrations", | |
| 367 | "connect_integration", | |
| AI Gateway: OpenAI's format, open models, and your own providers | 368 | "update_integration", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 369 | "disconnect_integration", |
| 370 | "test_integration", | |
| 371 | "list_webhooks", | |
| 372 | "create_webhook", | |
| 373 | "update_webhook", | |
| 374 | "delete_webhook", | |
| 375 | "ping_webhook", | |
| 376 | "list_webhook_deliveries", | |
| 377 | "redeliver_webhook", | |
| 378 | "dispatch_workflow", | |
| 379 | "cancel_workflow_run", | |
| 380 | "rerun_workflow_run", | |
| 381 | "update_workflow", | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 382 | // Nor lets runs or deployments through, or changes what holds them. |
| 383 | "approve_workflow_run", | |
| 384 | "review_pending_deployments", | |
| 385 | "update_environment", | |
| 386 | "delete_environment", | |
| 387 | "set_workflow_permissions", | |
| 388 | "set_fork_pr_approval", | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 389 | "set_actions_access", |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 390 | "create_repository_dispatch", |
| 391 | "set_workspace_workflow_permissions", | |
| Merge checks: statuses and check runs on every commit | 392 | // An agent never reports checks on its own work, nor asks for them |
| 393 | // to run again: what checks say is the integrations' to say. | |
| 394 | "create_commit_status", | |
| 395 | "create_check_run", | |
| 396 | "update_check_run", | |
| 397 | "rerequest_check_run", | |
| 398 | "rerequest_check_suite", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 399 | "list_actions_secrets", |
| 400 | "set_actions_secret", | |
| 401 | "delete_actions_secret", | |
| 402 | "list_actions_variables", | |
| 403 | "set_actions_variable", | |
| 404 | "delete_actions_variable", | |
| 405 | "list_collaborators", | |
| 406 | "get_collaborator_permission", | |
| 407 | "add_collaborator", | |
| 408 | "update_collaborator", | |
| 409 | "remove_collaborator", | |
| 410 | "list_repo_invitations", | |
| 411 | "revoke_repo_invitation", | |
| 412 | "list_my_repo_invitations", | |
| 413 | "accept_repo_invitation", | |
| 414 | "decline_repo_invitation", | |
| Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040) | 415 | // Nor answers a workspace invitation: only the person it is for does. |
| 416 | "accept_invitation", | |
| 417 | "decline_invitation", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 418 | "set_base_permission", |
| 419 | "list_outside_collaborators", | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 420 | // Deploy keys, which let a machine into a repository. |
| 421 | "list_deploy_keys", | |
| 422 | "get_deploy_key", | |
| 423 | "create_deploy_key", | |
| 424 | "delete_deploy_key", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 425 | // Teams: who is in which, and what they reach, is for people. |
| 426 | "create_team", | |
| 427 | "update_team", | |
| 428 | "delete_team", | |
| 429 | "set_team_member", | |
| 430 | "remove_team_member", | |
| 431 | "set_team_repo", | |
| 432 | "remove_team_repo", | |
| 433 | "set_team_review_assignment", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 434 | // Dismissing a secret lets it through push protection. |
| 435 | "dismiss_security_alert", | |
| 436 | "reopen_security_alert", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 437 | // Nor any other decision about security: closing or reopening an |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 438 | // alert, pushing past push protection or deciding who may, or changing |
| 439 | // what is looked for or when checks fail. | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 440 | "update_secret_scanning_alert", |
| 441 | "bypass_push_protection", | |
| 442 | "review_bypass_request", | |
| 443 | "create_custom_pattern", | |
| 444 | "update_custom_pattern", | |
| 445 | "delete_custom_pattern", | |
| 446 | "update_code_scanning_alert", | |
| 447 | "update_vulnerability_alert", | |
| 448 | "update_security_settings", | |
| 449 | "update_workspace_security_settings", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 450 | // A person's own inbox: a run acts as g1t, which has none. |
| API: notifications over REST and MCP, with notifications scopes | 451 | "list_notifications", |
| 452 | "get_notification_thread", | |
| 453 | "mark_notifications_read", | |
| 454 | "mark_thread_read", | |
| 455 | "mark_thread_done", | |
| 456 | "save_thread", | |
| 457 | "snooze_thread", | |
| 458 | "get_thread_subscription", | |
| 459 | "set_thread_subscription", | |
| 460 | "delete_thread_subscription", | |
| 461 | "get_repo_subscription", | |
| 462 | "set_repo_subscription", | |
| 463 | "delete_repo_subscription", | |
| 464 | "list_watched_repos", | |
| API: pinned projects over REST and MCP | 465 | // Pins are a person's own, as the inbox is. |
| 466 | "list_pinned_projects", | |
| 467 | "pin_project", | |
| 468 | "unpin_project", | |
| 469 | "reorder_pinned_projects", | |
| Merge packages: roles, Actions access, source label, soft delete, API | 470 | // Deleting packages and deciding who may use them is for people: an |
| 471 | // agent publishes its repository's packages and never deletes them. | |
| 472 | "update_package", | |
| 473 | "link_package", | |
| 474 | "unlink_package", | |
| 475 | "set_package_access", | |
| 476 | "remove_package_access", | |
| 477 | "set_package_actions_access", | |
| 478 | "remove_package_actions_access", | |
| 479 | "delete_package", | |
| 480 | "restore_package", | |
| 481 | "delete_package_version", | |
| 482 | "restore_package_version", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 483 | // Sharing an artifact and deleting one for good are for people. |
| Merge main into Artifacts Phase 2 | 484 | "set_workspace_artifact_access", |
| 485 | "purge_workspace_artifact", | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 486 | ]; |
| 487 | ||
| 488 | pub fn is_read(operation: &str) -> bool { | |
| 489 | READ_OPERATIONS.contains(&operation) | |
| 490 | } | |
| 491 | ||
| 492 | /// The API and MCP operations a run of `kind` may use with a credential | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 493 | /// for `usage`. Git is separate: see [`decide_git`]. None today: every run |
| 494 | /// still issued works through git alone, and a | |
| 495 | /// [retired](RunCredentialKind::retired) kind grants nothing. A stored | |
| 496 | /// token keeps the operations it was made with until it expires. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 497 | pub fn operations_for(kind: RunCredentialKind, usage: CredentialUse) -> Vec<&'static str> { |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 498 | let _ = (kind, usage); |
| 499 | Vec::new() | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 500 | } |
| 501 | ||
| 502 | /// What a run's credential may do, in the scope vocabulary that access | |
| 503 | /// tokens use (see [`crate::scopes`]): the scopes of its operations, and | |
| 504 | /// for a runner, git's. Its operations, its repository and its run still | |
| 505 | /// bound it more tightly than these scopes say. | |
| 506 | pub fn run_scopes(kind: RunCredentialKind, usage: CredentialUse) -> Vec<crate::scopes::Scope> { | |
| 507 | use crate::scopes::{Scope, normalize, scope_for}; | |
| 508 | let mut scopes: Vec<Scope> = operations_for(kind, usage) | |
| 509 | .into_iter() | |
| 510 | .filter_map(scope_for) | |
| 511 | .collect(); | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 512 | if usage == CredentialUse::Runner && !kind.retired() { |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 513 | scopes.push(Scope::CodeRead); |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 514 | if kind == RunCredentialKind::Bump { |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 515 | scopes.push(Scope::CodeWrite); |
| 516 | } | |
| 517 | } | |
| 518 | normalize(&mut scopes); | |
| 519 | scopes | |
| 520 | } | |
| 521 | ||
| 522 | /// Operations that change a pull request, which a runner may do only to | |
| 523 | /// the pull request its run works on. | |
| The merge queue works out who answers for a failed combination and tells the work: a state that fails while an entry ahead of it is still being tested is held until those ahead settle, so a broken change is blamed on itself and the entries tested with it are tested again instead of being taken out; an entry whose own checks pass but which fails with several entries ahead has smaller combinations (the default branch, half of the suspects and the entry) built and checked by the merge_group workflows, halving the suspects within three extra builds, and never landed. The result goes on the pull request's and its issue's timelines and into its session, as an integration_failure decision record (tested), and as integration.failed with the pull request, its issue, the combination, the failing check's name, conclusion and link, whether it was bisected, the suspects and the other pull requests; a combination that passes is an integration_passed decision and integration.passed. An entry the queue took out keeps its merge request: once a new commit's checks all pass, g1t merges it again as whoever queued it, which puts it back in the queue under the branch's rules as they are then. | 524 | const PULL_WRITES: &[&str] = &["record_session", "record_decision", "mark_pull_request_ready"]; |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 525 | |
| 526 | /// Whether something was allowed, and the rule that decided it. | |
| 527 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 528 | pub struct Decision { | |
| 529 | pub allowed: bool, | |
| 530 | /// A short, stable name: `run:implement/tools`, `never`, | |
| 531 | /// `scope:repository` and so on. Shown in the audit log. | |
| 532 | pub rule: String, | |
| 533 | /// Why it was refused, for the caller. | |
| 534 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 535 | pub reason: Option<String>, | |
| 536 | } | |
| 537 | ||
| 538 | impl Decision { | |
| 539 | pub fn allow(rule: impl Into<String>) -> Self { | |
| 540 | Decision { | |
| 541 | allowed: true, | |
| 542 | rule: rule.into(), | |
| 543 | reason: None, | |
| 544 | } | |
| 545 | } | |
| 546 | ||
| 547 | pub fn deny(rule: impl Into<String>, reason: impl Into<String>) -> Self { | |
| 548 | Decision { | |
| 549 | allowed: false, | |
| 550 | rule: rule.into(), | |
| 551 | reason: Some(reason.into()), | |
| 552 | } | |
| 553 | } | |
| 554 | } | |
| 555 | ||
| 556 | fn same_repo(a: &RepoPath, b: &RepoPath) -> bool { | |
| 557 | a.namespace.eq_ignore_ascii_case(&b.namespace) && a.name.eq_ignore_ascii_case(&b.name) | |
| 558 | } | |
| 559 | ||
| 560 | fn scope_rule(scope: &AgentScope) -> String { | |
| 561 | match &scope.run { | |
| 562 | Some(run) => format!("run:{}/{}", run.kind.as_str(), run.usage.as_str()), | |
| 563 | None => "agent-token".to_owned(), | |
| 564 | } | |
| 565 | } | |
| 566 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 567 | /// Whether `user`, resolved from a run credential with `scope`, may use |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 568 | /// `operation`. `repo` is the repository the call names, if any, and |
| 569 | /// `needs_repo` whether the operation is about one; `number` the issue or | |
| 570 | /// pull request it names. | |
| 571 | pub fn decide_operation( | |
| 572 | user: &User, | |
| 573 | scope: &AgentScope, | |
| 574 | operation: &str, | |
| 575 | repo: Option<&RepoPath>, | |
| 576 | needs_repo: bool, | |
| 577 | number: Option<u32>, | |
| 578 | ) -> Decision { | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 579 | let who = "A run's token"; |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 580 | if NEVER.contains(&operation) { |
| 581 | return Decision::deny( | |
| 582 | "never", | |
| 583 | format!( | |
| 584 | "{who} can never use {operation}: settings, members, tokens, billing, integrations, webhooks, secrets and merging are for people." | |
| 585 | ), | |
| 586 | ); | |
| 587 | } | |
| 588 | if !scope.operations.iter().any(|name| name == operation) { | |
| 589 | return Decision::deny( | |
| 590 | "scope:operation", | |
| 591 | format!("{who} for this run cannot use {operation}."), | |
| 592 | ); | |
| 593 | } | |
| 594 | if needs_repo && !repo.is_some_and(|asked| same_repo(asked, &scope.repo)) { | |
| 595 | return Decision::deny( | |
| 596 | "scope:repository", | |
| 597 | format!( | |
| 598 | "{who} works in {}/{} only.", | |
| 599 | scope.repo.namespace, scope.repo.name | |
| 600 | ), | |
| 601 | ); | |
| 602 | } | |
| 603 | // The intersection: the person it acts for must still be able to work | |
| 604 | // in the repository's workspace, as a member or with a role on its | |
| 605 | // repositories. What it may do in the repository itself is their | |
| 606 | // role there, which services check (`access::can`). | |
| 607 | if !crate::access::has_access_in(user, &scope.repo.namespace) { | |
| 608 | return Decision::deny( | |
| 609 | "on-behalf-of:membership", | |
| 610 | format!( | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 611 | "The person this run works for is no longer a member of {}.", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 612 | scope.repo.namespace |
| 613 | ), | |
| 614 | ); | |
| 615 | } | |
| 616 | if let Some(run) = &scope.run | |
| 617 | && run.usage == CredentialUse::Runner | |
| 618 | && PULL_WRITES.contains(&operation) | |
| 619 | && run.number.is_some() | |
| 620 | && number != run.number | |
| 621 | { | |
| 622 | return Decision::deny( | |
| 623 | "scope:pull", | |
| 624 | format!( | |
| 625 | "{who} can change pull request #{} only.", | |
| 626 | run.number.unwrap_or_default() | |
| 627 | ), | |
| 628 | ); | |
| 629 | } | |
| 630 | Decision::allow(scope_rule(scope)) | |
| 631 | } | |
| 632 | ||
| 633 | /// Whether a run credential may clone or fetch (`write` false), or push to | |
| 634 | /// (`write` true), the repository at `repo`. | |
| 635 | pub fn decide_git(scope: &AgentScope, repo: &RepoPath, write: bool) -> Decision { | |
| 636 | let Some(run) = scope | |
| 637 | .run | |
| 638 | .as_ref() | |
| 639 | .filter(|run| run.usage == CredentialUse::Runner) | |
| 640 | else { | |
| 641 | return Decision::deny( | |
| 642 | "git:not-a-run", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 643 | "A tools token cannot be used with git.", |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 644 | ); |
| 645 | }; | |
| 646 | let pushable = run.push.iter().any(|grant| same_repo(&grant.repo, repo)); | |
| 647 | if write { | |
| 648 | return if pushable { | |
| 649 | Decision::allow(format!("{}:push", scope_rule(scope))) | |
| 650 | } else { | |
| 651 | Decision::deny( | |
| 652 | "git:push", | |
| 653 | format!( | |
| 654 | "A {} run cannot push to {}/{}.", | |
| 655 | run.kind.as_str(), | |
| 656 | repo.namespace, | |
| 657 | repo.name | |
| 658 | ), | |
| 659 | ) | |
| 660 | }; | |
| 661 | } | |
| 662 | let readable = pushable | |
| 663 | || same_repo(&scope.repo, repo) | |
| 664 | || run.read.iter().any(|path| same_repo(path, repo)); | |
| 665 | if readable { | |
| 666 | Decision::allow(format!("{}:read", scope_rule(scope))) | |
| 667 | } else { | |
| 668 | Decision::deny( | |
| 669 | "git:read", | |
| 670 | format!( | |
| 671 | "A {} run cannot read {}/{}.", | |
| 672 | run.kind.as_str(), | |
| 673 | repo.namespace, | |
| 674 | repo.name | |
| 675 | ), | |
| 676 | ) | |
| 677 | } | |
| 678 | } | |
| 679 | ||
| 680 | /// Whether a push to `repo` is limited to certain branches, so that the | |
| 681 | /// refs it moves have to be read and checked with [`decide_refs`]. | |
| 682 | pub fn limits_branches(scope: &AgentScope, repo: &RepoPath) -> bool { | |
| 683 | scope | |
| 684 | .run | |
| 685 | .iter() | |
| 686 | .flat_map(|run| run.push.iter()) | |
| 687 | .any(|grant| same_repo(&grant.repo, repo) && grant.branch.is_some()) | |
| 688 | } | |
| 689 | ||
| 690 | /// Whether a push to `repo` may move `refs` (full refs, such as | |
| 691 | /// `refs/heads/main`). Tags are never a run's to move. | |
| 692 | pub fn decide_refs(scope: &AgentScope, repo: &RepoPath, refs: &[String]) -> Decision { | |
| 693 | let repo_decision = decide_git(scope, repo, true); | |
| 694 | if !repo_decision.allowed { | |
| 695 | return repo_decision; | |
| 696 | } | |
| 697 | let grants: Vec<&GitGrant> = scope | |
| 698 | .run | |
| 699 | .iter() | |
| 700 | .flat_map(|run| run.push.iter()) | |
| 701 | .filter(|grant| same_repo(&grant.repo, repo)) | |
| 702 | .collect(); | |
| 703 | for git_ref in refs { | |
| 704 | let Some(branch) = git_ref.strip_prefix("refs/heads/") else { | |
| 705 | return Decision::deny("git:ref", format!("A run cannot push {git_ref}.")); | |
| 706 | }; | |
| 707 | let allowed = grants | |
| 708 | .iter() | |
| 709 | .any(|grant| grant.branch.as_deref().is_none_or(|only| only == branch)); | |
| 710 | if !allowed { | |
| 711 | return Decision::deny( | |
| 712 | "git:ref", | |
| 713 | format!( | |
| 714 | "A run cannot push to {branch} in {}/{}.", | |
| 715 | repo.namespace, repo.name | |
| 716 | ), | |
| 717 | ); | |
| 718 | } | |
| 719 | } | |
| 720 | repo_decision | |
| 721 | } | |
| 722 | ||
| 723 | /// The most an agent may be on a repository, whoever it works for: it | |
| 724 | /// can push, merge and run, never change settings or who has access. | |
| 725 | pub const AGENT_CEILING: RepoRole = RepoRole::Write; | |
| 726 | ||
| 727 | /// The memberships an agent working for `person` has: the run's | |
| 728 | /// workspace, as a member, only if the person is in it now, with the | |
| 729 | /// person's role on its repositories (an owner's Admin included) cut down | |
| 730 | /// to [`AGENT_CEILING`]. | |
| 731 | pub fn intersect(person: &[Membership], namespace: &str) -> Vec<Membership> { | |
| 732 | let namespace = namespace.to_lowercase(); | |
| 733 | person | |
| 734 | .iter() | |
| 735 | .filter(|membership| membership.slug == namespace) | |
| 736 | .map(|membership| { | |
| 737 | let base = match membership.role { | |
| 738 | Role::Owner => BasePermission::Admin, | |
| 739 | Role::Member => membership.base_permission.unwrap_or_default(), | |
| 740 | }; | |
| 741 | Membership { | |
| 742 | role: Role::Member, | |
| 743 | base_permission: Some(match base { | |
| 744 | BasePermission::Admin => BasePermission::Write, | |
| 745 | base => base, | |
| 746 | }), | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 747 | // Billing and security manager are the person's, never |
| 748 | // their agent's. | |
| 749 | org_roles: Vec::new(), | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 750 | ..membership.clone() |
| 751 | } | |
| 752 | }) | |
| 753 | .collect() | |
| 754 | } | |
| 755 | ||
| 756 | /// The repository grants an agent working for `person` has: those in the | |
| 757 | /// run's workspace, each cut down to [`AGENT_CEILING`]. | |
| 758 | pub fn intersect_grants(person: &[RepoGrant], namespace: &str) -> Vec<RepoGrant> { | |
| 759 | let namespace = namespace.to_lowercase(); | |
| 760 | person | |
| 761 | .iter() | |
| 762 | .filter(|grant| grant.workspace == namespace) | |
| 763 | .map(|grant| RepoGrant { | |
| 764 | role: grant.role.min(AGENT_CEILING), | |
| 765 | ..grant.clone() | |
| 766 | }) | |
| 767 | .collect() | |
| 768 | } | |
| 769 | ||
| 770 | /// Who a runner's credential acts as downstream: the person, with only the | |
| 771 | /// agent's (already intersected) memberships. `None` for anything else. | |
| 772 | pub fn as_person(user: &User) -> Option<User> { | |
| 773 | let acting = user.acting.as_ref()?; | |
| 774 | if user.kind != PrincipalKind::Agent { | |
| 775 | return None; | |
| 776 | } | |
| 777 | let run = acting.run()?; | |
| 778 | if run.usage != CredentialUse::Runner { | |
| 779 | return None; | |
| 780 | } | |
| 781 | Some(User { | |
| 782 | id: acting.on_behalf_of.id.clone(), | |
| 783 | username: acting.on_behalf_of.username.clone(), | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 784 | display_username: None, |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 785 | kind: PrincipalKind::User, |
| 786 | verified: user.verified, | |
| 787 | workspaces: user.workspaces.clone(), | |
| 788 | avatar: None, | |
| 789 | acting: None, | |
| 790 | grants: user.grants.clone(), | |
| 791 | token: None, | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 792 | held: Vec::new(), |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 793 | }) |
| 794 | } | |
| 795 | ||
| 796 | /// How an actor is described: "g1t on behalf of syntaqx". | |
| 797 | pub fn describe(user: &User) -> String { | |
| 798 | match &user.acting { | |
| 799 | Some(acting) => format!( | |
| 800 | "{} on behalf of {}", | |
| 801 | acting.agent, acting.on_behalf_of.username | |
| 802 | ), | |
| 803 | None => user.username.clone(), | |
| 804 | } | |
| 805 | } | |
| 806 | ||
| 807 | #[cfg(test)] | |
| 808 | mod tests { | |
| 809 | use super::*; | |
| 810 | ||
| 811 | #[test] | |
| 812 | fn a_run_s_scopes_are_never_admin() { | |
| 813 | for kind in RunCredentialKind::ALL { | |
| 814 | for usage in [CredentialUse::Runner, CredentialUse::Tools] { | |
| 815 | let scopes = run_scopes(kind, usage); | |
| 816 | assert!(scopes.iter().all(|scope| !scope.dangerous()), "{kind:?} {usage:?}: {scopes:?}"); | |
| 817 | } | |
| 818 | } | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 819 | use crate::scopes::Scope; |
| 820 | let bump = run_scopes(RunCredentialKind::Bump, CredentialUse::Runner); | |
| 821 | assert!(bump.len() == 2 && bump.contains(&Scope::CodeRead) && bump.contains(&Scope::CodeWrite)); | |
| 822 | assert_eq!(run_scopes(RunCredentialKind::Checks, CredentialUse::Runner), [Scope::CodeRead]); | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 823 | } |
| 824 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 825 | /// The hosted agents' kinds still parse, so stored tokens read, but |
| 826 | /// they grant nothing: no operations, no scopes. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 827 | #[test] |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 828 | fn retired_kinds_parse_and_grant_nothing() { |
| 829 | for kind in RunCredentialKind::ALL { | |
| 830 | let parsed: RunCredentialKind = serde_json::from_value(serde_json::json!(kind.as_str())).unwrap(); | |
| 831 | assert_eq!(parsed, kind); | |
| 832 | for usage in [CredentialUse::Runner, CredentialUse::Tools] { | |
| 833 | assert!(operations_for(kind, usage).is_empty(), "{kind:?} {usage:?}"); | |
| 834 | if kind.retired() { | |
| 835 | assert!(run_scopes(kind, usage).is_empty(), "{kind:?} {usage:?}"); | |
| 836 | } | |
| 837 | } | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 838 | } |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 839 | let retired: Vec<&str> = RunCredentialKind::ALL.iter().filter(|kind| kind.retired()).map(|kind| kind.as_str()).collect(); |
| 840 | assert_eq!(retired, ["implement", "revise", "review", "answer", "update", "plan"]); | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 841 | } |
| 842 | ||
| 843 | #[test] | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 844 | fn site_wide_search_only_reads() { |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 845 | assert!(is_read("search")); |
| 846 | assert!(!NEVER.contains(&"search")); | |
| 847 | } | |
| 848 | ||
| 849 | fn path(namespace: &str, name: &str) -> RepoPath { | |
| 850 | RepoPath { | |
| 851 | namespace: namespace.to_owned(), | |
| 852 | name: name.to_owned(), | |
| 853 | } | |
| 854 | } | |
| 855 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 856 | /// What a token stored before the hosted agents were retired lists: |
| 857 | /// the operations it was made with, which it keeps until it expires. | |
| 858 | fn stored_operations(kind: RunCredentialKind, usage: CredentialUse) -> Vec<String> { | |
| 859 | use RunCredentialKind as K; | |
| 860 | let names: Vec<&str> = match usage { | |
| 861 | CredentialUse::Runner => match kind { | |
| 862 | K::Implement => vec!["get_repo", "get_pull_request", "record_session", "mark_pull_request_ready"], | |
| 863 | K::Revise | K::Answer | K::Update => vec!["get_repo", "get_pull_request", "record_session"], | |
| 864 | _ => vec![], | |
| 865 | }, | |
| 866 | CredentialUse::Tools => match kind { | |
| 867 | K::Implement | K::Revise | K::Answer => vec!["get_issue", "get_pull_request", "create_issue", "add_comment", "remember"], | |
| 868 | K::Review => vec!["get_issue", "get_pull_request", "add_comment", "review_pull_request"], | |
| 869 | K::Plan => vec!["get_issue", "get_pull_request", "create_issue"], | |
| 870 | K::Update => vec!["get_issue", "get_pull_request"], | |
| 871 | _ => vec![], | |
| 872 | }, | |
| 873 | }; | |
| 874 | names.into_iter().map(str::to_owned).collect() | |
| 875 | } | |
| 876 | ||
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 877 | fn scope(kind: RunCredentialKind, usage: CredentialUse) -> AgentScope { |
| 878 | AgentScope { | |
| 879 | repo: path("acme", "rocket"), | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 880 | operations: stored_operations(kind, usage), |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 881 | run: Some(RunBinding { |
| 882 | kind, | |
| 883 | usage, | |
| 884 | run_id: Some("run_1".to_owned()), | |
| 885 | number: Some(7), | |
| 886 | agent: "g1t".to_owned(), | |
| 887 | system: false, | |
| 888 | read: vec![path("acme", "rocket")], | |
| 889 | push: match kind { | |
| 890 | RunCredentialKind::Implement | |
| 891 | | RunCredentialKind::Revise | |
| 892 | | RunCredentialKind::Answer => vec![GitGrant { | |
| 893 | repo: path("pulls", "pul_7"), | |
| 894 | branch: None, | |
| 895 | }], | |
| 896 | RunCredentialKind::Update => vec![GitGrant { | |
| 897 | repo: path("acme", "rocket"), | |
| 898 | branch: Some("fix-login".to_owned()), | |
| 899 | }], | |
| 900 | _ => vec![], | |
| 901 | }, | |
| 902 | }), | |
| 903 | } | |
| 904 | } | |
| 905 | ||
| 906 | fn agent(member_of: &[&str], scope: AgentScope) -> User { | |
| 907 | User { | |
| 908 | id: "usr_g1t_agent".to_owned(), | |
| 909 | username: "g1t".to_owned(), | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 910 | display_username: None, |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 911 | kind: PrincipalKind::Agent, |
| 912 | verified: true, | |
| 913 | workspaces: member_of | |
| 914 | .iter() | |
| 915 | .map(|slug| Membership::member(*slug)) | |
| 916 | .collect(), | |
| 917 | avatar: None, | |
| 918 | grants: Vec::new(), | |
| 919 | token: None, | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 920 | held: Vec::new(), |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 921 | acting: Some(Box::new(Acting { |
| 922 | credential_id: "tok_1".to_owned(), | |
| 923 | agent: "g1t".to_owned(), | |
| 924 | on_behalf_of: Principal { | |
| 925 | id: "usr_1".to_owned(), | |
| 926 | username: "syntaqx".to_owned(), | |
| 927 | }, | |
| 928 | scope, | |
| 929 | })), | |
| 930 | } | |
| 931 | } | |
| 932 | ||
| 933 | fn op(kind: RunCredentialKind, usage: CredentialUse, operation: &str) -> Decision { | |
| 934 | let scope = scope(kind, usage); | |
| 935 | let user = agent(&["acme"], scope.clone()); | |
| 936 | decide_operation( | |
| 937 | &user, | |
| 938 | &scope, | |
| 939 | operation, | |
| 940 | Some(&path("acme", "rocket")), | |
| 941 | true, | |
| 942 | Some(7), | |
| 943 | ) | |
| 944 | } | |
| 945 | ||
| 946 | use CredentialUse::{Runner, Tools}; | |
| 947 | use RunCredentialKind as K; | |
| 948 | ||
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 949 | /// A stored token may use only what it lists, and never what no run |
| 950 | /// may. | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 951 | #[test] |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 952 | fn a_stored_token_uses_only_what_it_lists() { |
| 953 | assert!(op(K::Implement, Tools, "create_issue").allowed); | |
| 954 | assert!(op(K::Review, Tools, "review_pull_request").allowed); | |
| 955 | assert!(!op(K::Review, Tools, "create_issue").allowed); | |
| 956 | assert!(!op(K::Plan, Tools, "add_comment").allowed); | |
| 957 | assert!(!op(K::Checks, Tools, "get_issue").allowed); | |
| 958 | for operation in ["record_session", "merge_pull_request", "update_repo_settings", "create_webhook", "set_actions_secret"] { | |
| 959 | for kind in [K::Implement, K::Revise, K::Answer, K::Review, K::Plan, K::Checks] { | |
| 960 | assert!(!op(kind, Tools, operation).allowed, "{operation} by a {} run's tools", kind.as_str()); | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 961 | } |
| 962 | } | |
| 963 | } | |
| 964 | ||
| 965 | #[test] | |
| 966 | fn runner_matrix() { | |
| 967 | assert!(op(K::Implement, Runner, "record_session").allowed); | |
| 968 | assert!(op(K::Implement, Runner, "mark_pull_request_ready").allowed); | |
| 969 | assert!(op(K::Revise, Runner, "record_session").allowed); | |
| 970 | assert!(!op(K::Revise, Runner, "mark_pull_request_ready").allowed); | |
| 971 | assert!(!op(K::Implement, Runner, "create_issue").allowed); | |
| 972 | assert!(!op(K::Review, Runner, "record_session").allowed); | |
| 973 | assert!(!op(K::Checks, Runner, "get_issue").allowed); | |
| 974 | } | |
| 975 | ||
| 976 | #[test] | |
| 977 | fn settings_billing_tokens_and_members_are_never_reachable() { | |
| 978 | for kind in RunCredentialKind::ALL { | |
| 979 | for usage in [Runner, Tools] { | |
| 980 | for operation in NEVER.iter().copied() { | |
| 981 | let decision = op(kind, usage, operation); | |
| 982 | assert!(!decision.allowed); | |
| 983 | assert_eq!(decision.rule, "never"); | |
| 984 | } | |
| 985 | } | |
| 986 | } | |
| 987 | // Even a scope that lists one is refused. | |
| 988 | let mut wide = scope(K::Implement, Tools); | |
| 989 | wide.operations.push("merge_pull_request".to_owned()); | |
| 990 | let user = agent(&["acme"], wide.clone()); | |
| 991 | let decision = decide_operation( | |
| 992 | &user, | |
| 993 | &wide, | |
| 994 | "merge_pull_request", | |
| 995 | Some(&path("acme", "rocket")), | |
| 996 | true, | |
| 997 | Some(7), | |
| 998 | ); | |
| 999 | assert_eq!(decision.rule, "never"); | |
| 1000 | } | |
| 1001 | ||
| 1002 | #[test] | |
| 1003 | fn another_repository_is_refused() { | |
| 1004 | let scope = scope(K::Implement, Tools); | |
| 1005 | let user = agent(&["acme"], scope.clone()); | |
| 1006 | let decision = decide_operation( | |
| 1007 | &user, | |
| 1008 | &scope, | |
| 1009 | "create_issue", | |
| 1010 | Some(&path("acme", "other")), | |
| 1011 | true, | |
| 1012 | None, | |
| 1013 | ); | |
| 1014 | assert!(!decision.allowed); | |
| 1015 | assert_eq!(decision.rule, "scope:repository"); | |
| 1016 | let decision = decide_operation(&user, &scope, "create_issue", None, true, None); | |
| 1017 | assert_eq!(decision.rule, "scope:repository"); | |
| 1018 | // The repository's name is matched without regard to case. | |
| 1019 | let decision = decide_operation( | |
| 1020 | &user, | |
| 1021 | &scope, | |
| 1022 | "create_issue", | |
| 1023 | Some(&path("Acme", "Rocket")), | |
| 1024 | true, | |
| 1025 | None, | |
| 1026 | ); | |
| 1027 | assert!(decision.allowed); | |
| 1028 | assert_eq!(decision.rule, "run:implement/tools"); | |
| 1029 | } | |
| 1030 | ||
| 1031 | #[test] | |
| 1032 | fn the_permission_is_the_intersection_with_the_person() { | |
| 1033 | let scope = scope(K::Implement, Tools); | |
| 1034 | // The person left the workspace: their agent can do nothing there. | |
| 1035 | let user = agent(&[], scope.clone()); | |
| 1036 | let decision = decide_operation( | |
| 1037 | &user, | |
| 1038 | &scope, | |
| 1039 | "get_issue", | |
| 1040 | Some(&path("acme", "rocket")), | |
| 1041 | true, | |
| 1042 | Some(1), | |
| 1043 | ); | |
| 1044 | assert!(!decision.allowed); | |
| 1045 | assert_eq!(decision.rule, "on-behalf-of:membership"); | |
| 1046 | // And an owner's agent is only ever a member. | |
| 1047 | let owner = vec![ | |
| 1048 | Membership { | |
| 1049 | slug: "acme".to_owned(), | |
| 1050 | role: Role::Owner, | |
| 1051 | name: None, | |
| 1052 | avatar: None, | |
| 1053 | base_permission: Some(BasePermission::None), | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 1054 | team_creation: None, |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 1055 | org_roles: vec![crate::OrgRole::SecurityManager], |
| 1056 | privileges: None, | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 1057 | }, |
| 1058 | Membership::member("elsewhere"), | |
| 1059 | ]; | |
| 1060 | let memberships = intersect(&owner, "Acme"); | |
| 1061 | assert_eq!(memberships.len(), 1); | |
| 1062 | assert_eq!(memberships[0].slug, "acme"); | |
| 1063 | assert_eq!(memberships[0].role, Role::Member); | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 1064 | assert!(memberships[0].org_roles.is_empty()); |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 1065 | assert!(intersect(&owner, "nowhere").is_empty()); |
| 1066 | } | |
| 1067 | ||
| 1068 | /// An agent gets at most the person's role on the repository, and | |
| 1069 | /// never more than Write; nothing outside the run's workspace. | |
| 1070 | #[test] | |
| 1071 | fn an_agent_has_at_most_its_persons_role() { | |
| 1072 | use crate::access::{Capability, RepoRef, can, permission}; | |
| 1073 | let rocket = RepoRef { id: "rep_1", namespace: "acme", private: true }; | |
| 1074 | let other = RepoRef { id: "rep_2", namespace: "acme", private: true }; | |
| 1075 | let elsewhere = RepoRef { id: "rep_3", namespace: "globex", private: true }; | |
| 1076 | let tools = scope(K::Implement, Tools); | |
| 1077 | let scope = scope(K::Implement, Runner); | |
| 1078 | // An owner's agent: Write, never Admin. | |
| 1079 | let owner = [Membership { role: Role::Owner, ..Membership::member("acme") }, Membership::member("globex")]; | |
| 1080 | let mut agent_user = agent(&[], scope.clone()); | |
| 1081 | agent_user.workspaces = intersect(&owner, "acme"); | |
| 1082 | assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write)); | |
| 1083 | assert!(!can(Some(&agent_user), rocket, Capability::ManageSettings)); | |
| 1084 | assert_eq!(permission(Some(&agent_user), elsewhere), None); | |
| 1085 | // A member whose workspace gives Read: Read, so it cannot push. | |
| 1086 | let reader = [Membership { base_permission: Some(BasePermission::Read), ..Membership::member("acme") }]; | |
| 1087 | agent_user.workspaces = intersect(&reader, "acme"); | |
| 1088 | assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Read)); | |
| 1089 | assert!(!can(Some(&agent_user), rocket, Capability::Push)); | |
| 1090 | // An outside collaborator with Maintain on one repository: Write | |
| 1091 | // there, nothing elsewhere, and the run is allowed. | |
| 1092 | let grants = [ | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1093 | RepoGrant { repo_id: "rep_1".into(), workspace: "acme".into(), role: RepoRole::Maintain, team: None }, |
| 1094 | RepoGrant { repo_id: "rep_3".into(), workspace: "globex".into(), role: RepoRole::Admin, team: None }, | |
| Cargo on g1t.sh: a sparse registry per workspace, cargo publish/add/yank/search with a g1t token; npm rows show their version | 1095 | ]; |
| 1096 | agent_user.workspaces = intersect(&[], "acme"); | |
| 1097 | agent_user.grants = intersect_grants(&grants, "Acme"); | |
| 1098 | assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write)); | |
| 1099 | assert_eq!(permission(Some(&agent_user), other), None); | |
| 1100 | assert_eq!(permission(Some(&agent_user), elsewhere), None); | |
| 1101 | let decision = decide_operation(&agent_user, &tools, "get_issue", Some(&path("acme", "rocket")), true, Some(1)); | |
| 1102 | assert!(decision.allowed, "{}", decision.reason.unwrap_or_default()); | |
| 1103 | // The person, downstream of a runner's credential, carries the same. | |
| 1104 | let person = as_person(&agent_user).expect("a runner acts as the person"); | |
| 1105 | assert_eq!(permission(Some(&person), rocket), Some(RepoRole::Write)); | |
| 1106 | } | |
| 1107 | ||
| 1108 | #[test] | |
| 1109 | fn a_runner_changes_only_its_own_pull_request() { | |
| 1110 | let scope = scope(K::Implement, Runner); | |
| 1111 | let user = agent(&["acme"], scope.clone()); | |
| 1112 | let repo = path("acme", "rocket"); | |
| 1113 | let other = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(8)); | |
| 1114 | assert!(!other.allowed); | |
| 1115 | assert_eq!(other.rule, "scope:pull"); | |
| 1116 | let own = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(7)); | |
| 1117 | assert!(own.allowed); | |
| 1118 | // Reading another is fine. | |
| 1119 | assert!( | |
| 1120 | decide_operation( | |
| 1121 | &user, | |
| 1122 | &scope, | |
| 1123 | "get_pull_request", | |
| 1124 | Some(&repo), | |
| 1125 | true, | |
| 1126 | Some(8) | |
| 1127 | ) | |
| 1128 | .allowed | |
| 1129 | ); | |
| 1130 | } | |
| 1131 | ||
| 1132 | #[test] | |
| 1133 | fn git_matrix() { | |
| 1134 | let fork = path("pulls", "pul_7"); | |
| 1135 | let upstream = path("acme", "rocket"); | |
| 1136 | let elsewhere = path("acme", "billing"); | |
| 1137 | let implement = scope(K::Implement, Runner); | |
| 1138 | assert!(decide_git(&implement, &fork, true).allowed); | |
| 1139 | assert!(decide_git(&implement, &fork, false).allowed); | |
| 1140 | assert!(decide_git(&implement, &upstream, false).allowed); | |
| 1141 | assert_eq!(decide_git(&implement, &upstream, true).rule, "git:push"); | |
| 1142 | assert_eq!(decide_git(&implement, &elsewhere, false).rule, "git:read"); | |
| 1143 | let review = scope(K::Review, Runner); | |
| 1144 | assert!(decide_git(&review, &upstream, false).allowed); | |
| 1145 | assert!(!decide_git(&review, &upstream, true).allowed); | |
| 1146 | assert!(!decide_git(&review, &fork, true).allowed); | |
| 1147 | // A tools token made before run credentials never reaches git. | |
| 1148 | let old = AgentScope { | |
| 1149 | repo: upstream.clone(), | |
| 1150 | operations: vec!["get_issue".to_owned()], | |
| 1151 | run: None, | |
| 1152 | }; | |
| 1153 | assert_eq!(decide_git(&old, &upstream, false).rule, "git:not-a-run"); | |
| 1154 | // Nor does an agent's tools token. | |
| 1155 | assert_eq!( | |
| 1156 | decide_git(&scope(K::Implement, Tools), &upstream, false).rule, | |
| 1157 | "git:not-a-run" | |
| 1158 | ); | |
| 1159 | } | |
| 1160 | ||
| 1161 | #[test] | |
| 1162 | fn a_push_moves_only_granted_branches() { | |
| 1163 | let update = scope(K::Update, Runner); | |
| 1164 | let repo = path("acme", "rocket"); | |
| 1165 | let refs = |names: &[&str]| { | |
| 1166 | names | |
| 1167 | .iter() | |
| 1168 | .map(|name| (*name).to_owned()) | |
| 1169 | .collect::<Vec<_>>() | |
| 1170 | }; | |
| 1171 | assert!(decide_refs(&update, &repo, &refs(&["refs/heads/fix-login"])).allowed); | |
| 1172 | assert_eq!( | |
| 1173 | decide_refs(&update, &repo, &refs(&["refs/heads/main"])).rule, | |
| 1174 | "git:ref" | |
| 1175 | ); | |
| 1176 | assert_eq!( | |
| 1177 | decide_refs( | |
| 1178 | &update, | |
| 1179 | &repo, | |
| 1180 | &refs(&["refs/heads/fix-login", "refs/tags/v1"]) | |
| 1181 | ) | |
| 1182 | .rule, | |
| 1183 | "git:ref" | |
| 1184 | ); | |
| 1185 | let implement = scope(K::Implement, Runner); | |
| 1186 | assert!( | |
| 1187 | decide_refs( | |
| 1188 | &implement, | |
| 1189 | &path("pulls", "pul_7"), | |
| 1190 | &refs(&["refs/heads/main"]) | |
| 1191 | ) | |
| 1192 | .allowed | |
| 1193 | ); | |
| 1194 | } | |
| 1195 | ||
| 1196 | #[test] | |
| 1197 | fn a_runner_acts_downstream_as_the_person() { | |
| 1198 | let user = agent(&["acme"], scope(K::Implement, Runner)); | |
| 1199 | let person = as_person(&user).unwrap(); | |
| 1200 | assert_eq!(person.id, "usr_1"); | |
| 1201 | assert_eq!(person.username, "syntaqx"); | |
| 1202 | assert_eq!(person.kind, PrincipalKind::User); | |
| 1203 | assert!(person.is_member("acme")); | |
| 1204 | assert!(person.acting.is_none()); | |
| 1205 | assert_eq!(describe(&user), "g1t on behalf of syntaqx"); | |
| 1206 | // The tools act as the agent. | |
| 1207 | assert!(as_person(&agent(&["acme"], scope(K::Implement, Tools))).is_none()); | |
| 1208 | } | |
| 1209 | ||
| 1210 | #[test] | |
| 1211 | fn scopes_without_a_run_still_parse() { | |
| 1212 | let old: AgentScope = serde_json::from_str( | |
| 1213 | r#"{"repo":{"namespace":"acme","name":"rocket"},"operations":["get_issue"]}"#, | |
| 1214 | ) | |
| 1215 | .unwrap(); | |
| 1216 | assert!(old.run.is_none()); | |
| 1217 | let written = serde_json::to_string(&scope(K::Review, Tools)).unwrap(); | |
| 1218 | assert!(written.contains(r#""use":"tools""#)); | |
| 1219 | assert!(written.contains(r#""kind":"review""#)); | |
| 1220 | let back: AgentScope = serde_json::from_str(&written).unwrap(); | |
| 1221 | assert_eq!(back.run.unwrap().kind, K::Review); | |
| 1222 | // Only g1t's own runs say so; every other reads as not. | |
| 1223 | assert!(!written.contains("system")); | |
| 1224 | assert!(!back_run(&written).system); | |
| 1225 | let mut own = scope(K::Bump, Runner); | |
| 1226 | own.run.as_mut().unwrap().system = true; | |
| 1227 | let written = serde_json::to_string(&own).unwrap(); | |
| 1228 | assert!(written.contains(r#""system":true"#)); | |
| 1229 | assert!(back_run(&written).system); | |
| 1230 | } | |
| 1231 | ||
| 1232 | fn back_run(written: &str) -> RunBinding { | |
| 1233 | serde_json::from_str::<AgentScope>(written).unwrap().run.unwrap() | |
| 1234 | } | |
| 1235 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.