Skip to content

g1t/crates/contracts/src/credentials.rs

1,177 lines41,381 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",
230 "list_repos",
231 "get_repo",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look232 "list_deleted_repos",
233 "list_collaborators",
234 "get_collaborator_permission",
235 "list_repo_invitations",
236 "list_my_repo_invitations",
237 "list_outside_collaborators",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API238 "get_repo_settings",
Fast pages, required checks on the branch, self-hosted runners, honest incidents239 "list_check_names",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API240 "get_merge_queue",
241 "recall",
242 "list_issues",
243 "get_issue",
244 "get_plan",
245 "list_labels",
246 "list_pull_requests",
247 "get_pull_request",
248 "read_session",
249 "get_pull_request_changes",
250 "list_events",
251 "get_context",
252 "search_context",
253 "get_entity",
Search across all of g1t, Explore, and a command palette254 "search",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API255 "list_workflows",
256 "list_workflow_runs",
257 "get_workflow_run",
258 "get_job_logs",
259 "list_integrations",
260 "get_model_routes",
261 "list_webhooks",
262 "list_webhook_deliveries",
263 "list_actions_secrets",
264 "list_actions_variables",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily265 "list_security_alerts",
API: notifications over REST and MCP, with notifications scopes266 "list_notifications",
267 "get_notification_thread",
268 "get_thread_subscription",
269 "get_repo_subscription",
270 "list_watched_repos",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API271];
272
273/// What no agent's token may ever do, whatever its scope says: workspaces,
274/// repositories' settings, members, tokens, billing, integrations,
275/// webhooks, secrets, workflows' controls, merging, and putting more agents
276/// to work.
277pub const NEVER: &[&str] = &[
278 "create_workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look279 "delete_workspace",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily280 "update_workspace",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look281 "transfer_repo",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API282 "create_repo",
283 "update_repo",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look284 "delete_repo",
285 "list_deleted_repos",
286 "restore_repo",
287 "purge_repo",
288 "rename_repo",
289 "archive_repo",
290 "unarchive_repo",
291 "set_repo_visibility",
292 "rename_branch",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API293 "update_repo_settings",
294 "merge_pull_request",
295 "assign_issue",
296 "plan_work",
297 "apply_plan",
298 "import_issue",
299 "list_integrations",
300 "connect_integration",
301 "disconnect_integration",
302 "test_integration",
303 "get_model_routes",
304 "set_model_routes",
305 "list_webhooks",
306 "create_webhook",
307 "update_webhook",
308 "delete_webhook",
309 "ping_webhook",
310 "list_webhook_deliveries",
311 "redeliver_webhook",
312 "dispatch_workflow",
313 "cancel_workflow_run",
314 "rerun_workflow_run",
315 "update_workflow",
316 "list_actions_secrets",
317 "set_actions_secret",
318 "delete_actions_secret",
319 "list_actions_variables",
320 "set_actions_variable",
321 "delete_actions_variable",
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 "list_collaborators",
323 "get_collaborator_permission",
324 "add_collaborator",
325 "update_collaborator",
326 "remove_collaborator",
327 "list_repo_invitations",
328 "revoke_repo_invitation",
329 "list_my_repo_invitations",
330 "accept_repo_invitation",
331 "decline_repo_invitation",
332 "set_base_permission",
333 "list_outside_collaborators",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily334 // Dismissing a secret lets it through push protection.
335 "dismiss_security_alert",
336 "reopen_security_alert",
API: notifications over REST and MCP, with notifications scopes337 // A person's own inbox: g1t's agents act as g1t, which has none.
338 "list_notifications",
339 "get_notification_thread",
340 "mark_notifications_read",
341 "mark_thread_read",
342 "mark_thread_done",
343 "save_thread",
344 "snooze_thread",
345 "get_thread_subscription",
346 "set_thread_subscription",
347 "delete_thread_subscription",
348 "get_repo_subscription",
349 "set_repo_subscription",
350 "delete_repo_subscription",
351 "list_watched_repos",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API352];
353
354/// Reading what an agent needs to know about its repository.
355const TOOLS_READ: &[&str] = &[
356 "get_repo",
357 "list_issues",
358 "get_issue",
359 "list_labels",
360 "list_pull_requests",
361 "get_pull_request",
362 "get_pull_request_changes",
363 "read_session",
364 "get_merge_queue",
365 "list_events",
366 "recall",
367 "search_context",
368 "get_entity",
Search across all of g1t, Explore, and a command palette369 "search",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API370 "list_workflows",
371 "list_workflow_runs",
372 "get_workflow_run",
373 "get_job_logs",
374];
375
376pub fn is_read(operation: &str) -> bool {
377 READ_OPERATIONS.contains(&operation)
378}
379
380/// The API and MCP operations a run of `kind` may use with a credential
381/// for `usage`. Git is separate: see [`decide_git`].
382pub fn operations_for(kind: RunCredentialKind, usage: CredentialUse) -> Vec<&'static str> {
383 use RunCredentialKind as K;
384 let mut operations: Vec<&'static str> = Vec::new();
385 match usage {
386 CredentialUse::Runner => {
387 if kind.works_on_a_pull() {
388 operations.extend(["get_repo", "get_pull_request", "record_session"]);
389 }
390 if kind == K::Implement {
391 operations.push("mark_pull_request_ready");
392 }
393 }
394 CredentialUse::Tools => match kind {
395 K::Implement | K::Revise | K::Answer => {
396 operations.extend(TOOLS_READ.iter().copied());
397 operations.extend([
398 "create_issue",
399 "add_comment",
400 "take_messages",
401 "remember",
402 "message_agent",
403 "answer_message",
404 "get_context",
405 ]);
406 }
407 K::Review => {
408 operations.extend(TOOLS_READ.iter().copied());
409 operations.extend(["add_comment", "review_pull_request", "get_context"]);
410 }
411 K::Plan => {
412 operations.extend(TOOLS_READ.iter().copied());
413 operations.extend(["create_issue", "get_context"]);
414 }
415 K::Update => operations.extend(TOOLS_READ.iter().copied()),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily416 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 API417 },
418 }
419 operations
420}
421
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step422/// What a run's credential may do, in the scope vocabulary that access
423/// tokens use (see [`crate::scopes`]): the scopes of its operations, and
424/// for a runner, git's. Its operations, its repository and its run still
425/// bound it more tightly than these scopes say.
426pub fn run_scopes(kind: RunCredentialKind, usage: CredentialUse) -> Vec<crate::scopes::Scope> {
427 use crate::scopes::{Scope, normalize, scope_for};
428 let mut scopes: Vec<Scope> = operations_for(kind, usage)
429 .into_iter()
430 .filter_map(scope_for)
431 .collect();
432 if usage == CredentialUse::Runner {
433 scopes.push(Scope::CodeRead);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily434 if matches!(
435 kind,
436 RunCredentialKind::Implement
437 | RunCredentialKind::Revise
438 | RunCredentialKind::Answer
439 | RunCredentialKind::Update
440 | RunCredentialKind::Bump
441 ) {
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step442 scopes.push(Scope::CodeWrite);
443 }
444 }
445 normalize(&mut scopes);
446 scopes
447}
448
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API449/// Operations that change a pull request, which a runner may do only to
450/// the pull request its run works on.
451const PULL_WRITES: &[&str] = &["record_session", "mark_pull_request_ready"];
452
453/// Whether something was allowed, and the rule that decided it.
454#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
455pub struct Decision {
456 pub allowed: bool,
457 /// A short, stable name: `run:implement/tools`, `never`,
458 /// `scope:repository` and so on. Shown in the audit log.
459 pub rule: String,
460 /// Why it was refused, for the caller.
461 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub reason: Option<String>,
463}
464
465impl Decision {
466 pub fn allow(rule: impl Into<String>) -> Self {
467 Decision {
468 allowed: true,
469 rule: rule.into(),
470 reason: None,
471 }
472 }
473
474 pub fn deny(rule: impl Into<String>, reason: impl Into<String>) -> Self {
475 Decision {
476 allowed: false,
477 rule: rule.into(),
478 reason: Some(reason.into()),
479 }
480 }
481}
482
483fn same_repo(a: &RepoPath, b: &RepoPath) -> bool {
484 a.namespace.eq_ignore_ascii_case(&b.namespace) && a.name.eq_ignore_ascii_case(&b.name)
485}
486
487fn scope_rule(scope: &AgentScope) -> String {
488 match &scope.run {
489 Some(run) => format!("run:{}/{}", run.kind.as_str(), run.usage.as_str()),
490 None => "agent-token".to_owned(),
491 }
492}
493
494/// Whether `user`, resolved from an agent's token with `scope`, may use
495/// `operation`. `repo` is the repository the call names, if any, and
496/// `needs_repo` whether the operation is about one; `number` the issue or
497/// pull request it names.
498pub fn decide_operation(
499 user: &User,
500 scope: &AgentScope,
501 operation: &str,
502 repo: Option<&RepoPath>,
503 needs_repo: bool,
504 number: Option<u32>,
505) -> Decision {
506 let who = "A g1t agent's token";
507 if NEVER.contains(&operation) {
508 return Decision::deny(
509 "never",
510 format!(
511 "{who} can never use {operation}: settings, members, tokens, billing, integrations, webhooks, secrets and merging are for people."
512 ),
513 );
514 }
515 if !scope.operations.iter().any(|name| name == operation) {
516 return Decision::deny(
517 "scope:operation",
518 format!("{who} for this run cannot use {operation}."),
519 );
520 }
521 if needs_repo && !repo.is_some_and(|asked| same_repo(asked, &scope.repo)) {
522 return Decision::deny(
523 "scope:repository",
524 format!(
525 "{who} works in {}/{} only.",
526 scope.repo.namespace, scope.repo.name
527 ),
528 );
529 }
530 // 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 look531 // in the repository's workspace, as a member or with a role on its
532 // repositories. What it may do in the repository itself is their
533 // role there, which services check (`access::can`).
534 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 API535 return Decision::deny(
536 "on-behalf-of:membership",
537 format!(
538 "The person this agent works for is no longer a member of {}.",
539 scope.repo.namespace
540 ),
541 );
542 }
543 if let Some(run) = &scope.run
544 && run.usage == CredentialUse::Runner
545 && PULL_WRITES.contains(&operation)
546 && run.number.is_some()
547 && number != run.number
548 {
549 return Decision::deny(
550 "scope:pull",
551 format!(
552 "{who} can change pull request #{} only.",
553 run.number.unwrap_or_default()
554 ),
555 );
556 }
557 Decision::allow(scope_rule(scope))
558}
559
560/// Whether a run credential may clone or fetch (`write` false), or push to
561/// (`write` true), the repository at `repo`.
562pub fn decide_git(scope: &AgentScope, repo: &RepoPath, write: bool) -> Decision {
563 let Some(run) = scope
564 .run
565 .as_ref()
566 .filter(|run| run.usage == CredentialUse::Runner)
567 else {
568 return Decision::deny(
569 "git:not-a-run",
570 "A g1t agent's tools token cannot be used with git.",
571 );
572 };
573 let pushable = run.push.iter().any(|grant| same_repo(&grant.repo, repo));
574 if write {
575 return if pushable {
576 Decision::allow(format!("{}:push", scope_rule(scope)))
577 } else {
578 Decision::deny(
579 "git:push",
580 format!(
581 "A {} run cannot push to {}/{}.",
582 run.kind.as_str(),
583 repo.namespace,
584 repo.name
585 ),
586 )
587 };
588 }
589 let readable = pushable
590 || same_repo(&scope.repo, repo)
591 || run.read.iter().any(|path| same_repo(path, repo));
592 if readable {
593 Decision::allow(format!("{}:read", scope_rule(scope)))
594 } else {
595 Decision::deny(
596 "git:read",
597 format!(
598 "A {} run cannot read {}/{}.",
599 run.kind.as_str(),
600 repo.namespace,
601 repo.name
602 ),
603 )
604 }
605}
606
607/// Whether a push to `repo` is limited to certain branches, so that the
608/// refs it moves have to be read and checked with [`decide_refs`].
609pub fn limits_branches(scope: &AgentScope, repo: &RepoPath) -> bool {
610 scope
611 .run
612 .iter()
613 .flat_map(|run| run.push.iter())
614 .any(|grant| same_repo(&grant.repo, repo) && grant.branch.is_some())
615}
616
617/// Whether a push to `repo` may move `refs` (full refs, such as
618/// `refs/heads/main`). Tags are never a run's to move.
619pub fn decide_refs(scope: &AgentScope, repo: &RepoPath, refs: &[String]) -> Decision {
620 let repo_decision = decide_git(scope, repo, true);
621 if !repo_decision.allowed {
622 return repo_decision;
623 }
624 let grants: Vec<&GitGrant> = scope
625 .run
626 .iter()
627 .flat_map(|run| run.push.iter())
628 .filter(|grant| same_repo(&grant.repo, repo))
629 .collect();
630 for git_ref in refs {
631 let Some(branch) = git_ref.strip_prefix("refs/heads/") else {
632 return Decision::deny("git:ref", format!("A run cannot push {git_ref}."));
633 };
634 let allowed = grants
635 .iter()
636 .any(|grant| grant.branch.as_deref().is_none_or(|only| only == branch));
637 if !allowed {
638 return Decision::deny(
639 "git:ref",
640 format!(
641 "A run cannot push to {branch} in {}/{}.",
642 repo.namespace, repo.name
643 ),
644 );
645 }
646 }
647 repo_decision
648}
649
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look650/// The most an agent may be on a repository, whoever it works for: it
651/// can push, merge and run, never change settings or who has access.
652pub const AGENT_CEILING: RepoRole = RepoRole::Write;
653
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API654/// 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 look655/// workspace, as a member, only if the person is in it now, with the
656/// person's role on its repositories (an owner's Admin included) cut down
657/// to [`AGENT_CEILING`].
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API658pub fn intersect(person: &[Membership], namespace: &str) -> Vec<Membership> {
659 let namespace = namespace.to_lowercase();
660 person
661 .iter()
662 .filter(|membership| membership.slug == namespace)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look663 .map(|membership| {
664 let base = match membership.role {
665 Role::Owner => BasePermission::Admin,
666 Role::Member => membership.base_permission.unwrap_or_default(),
667 };
668 Membership {
669 role: Role::Member,
670 base_permission: Some(match base {
671 BasePermission::Admin => BasePermission::Write,
672 base => base,
673 }),
674 ..membership.clone()
675 }
676 })
677 .collect()
678}
679
680/// The repository grants an agent working for `person` has: those in the
681/// run's workspace, each cut down to [`AGENT_CEILING`].
682pub fn intersect_grants(person: &[RepoGrant], namespace: &str) -> Vec<RepoGrant> {
683 let namespace = namespace.to_lowercase();
684 person
685 .iter()
686 .filter(|grant| grant.workspace == namespace)
687 .map(|grant| RepoGrant {
688 role: grant.role.min(AGENT_CEILING),
689 ..grant.clone()
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API690 })
691 .collect()
692}
693
694/// Who a runner's credential acts as downstream: the person, with only the
695/// agent's (already intersected) memberships. `None` for anything else.
696pub fn as_person(user: &User) -> Option<User> {
697 let acting = user.acting.as_ref()?;
698 if user.kind != PrincipalKind::Agent {
699 return None;
700 }
701 let run = acting.run()?;
702 if run.usage != CredentialUse::Runner {
703 return None;
704 }
705 Some(User {
706 id: acting.on_behalf_of.id.clone(),
707 username: acting.on_behalf_of.username.clone(),
708 kind: PrincipalKind::User,
709 verified: user.verified,
710 workspaces: user.workspaces.clone(),
711 avatar: None,
712 acting: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look713 grants: user.grants.clone(),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step714 token: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API715 })
716}
717
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent718/// 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 API719pub fn describe(user: &User) -> String {
720 match &user.acting {
721 Some(acting) => format!(
722 "{} on behalf of {}",
723 acting.agent, acting.on_behalf_of.username
724 ),
725 None => user.username.clone(),
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step734 fn a_run_s_scopes_are_never_admin() {
735 for kind in RunCredentialKind::ALL {
736 for usage in [CredentialUse::Runner, CredentialUse::Tools] {
737 let scopes = run_scopes(kind, usage);
738 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{kind:?} {usage:?}: {scopes:?}");
739 }
740 }
741 let review = run_scopes(RunCredentialKind::Review, CredentialUse::Tools);
742 assert!(review.contains(&crate::scopes::Scope::PullRequestsWrite));
743 assert!(!review.contains(&crate::scopes::Scope::CodeWrite));
744 }
745
746 #[test]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API747 fn agents_can_search_the_context_hub() {
748 for kind in [RunCredentialKind::Implement, RunCredentialKind::Review, RunCredentialKind::Plan] {
749 let tools = operations_for(kind, CredentialUse::Tools);
750 assert!(tools.contains(&"search_context") && tools.contains(&"get_entity"));
751 }
752 assert!(is_read("search_context") && is_read("get_entity"));
753 }
754
Search across all of g1t, Explore, and a command palette755 #[test]
756 fn agents_can_search_all_of_g1t() {
757 // Site-wide search only reads: every run that reads its repository
758 // may use it, and nothing that never reads gets it.
759 assert!(is_read("search"));
760 assert!(!NEVER.contains(&"search"));
761 for kind in [
762 RunCredentialKind::Implement,
763 RunCredentialKind::Revise,
764 RunCredentialKind::Answer,
765 RunCredentialKind::Review,
766 RunCredentialKind::Plan,
767 RunCredentialKind::Update,
768 ] {
769 let tools = operations_for(kind, CredentialUse::Tools);
770 assert!(tools.contains(&"search"), "{kind:?} should search");
771 // The context hub's search stays its own tool beside it.
772 assert!(tools.contains(&"search_context"), "{kind:?} keeps search_context");
773 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily774 for kind in [RunCredentialKind::Checks, RunCredentialKind::Queue, RunCredentialKind::Mergecheck, RunCredentialKind::Deploy, RunCredentialKind::Bump] {
Search across all of g1t, Explore, and a command palette775 assert!(!operations_for(kind, CredentialUse::Tools).contains(&"search"));
776 }
777 assert!(!operations_for(RunCredentialKind::Implement, CredentialUse::Runner).contains(&"search"));
778 }
779
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API780 fn path(namespace: &str, name: &str) -> RepoPath {
781 RepoPath {
782 namespace: namespace.to_owned(),
783 name: name.to_owned(),
784 }
785 }
786
787 fn scope(kind: RunCredentialKind, usage: CredentialUse) -> AgentScope {
788 AgentScope {
789 repo: path("acme", "rocket"),
790 operations: operations_for(kind, usage)
791 .into_iter()
792 .map(str::to_owned)
793 .collect(),
794 run: Some(RunBinding {
795 kind,
796 usage,
797 run_id: Some("run_1".to_owned()),
798 number: Some(7),
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent799 agent: "g1t".to_owned(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily800 system: false,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API801 read: vec![path("acme", "rocket")],
802 push: match kind {
803 RunCredentialKind::Implement
804 | RunCredentialKind::Revise
805 | RunCredentialKind::Answer => vec![GitGrant {
806 repo: path("pulls", "pul_7"),
807 branch: None,
808 }],
809 RunCredentialKind::Update => vec![GitGrant {
810 repo: path("acme", "rocket"),
811 branch: Some("fix-login".to_owned()),
812 }],
813 _ => vec![],
814 },
815 }),
816 }
817 }
818
819 fn agent(member_of: &[&str], scope: AgentScope) -> User {
820 User {
821 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-agent822 username: "g1t".to_owned(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API823 kind: PrincipalKind::Agent,
824 verified: true,
825 workspaces: member_of
826 .iter()
827 .map(|slug| Membership::member(*slug))
828 .collect(),
829 avatar: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look830 grants: Vec::new(),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step831 token: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API832 acting: Some(Box::new(Acting {
833 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-agent834 agent: "g1t".to_owned(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API835 on_behalf_of: Principal {
836 id: "usr_1".to_owned(),
837 username: "syntaqx".to_owned(),
838 },
839 scope,
840 })),
841 }
842 }
843
844 fn op(kind: RunCredentialKind, usage: CredentialUse, operation: &str) -> Decision {
845 let scope = scope(kind, usage);
846 let user = agent(&["acme"], scope.clone());
847 decide_operation(
848 &user,
849 &scope,
850 operation,
851 Some(&path("acme", "rocket")),
852 true,
853 Some(7),
854 )
855 }
856
857 use CredentialUse::{Runner, Tools};
858 use RunCredentialKind as K;
859
860 /// Which operations each kind of run may use through its tools: the
861 /// allowed and denied matrix.
862 #[test]
863 fn tools_matrix() {
864 let cases: [(&str, [bool; 6]); 12] = [
865 // implement revise answer review plan checks
866 ("get_issue", [true, true, true, true, true, false]),
867 ("create_issue", [true, true, true, false, true, false]),
868 ("add_comment", [true, true, true, true, false, false]),
869 (
870 "review_pull_request",
871 [false, false, false, true, false, false],
872 ),
873 ("remember", [true, true, true, false, false, false]),
874 ("take_messages", [true, true, true, false, false, false]),
875 ("record_session", [false, false, false, false, false, false]),
876 (
877 "merge_pull_request",
878 [false, false, false, false, false, false],
879 ),
880 (
881 "update_repo_settings",
882 [false, false, false, false, false, false],
883 ),
884 ("create_webhook", [false, false, false, false, false, false]),
885 (
886 "set_actions_secret",
887 [false, false, false, false, false, false],
888 ),
889 ("assign_issue", [false, false, false, false, false, false]),
890 ];
891 let kinds = [
892 K::Implement,
893 K::Revise,
894 K::Answer,
895 K::Review,
896 K::Plan,
897 K::Checks,
898 ];
899 for (operation, expected) in cases {
900 for (kind, allowed) in kinds.into_iter().zip(expected) {
901 assert_eq!(
902 op(kind, Tools, operation).allowed,
903 allowed,
904 "{operation} by a {} run's tools",
905 kind.as_str()
906 );
907 }
908 }
909 }
910
911 #[test]
912 fn runner_matrix() {
913 assert!(op(K::Implement, Runner, "record_session").allowed);
914 assert!(op(K::Implement, Runner, "mark_pull_request_ready").allowed);
915 assert!(op(K::Revise, Runner, "record_session").allowed);
916 assert!(!op(K::Revise, Runner, "mark_pull_request_ready").allowed);
917 assert!(!op(K::Implement, Runner, "create_issue").allowed);
918 assert!(!op(K::Review, Runner, "record_session").allowed);
919 assert!(!op(K::Checks, Runner, "get_issue").allowed);
920 }
921
922 #[test]
923 fn settings_billing_tokens_and_members_are_never_reachable() {
924 for kind in RunCredentialKind::ALL {
925 for usage in [Runner, Tools] {
926 for operation in NEVER.iter().copied() {
927 let decision = op(kind, usage, operation);
928 assert!(!decision.allowed);
929 assert_eq!(decision.rule, "never");
930 }
931 }
932 }
933 // Even a scope that lists one is refused.
934 let mut wide = scope(K::Implement, Tools);
935 wide.operations.push("merge_pull_request".to_owned());
936 let user = agent(&["acme"], wide.clone());
937 let decision = decide_operation(
938 &user,
939 &wide,
940 "merge_pull_request",
941 Some(&path("acme", "rocket")),
942 true,
943 Some(7),
944 );
945 assert_eq!(decision.rule, "never");
946 }
947
948 #[test]
949 fn another_repository_is_refused() {
950 let scope = scope(K::Implement, Tools);
951 let user = agent(&["acme"], scope.clone());
952 let decision = decide_operation(
953 &user,
954 &scope,
955 "create_issue",
956 Some(&path("acme", "other")),
957 true,
958 None,
959 );
960 assert!(!decision.allowed);
961 assert_eq!(decision.rule, "scope:repository");
962 let decision = decide_operation(&user, &scope, "create_issue", None, true, None);
963 assert_eq!(decision.rule, "scope:repository");
964 // The repository's name is matched without regard to case.
965 let decision = decide_operation(
966 &user,
967 &scope,
968 "create_issue",
969 Some(&path("Acme", "Rocket")),
970 true,
971 None,
972 );
973 assert!(decision.allowed);
974 assert_eq!(decision.rule, "run:implement/tools");
975 }
976
977 #[test]
978 fn the_permission_is_the_intersection_with_the_person() {
979 let scope = scope(K::Implement, Tools);
980 // The person left the workspace: their agent can do nothing there.
981 let user = agent(&[], scope.clone());
982 let decision = decide_operation(
983 &user,
984 &scope,
985 "get_issue",
986 Some(&path("acme", "rocket")),
987 true,
988 Some(1),
989 );
990 assert!(!decision.allowed);
991 assert_eq!(decision.rule, "on-behalf-of:membership");
992 // And an owner's agent is only ever a member.
993 let owner = vec![
994 Membership {
995 slug: "acme".to_owned(),
996 role: Role::Owner,
997 name: None,
998 avatar: None,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look999 base_permission: Some(BasePermission::None),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1000 },
1001 Membership::member("elsewhere"),
1002 ];
1003 let memberships = intersect(&owner, "Acme");
1004 assert_eq!(memberships.len(), 1);
1005 assert_eq!(memberships[0].slug, "acme");
1006 assert_eq!(memberships[0].role, Role::Member);
1007 assert!(intersect(&owner, "nowhere").is_empty());
1008 }
1009
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1010 /// An agent gets at most the person's role on the repository, and
1011 /// never more than Write; nothing outside the run's workspace.
1012 #[test]
1013 fn an_agent_has_at_most_its_persons_role() {
1014 use crate::access::{Capability, RepoRef, can, permission};
1015 let rocket = RepoRef { id: "rep_1", namespace: "acme", private: true };
1016 let other = RepoRef { id: "rep_2", namespace: "acme", private: true };
1017 let elsewhere = RepoRef { id: "rep_3", namespace: "globex", private: true };
1018 let tools = scope(K::Implement, Tools);
1019 let scope = scope(K::Implement, Runner);
1020 // An owner's agent: Write, never Admin.
1021 let owner = [Membership { role: Role::Owner, ..Membership::member("acme") }, Membership::member("globex")];
1022 let mut agent_user = agent(&[], scope.clone());
1023 agent_user.workspaces = intersect(&owner, "acme");
1024 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1025 assert!(!can(Some(&agent_user), rocket, Capability::ManageSettings));
1026 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1027 // A member whose workspace gives Read: Read, so it cannot push.
1028 let reader = [Membership { base_permission: Some(BasePermission::Read), ..Membership::member("acme") }];
1029 agent_user.workspaces = intersect(&reader, "acme");
1030 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Read));
1031 assert!(!can(Some(&agent_user), rocket, Capability::Push));
1032 // An outside collaborator with Maintain on one repository: Write
1033 // there, nothing elsewhere, and the run is allowed.
1034 let grants = [
1035 RepoGrant { repo_id: "rep_1".into(), workspace: "acme".into(), role: RepoRole::Maintain },
1036 RepoGrant { repo_id: "rep_3".into(), workspace: "globex".into(), role: RepoRole::Admin },
1037 ];
1038 agent_user.workspaces = intersect(&[], "acme");
1039 agent_user.grants = intersect_grants(&grants, "Acme");
1040 assert_eq!(permission(Some(&agent_user), rocket), Some(RepoRole::Write));
1041 assert_eq!(permission(Some(&agent_user), other), None);
1042 assert_eq!(permission(Some(&agent_user), elsewhere), None);
1043 let decision = decide_operation(&agent_user, &tools, "get_issue", Some(&path("acme", "rocket")), true, Some(1));
1044 assert!(decision.allowed, "{}", decision.reason.unwrap_or_default());
1045 // The person, downstream of a runner's credential, carries the same.
1046 let person = as_person(&agent_user).expect("a runner acts as the person");
1047 assert_eq!(permission(Some(&person), rocket), Some(RepoRole::Write));
1048 }
1049
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1050 #[test]
1051 fn a_runner_changes_only_its_own_pull_request() {
1052 let scope = scope(K::Implement, Runner);
1053 let user = agent(&["acme"], scope.clone());
1054 let repo = path("acme", "rocket");
1055 let other = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(8));
1056 assert!(!other.allowed);
1057 assert_eq!(other.rule, "scope:pull");
1058 let own = decide_operation(&user, &scope, "record_session", Some(&repo), true, Some(7));
1059 assert!(own.allowed);
1060 // Reading another is fine.
1061 assert!(
1062 decide_operation(
1063 &user,
1064 &scope,
1065 "get_pull_request",
1066 Some(&repo),
1067 true,
1068 Some(8)
1069 )
1070 .allowed
1071 );
1072 }
1073
1074 #[test]
1075 fn git_matrix() {
1076 let fork = path("pulls", "pul_7");
1077 let upstream = path("acme", "rocket");
1078 let elsewhere = path("acme", "billing");
1079 let implement = scope(K::Implement, Runner);
1080 assert!(decide_git(&implement, &fork, true).allowed);
1081 assert!(decide_git(&implement, &fork, false).allowed);
1082 assert!(decide_git(&implement, &upstream, false).allowed);
1083 assert_eq!(decide_git(&implement, &upstream, true).rule, "git:push");
1084 assert_eq!(decide_git(&implement, &elsewhere, false).rule, "git:read");
1085 let review = scope(K::Review, Runner);
1086 assert!(decide_git(&review, &upstream, false).allowed);
1087 assert!(!decide_git(&review, &upstream, true).allowed);
1088 assert!(!decide_git(&review, &fork, true).allowed);
1089 // A tools token made before run credentials never reaches git.
1090 let old = AgentScope {
1091 repo: upstream.clone(),
1092 operations: vec!["get_issue".to_owned()],
1093 run: None,
1094 };
1095 assert_eq!(decide_git(&old, &upstream, false).rule, "git:not-a-run");
1096 // Nor does an agent's tools token.
1097 assert_eq!(
1098 decide_git(&scope(K::Implement, Tools), &upstream, false).rule,
1099 "git:not-a-run"
1100 );
1101 }
1102
1103 #[test]
1104 fn a_push_moves_only_granted_branches() {
1105 let update = scope(K::Update, Runner);
1106 let repo = path("acme", "rocket");
1107 let refs = |names: &[&str]| {
1108 names
1109 .iter()
1110 .map(|name| (*name).to_owned())
1111 .collect::<Vec<_>>()
1112 };
1113 assert!(decide_refs(&update, &repo, &refs(&["refs/heads/fix-login"])).allowed);
1114 assert_eq!(
1115 decide_refs(&update, &repo, &refs(&["refs/heads/main"])).rule,
1116 "git:ref"
1117 );
1118 assert_eq!(
1119 decide_refs(
1120 &update,
1121 &repo,
1122 &refs(&["refs/heads/fix-login", "refs/tags/v1"])
1123 )
1124 .rule,
1125 "git:ref"
1126 );
1127 let implement = scope(K::Implement, Runner);
1128 assert!(
1129 decide_refs(
1130 &implement,
1131 &path("pulls", "pul_7"),
1132 &refs(&["refs/heads/main"])
1133 )
1134 .allowed
1135 );
1136 }
1137
1138 #[test]
1139 fn a_runner_acts_downstream_as_the_person() {
1140 let user = agent(&["acme"], scope(K::Implement, Runner));
1141 let person = as_person(&user).unwrap();
1142 assert_eq!(person.id, "usr_1");
1143 assert_eq!(person.username, "syntaqx");
1144 assert_eq!(person.kind, PrincipalKind::User);
1145 assert!(person.is_member("acme"));
1146 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-agent1147 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 API1148 // The tools act as the agent.
1149 assert!(as_person(&agent(&["acme"], scope(K::Implement, Tools))).is_none());
1150 }
1151
1152 #[test]
1153 fn scopes_without_a_run_still_parse() {
1154 let old: AgentScope = serde_json::from_str(
1155 r#"{"repo":{"namespace":"acme","name":"rocket"},"operations":["get_issue"]}"#,
1156 )
1157 .unwrap();
1158 assert!(old.run.is_none());
1159 let written = serde_json::to_string(&scope(K::Review, Tools)).unwrap();
1160 assert!(written.contains(r#""use":"tools""#));
1161 assert!(written.contains(r#""kind":"review""#));
1162 let back: AgentScope = serde_json::from_str(&written).unwrap();
1163 assert_eq!(back.run.unwrap().kind, K::Review);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1164 // Only g1t's own runs say so; every other reads as not.
1165 assert!(!written.contains("system"));
1166 assert!(!back_run(&written).system);
1167 let mut own = scope(K::Bump, Runner);
1168 own.run.as_mut().unwrap().system = true;
1169 let written = serde_json::to_string(&own).unwrap();
1170 assert!(written.contains(r#""system":true"#));
1171 assert!(back_run(&written).system);
1172 }
1173
1174 fn back_run(written: &str) -> RunBinding {
1175 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 API1176 }
1177}