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,235 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.

The Packages page names every registry it has1//! 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`]).
The Packages page names every registry it has16//!
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.
The Packages page names every registry it has29#[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.
The Packages page names every registry it has35 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`].
The Packages page names every registry it has37 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`].
The Packages page names every registry it has39 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`].
The Packages page names every registry it has41 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`].
The Packages page names every registry it has43 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`].
The Packages page names every registry it has45 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 {
The Packages page names every registry it has90 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
The Packages page names every registry it has95 | 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
The Packages page names every registry it has98 )
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.
The Packages page names every registry it has109 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.
The Packages page names every registry it has112 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.
The Packages page names every registry it has136#[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.
The Packages page names every registry it has143 #[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`.
The Packages page names every registry it has149 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
The Packages page names every registry it has157 /// 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
The Packages page names every registry it has181/// 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`.
The Packages page names every registry it has203#[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.
The Packages page names every registry it has222#[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",
The Packages page names every registry it has250 "list_repos",
251 "get_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97252 "list_projects",
253 "get_project",
The Packages page names every registry it has254 "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",
The Packages page names every registry it has267 "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",
The Packages page names every registry it has277 "list_pull_requests",
278 "get_pull_request",
279 "read_session",
Merge branch 'worktree-agent-ab8033ab973f8d99a'280 "list_decisions",
281 "why",
The Packages page names every registry it has282 "get_pull_request_changes",
283 "list_events",
284 "get_context",
285 "search",
286 "list_workflows",
287 "list_workflow_runs",
288 "get_workflow_run",
289 "get_job_logs",
Merge branch 'worktree-agent-a3abfcce648e87dca'290 "get_pending_deployments",
291 "get_workflow_permissions",
292 "get_fork_pr_approval",
Merge checks: statuses and check runs on every commit293 "list_commit_statuses",
294 "get_combined_status",
295 "list_check_runs_for_ref",
296 "get_check_run",
297 "list_check_run_annotations",
298 "list_check_suites_for_ref",
299 "get_check_suite",
The Packages page names every registry it has300 "list_integrations",
301 "list_webhooks",
302 "list_webhook_deliveries",
303 "list_actions_secrets",
304 "list_actions_variables",
305 "list_security_alerts",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar306 "list_secret_scanning_alerts",
307 "get_secret_scanning_alert",
308 "list_secret_scanning_locations",
309 "list_bypass_requests",
310 "list_custom_patterns",
311 "list_code_scanning_alerts",
312 "get_code_scanning_alert",
313 "list_code_scanning_analyses",
314 "get_sarif_upload",
315 "list_vulnerability_alerts",
316 "get_vulnerability_alert",
317 "get_dependency_graph",
318 "get_sbom",
319 "compare_dependencies",
320 "get_security_settings",
321 "get_workspace_security_settings",
322 "get_security_overview",
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9323 "list_security_advisories",
324 "get_security_advisory",
325 "list_vulnerability_reports",
API: notifications over REST and MCP, with notifications scopes326 "list_notifications",
327 "get_notification_thread",
328 "get_thread_subscription",
329 "get_repo_subscription",
330 "list_watched_repos",
API: pinned projects over REST and MCP331 "list_pinned_projects",
The Packages page names every registry it has332];
333
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.334/// What no run credential may ever do, whatever its scope says:
335/// workspaces, repositories' settings, members, tokens, billing,
336/// integrations, webhooks, secrets, workflows' controls and merging.
The Packages page names every registry it has337pub const NEVER: &[&str] = &[
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.338 // Billing is people's: runs never spend or change it.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit339 "set_budget",
The Packages page names every registry it has340 "create_workspace",
341 "delete_workspace",
342 "update_workspace",
Merge main (membership, two-factor, GitHub repo roles) into tokens343 // So is who belongs to a workspace and who owns it.
344 "list_members",
345 "update_member",
346 "remove_member",
347 "transfer_ownership",
348 "leave_workspace",
The Packages page names every registry it has349 "transfer_repo",
350 "create_repo",
351 "update_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97352 "update_project",
The Packages page names every registry it has353 "delete_repo",
354 "list_deleted_repos",
355 "restore_repo",
356 "purge_repo",
357 "rename_repo",
358 "archive_repo",
359 "unarchive_repo",
360 "set_repo_visibility",
361 "rename_branch",
The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days.362 "delete_branch",
The Packages page names every registry it has363 "update_repo_settings",
364 "merge_pull_request",
365 "import_issue",
366 "list_integrations",
367 "connect_integration",
AI Gateway: OpenAI's format, open models, and your own providers368 "update_integration",
The Packages page names every registry it has369 "disconnect_integration",
370 "test_integration",
371 "list_webhooks",
372 "create_webhook",
373 "update_webhook",
374 "delete_webhook",
375 "ping_webhook",
376 "list_webhook_deliveries",
377 "redeliver_webhook",
378 "dispatch_workflow",
379 "cancel_workflow_run",
380 "rerun_workflow_run",
381 "update_workflow",
Merge branch 'worktree-agent-a3abfcce648e87dca'382 // Nor lets runs or deployments through, or changes what holds them.
383 "approve_workflow_run",
384 "review_pending_deployments",
385 "update_environment",
386 "delete_environment",
387 "set_workflow_permissions",
388 "set_fork_pr_approval",
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts389 "set_actions_access",
Merge branch 'worktree-agent-a3abfcce648e87dca'390 "create_repository_dispatch",
391 "set_workspace_workflow_permissions",
Merge checks: statuses and check runs on every commit392 // An agent never reports checks on its own work, nor asks for them
393 // to run again: what checks say is the integrations' to say.
394 "create_commit_status",
395 "create_check_run",
396 "update_check_run",
397 "rerequest_check_run",
398 "rerequest_check_suite",
The Packages page names every registry it has399 "list_actions_secrets",
400 "set_actions_secret",
401 "delete_actions_secret",
402 "list_actions_variables",
403 "set_actions_variable",
404 "delete_actions_variable",
405 "list_collaborators",
406 "get_collaborator_permission",
407 "add_collaborator",
408 "update_collaborator",
409 "remove_collaborator",
410 "list_repo_invitations",
411 "revoke_repo_invitation",
412 "list_my_repo_invitations",
413 "accept_repo_invitation",
414 "decline_repo_invitation",
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)415 // Nor answers a workspace invitation: only the person it is for does.
416 "accept_invitation",
417 "decline_invitation",
The Packages page names every registry it has418 "set_base_permission",
419 "list_outside_collaborators",
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca420 // Deploy keys, which let a machine into a repository.
421 "list_deploy_keys",
422 "get_deploy_key",
423 "create_deploy_key",
424 "delete_deploy_key",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar425 // Teams: who is in which, and what they reach, is for people.
426 "create_team",
427 "update_team",
428 "delete_team",
429 "set_team_member",
430 "remove_team_member",
431 "set_team_repo",
432 "remove_team_repo",
433 "set_team_review_assignment",
The Packages page names every registry it has434 // Dismissing a secret lets it through push protection.
435 "dismiss_security_alert",
436 "reopen_security_alert",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar437 // Nor any other decision about security: closing or reopening an
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.438 // alert, pushing past push protection or deciding who may, or changing
439 // what is looked for or when checks fail.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar440 "update_secret_scanning_alert",
441 "bypass_push_protection",
442 "review_bypass_request",
443 "create_custom_pattern",
444 "update_custom_pattern",
445 "delete_custom_pattern",
446 "update_code_scanning_alert",
447 "update_vulnerability_alert",
448 "update_security_settings",
449 "update_workspace_security_settings",
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.450 // A person's own inbox: a run acts as g1t, which has none.
API: notifications over REST and MCP, with notifications scopes451 "list_notifications",
452 "get_notification_thread",
453 "mark_notifications_read",
454 "mark_thread_read",
455 "mark_thread_done",
456 "save_thread",
457 "snooze_thread",
458 "get_thread_subscription",
459 "set_thread_subscription",
460 "delete_thread_subscription",
461 "get_repo_subscription",
462 "set_repo_subscription",
463 "delete_repo_subscription",
464 "list_watched_repos",
API: pinned projects over REST and MCP465 // Pins are a person's own, as the inbox is.
466 "list_pinned_projects",
467 "pin_project",
468 "unpin_project",
469 "reorder_pinned_projects",
Merge packages: roles, Actions access, source label, soft delete, API470 // Deleting packages and deciding who may use them is for people: an
471 // agent publishes its repository's packages and never deletes them.
472 "update_package",
473 "link_package",
474 "unlink_package",
475 "set_package_access",
476 "remove_package_access",
477 "set_package_actions_access",
478 "remove_package_actions_access",
479 "delete_package",
480 "restore_package",
481 "delete_package_version",
482 "restore_package_version",
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.483 // Sharing an artifact and deleting one for good are for people.
Merge main into Artifacts Phase 2484 "set_workspace_artifact_access",
485 "purge_workspace_artifact",
The Packages page names every registry it has486];
487
488pub fn is_read(operation: &str) -> bool {
489 READ_OPERATIONS.contains(&operation)
490}
491
492/// The API and MCP operations a run of `kind` may use with a credential
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.493/// for `usage`. Git is separate: see [`decide_git`]. None today: every run
494/// still issued works through git alone, and a
495/// [retired](RunCredentialKind::retired) kind grants nothing. A stored
496/// token keeps the operations it was made with until it expires.
The Packages page names every registry it has497pub fn operations_for(kind: RunCredentialKind, usage: CredentialUse) -> Vec<&'static str> {
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.498 let _ = (kind, usage);
499 Vec::new()
The Packages page names every registry it has500}
501
502/// What a run's credential may do, in the scope vocabulary that access
503/// tokens use (see [`crate::scopes`]): the scopes of its operations, and
504/// for a runner, git's. Its operations, its repository and its run still
505/// bound it more tightly than these scopes say.
506pub fn run_scopes(kind: RunCredentialKind, usage: CredentialUse) -> Vec<crate::scopes::Scope> {
507 use crate::scopes::{Scope, normalize, scope_for};
508 let mut scopes: Vec<Scope> = operations_for(kind, usage)
509 .into_iter()
510 .filter_map(scope_for)
511 .collect();
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.512 if usage == CredentialUse::Runner && !kind.retired() {
The Packages page names every registry it has513 scopes.push(Scope::CodeRead);
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.514 if kind == RunCredentialKind::Bump {
The Packages page names every registry it has515 scopes.push(Scope::CodeWrite);
516 }
517 }
518 normalize(&mut scopes);
519 scopes
520}
521
522/// Operations that change a pull request, which a runner may do only to
523/// the pull request its run works on.
Merge branch 'worktree-agent-ab8033ab973f8d99a'524const PULL_WRITES: &[&str] = &["record_session", "record_decision", "mark_pull_request_ready"];
The Packages page names every registry it has525
526/// Whether something was allowed, and the rule that decided it.
527#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
528pub struct Decision {
529 pub allowed: bool,
530 /// A short, stable name: `run:implement/tools`, `never`,
531 /// `scope:repository` and so on. Shown in the audit log.
532 pub rule: String,
533 /// Why it was refused, for the caller.
534 #[serde(default, skip_serializing_if = "Option::is_none")]
535 pub reason: Option<String>,
536}
537
538impl Decision {
539 pub fn allow(rule: impl Into<String>) -> Self {
540 Decision {
541 allowed: true,
542 rule: rule.into(),
543 reason: None,
544 }
545 }
546
547 pub fn deny(rule: impl Into<String>, reason: impl Into<String>) -> Self {
548 Decision {
549 allowed: false,
550 rule: rule.into(),
551 reason: Some(reason.into()),
552 }
553 }
554}
555
556fn same_repo(a: &RepoPath, b: &RepoPath) -> bool {
557 a.namespace.eq_ignore_ascii_case(&b.namespace) && a.name.eq_ignore_ascii_case(&b.name)
558}
559
560fn scope_rule(scope: &AgentScope) -> String {
561 match &scope.run {
562 Some(run) => format!("run:{}/{}", run.kind.as_str(), run.usage.as_str()),
563 None => "agent-token".to_owned(),
564 }
565}
566
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.567/// Whether `user`, resolved from a run credential with `scope`, may use
The Packages page names every registry it has568/// `operation`. `repo` is the repository the call names, if any, and
569/// `needs_repo` whether the operation is about one; `number` the issue or
570/// pull request it names.
571pub fn decide_operation(
572 user: &User,
573 scope: &AgentScope,
574 operation: &str,
575 repo: Option<&RepoPath>,
576 needs_repo: bool,
577 number: Option<u32>,
578) -> Decision {
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.579 let who = "A run's token";
The Packages page names every registry it has580 if NEVER.contains(&operation) {
581 return Decision::deny(
582 "never",
583 format!(
584 "{who} can never use {operation}: settings, members, tokens, billing, integrations, webhooks, secrets and merging are for people."
585 ),
586 );
587 }
588 if !scope.operations.iter().any(|name| name == operation) {
589 return Decision::deny(
590 "scope:operation",
591 format!("{who} for this run cannot use {operation}."),
592 );
593 }
594 if needs_repo && !repo.is_some_and(|asked| same_repo(asked, &scope.repo)) {
595 return Decision::deny(
596 "scope:repository",
597 format!(
598 "{who} works in {}/{} only.",
599 scope.repo.namespace, scope.repo.name
600 ),
601 );
602 }
603 // The intersection: the person it acts for must still be able to work
604 // in the repository's workspace, as a member or with a role on its
605 // repositories. What it may do in the repository itself is their
606 // role there, which services check (`access::can`).
607 if !crate::access::has_access_in(user, &scope.repo.namespace) {
608 return Decision::deny(
609 "on-behalf-of:membership",
610 format!(
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.611 "The person this run works for is no longer a member of {}.",
The Packages page names every registry it has612 scope.repo.namespace
613 ),
614 );
615 }
616 if let Some(run) = &scope.run
617 && run.usage == CredentialUse::Runner
618 && PULL_WRITES.contains(&operation)
619 && run.number.is_some()
620 && number != run.number
621 {
622 return Decision::deny(
623 "scope:pull",
624 format!(
625 "{who} can change pull request #{} only.",
626 run.number.unwrap_or_default()
627 ),
628 );
629 }
630 Decision::allow(scope_rule(scope))
631}
632
633/// Whether a run credential may clone or fetch (`write` false), or push to
634/// (`write` true), the repository at `repo`.
635pub fn decide_git(scope: &AgentScope, repo: &RepoPath, write: bool) -> Decision {
636 let Some(run) = scope
637 .run
638 .as_ref()
639 .filter(|run| run.usage == CredentialUse::Runner)
640 else {
641 return Decision::deny(
642 "git:not-a-run",
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.643 "A tools token cannot be used with git.",
The Packages page names every registry it has644 );
645 };
646 let pushable = run.push.iter().any(|grant| same_repo(&grant.repo, repo));
647 if write {
648 return if pushable {
649 Decision::allow(format!("{}:push", scope_rule(scope)))
650 } else {
651 Decision::deny(
652 "git:push",
653 format!(
654 "A {} run cannot push to {}/{}.",
655 run.kind.as_str(),
656 repo.namespace,
657 repo.name
658 ),
659 )
660 };
661 }
662 let readable = pushable
663 || same_repo(&scope.repo, repo)
664 || run.read.iter().any(|path| same_repo(path, repo));
665 if readable {
666 Decision::allow(format!("{}:read", scope_rule(scope)))
667 } else {
668 Decision::deny(
669 "git:read",
670 format!(
671 "A {} run cannot read {}/{}.",
672 run.kind.as_str(),
673 repo.namespace,
674 repo.name
675 ),
676 )
677 }
678}
679
680/// Whether a push to `repo` is limited to certain branches, so that the
681/// refs it moves have to be read and checked with [`decide_refs`].
682pub fn limits_branches(scope: &AgentScope, repo: &RepoPath) -> bool {
683 scope
684 .run
685 .iter()
686 .flat_map(|run| run.push.iter())
687 .any(|grant| same_repo(&grant.repo, repo) && grant.branch.is_some())
688}
689
690/// Whether a push to `repo` may move `refs` (full refs, such as
691/// `refs/heads/main`). Tags are never a run's to move.
692pub fn decide_refs(scope: &AgentScope, repo: &RepoPath, refs: &[String]) -> Decision {
693 let repo_decision = decide_git(scope, repo, true);
694 if !repo_decision.allowed {
695 return repo_decision;
696 }
697 let grants: Vec<&GitGrant> = scope
698 .run
699 .iter()
700 .flat_map(|run| run.push.iter())
701 .filter(|grant| same_repo(&grant.repo, repo))
702 .collect();
703 for git_ref in refs {
704 let Some(branch) = git_ref.strip_prefix("refs/heads/") else {
705 return Decision::deny("git:ref", format!("A run cannot push {git_ref}."));
706 };
707 let allowed = grants
708 .iter()
709 .any(|grant| grant.branch.as_deref().is_none_or(|only| only == branch));
710 if !allowed {
711 return Decision::deny(
712 "git:ref",
713 format!(
714 "A run cannot push to {branch} in {}/{}.",
715 repo.namespace, repo.name
716 ),
717 );
718 }
719 }
720 repo_decision
721}
722
723/// The most an agent may be on a repository, whoever it works for: it
724/// can push, merge and run, never change settings or who has access.
725pub const AGENT_CEILING: RepoRole = RepoRole::Write;
726
727/// The memberships an agent working for `person` has: the run's
728/// workspace, as a member, only if the person is in it now, with the
729/// person's role on its repositories (an owner's Admin included) cut down
730/// to [`AGENT_CEILING`].
731pub fn intersect(person: &[Membership], namespace: &str) -> Vec<Membership> {
732 let namespace = namespace.to_lowercase();
733 person
734 .iter()
735 .filter(|membership| membership.slug == namespace)
736 .map(|membership| {
737 let base = match membership.role {
738 Role::Owner => BasePermission::Admin,
739 Role::Member => membership.base_permission.unwrap_or_default(),
740 };
741 Membership {
742 role: Role::Member,
743 base_permission: Some(match base {
744 BasePermission::Admin => BasePermission::Write,
745 base => base,
746 }),
Merge main (membership, two-factor, GitHub repo roles) into tokens747 // Billing and security manager are the person's, never
748 // their agent's.
749 org_roles: Vec::new(),
The Packages page names every registry it has750 ..membership.clone()
751 }
752 })
753 .collect()
754}
755
756/// The repository grants an agent working for `person` has: those in the
757/// run's workspace, each cut down to [`AGENT_CEILING`].
758pub fn intersect_grants(person: &[RepoGrant], namespace: &str) -> Vec<RepoGrant> {
759 let namespace = namespace.to_lowercase();
760 person
761 .iter()
762 .filter(|grant| grant.workspace == namespace)
763 .map(|grant| RepoGrant {
764 role: grant.role.min(AGENT_CEILING),
765 ..grant.clone()
766 })
767 .collect()
768}
769
770/// Who a runner's credential acts as downstream: the person, with only the
771/// agent's (already intersected) memberships. `None` for anything else.
772pub fn as_person(user: &User) -> Option<User> {
773 let acting = user.acting.as_ref()?;
774 if user.kind != PrincipalKind::Agent {
775 return None;
776 }
777 let run = acting.run()?;
778 if run.usage != CredentialUse::Runner {
779 return None;
780 }
781 Some(User {
782 id: acting.on_behalf_of.id.clone(),
783 username: acting.on_behalf_of.username.clone(),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers784 display_username: None,
The Packages page names every registry it has785 kind: PrincipalKind::User,
786 verified: user.verified,
787 workspaces: user.workspaces.clone(),
788 avatar: None,
789 acting: None,
790 grants: user.grants.clone(),
791 token: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens792 held: Vec::new(),
The Packages page names every registry it has793 })
794}
795
796/// How an actor is described: "g1t on behalf of syntaqx".
797pub fn describe(user: &User) -> String {
798 match &user.acting {
799 Some(acting) => format!(
800 "{} on behalf of {}",
801 acting.agent, acting.on_behalf_of.username
802 ),
803 None => user.username.clone(),
804 }
805}
806
807#[cfg(test)]
808mod tests {
809 use super::*;
810
811 #[test]
812 fn a_run_s_scopes_are_never_admin() {
813 for kind in RunCredentialKind::ALL {
814 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
815 let scopes = run_scopes(kind, usage);
816 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{kind:?} {usage:?}: {scopes:?}");
817 }
818 }
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.819 use crate::scopes::Scope;
820 let bump = run_scopes(RunCredentialKind::Bump, CredentialUse::Runner);
821 assert!(bump.len() == 2 && bump.contains(&Scope::CodeRead) && bump.contains(&Scope::CodeWrite));
822 assert_eq!(run_scopes(RunCredentialKind::Checks, CredentialUse::Runner), [Scope::CodeRead]);
The Packages page names every registry it has823 }
824
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.825 /// The hosted agents' kinds still parse, so stored tokens read, but
826 /// they grant nothing: no operations, no scopes.
The Packages page names every registry it has827 #[test]
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.828 fn retired_kinds_parse_and_grant_nothing() {
829 for kind in RunCredentialKind::ALL {
830 let parsed: RunCredentialKind = serde_json::from_value(serde_json::json!(kind.as_str())).unwrap();
831 assert_eq!(parsed, kind);
832 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
833 assert!(operations_for(kind, usage).is_empty(), "{kind:?} {usage:?}");
834 if kind.retired() {
835 assert!(run_scopes(kind, usage).is_empty(), "{kind:?} {usage:?}");
836 }
837 }
The Packages page names every registry it has838 }
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.839 let retired: Vec<&str> = RunCredentialKind::ALL.iter().filter(|kind| kind.retired()).map(|kind| kind.as_str()).collect();
840 assert_eq!(retired, ["implement", "revise", "review", "answer", "update", "plan"]);
The Packages page names every registry it has841 }
842
843 #[test]
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.844 fn site_wide_search_only_reads() {
The Packages page names every registry it has845 assert!(is_read("search"));
846 assert!(!NEVER.contains(&"search"));
847 }
848
849 fn path(namespace: &str, name: &str) -> RepoPath {
850 RepoPath {
851 namespace: namespace.to_owned(),
852 name: name.to_owned(),
853 }
854 }
855
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.856 /// What a token stored before the hosted agents were retired lists:
857 /// the operations it was made with, which it keeps until it expires.
858 fn stored_operations(kind: RunCredentialKind, usage: CredentialUse) -> Vec<String> {
859 use RunCredentialKind as K;
860 let names: Vec<&str> = match usage {
861 CredentialUse::Runner => match kind {
862 K::Implement => vec!["get_repo", "get_pull_request", "record_session", "mark_pull_request_ready"],
863 K::Revise | K::Answer | K::Update => vec!["get_repo", "get_pull_request", "record_session"],
864 _ => vec![],
865 },
866 CredentialUse::Tools => match kind {
867 K::Implement | K::Revise | K::Answer => vec!["get_issue", "get_pull_request", "create_issue", "add_comment", "remember"],
868 K::Review => vec!["get_issue", "get_pull_request", "add_comment", "review_pull_request"],
869 K::Plan => vec!["get_issue", "get_pull_request", "create_issue"],
870 K::Update => vec!["get_issue", "get_pull_request"],
871 _ => vec![],
872 },
873 };
874 names.into_iter().map(str::to_owned).collect()
875 }
876
The Packages page names every registry it has877 fn scope(kind: RunCredentialKind, usage: CredentialUse) -> AgentScope {
878 AgentScope {
879 repo: path("acme", "rocket"),
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.880 operations: stored_operations(kind, usage),
The Packages page names every registry it has881 run: Some(RunBinding {
882 kind,
883 usage,
884 run_id: Some("run_1".to_owned()),
885 number: Some(7),
886 agent: "g1t".to_owned(),
887 system: false,
888 read: vec![path("acme", "rocket")],
889 push: match kind {
890 RunCredentialKind::Implement
891 | RunCredentialKind::Revise
892 | RunCredentialKind::Answer => vec![GitGrant {
893 repo: path("pulls", "pul_7"),
894 branch: None,
895 }],
896 RunCredentialKind::Update => vec![GitGrant {
897 repo: path("acme", "rocket"),
898 branch: Some("fix-login".to_owned()),
899 }],
900 _ => vec![],
901 },
902 }),
903 }
904 }
905
906 fn agent(member_of: &[&str], scope: AgentScope) -> User {
907 User {
908 id: "usr_g1t_agent".to_owned(),
909 username: "g1t".to_owned(),
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers910 display_username: None,
The Packages page names every registry it has911 kind: PrincipalKind::Agent,
912 verified: true,
913 workspaces: member_of
914 .iter()
915 .map(|slug| Membership::member(*slug))
916 .collect(),
917 avatar: None,
918 grants: Vec::new(),
919 token: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens920 held: Vec::new(),
The Packages page names every registry it has921 acting: Some(Box::new(Acting {
922 credential_id: "tok_1".to_owned(),
923 agent: "g1t".to_owned(),
924 on_behalf_of: Principal {
925 id: "usr_1".to_owned(),
926 username: "syntaqx".to_owned(),
927 },
928 scope,
929 })),
930 }
931 }
932
933 fn op(kind: RunCredentialKind, usage: CredentialUse, operation: &str) -> Decision {
934 let scope = scope(kind, usage);
935 let user = agent(&["acme"], scope.clone());
936 decide_operation(
937 &user,
938 &scope,
939 operation,
940 Some(&path("acme", "rocket")),
941 true,
942 Some(7),
943 )
944 }
945
946 use CredentialUse::{Runner, Tools};
947 use RunCredentialKind as K;
948
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.949 /// A stored token may use only what it lists, and never what no run
950 /// may.
The Packages page names every registry it has951 #[test]
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.952 fn a_stored_token_uses_only_what_it_lists() {
953 assert!(op(K::Implement, Tools, "create_issue").allowed);
954 assert!(op(K::Review, Tools, "review_pull_request").allowed);
955 assert!(!op(K::Review, Tools, "create_issue").allowed);
956 assert!(!op(K::Plan, Tools, "add_comment").allowed);
957 assert!(!op(K::Checks, Tools, "get_issue").allowed);
958 for operation in ["record_session", "merge_pull_request", "update_repo_settings", "create_webhook", "set_actions_secret"] {
959 for kind in [K::Implement, K::Revise, K::Answer, K::Review, K::Plan, K::Checks] {
960 assert!(!op(kind, Tools, operation).allowed, "{operation} by a {} run's tools", kind.as_str());
The Packages page names every registry it has961 }
962 }
963 }
964
965 #[test]
966 fn runner_matrix() {
967 assert!(op(K::Implement, Runner, "record_session").allowed);
968 assert!(op(K::Implement, Runner, "mark_pull_request_ready").allowed);
969 assert!(op(K::Revise, Runner, "record_session").allowed);
970 assert!(!op(K::Revise, Runner, "mark_pull_request_ready").allowed);
971 assert!(!op(K::Implement, Runner, "create_issue").allowed);
972 assert!(!op(K::Review, Runner, "record_session").allowed);
973 assert!(!op(K::Checks, Runner, "get_issue").allowed);
974 }
975
976 #[test]
977 fn settings_billing_tokens_and_members_are_never_reachable() {
978 for kind in RunCredentialKind::ALL {
979 for usage in [Runner, Tools] {
980 for operation in NEVER.iter().copied() {
981 let decision = op(kind, usage, operation);
982 assert!(!decision.allowed);
983 assert_eq!(decision.rule, "never");
984 }
985 }
986 }
987 // Even a scope that lists one is refused.
988 let mut wide = scope(K::Implement, Tools);
989 wide.operations.push("merge_pull_request".to_owned());
990 let user = agent(&["acme"], wide.clone());
991 let decision = decide_operation(
992 &user,
993 &wide,
994 "merge_pull_request",
995 Some(&path("acme", "rocket")),
996 true,
997 Some(7),
998 );
999 assert_eq!(decision.rule, "never");
1000 }
1001
1002 #[test]
1003 fn another_repository_is_refused() {
1004 let scope = scope(K::Implement, Tools);
1005 let user = agent(&["acme"], scope.clone());
1006 let decision = decide_operation(
1007 &user,
1008 &scope,
1009 "create_issue",
1010 Some(&path("acme", "other")),
1011 true,
1012 None,
1013 );
1014 assert!(!decision.allowed);
1015 assert_eq!(decision.rule, "scope:repository");
1016 let decision = decide_operation(&user, &scope, "create_issue", None, true, None);
1017 assert_eq!(decision.rule, "scope:repository");
1018 // The repository's name is matched without regard to case.
1019 let decision = decide_operation(
1020 &user,
1021 &scope,
1022 "create_issue",
1023 Some(&path("Acme", "Rocket")),
1024 true,
1025 None,
1026 );
1027 assert!(decision.allowed);
1028 assert_eq!(decision.rule, "run:implement/tools");
1029 }
1030
1031 #[test]
1032 fn the_permission_is_the_intersection_with_the_person() {
1033 let scope = scope(K::Implement, Tools);
1034 // The person left the workspace: their agent can do nothing there.
1035 let user = agent(&[], scope.clone());
1036 let decision = decide_operation(
1037 &user,
1038 &scope,
1039 "get_issue",
1040 Some(&path("acme", "rocket")),
1041 true,
1042 Some(1),
1043 );
1044 assert!(!decision.allowed);
1045 assert_eq!(decision.rule, "on-behalf-of:membership");
1046 // And an owner's agent is only ever a member.
1047 let owner = vec![
1048 Membership {
1049 slug: "acme".to_owned(),
1050 role: Role::Owner,
1051 name: None,
1052 avatar: None,
1053 base_permission: Some(BasePermission::None),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1054 team_creation: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens1055 org_roles: vec![crate::OrgRole::SecurityManager],
1056 privileges: None,
The Packages page names every registry it has1057 },
1058 Membership::member("elsewhere"),
1059 ];
1060 let memberships = intersect(&owner, "Acme");
1061 assert_eq!(memberships.len(), 1);
1062 assert_eq!(memberships[0].slug, "acme");
1063 assert_eq!(memberships[0].role, Role::Member);
Merge main (membership, two-factor, GitHub repo roles) into tokens1064 assert!(memberships[0].org_roles.is_empty());
The Packages page names every registry it has1065 assert!(intersect(&owner, "nowhere").is_empty());
1066 }
1067
1068 /// An agent gets at most the person's role on the repository, and
1069 /// never more than Write; nothing outside the run's workspace.
1070 #[test]
1071 fn an_agent_has_at_most_its_persons_role() {
1072 use crate::access::{Capability, RepoRef, can, permission};
1073 let rocket = RepoRef { id: "rep_1", namespace: "acme", private: true };
1074 let other = RepoRef { id: "rep_2", namespace: "acme", private: true };
1075 let elsewhere = RepoRef { id: "rep_3", namespace: "globex", private: true };
1076 let tools = scope(K::Implement, Tools);
1077 let scope = scope(K::Implement, Runner);
1078 // An owner's agent: Write, never Admin.
1079 let owner = [Membership { role: Role::Owner, ..Membership::member("acme") }, Membership::member("globex")];
1080 let mut agent_user = agent(&[], scope.clone());
1081 agent_user.workspaces = intersect(&owner, "acme");
1082 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1083 assert!(!can(Some(&agent_user), rocket, Capability::ManageSettings));
1084 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1085 // A member whose workspace gives Read: Read, so it cannot push.
1086 let reader = [Membership { base_permission: Some(BasePermission::Read), ..Membership::member("acme") }];
1087 agent_user.workspaces = intersect(&reader, "acme");
1088 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Read));
1089 assert!(!can(Some(&agent_user), rocket, Capability::Push));
1090 // An outside collaborator with Maintain on one repository: Write
1091 // there, nothing elsewhere, and the run is allowed.
1092 let grants = [
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1093 RepoGrant { repo_id: "rep_1".into(), workspace: "acme".into(), role: RepoRole::Maintain, team: None },
1094 RepoGrant { repo_id: "rep_3".into(), workspace: "globex".into(), role: RepoRole::Admin, team: None },
The Packages page names every registry it has1095 ];
1096 agent_user.workspaces = intersect(&[], "acme");
1097 agent_user.grants = intersect_grants(&grants, "Acme");
1098 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1099 assert_eq!(permission(Some(&agent_user), other), None);
1100 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1101 let decision = decide_operation(&agent_user, &tools, "get_issue", Some(&path("acme", "rocket")), true, Some(1));
1102 assert!(decision.allowed, "{}", decision.reason.unwrap_or_default());
1103 // The person, downstream of a runner's credential, carries the same.
1104 let person = as_person(&agent_user).expect("a runner acts as the person");
1105 assert_eq!(permission(Some(&person), rocket), Some(RepoRole::Write));
1106 }
1107
1108 #[test]
1109 fn a_runner_changes_only_its_own_pull_request() {
1110 let scope = scope(K::Implement, Runner);
1111 let user = agent(&["acme"], scope.clone());
1112 let repo = path("acme", "rocket");
1113 let other = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(8));
1114 assert!(!other.allowed);
1115 assert_eq!(other.rule, "scope:pull");
1116 let own = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(7));
1117 assert!(own.allowed);
1118 // Reading another is fine.
1119 assert!(
1120 decide_operation(
1121 &user,
1122 &scope,
1123 "get_pull_request",
1124 Some(&repo),
1125 true,
1126 Some(8)
1127 )
1128 .allowed
1129 );
1130 }
1131
1132 #[test]
1133 fn git_matrix() {
1134 let fork = path("pulls", "pul_7");
1135 let upstream = path("acme", "rocket");
1136 let elsewhere = path("acme", "billing");
1137 let implement = scope(K::Implement, Runner);
1138 assert!(decide_git(&implement, &fork, true).allowed);
1139 assert!(decide_git(&implement, &fork, false).allowed);
1140 assert!(decide_git(&implement, &upstream, false).allowed);
1141 assert_eq!(decide_git(&implement, &upstream, true).rule, "git:push");
1142 assert_eq!(decide_git(&implement, &elsewhere, false).rule, "git:read");
1143 let review = scope(K::Review, Runner);
1144 assert!(decide_git(&review, &upstream, false).allowed);
1145 assert!(!decide_git(&review, &upstream, true).allowed);
1146 assert!(!decide_git(&review, &fork, true).allowed);
1147 // A tools token made before run credentials never reaches git.
1148 let old = AgentScope {
1149 repo: upstream.clone(),
1150 operations: vec!["get_issue".to_owned()],
1151 run: None,
1152 };
1153 assert_eq!(decide_git(&old, &upstream, false).rule, "git:not-a-run");
1154 // Nor does an agent's tools token.
1155 assert_eq!(
1156 decide_git(&scope(K::Implement, Tools), &upstream, false).rule,
1157 "git:not-a-run"
1158 );
1159 }
1160
1161 #[test]
1162 fn a_push_moves_only_granted_branches() {
1163 let update = scope(K::Update, Runner);
1164 let repo = path("acme", "rocket");
1165 let refs = |names: &[&str]| {
1166 names
1167 .iter()
1168 .map(|name| (*name).to_owned())
1169 .collect::<Vec<_>>()
1170 };
1171 assert!(decide_refs(&update, &repo, &refs(&["refs/heads/fix-login"])).allowed);
1172 assert_eq!(
1173 decide_refs(&update, &repo, &refs(&["refs/heads/main"])).rule,
1174 "git:ref"
1175 );
1176 assert_eq!(
1177 decide_refs(
1178 &update,
1179 &repo,
1180 &refs(&["refs/heads/fix-login", "refs/tags/v1"])
1181 )
1182 .rule,
1183 "git:ref"
1184 );
1185 let implement = scope(K::Implement, Runner);
1186 assert!(
1187 decide_refs(
1188 &implement,
1189 &path("pulls", "pul_7"),
1190 &refs(&["refs/heads/main"])
1191 )
1192 .allowed
1193 );
1194 }
1195
1196 #[test]
1197 fn a_runner_acts_downstream_as_the_person() {
1198 let user = agent(&["acme"], scope(K::Implement, Runner));
1199 let person = as_person(&user).unwrap();
1200 assert_eq!(person.id, "usr_1");
1201 assert_eq!(person.username, "syntaqx");
1202 assert_eq!(person.kind, PrincipalKind::User);
1203 assert!(person.is_member("acme"));
1204 assert!(person.acting.is_none());
1205 assert_eq!(describe(&user), "g1t on behalf of syntaqx");
1206 // The tools act as the agent.
1207 assert!(as_person(&agent(&["acme"], scope(K::Implement, Tools))).is_none());
1208 }
1209
1210 #[test]
1211 fn scopes_without_a_run_still_parse() {
1212 let old: AgentScope = serde_json::from_str(
1213 r#"{"repo":{"namespace":"acme","name":"rocket"},"operations":["get_issue"]}"#,
1214 )
1215 .unwrap();
1216 assert!(old.run.is_none());
1217 let written = serde_json::to_string(&scope(K::Review, Tools)).unwrap();
1218 assert!(written.contains(r#""use":"tools""#));
1219 assert!(written.contains(r#""kind":"review""#));
1220 let back: AgentScope = serde_json::from_str(&written).unwrap();
1221 assert_eq!(back.run.unwrap().kind, K::Review);
1222 // Only g1t's own runs say so; every other reads as not.
1223 assert!(!written.contains("system"));
1224 assert!(!back_run(&written).system);
1225 let mut own = scope(K::Bump, Runner);
1226 own.run.as_mut().unwrap().system = true;
1227 let written = serde_json::to_string(&own).unwrap();
1228 assert!(written.contains(r#""system":true"#));
1229 assert!(back_run(&written).system);
1230 }
1231
1232 fn back_run(written: &str) -> RunBinding {
1233 serde_json::from_str::<AgentScope>(written).unwrap().run.unwrap()
1234 }
1235}

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