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
248 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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1//! Run credentials: a token per sandbox run, bound to the run, its
2//! repository and what its kind of work needs, acting as an agent on
3//! behalf of the person who started the work. See
4//! `g1t_contracts::credentials` for the policy.
5
6use g1t_contracts::credentials::{
7 Acting, BindRunCredentialsArgs, CreateRunCredentialArgs, Principal, RevokeRunCredentialsArgs,
8 RunBinding, intersect, intersect_grants, operations_for,
9};
10use g1t_contracts::identity::{
11 AGENT_ID, AGENT_NAME, AgentScope, CreateAccessTokenArgs, CreatedAccessToken,
12};
13use g1t_contracts::time::SQL_NOW;
14use g1t_contracts::{PrincipalKind, User, Viewer};
15use worker::Result;
16use worker::wasm_bindgen::JsValue;
17
18use crate::Identity;
19
20/// No run outlives this, whatever its caller asks for.
21const MAX_RUN_TTL_SECONDS: u64 = 6 * 60 * 60;
22const MIN_RUN_TTL_SECONDS: u64 = 60;
23/// More hashes than one sandbox ever holds.
24const MAX_HASHES: usize = 8;
25
26/// A SHA-256 in lowercase hex, as tokens are stored.
27fn is_hash(value: &str) -> bool {
28 value.len() == 64
29 && value
30 .bytes()
31 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
32}
33
34impl Identity {
35 /// The composite identity behind an agent's token: the agent, acting
36 /// for the person (or workspace) the token was made for, in the run's
37 /// workspace only, and only while that person still belongs to it.
38 pub(crate) async fn agent_principal(
39 &self,
40 credential_id: &str,
41 user_id: Option<&str>,
42 workspace_id: Option<&str>,
43 scope: AgentScope,
44 ) -> Result<Viewer> {
45 let person = match (user_id, workspace_id) {
46 (Some(id), _) => {
47 self.find_user(
48 "SELECT id, username, email_verified_at IS NOT NULL AS verified
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)49 FROM users WHERE id = ? AND deleted_at IS NULL",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas50 id,
51 )
52 .await?
53 }
54 (None, Some(id)) => self.workspace_principal(id).await?,
55 (None, None) => None,
56 };
57 // The person is gone: so is everything that acted for them.
58 let Some(person) = person else {
59 return Ok(None);
60 };
61 let agent = scope
62 .run
63 .as_ref()
64 .map(|run| run.agent.clone())
65 .unwrap_or_else(|| AGENT_NAME.to_owned());
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily66 // g1t's own run: the workspace's credential, acting as g1t, so the
67 // pull request and working copy g1t opened are its own (run.system).
68 let on_behalf_of = if scope.run.as_ref().is_some_and(|run| run.system) {
69 Principal {
70 id: g1t_contracts::system::ID.to_owned(),
71 username: g1t_contracts::system::USERNAME.to_owned(),
72 }
73 } else {
74 Principal {
75 id: person.id,
76 username: person.username,
77 }
78 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas79 Ok(Some(User {
80 id: AGENT_ID.to_owned(),
81 username: AGENT_NAME.to_owned(),
82 kind: PrincipalKind::Agent,
83 verified: person.verified,
84 workspaces: intersect(&person.workspaces, &scope.repo.namespace),
85 // The person's roles on the workspace's repositories, so an
86 // outside collaborator's agent works where they may, and never
87 // beyond Write (credentials::AGENT_CEILING).
88 grants: intersect_grants(&person.grants, &scope.repo.namespace),
89 acting: Some(Box::new(Acting {
90 credential_id: credential_id.to_owned(),
91 agent,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily92 on_behalf_of,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas93 scope,
94 })),
95 ..User::default()
96 }))
97 }
98
99 pub async fn create_run_credential(
100 &self,
101 a: CreateRunCredentialArgs,
102 ) -> Result<CreatedAccessToken> {
103 let agent = a
104 .agent
105 .filter(|agent| !agent.trim().is_empty())
106 .unwrap_or_else(|| AGENT_NAME.to_owned());
107 let scope = AgentScope {
108 repo: a.repo.clone(),
109 operations: operations_for(a.kind, a.usage)
110 .into_iter()
111 .map(str::to_owned)
112 .collect(),
113 run: Some(RunBinding {
114 kind: a.kind,
115 usage: a.usage,
116 run_id: None,
117 number: a.number,
118 agent: agent.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily119 system: a.on_behalf_of.kind == PrincipalKind::System,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas120 read: a.read,
121 push: a.push,
122 }),
123 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily124 // g1t's own work (a security update, an agent it put on an upgrade)
125 // has no person behind it: its credential acts for the workspace,
126 // as a workspace's own token would.
127 let on_behalf_of = if a.on_behalf_of.kind == PrincipalKind::System {
128 match self.workspace_id_of(&a.repo.namespace).await? {
129 Some(id) => User {
130 id,
131 username: a.repo.namespace.to_lowercase(),
132 kind: PrincipalKind::Workspace,
133 ..User::default()
134 },
135 None => return Err(worker::Error::RustError(format!("no workspace {}", a.repo.namespace))),
136 }
137 } else {
138 a.on_behalf_of
139 };
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas140 let created = self
141 .create_access_token(CreateAccessTokenArgs {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily142 user: on_behalf_of,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas143 name: format!(
144 "{agent}: {} run in {}/{}{}",
145 a.kind.as_str(),
146 a.repo.namespace,
147 a.repo.name,
148 a.number.map(|n| format!("#{n}")).unwrap_or_default()
149 ),
150 ttl_seconds: Some(
151 a.ttl_seconds
152 .clamp(MIN_RUN_TTL_SECONDS, MAX_RUN_TTL_SECONDS),
153 ),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step154 // A run credential's scope is its run's; see agent_scope.
155 scopes: None,
156 listed: false,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas157 })
158 .await?;
159 self.db
160 .prepare("UPDATE access_tokens SET agent_scope = ? WHERE id = ?")
161 .bind(&[
162 serde_json::to_string(&scope)?.into(),
163 created.info.id.as_str().into(),
164 ])?
165 .run()
166 .await?;
167 Ok(created)
168 }
169
170 /// Ties a sandbox's credentials to the agent run it recorded. A token
171 /// already bound keeps its run.
172 pub async fn bind_run_credentials(&self, a: BindRunCredentialsArgs) -> Result<bool> {
173 let hashes: Vec<&String> = a
174 .token_hashes
175 .iter()
176 .filter(|hash| is_hash(hash))
177 .take(MAX_HASHES)
178 .collect();
179 if hashes.is_empty() || a.run_id.trim().is_empty() {
180 return Ok(false);
181 }
182 let marks = vec!["?"; hashes.len()].join(", ");
183 let mut values: Vec<JsValue> = vec![a.run_id.as_str().into(), a.run_id.as_str().into()];
184 values.extend(hashes.iter().map(|hash| JsValue::from(hash.as_str())));
185 self.db
186 .prepare(format!(
187 "UPDATE access_tokens
188 SET run_id = ?, agent_scope = json_set(agent_scope, '$.run.runId', ?)
189 WHERE token_hash IN ({marks}) AND run_id IS NULL
190 AND json_extract(agent_scope, '$.run') IS NOT NULL"
191 ))
192 .bind(&values)?
193 .run()
194 .await?;
195 Ok(true)
196 }
197
198 /// Ends a sandbox's credentials: they stop working at once. Only run
199 /// credentials are touched.
200 pub async fn revoke_run_credentials(&self, a: RevokeRunCredentialsArgs) -> Result<bool> {
201 let hashes: Vec<&String> = a
202 .token_hashes
203 .iter()
204 .filter(|hash| is_hash(hash))
205 .take(MAX_HASHES)
206 .collect();
207 let mut conditions = Vec::new();
208 let mut values: Vec<JsValue> = Vec::new();
209 if !hashes.is_empty() {
210 conditions.push(format!(
211 "token_hash IN ({})",
212 vec!["?"; hashes.len()].join(", ")
213 ));
214 values.extend(hashes.iter().map(|hash| JsValue::from(hash.as_str())));
215 }
216 if let Some(run_id) = a.run_id.as_deref().filter(|id| !id.trim().is_empty()) {
217 conditions.push("run_id = ?".to_owned());
218 values.push(run_id.into());
219 }
220 if conditions.is_empty() {
221 return Ok(false);
222 }
223 self.db
224 .prepare(format!(
225 "UPDATE access_tokens SET expires_at = {SQL_NOW}
226 WHERE ({}) AND json_extract(agent_scope, '$.run') IS NOT NULL
227 AND (expires_at IS NULL OR expires_at > {SQL_NOW})",
228 conditions.join(" OR ")
229 ))
230 .bind(&values)?
231 .run()
232 .await?;
233 Ok(true)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::is_hash;
240
241 #[test]
242 fn only_hashes_are_taken() {
243 assert!(is_hash(&"a1".repeat(32)));
244 assert!(!is_hash(&"A1".repeat(32)));
245 assert!(!is_hash("g1t_0123"));
246 assert!(!is_hash(&"0".repeat(63)));
247 }
248}

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