Skip to content

g1t/crates/contracts/src/credentials.rs

1,271 lines44,273 bytesCodeBlame

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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! Run credentials: the least-privilege tokens a sandbox works with.
2//!
3//! Every sandbox run gets its own tokens, bound to the run, its repository
4//! (and the pull request's fork), what that kind of run needs to do, and an
5//! expiry no later than the run's timeout. Each carries a composite
6//! identity: an agent acting on behalf of the person who started the work.
7//! What it may do is the intersection of the two: the run's scope, and what
8//! that person may do right now.
9//!
10//! The policy lives here, as pure functions, so that identity (which mints
11//! the tokens), the API (which serves REST and MCP) and repos (which serves
12//! git) all enforce the same rules, and so the rules can be tested.
13
14use serde::{Deserialize, Serialize};
15
16use crate::identity::AgentScope;
17use crate::repos::RepoPath;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look18use crate::access::{BasePermission, RepoGrant, RepoRole};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API19use crate::{Membership, PrincipalKind, Role, User};
20
21/// What a run does, as far as its credentials are concerned. The same names
22/// as [`crate::agents::RunKind`], plus `deploy`, a build of one commit.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum RunCredentialKind {
26 Implement,
27 Revise,
28 Review,
29 Answer,
30 Update,
31 Plan,
32 Checks,
33 Queue,
34 Mergecheck,
35 Deploy,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily36 /// A security update: raising one package's version in its lockfiles
37 /// and pushing that to a branch of its own. Not an agent.
38 Bump,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API39}
40
41impl RunCredentialKind {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily42 pub const ALL: [RunCredentialKind; 11] = [
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API43 RunCredentialKind::Implement,
44 RunCredentialKind::Revise,
45 RunCredentialKind::Review,
46 RunCredentialKind::Answer,
47 RunCredentialKind::Update,
48 RunCredentialKind::Plan,
49 RunCredentialKind::Checks,
50 RunCredentialKind::Queue,
51 RunCredentialKind::Mergecheck,
52 RunCredentialKind::Deploy,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily53 RunCredentialKind::Bump,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API54 ];
55
56 pub fn as_str(self) -> &'static str {
57 match self {
58 RunCredentialKind::Implement => "implement",
59 RunCredentialKind::Revise => "revise",
60 RunCredentialKind::Review => "review",
61 RunCredentialKind::Answer => "answer",
62 RunCredentialKind::Update => "update",
63 RunCredentialKind::Plan => "plan",
64 RunCredentialKind::Checks => "checks",
65 RunCredentialKind::Queue => "queue",
66 RunCredentialKind::Mergecheck => "mergecheck",
67 RunCredentialKind::Deploy => "deploy",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily68 RunCredentialKind::Bump => "bump",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API69 }
70 }
71
72 /// Whether the run works on one pull request, whose session and
73 /// readiness it reports.
74 fn works_on_a_pull(self) -> bool {
75 matches!(
76 self,
77 RunCredentialKind::Implement
78 | RunCredentialKind::Revise
79 | RunCredentialKind::Answer
80 | RunCredentialKind::Update
81 )
82 }
83}
84
85/// Which part of a sandbox a credential is for.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum CredentialUse {
89 /// g1t's runner: cloning, pushing the result, recording the session.
90 /// It acts as the person downstream, so that what it pushes and records
91 /// is theirs, within the run's scope.
92 Runner,
93 /// The agent's own tools, over MCP. It acts as the agent.
94 Tools,
95}
96
97impl CredentialUse {
98 pub fn as_str(self) -> &'static str {
99 match self {
100 CredentialUse::Runner => "runner",
101 CredentialUse::Tools => "tools",
102 }
103 }
104}
105
106/// A repository a run may push to, and the one branch, if only one.
107#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
108pub struct GitGrant {
109 pub repo: RepoPath,
110 /// Null: any branch. A pull request's fork is its own repository, so
111 /// the whole of it is the pull request's.
112 #[serde(default)]
113 pub branch: Option<String>,
114}
115
116/// What binds an agent's token to one run. Absent on agent tokens made
117/// before run credentials, which keep working for the API only.
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct RunBinding {
121 pub kind: RunCredentialKind,
122 #[serde(rename = "use")]
123 pub usage: CredentialUse,
124 /// The agent run, once the sandbox has recorded it.
125 #[serde(default)]
126 pub run_id: Option<String>,
127 /// The pull request the run works on, for the kinds that work on one.
128 #[serde(default)]
129 pub number: Option<u32>,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent130 /// The agent's name, such as `g1t`.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API131 pub agent: String,
132 /// Repositories it may clone and fetch, besides those it may push to.
133 #[serde(default)]
134 pub read: Vec<RepoPath>,
135 /// Where it may push.
136 #[serde(default)]
137 pub push: Vec<GitGrant>,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily138 /// g1t's own run (a security update, an agent g1t put on one): the
139 /// credential belongs to the workspace, and acts on behalf of g1t
140 /// (`system::ID`), so what it does is g1t's, and the pull request g1t
141 /// opened, and its working copy, are its own.
142 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
143 pub system: bool,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API144}
145
146/// A person, by id and name.
147#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
148pub struct Principal {
149 pub id: String,
150 pub username: String,
151}
152
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights153impl From<&User> for Principal {
154 fn from(user: &User) -> Self {
155 Principal {
156 id: user.id.clone(),
157 username: user.username.clone(),
158 }
159 }
160}
161
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API162/// Set on a [`User`] resolved from an agent's token: the composite
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent163/// identity, "g1t on behalf of syntaqx", and what it may do.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API164#[derive(Clone, Debug, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct Acting {
167 /// The token's id, as audit entries name it.
168 pub credential_id: String,
169 pub agent: String,
170 pub on_behalf_of: Principal,
171 pub scope: AgentScope,
172}
173
174impl Acting {
175 pub fn run(&self) -> Option<&RunBinding> {
176 self.scope.run.as_ref()
177 }
178}
179
180/// `create_run_credential`: a token for one sandbox run. It acts as
181/// `agent` on behalf of `on_behalf_of`, can do only what `kind` and `usage`
182/// allow in `repo`, and expires after `ttl_seconds`, which should be the
183/// run's timeout. Returns `CreatedAccessToken`.
184#[derive(Clone, Debug, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct CreateRunCredentialArgs {
187 pub on_behalf_of: User,
188 pub repo: RepoPath,
189 pub kind: RunCredentialKind,
190 #[serde(rename = "use")]
191 pub usage: CredentialUse,
192 #[serde(default)]
193 pub number: Option<u32>,
194 #[serde(default)]
195 pub read: Vec<RepoPath>,
196 #[serde(default)]
197 pub push: Vec<GitGrant>,
198 pub ttl_seconds: u64,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent199 /// Defaults to `g1t`.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API200 #[serde(default)]
201 pub agent: Option<String>,
202}
203
204/// `bind_run_credentials`: ties tokens, named by the SHA-256 of their
205/// text in hex, to the agent run their sandbox recorded. Returns how many.
206#[derive(Clone, Debug, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct BindRunCredentialsArgs {
209 pub token_hashes: Vec<String>,
210 pub run_id: String,
211}
212
213/// `revoke_run_credentials`: ends tokens when their sandbox stops, by hash
214/// or by run. Only run credentials are touched, never a token a person
215/// made. Returns how many.
216#[derive(Clone, Debug, Default, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct RevokeRunCredentialsArgs {
219 #[serde(default)]
220 pub token_hashes: Vec<String>,
221 #[serde(default)]
222 pub run_id: Option<String>,
223}
224
225// --- Policy --------------------------------------------------------------
226
227/// Operations that only read.
228pub const READ_OPERATIONS: &[&str] = &[
229 "whoami",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit230 "get_usage",
231 "get_budget",
232 "get_ai_credit",
233 "list_invoices",
234 "get_billing_details",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API235 "list_repos",
236 "get_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97237 "list_projects",
238 "get_project",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look239 "list_deleted_repos",
240 "list_collaborators",
241 "get_collaborator_permission",
242 "list_repo_invitations",
243 "list_my_repo_invitations",
244 "list_outside_collaborators",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar245 "list_teams",
246 "get_team",
247 "list_team_members",
248 "list_child_teams",
249 "list_team_repos",
250 "list_user_teams",
251 "get_codeowners_errors",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API252 "get_repo_settings",
Fast pages, required checks on the branch, self-hosted runners, honest incidents253 "list_check_names",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API254 "get_merge_queue",
255 "recall",
256 "list_issues",
257 "get_issue",
258 "get_plan",
259 "list_labels",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar260 "list_issue_labels",
261 "list_milestones",
262 "get_milestone",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API263 "list_pull_requests",
264 "get_pull_request",
265 "read_session",
266 "get_pull_request_changes",
267 "list_events",
268 "get_context",
269 "search_context",
270 "get_entity",
Search across all of g1t, Explore, and a command palette271 "search",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API272 "list_workflows",
273 "list_workflow_runs",
274 "get_workflow_run",
275 "get_job_logs",
Merge checks: statuses and check runs on every commit276 "list_commit_statuses",
277 "get_combined_status",
278 "list_check_runs_for_ref",
279 "get_check_run",
280 "list_check_run_annotations",
281 "list_check_suites_for_ref",
282 "get_check_suite",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API283 "list_integrations",
284 "get_model_routes",
285 "list_webhooks",
286 "list_webhook_deliveries",
287 "list_actions_secrets",
288 "list_actions_variables",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily289 "list_security_alerts",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar290 "list_secret_scanning_alerts",
291 "get_secret_scanning_alert",
292 "list_secret_scanning_locations",
293 "list_bypass_requests",
294 "list_custom_patterns",
295 "list_code_scanning_alerts",
296 "get_code_scanning_alert",
297 "list_code_scanning_analyses",
298 "get_sarif_upload",
299 "list_vulnerability_alerts",
300 "get_vulnerability_alert",
301 "get_dependency_graph",
302 "get_sbom",
303 "compare_dependencies",
304 "get_security_settings",
305 "get_workspace_security_settings",
306 "get_security_overview",
API: notifications over REST and MCP, with notifications scopes307 "list_notifications",
308 "get_notification_thread",
309 "get_thread_subscription",
310 "get_repo_subscription",
311 "list_watched_repos",
API: pinned projects over REST and MCP312 "list_pinned_projects",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API313];
314
315/// What no agent's token may ever do, whatever its scope says: workspaces,
316/// repositories' settings, members, tokens, billing, integrations,
317/// webhooks, secrets, workflows' controls, merging, and putting more agents
318/// to work.
319pub const NEVER: &[&str] = &[
Usage, Billing settings and prepaid AI credit; fixes from the UX audit320 // Billing is people's: agents never spend or change it.
321 "set_budget",
322 "buy_ai_credit",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API323 "create_workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look324 "delete_workspace",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily325 "update_workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look326 "transfer_repo",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API327 "create_repo",
328 "update_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97329 "update_project",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look330 "delete_repo",
331 "list_deleted_repos",
332 "restore_repo",
333 "purge_repo",
334 "rename_repo",
335 "archive_repo",
336 "unarchive_repo",
337 "set_repo_visibility",
338 "rename_branch",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API339 "update_repo_settings",
340 "merge_pull_request",
341 "assign_issue",
342 "plan_work",
343 "apply_plan",
344 "import_issue",
345 "list_integrations",
346 "connect_integration",
AI Gateway: OpenAI's format, open models, and your own providers347 "update_integration",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API348 "disconnect_integration",
349 "test_integration",
350 "get_model_routes",
351 "set_model_routes",
352 "list_webhooks",
353 "create_webhook",
354 "update_webhook",
355 "delete_webhook",
356 "ping_webhook",
357 "list_webhook_deliveries",
358 "redeliver_webhook",
359 "dispatch_workflow",
360 "cancel_workflow_run",
361 "rerun_workflow_run",
362 "update_workflow",
Merge checks: statuses and check runs on every commit363 // An agent never reports checks on its own work, nor asks for them
364 // to run again: what checks say is the integrations' to say.
365 "create_commit_status",
366 "create_check_run",
367 "update_check_run",
368 "rerequest_check_run",
369 "rerequest_check_suite",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API370 "list_actions_secrets",
371 "set_actions_secret",
372 "delete_actions_secret",
373 "list_actions_variables",
374 "set_actions_variable",
375 "delete_actions_variable",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look376 "list_collaborators",
377 "get_collaborator_permission",
378 "add_collaborator",
379 "update_collaborator",
380 "remove_collaborator",
381 "list_repo_invitations",
382 "revoke_repo_invitation",
383 "list_my_repo_invitations",
384 "accept_repo_invitation",
385 "decline_repo_invitation",
386 "set_base_permission",
387 "list_outside_collaborators",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar388 // Teams: who is in which, and what they reach, is for people.
389 "create_team",
390 "update_team",
391 "delete_team",
392 "set_team_member",
393 "remove_team_member",
394 "set_team_repo",
395 "remove_team_repo",
396 "set_team_review_assignment",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily397 // Dismissing a secret lets it through push protection.
398 "dismiss_security_alert",
399 "reopen_security_alert",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar400 // Nor any other decision about security: closing or reopening an
401 // alert, pushing past push protection or deciding who may, changing
402 // what is looked for or when checks fail, or putting more agents to
403 // work. An agent fixes what it finds in its own pull request.
404 "update_secret_scanning_alert",
405 "bypass_push_protection",
406 "review_bypass_request",
407 "create_custom_pattern",
408 "update_custom_pattern",
409 "delete_custom_pattern",
410 "update_code_scanning_alert",
411 "update_vulnerability_alert",
412 "fix_security_alert",
413 "update_security_settings",
414 "update_workspace_security_settings",
API: notifications over REST and MCP, with notifications scopes415 // A person's own inbox: g1t's agents act as g1t, which has none.
416 "list_notifications",
417 "get_notification_thread",
418 "mark_notifications_read",
419 "mark_thread_read",
420 "mark_thread_done",
421 "save_thread",
422 "snooze_thread",
423 "get_thread_subscription",
424 "set_thread_subscription",
425 "delete_thread_subscription",
426 "get_repo_subscription",
427 "set_repo_subscription",
428 "delete_repo_subscription",
429 "list_watched_repos",
API: pinned projects over REST and MCP430 // Pins are a person's own, as the inbox is.
431 "list_pinned_projects",
432 "pin_project",
433 "unpin_project",
434 "reorder_pinned_projects",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API435];
436
437/// Reading what an agent needs to know about its repository.
438const TOOLS_READ: &[&str] = &[
439 "get_repo",
440 "list_issues",
441 "get_issue",
442 "list_labels",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar443 "list_issue_labels",
444 "list_milestones",
445 "get_milestone",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API446 "list_pull_requests",
447 "get_pull_request",
448 "get_pull_request_changes",
449 "read_session",
450 "get_merge_queue",
451 "list_events",
452 "recall",
453 "search_context",
454 "get_entity",
Search across all of g1t, Explore, and a command palette455 "search",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API456 "list_workflows",
457 "list_workflow_runs",
458 "get_workflow_run",
459 "get_job_logs",
Merge checks: statuses and check runs on every commit460 "list_commit_statuses",
461 "get_combined_status",
462 "list_check_runs_for_ref",
463 "get_check_run",
464 "list_check_run_annotations",
465 "list_check_suites_for_ref",
466 "get_check_suite",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API467];
468
469pub fn is_read(operation: &str) -> bool {
470 READ_OPERATIONS.contains(&operation)
471}
472
473/// The API and MCP operations a run of `kind` may use with a credential
474/// for `usage`. Git is separate: see [`decide_git`].
475pub fn operations_for(kind: RunCredentialKind, usage: CredentialUse) -> Vec<&'static str> {
476 use RunCredentialKind as K;
477 let mut operations: Vec<&'static str> = Vec::new();
478 match usage {
479 CredentialUse::Runner => {
480 if kind.works_on_a_pull() {
481 operations.extend(["get_repo", "get_pull_request", "record_session"]);
482 }
483 if kind == K::Implement {
484 operations.push("mark_pull_request_ready");
485 }
486 }
487 CredentialUse::Tools => match kind {
488 K::Implement | K::Revise | K::Answer => {
489 operations.extend(TOOLS_READ.iter().copied());
490 operations.extend([
491 "create_issue",
492 "add_comment",
493 "take_messages",
494 "remember",
495 "message_agent",
496 "answer_message",
497 "get_context",
498 ]);
499 }
500 K::Review => {
501 operations.extend(TOOLS_READ.iter().copied());
502 operations.extend(["add_comment", "review_pull_request", "get_context"]);
503 }
504 K::Plan => {
505 operations.extend(TOOLS_READ.iter().copied());
506 operations.extend(["create_issue", "get_context"]);
507 }
508 K::Update => operations.extend(TOOLS_READ.iter().copied()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily509 K::Checks | K::Queue | K::Mergecheck | K::Deploy | K::Bump => {}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API510 },
511 }
512 operations
513}
514
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step515/// What a run's credential may do, in the scope vocabulary that access
516/// tokens use (see [`crate::scopes`]): the scopes of its operations, and
517/// for a runner, git's. Its operations, its repository and its run still
518/// bound it more tightly than these scopes say.
519pub fn run_scopes(kind: RunCredentialKind, usage: CredentialUse) -> Vec<crate::scopes::Scope> {
520 use crate::scopes::{Scope, normalize, scope_for};
521 let mut scopes: Vec<Scope> = operations_for(kind, usage)
522 .into_iter()
523 .filter_map(scope_for)
524 .collect();
525 if usage == CredentialUse::Runner {
526 scopes.push(Scope::CodeRead);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily527 if matches!(
528 kind,
529 RunCredentialKind::Implement
530 | RunCredentialKind::Revise
531 | RunCredentialKind::Answer
532 | RunCredentialKind::Update
533 | RunCredentialKind::Bump
534 ) {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step535 scopes.push(Scope::CodeWrite);
536 }
537 }
538 normalize(&mut scopes);
539 scopes
540}
541
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API542/// Operations that change a pull request, which a runner may do only to
543/// the pull request its run works on.
544const PULL_WRITES: &[&str] = &["record_session", "mark_pull_request_ready"];
545
546/// Whether something was allowed, and the rule that decided it.
547#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
548pub struct Decision {
549 pub allowed: bool,
550 /// A short, stable name: `run:implement/tools`, `never`,
551 /// `scope:repository` and so on. Shown in the audit log.
552 pub rule: String,
553 /// Why it was refused, for the caller.
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub reason: Option<String>,
556}
557
558impl Decision {
559 pub fn allow(rule: impl Into<String>) -> Self {
560 Decision {
561 allowed: true,
562 rule: rule.into(),
563 reason: None,
564 }
565 }
566
567 pub fn deny(rule: impl Into<String>, reason: impl Into<String>) -> Self {
568 Decision {
569 allowed: false,
570 rule: rule.into(),
571 reason: Some(reason.into()),
572 }
573 }
574}
575
576fn same_repo(a: &RepoPath, b: &RepoPath) -> bool {
577 a.namespace.eq_ignore_ascii_case(&b.namespace) && a.name.eq_ignore_ascii_case(&b.name)
578}
579
580fn scope_rule(scope: &AgentScope) -> String {
581 match &scope.run {
582 Some(run) => format!("run:{}/{}", run.kind.as_str(), run.usage.as_str()),
583 None => "agent-token".to_owned(),
584 }
585}
586
587/// Whether `user`, resolved from an agent's token with `scope`, may use
588/// `operation`. `repo` is the repository the call names, if any, and
589/// `needs_repo` whether the operation is about one; `number` the issue or
590/// pull request it names.
591pub fn decide_operation(
592 user: &User,
593 scope: &AgentScope,
594 operation: &str,
595 repo: Option<&RepoPath>,
596 needs_repo: bool,
597 number: Option<u32>,
598) -> Decision {
599 let who = "A g1t agent's token";
600 if NEVER.contains(&operation) {
601 return Decision::deny(
602 "never",
603 format!(
604 "{who} can never use {operation}: settings, members, tokens, billing, integrations, webhooks, secrets and merging are for people."
605 ),
606 );
607 }
608 if !scope.operations.iter().any(|name| name == operation) {
609 return Decision::deny(
610 "scope:operation",
611 format!("{who} for this run cannot use {operation}."),
612 );
613 }
614 if needs_repo && !repo.is_some_and(|asked| same_repo(asked, &scope.repo)) {
615 return Decision::deny(
616 "scope:repository",
617 format!(
618 "{who} works in {}/{} only.",
619 scope.repo.namespace, scope.repo.name
620 ),
621 );
622 }
623 // The intersection: the person it acts for must still be able to work
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look624 // in the repository's workspace, as a member or with a role on its
625 // repositories. What it may do in the repository itself is their
626 // role there, which services check (`access::can`).
627 if !crate::access::has_access_in(user, &scope.repo.namespace) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API628 return Decision::deny(
629 "on-behalf-of:membership",
630 format!(
631 "The person this agent works for is no longer a member of {}.",
632 scope.repo.namespace
633 ),
634 );
635 }
636 if let Some(run) = &scope.run
637 && run.usage == CredentialUse::Runner
638 && PULL_WRITES.contains(&operation)
639 && run.number.is_some()
640 && number != run.number
641 {
642 return Decision::deny(
643 "scope:pull",
644 format!(
645 "{who} can change pull request #{} only.",
646 run.number.unwrap_or_default()
647 ),
648 );
649 }
650 Decision::allow(scope_rule(scope))
651}
652
653/// Whether a run credential may clone or fetch (`write` false), or push to
654/// (`write` true), the repository at `repo`.
655pub fn decide_git(scope: &AgentScope, repo: &RepoPath, write: bool) -> Decision {
656 let Some(run) = scope
657 .run
658 .as_ref()
659 .filter(|run| run.usage == CredentialUse::Runner)
660 else {
661 return Decision::deny(
662 "git:not-a-run",
663 "A g1t agent's tools token cannot be used with git.",
664 );
665 };
666 let pushable = run.push.iter().any(|grant| same_repo(&grant.repo, repo));
667 if write {
668 return if pushable {
669 Decision::allow(format!("{}:push", scope_rule(scope)))
670 } else {
671 Decision::deny(
672 "git:push",
673 format!(
674 "A {} run cannot push to {}/{}.",
675 run.kind.as_str(),
676 repo.namespace,
677 repo.name
678 ),
679 )
680 };
681 }
682 let readable = pushable
683 || same_repo(&scope.repo, repo)
684 || run.read.iter().any(|path| same_repo(path, repo));
685 if readable {
686 Decision::allow(format!("{}:read", scope_rule(scope)))
687 } else {
688 Decision::deny(
689 "git:read",
690 format!(
691 "A {} run cannot read {}/{}.",
692 run.kind.as_str(),
693 repo.namespace,
694 repo.name
695 ),
696 )
697 }
698}
699
700/// Whether a push to `repo` is limited to certain branches, so that the
701/// refs it moves have to be read and checked with [`decide_refs`].
702pub fn limits_branches(scope: &AgentScope, repo: &RepoPath) -> bool {
703 scope
704 .run
705 .iter()
706 .flat_map(|run| run.push.iter())
707 .any(|grant| same_repo(&grant.repo, repo) && grant.branch.is_some())
708}
709
710/// Whether a push to `repo` may move `refs` (full refs, such as
711/// `refs/heads/main`). Tags are never a run's to move.
712pub fn decide_refs(scope: &AgentScope, repo: &RepoPath, refs: &[String]) -> Decision {
713 let repo_decision = decide_git(scope, repo, true);
714 if !repo_decision.allowed {
715 return repo_decision;
716 }
717 let grants: Vec<&GitGrant> = scope
718 .run
719 .iter()
720 .flat_map(|run| run.push.iter())
721 .filter(|grant| same_repo(&grant.repo, repo))
722 .collect();
723 for git_ref in refs {
724 let Some(branch) = git_ref.strip_prefix("refs/heads/") else {
725 return Decision::deny("git:ref", format!("A run cannot push {git_ref}."));
726 };
727 let allowed = grants
728 .iter()
729 .any(|grant| grant.branch.as_deref().is_none_or(|only| only == branch));
730 if !allowed {
731 return Decision::deny(
732 "git:ref",
733 format!(
734 "A run cannot push to {branch} in {}/{}.",
735 repo.namespace, repo.name
736 ),
737 );
738 }
739 }
740 repo_decision
741}
742
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look743/// The most an agent may be on a repository, whoever it works for: it
744/// can push, merge and run, never change settings or who has access.
745pub const AGENT_CEILING: RepoRole = RepoRole::Write;
746
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API747/// The memberships an agent working for `person` has: the run's
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look748/// workspace, as a member, only if the person is in it now, with the
749/// person's role on its repositories (an owner's Admin included) cut down
750/// to [`AGENT_CEILING`].
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API751pub fn intersect(person: &[Membership], namespace: &str) -> Vec<Membership> {
752 let namespace = namespace.to_lowercase();
753 person
754 .iter()
755 .filter(|membership| membership.slug == namespace)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look756 .map(|membership| {
757 let base = match membership.role {
758 Role::Owner => BasePermission::Admin,
759 Role::Member => membership.base_permission.unwrap_or_default(),
760 };
761 Membership {
762 role: Role::Member,
763 base_permission: Some(match base {
764 BasePermission::Admin => BasePermission::Write,
765 base => base,
766 }),
767 ..membership.clone()
768 }
769 })
770 .collect()
771}
772
773/// The repository grants an agent working for `person` has: those in the
774/// run's workspace, each cut down to [`AGENT_CEILING`].
775pub fn intersect_grants(person: &[RepoGrant], namespace: &str) -> Vec<RepoGrant> {
776 let namespace = namespace.to_lowercase();
777 person
778 .iter()
779 .filter(|grant| grant.workspace == namespace)
780 .map(|grant| RepoGrant {
781 role: grant.role.min(AGENT_CEILING),
782 ..grant.clone()
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API783 })
784 .collect()
785}
786
787/// Who a runner's credential acts as downstream: the person, with only the
788/// agent's (already intersected) memberships. `None` for anything else.
789pub fn as_person(user: &User) -> Option<User> {
790 let acting = user.acting.as_ref()?;
791 if user.kind != PrincipalKind::Agent {
792 return None;
793 }
794 let run = acting.run()?;
795 if run.usage != CredentialUse::Runner {
796 return None;
797 }
798 Some(User {
799 id: acting.on_behalf_of.id.clone(),
800 username: acting.on_behalf_of.username.clone(),
801 kind: PrincipalKind::User,
802 verified: user.verified,
803 workspaces: user.workspaces.clone(),
804 avatar: None,
805 acting: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look806 grants: user.grants.clone(),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step807 token: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API808 })
809}
810
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent811/// How an actor is described: "g1t on behalf of syntaqx".
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API812pub fn describe(user: &User) -> String {
813 match &user.acting {
814 Some(acting) => format!(
815 "{} on behalf of {}",
816 acting.agent, acting.on_behalf_of.username
817 ),
818 None => user.username.clone(),
819 }
820}
821
822#[cfg(test)]
823mod tests {
824 use super::*;
825
826 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step827 fn a_run_s_scopes_are_never_admin() {
828 for kind in RunCredentialKind::ALL {
829 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
830 let scopes = run_scopes(kind, usage);
831 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{kind:?} {usage:?}: {scopes:?}");
832 }
833 }
834 let review = run_scopes(RunCredentialKind::Review, CredentialUse::Tools);
835 assert!(review.contains(&crate::scopes::Scope::PullRequestsWrite));
836 assert!(!review.contains(&crate::scopes::Scope::CodeWrite));
837 }
838
839 #[test]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API840 fn agents_can_search_the_context_hub() {
841 for kind in [RunCredentialKind::Implement, RunCredentialKind::Review, RunCredentialKind::Plan] {
842 let tools = operations_for(kind, CredentialUse::Tools);
843 assert!(tools.contains(&"search_context") && tools.contains(&"get_entity"));
844 }
845 assert!(is_read("search_context") && is_read("get_entity"));
846 }
847
Search across all of g1t, Explore, and a command palette848 #[test]
849 fn agents_can_search_all_of_g1t() {
850 // Site-wide search only reads: every run that reads its repository
851 // may use it, and nothing that never reads gets it.
852 assert!(is_read("search"));
853 assert!(!NEVER.contains(&"search"));
854 for kind in [
855 RunCredentialKind::Implement,
856 RunCredentialKind::Revise,
857 RunCredentialKind::Answer,
858 RunCredentialKind::Review,
859 RunCredentialKind::Plan,
860 RunCredentialKind::Update,
861 ] {
862 let tools = operations_for(kind, CredentialUse::Tools);
863 assert!(tools.contains(&"search"), "{kind:?} should search");
864 // The context hub's search stays its own tool beside it.
865 assert!(tools.contains(&"search_context"), "{kind:?} keeps search_context");
866 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily867 for kind in [RunCredentialKind::Checks, RunCredentialKind::Queue, RunCredentialKind::Mergecheck, RunCredentialKind::Deploy, RunCredentialKind::Bump] {
Search across all of g1t, Explore, and a command palette868 assert!(!operations_for(kind, CredentialUse::Tools).contains(&"search"));
869 }
870 assert!(!operations_for(RunCredentialKind::Implement, CredentialUse::Runner).contains(&"search"));
871 }
872
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API873 fn path(namespace: &str, name: &str) -> RepoPath {
874 RepoPath {
875 namespace: namespace.to_owned(),
876 name: name.to_owned(),
877 }
878 }
879
880 fn scope(kind: RunCredentialKind, usage: CredentialUse) -> AgentScope {
881 AgentScope {
882 repo: path("acme", "rocket"),
883 operations: operations_for(kind, usage)
884 .into_iter()
885 .map(str::to_owned)
886 .collect(),
887 run: Some(RunBinding {
888 kind,
889 usage,
890 run_id: Some("run_1".to_owned()),
891 number: Some(7),
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent892 agent: "g1t".to_owned(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily893 system: false,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API894 read: vec![path("acme", "rocket")],
895 push: match kind {
896 RunCredentialKind::Implement
897 | RunCredentialKind::Revise
898 | RunCredentialKind::Answer => vec![GitGrant {
899 repo: path("pulls", "pul_7"),
900 branch: None,
901 }],
902 RunCredentialKind::Update => vec![GitGrant {
903 repo: path("acme", "rocket"),
904 branch: Some("fix-login".to_owned()),
905 }],
906 _ => vec![],
907 },
908 }),
909 }
910 }
911
912 fn agent(member_of: &[&str], scope: AgentScope) -> User {
913 User {
914 id: "usr_g1t_agent".to_owned(),
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent915 username: "g1t".to_owned(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API916 kind: PrincipalKind::Agent,
917 verified: true,
918 workspaces: member_of
919 .iter()
920 .map(|slug| Membership::member(*slug))
921 .collect(),
922 avatar: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look923 grants: Vec::new(),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step924 token: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API925 acting: Some(Box::new(Acting {
926 credential_id: "tok_1".to_owned(),
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent927 agent: "g1t".to_owned(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API928 on_behalf_of: Principal {
929 id: "usr_1".to_owned(),
930 username: "syntaqx".to_owned(),
931 },
932 scope,
933 })),
934 }
935 }
936
937 fn op(kind: RunCredentialKind, usage: CredentialUse, operation: &str) -> Decision {
938 let scope = scope(kind, usage);
939 let user = agent(&["acme"], scope.clone());
940 decide_operation(
941 &user,
942 &scope,
943 operation,
944 Some(&path("acme", "rocket")),
945 true,
946 Some(7),
947 )
948 }
949
950 use CredentialUse::{Runner, Tools};
951 use RunCredentialKind as K;
952
953 /// Which operations each kind of run may use through its tools: the
954 /// allowed and denied matrix.
955 #[test]
956 fn tools_matrix() {
957 let cases: [(&str, [bool; 6]); 12] = [
958 // implement revise answer review plan checks
959 ("get_issue", [true, true, true, true, true, false]),
960 ("create_issue", [true, true, true, false, true, false]),
961 ("add_comment", [true, true, true, true, false, false]),
962 (
963 "review_pull_request",
964 [false, false, false, true, false, false],
965 ),
966 ("remember", [true, true, true, false, false, false]),
967 ("take_messages", [true, true, true, false, false, false]),
968 ("record_session", [false, false, false, false, false, false]),
969 (
970 "merge_pull_request",
971 [false, false, false, false, false, false],
972 ),
973 (
974 "update_repo_settings",
975 [false, false, false, false, false, false],
976 ),
977 ("create_webhook", [false, false, false, false, false, false]),
978 (
979 "set_actions_secret",
980 [false, false, false, false, false, false],
981 ),
982 ("assign_issue", [false, false, false, false, false, false]),
983 ];
984 let kinds = [
985 K::Implement,
986 K::Revise,
987 K::Answer,
988 K::Review,
989 K::Plan,
990 K::Checks,
991 ];
992 for (operation, expected) in cases {
993 for (kind, allowed) in kinds.into_iter().zip(expected) {
994 assert_eq!(
995 op(kind, Tools, operation).allowed,
996 allowed,
997 "{operation} by a {} run's tools",
998 kind.as_str()
999 );
1000 }
1001 }
1002 }
1003
1004 #[test]
1005 fn runner_matrix() {
1006 assert!(op(K::Implement, Runner, "record_session").allowed);
1007 assert!(op(K::Implement, Runner, "mark_pull_request_ready").allowed);
1008 assert!(op(K::Revise, Runner, "record_session").allowed);
1009 assert!(!op(K::Revise, Runner, "mark_pull_request_ready").allowed);
1010 assert!(!op(K::Implement, Runner, "create_issue").allowed);
1011 assert!(!op(K::Review, Runner, "record_session").allowed);
1012 assert!(!op(K::Checks, Runner, "get_issue").allowed);
1013 }
1014
1015 #[test]
1016 fn settings_billing_tokens_and_members_are_never_reachable() {
1017 for kind in RunCredentialKind::ALL {
1018 for usage in [Runner, Tools] {
1019 for operation in NEVER.iter().copied() {
1020 let decision = op(kind, usage, operation);
1021 assert!(!decision.allowed);
1022 assert_eq!(decision.rule, "never");
1023 }
1024 }
1025 }
1026 // Even a scope that lists one is refused.
1027 let mut wide = scope(K::Implement, Tools);
1028 wide.operations.push("merge_pull_request".to_owned());
1029 let user = agent(&["acme"], wide.clone());
1030 let decision = decide_operation(
1031 &user,
1032 &wide,
1033 "merge_pull_request",
1034 Some(&path("acme", "rocket")),
1035 true,
1036 Some(7),
1037 );
1038 assert_eq!(decision.rule, "never");
1039 }
1040
1041 #[test]
1042 fn another_repository_is_refused() {
1043 let scope = scope(K::Implement, Tools);
1044 let user = agent(&["acme"], scope.clone());
1045 let decision = decide_operation(
1046 &user,
1047 &scope,
1048 "create_issue",
1049 Some(&path("acme", "other")),
1050 true,
1051 None,
1052 );
1053 assert!(!decision.allowed);
1054 assert_eq!(decision.rule, "scope:repository");
1055 let decision = decide_operation(&user, &scope, "create_issue", None, true, None);
1056 assert_eq!(decision.rule, "scope:repository");
1057 // The repository's name is matched without regard to case.
1058 let decision = decide_operation(
1059 &user,
1060 &scope,
1061 "create_issue",
1062 Some(&path("Acme", "Rocket")),
1063 true,
1064 None,
1065 );
1066 assert!(decision.allowed);
1067 assert_eq!(decision.rule, "run:implement/tools");
1068 }
1069
1070 #[test]
1071 fn the_permission_is_the_intersection_with_the_person() {
1072 let scope = scope(K::Implement, Tools);
1073 // The person left the workspace: their agent can do nothing there.
1074 let user = agent(&[], scope.clone());
1075 let decision = decide_operation(
1076 &user,
1077 &scope,
1078 "get_issue",
1079 Some(&path("acme", "rocket")),
1080 true,
1081 Some(1),
1082 );
1083 assert!(!decision.allowed);
1084 assert_eq!(decision.rule, "on-behalf-of:membership");
1085 // And an owner's agent is only ever a member.
1086 let owner = vec![
1087 Membership {
1088 slug: "acme".to_owned(),
1089 role: Role::Owner,
1090 name: None,
1091 avatar: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1092 base_permission: Some(BasePermission::None),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1093 team_creation: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1094 },
1095 Membership::member("elsewhere"),
1096 ];
1097 let memberships = intersect(&owner, "Acme");
1098 assert_eq!(memberships.len(), 1);
1099 assert_eq!(memberships[0].slug, "acme");
1100 assert_eq!(memberships[0].role, Role::Member);
1101 assert!(intersect(&owner, "nowhere").is_empty());
1102 }
1103
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1104 /// An agent gets at most the person's role on the repository, and
1105 /// never more than Write; nothing outside the run's workspace.
1106 #[test]
1107 fn an_agent_has_at_most_its_persons_role() {
1108 use crate::access::{Capability, RepoRef, can, permission};
1109 let rocket = RepoRef { id: "rep_1", namespace: "acme", private: true };
1110 let other = RepoRef { id: "rep_2", namespace: "acme", private: true };
1111 let elsewhere = RepoRef { id: "rep_3", namespace: "globex", private: true };
1112 let tools = scope(K::Implement, Tools);
1113 let scope = scope(K::Implement, Runner);
1114 // An owner's agent: Write, never Admin.
1115 let owner = [Membership { role: Role::Owner, ..Membership::member("acme") }, Membership::member("globex")];
1116 let mut agent_user = agent(&[], scope.clone());
1117 agent_user.workspaces = intersect(&owner, "acme");
1118 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1119 assert!(!can(Some(&agent_user), rocket, Capability::ManageSettings));
1120 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1121 // A member whose workspace gives Read: Read, so it cannot push.
1122 let reader = [Membership { base_permission: Some(BasePermission::Read), ..Membership::member("acme") }];
1123 agent_user.workspaces = intersect(&reader, "acme");
1124 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Read));
1125 assert!(!can(Some(&agent_user), rocket, Capability::Push));
1126 // An outside collaborator with Maintain on one repository: Write
1127 // there, nothing elsewhere, and the run is allowed.
1128 let grants = [
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1129 RepoGrant { repo_id: "rep_1".into(), workspace: "acme".into(), role: RepoRole::Maintain, team: None },
1130 RepoGrant { repo_id: "rep_3".into(), workspace: "globex".into(), role: RepoRole::Admin, team: None },
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1131 ];
1132 agent_user.workspaces = intersect(&[], "acme");
1133 agent_user.grants = intersect_grants(&grants, "Acme");
1134 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1135 assert_eq!(permission(Some(&agent_user), other), None);
1136 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1137 let decision = decide_operation(&agent_user, &tools, "get_issue", Some(&path("acme", "rocket")), true, Some(1));
1138 assert!(decision.allowed, "{}", decision.reason.unwrap_or_default());
1139 // The person, downstream of a runner's credential, carries the same.
1140 let person = as_person(&agent_user).expect("a runner acts as the person");
1141 assert_eq!(permission(Some(&person), rocket), Some(RepoRole::Write));
1142 }
1143
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1144 #[test]
1145 fn a_runner_changes_only_its_own_pull_request() {
1146 let scope = scope(K::Implement, Runner);
1147 let user = agent(&["acme"], scope.clone());
1148 let repo = path("acme", "rocket");
1149 let other = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(8));
1150 assert!(!other.allowed);
1151 assert_eq!(other.rule, "scope:pull");
1152 let own = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(7));
1153 assert!(own.allowed);
1154 // Reading another is fine.
1155 assert!(
1156 decide_operation(
1157 &user,
1158 &scope,
1159 "get_pull_request",
1160 Some(&repo),
1161 true,
1162 Some(8)
1163 )
1164 .allowed
1165 );
1166 }
1167
1168 #[test]
1169 fn git_matrix() {
1170 let fork = path("pulls", "pul_7");
1171 let upstream = path("acme", "rocket");
1172 let elsewhere = path("acme", "billing");
1173 let implement = scope(K::Implement, Runner);
1174 assert!(decide_git(&implement, &fork, true).allowed);
1175 assert!(decide_git(&implement, &fork, false).allowed);
1176 assert!(decide_git(&implement, &upstream, false).allowed);
1177 assert_eq!(decide_git(&implement, &upstream, true).rule, "git:push");
1178 assert_eq!(decide_git(&implement, &elsewhere, false).rule, "git:read");
1179 let review = scope(K::Review, Runner);
1180 assert!(decide_git(&review, &upstream, false).allowed);
1181 assert!(!decide_git(&review, &upstream, true).allowed);
1182 assert!(!decide_git(&review, &fork, true).allowed);
1183 // A tools token made before run credentials never reaches git.
1184 let old = AgentScope {
1185 repo: upstream.clone(),
1186 operations: vec!["get_issue".to_owned()],
1187 run: None,
1188 };
1189 assert_eq!(decide_git(&old, &upstream, false).rule, "git:not-a-run");
1190 // Nor does an agent's tools token.
1191 assert_eq!(
1192 decide_git(&scope(K::Implement, Tools), &upstream, false).rule,
1193 "git:not-a-run"
1194 );
1195 }
1196
1197 #[test]
1198 fn a_push_moves_only_granted_branches() {
1199 let update = scope(K::Update, Runner);
1200 let repo = path("acme", "rocket");
1201 let refs = |names: &[&str]| {
1202 names
1203 .iter()
1204 .map(|name| (*name).to_owned())
1205 .collect::<Vec<_>>()
1206 };
1207 assert!(decide_refs(&update, &repo, &refs(&["refs/heads/fix-login"])).allowed);
1208 assert_eq!(
1209 decide_refs(&update, &repo, &refs(&["refs/heads/main"])).rule,
1210 "git:ref"
1211 );
1212 assert_eq!(
1213 decide_refs(
1214 &update,
1215 &repo,
1216 &refs(&["refs/heads/fix-login", "refs/tags/v1"])
1217 )
1218 .rule,
1219 "git:ref"
1220 );
1221 let implement = scope(K::Implement, Runner);
1222 assert!(
1223 decide_refs(
1224 &implement,
1225 &path("pulls", "pul_7"),
1226 &refs(&["refs/heads/main"])
1227 )
1228 .allowed
1229 );
1230 }
1231
1232 #[test]
1233 fn a_runner_acts_downstream_as_the_person() {
1234 let user = agent(&["acme"], scope(K::Implement, Runner));
1235 let person = as_person(&user).unwrap();
1236 assert_eq!(person.id, "usr_1");
1237 assert_eq!(person.username, "syntaqx");
1238 assert_eq!(person.kind, PrincipalKind::User);
1239 assert!(person.is_member("acme"));
1240 assert!(person.acting.is_none());
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1241 assert_eq!(describe(&user), "g1t on behalf of syntaqx");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1242 // The tools act as the agent.
1243 assert!(as_person(&agent(&["acme"], scope(K::Implement, Tools))).is_none());
1244 }
1245
1246 #[test]
1247 fn scopes_without_a_run_still_parse() {
1248 let old: AgentScope = serde_json::from_str(
1249 r#"{"repo":{"namespace":"acme","name":"rocket"},"operations":["get_issue"]}"#,
1250 )
1251 .unwrap();
1252 assert!(old.run.is_none());
1253 let written = serde_json::to_string(&scope(K::Review, Tools)).unwrap();
1254 assert!(written.contains(r#""use":"tools""#));
1255 assert!(written.contains(r#""kind":"review""#));
1256 let back: AgentScope = serde_json::from_str(&written).unwrap();
1257 assert_eq!(back.run.unwrap().kind, K::Review);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1258 // Only g1t's own runs say so; every other reads as not.
1259 assert!(!written.contains("system"));
1260 assert!(!back_run(&written).system);
1261 let mut own = scope(K::Bump, Runner);
1262 own.run.as_mut().unwrap().system = true;
1263 let written = serde_json::to_string(&own).unwrap();
1264 assert!(written.contains(r#""system":true"#));
1265 assert!(back_run(&written).system);
1266 }
1267
1268 fn back_run(written: &str) -> RunBinding {
1269 serde_json::from_str::<AgentScope>(written).unwrap().run.unwrap()
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1270 }
1271}

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