Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,233 linesCodeBlameRaw

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.

Mission control counts people's direct pushes to the default branch1//! 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`]).
Mission control counts people's direct pushes to the default branch16//!
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
21use serde::{Deserialize, Serialize};
22
23use crate::identity::AgentScope;
24use crate::repos::RepoPath;
25use crate::access::{BasePermission, RepoGrant, RepoRole};
26use 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.
Mission control counts people's direct pushes to the default branch29#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub 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.
Mission control counts people's direct pushes to the default branch35 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`].
Mission control counts people's direct pushes to the default branch37 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`].
Mission control counts people's direct pushes to the default branch39 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`].
Mission control counts people's direct pushes to the default branch41 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`].
Mission control counts people's direct pushes to the default branch43 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`].
Mission control counts people's direct pushes to the default branch45 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
55impl 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 {
Mission control counts people's direct pushes to the default branch90 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
Mission control counts people's direct pushes to the default branch95 | 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
Mission control counts people's direct pushes to the default branch98 )
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")]
105pub 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.
Mission control counts people's direct pushes to the default branch109 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.
Mission control counts people's direct pushes to the default branch112 Tools,
113}
114
115impl 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)]
126pub 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.
Mission control counts people's direct pushes to the default branch136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub 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.
Mission control counts people's direct pushes to the default branch143 #[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`.
Mission control counts people's direct pushes to the default branch149 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
Mission control counts people's direct pushes to the default branch157 /// 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)]
166pub struct Principal {
167 pub id: String,
168 pub username: String,
169}
170
171impl 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
Mission control counts people's direct pushes to the default branch181/// identity, "g1t on behalf of syntaqx", and what it may do.
182#[derive(Clone, Debug, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub 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
192impl 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`.
Mission control counts people's direct pushes to the default branch203#[derive(Clone, Debug, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub 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.
Mission control counts people's direct pushes to the default branch222#[derive(Clone, Debug, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub 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")]
234pub 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.
244pub const READ_OPERATIONS: &[&str] = &[
245 "whoami",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit246 "get_usage",
247 "get_budget",
248 "list_invoices",
249 "get_billing_details",
Mission control counts people's direct pushes to the default branch250 "list_repos",
251 "get_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97252 "list_projects",
253 "get_project",
Mission control counts people's direct pushes to the default branch254 "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 bar260 "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",
Mission control counts people's direct pushes to the default branch267 "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 bar274 "list_issue_labels",
275 "list_milestones",
276 "get_milestone",
Mission control counts people's direct pushes to the default branch277 "list_pull_requests",
278 "get_pull_request",
279 "read_session",
280 "get_pull_request_changes",
281 "list_events",
282 "get_context",
283 "search",
284 "list_workflows",
285 "list_workflow_runs",
286 "get_workflow_run",
287 "get_job_logs",
Merge branch 'worktree-agent-a3abfcce648e87dca'288 "get_pending_deployments",
289 "get_workflow_permissions",
290 "get_fork_pr_approval",
Merge checks: statuses and check runs on every commit291 "list_commit_statuses",
292 "get_combined_status",
293 "list_check_runs_for_ref",
294 "get_check_run",
295 "list_check_run_annotations",
296 "list_check_suites_for_ref",
297 "get_check_suite",
Mission control counts people's direct pushes to the default branch298 "list_integrations",
299 "list_webhooks",
300 "list_webhook_deliveries",
301 "list_actions_secrets",
302 "list_actions_variables",
303 "list_security_alerts",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar304 "list_secret_scanning_alerts",
305 "get_secret_scanning_alert",
306 "list_secret_scanning_locations",
307 "list_bypass_requests",
308 "list_custom_patterns",
309 "list_code_scanning_alerts",
310 "get_code_scanning_alert",
311 "list_code_scanning_analyses",
312 "get_sarif_upload",
313 "list_vulnerability_alerts",
314 "get_vulnerability_alert",
315 "get_dependency_graph",
316 "get_sbom",
317 "compare_dependencies",
318 "get_security_settings",
319 "get_workspace_security_settings",
320 "get_security_overview",
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9321 "list_security_advisories",
322 "get_security_advisory",
323 "list_vulnerability_reports",
API: notifications over REST and MCP, with notifications scopes324 "list_notifications",
325 "get_notification_thread",
326 "get_thread_subscription",
327 "get_repo_subscription",
328 "list_watched_repos",
API: pinned projects over REST and MCP329 "list_pinned_projects",
Mission control counts people's direct pushes to the default branch330];
331
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.332/// What no run credential may ever do, whatever its scope says:
333/// workspaces, repositories' settings, members, tokens, billing,
334/// integrations, webhooks, secrets, workflows' controls and merging.
Mission control counts people's direct pushes to the default branch335pub 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.336 // Billing is people's: runs never spend or change it.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit337 "set_budget",
Mission control counts people's direct pushes to the default branch338 "create_workspace",
339 "delete_workspace",
340 "update_workspace",
Merge main (membership, two-factor, GitHub repo roles) into tokens341 // So is who belongs to a workspace and who owns it.
342 "list_members",
343 "update_member",
344 "remove_member",
345 "transfer_ownership",
346 "leave_workspace",
Mission control counts people's direct pushes to the default branch347 "transfer_repo",
348 "create_repo",
349 "update_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97350 "update_project",
Mission control counts people's direct pushes to the default branch351 "delete_repo",
352 "list_deleted_repos",
353 "restore_repo",
354 "purge_repo",
355 "rename_repo",
356 "archive_repo",
357 "unarchive_repo",
358 "set_repo_visibility",
359 "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.360 "delete_branch",
Mission control counts people's direct pushes to the default branch361 "update_repo_settings",
362 "merge_pull_request",
363 "import_issue",
364 "list_integrations",
365 "connect_integration",
AI Gateway: OpenAI's format, open models, and your own providers366 "update_integration",
Mission control counts people's direct pushes to the default branch367 "disconnect_integration",
368 "test_integration",
369 "list_webhooks",
370 "create_webhook",
371 "update_webhook",
372 "delete_webhook",
373 "ping_webhook",
374 "list_webhook_deliveries",
375 "redeliver_webhook",
376 "dispatch_workflow",
377 "cancel_workflow_run",
378 "rerun_workflow_run",
379 "update_workflow",
Merge branch 'worktree-agent-a3abfcce648e87dca'380 // Nor lets runs or deployments through, or changes what holds them.
381 "approve_workflow_run",
382 "review_pending_deployments",
383 "update_environment",
384 "delete_environment",
385 "set_workflow_permissions",
386 "set_fork_pr_approval",
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts387 "set_actions_access",
Merge branch 'worktree-agent-a3abfcce648e87dca'388 "create_repository_dispatch",
389 "set_workspace_workflow_permissions",
Merge checks: statuses and check runs on every commit390 // An agent never reports checks on its own work, nor asks for them
391 // to run again: what checks say is the integrations' to say.
392 "create_commit_status",
393 "create_check_run",
394 "update_check_run",
395 "rerequest_check_run",
396 "rerequest_check_suite",
Mission control counts people's direct pushes to the default branch397 "list_actions_secrets",
398 "set_actions_secret",
399 "delete_actions_secret",
400 "list_actions_variables",
401 "set_actions_variable",
402 "delete_actions_variable",
403 "list_collaborators",
404 "get_collaborator_permission",
405 "add_collaborator",
406 "update_collaborator",
407 "remove_collaborator",
408 "list_repo_invitations",
409 "revoke_repo_invitation",
410 "list_my_repo_invitations",
411 "accept_repo_invitation",
412 "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)413 // Nor answers a workspace invitation: only the person it is for does.
414 "accept_invitation",
415 "decline_invitation",
Mission control counts people's direct pushes to the default branch416 "set_base_permission",
417 "list_outside_collaborators",
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca418 // Deploy keys, which let a machine into a repository.
419 "list_deploy_keys",
420 "get_deploy_key",
421 "create_deploy_key",
422 "delete_deploy_key",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar423 // Teams: who is in which, and what they reach, is for people.
424 "create_team",
425 "update_team",
426 "delete_team",
427 "set_team_member",
428 "remove_team_member",
429 "set_team_repo",
430 "remove_team_repo",
431 "set_team_review_assignment",
Mission control counts people's direct pushes to the default branch432 // Dismissing a secret lets it through push protection.
433 "dismiss_security_alert",
434 "reopen_security_alert",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar435 // 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.436 // alert, pushing past push protection or deciding who may, or changing
437 // what is looked for or when checks fail.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar438 "update_secret_scanning_alert",
439 "bypass_push_protection",
440 "review_bypass_request",
441 "create_custom_pattern",
442 "update_custom_pattern",
443 "delete_custom_pattern",
444 "update_code_scanning_alert",
445 "update_vulnerability_alert",
446 "update_security_settings",
447 "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.448 // A person's own inbox: a run acts as g1t, which has none.
API: notifications over REST and MCP, with notifications scopes449 "list_notifications",
450 "get_notification_thread",
451 "mark_notifications_read",
452 "mark_thread_read",
453 "mark_thread_done",
454 "save_thread",
455 "snooze_thread",
456 "get_thread_subscription",
457 "set_thread_subscription",
458 "delete_thread_subscription",
459 "get_repo_subscription",
460 "set_repo_subscription",
461 "delete_repo_subscription",
462 "list_watched_repos",
API: pinned projects over REST and MCP463 // Pins are a person's own, as the inbox is.
464 "list_pinned_projects",
465 "pin_project",
466 "unpin_project",
467 "reorder_pinned_projects",
Merge packages: roles, Actions access, source label, soft delete, API468 // Deleting packages and deciding who may use them is for people: an
469 // agent publishes its repository's packages and never deletes them.
470 "update_package",
471 "link_package",
472 "unlink_package",
473 "set_package_access",
474 "remove_package_access",
475 "set_package_actions_access",
476 "remove_package_actions_access",
477 "delete_package",
478 "restore_package",
479 "delete_package_version",
480 "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.481 // Sharing an artifact and deleting one for good are for people.
Merge main into Artifacts Phase 2482 "set_workspace_artifact_access",
483 "purge_workspace_artifact",
Mission control counts people's direct pushes to the default branch484];
485
486pub fn is_read(operation: &str) -> bool {
487 READ_OPERATIONS.contains(&operation)
488}
489
490/// 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.491/// for `usage`. Git is separate: see [`decide_git`]. None today: every run
492/// still issued works through git alone, and a
493/// [retired](RunCredentialKind::retired) kind grants nothing. A stored
494/// token keeps the operations it was made with until it expires.
Mission control counts people's direct pushes to the default branch495pub 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.496 let _ = (kind, usage);
497 Vec::new()
Mission control counts people's direct pushes to the default branch498}
499
500/// What a run's credential may do, in the scope vocabulary that access
501/// tokens use (see [`crate::scopes`]): the scopes of its operations, and
502/// for a runner, git's. Its operations, its repository and its run still
503/// bound it more tightly than these scopes say.
504pub fn run_scopes(kind: RunCredentialKind, usage: CredentialUse) -> Vec<crate::scopes::Scope> {
505 use crate::scopes::{Scope, normalize, scope_for};
506 let mut scopes: Vec<Scope> = operations_for(kind, usage)
507 .into_iter()
508 .filter_map(scope_for)
509 .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.510 if usage == CredentialUse::Runner && !kind.retired() {
Mission control counts people's direct pushes to the default branch511 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.512 if kind == RunCredentialKind::Bump {
Mission control counts people's direct pushes to the default branch513 scopes.push(Scope::CodeWrite);
514 }
515 }
516 normalize(&mut scopes);
517 scopes
518}
519
520/// Operations that change a pull request, which a runner may do only to
521/// the pull request its run works on.
522const PULL_WRITES: &[&str] = &["record_session", "mark_pull_request_ready"];
523
524/// Whether something was allowed, and the rule that decided it.
525#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
526pub struct Decision {
527 pub allowed: bool,
528 /// A short, stable name: `run:implement/tools`, `never`,
529 /// `scope:repository` and so on. Shown in the audit log.
530 pub rule: String,
531 /// Why it was refused, for the caller.
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub reason: Option<String>,
534}
535
536impl Decision {
537 pub fn allow(rule: impl Into<String>) -> Self {
538 Decision {
539 allowed: true,
540 rule: rule.into(),
541 reason: None,
542 }
543 }
544
545 pub fn deny(rule: impl Into<String>, reason: impl Into<String>) -> Self {
546 Decision {
547 allowed: false,
548 rule: rule.into(),
549 reason: Some(reason.into()),
550 }
551 }
552}
553
554fn same_repo(a: &RepoPath, b: &RepoPath) -> bool {
555 a.namespace.eq_ignore_ascii_case(&b.namespace) && a.name.eq_ignore_ascii_case(&b.name)
556}
557
558fn scope_rule(scope: &AgentScope) -> String {
559 match &scope.run {
560 Some(run) => format!("run:{}/{}", run.kind.as_str(), run.usage.as_str()),
561 None => "agent-token".to_owned(),
562 }
563}
564
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.565/// Whether `user`, resolved from a run credential with `scope`, may use
Mission control counts people's direct pushes to the default branch566/// `operation`. `repo` is the repository the call names, if any, and
567/// `needs_repo` whether the operation is about one; `number` the issue or
568/// pull request it names.
569pub fn decide_operation(
570 user: &User,
571 scope: &AgentScope,
572 operation: &str,
573 repo: Option<&RepoPath>,
574 needs_repo: bool,
575 number: Option<u32>,
576) -> 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.577 let who = "A run's token";
Mission control counts people's direct pushes to the default branch578 if NEVER.contains(&operation) {
579 return Decision::deny(
580 "never",
581 format!(
582 "{who} can never use {operation}: settings, members, tokens, billing, integrations, webhooks, secrets and merging are for people."
583 ),
584 );
585 }
586 if !scope.operations.iter().any(|name| name == operation) {
587 return Decision::deny(
588 "scope:operation",
589 format!("{who} for this run cannot use {operation}."),
590 );
591 }
592 if needs_repo && !repo.is_some_and(|asked| same_repo(asked, &scope.repo)) {
593 return Decision::deny(
594 "scope:repository",
595 format!(
596 "{who} works in {}/{} only.",
597 scope.repo.namespace, scope.repo.name
598 ),
599 );
600 }
601 // The intersection: the person it acts for must still be able to work
602 // in the repository's workspace, as a member or with a role on its
603 // repositories. What it may do in the repository itself is their
604 // role there, which services check (`access::can`).
605 if !crate::access::has_access_in(user, &scope.repo.namespace) {
606 return Decision::deny(
607 "on-behalf-of:membership",
608 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.609 "The person this run works for is no longer a member of {}.",
Mission control counts people's direct pushes to the default branch610 scope.repo.namespace
611 ),
612 );
613 }
614 if let Some(run) = &scope.run
615 && run.usage == CredentialUse::Runner
616 && PULL_WRITES.contains(&operation)
617 && run.number.is_some()
618 && number != run.number
619 {
620 return Decision::deny(
621 "scope:pull",
622 format!(
623 "{who} can change pull request #{} only.",
624 run.number.unwrap_or_default()
625 ),
626 );
627 }
628 Decision::allow(scope_rule(scope))
629}
630
631/// Whether a run credential may clone or fetch (`write` false), or push to
632/// (`write` true), the repository at `repo`.
633pub fn decide_git(scope: &AgentScope, repo: &RepoPath, write: bool) -> Decision {
634 let Some(run) = scope
635 .run
636 .as_ref()
637 .filter(|run| run.usage == CredentialUse::Runner)
638 else {
639 return Decision::deny(
640 "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.641 "A tools token cannot be used with git.",
Mission control counts people's direct pushes to the default branch642 );
643 };
644 let pushable = run.push.iter().any(|grant| same_repo(&grant.repo, repo));
645 if write {
646 return if pushable {
647 Decision::allow(format!("{}:push", scope_rule(scope)))
648 } else {
649 Decision::deny(
650 "git:push",
651 format!(
652 "A {} run cannot push to {}/{}.",
653 run.kind.as_str(),
654 repo.namespace,
655 repo.name
656 ),
657 )
658 };
659 }
660 let readable = pushable
661 || same_repo(&scope.repo, repo)
662 || run.read.iter().any(|path| same_repo(path, repo));
663 if readable {
664 Decision::allow(format!("{}:read", scope_rule(scope)))
665 } else {
666 Decision::deny(
667 "git:read",
668 format!(
669 "A {} run cannot read {}/{}.",
670 run.kind.as_str(),
671 repo.namespace,
672 repo.name
673 ),
674 )
675 }
676}
677
678/// Whether a push to `repo` is limited to certain branches, so that the
679/// refs it moves have to be read and checked with [`decide_refs`].
680pub fn limits_branches(scope: &AgentScope, repo: &RepoPath) -> bool {
681 scope
682 .run
683 .iter()
684 .flat_map(|run| run.push.iter())
685 .any(|grant| same_repo(&grant.repo, repo) && grant.branch.is_some())
686}
687
688/// Whether a push to `repo` may move `refs` (full refs, such as
689/// `refs/heads/main`). Tags are never a run's to move.
690pub fn decide_refs(scope: &AgentScope, repo: &RepoPath, refs: &[String]) -> Decision {
691 let repo_decision = decide_git(scope, repo, true);
692 if !repo_decision.allowed {
693 return repo_decision;
694 }
695 let grants: Vec<&GitGrant> = scope
696 .run
697 .iter()
698 .flat_map(|run| run.push.iter())
699 .filter(|grant| same_repo(&grant.repo, repo))
700 .collect();
701 for git_ref in refs {
702 let Some(branch) = git_ref.strip_prefix("refs/heads/") else {
703 return Decision::deny("git:ref", format!("A run cannot push {git_ref}."));
704 };
705 let allowed = grants
706 .iter()
707 .any(|grant| grant.branch.as_deref().is_none_or(|only| only == branch));
708 if !allowed {
709 return Decision::deny(
710 "git:ref",
711 format!(
712 "A run cannot push to {branch} in {}/{}.",
713 repo.namespace, repo.name
714 ),
715 );
716 }
717 }
718 repo_decision
719}
720
721/// The most an agent may be on a repository, whoever it works for: it
722/// can push, merge and run, never change settings or who has access.
723pub const AGENT_CEILING: RepoRole = RepoRole::Write;
724
725/// The memberships an agent working for `person` has: the run's
726/// workspace, as a member, only if the person is in it now, with the
727/// person's role on its repositories (an owner's Admin included) cut down
728/// to [`AGENT_CEILING`].
729pub fn intersect(person: &[Membership], namespace: &str) -> Vec<Membership> {
730 let namespace = namespace.to_lowercase();
731 person
732 .iter()
733 .filter(|membership| membership.slug == namespace)
734 .map(|membership| {
735 let base = match membership.role {
736 Role::Owner => BasePermission::Admin,
737 Role::Member => membership.base_permission.unwrap_or_default(),
738 };
739 Membership {
740 role: Role::Member,
741 base_permission: Some(match base {
742 BasePermission::Admin => BasePermission::Write,
743 base => base,
744 }),
Merge main (membership, two-factor, GitHub repo roles) into tokens745 // Billing and security manager are the person's, never
746 // their agent's.
747 org_roles: Vec::new(),
Mission control counts people's direct pushes to the default branch748 ..membership.clone()
749 }
750 })
751 .collect()
752}
753
754/// The repository grants an agent working for `person` has: those in the
755/// run's workspace, each cut down to [`AGENT_CEILING`].
756pub fn intersect_grants(person: &[RepoGrant], namespace: &str) -> Vec<RepoGrant> {
757 let namespace = namespace.to_lowercase();
758 person
759 .iter()
760 .filter(|grant| grant.workspace == namespace)
761 .map(|grant| RepoGrant {
762 role: grant.role.min(AGENT_CEILING),
763 ..grant.clone()
764 })
765 .collect()
766}
767
768/// Who a runner's credential acts as downstream: the person, with only the
769/// agent's (already intersected) memberships. `None` for anything else.
770pub fn as_person(user: &User) -> Option<User> {
771 let acting = user.acting.as_ref()?;
772 if user.kind != PrincipalKind::Agent {
773 return None;
774 }
775 let run = acting.run()?;
776 if run.usage != CredentialUse::Runner {
777 return None;
778 }
779 Some(User {
780 id: acting.on_behalf_of.id.clone(),
781 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 managers782 display_username: None,
Mission control counts people's direct pushes to the default branch783 kind: PrincipalKind::User,
784 verified: user.verified,
785 workspaces: user.workspaces.clone(),
786 avatar: None,
787 acting: None,
788 grants: user.grants.clone(),
789 token: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens790 held: Vec::new(),
Mission control counts people's direct pushes to the default branch791 })
792}
793
794/// How an actor is described: "g1t on behalf of syntaqx".
795pub fn describe(user: &User) -> String {
796 match &user.acting {
797 Some(acting) => format!(
798 "{} on behalf of {}",
799 acting.agent, acting.on_behalf_of.username
800 ),
801 None => user.username.clone(),
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn a_run_s_scopes_are_never_admin() {
811 for kind in RunCredentialKind::ALL {
812 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
813 let scopes = run_scopes(kind, usage);
814 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{kind:?} {usage:?}: {scopes:?}");
815 }
816 }
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.817 use crate::scopes::Scope;
818 let bump = run_scopes(RunCredentialKind::Bump, CredentialUse::Runner);
819 assert!(bump.len() == 2 && bump.contains(&Scope::CodeRead) && bump.contains(&Scope::CodeWrite));
820 assert_eq!(run_scopes(RunCredentialKind::Checks, CredentialUse::Runner), [Scope::CodeRead]);
Mission control counts people's direct pushes to the default branch821 }
822
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.823 /// The hosted agents' kinds still parse, so stored tokens read, but
824 /// they grant nothing: no operations, no scopes.
Mission control counts people's direct pushes to the default branch825 #[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.826 fn retired_kinds_parse_and_grant_nothing() {
827 for kind in RunCredentialKind::ALL {
828 let parsed: RunCredentialKind = serde_json::from_value(serde_json::json!(kind.as_str())).unwrap();
829 assert_eq!(parsed, kind);
830 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
831 assert!(operations_for(kind, usage).is_empty(), "{kind:?} {usage:?}");
832 if kind.retired() {
833 assert!(run_scopes(kind, usage).is_empty(), "{kind:?} {usage:?}");
834 }
835 }
Mission control counts people's direct pushes to the default branch836 }
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.837 let retired: Vec<&str> = RunCredentialKind::ALL.iter().filter(|kind| kind.retired()).map(|kind| kind.as_str()).collect();
838 assert_eq!(retired, ["implement", "revise", "review", "answer", "update", "plan"]);
Mission control counts people's direct pushes to the default branch839 }
840
841 #[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.842 fn site_wide_search_only_reads() {
Mission control counts people's direct pushes to the default branch843 assert!(is_read("search"));
844 assert!(!NEVER.contains(&"search"));
845 }
846
847 fn path(namespace: &str, name: &str) -> RepoPath {
848 RepoPath {
849 namespace: namespace.to_owned(),
850 name: name.to_owned(),
851 }
852 }
853
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.854 /// What a token stored before the hosted agents were retired lists:
855 /// the operations it was made with, which it keeps until it expires.
856 fn stored_operations(kind: RunCredentialKind, usage: CredentialUse) -> Vec<String> {
857 use RunCredentialKind as K;
858 let names: Vec<&str> = match usage {
859 CredentialUse::Runner => match kind {
860 K::Implement => vec!["get_repo", "get_pull_request", "record_session", "mark_pull_request_ready"],
861 K::Revise | K::Answer | K::Update => vec!["get_repo", "get_pull_request", "record_session"],
862 _ => vec![],
863 },
864 CredentialUse::Tools => match kind {
865 K::Implement | K::Revise | K::Answer => vec!["get_issue", "get_pull_request", "create_issue", "add_comment", "remember"],
866 K::Review => vec!["get_issue", "get_pull_request", "add_comment", "review_pull_request"],
867 K::Plan => vec!["get_issue", "get_pull_request", "create_issue"],
868 K::Update => vec!["get_issue", "get_pull_request"],
869 _ => vec![],
870 },
871 };
872 names.into_iter().map(str::to_owned).collect()
873 }
874
Mission control counts people's direct pushes to the default branch875 fn scope(kind: RunCredentialKind, usage: CredentialUse) -> AgentScope {
876 AgentScope {
877 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.878 operations: stored_operations(kind, usage),
Mission control counts people's direct pushes to the default branch879 run: Some(RunBinding {
880 kind,
881 usage,
882 run_id: Some("run_1".to_owned()),
883 number: Some(7),
884 agent: "g1t".to_owned(),
885 system: false,
886 read: vec![path("acme", "rocket")],
887 push: match kind {
888 RunCredentialKind::Implement
889 | RunCredentialKind::Revise
890 | RunCredentialKind::Answer => vec![GitGrant {
891 repo: path("pulls", "pul_7"),
892 branch: None,
893 }],
894 RunCredentialKind::Update => vec![GitGrant {
895 repo: path("acme", "rocket"),
896 branch: Some("fix-login".to_owned()),
897 }],
898 _ => vec![],
899 },
900 }),
901 }
902 }
903
904 fn agent(member_of: &[&str], scope: AgentScope) -> User {
905 User {
906 id: "usr_g1t_agent".to_owned(),
907 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 managers908 display_username: None,
Mission control counts people's direct pushes to the default branch909 kind: PrincipalKind::Agent,
910 verified: true,
911 workspaces: member_of
912 .iter()
913 .map(|slug| Membership::member(*slug))
914 .collect(),
915 avatar: None,
916 grants: Vec::new(),
917 token: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens918 held: Vec::new(),
Mission control counts people's direct pushes to the default branch919 acting: Some(Box::new(Acting {
920 credential_id: "tok_1".to_owned(),
921 agent: "g1t".to_owned(),
922 on_behalf_of: Principal {
923 id: "usr_1".to_owned(),
924 username: "syntaqx".to_owned(),
925 },
926 scope,
927 })),
928 }
929 }
930
931 fn op(kind: RunCredentialKind, usage: CredentialUse, operation: &str) -> Decision {
932 let scope = scope(kind, usage);
933 let user = agent(&["acme"], scope.clone());
934 decide_operation(
935 &user,
936 &scope,
937 operation,
938 Some(&path("acme", "rocket")),
939 true,
940 Some(7),
941 )
942 }
943
944 use CredentialUse::{Runner, Tools};
945 use RunCredentialKind as K;
946
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.947 /// A stored token may use only what it lists, and never what no run
948 /// may.
Mission control counts people's direct pushes to the default branch949 #[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.950 fn a_stored_token_uses_only_what_it_lists() {
951 assert!(op(K::Implement, Tools, "create_issue").allowed);
952 assert!(op(K::Review, Tools, "review_pull_request").allowed);
953 assert!(!op(K::Review, Tools, "create_issue").allowed);
954 assert!(!op(K::Plan, Tools, "add_comment").allowed);
955 assert!(!op(K::Checks, Tools, "get_issue").allowed);
956 for operation in ["record_session", "merge_pull_request", "update_repo_settings", "create_webhook", "set_actions_secret"] {
957 for kind in [K::Implement, K::Revise, K::Answer, K::Review, K::Plan, K::Checks] {
958 assert!(!op(kind, Tools, operation).allowed, "{operation} by a {} run's tools", kind.as_str());
Mission control counts people's direct pushes to the default branch959 }
960 }
961 }
962
963 #[test]
964 fn runner_matrix() {
965 assert!(op(K::Implement, Runner, "record_session").allowed);
966 assert!(op(K::Implement, Runner, "mark_pull_request_ready").allowed);
967 assert!(op(K::Revise, Runner, "record_session").allowed);
968 assert!(!op(K::Revise, Runner, "mark_pull_request_ready").allowed);
969 assert!(!op(K::Implement, Runner, "create_issue").allowed);
970 assert!(!op(K::Review, Runner, "record_session").allowed);
971 assert!(!op(K::Checks, Runner, "get_issue").allowed);
972 }
973
974 #[test]
975 fn settings_billing_tokens_and_members_are_never_reachable() {
976 for kind in RunCredentialKind::ALL {
977 for usage in [Runner, Tools] {
978 for operation in NEVER.iter().copied() {
979 let decision = op(kind, usage, operation);
980 assert!(!decision.allowed);
981 assert_eq!(decision.rule, "never");
982 }
983 }
984 }
985 // Even a scope that lists one is refused.
986 let mut wide = scope(K::Implement, Tools);
987 wide.operations.push("merge_pull_request".to_owned());
988 let user = agent(&["acme"], wide.clone());
989 let decision = decide_operation(
990 &user,
991 &wide,
992 "merge_pull_request",
993 Some(&path("acme", "rocket")),
994 true,
995 Some(7),
996 );
997 assert_eq!(decision.rule, "never");
998 }
999
1000 #[test]
1001 fn another_repository_is_refused() {
1002 let scope = scope(K::Implement, Tools);
1003 let user = agent(&["acme"], scope.clone());
1004 let decision = decide_operation(
1005 &user,
1006 &scope,
1007 "create_issue",
1008 Some(&path("acme", "other")),
1009 true,
1010 None,
1011 );
1012 assert!(!decision.allowed);
1013 assert_eq!(decision.rule, "scope:repository");
1014 let decision = decide_operation(&user, &scope, "create_issue", None, true, None);
1015 assert_eq!(decision.rule, "scope:repository");
1016 // The repository's name is matched without regard to case.
1017 let decision = decide_operation(
1018 &user,
1019 &scope,
1020 "create_issue",
1021 Some(&path("Acme", "Rocket")),
1022 true,
1023 None,
1024 );
1025 assert!(decision.allowed);
1026 assert_eq!(decision.rule, "run:implement/tools");
1027 }
1028
1029 #[test]
1030 fn the_permission_is_the_intersection_with_the_person() {
1031 let scope = scope(K::Implement, Tools);
1032 // The person left the workspace: their agent can do nothing there.
1033 let user = agent(&[], scope.clone());
1034 let decision = decide_operation(
1035 &user,
1036 &scope,
1037 "get_issue",
1038 Some(&path("acme", "rocket")),
1039 true,
1040 Some(1),
1041 );
1042 assert!(!decision.allowed);
1043 assert_eq!(decision.rule, "on-behalf-of:membership");
1044 // And an owner's agent is only ever a member.
1045 let owner = vec![
1046 Membership {
1047 slug: "acme".to_owned(),
1048 role: Role::Owner,
1049 name: None,
1050 avatar: None,
1051 base_permission: Some(BasePermission::None),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1052 team_creation: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens1053 org_roles: vec![crate::OrgRole::SecurityManager],
1054 privileges: None,
Mission control counts people's direct pushes to the default branch1055 },
1056 Membership::member("elsewhere"),
1057 ];
1058 let memberships = intersect(&owner, "Acme");
1059 assert_eq!(memberships.len(), 1);
1060 assert_eq!(memberships[0].slug, "acme");
1061 assert_eq!(memberships[0].role, Role::Member);
Merge main (membership, two-factor, GitHub repo roles) into tokens1062 assert!(memberships[0].org_roles.is_empty());
Mission control counts people's direct pushes to the default branch1063 assert!(intersect(&owner, "nowhere").is_empty());
1064 }
1065
1066 /// An agent gets at most the person's role on the repository, and
1067 /// never more than Write; nothing outside the run's workspace.
1068 #[test]
1069 fn an_agent_has_at_most_its_persons_role() {
1070 use crate::access::{Capability, RepoRef, can, permission};
1071 let rocket = RepoRef { id: "rep_1", namespace: "acme", private: true };
1072 let other = RepoRef { id: "rep_2", namespace: "acme", private: true };
1073 let elsewhere = RepoRef { id: "rep_3", namespace: "globex", private: true };
1074 let tools = scope(K::Implement, Tools);
1075 let scope = scope(K::Implement, Runner);
1076 // An owner's agent: Write, never Admin.
1077 let owner = [Membership { role: Role::Owner, ..Membership::member("acme") }, Membership::member("globex")];
1078 let mut agent_user = agent(&[], scope.clone());
1079 agent_user.workspaces = intersect(&owner, "acme");
1080 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1081 assert!(!can(Some(&agent_user), rocket, Capability::ManageSettings));
1082 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1083 // A member whose workspace gives Read: Read, so it cannot push.
1084 let reader = [Membership { base_permission: Some(BasePermission::Read), ..Membership::member("acme") }];
1085 agent_user.workspaces = intersect(&reader, "acme");
1086 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Read));
1087 assert!(!can(Some(&agent_user), rocket, Capability::Push));
1088 // An outside collaborator with Maintain on one repository: Write
1089 // there, nothing elsewhere, and the run is allowed.
1090 let grants = [
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1091 RepoGrant { repo_id: "rep_1".into(), workspace: "acme".into(), role: RepoRole::Maintain, team: None },
1092 RepoGrant { repo_id: "rep_3".into(), workspace: "globex".into(), role: RepoRole::Admin, team: None },
Mission control counts people's direct pushes to the default branch1093 ];
1094 agent_user.workspaces = intersect(&[], "acme");
1095 agent_user.grants = intersect_grants(&grants, "Acme");
1096 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1097 assert_eq!(permission(Some(&agent_user), other), None);
1098 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1099 let decision = decide_operation(&agent_user, &tools, "get_issue", Some(&path("acme", "rocket")), true, Some(1));
1100 assert!(decision.allowed, "{}", decision.reason.unwrap_or_default());
1101 // The person, downstream of a runner's credential, carries the same.
1102 let person = as_person(&agent_user).expect("a runner acts as the person");
1103 assert_eq!(permission(Some(&person), rocket), Some(RepoRole::Write));
1104 }
1105
1106 #[test]
1107 fn a_runner_changes_only_its_own_pull_request() {
1108 let scope = scope(K::Implement, Runner);
1109 let user = agent(&["acme"], scope.clone());
1110 let repo = path("acme", "rocket");
1111 let other = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(8));
1112 assert!(!other.allowed);
1113 assert_eq!(other.rule, "scope:pull");
1114 let own = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(7));
1115 assert!(own.allowed);
1116 // Reading another is fine.
1117 assert!(
1118 decide_operation(
1119 &user,
1120 &scope,
1121 "get_pull_request",
1122 Some(&repo),
1123 true,
1124 Some(8)
1125 )
1126 .allowed
1127 );
1128 }
1129
1130 #[test]
1131 fn git_matrix() {
1132 let fork = path("pulls", "pul_7");
1133 let upstream = path("acme", "rocket");
1134 let elsewhere = path("acme", "billing");
1135 let implement = scope(K::Implement, Runner);
1136 assert!(decide_git(&implement, &fork, true).allowed);
1137 assert!(decide_git(&implement, &fork, false).allowed);
1138 assert!(decide_git(&implement, &upstream, false).allowed);
1139 assert_eq!(decide_git(&implement, &upstream, true).rule, "git:push");
1140 assert_eq!(decide_git(&implement, &elsewhere, false).rule, "git:read");
1141 let review = scope(K::Review, Runner);
1142 assert!(decide_git(&review, &upstream, false).allowed);
1143 assert!(!decide_git(&review, &upstream, true).allowed);
1144 assert!(!decide_git(&review, &fork, true).allowed);
1145 // A tools token made before run credentials never reaches git.
1146 let old = AgentScope {
1147 repo: upstream.clone(),
1148 operations: vec!["get_issue".to_owned()],
1149 run: None,
1150 };
1151 assert_eq!(decide_git(&old, &upstream, false).rule, "git:not-a-run");
1152 // Nor does an agent's tools token.
1153 assert_eq!(
1154 decide_git(&scope(K::Implement, Tools), &upstream, false).rule,
1155 "git:not-a-run"
1156 );
1157 }
1158
1159 #[test]
1160 fn a_push_moves_only_granted_branches() {
1161 let update = scope(K::Update, Runner);
1162 let repo = path("acme", "rocket");
1163 let refs = |names: &[&str]| {
1164 names
1165 .iter()
1166 .map(|name| (*name).to_owned())
1167 .collect::<Vec<_>>()
1168 };
1169 assert!(decide_refs(&update, &repo, &refs(&["refs/heads/fix-login"])).allowed);
1170 assert_eq!(
1171 decide_refs(&update, &repo, &refs(&["refs/heads/main"])).rule,
1172 "git:ref"
1173 );
1174 assert_eq!(
1175 decide_refs(
1176 &update,
1177 &repo,
1178 &refs(&["refs/heads/fix-login", "refs/tags/v1"])
1179 )
1180 .rule,
1181 "git:ref"
1182 );
1183 let implement = scope(K::Implement, Runner);
1184 assert!(
1185 decide_refs(
1186 &implement,
1187 &path("pulls", "pul_7"),
1188 &refs(&["refs/heads/main"])
1189 )
1190 .allowed
1191 );
1192 }
1193
1194 #[test]
1195 fn a_runner_acts_downstream_as_the_person() {
1196 let user = agent(&["acme"], scope(K::Implement, Runner));
1197 let person = as_person(&user).unwrap();
1198 assert_eq!(person.id, "usr_1");
1199 assert_eq!(person.username, "syntaqx");
1200 assert_eq!(person.kind, PrincipalKind::User);
1201 assert!(person.is_member("acme"));
1202 assert!(person.acting.is_none());
1203 assert_eq!(describe(&user), "g1t on behalf of syntaqx");
1204 // The tools act as the agent.
1205 assert!(as_person(&agent(&["acme"], scope(K::Implement, Tools))).is_none());
1206 }
1207
1208 #[test]
1209 fn scopes_without_a_run_still_parse() {
1210 let old: AgentScope = serde_json::from_str(
1211 r#"{"repo":{"namespace":"acme","name":"rocket"},"operations":["get_issue"]}"#,
1212 )
1213 .unwrap();
1214 assert!(old.run.is_none());
1215 let written = serde_json::to_string(&scope(K::Review, Tools)).unwrap();
1216 assert!(written.contains(r#""use":"tools""#));
1217 assert!(written.contains(r#""kind":"review""#));
1218 let back: AgentScope = serde_json::from_str(&written).unwrap();
1219 assert_eq!(back.run.unwrap().kind, K::Review);
1220 // Only g1t's own runs say so; every other reads as not.
1221 assert!(!written.contains("system"));
1222 assert!(!back_run(&written).system);
1223 let mut own = scope(K::Bump, Runner);
1224 own.run.as_mut().unwrap().system = true;
1225 let written = serde_json::to_string(&own).unwrap();
1226 assert!(written.contains(r#""system":true"#));
1227 assert!(back_run(&written).system);
1228 }
1229
1230 fn back_run(written: &str) -> RunBinding {
1231 serde_json::from_str::<AgentScope>(written).unwrap().run.unwrap()
1232 }
1233}

This file's history is long; its oldest lines are credited to the oldest commit read.