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

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

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