Skip to content
1,342 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.

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 branch 'worktree-agent-a3abfcce648e87dca'276 "get_pending_deployments",
277 "get_workflow_permissions",
278 "get_fork_pr_approval",
Merge checks: statuses and check runs on every commit279 "list_commit_statuses",
280 "get_combined_status",
281 "list_check_runs_for_ref",
282 "get_check_run",
283 "list_check_run_annotations",
284 "list_check_suites_for_ref",
285 "get_check_suite",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API286 "list_integrations",
287 "get_model_routes",
288 "list_webhooks",
289 "list_webhook_deliveries",
290 "list_actions_secrets",
291 "list_actions_variables",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily292 "list_security_alerts",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar293 "list_secret_scanning_alerts",
294 "get_secret_scanning_alert",
295 "list_secret_scanning_locations",
296 "list_bypass_requests",
297 "list_custom_patterns",
298 "list_code_scanning_alerts",
299 "get_code_scanning_alert",
300 "list_code_scanning_analyses",
301 "get_sarif_upload",
302 "list_vulnerability_alerts",
303 "get_vulnerability_alert",
304 "get_dependency_graph",
305 "get_sbom",
306 "compare_dependencies",
307 "get_security_settings",
308 "get_workspace_security_settings",
309 "get_security_overview",
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.310 "list_security_advisories",
311 "get_security_advisory",
312 "list_vulnerability_reports",
API: notifications over REST and MCP, with notifications scopes313 "list_notifications",
314 "get_notification_thread",
315 "get_thread_subscription",
316 "get_repo_subscription",
317 "list_watched_repos",
API: pinned projects over REST and MCP318 "list_pinned_projects",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API319];
320
321/// What no agent's token may ever do, whatever its scope says: workspaces,
322/// repositories' settings, members, tokens, billing, integrations,
323/// webhooks, secrets, workflows' controls, merging, and putting more agents
324/// to work.
325pub const NEVER: &[&str] = &[
Usage, Billing settings and prepaid AI credit; fixes from the UX audit326 // Billing is people's: agents never spend or change it.
327 "set_budget",
328 "buy_ai_credit",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API329 "create_workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look330 "delete_workspace",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily331 "update_workspace",
Merge main (membership, two-factor, GitHub repo roles) into tokens332 // So is who belongs to a workspace and who owns it.
333 "list_members",
334 "update_member",
335 "remove_member",
336 "transfer_ownership",
337 "leave_workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look338 "transfer_repo",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API339 "create_repo",
340 "update_repo",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97341 "update_project",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look342 "delete_repo",
343 "list_deleted_repos",
344 "restore_repo",
345 "purge_repo",
346 "rename_repo",
347 "archive_repo",
348 "unarchive_repo",
349 "set_repo_visibility",
350 "rename_branch",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API351 "update_repo_settings",
352 "merge_pull_request",
353 "assign_issue",
354 "plan_work",
355 "apply_plan",
356 "import_issue",
357 "list_integrations",
358 "connect_integration",
AI Gateway: OpenAI's format, open models, and your own providers359 "update_integration",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API360 "disconnect_integration",
361 "test_integration",
362 "get_model_routes",
363 "set_model_routes",
364 "list_webhooks",
365 "create_webhook",
366 "update_webhook",
367 "delete_webhook",
368 "ping_webhook",
369 "list_webhook_deliveries",
370 "redeliver_webhook",
371 "dispatch_workflow",
372 "cancel_workflow_run",
373 "rerun_workflow_run",
374 "update_workflow",
Merge branch 'worktree-agent-a3abfcce648e87dca'375 // Nor lets runs or deployments through, or changes what holds them.
376 "approve_workflow_run",
377 "review_pending_deployments",
378 "update_environment",
379 "delete_environment",
380 "set_workflow_permissions",
381 "set_fork_pr_approval",
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts382 "set_actions_access",
Merge branch 'worktree-agent-a3abfcce648e87dca'383 "create_repository_dispatch",
384 "set_workspace_workflow_permissions",
Merge checks: statuses and check runs on every commit385 // An agent never reports checks on its own work, nor asks for them
386 // to run again: what checks say is the integrations' to say.
387 "create_commit_status",
388 "create_check_run",
389 "update_check_run",
390 "rerequest_check_run",
391 "rerequest_check_suite",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API392 "list_actions_secrets",
393 "set_actions_secret",
394 "delete_actions_secret",
395 "list_actions_variables",
396 "set_actions_variable",
397 "delete_actions_variable",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look398 "list_collaborators",
399 "get_collaborator_permission",
400 "add_collaborator",
401 "update_collaborator",
402 "remove_collaborator",
403 "list_repo_invitations",
404 "revoke_repo_invitation",
405 "list_my_repo_invitations",
406 "accept_repo_invitation",
407 "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)408 // Nor answers a workspace invitation: only the person it is for does.
409 "accept_invitation",
410 "decline_invitation",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look411 "set_base_permission",
412 "list_outside_collaborators",
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca413 // Deploy keys, which let a machine into a repository.
414 "list_deploy_keys",
415 "get_deploy_key",
416 "create_deploy_key",
417 "delete_deploy_key",
Merge branch 'mirroring' into artifacts-mode418 // Moving a repository to g1t for good, and a remote's token and
419 // levers, are a person's to decide.
420 "move_mirror_to_g1t",
421 "add_mirror_remote",
422 "update_mirror_remote",
423 "remove_mirror_remote",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar424 // Teams: who is in which, and what they reach, is for people.
425 "create_team",
426 "update_team",
427 "delete_team",
428 "set_team_member",
429 "remove_team_member",
430 "set_team_repo",
431 "remove_team_repo",
432 "set_team_review_assignment",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily433 // Dismissing a secret lets it through push protection.
434 "dismiss_security_alert",
435 "reopen_security_alert",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar436 // Nor any other decision about security: closing or reopening an
437 // alert, pushing past push protection or deciding who may, changing
438 // what is looked for or when checks fail, or putting more agents to
439 // work. An agent fixes what it finds in its own pull request.
440 "update_secret_scanning_alert",
441 "bypass_push_protection",
442 "review_bypass_request",
443 "create_custom_pattern",
444 "update_custom_pattern",
445 "delete_custom_pattern",
446 "update_code_scanning_alert",
447 "update_vulnerability_alert",
448 "fix_security_alert",
449 "update_security_settings",
450 "update_workspace_security_settings",
API: notifications over REST and MCP, with notifications scopes451 // A person's own inbox: g1t's agents act as g1t, which has none.
452 "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",
Merge main into Artifacts Phase 2484 // Sharing an artifact and deleting one for good are for people: an
485 // agent shares only through the agents service, with the people
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.486 // already in its conversation.
Merge main into Artifacts Phase 2487 "set_workspace_artifact_access",
488 "purge_workspace_artifact",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API489];
490
491/// Reading what an agent needs to know about its repository.
492const TOOLS_READ: &[&str] = &[
493 "get_repo",
494 "list_issues",
495 "get_issue",
496 "list_labels",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar497 "list_issue_labels",
498 "list_milestones",
499 "get_milestone",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API500 "list_pull_requests",
501 "get_pull_request",
502 "get_pull_request_changes",
503 "read_session",
504 "get_merge_queue",
505 "list_events",
506 "recall",
507 "search_context",
508 "get_entity",
Search across all of g1t, Explore, and a command palette509 "search",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API510 "list_workflows",
511 "list_workflow_runs",
512 "get_workflow_run",
513 "get_job_logs",
Merge branch 'worktree-agent-a3abfcce648e87dca'514 "get_pending_deployments",
515 "get_workflow_permissions",
516 "get_fork_pr_approval",
Merge checks: statuses and check runs on every commit517 "list_commit_statuses",
518 "get_combined_status",
519 "list_check_runs_for_ref",
520 "get_check_run",
521 "list_check_run_annotations",
522 "list_check_suites_for_ref",
523 "get_check_suite",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API524];
525
526pub fn is_read(operation: &str) -> bool {
527 READ_OPERATIONS.contains(&operation)
528}
529
530/// The API and MCP operations a run of `kind` may use with a credential
531/// for `usage`. Git is separate: see [`decide_git`].
532pub fn operations_for(kind: RunCredentialKind, usage: CredentialUse) -> Vec<&'static str> {
533 use RunCredentialKind as K;
534 let mut operations: Vec<&'static str> = Vec::new();
535 match usage {
536 CredentialUse::Runner => {
537 if kind.works_on_a_pull() {
538 operations.extend(["get_repo", "get_pull_request", "record_session"]);
539 }
540 if kind == K::Implement {
541 operations.push("mark_pull_request_ready");
542 }
543 }
544 CredentialUse::Tools => match kind {
545 K::Implement | K::Revise | K::Answer => {
546 operations.extend(TOOLS_READ.iter().copied());
547 operations.extend([
548 "create_issue",
549 "add_comment",
550 "take_messages",
551 "remember",
552 "message_agent",
553 "answer_message",
554 "get_context",
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.555 "customize_bundle",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API556 ]);
557 }
558 K::Review => {
559 operations.extend(TOOLS_READ.iter().copied());
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.560 operations.extend(["add_comment", "review_pull_request", "get_context", "customize_bundle"]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API561 }
562 K::Plan => {
563 operations.extend(TOOLS_READ.iter().copied());
Merge branch 'customize' into main: Customize replaces the Marketplace, the Artifacts trash and spaces changes, the Security-free cleanup, with the desktop app's own title bar kept in the shell.564 operations.extend(["create_issue", "get_context", "customize_bundle"]);
565 }
566 K::Update => {
567 operations.extend(TOOLS_READ.iter().copied());
568 operations.push("customize_bundle");
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API569 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily570 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 API571 },
572 }
573 operations
574}
575
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step576/// What a run's credential may do, in the scope vocabulary that access
577/// tokens use (see [`crate::scopes`]): the scopes of its operations, and
578/// for a runner, git's. Its operations, its repository and its run still
579/// bound it more tightly than these scopes say.
580pub fn run_scopes(kind: RunCredentialKind, usage: CredentialUse) -> Vec<crate::scopes::Scope> {
581 use crate::scopes::{Scope, normalize, scope_for};
582 let mut scopes: Vec<Scope> = operations_for(kind, usage)
583 .into_iter()
584 .filter_map(scope_for)
585 .collect();
586 if usage == CredentialUse::Runner {
587 scopes.push(Scope::CodeRead);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily588 if matches!(
589 kind,
590 RunCredentialKind::Implement
591 | RunCredentialKind::Revise
592 | RunCredentialKind::Answer
593 | RunCredentialKind::Update
594 | RunCredentialKind::Bump
595 ) {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step596 scopes.push(Scope::CodeWrite);
597 }
598 }
599 normalize(&mut scopes);
600 scopes
601}
602
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API603/// Operations that change a pull request, which a runner may do only to
604/// the pull request its run works on.
605const PULL_WRITES: &[&str] = &["record_session", "mark_pull_request_ready"];
606
607/// Whether something was allowed, and the rule that decided it.
608#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
609pub struct Decision {
610 pub allowed: bool,
611 /// A short, stable name: `run:implement/tools`, `never`,
612 /// `scope:repository` and so on. Shown in the audit log.
613 pub rule: String,
614 /// Why it was refused, for the caller.
615 #[serde(default, skip_serializing_if = "Option::is_none")]
616 pub reason: Option<String>,
617}
618
619impl Decision {
620 pub fn allow(rule: impl Into<String>) -> Self {
621 Decision {
622 allowed: true,
623 rule: rule.into(),
624 reason: None,
625 }
626 }
627
628 pub fn deny(rule: impl Into<String>, reason: impl Into<String>) -> Self {
629 Decision {
630 allowed: false,
631 rule: rule.into(),
632 reason: Some(reason.into()),
633 }
634 }
635}
636
637fn same_repo(a: &RepoPath, b: &RepoPath) -> bool {
638 a.namespace.eq_ignore_ascii_case(&b.namespace) && a.name.eq_ignore_ascii_case(&b.name)
639}
640
641fn scope_rule(scope: &AgentScope) -> String {
642 match &scope.run {
643 Some(run) => format!("run:{}/{}", run.kind.as_str(), run.usage.as_str()),
644 None => "agent-token".to_owned(),
645 }
646}
647
648/// Whether `user`, resolved from an agent's token with `scope`, may use
649/// `operation`. `repo` is the repository the call names, if any, and
650/// `needs_repo` whether the operation is about one; `number` the issue or
651/// pull request it names.
652pub fn decide_operation(
653 user: &User,
654 scope: &AgentScope,
655 operation: &str,
656 repo: Option<&RepoPath>,
657 needs_repo: bool,
658 number: Option<u32>,
659) -> Decision {
660 let who = "A g1t agent's token";
661 if NEVER.contains(&operation) {
662 return Decision::deny(
663 "never",
664 format!(
665 "{who} can never use {operation}: settings, members, tokens, billing, integrations, webhooks, secrets and merging are for people."
666 ),
667 );
668 }
669 if !scope.operations.iter().any(|name| name == operation) {
670 return Decision::deny(
671 "scope:operation",
672 format!("{who} for this run cannot use {operation}."),
673 );
674 }
675 if needs_repo && !repo.is_some_and(|asked| same_repo(asked, &scope.repo)) {
676 return Decision::deny(
677 "scope:repository",
678 format!(
679 "{who} works in {}/{} only.",
680 scope.repo.namespace, scope.repo.name
681 ),
682 );
683 }
684 // 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 look685 // in the repository's workspace, as a member or with a role on its
686 // repositories. What it may do in the repository itself is their
687 // role there, which services check (`access::can`).
688 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 API689 return Decision::deny(
690 "on-behalf-of:membership",
691 format!(
692 "The person this agent works for is no longer a member of {}.",
693 scope.repo.namespace
694 ),
695 );
696 }
697 if let Some(run) = &scope.run
698 && run.usage == CredentialUse::Runner
699 && PULL_WRITES.contains(&operation)
700 && run.number.is_some()
701 && number != run.number
702 {
703 return Decision::deny(
704 "scope:pull",
705 format!(
706 "{who} can change pull request #{} only.",
707 run.number.unwrap_or_default()
708 ),
709 );
710 }
711 Decision::allow(scope_rule(scope))
712}
713
714/// Whether a run credential may clone or fetch (`write` false), or push to
715/// (`write` true), the repository at `repo`.
716pub fn decide_git(scope: &AgentScope, repo: &RepoPath, write: bool) -> Decision {
717 let Some(run) = scope
718 .run
719 .as_ref()
720 .filter(|run| run.usage == CredentialUse::Runner)
721 else {
722 return Decision::deny(
723 "git:not-a-run",
724 "A g1t agent's tools token cannot be used with git.",
725 );
726 };
727 let pushable = run.push.iter().any(|grant| same_repo(&grant.repo, repo));
728 if write {
729 return if pushable {
730 Decision::allow(format!("{}:push", scope_rule(scope)))
731 } else {
732 Decision::deny(
733 "git:push",
734 format!(
735 "A {} run cannot push to {}/{}.",
736 run.kind.as_str(),
737 repo.namespace,
738 repo.name
739 ),
740 )
741 };
742 }
743 let readable = pushable
744 || same_repo(&scope.repo, repo)
745 || run.read.iter().any(|path| same_repo(path, repo));
746 if readable {
747 Decision::allow(format!("{}:read", scope_rule(scope)))
748 } else {
749 Decision::deny(
750 "git:read",
751 format!(
752 "A {} run cannot read {}/{}.",
753 run.kind.as_str(),
754 repo.namespace,
755 repo.name
756 ),
757 )
758 }
759}
760
761/// Whether a push to `repo` is limited to certain branches, so that the
762/// refs it moves have to be read and checked with [`decide_refs`].
763pub fn limits_branches(scope: &AgentScope, repo: &RepoPath) -> bool {
764 scope
765 .run
766 .iter()
767 .flat_map(|run| run.push.iter())
768 .any(|grant| same_repo(&grant.repo, repo) && grant.branch.is_some())
769}
770
771/// Whether a push to `repo` may move `refs` (full refs, such as
772/// `refs/heads/main`). Tags are never a run's to move.
773pub fn decide_refs(scope: &AgentScope, repo: &RepoPath, refs: &[String]) -> Decision {
774 let repo_decision = decide_git(scope, repo, true);
775 if !repo_decision.allowed {
776 return repo_decision;
777 }
778 let grants: Vec<&GitGrant> = scope
779 .run
780 .iter()
781 .flat_map(|run| run.push.iter())
782 .filter(|grant| same_repo(&grant.repo, repo))
783 .collect();
784 for git_ref in refs {
785 let Some(branch) = git_ref.strip_prefix("refs/heads/") else {
786 return Decision::deny("git:ref", format!("A run cannot push {git_ref}."));
787 };
788 let allowed = grants
789 .iter()
790 .any(|grant| grant.branch.as_deref().is_none_or(|only| only == branch));
791 if !allowed {
792 return Decision::deny(
793 "git:ref",
794 format!(
795 "A run cannot push to {branch} in {}/{}.",
796 repo.namespace, repo.name
797 ),
798 );
799 }
800 }
801 repo_decision
802}
803
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look804/// The most an agent may be on a repository, whoever it works for: it
805/// can push, merge and run, never change settings or who has access.
806pub const AGENT_CEILING: RepoRole = RepoRole::Write;
807
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API808/// 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 look809/// workspace, as a member, only if the person is in it now, with the
810/// person's role on its repositories (an owner's Admin included) cut down
811/// to [`AGENT_CEILING`].
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API812pub fn intersect(person: &[Membership], namespace: &str) -> Vec<Membership> {
813 let namespace = namespace.to_lowercase();
814 person
815 .iter()
816 .filter(|membership| membership.slug == namespace)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look817 .map(|membership| {
818 let base = match membership.role {
819 Role::Owner => BasePermission::Admin,
820 Role::Member => membership.base_permission.unwrap_or_default(),
821 };
822 Membership {
823 role: Role::Member,
824 base_permission: Some(match base {
825 BasePermission::Admin => BasePermission::Write,
826 base => base,
827 }),
Merge main (membership, two-factor, GitHub repo roles) into tokens828 // Billing and security manager are the person's, never
829 // their agent's.
830 org_roles: Vec::new(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look831 ..membership.clone()
832 }
833 })
834 .collect()
835}
836
837/// The repository grants an agent working for `person` has: those in the
838/// run's workspace, each cut down to [`AGENT_CEILING`].
839pub fn intersect_grants(person: &[RepoGrant], namespace: &str) -> Vec<RepoGrant> {
840 let namespace = namespace.to_lowercase();
841 person
842 .iter()
843 .filter(|grant| grant.workspace == namespace)
844 .map(|grant| RepoGrant {
845 role: grant.role.min(AGENT_CEILING),
846 ..grant.clone()
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API847 })
848 .collect()
849}
850
851/// Who a runner's credential acts as downstream: the person, with only the
852/// agent's (already intersected) memberships. `None` for anything else.
853pub fn as_person(user: &User) -> Option<User> {
854 let acting = user.acting.as_ref()?;
855 if user.kind != PrincipalKind::Agent {
856 return None;
857 }
858 let run = acting.run()?;
859 if run.usage != CredentialUse::Runner {
860 return None;
861 }
862 Some(User {
863 id: acting.on_behalf_of.id.clone(),
864 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 managers865 display_username: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API866 kind: PrincipalKind::User,
867 verified: user.verified,
868 workspaces: user.workspaces.clone(),
869 avatar: None,
870 acting: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871 grants: user.grants.clone(),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step872 token: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens873 held: Vec::new(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API874 })
875}
876
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent877/// 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 API878pub fn describe(user: &User) -> String {
879 match &user.acting {
880 Some(acting) => format!(
881 "{} on behalf of {}",
882 acting.agent, acting.on_behalf_of.username
883 ),
884 None => user.username.clone(),
885 }
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step893 fn a_run_s_scopes_are_never_admin() {
894 for kind in RunCredentialKind::ALL {
895 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
896 let scopes = run_scopes(kind, usage);
897 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{kind:?} {usage:?}: {scopes:?}");
898 }
899 }
900 let review = run_scopes(RunCredentialKind::Review, CredentialUse::Tools);
901 assert!(review.contains(&crate::scopes::Scope::PullRequestsWrite));
902 assert!(!review.contains(&crate::scopes::Scope::CodeWrite));
903 }
904
905 #[test]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API906 fn agents_can_search_the_context_hub() {
907 for kind in [RunCredentialKind::Implement, RunCredentialKind::Review, RunCredentialKind::Plan] {
908 let tools = operations_for(kind, CredentialUse::Tools);
909 assert!(tools.contains(&"search_context") && tools.contains(&"get_entity"));
910 }
911 assert!(is_read("search_context") && is_read("get_entity"));
912 }
913
Search across all of g1t, Explore, and a command palette914 #[test]
915 fn agents_can_search_all_of_g1t() {
916 // Site-wide search only reads: every run that reads its repository
917 // may use it, and nothing that never reads gets it.
918 assert!(is_read("search"));
919 assert!(!NEVER.contains(&"search"));
920 for kind in [
921 RunCredentialKind::Implement,
922 RunCredentialKind::Revise,
923 RunCredentialKind::Answer,
924 RunCredentialKind::Review,
925 RunCredentialKind::Plan,
926 RunCredentialKind::Update,
927 ] {
928 let tools = operations_for(kind, CredentialUse::Tools);
929 assert!(tools.contains(&"search"), "{kind:?} should search");
930 // The context hub's search stays its own tool beside it.
931 assert!(tools.contains(&"search_context"), "{kind:?} keeps search_context");
932 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily933 for kind in [RunCredentialKind::Checks, RunCredentialKind::Queue, RunCredentialKind::Mergecheck, RunCredentialKind::Deploy, RunCredentialKind::Bump] {
Search across all of g1t, Explore, and a command palette934 assert!(!operations_for(kind, CredentialUse::Tools).contains(&"search"));
935 }
936 assert!(!operations_for(RunCredentialKind::Implement, CredentialUse::Runner).contains(&"search"));
937 }
938
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API939 fn path(namespace: &str, name: &str) -> RepoPath {
940 RepoPath {
941 namespace: namespace.to_owned(),
942 name: name.to_owned(),
943 }
944 }
945
946 fn scope(kind: RunCredentialKind, usage: CredentialUse) -> AgentScope {
947 AgentScope {
948 repo: path("acme", "rocket"),
949 operations: operations_for(kind, usage)
950 .into_iter()
951 .map(str::to_owned)
952 .collect(),
953 run: Some(RunBinding {
954 kind,
955 usage,
956 run_id: Some("run_1".to_owned()),
957 number: Some(7),
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent958 agent: "g1t".to_owned(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily959 system: false,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API960 read: vec![path("acme", "rocket")],
961 push: match kind {
962 RunCredentialKind::Implement
963 | RunCredentialKind::Revise
964 | RunCredentialKind::Answer => vec![GitGrant {
965 repo: path("pulls", "pul_7"),
966 branch: None,
967 }],
968 RunCredentialKind::Update => vec![GitGrant {
969 repo: path("acme", "rocket"),
970 branch: Some("fix-login".to_owned()),
971 }],
972 _ => vec![],
973 },
974 }),
975 }
976 }
977
978 fn agent(member_of: &[&str], scope: AgentScope) -> User {
979 User {
980 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-agent981 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 managers982 display_username: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API983 kind: PrincipalKind::Agent,
984 verified: true,
985 workspaces: member_of
986 .iter()
987 .map(|slug| Membership::member(*slug))
988 .collect(),
989 avatar: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look990 grants: Vec::new(),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step991 token: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens992 held: Vec::new(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API993 acting: Some(Box::new(Acting {
994 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-agent995 agent: "g1t".to_owned(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API996 on_behalf_of: Principal {
997 id: "usr_1".to_owned(),
998 username: "syntaqx".to_owned(),
999 },
1000 scope,
1001 })),
1002 }
1003 }
1004
1005 fn op(kind: RunCredentialKind, usage: CredentialUse, operation: &str) -> Decision {
1006 let scope = scope(kind, usage);
1007 let user = agent(&["acme"], scope.clone());
1008 decide_operation(
1009 &user,
1010 &scope,
1011 operation,
1012 Some(&path("acme", "rocket")),
1013 true,
1014 Some(7),
1015 )
1016 }
1017
1018 use CredentialUse::{Runner, Tools};
1019 use RunCredentialKind as K;
1020
1021 /// Which operations each kind of run may use through its tools: the
1022 /// allowed and denied matrix.
1023 #[test]
1024 fn tools_matrix() {
1025 let cases: [(&str, [bool; 6]); 12] = [
1026 // implement revise answer review plan checks
1027 ("get_issue", [true, true, true, true, true, false]),
1028 ("create_issue", [true, true, true, false, true, false]),
1029 ("add_comment", [true, true, true, true, false, false]),
1030 (
1031 "review_pull_request",
1032 [false, false, false, true, false, false],
1033 ),
1034 ("remember", [true, true, true, false, false, false]),
1035 ("take_messages", [true, true, true, false, false, false]),
1036 ("record_session", [false, false, false, false, false, false]),
1037 (
1038 "merge_pull_request",
1039 [false, false, false, false, false, false],
1040 ),
1041 (
1042 "update_repo_settings",
1043 [false, false, false, false, false, false],
1044 ),
1045 ("create_webhook", [false, false, false, false, false, false]),
1046 (
1047 "set_actions_secret",
1048 [false, false, false, false, false, false],
1049 ),
1050 ("assign_issue", [false, false, false, false, false, false]),
1051 ];
1052 let kinds = [
1053 K::Implement,
1054 K::Revise,
1055 K::Answer,
1056 K::Review,
1057 K::Plan,
1058 K::Checks,
1059 ];
1060 for (operation, expected) in cases {
1061 for (kind, allowed) in kinds.into_iter().zip(expected) {
1062 assert_eq!(
1063 op(kind, Tools, operation).allowed,
1064 allowed,
1065 "{operation} by a {} run's tools",
1066 kind.as_str()
1067 );
1068 }
1069 }
1070 }
1071
1072 #[test]
1073 fn runner_matrix() {
1074 assert!(op(K::Implement, Runner, "record_session").allowed);
1075 assert!(op(K::Implement, Runner, "mark_pull_request_ready").allowed);
1076 assert!(op(K::Revise, Runner, "record_session").allowed);
1077 assert!(!op(K::Revise, Runner, "mark_pull_request_ready").allowed);
1078 assert!(!op(K::Implement, Runner, "create_issue").allowed);
1079 assert!(!op(K::Review, Runner, "record_session").allowed);
1080 assert!(!op(K::Checks, Runner, "get_issue").allowed);
1081 }
1082
1083 #[test]
1084 fn settings_billing_tokens_and_members_are_never_reachable() {
1085 for kind in RunCredentialKind::ALL {
1086 for usage in [Runner, Tools] {
1087 for operation in NEVER.iter().copied() {
1088 let decision = op(kind, usage, operation);
1089 assert!(!decision.allowed);
1090 assert_eq!(decision.rule, "never");
1091 }
1092 }
1093 }
1094 // Even a scope that lists one is refused.
1095 let mut wide = scope(K::Implement, Tools);
1096 wide.operations.push("merge_pull_request".to_owned());
1097 let user = agent(&["acme"], wide.clone());
1098 let decision = decide_operation(
1099 &user,
1100 &wide,
1101 "merge_pull_request",
1102 Some(&path("acme", "rocket")),
1103 true,
1104 Some(7),
1105 );
1106 assert_eq!(decision.rule, "never");
1107 }
1108
1109 #[test]
1110 fn another_repository_is_refused() {
1111 let scope = scope(K::Implement, Tools);
1112 let user = agent(&["acme"], scope.clone());
1113 let decision = decide_operation(
1114 &user,
1115 &scope,
1116 "create_issue",
1117 Some(&path("acme", "other")),
1118 true,
1119 None,
1120 );
1121 assert!(!decision.allowed);
1122 assert_eq!(decision.rule, "scope:repository");
1123 let decision = decide_operation(&user, &scope, "create_issue", None, true, None);
1124 assert_eq!(decision.rule, "scope:repository");
1125 // The repository's name is matched without regard to case.
1126 let decision = decide_operation(
1127 &user,
1128 &scope,
1129 "create_issue",
1130 Some(&path("Acme", "Rocket")),
1131 true,
1132 None,
1133 );
1134 assert!(decision.allowed);
1135 assert_eq!(decision.rule, "run:implement/tools");
1136 }
1137
1138 #[test]
1139 fn the_permission_is_the_intersection_with_the_person() {
1140 let scope = scope(K::Implement, Tools);
1141 // The person left the workspace: their agent can do nothing there.
1142 let user = agent(&[], scope.clone());
1143 let decision = decide_operation(
1144 &user,
1145 &scope,
1146 "get_issue",
1147 Some(&path("acme", "rocket")),
1148 true,
1149 Some(1),
1150 );
1151 assert!(!decision.allowed);
1152 assert_eq!(decision.rule, "on-behalf-of:membership");
1153 // And an owner's agent is only ever a member.
1154 let owner = vec![
1155 Membership {
1156 slug: "acme".to_owned(),
1157 role: Role::Owner,
1158 name: None,
1159 avatar: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1160 base_permission: Some(BasePermission::None),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1161 team_creation: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens1162 org_roles: vec![crate::OrgRole::SecurityManager],
1163 privileges: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1164 },
1165 Membership::member("elsewhere"),
1166 ];
1167 let memberships = intersect(&owner, "Acme");
1168 assert_eq!(memberships.len(), 1);
1169 assert_eq!(memberships[0].slug, "acme");
1170 assert_eq!(memberships[0].role, Role::Member);
Merge main (membership, two-factor, GitHub repo roles) into tokens1171 assert!(memberships[0].org_roles.is_empty());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1172 assert!(intersect(&owner, "nowhere").is_empty());
1173 }
1174
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1175 /// An agent gets at most the person's role on the repository, and
1176 /// never more than Write; nothing outside the run's workspace.
1177 #[test]
1178 fn an_agent_has_at_most_its_persons_role() {
1179 use crate::access::{Capability, RepoRef, can, permission};
1180 let rocket = RepoRef { id: "rep_1", namespace: "acme", private: true };
1181 let other = RepoRef { id: "rep_2", namespace: "acme", private: true };
1182 let elsewhere = RepoRef { id: "rep_3", namespace: "globex", private: true };
1183 let tools = scope(K::Implement, Tools);
1184 let scope = scope(K::Implement, Runner);
1185 // An owner's agent: Write, never Admin.
1186 let owner = [Membership { role: Role::Owner, ..Membership::member("acme") }, Membership::member("globex")];
1187 let mut agent_user = agent(&[], scope.clone());
1188 agent_user.workspaces = intersect(&owner, "acme");
1189 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1190 assert!(!can(Some(&agent_user), rocket, Capability::ManageSettings));
1191 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1192 // A member whose workspace gives Read: Read, so it cannot push.
1193 let reader = [Membership { base_permission: Some(BasePermission::Read), ..Membership::member("acme") }];
1194 agent_user.workspaces = intersect(&reader, "acme");
1195 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Read));
1196 assert!(!can(Some(&agent_user), rocket, Capability::Push));
1197 // An outside collaborator with Maintain on one repository: Write
1198 // there, nothing elsewhere, and the run is allowed.
1199 let grants = [
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1200 RepoGrant { repo_id: "rep_1".into(), workspace: "acme".into(), role: RepoRole::Maintain, team: None },
1201 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 look1202 ];
1203 agent_user.workspaces = intersect(&[], "acme");
1204 agent_user.grants = intersect_grants(&grants, "Acme");
1205 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1206 assert_eq!(permission(Some(&agent_user), other), None);
1207 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1208 let decision = decide_operation(&agent_user, &tools, "get_issue", Some(&path("acme", "rocket")), true, Some(1));
1209 assert!(decision.allowed, "{}", decision.reason.unwrap_or_default());
1210 // The person, downstream of a runner's credential, carries the same.
1211 let person = as_person(&agent_user).expect("a runner acts as the person");
1212 assert_eq!(permission(Some(&person), rocket), Some(RepoRole::Write));
1213 }
1214
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1215 #[test]
1216 fn a_runner_changes_only_its_own_pull_request() {
1217 let scope = scope(K::Implement, Runner);
1218 let user = agent(&["acme"], scope.clone());
1219 let repo = path("acme", "rocket");
1220 let other = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(8));
1221 assert!(!other.allowed);
1222 assert_eq!(other.rule, "scope:pull");
1223 let own = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(7));
1224 assert!(own.allowed);
1225 // Reading another is fine.
1226 assert!(
1227 decide_operation(
1228 &user,
1229 &scope,
1230 "get_pull_request",
1231 Some(&repo),
1232 true,
1233 Some(8)
1234 )
1235 .allowed
1236 );
1237 }
1238
1239 #[test]
1240 fn git_matrix() {
1241 let fork = path("pulls", "pul_7");
1242 let upstream = path("acme", "rocket");
1243 let elsewhere = path("acme", "billing");
1244 let implement = scope(K::Implement, Runner);
1245 assert!(decide_git(&implement, &fork, true).allowed);
1246 assert!(decide_git(&implement, &fork, false).allowed);
1247 assert!(decide_git(&implement, &upstream, false).allowed);
1248 assert_eq!(decide_git(&implement, &upstream, true).rule, "git:push");
1249 assert_eq!(decide_git(&implement, &elsewhere, false).rule, "git:read");
1250 let review = scope(K::Review, Runner);
1251 assert!(decide_git(&review, &upstream, false).allowed);
1252 assert!(!decide_git(&review, &upstream, true).allowed);
1253 assert!(!decide_git(&review, &fork, true).allowed);
1254 // A tools token made before run credentials never reaches git.
1255 let old = AgentScope {
1256 repo: upstream.clone(),
1257 operations: vec!["get_issue".to_owned()],
1258 run: None,
1259 };
1260 assert_eq!(decide_git(&old, &upstream, false).rule, "git:not-a-run");
1261 // Nor does an agent's tools token.
1262 assert_eq!(
1263 decide_git(&scope(K::Implement, Tools), &upstream, false).rule,
1264 "git:not-a-run"
1265 );
1266 }
1267
1268 #[test]
1269 fn a_push_moves_only_granted_branches() {
1270 let update = scope(K::Update, Runner);
1271 let repo = path("acme", "rocket");
1272 let refs = |names: &[&str]| {
1273 names
1274 .iter()
1275 .map(|name| (*name).to_owned())
1276 .collect::<Vec<_>>()
1277 };
1278 assert!(decide_refs(&update, &repo, &refs(&["refs/heads/fix-login"])).allowed);
1279 assert_eq!(
1280 decide_refs(&update, &repo, &refs(&["refs/heads/main"])).rule,
1281 "git:ref"
1282 );
1283 assert_eq!(
1284 decide_refs(
1285 &update,
1286 &repo,
1287 &refs(&["refs/heads/fix-login", "refs/tags/v1"])
1288 )
1289 .rule,
1290 "git:ref"
1291 );
1292 let implement = scope(K::Implement, Runner);
1293 assert!(
1294 decide_refs(
1295 &implement,
1296 &path("pulls", "pul_7"),
1297 &refs(&["refs/heads/main"])
1298 )
1299 .allowed
1300 );
1301 }
1302
1303 #[test]
1304 fn a_runner_acts_downstream_as_the_person() {
1305 let user = agent(&["acme"], scope(K::Implement, Runner));
1306 let person = as_person(&user).unwrap();
1307 assert_eq!(person.id, "usr_1");
1308 assert_eq!(person.username, "syntaqx");
1309 assert_eq!(person.kind, PrincipalKind::User);
1310 assert!(person.is_member("acme"));
1311 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-agent1312 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 API1313 // The tools act as the agent.
1314 assert!(as_person(&agent(&["acme"], scope(K::Implement, Tools))).is_none());
1315 }
1316
1317 #[test]
1318 fn scopes_without_a_run_still_parse() {
1319 let old: AgentScope = serde_json::from_str(
1320 r#"{"repo":{"namespace":"acme","name":"rocket"},"operations":["get_issue"]}"#,
1321 )
1322 .unwrap();
1323 assert!(old.run.is_none());
1324 let written = serde_json::to_string(&scope(K::Review, Tools)).unwrap();
1325 assert!(written.contains(r#""use":"tools""#));
1326 assert!(written.contains(r#""kind":"review""#));
1327 let back: AgentScope = serde_json::from_str(&written).unwrap();
1328 assert_eq!(back.run.unwrap().kind, K::Review);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1329 // Only g1t's own runs say so; every other reads as not.
1330 assert!(!written.contains("system"));
1331 assert!(!back_run(&written).system);
1332 let mut own = scope(K::Bump, Runner);
1333 own.run.as_mut().unwrap().system = true;
1334 let written = serde_json::to_string(&own).unwrap();
1335 assert!(written.contains(r#""system":true"#));
1336 assert!(back_run(&written).system);
1337 }
1338
1339 fn back_run(written: &str) -> RunBinding {
1340 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 API1341 }
1342}

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