@rosthq/cli 0.7.55 → 0.7.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -13258,6 +13258,7 @@ var require_main3 = __commonJS({
13258
13258
 
13259
13259
  // src/index.ts
13260
13260
  import { realpathSync as realpathSync3 } from "node:fs";
13261
+ import { createInterface } from "node:readline/promises";
13261
13262
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
13262
13263
 
13263
13264
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -39054,6 +39055,23 @@ var agentBuilderToolProposalSchema = external_exports.object({
39054
39055
  connected: external_exports.boolean(),
39055
39056
  declined: external_exports.boolean()
39056
39057
  }).strict();
39058
+ var agentBuilderConfirmationEvidenceSchema = external_exports.object({
39059
+ source: external_exports.object({
39060
+ kind: external_exports.enum(["transcript", "artifact"]),
39061
+ summary: external_exports.string().trim().min(1).max(240)
39062
+ }).strict().optional(),
39063
+ proposedLifecycleCommand: external_exports.object({
39064
+ commandId: external_exports.enum(["agent.create_custom", "agent.configure_tools", "agent.run_dry_run", "agent.go_live"]),
39065
+ summary: external_exports.string().trim().min(1).max(360),
39066
+ inputSummary: external_exports.string().trim().min(1).max(360).optional()
39067
+ }).strict(),
39068
+ stageAudit: external_exports.object({
39069
+ guardResult: external_exports.enum(["allowed", "denied_manifest", "denied_budget", "escalated", "denied_tenant_policy"]),
39070
+ outcome: external_exports.string().trim().min(1).max(120),
39071
+ manifestClause: external_exports.string().trim().min(1).max(240).optional(),
39072
+ toolCallId: external_exports.string().trim().min(1).max(120).optional()
39073
+ }).strict().optional()
39074
+ }).strict();
39057
39075
  var agentBuilderQuestionSchema = external_exports.object({
39058
39076
  id: external_exports.string().trim().min(1).max(120),
39059
39077
  field: external_exports.enum([
@@ -39089,6 +39107,7 @@ var agentBuilderPendingConfirmationCardSchema = external_exports.object({
39089
39107
  humanGateLabel: external_exports.string().trim().min(1).max(120),
39090
39108
  riskLevel: external_exports.enum(["normal", "sensitive", "dangerous"]),
39091
39109
  confirmationId: external_exports.string().trim().min(1).max(120).optional(),
39110
+ evidence: agentBuilderConfirmationEvidenceSchema.optional(),
39092
39111
  approveVia: external_exports.object({
39093
39112
  webUrl: external_exports.string().trim().min(1).max(500).optional(),
39094
39113
  cliCommand: external_exports.string().trim().min(1).max(500).optional()
@@ -39193,6 +39212,13 @@ var AGENT_POLICY_DEFAULT = {
39193
39212
  max_autonomous_risk: AGENT_POLICY_PROFILE_CEILINGS.balanced
39194
39213
  };
39195
39214
 
39215
+ // ../../packages/protocol/src/confirmation-policy.ts
39216
+ var CLI_CONFIRMATION_MODES = ["trusted_operator", "careful"];
39217
+ var cliConfirmationModeSchema = external_exports.enum(CLI_CONFIRMATION_MODES);
39218
+ var storedConfirmationPolicySchema = external_exports.object({
39219
+ cli_mode: cliConfirmationModeSchema
39220
+ }).strict();
39221
+
39196
39222
  // ../../packages/protocol/src/aicos.ts
39197
39223
  var uuidSchema5 = external_exports.string().uuid();
39198
39224
  var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.unknown());
@@ -39307,7 +39333,12 @@ var aicosPendingTurnSchema = external_exports.object({
39307
39333
  linkedRunId: uuidSchema5.nullable(),
39308
39334
  interactiveDeadline: external_exports.string().datetime({ offset: true }),
39309
39335
  terminalReason: external_exports.string().min(1).max(120).nullable().default(null),
39336
+ claimLatencyMs: external_exports.number().int().nonnegative().nullable().default(null),
39337
+ runtimeDurationMs: external_exports.number().int().nonnegative().nullable().default(null),
39310
39338
  totalDurationMs: external_exports.number().int().nonnegative().nullable().default(null),
39339
+ createdAt: external_exports.string().datetime({ offset: true }),
39340
+ claimedAt: external_exports.string().datetime({ offset: true }).nullable().default(null),
39341
+ startedAt: external_exports.string().datetime({ offset: true }).nullable().default(null),
39311
39342
  updatedAt: external_exports.string().datetime({ offset: true })
39312
39343
  }).strict();
39313
39344
  var aicosMessageRoleSchema = external_exports.enum(["user", "assistant", "tool", "system"]);
@@ -39434,17 +39465,38 @@ var AICOS_ATTACHMENT_CONTENT_TYPES = ["image/png", "image/jpeg", "image/webp", "
39434
39465
  var AICOS_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
39435
39466
  var AICOS_MAX_ATTACHMENTS_PER_TURN = 4;
39436
39467
  var aicosAttachmentContentTypeSchema = external_exports.enum(AICOS_ATTACHMENT_CONTENT_TYPES);
39468
+ var AICOS_ATTACHMENT_READINESS = ["ready", "unavailable", "expired"];
39469
+ var aicosAttachmentReadinessSchema = external_exports.enum(AICOS_ATTACHMENT_READINESS);
39437
39470
  var aicosAttachmentSchema = external_exports.object({
39438
39471
  id: uuidSchema5,
39439
39472
  contentType: aicosAttachmentContentTypeSchema,
39440
39473
  sizeBytes: external_exports.number().int().positive(),
39441
39474
  width: external_exports.number().int().positive().nullable(),
39442
39475
  height: external_exports.number().int().positive().nullable(),
39443
- filename: external_exports.string().max(255).nullable()
39476
+ filename: external_exports.string().max(255).nullable(),
39477
+ // DER-1470: null/absent ref_status reads as "ready" (usable) for back-compat.
39478
+ readiness: aicosAttachmentReadinessSchema.default("ready")
39444
39479
  }).strict();
39445
39480
  var aicosAttachmentUploadResultSchema = external_exports.object({
39446
39481
  attachment: aicosAttachmentSchema
39447
39482
  }).strict();
39483
+ var aicosRunnerAttachmentRefSchema = external_exports.object({
39484
+ id: uuidSchema5,
39485
+ content_type: aicosAttachmentContentTypeSchema,
39486
+ size_bytes: external_exports.number().int().positive(),
39487
+ width: external_exports.number().int().positive().nullable(),
39488
+ height: external_exports.number().int().positive().nullable(),
39489
+ filename: external_exports.string().max(255).nullable(),
39490
+ signed_url: external_exports.string().url().nullable(),
39491
+ signed_url_expires_in_seconds: external_exports.number().int().positive().nullable(),
39492
+ expires_at: external_exports.string().datetime({ offset: true }).nullable(),
39493
+ readiness: aicosAttachmentReadinessSchema
39494
+ }).strict();
39495
+ var AICOS_RUNNER_ATTACHMENT_OUTCOMES = ["used", "expired", "load_failed"];
39496
+ var aicosRunnerAttachmentOutcomeSchema = external_exports.object({
39497
+ id: uuidSchema5,
39498
+ outcome: external_exports.enum(AICOS_RUNNER_ATTACHMENT_OUTCOMES)
39499
+ }).strict();
39448
39500
  var aicosMessageSchema = external_exports.object({
39449
39501
  id: uuidSchema5,
39450
39502
  sessionId: uuidSchema5,
@@ -39486,6 +39538,7 @@ var aicosAgentBuilderMetadataSchema = external_exports.object({
39486
39538
  agentBuilder: agentBuilderCompilationSchema
39487
39539
  }).strict();
39488
39540
  var aicosTurnRequestSchema = external_exports.object({
39541
+ requestId: uuidSchema5,
39489
39542
  sessionId: uuidSchema5.nullable().optional(),
39490
39543
  purpose: aicosConversationPurposeSchema.default("general"),
39491
39544
  // Empty is permitted only when attachments are present (an image-only turn);
@@ -41153,6 +41206,8 @@ var MCP_ONBOARDING_TOOL_NAMES = {
41153
41206
  setCompass: `${MCP_TOOL_PREFIX}_set_compass`,
41154
41207
  staffSeat: `${MCP_TOOL_PREFIX}_staff_seat`
41155
41208
  };
41209
+ var FORGE_TURN_HARD_CEILING_SECONDS = 30 * 60;
41210
+ var FORGE_TURN_PHASE_OVERHEAD_SECONDS = 20 * 60;
41156
41211
 
41157
41212
  // ../../packages/protocol/src/mcp-onboarding.ts
41158
41213
  var MCP_TOKEN_ABSOLUTE_MAX_EXPIRY_MS = (MCP_TOKEN_NO_EXPIRY_SENTINEL_DAYS + 1) * 24 * 60 * 60 * 1e3;
@@ -41340,7 +41395,15 @@ var pendingConfirmationSchema = external_exports.object({
41340
41395
  }).strict()
41341
41396
  }).strict();
41342
41397
  var confirmationApproveInputSchema = external_exports.object({
41343
- confirmation_id: external_exports.string().min(1)
41398
+ confirmation_id: external_exports.string().min(1),
41399
+ // DER-1490 — the auditable "reviewed" signal. The CLI sets it true ONLY after
41400
+ // rendering the confirmation gate to (and, at an interactive TTY, getting an
41401
+ // explicit `y` from) the human. Absent = a silent auto-replay; the server refuses
41402
+ // that path under the tenant's `careful` mode, and for `dangerous` risk in either
41403
+ // mode (see requiresReviewSignal). It is an explicit auditable field, not a TTY
41404
+ // heuristic — the server cannot cryptographically distinguish a human `y` from an
41405
+ // agent replaying a live session token (both arrive as source cli, actor.kind user).
41406
+ reviewed: external_exports.boolean().optional()
41344
41407
  }).strict();
41345
41408
  var confirmationRejectInputSchema = external_exports.object({
41346
41409
  confirmation_id: external_exports.string().min(1),
@@ -41887,7 +41950,7 @@ var fleetDigestAgentSchema = fleetOverviewRowSchema.extend({
41887
41950
  spend_7d_usd: external_exports.string(),
41888
41951
  spend_30d_usd: external_exports.string(),
41889
41952
  cost_drift_percent: external_exports.number().int().nullable(),
41890
- health_status: external_exports.enum(["needs_review", "stale", "setup_gap", "healthy"]),
41953
+ health_status: external_exports.enum(["needs_review", "stale", "setup_gap", "awaiting_data", "healthy"]),
41891
41954
  health_reasons: external_exports.array(external_exports.string()),
41892
41955
  recent_failed_runs: external_exports.array(fleetDigestRunSchema),
41893
41956
  unresolved_errors: external_exports.array(fleetDigestErrorSchema),
@@ -42127,6 +42190,7 @@ var runnerExecutionStateSchema = external_exports.enum([
42127
42190
  "processing",
42128
42191
  "idle_unproven",
42129
42192
  "queued_unclaimed",
42193
+ "model_account_exhausted",
42130
42194
  "missing_runtime",
42131
42195
  "not_connected",
42132
42196
  "revoked",
@@ -42142,8 +42206,9 @@ var runnerExecutionReadinessSchema = external_exports.object({
42142
42206
  recent_completed_at: isoDateTime4.nullable(),
42143
42207
  recent_failed_at: isoDateTime4.nullable()
42144
42208
  }).strict();
42209
+ var runnerSandboxPostureSchema = external_exports.enum(["guard", "strict", "off", "none", "unknown"]);
42145
42210
  var runnerPostureSchema = external_exports.object({
42146
- sandbox: external_exports.enum(["guard", "strict", "off", "none", "unknown"]),
42211
+ sandbox: runnerSandboxPostureSchema,
42147
42212
  network: external_exports.enum(["allow", "deny", "unknown"]),
42148
42213
  capability_set: external_exports.array(external_exports.string())
42149
42214
  }).strict();
@@ -42385,6 +42450,17 @@ var agentPolicyUpdateInputSchema = external_exports.object({
42385
42450
  });
42386
42451
  }
42387
42452
  });
42453
+ var confirmationPolicyDtoSchema = external_exports.object({
42454
+ cli_mode: cliConfirmationModeSchema,
42455
+ explicit: external_exports.boolean()
42456
+ }).strict();
42457
+ var confirmationPolicyGetInputSchema = external_exports.object({}).strict();
42458
+ var confirmationPolicyGetOutputSchema = external_exports.object({
42459
+ confirmation_policy: confirmationPolicyDtoSchema
42460
+ }).strict();
42461
+ var confirmationPolicyUpdateInputSchema = external_exports.object({
42462
+ cli_mode: cliConfirmationModeSchema
42463
+ }).strict();
42388
42464
  var tenantSpendStatusSchema = external_exports.object({
42389
42465
  spend_usd: external_exports.string(),
42390
42466
  soft_cap_usd: external_exports.string().nullable(),
@@ -42880,6 +42956,20 @@ var softwareTerminationBoundSchema = external_exports.object({
42880
42956
  max_wall_clock_seconds: external_exports.number().int().positive().max(7 * 24 * 60 * 60).optional(),
42881
42957
  cost_budget_usd: external_exports.number().nonnegative().max(1e6).optional()
42882
42958
  }).strict();
42959
+ var softwarePhaseSchedulerStateSchema = external_exports.object({
42960
+ status: external_exports.enum(["queued", "parked", "not_configured"]),
42961
+ reason: external_exports.enum([
42962
+ "phase_run_not_found",
42963
+ "missing_owning_seat",
42964
+ "missing_runner_agent",
42965
+ "runner_not_paired",
42966
+ "work_order_expired",
42967
+ "runner_offline"
42968
+ ]).nullable(),
42969
+ message: external_exports.string(),
42970
+ remediation_href: external_exports.string().nullable(),
42971
+ work_order_id: uuid10.nullable()
42972
+ });
42883
42973
  var softwareBuildRequestSummarySchema = external_exports.object({
42884
42974
  id: uuid10,
42885
42975
  software_project_id: uuid10,
@@ -42894,7 +42984,8 @@ var softwareBuildRequestSummarySchema = external_exports.object({
42894
42984
  max_wall_clock_seconds: external_exports.number().int().nullable(),
42895
42985
  cost_budget_usd: external_exports.string().nullable(),
42896
42986
  iterations_used: external_exports.number().int(),
42897
- created_at: external_exports.string()
42987
+ created_at: external_exports.string(),
42988
+ scheduler_state: softwarePhaseSchedulerStateSchema.nullable()
42898
42989
  });
42899
42990
  var softwarePhaseRunSummarySchema = external_exports.object({
42900
42991
  id: uuid10,
@@ -42903,7 +42994,8 @@ var softwarePhaseRunSummarySchema = external_exports.object({
42903
42994
  attempt_no: external_exports.number().int(),
42904
42995
  entered_at: external_exports.string().nullable(),
42905
42996
  exited_at: external_exports.string().nullable(),
42906
- created_at: external_exports.string()
42997
+ created_at: external_exports.string(),
42998
+ scheduler_state: softwarePhaseSchedulerStateSchema.nullable()
42907
42999
  });
42908
43000
  var softwareGateSummarySchema = external_exports.object({
42909
43001
  id: uuid10,
@@ -43054,7 +43146,8 @@ var softwareRequestCreateOutputSchema = external_exports.object({
43054
43146
  software_project_id: uuid10,
43055
43147
  status: softwareRequestStatusSchema,
43056
43148
  current_phase: softwarePhaseSchema,
43057
- phase_run_id: uuid10
43149
+ phase_run_id: uuid10,
43150
+ scheduler_state: softwarePhaseSchedulerStateSchema
43058
43151
  });
43059
43152
  var softwareRequestListInputSchema = external_exports.object({
43060
43153
  limit: external_exports.number().int().positive().max(200).optional()
@@ -44175,6 +44268,13 @@ var issueEventPayloadSchema = external_exports.object({
44175
44268
  severity: external_exports.enum(["low", "med", "high", "critical"])
44176
44269
  }).strict();
44177
44270
 
44271
+ // ../../packages/protocol/src/held-tool-action.ts
44272
+ var heldToolActionReplaySchema = external_exports.object({
44273
+ tool_name: external_exports.string().min(1),
44274
+ args: external_exports.record(external_exports.string(), external_exports.unknown()),
44275
+ run_id: external_exports.string().uuid()
44276
+ }).strict();
44277
+
44178
44278
  // ../../packages/protocol/src/operating.ts
44179
44279
  var uuidSchema17 = external_exports.string().uuid();
44180
44280
  var taskListInputSchema = external_exports.object({}).strict();
@@ -44340,7 +44440,12 @@ var escalationDecisionOutputSchema = external_exports.object({
44340
44440
  status: escalationStatusSchema,
44341
44441
  decision_id: uuidSchema18,
44342
44442
  decided_by: uuidSchema18,
44343
- issue_id: uuidSchema18.nullable()
44443
+ issue_id: uuidSchema18.nullable(),
44444
+ // DER-1478: true when the resolved escalation carries a durable replay_action
44445
+ // (an approval-gated connector write) that the approvals path should actuate
44446
+ // after this decision is recorded. Omitted (not false) for non-connector
44447
+ // escalations to keep existing callers' output shape unchanged.
44448
+ replay_action_pending: external_exports.boolean().optional()
44344
44449
  }).strict();
44345
44450
 
44346
44451
  // ../../packages/protocol/src/index.ts
@@ -44766,7 +44871,7 @@ The Compass is drafted, then activated by a human through supersession.
44766
44871
  order: 15,
44767
44872
  title: "AICOS chat guide",
44768
44873
  summary: "How the AI Chief of Staff chat works in the authenticated app shell and what it can safely do today.",
44769
- version: "2026-07-08.1",
44874
+ version: "2026-07-10.2",
44770
44875
  public: true,
44771
44876
  audiences: ["human", "in_app_agent"],
44772
44877
  stages: ["company_setup", "operating_rhythm"],
@@ -44834,10 +44939,12 @@ Each turn carries server-normalized page context: route template, current path,
44834
44939
 
44835
44940
  The current chat harness shows the effective lane and model. Cloud is the default and remains the enforced lane during onboarding, even if a post-onboarding lane has been selected. After onboarding, an owner or admin can select cloud, runner, or MCP only when the server says that lane is ready. Lane changes update the governed AICOS agent seat and write an append-only audit event; the browser never decides tenant authority or runner/MCP readiness.
44836
44941
 
44837
- AICOS also has an explicit brain selection. Cloud can use the included managed allowance or a tenant Anthropic key (BYOK). Runner can target the paired machine's Claude subscription or Codex subscription. MCP is labeled as the MCP client. Read this selection with \`aicos.brain_settings.get\`; owners can update it with \`aicos.brain_settings.update\` (CLI flags: \`--lane\`, \`--cloud-brain managed|byok\`, \`--runner-brain claude-cli|codex-cli\`). BYOK requires an active tenant Anthropic key. Runner and MCP answers are labeled in the transcript and do not consume the managed cloud allowance.
44942
+ AICOS also has an explicit brain selection. Cloud can use the included managed allowance or a tenant Anthropic key (BYOK). Runner brain preference still records Claude or Codex, but interactive AICOS runner turns are currently gated to Claude while the Codex path remains unverified. MCP is labeled as the MCP client. Read this selection with \`aicos.brain_settings.get\`; owners can update it with \`aicos.brain_settings.update\` (CLI flags: \`--lane\`, \`--cloud-brain managed|byok\`, \`--runner-brain claude-cli|codex-cli\`). BYOK requires an active tenant Anthropic key. Choosing \`codex-cli\` for AICOS runner mode is currently rejected with a precondition error until the governed interactive runner path is verified. Runner and MCP answers are labeled in the transcript and do not consume the managed cloud allowance. If the selected runner reports that its active local model account has exhausted its allowance, Settings surfaces **Allowance exhausted** before the user sends another AICOS turn so they can switch lanes, switch runner accounts, or keep working manually instead of waiting on a doomed runner round-trip.
44838
44943
 
44839
44944
  Runner mode uses the same AICOS sessions and transcript. Interactive chat turns are tracked separately from scheduled work orders: each user message has one user-owned turn execution with a short deadline, so two users' chat messages cannot collapse into the same scheduled-work slot. Scheduled and Forge runner work still use work orders. Runner claim packets carry ids and execution metadata, not the user's free-text request. After claim, the local runner calls the governed AICOS context-load tool, which re-checks seat scope and returns the bounded transcript, session summary, route context, tenant clock, grounding text, and source counts authorized for that turn. For execute-ready runners, the claim also carries a server-built Seat work contract that names the governed runtime profile, the AICOS context-loader tool, and the Seat's permission manifest; action attempts still route through command guards, tool-call audit, and pending confirmations. The runner treats loaded tenant clock, context, and contract as authoritative instead of substituting its own runtime clock, local files, or self-invented authority. A runner must be paired, execute-ready, and backed by the live AICOS agent before it can be selected.
44840
44945
 
44946
+ Cloud and runner submissions carry a stable request ID. Distinct requests that compete while a turn is active receive HTTP \`409\` with code \`AICOS_TURN_CONFLICT\`; the losing request writes no partial user message and consumes none of its attachments. Runner acquisition commits its user message, execution, attachment links, queued transcript projection, and session snapshot together, so a failed projection leaves none of them half-written. Retrying the same request ID returns the original turn and does not rerun the model, builder, onboarding, or runner-queue side effects, even if the currently selected lane or runner readiness changed after the original submission.
44947
+
44841
44948
  When a runner claims an interactive AICOS turn, the runner reports start and final result back to {{brand}}. The turn row records claim/start/finish timing, terminal reason, model label, and token counts when the runner or cloud path provides them. The server appends exactly one assistant transcript message for the result, links that message to run evidence, and treats duplicate or late result reports as idempotent. If the runner answer includes a validated in-app link, the web panel can surface the same consent-required navigation suggestion once polling sees that the pending turn is terminal. If a queued, claimed, or running interactive turn passes its short deadline before a runner writes back, the turn is marked offline and the transcript receives a terminal assistant message instead of leaving a permanent "queued" placeholder.
44842
44949
 
44843
44950
  While a runner turn is queued, claimed, or running, the chat panel keeps showing the pending state and polls the session until the answer or terminal status arrives. The composer, model picker, and lane picker are disabled during that active turn so a second browser tab or direct API call cannot overlap another runner turn in the same thread. Terminal runner statuses such as failed, offline, or canceled stay visible only while they are still the latest user turn; if the user continues the conversation in another lane, the stale terminal marker is no longer projected at the bottom of the thread.
@@ -44852,7 +44959,7 @@ You can attach images to a message from the composer. {{brand}} accepts PNG, JPE
44852
44959
 
44853
44960
  AICOS can also retrieve bounded summaries from prior sessions owned by the same tenant user and surface them as session-memory read models. Setup-limited users do not retrieve sensitive historical run or company-memory summaries through this path. This is a practical memory seam for AICOS continuity; it is not yet the future Company Brain, and it does not make unaccepted knowledge authoritative.
44854
44961
 
44855
- Platform allowance and BYOK state are checked before routing; an exhausted included allowance keeps AICOS in guided mode and points the user toward BYOK, billing, or manual continuation. The chat transcript and Settings surface the exhaustion reason and provide direct paths to add a tenant Anthropic key or review the plan. Because the DER-1068 implementation still uses the deterministic harness, it does not create fake llm_calls rows or settle fake allowance usage; reservation/settlement belongs to the real provider call path.
44962
+ Platform allowance and BYOK state are checked before routing; an exhausted included allowance keeps AICOS in guided mode and points the user toward BYOK, billing, or manual continuation. The chat transcript and Settings surface the exhaustion reason and provide direct paths to add a tenant Anthropic key or review the plan. Cloud AICOS provider attempts write canonical run evidence and one linked \`llm_calls\` row with source metadata \`aicos_chat\`, prompt version, latency, token usage, cost, key source, and typed outcome. Managed-key turns consult the tenant hard cap before provider execution; BYOK turns never fall back to the managed platform key. Provider timeouts, provider server errors, empty output, and BYOK credential or allowance failures stay visible as terminal failed turns with retry or Settings recovery instead of being converted into a successful generic answer.
44856
44963
 
44857
44964
  During onboarding, the deterministic harness may execute only explicit low-risk draft actions. In the Responsibility Graph step, a user with setup/action authority can ask AICOS to create one named draft seat per turn; the seat stays vacant, is traceable to the AICOS message through artifact sources, and still needs human review before staffing, Charters, manifests, or go-live. Compass and Charter help remains draft guidance and clarifying questions until the live drafting planner lands. AICOS never silently finishes onboarding, approves Compass or Charter content, signs manifests, goes live, stores credentials, changes autonomy policy, or invites external people; those actions remain human-confirmed.
44858
44965
 
@@ -44915,7 +45022,7 @@ Treat AICOS as a coordinator over the operating system. It can become more usefu
44915
45022
  order: 20,
44916
45023
  title: "Responsibility Graph playbook",
44917
45024
  summary: "How to build a functions-first graph with seats, owners, Stewards, vacancies, and clean authority.",
44918
- version: "2026-07-07.2",
45025
+ version: "2026-07-10.1",
44919
45026
  public: true,
44920
45027
  audiences: ["human", "cli", "mcp", "in_app_agent"],
44921
45028
  stages: ["graph_design", "staffing"],
@@ -44959,7 +45066,7 @@ The graph always resolves to a single canonical root seat (Founder & CEO in the
44959
45066
 
44960
45067
  AICOS always stays a peer of the root (never a child of its steward) and never appears as an orphan in the unassigned set \u2014 this is intentional and not a data-quality issue.
44961
45068
 
44962
- Seats that do not reach the single root through the resolved parent chain are listed as **unassigned** (with \`unassigned: true\` and no incoming edge). Each \`graph.get\` node carries an \`unassigned\` boolean, and the CLI \`rost graph show\` output prints unassigned seats under a separate \`Unassigned\` section so operators can see which parts of the chart need attention. The web canvas parks unassigned seats at the bottom of the chart instead of inventing fake root edges.
45069
+ Seats that do not reach the single root through the resolved parent chain are listed as **unassigned** (with \`unassigned: true\` and no incoming edge). Each \`graph.get\` node carries an \`unassigned\` boolean, and the CLI \`rost graph show\` output prints unassigned seats under a separate \`Unassigned\` section so operators can see which parts of the chart need attention. The web canvas parks unassigned seats at the bottom of the chart instead of inventing fake root edges, and when any exist a floating **Unassigned seats \u2014 needs attention** panel lists them by name; selecting one flies the canvas to that seat so the gap is one click from a fix, not just visible in the baseline row.
44963
45070
 
44964
45071
  ## Forge Developer Team seats are pinned
44965
45072
 
@@ -44992,7 +45099,9 @@ The graph also carries a mission-control panel. It pulls the same operational st
44992
45099
 
44993
45100
  ## An agent seat's profile
44994
45101
 
44995
- A staffed agent's seat page opens on a profile built to answer one question in ten seconds: what is this agent, what does it know, what can it touch, and who is accountable. The hero shows live/paused/draft status, a one-line description, and when it last ran; a right-hand fact sheet stays visible on every tab with the Charter version, Skills, Connections (with a "connect" prompt when a Skill needs a provider that is not yet linked), an Access summary, and tool/held/model counts. Four tabs organize the rest: **Overview** (recent runs as one-line summaries, 30-day cost by model, and a version-history feed that tracks the agent itself as an actor alongside human Charter decisions), **Charter** (the accepted Charter rendered as a document \u2014 purpose, responsibilities, owned measurables, decision authority \u2014 with its version chip, signer, and an Amend action that opens the same draft-and-confirm amendment flow as the Charter Builder), **Access** (plain-language cards for what the agent does while you are watching versus running unattended, then every granted tool grouped under its Connection with a one-line description and Read/Draft/Held status), and **Activity** (the full Trust Card below). AICOS's seat keeps its own operations panel instead of this profile, because it is the company's foundation seat rather than a generic staffed agent.
45102
+ A staffed non-AICOS agent's seat page uses one profile shell with eight URL-addressable tabs: **Overview**, **Charter**, **Tools & connections**, **Skills**, **Autonomy**, **Signals**, **Activity**, and **Settings**. The hero shows status, Charter purpose, Steward, seat path, lane, model, and last run. The sidebar keeps access, connection, tool/Skill/Signal/escalation/Charter counters, and schedule facts visible while the tabs organize the existing read models and controls. Legacy links such as \`#agent-ops\`, \`#recent-runs\`, and \`#run-<id>\` select the matching Settings or Activity tab. Human seats and the AICOS foundation seat keep their dedicated pages. Members without sensitive-data access see the same shell with explicit unavailable states for run, tool-call, cost, connection-status, and escalation facts; the page never broadens Trust Card access or turns an unavailable read into a confident empty/disconnected claim.
45103
+
45104
+ Agent creation at \`/agents/new\` begins with four doors: **Describe the job** opens the existing in-app builder, **Start from a template** opens the stock-template picker, **Build manually** opens the custom setup path, and **Import** opens an in-app format explainer. Definition-file import is roadmap-only and never opens a bare file picker; operators can instead continue to the existing-agent pairing flow. Existing \`?mode=template|custom|existing\` and \`?seat=\` resume links continue directly to their current flows.
44996
45105
 
44997
45106
  ## Review an agent seat's delivered work
44998
45107
 
@@ -45575,7 +45684,7 @@ Use the lower-level commands after health names a finding: \`agent.get_run\` for
45575
45684
  order: 46,
45576
45685
  title: "Tool access and vault",
45577
45686
  summary: "How to give agents access to tools without exposing raw credentials or expanding authority by accident.",
45578
- version: "2026-07-02.3",
45687
+ version: "2026-07-02.4",
45579
45688
  public: true,
45580
45689
  audiences: ["human", "cli", "mcp", "in_app_agent"],
45581
45690
  stages: ["staffing"],
@@ -45633,7 +45742,7 @@ Google can be connected from Settings once the workspace OAuth app is configured
45633
45742
 
45634
45743
  CLI and MCP can inspect connector readiness without seeing secrets: \`integration.list\` / \`rost_list_integrations\` lists connected providers and health metadata, \`integration.status\` / \`rost_get_integration_status\` reads one provider by id or name, and \`integration.test\` / \`rost_test_integration_connection\` runs the installed provider-specific health check. For Google, the test refreshes the vaulted OAuth credential and reads the Gmail profile, then records only account, scope, and health metadata.
45635
45744
 
45636
- \`integration.connect_rest\` / \`rost_connect_rest_integration\` creates or rotates the supported Baserow REST integration through the credential flow. It stores the token in the vault, persists only endpoint/account metadata plus a tenant-scoped integration row, marks the adapter as \`rest\`, and returns no vault ref or secret. Because the input includes a raw secret, generated CLI argv refuses the command; use Settings or the command API credential flow instead of shell arguments.
45745
+ \`integration.connect_rest\` creates or rotates the supported Baserow REST integration through the credential flow. It stores the token in the vault, persists only endpoint/account metadata plus a tenant-scoped integration row, marks the adapter as \`rest\`, and returns no vault ref or secret. Because the input includes a raw secret, generated CLI argv refuses the command and it is deliberately not exposed as an agent-callable MCP tool; run it from Settings or the command API credential flow instead of shell arguments.
45637
45746
 
45638
45747
  \`integration.readiness\` / \`{{cli}} integration readiness --provider google --json\` / \`rost_check_integration_readiness\` returns the operator setup checklist: app configuration status, tenant connection state, granted scopes, latest test state, the demo-safe partner path, external verification/CASA caveats, handler availability, and the next operator action. The checklist is metadata-only. It never returns access tokens, refresh tokens, client secrets, vault refs, or raw provider responses. Settings deliberately keeps the live Google card narrower: connected account, last successful test, granted capabilities, and connect/reconnect/test actions.
45639
45748
 
@@ -45653,7 +45762,7 @@ There is exactly one way to give a connected tool its credential, and it is the
45653
45762
 
45654
45763
  - Stage tools on a draft agent: \`agent.configure_tools\` / \`rost_configure_agent_tools\` \u2014 connect or decline proposed tools and stage credential-ingress requests. Pass vault references, never raw secret material.
45655
45764
  - Inspect connector readiness: \`integration.list\` / \`rost_list_integrations\`, \`integration.status\` / \`rost_get_integration_status\`, and \`integration.test\` / \`rost_test_integration_connection\` return provider metadata and health only.
45656
- - Store a secret: \`credential.ingress\` / \`rost_store_credential\` (scope: seat) persists only a vault reference. \`credential.ingress\` is \`credential_flow\` \u2014 it returns a pending confirmation and runs only with a real human-provided secret, captured as a vault reference. (Because it redacts that secret, the pending confirmation also shows a high-risk badge \u2014 see the confirmations guide for badge-versus-level.)
45765
+ - Store a secret: \`credential.ingress\` (scope: seat) persists only a vault reference. It is \`credential_flow\` \u2014 because it carries a real human-provided secret it runs only through the Settings vault-ingress flow (a human session); generated CLI argv refuses it and it is deliberately not an agent-callable MCP tool. (Because it redacts that secret, its confirmation also shows a high-risk badge \u2014 see the confirmations guide for badge-versus-level.)
45657
45766
  - Sign the manifest: \`charter.sign_manifest\` / \`rost_sign_charter_manifest\` requests human confirmation for the seat's permission manifest.
45658
45767
  - Mint local access: prefer \`{{cli}} mcp install\` for users; \`mcp_token.create\` is \`human_required\` and returns the token once. List metadata with \`mcp_token.list\` (never token material); revoke with \`mcp_token.revoke\`.
45659
45768
 
@@ -45721,7 +45830,7 @@ External connectors are being rolled out provider by provider, conservatively (r
45721
45830
  order: 48,
45722
45831
  title: "CLI and MCP installation guide",
45723
45832
  summary: "Install the public CLI, register remote token-backed MCP clients, and find the full command and tool catalog.",
45724
- version: "2026-07-05.7",
45833
+ version: "2026-07-10.2",
45725
45834
  public: true,
45726
45835
  audiences: ["human", "cli", "mcp", "in_app_agent"],
45727
45836
  stages: ["company_setup", "staffing"],
@@ -46082,7 +46191,7 @@ A leaked tenant-admin token can administer the whole tenant, not just one seat.
46082
46191
 
46083
46192
  ### Storing the Anthropic key and other credentials
46084
46193
 
46085
- Storing the tenant model key or any other secret goes through **credential ingress** as a vault reference \u2014 the secret is never pasted into a prompt, config, or log. Use \`rost_save_tenant_anthropic_key\` (\`tenant.anthropic_key.save\`) for the tenant Anthropic key, or \`rost_store_credential\` (\`credential.ingress\`) for other secrets; \`rost_configure_agent_tools\` stages credential requests but never accepts raw secrets. Credential storage is a gated \`credential_flow\` confirmation that a human approves. For the vault model and the security posture behind this, see the security-model-guide and the tool-access-and-vault guide; for model-bound data, BYOK provider handling, and local-client provider settings, see the ai-model-data-handling-guide; for the confirmation gate, see the confirmations-guide.
46194
+ Storing the tenant model key or any other secret goes through **credential ingress** as a vault reference \u2014 the secret is never pasted into a prompt, config, or log. Use \`rost_save_tenant_anthropic_key\` (\`tenant.anthropic_key.save\`) for the tenant Anthropic key; other secrets go through the Settings vault-ingress flow (\`credential.ingress\`), a human-only interactive flow that is deliberately not an agent-callable MCP tool and refuses raw secrets over CLI argv. \`rost_configure_agent_tools\` stages credential *requests* (provider, scope, and label only) but never accepts raw secrets. Credential storage is a gated \`credential_flow\` confirmation that a human approves. For the vault model and the security posture behind this, see the security-model-guide and the tool-access-and-vault guide; for model-bound data, BYOK provider handling, and local-client provider settings, see the ai-model-data-handling-guide; for the confirmation gate, see the confirmations-guide.
46086
46195
 
46087
46196
  ## When to use MCP or CLI
46088
46197
 
@@ -46187,7 +46296,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
46187
46296
  | \`{{cli}} integration list|readiness|status|test\` / credential flow for Baserow REST connect | \`integration.connect_rest\`, \`integration.list\`, \`integration.readiness\`, \`integration.status\`, \`integration.test\` | Create a vault-backed Baserow REST integration through the credential flow, list connector metadata, read setup-readiness, read one connector's health, and run provider-specific tests without exposing credentials. | Tenant-admin for connect; tenant for reads/tests | \`{{cli}} integration readiness --provider google --json\` |
46188
46297
  | \`{{cli}} system health\` | \`system.health\` | Read the scoped system-health snapshot across agent runs, unresolved errors, Signals, Cascade setup, work loop, integrations, runners, notifications, and Sync Brief readiness. | Tenant, member, or seat token | \`{{cli}} system health --json\`; \`{{cli}} system health --seat <id> --json\` |
46189
46298
  | \`{{cli}} command usage.report|usage.snapshot\` | \`usage.report\`, \`usage.snapshot\` | Read the AI-workforce ROI report (per-period agent turns, token/run cost, human-accepted value, exceptions, approvals) from immutable usage snapshots, and freeze a period's totals into a new snapshot (insert-only, idempotent). | Tenant | \`{{cli}} command usage.report --json\`; \`{{cli}} command usage.snapshot --json\` |
46190
- | \`{{cli}} settings get|update|product-learning|agent-policy|rename\` | \`settings.get\`, \`settings.update\`, \`settings.product_learning.get\`, \`settings.product_learning.update\`, \`settings.agent_policy.get\`, \`settings.agent_policy.update\`, \`tenant.rename\` | Read tenant settings, update budget caps, read or update product-learning participation, read or set the company autonomy ceiling, and rename the company. Budget, product-learning, and policy updates stop at human confirmation in non-interactive CLI/MCP sessions; company rename is owner-only and human-gated. | Tenant / Tenant-admin for updates | \`{{cli}} settings product-learning get --json\`; \`{{cli}} settings agent-policy get --json\`; \`{{cli}} settings rename --company "Acme Operations"\` |
46299
+ | \`{{cli}} settings get|update|product-learning|agent-policy|confirmation-mode|rename\` | \`settings.get\`, \`settings.update\`, \`settings.product_learning.get\`, \`settings.product_learning.update\`, \`settings.agent_policy.get\`, \`settings.agent_policy.update\`, \`settings.confirmation_policy.get\`, \`settings.confirmation_policy.update\`, \`tenant.rename\` | Read tenant settings, update budget caps, read or update product-learning participation, read or set the company autonomy ceiling, read or set the CLI confirmation mode, and rename the company. Budget, product-learning, and policy updates stop at human confirmation in non-interactive CLI/MCP sessions; company rename is owner-only and human-gated. | Tenant / Tenant-admin for updates | \`{{cli}} settings product-learning get --json\`; \`{{cli}} settings agent-policy get --json\`; \`{{cli}} settings confirmation-mode get --json\`; \`{{cli}} settings rename --company "Acme Operations"\` |
46191
46300
  | \`{{cli}} member invite|update|remove\` | \`member.invite\`, \`member.update\`, \`member.remove\` | Manage tenant members; execution requires an owner or admin membership. | Tenant | \`{{cli}} member invite --email ops@example.com --role member\` |
46192
46301
  | \`{{cli}} agent templates|create|setup|tools|dry-run|go-live|status|run-now|fleet-digest|get-run|show\` | \`agent_template.list\`, \`agent.create_from_template\`, \`agent.create_custom\`, \`agent_setup.get\`, \`agent_setup.update\`, \`agent.configure_tools\`, \`agent.run_dry_run\`, \`agent.go_live\`, \`agent.status\`, \`agent.run_now\`, \`agent.fleet_digest\`, \`agent.get_run\`, \`agent.show_markdown\` | Run the full agent setup and operation flow: list templates, create a draft from a template or guided custom answers (with \`--model\` and \`--effort\`), read or answer setup state, connect or decline tools, dry-run, go live, run on demand, capture the fleet-health digest, read one run's transcript/error diagnostics, and show a markdown readout. Human CLI dry-runs print the rehearsal transcript and, when present, the per-tool preview labels (\`Would run\`, \`Blocked\`, \`Escalated\`) before go-live. Create and go-live stop at human gates; the dry-run is ungated by human approval but requires a signed manifest first. | Tenant and seat | \`{{cli}} agent fleet-digest --json\` |
46193
46302
  | \`{{cli}} tools list\` | \`tool.catalog\` | List the discoverable tool catalog the builder reads (id, scope tiers, credential requirement, access policy, and execution-boundary guidance). | Tenant | \`{{cli}} tools list --json\` |
@@ -46200,7 +46309,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
46200
46309
  | \`{{cli}} deliverable list|get|create|attach\` | \`deliverable.list\`, \`deliverable.get\`, \`deliverable.create\`, \`deliverable.attach\` | List, get, create, or attach agent deliverables for a seat. Deliverables are durable work outputs visible across UI, CLI, and MCP. | Tenant (list/get), Seat (create/attach) | \`{{cli}} deliverable list --seat-id <id> --json\`; \`{{cli}} deliverable create --title "Brief" --kind brief\` |
46201
46310
  | \`{{cli}} graph show\` | \`graph.get\` | Print a table view of the Responsibility Graph with explicit \`seat_id\` and \`parent_seat_id\` columns. | Tenant | \`{{cli}} graph show\` |
46202
46311
  | \`{{cli}} seat list|get|create|rename|reparent|decommission\` | \`graph.get\`, \`seat.get\`, \`seat.create\`, \`seat.rename\`, \`seat.reparent\`, \`seat.decommission\`, \`seat.decommission_preview\` | Work with seats as first-class CLI primitives. \`seat decommission --dry-run\` previews affected occupancies, agents, Charters, tokens, credentials, work orders, and schedules before the human-gated teardown. | Tenant / Seat for targeted mutations | \`{{cli}} seat list\`; \`{{cli}} seat decommission --seat-id <id> --dry-run\` |
46203
- | \`{{cli}} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume\` | \`software_factory.project.create\`, \`software_factory.project.list\`, \`software_factory.request.create\`, \`software_factory.request.list\`, \`software_factory.request.show\`, \`software_factory.request.pause\`, \`software_factory.request.cancel\`, \`software_factory.request.resume\` | Create/list Forge projects, open/read governed Forge build requests, and control request lifecycle. Project creation plus pause/cancel/resume are tenant-admin, entitlement-gated, and human-gated; reads and request creation require the Forge add-on. Pause halts new runner claims at the next task checkpoint, cancel drives terminal canceled, and resume requeues from the last completed task checkpoint. | Tenant / Tenant-admin for create and lifecycle controls | \`{{cli}} forge project-create --name "Leiluna app" --base-branch main\`; \`{{cli}} forge request-create --project <slug-or-name> --title "Add invoice export"\` (accepts \`--software-project-id <id>\` instead of \`--project\` too); \`{{cli}} forge request-pause --build-request-id <id> --reason "Operator hold"\` |
46312
+ | \`{{cli}} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume\` | \`software_factory.project.create\`, \`software_factory.project.list\`, \`software_factory.request.create\`, \`software_factory.request.list\`, \`software_factory.request.show\`, \`software_factory.request.pause\`, \`software_factory.request.cancel\`, \`software_factory.request.resume\` | Create/list Forge projects, open/read governed Forge build requests, and control request lifecycle. Request create/list/show return \`scheduler_state\`: \`queued\` when a runner work order exists, \`parked\` when the Forge team is staffed but no paired online runner can claim it, and \`not_configured\` when the runner-lane Forge Seat is missing. Parked and not-configured states link to \`/settings/runners/setup\`; runner pairing or the repair sweep re-enqueues the same pending intake idempotently. Project creation plus pause/cancel/resume are tenant-admin, entitlement-gated, and human-gated; reads and request creation require the Forge add-on. | Tenant / Tenant-admin for create and lifecycle controls | \`{{cli}} forge project-create --name "Leiluna app" --base-branch main\`; \`{{cli}} forge request-create --project <slug-or-name> --title "Add invoice export"\` (accepts \`--software-project-id <id>\` instead of \`--project\` too); \`{{cli}} forge request-pause --build-request-id <id> --reason "Operator hold"\` |
46204
46313
 
46205
46314
  Skills wrapper help:
46206
46315
 
@@ -46293,8 +46402,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46293
46402
  | \`rost_skip_charter_draft\` | \`charter.skip\` | Mark a Charter draft as skipped. | Seat or tenant-admin | Call only when a human defers the seat. |
46294
46403
  | \`rost_apply_charter_seat_type_recommendation\` | \`charter.apply_seat_type_recommendation\` | Apply a Charter's seat-type recommendation. | Seat or tenant-admin | Call after reviewing the draft. |
46295
46404
  | \`rost_sign_charter_manifest\` | \`charter.sign_manifest\` | Request human confirmation for a permission manifest. | Seat or tenant-admin | Use after tool permissions are reviewed. |
46296
- | \`rost_approve_pending_confirmation\` | \`confirmation.approve\` | Approve a pending confirmation. | Tenant | Call with \`{"confirmation_id":"<confirmation-id>"}\`. |
46297
- | \`rost_reject_pending_confirmation\` | \`confirmation.reject\` | Reject a pending confirmation. | Tenant | Call with \`{"confirmation_id":"<confirmation-id>"}\`. |
46298
46405
  | \`rost_draft_compass\` | \`compass.draft\` | Create a Compass draft. | Tenant | Call with a concise Compass document. |
46299
46406
  | \`rost_update_compass_draft\` | \`compass.update_draft\` | Replace a Compass draft document. | Tenant | Call with the \`compass_version_id\` from \`compass.draft\` as \`draft_id\` and the updated document. |
46300
46407
  | \`rost_approve_compass_version\` | \`compass.approve_version\` | Activate a Compass version by supersession. | Tenant | Call after human review. |
@@ -46304,7 +46411,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46304
46411
  | \`rost_assign_user_to_seat\` | \`staffing.assign_user\` | Assign a human user occupancy to a seat. | Seat or tenant-admin | Call with \`seat_id\` and \`user_id\`. |
46305
46412
  | \`rost_assign_dry_run_agent_to_seat\` | \`staffing.assign_agent_dry_run\` | Assign an agent occupancy in dry-run mode. | Seat or tenant-admin | Call only after Steward chain is clear. |
46306
46413
  | \`rost_staff_seat\` | \`staffing.assign\` | Compatibility helper for human, agent, or hybrid staffing. | Seat or tenant-admin | Call with \`seat_id\` and \`occupant\`. |
46307
- | \`rost_store_credential\` | \`credential.ingress\` | Store a secret through the vault and persist only a vault reference. | Seat or tenant-admin | Use vault-backed values only. |
46308
46414
  | \`rost_go_live\` | \`agent.go_live\` | Promote a dry-run agent seat to live. | Seat or tenant-admin | Call after human approval. |
46309
46415
  | \`rost_create_mcp_token\` | \`mcp_token.create\` | Mint a tenant-admin or seat-scoped MCP token. | Tenant | Prefer \`{{cli}} mcp install\` for users. |
46310
46416
  | \`rost_revoke_mcp_token\` | \`mcp_token.revoke\` | Revoke an MCP token immediately. | Tenant | Call with \`token_id\`. |
@@ -46358,7 +46464,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46358
46464
  | \`rost_resolve_error_log\` | \`error_log.resolve\` | Acknowledge or resolve an error log with a required human reason and optional task, issue, or PR link. | Tenant | Call with \`error_log_id\`, \`reason\`, and \`disposition\` \`acknowledged\` or \`resolved\`. |
46359
46465
  | \`rost_supersede_error_log\` | \`error_log.supersede\` | Mark an older error log as superseded by a newer in-tenant error log. | Tenant | Call with \`error_log_id\`, \`new_error_log_id\`, and \`reason\`. |
46360
46466
  | \`rost_list_integrations\` | \`integration.list\` | List connected integration metadata and latest health state. | Tenant | Call with \`{}\` or \`{"provider":"google"}\`; no secrets or vault refs are returned. |
46361
- | \`rost_connect_rest_integration\` | \`integration.connect_rest\` | Create or rotate the vault-backed Baserow REST integration for Signal pulls. | Tenant-admin | Use the credential flow with provider \`baserow\`, HTTPS endpoint, scope, secret name, and secret; no vault refs or secrets are returned. |
46362
46467
  | \`rost_check_integration_readiness\` | \`integration.readiness\` | Return the connector setup checklist: OAuth env/callback, tenant connection, scopes, latest test state, external verification/CASA, handler availability, and DER-coded next actions. | Tenant | Call with \`{"provider":"google"}\`; metadata only, no secrets or vault refs. |
46363
46468
  | \`rost_get_integration_status\` | \`integration.status\` | Read one integration's metadata by provider or integration id. | Tenant | Call with \`{"provider":"google"}\` or \`{"integration_id":"<id>"}\`. |
46364
46469
  | \`rost_test_integration_connection\` | \`integration.test\` | Run the provider-specific connection test and update integration health. | Tenant | Call with \`{"provider":"google"}\`; result is bounded metadata only. |
@@ -46374,8 +46479,10 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46374
46479
  | \`rost_update_product_learning_policy\` | \`settings.product_learning.update\` | Set product-learning mode. | Tenant-admin | Human-gated; call with \`{"mode":"enabled"}\`, \`{"mode":"disabled"}\`, or \`{"mode":"enterprise_contract"}\`. |
46375
46480
  | \`rost_get_company_autonomy_ceiling\` | \`settings.agent_policy.get\` | Read the company autonomy ceiling (Company Guardrails): profile, enforcement, and max_autonomous_risk. | Tenant | Call with \`{}\`; metadata only. |
46376
46481
  | \`rost_set_company_autonomy_ceiling\` | \`settings.agent_policy.update\` | Set the company autonomy ceiling. | Tenant-admin | Owner-only, human-gated; call with \`{"profile":"locked_down"}\` (or \`balanced\`/\`high_autonomy\`, or \`{"profile":"custom","max_autonomous_risk":"high"}\`). Enforced by default. |
46482
+ | \`rost_get_cli_confirmation_mode\` | \`settings.confirmation_policy.get\` | Read the CLI confirmation mode: \`careful\` (the strict default \u2014 the CLI cannot silently auto-approve a pending confirmation) or \`trusted_operator\` (an owner opt-in). | Tenant | Call with \`{}\`; metadata only. |
46483
+ | \`rost_set_cli_confirmation_mode\` | \`settings.confirmation_policy.update\` | Set the CLI confirmation mode. | Tenant-admin | Owner-only, human-gated; call with \`{"cli_mode":"trusted_operator"}\` (or \`{"cli_mode":"careful"}\`). Choosing \`trusted_operator\` establishes an ADR-0018 \xA76 standing authorization; a \`dangerous\`-risk confirmation always requires interactive human review in either mode. |
46377
46484
  | \`rost_get_aicos_brain_settings\` | \`aicos.brain_settings.get\` | Read AICOS lane and brain labels for cloud managed/BYOK, runner Claude/Codex, and MCP client. | Tenant | Call with \`{}\`; returns enum labels only, never keys, vault refs, or runner secrets. |
46378
- | \`rost_update_aicos_brain_settings\` | \`aicos.brain_settings.update\` | Update AICOS lane and cloud/runner brain preferences. | Tenant-admin | Owner-only and human-gated; call with \`{"lane":"runner","runner_brain":"codex-cli"}\` or \`{"cloud_brain":"byok"}\`. BYOK requires an active tenant Anthropic key. |
46485
+ | \`rost_update_aicos_brain_settings\` | \`aicos.brain_settings.update\` | Update AICOS lane and cloud/runner brain preferences. | Tenant-admin | Owner-only and human-gated; call with \`{"lane":"runner","runner_brain":"claude-cli"}\` or \`{"cloud_brain":"byok"}\`. BYOK requires an active tenant Anthropic key. For AICOS, selecting \`codex-cli\` is currently rejected until the governed interactive runner path is verified. |
46379
46486
  | \`rost_list_signals\` | \`signal.list\` | List measurables with their latest reading and on/off-track state. | Seat or tenant-admin | Call with \`{}\` to find measurable ids. |
46380
46487
  | \`rost_get_signal\` | \`signal.get\` | Read a measurable with its full reading history. | Seat or tenant-admin | Call with \`{"measurable_id":"<id>"}\`. |
46381
46488
  | \`rost_confirm_signal_reading\` | \`signal.confirm_reading\` | Confirm an unconfirmed reading as human-verified. | Seat or tenant-admin | Humans confirm; call with \`{"reading_id":"<id>"}\`. |
@@ -46442,13 +46549,13 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46442
46549
  | \`rost_revoke_skill_from_seat\` | \`skill.revoke_from_seat\` | Human-gated revocation that stops future Skill use without deleting historical activations. | Tenant-admin | Call with \`{"assignment_id":"<assignment-id>"}\`; non-interactive callers receive a confirmation handoff. |
46443
46550
  | \`rost_list_model_catalog\` | \`model.catalog\` | List guided model tiers \u2014 recommendations, token prices, cost bands, best-fit work, and model ids for \`--model\`. | Tenant | Call with \`{}\`. |
46444
46551
  | \`rost_create_a_forge_project\` | \`software_factory.project.create\` | Create an active Forge software project before binding repositories or opening build requests. Tenant-admin, human-gated, and entitlement-gated. | Tenant-admin | Call with \`{"name":"Leiluna app","slug":"leiluna-app","base_branch":"main"}\`; non-interactive callers receive a confirmation handoff. |
46445
- | \`rost_create_a_forge_build_request\` | \`software_factory.request.create\` | Open a governed Forge build request against a connected software project; records the request plus its initial intake phase run. Requires the Forge add-on; the title is untrusted display text. | Tenant | Call with \`{"project":"<slug-or-name>","title":"Add invoice export"}\` (or \`{"software_project_id":"<project-id>", ...}\`). |
46552
+ | \`rost_create_a_forge_build_request\` | \`software_factory.request.create\` | Open a governed Forge build request against a connected software project; records the request plus its initial intake phase run and returns \`scheduler_state\` so MCP callers can distinguish queued work from \`parked\` runner setup or \`not_configured\` staffing gaps. Requires the Forge add-on; the title is untrusted display text. | Tenant | Call with \`{"project":"<slug-or-name>","title":"Add invoice export"}\` (or \`{"software_project_id":"<project-id>", ...}\`). |
46446
46553
  | \`rost_pause_a_forge_build_request\` | \`software_factory.request.pause\` | Pause an in-flight Forge build request. New runner claims stop immediately; running work halts at the next task checkpoint. Human-gated. | Tenant-admin | Call with \`{"build_request_id":"<request-id>","reason":"Operator hold"}\`; non-interactive callers receive a confirmation handoff. |
46447
46554
  | \`rost_cancel_a_forge_build_request\` | \`software_factory.request.cancel\` | Cancel a Forge build request and expire active runner work orders. This is terminal; use pause for reversible holds. Human-gated. | Tenant-admin | Call with \`{"build_request_id":"<request-id>","reason":"Out of scope"}\`; non-interactive callers receive a confirmation handoff. |
46448
46555
  | \`rost_resume_a_forge_build_request\` | \`software_factory.request.resume\` | Resume a paused or human-blocked Forge build request from the last completed task checkpoint and requeue the current phase. Human-gated. | Tenant-admin | Call with \`{"build_request_id":"<request-id>","reason":"Budget raised"}\`; non-interactive callers receive a confirmation handoff. |
46449
46556
  | \`rost_list_forge_projects\` | \`software_factory.project.list\` | List active Forge software projects so a tenant can choose a project for build requests, GitHub repository bindings, and config. | Tenant | Call with \`{}\`; use the returned \`id\` as \`software_project_id\`. |
46450
- | \`rost_list_forge_build_requests\` | \`software_factory.request.list\` | The Forge control-room board: build requests with status, current phase, and risk. Read-only; requires the Forge add-on. | Tenant | Call with \`{}\` or \`{"limit":20}\`. |
46451
- | \`rost_show_a_forge_build_request\` | \`software_factory.request.show\` | The Forge request detail: the request, current phase, phase-run history, and gates. Read-only; requires the Forge add-on. | Tenant | Call with \`{"build_request_id":"<request-id>"}\`. |
46557
+ | \`rost_list_forge_build_requests\` | \`software_factory.request.list\` | The Forge control-room board: build requests with status, current phase, risk, and current \`scheduler_state\` remediation. Read-only; requires the Forge add-on. | Tenant | Call with \`{}\` or \`{"limit":20}\`. |
46558
+ | \`rost_show_a_forge_build_request\` | \`software_factory.request.show\` | The Forge request detail: the request, current phase, phase-run history, gates, and current \`scheduler_state\` remediation. Read-only; requires the Forge add-on. | Tenant | Call with \`{"build_request_id":"<request-id>"}\`. |
46452
46559
  | \`rost_advance_a_forge_build_request_phase\` | \`software_factory.phase.advance\` | Advance a build request to its next phase; the server enforces the closed phase state machine and rejects an illegal transition. | Tenant | Call with \`{"build_request_id":"<request-id>","to_phase":"discovery_scoping"}\`. |
46453
46560
  | \`rost_decide_a_forge_gate\` | \`software_factory.gate.decide\` | A human approves or rejects a Forge gate; an agent caller routes to \`/approvals\` and can never self-approve. Sensitive gates record a linked human decision. | Tenant | Call with \`{"gate_id":"<gate-id>","decision":"approve"}\`; non-interactive callers receive a confirmation handoff. |
46454
46561
  | \`rost_answer_forge_plan_clarifications\` | \`software_factory.request.answer_clarification\` | Write structured human answers to a draft plan's clarifying questions and optionally mark it ready for plan review; answers append to the versioned planning artifact and never widen authority or automation mode. | Tenant | Call with \`{"build_request_id":"<request-id>","answers":[{"question":"Which database?","answer":"Postgres"}],"mark_ready_for_plan_review":true}\`. |
@@ -46472,7 +46579,7 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
46472
46579
  | \`rost_create_a_forge_vercel_preview_deployment\` | \`software_factory.vercel.deploy_preview\` | Create a Vercel preview deployment for a linked project after Forge deploy-preview authority, the Vercel OAuth vault-ref connection, and required preview secret/config grants are verified. | Tenant | Call with \`{"software_project_id":"<project-id>","commit_sha":"<sha>","git_ref":"feature/example","seat_id":"<seat-id>"}\`; returns deployment metadata, not tokens. |
46473
46580
  | \`rost_list_forge_runner_capacity\` | \`software_factory.capacity.list\` | List Forge runner capacity observations (advisory scheduling input only, never an authority input). Read-only; requires the Forge add-on. | Tenant | Call with \`{}\` or \`{"runner_id":"<runner-id>"}\`. |
46474
46581
 
46475
- The web Forge control room at \`/forge\` uses the same command path for project selection, build request creation/list/detail reads, request pause/cancel/resume controls, runner capacity, configuration status, and Developer Team install. A request detail page shows phase history, open gates, plan progress, loop/time/cost ceilings, and latest plan-review questions from \`software_factory.request.show\`; answering a clarification, approving a plan, rejecting it, pausing, canceling, or resuming calls the same Forge commands exposed over CLI/MCP. Lifecycle controls deliberately stage pending confirmations before changing runner state. The install button also stages a pending confirmation before durable seats, Skills, authority grants, or live agent activation are applied; the install page states plainly that approving activates the 7 agents at a pull-request-only, observe-first posture and surfaces the company autonomy ceiling.
46582
+ The web Forge control room at \`/forge\` uses the same command path for project selection, build request creation/list/detail reads, request pause/cancel/resume controls, runner capacity, configuration status, and Developer Team install. Request cards and detail pages surface \`scheduler_state\` directly: queued work shows its work-order state, parked work explains that no paired online runner is available, and not-configured work sends the operator to \`/settings/runners/setup\`. A request detail page also shows phase history, open gates, plan progress, loop/time/cost ceilings, and latest plan-review questions from \`software_factory.request.show\`; answering a clarification, approving a plan, rejecting it, pausing, canceling, or resuming calls the same Forge commands exposed over CLI/MCP. Lifecycle controls deliberately stage pending confirmations before changing runner state. The install button also stages a pending confirmation before durable seats, Skills, authority grants, or live agent activation are applied; the install page states plainly that approving activates the 7 agents at a pull-request-only, observe-first posture and surfaces the company autonomy ceiling.
46476
46583
 
46477
46584
  #### Forge GitHub App access
46478
46585
 
@@ -47072,7 +47179,7 @@ Agents can suggest commitments and report progress. They should not create a new
47072
47179
  order: 61,
47073
47180
  title: "Signal guide",
47074
47181
  summary: "How to define and read measurables so the company runs on evidence instead of status theater.",
47075
- version: "2026-07-04.2",
47182
+ version: "2026-07-08.1",
47076
47183
  public: true,
47077
47184
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47078
47185
  stages: ["operating_rhythm"],
@@ -47135,6 +47242,10 @@ Avoid vanity numbers, manual-only status fields, and metrics nobody can act on.
47135
47242
 
47136
47243
  A human can run the whole loop from the Signal page. The page opens answer-first \u2014 one computed line ("2 of 14 measurables need attention," or "All 14 measurables on target" when nothing needs you) with only the exception rows fully visible; healthy measurables collapse to a single "N on target" count you can expand. Every create/edit action lives behind the one "+ Add" button, which opens a drawer with four segments: **Log reading** records a human reading (the same \`signal.correct_reading\` path, or a row's own "Log reading" button seeds the drawer to that measurable), **New measurable** creates one against a seat (the \`measurable.create\` path, plus adopting from the template catalog), **Sources**, and **CSV import**. You do not need an agent to keep Signal current.
47137
47244
 
47245
+ ## Per-seat budget alerts
47246
+
47247
+ The Signal page shows a seat-cost strip: each seat's trailing 30-day agent run spend. On top of that you can set a per-seat soft budget alert \u2014 a spend threshold in USD. When a seat's 30-day spend approaches its threshold (at 80%) or crosses it, a visible alert appears in the cost strip on the scorecard. This alert is advisory only: it never blocks a run or pauses an agent (hard budget enforcement is a separate, later capability). Set a threshold from the cost strip by choosing an agent seat and entering an amount, or leave the amount blank to clear it. Thresholds are per tenant and per seat; setting one records a durable event.
47248
+
47138
47249
  ## Import a scorecard from CSV
47139
47250
 
47140
47251
  To move off a spreadsheet or another operating tool, open the Signal page's "+ Add" drawer and choose the "CSV import" segment (the \`signal.import\` command, also available on CLI/MCP). Each row becomes a measurable owned by the seat whose name matches the row's owner, with its target, unit, direction, cadence, and trailing readings. Import is idempotent \u2014 re-importing upserts by period \u2014 and a row whose cadence conflicts with an existing measurable is reported and skipped, not overwritten.
@@ -47496,11 +47607,11 @@ Do not claim that a live charge happened unless Stripe test/live evidence proves
47496
47607
  order: 72,
47497
47608
  title: "Settings guide",
47498
47609
  summary: "How to use Settings as the control plane for company access, channels, providers, tokens, and operating defaults.",
47499
- version: "2026-07-04.3",
47610
+ version: "2026-07-09.2",
47500
47611
  public: true,
47501
47612
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47502
47613
  stages: ["company_setup", "staffing"],
47503
- relatedCommandIds: ["onboarding.create_invite", "mcp_token.create", "mcp_token.revoke", "mcp_token.list", "integration.list", "integration.readiness", "integration.status", "integration.test", "settings.get", "settings.update", "tenant.rename", "settings.sync_brief_scope.get", "settings.sync_brief_scope.update", "settings.product_learning.get", "settings.product_learning.update", "settings.agent_policy.get", "settings.agent_policy.update", "aicos.brain_settings.get", "aicos.brain_settings.update"],
47614
+ relatedCommandIds: ["onboarding.create_invite", "mcp_token.create", "mcp_token.revoke", "mcp_token.list", "integration.list", "integration.readiness", "integration.status", "integration.test", "settings.get", "settings.update", "tenant.rename", "settings.sync_brief_scope.get", "settings.sync_brief_scope.update", "settings.product_learning.get", "settings.product_learning.update", "settings.agent_policy.get", "settings.agent_policy.update", "settings.confirmation_policy.get", "settings.confirmation_policy.update", "aicos.brain_settings.get", "aicos.brain_settings.update"],
47504
47615
  legal: { publicRisk: "low", notes: ["{{brand}}-native settings guidance."] },
47505
47616
  sources: [
47506
47617
  {
@@ -47521,7 +47632,7 @@ Start with members and invites, then provider and channel connections, then MCP
47521
47632
 
47522
47633
  Settings is reached from the footer gear in the left sidebar (the nav collapse moved it out of the primary item list). It is a left-rail master\u2013detail layout: a grouped rail (General, Team & Access, Billing, AI & Agents, Connections, Advanced) selects one section at a time, shown in the detail pane. The active section is a URL search param, so \`/settings?section=<id>\` is linkable and survives a refresh. The Connections section is the workspace-level provider surface: Google, Slack, QuickBooks, local runners, seat-scoped agent tokens, and stored secrets appear as provider cards with status, used-for chips, actions, and the shared Access language (**Off / Read / Draft / Act with approval**). Detailed management forms for tokens, channels, runners, provider links, and stored vault entries remain reachable from those cards.
47523
47634
 
47524
- Legacy hash deep-links still resolve \u2014 \`/settings#integrations\`, \`#channels\`, and \`#vault\` redirect to \`?section=connections\`; other hashes such as \`#members\` and \`#guardrails\` redirect to their matching section. \`#runners\` is the one exception: it keeps resolving to the dedicated Connected machines management table (not the Connections overview), since System Health and seat pages deep-link there expecting the per-runner revoke actions. Documented shortcut links resolve to the right section instead of a dead end: \`/settings/integrations\` and \`/readiness\` open the Connections section (connector readiness lives inside it, not on a standalone page), \`/settings/policy\` opens the Product learning section, and MCP/CLI access tokens live in the Agent access tokens section (not a separate \`/mcp\` page \u2014 \`/mcp\` is the MCP protocol endpoint itself, not a browsable settings screen). The Advanced group links out to \`/migration/ninety\`, the one settings-adjacent page with no home in the grouped rail. After you save a change, the app keeps you on the section you edited rather than returning to the top.
47635
+ Section state is fully server-side: every in-app link, redirect, and save uses \`?section=<id>\`, never a \`#anchor\` \u2014 there is no client-side hash shim to desync from. Documented shortcut pages redirect straight to the right section instead of a dead end: \`/settings/integrations\` and \`/readiness\` redirect to \`?section=connections\` (connector readiness lives inside the Connections overview, not on a standalone page), \`/settings/members\` redirects to \`?section=members\`, \`/settings/policy\` redirects to \`?section=guardrails\`, and MCP/CLI access tokens live in the Agent access tokens section (not a separate \`/mcp\` page \u2014 \`/mcp\` is the MCP protocol endpoint itself, not a browsable settings screen). The Advanced group links out to \`/migration/ninety\`, the one settings-adjacent page with no home in the grouped rail. Every settings form carries the section it lives in, so saving a change redirects back to that same section with no flash, instead of returning to the top.
47525
47636
 
47526
47637
  ## What belongs in Settings
47527
47638
 
@@ -47577,7 +47688,7 @@ Agents that run on {{brand}}-managed inference draw against a tenant inference b
47577
47688
  - Set the hard cap with \`settings.update\` (CLI: \`{{cli}} settings update --hard-cap-usd <amount>\`). The optional soft cap warns before the hard cap and must be less than or equal to it.
47578
47689
  - The sandbox dry run is free and is never blocked by the cap, so a fresh company can charter, dry-run, and take an agent live before setting a budget. The cap applies only to real managed-inference runs.
47579
47690
  - A company that brings its own provider key (BYOK) is metered on that key and is not subject to the {{brand}}-managed hard cap. BYOK changes the provider account used for eligible cloud calls, not the Charter, tool guard, human gate, or data-retention posture; see the ai-model-data-handling-guide before making provider-handling claims.
47580
- - AICOS cloud brain selection is managed with \`aicos.brain_settings.get\` and owner-gated \`aicos.brain_settings.update\`. Managed cloud answers draw from the included allowance; BYOK cloud answers meter against the tenant key. Runner Claude, Runner Codex, and MCP client answers are labeled separately and do not draw from the managed cloud allowance.
47691
+ - AICOS cloud brain selection is managed with \`aicos.brain_settings.get\` and owner-gated \`aicos.brain_settings.update\`. Managed cloud answers draw from the included allowance; BYOK cloud answers meter against the tenant key. Runner Claude and MCP client answers are labeled separately and do not draw from the managed cloud allowance. The AICOS Codex runner choice remains gated off until the governed interactive runner path is verified.
47581
47692
 
47582
47693
  ## Company autonomy ceiling (Company Guardrails)
47583
47694
 
@@ -47590,6 +47701,15 @@ The company autonomy ceiling is a single tenant-wide dial that caps how much ANY
47590
47701
  - An explicitly set ceiling is ENFORCED by default. Enforcement has two modes: \`enforce\` blocks an over-ceiling autonomous call and routes it to the steward (it is never a dead-end deny \u2014 a human can still approve), and \`observe\` allows the call but logs it. The platform default for a company that never set a policy is balanced + observe, so existing live agents are never retro-bricked.
47591
47702
  - When enforcement is on, signing a manifest or taking an agent live is REFUSED if the manifest grants always_allow to a tool whose risk exceeds the ceiling; the error names the exact ceiling, tool, and risk in its \`details\`. Lower that tool to always_ask, or raise the ceiling, then retry.
47592
47703
 
47704
+ ## CLI confirmation mode
47705
+
47706
+ The CLI confirmation mode is a separate tenant-wide dial. It does not change what an agent may do (that is the company autonomy ceiling above); it changes whether the {{cli}} CLI tool itself may stand in for a human at an already-gated confirmation.
47707
+
47708
+ - \`careful\` is the strict default for every company, including internal ones. The server refuses the CLI's silent auto-replay of a pending confirmation \u2014 a human must review it before it is approved.
47709
+ - \`trusted_operator\` is a deliberate tenant_admin opt-in (an ADR-0018 \xA76 standing authorization) that lets the CLI silently auto-approve a non-\`dangerous\` confirmation. A \`dangerous\`-risk confirmation always requires interactive human review, in either mode.
47710
+ - Read it with \`settings.confirmation_policy.get\` (CLI: \`{{cli}} settings confirmation-mode get\`). Set it with \`settings.confirmation_policy.update\` (CLI: \`{{cli}} settings confirmation-mode set --mode trusted_operator\`). Owner-only and human-gated.
47711
+ - Stored at \`tenants.settings.confirmation_policy\`; enforced server-side in \`confirmation.approve\`, so a stale or modified local CLI cannot bypass it.
47712
+
47593
47713
  ## Agent guidance
47594
47714
 
47595
47715
  Agents may explain which setting is needed and why. They should not ask users to paste secrets into chat or tool arguments. When credentials are required, route the user to the vault-backed setup flow.`
@@ -47639,7 +47759,7 @@ When a user asks to add a person, clarify whether they mean app access, seat occ
47639
47759
  order: 74,
47640
47760
  title: "Notifications guide",
47641
47761
  summary: "How {{brand}} should notify humans about decisions, escalations, stale work, and agent boundaries.",
47642
- version: "2026-06-27.1",
47762
+ version: "2026-07-09.1",
47643
47763
  public: true,
47644
47764
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47645
47765
  stages: ["operating_rhythm"],
@@ -47664,6 +47784,7 @@ Notifications should move decisions to the right human without turning {{brand}}
47664
47784
  - A Signal is broken or stale.
47665
47785
  - A Friction item needs a decision.
47666
47786
  - A Sync decision creates a handoff.
47787
+ - A seat's reporting line or parent changes (\`seat.reparent\`, a seat merge, or \`agent_setup.update\`) \u2014 the affected seat's Steward and the tenant owners get an in-app notification, so a structural change is never silent (the DER-1388 incident this closes was only caught because a human happened to notice the org chart looked wrong).
47667
47788
 
47668
47789
  ## Diagnose failed deliveries
47669
47790
 
@@ -47678,7 +47799,7 @@ Every notification should include the seat, cause, evidence, and requested decis
47678
47799
  order: 75,
47679
47800
  title: "Local runner guide",
47680
47801
  summary: "How local agent sessions and runner surfaces should operate through {{brand}} without bypassing Charters or audit.",
47681
- version: "2026-07-07.3",
47802
+ version: "2026-07-10.1",
47682
47803
  public: true,
47683
47804
  audiences: ["human", "cli", "mcp", "in_app_agent"],
47684
47805
  stages: ["staffing", "operating_rhythm"],
@@ -47730,11 +47851,11 @@ The runner bearer secret is stored locally and is never displayed. Each claimed
47730
47851
 
47731
47852
  ## Interactive AICOS runner turns
47732
47853
 
47733
- AICOS runner chat uses the same runner process but not the scheduled work-order queue. The runner claims an \`agent_turn_executions\` item for one user message, starts it, executes the local Claude or Codex runtime with bounded AICOS context, then reports the final assistant answer back to {{brand}}. Execute-ready AICOS claims include a server-built Seat work contract: the governed runtime profile, the AICOS context-loader tool, and the Seat permission manifest. Actions invoked from that local runtime still pass through command guards, \`tool_calls\` audit, pending confirmations, human gates, and tenant isolation before anything durable changes. The transcript write-back is server-owned: one terminal result creates one assistant message, links it to run evidence, and records the final turn status, terminal reason, timings, model, and token telemetry when present. Retrying the same final report does not append a second answer. If an interactive turn expires while queued, claimed, or running, {{brand}} marks it offline and appends a visible terminal assistant message so the user is not left waiting on a stale queued state.
47854
+ AICOS runner chat uses the same runner process but not the scheduled work-order queue. The runner claims an \`agent_turn_executions\` item for one user message, starts it, executes the local Claude runtime with bounded AICOS context, then reports the final assistant answer back to {{brand}}. Execute-ready AICOS claims include a server-built Seat work contract: the governed runtime profile, the AICOS context-loader tool, and the Seat permission manifest. Actions invoked from that local runtime still pass through command guards, \`tool_calls\` audit, pending confirmations, human gates, and tenant isolation before anything durable changes. The transcript write-back is server-owned: one terminal result creates one assistant message, links it to run evidence, and records the final turn status, terminal reason, timings, model, and token telemetry when present. Retrying the same final report does not append a second answer. Codex remains unavailable for interactive AICOS runner turns until the governed runner path is verified. If an interactive turn expires while queued, claimed, or running, {{brand}} marks it offline and appends a visible terminal assistant message so the user is not left waiting on a stale queued state.
47734
47855
 
47735
47856
  ## Forge scheduler work
47736
47857
 
47737
- When the Forge add-on is enabled, Forge phase work uses the same runner work-order lane. A Forge phase with an owning runner-agent Seat is linked to a normal \`work_orders\` row; the runner claims it, starts it, reports the result, and the server advances the linked phase. There is no separate local lease store.
47858
+ When the Forge add-on is enabled, Forge phase work uses the same runner work-order lane. A Forge phase with an owning runner-agent Seat is linked to a normal \`work_orders\` row only when a paired online runner can claim it; otherwise the phase stays pending with \`scheduler_state.status = "parked"\` and the remediation link points to \`/settings/runners/setup\`. Pairing a runner or the repair sweep re-enqueues the same pending phase idempotently, so operators do not get duplicate intake rows or silent expiring work orders. The runner claims queued work, starts it, reports the result, and the server advances the linked phase. There is no separate local lease store.
47738
47859
 
47739
47860
  A build request decomposes into changesets and tasks tracked as queryable execution state. A changeset is the pull-request boundary and the revert unit (default one per request; additional changesets only on a named split criterion such as risk isolation or a reviewability-size budget). A task is one coding session inside a changeset, carrying its own verification and a per-task checkpoint that is the safe resume and pause boundary. Plan-conformance findings loop a changeset back to implementation; a finding can only be accepted, rather than resolved, through a recorded human decision.
47740
47861
 
@@ -48616,7 +48737,7 @@ This worked document **omits** \`unanswered_boundaries\` and \`seat_type_recomme
48616
48737
  order: 43,
48617
48738
  title: "Agent builder guide",
48618
48739
  summary: "The full agent setup sequence on the CLI/MCP path \u2014 seat, steward, job, boundaries, tools, credentials, model, schedule, dry-run, go-live \u2014 with the structured model config and access tiers.",
48619
- version: "2026-06-29.1",
48740
+ version: "2026-07-10.1",
48620
48741
  public: true,
48621
48742
  audiences: ["cli", "mcp", "in_app_agent"],
48622
48743
  stages: ["staffing"],
@@ -48660,7 +48781,7 @@ Building teams of controlled agent workers is the product's core differentiator.
48660
48781
  ## The full setup sequence
48661
48782
 
48662
48783
  1. **Seat** \u2014 the agent occupies a seat in the Responsibility Graph (a function, not a person). Create or pick the seat first.
48663
- 2. **Steward** \u2014 every agent needs a human steward chain (invariant: no orphan agents). Set \`steward_seat_id\` so the chain resolves to a human; without it the occupancy is blocked.
48784
+ 2. **Steward** \u2014 every agent needs a human steward chain (invariant: no orphan agents). When the creating human occupies a qualifying seat, setup defaults the new agent's Steward to that seat and, for an unplaced fresh seat, uses it as the parent. You can still set \`steward_seat_id\` explicitly; an explicit Steward wins. Without a qualifying creator seat or explicit Steward, the occupancy is blocked and \`agent_setup.get\` returns remediation candidates.
48664
48785
  3. **Job** \u2014 what the seat owns. On \`agent.create_custom\` this is the operational answers (what it owns, what success looks like, what it must never do alone); for a strong contract, submit the full Charter directly via \`charter.update_draft\` (see the charter-authoring deep-dive).
48665
48786
  4. **Boundaries** \u2014 the Charter's \`decision_authority\` (can-do / must-ask / never / escalate), \`escalation_rules\`, and \`budget\`. Conservative by default: send/spend/irreversible actions are approval-gated or escalated.
48666
48787
  5. **Tools** \u2014 pick from the discoverable catalog (\`{{cli}} tools list\`); each tool has a default scope tier and access policy. Connect or decline via \`agent.configure_tools\`. Selecting a tool records permission; live handlers execute later only behind the signed manifest, server guard, required credential or binding, and connector-specific approval boundary.
@@ -48949,6 +49070,13 @@ var approveViaSchema = external_exports.object({
48949
49070
  var pendingConfirmationSchema2 = external_exports.object({
48950
49071
  confirmationId: external_exports.string().min(1),
48951
49072
  commandId: external_exports.string().min(1),
49073
+ // DER-1490: carried through so an inline auto-approve can always render the
49074
+ // gate before approving (D4-i). Optional/tolerant — an older/malformed
49075
+ // response still resolves confirmationId/commandId, just without the extra
49076
+ // gate detail.
49077
+ summary: external_exports.string().optional().catch(void 0),
49078
+ riskLevel: external_exports.enum(["normal", "sensitive", "dangerous"]).optional().catch(void 0),
49079
+ diff: external_exports.unknown().optional(),
48952
49080
  approveVia: approveViaSchema.optional().catch(void 0)
48953
49081
  }).passthrough();
48954
49082
  var confirmationApprovalOutputSchema = external_exports.object({
@@ -49065,16 +49193,22 @@ function brandEnvKey3(suffix) {
49065
49193
  async function renderMcpInstall(input) {
49066
49194
  const stderrLines = [];
49067
49195
  const canAutoApprove = input.canAutoApprove ?? false;
49196
+ const reviewed = input.reviewed ?? false;
49197
+ const approval = {
49198
+ canAutoApprove,
49199
+ reviewed,
49200
+ pushStderr: (line) => stderrLines.push(line)
49201
+ };
49068
49202
  if (input.options.rotateTokenId === void 0 && input.options.scope === void 0) {
49069
49203
  throw new Error(MISSING_SCOPE_MESSAGE);
49070
49204
  }
49071
- const mintScope = input.options.rotateTokenId === void 0 ? { scope: input.options.scope, ...input.options.seatId === void 0 ? {} : { seatId: input.options.seatId } } : await resolveRotationScope(input.client, input.options, canAutoApprove);
49072
- const token = await createMcpToken(input.client, mcpTokenCreateBody(mintScope, input.options.expiry), canAutoApprove);
49205
+ const mintScope = input.options.rotateTokenId === void 0 ? { scope: input.options.scope, ...input.options.seatId === void 0 ? {} : { seatId: input.options.seatId } } : await resolveRotationScope(input.client, input.options, approval);
49206
+ const token = await createMcpToken(input.client, mcpTokenCreateBody(mintScope, input.options.expiry), approval);
49073
49207
  let rotationOldRevoked = null;
49074
49208
  if (input.options.rotateTokenId !== void 0) {
49075
49209
  const oldTokenId = input.options.rotateTokenId;
49076
49210
  try {
49077
- const output = await executeWithConfirmation(input.client, "mcp_token.revoke", { token_id: oldTokenId }, canAutoApprove);
49211
+ const output = await executeWithConfirmation(input.client, "mcp_token.revoke", { token_id: oldTokenId }, approval);
49078
49212
  const revoke = mcpTokenRevokeOutputSchema2.safeParse(output);
49079
49213
  rotationOldRevoked = revoke.success && revoke.data.revoked === true;
49080
49214
  if (!rotationOldRevoked) {
@@ -49155,9 +49289,9 @@ function mcpTokenCreateBody(mint, expiry) {
49155
49289
  }
49156
49290
  return base;
49157
49291
  }
49158
- async function resolveRotationScope(client, options, canAutoApprove) {
49292
+ async function resolveRotationScope(client, options, approval) {
49159
49293
  const oldTokenId = options.rotateTokenId;
49160
- const listOutput = await executeWithConfirmation(client, "mcp_token.list", { include_revoked: true }, canAutoApprove);
49294
+ const listOutput = await executeWithConfirmation(client, "mcp_token.list", { include_revoked: true }, approval);
49161
49295
  const parsed = mcpTokenListOutputSchema2.safeParse(listOutput);
49162
49296
  if (!parsed.success) {
49163
49297
  throw new Error("Could not read the token list to rotate \u2014 mcp_token.list returned an unexpected shape.");
@@ -49220,11 +49354,11 @@ function revokeFailureReason(error51) {
49220
49354
  }
49221
49355
  return "unknown error";
49222
49356
  }
49223
- async function createMcpToken(client, body, canAutoApprove) {
49224
- const output = await executeWithConfirmation(client, "mcp_token.create", body, canAutoApprove);
49357
+ async function createMcpToken(client, body, approval) {
49358
+ const output = await executeWithConfirmation(client, "mcp_token.create", body, approval);
49225
49359
  return mcpTokenOutputSchema.parse(output);
49226
49360
  }
49227
- async function executeWithConfirmation(client, commandId, body, canAutoApprove) {
49361
+ async function executeWithConfirmation(client, commandId, body, approval) {
49228
49362
  try {
49229
49363
  const result = await client.execute(commandId, body);
49230
49364
  return result.output;
@@ -49233,7 +49367,7 @@ async function executeWithConfirmation(client, commandId, body, canAutoApprove)
49233
49367
  if (!pending) {
49234
49368
  throw error51;
49235
49369
  }
49236
- if (!canAutoApprove) {
49370
+ if (!approval.canAutoApprove) {
49237
49371
  throw new PendingConfirmationError({
49238
49372
  confirmationId: pending.confirmationId,
49239
49373
  commandId: pending.commandId,
@@ -49241,13 +49375,33 @@ async function executeWithConfirmation(client, commandId, body, canAutoApprove)
49241
49375
  ...pending.approveVia?.webUrl === void 0 ? {} : { approveUrl: pending.approveVia.webUrl }
49242
49376
  });
49243
49377
  }
49378
+ approval.pushStderr(renderPendingConfirmationGate(pending));
49244
49379
  const approved = await client.execute("confirmation.approve", {
49245
- confirmation_id: pending.confirmationId
49380
+ confirmation_id: pending.confirmationId,
49381
+ // DER-1490: never claim a review that didn't happen — omit the field
49382
+ // entirely rather than sending `reviewed: false` (the server treats
49383
+ // "absent" and "false" the same, but an absent field is honest about there
49384
+ // being no signal either way).
49385
+ ...approval.reviewed ? { reviewed: true } : {}
49246
49386
  });
49247
49387
  const approvalOutput = confirmationApprovalOutputSchema.parse(approved.output);
49248
49388
  return approvalOutput.output;
49249
49389
  }
49250
49390
  }
49391
+ function renderPendingConfirmationGate(pending) {
49392
+ const lines = [`Confirmation required: ${pending.commandId} (${pending.confirmationId})`];
49393
+ if (pending.summary !== void 0) {
49394
+ lines.push(redactForLog(pending.summary));
49395
+ }
49396
+ if (pending.riskLevel !== void 0) {
49397
+ lines.push(`Risk level: ${pending.riskLevel}`);
49398
+ }
49399
+ if (pending.diff !== void 0) {
49400
+ lines.push(`Diff:
49401
+ ${redactForLog(pending.diff)}`);
49402
+ }
49403
+ return lines.join("\n");
49404
+ }
49251
49405
  function pendingCommandConfirmation(error51, commandId) {
49252
49406
  if (!(error51 instanceof CommandClientError) || error51.status !== 409 || !error51.response || error51.response.ok) {
49253
49407
  return null;
@@ -49569,9 +49723,9 @@ var COMMAND_MANIFEST = [
49569
49723
  { "id": "compass.set", "namespace": "compass", "action": "set", "title": "Set Compass", "description": "Compatibility command for the DER-626 MCP Compass tool.", "requiredScope": "tenant", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "draft_id", "flag": "draft-id", "type": "string", "required": false }, { "name": "approve", "flag": "approve", "type": "boolean", "required": false }], "hasComplexInput": true, "help": "Set Compass drafts in the product's own vocabulary and approve only with explicit human confirmation." },
49570
49724
  { "id": "compass.show_markdown", "namespace": "compass", "action": "show_markdown", "title": "Show Compass as markdown", "description": "Compose the current Compass and its open gaps into a clean, human-skimmable markdown card for review.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Render the current Compass and its open gaps as a clean markdown card to show your human a quick review." },
49571
49725
  { "id": "compass.update_draft", "namespace": "compass", "action": "update_draft", "title": "Update Compass draft", "description": "Replace a draft Compass document.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "draft_id", "flag": "draft-id", "type": "string", "required": true }], "hasComplexInput": true, "help": "Pass the compass_version_id returned by compass.draft as draft_id; update_draft replaces only an unpublished draft in this tenant." },
49572
- { "id": "confirmation.approve", "namespace": "confirmation", "action": "approve", "title": "Approve pending confirmation", "description": "Approve an in-flight confirmation and execute the original command as the approving human.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "confirmation_id", "flag": "confirmation-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Approve pending confirmations only when the requested durable change is clear to the human owner." },
49726
+ { "id": "confirmation.approve", "namespace": "confirmation", "action": "approve", "title": "Approve pending confirmation", "description": "Approve an in-flight confirmation and execute the original command as the approving human.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "confirmation_id", "flag": "confirmation-id", "type": "string", "required": true }, { "name": "reviewed", "flag": "reviewed", "type": "boolean", "required": false }], "hasComplexInput": false, "help": "Approve pending confirmations only when the requested durable change is clear to the human owner." },
49573
49727
  { "id": "confirmation.reject", "namespace": "confirmation", "action": "reject", "title": "Reject pending confirmation", "description": "Reject an in-flight confirmation without executing the original command.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "confirmation_id", "flag": "confirmation-id", "type": "string", "required": true }, { "name": "reason", "flag": "reason", "type": "string", "required": false }], "hasComplexInput": false, "help": "Reject pending confirmations when authority, evidence, or human intent is unclear." },
49574
- { "id": "credential.ingress", "namespace": "credential", "action": "ingress", "title": "Store credential", "description": "Store a secret through the vault and persist only the credential vault reference.", "requiredScope": "seat", "confirmation": "credential_flow", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": false }, { "name": "provider", "flag": "provider", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }], "hasComplexInput": false, "secretBlocked": true, "help": "Ingress credentials only through vault-backed flows; never place raw secrets in prompts, logs, or docs." },
49728
+ { "id": "credential.ingress", "namespace": "credential", "action": "ingress", "title": "Store credential", "description": "Store a secret through the vault and persist only the credential vault reference.", "requiredScope": "seat", "confirmation": "credential_flow", "exposeOverMcp": false, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": false }, { "name": "provider", "flag": "provider", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }], "hasComplexInput": false, "secretBlocked": true, "help": "Ingress credentials only through vault-backed flows; never place raw secrets in prompts, logs, or docs." },
49575
49729
  { "id": "deliverable.accept", "namespace": "deliverable", "action": "accept", "title": "Accept deliverable value", "description": "Record a human-confirmed accepted value (USD) on a deliverable. Humans decide; agents cannot self-accept. Re-accepting records a correction as a new event; prior events are never mutated.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "deliverable_id", "flag": "deliverable-id", "type": "string", "required": true }, { "name": "impact_value_usd", "flag": "impact-value-usd", "type": "number", "required": true }], "hasComplexInput": false, "help": "Record a human-confirmed accepted value (USD) on a deliverable; owner-only and human-gated. Humans decide \u2014 agents cannot self-accept. Re-accepting records a correction as a new event." },
49576
49730
  { "id": "deliverable.attach", "namespace": "deliverable", "action": "attach", "title": "Attach agent deliverable", "description": "Attach a deliverable to a source run, task, or work order for the acting seat. Source refs are validated server-side.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "run_id", "flag": "run-id", "type": "string", "required": false }, { "name": "task_id", "flag": "task-id", "type": "string", "required": false }, { "name": "work_order_id", "flag": "work-order-id", "type": "string", "required": false }, { "name": "kind", "flag": "kind", "type": "enum", "required": false, "enumValues": ["brief", "work_evidence", "artifact", "report", "other"] }, { "name": "title", "flag": "title", "type": "string", "required": true }, { "name": "summary", "flag": "summary", "type": "string", "required": false }, { "name": "content", "flag": "content", "type": "string", "required": false }], "hasComplexInput": true, "help": "Attach a scrubbed deliverable to a source run, task, or work order that belongs to the acting seat." },
49577
49731
  { "id": "deliverable.create", "namespace": "deliverable", "action": "create", "title": "Create agent deliverable", "description": "Create an agent deliverable for the acting seat. The deliverable is scrubbed for secrets before storage.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "kind", "flag": "kind", "type": "enum", "required": false, "enumValues": ["brief", "work_evidence", "artifact", "report", "other"] }, { "name": "title", "flag": "title", "type": "string", "required": true }, { "name": "summary", "flag": "summary", "type": "string", "required": false }, { "name": "content", "flag": "content", "type": "string", "required": false }, { "name": "delivered_at", "flag": "delivered-at", "type": "string", "required": false }], "hasComplexInput": true, "help": "Create a scrubbed durable work output for the acting seat without making a durable company decision." },
@@ -49604,7 +49758,7 @@ var COMMAND_MANIFEST = [
49604
49758
  { "id": "goal.unbind_measurable", "namespace": "goal", "action": "unbind_measurable", "title": "Unbind a Signal from a Cascade goal", "description": "Remove a measurable's drives_status binding from a goal. The goal keeps its last status; unbinding only stops future signal-driven updates.", "requiredScope": "seat", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "goal_id", "flag": "goal-id", "type": "string", "required": true }, { "name": "measurable_id", "flag": "measurable-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Remove a Signal's drives_status binding from a goal; the goal keeps its last status and stops receiving signal-driven updates." },
49605
49759
  { "id": "goal.update", "namespace": "goal", "action": "update", "title": "Update Cascade goal", "description": "Update a goal's title or definition of done. Topology (parent/seat) and status have dedicated commands.", "requiredScope": "tenant_admin", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "goal_id", "flag": "goal-id", "type": "string", "required": true }, { "name": "title", "flag": "title", "type": "string", "required": false }, { "name": "definition_of_done", "flag": "definition-of-done", "type": "string", "required": false }], "hasComplexInput": false, "help": "Update a goal's title or definition of done; topology and status have dedicated commands." },
49606
49760
  { "id": "graph.get", "namespace": "graph", "action": "get", "title": "Get Responsibility Graph", "description": "Return the tenant's Responsibility Graph: seats, edges, root, occupants, and status rollups.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read the Responsibility Graph to discover seat ids, parentage, occupants, and status before mutating or routing work." },
49607
- { "id": "integration.connect_rest", "namespace": "integration", "action": "connect_rest", "title": "Connect REST integration", "description": "Create or rotate the tenant Baserow REST integration using a vault-backed secret. Stores endpoint metadata only; never returns secrets or vault refs.", "requiredScope": "tenant_admin", "confirmation": "credential_flow", "exposeOverMcp": true, "fields": [{ "name": "provider", "flag": "provider", "type": "enum", "required": true, "enumValues": ["baserow"] }, { "name": "endpoint_url", "flag": "endpoint-url", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }, { "name": "account_label", "flag": "account-label", "type": "string", "required": false }], "hasComplexInput": false, "secretBlocked": true, "help": "Create or rotate a vault-backed Baserow REST integration for Signal pulls. Use the credential flow; never pass secrets through CLI argv." },
49761
+ { "id": "integration.connect_rest", "namespace": "integration", "action": "connect_rest", "title": "Connect REST integration", "description": "Create or rotate the tenant Baserow REST integration using a vault-backed secret. Stores endpoint metadata only; never returns secrets or vault refs.", "requiredScope": "tenant_admin", "confirmation": "credential_flow", "exposeOverMcp": false, "fields": [{ "name": "provider", "flag": "provider", "type": "enum", "required": true, "enumValues": ["baserow"] }, { "name": "endpoint_url", "flag": "endpoint-url", "type": "string", "required": true }, { "name": "scope_description", "flag": "scope-description", "type": "string", "required": true }, { "name": "secret_name", "flag": "secret-name", "type": "string", "required": true }, { "name": "account_label", "flag": "account-label", "type": "string", "required": false }], "hasComplexInput": false, "secretBlocked": true, "help": "Create or rotate a vault-backed Baserow REST integration for Signal pulls. Use the credential flow; never pass secrets through CLI argv." },
49608
49762
  { "id": "integration.discover", "namespace": "integration", "action": "discover", "title": "Discover a connection for signals", "description": "Read-only introspection of a connected source for signal binding. Returns non-secret adapter metadata and a rest-for-signals recipe skeleton to author against signal.preview. Exposes no secrets.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "integration_id", "flag": "integration-id", "type": "string", "required": true }], "hasComplexInput": false, "help": "Read-only introspection of a connected source: returns non-secret metadata + a recipe skeleton to author a signal pull against. No network." },
49609
49763
  { "id": "integration.list", "namespace": "integration", "action": "list", "title": "List integrations", "description": "List tenant integration metadata and health status. Returns account/scopes/capabilities only; never secrets or vault refs.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "provider", "flag": "provider", "type": "string", "required": false }], "hasComplexInput": false, "help": "List connected integrations and their metadata-only health state before enabling a connector-backed tool." },
49610
49764
  { "id": "integration.readiness", "namespace": "integration", "action": "readiness", "title": "Check integration readiness", "description": "Return a safe connector setup checklist for operators and agents. Starts with Google and exposes no secrets.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [{ "name": "provider", "flag": "provider", "type": "enum", "required": false, "enumValues": ["google"] }], "hasComplexInput": false, "help": "Read the connector setup checklist before relying on Google-backed agent work; the checklist names missing config, connection, scopes, external verification, and handler blockers." },
@@ -49646,6 +49800,8 @@ var COMMAND_MANIFEST = [
49646
49800
  { "id": "seat.set_type", "namespace": "seat", "action": "set_type", "title": "Set seat type", "description": "Set a Responsibility Graph seat type.", "requiredScope": "seat", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "seat_id", "flag": "seat-id", "type": "string", "required": true }, { "name": "seat_type", "flag": "seat-type", "type": "enum", "required": true, "enumValues": ["human", "agent", "hybrid"] }], "hasComplexInput": false, "help": "Seat type should follow the work, risk, measurable, and stewardship model." },
49647
49801
  { "id": "settings.agent_policy.get", "namespace": "settings", "action": "agent_policy.get", "title": "Get company autonomy ceiling", "description": "Read this company's Company Guardrails ceiling (ADR-0007): the profile, enforcement mode, and the most an agent may do autonomously (max_autonomous_risk). Returns metadata only.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read this company's autonomy ceiling (Company Guardrails, ADR-0007): profile, enforcement mode, and the most an agent may do autonomously." },
49648
49802
  { "id": "settings.agent_policy.update", "namespace": "settings", "action": "agent_policy.update", "title": "Set company autonomy ceiling", "description": "Set the Company Guardrails ceiling (ADR-0007): a named profile (locked_down, balanced, high_autonomy) or custom + max_autonomous_risk. Tenant-admin and human-gated. An explicitly set ceiling is ENFORCED by default; pass enforcement=observe to stage a change without blocking.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "profile", "flag": "profile", "type": "enum", "required": true, "enumValues": ["locked_down", "balanced", "high_autonomy", "custom"] }, { "name": "max_autonomous_risk", "flag": "max-autonomous-risk", "type": "enum", "required": false, "enumValues": ["low", "medium", "high", "critical"] }, { "name": "enforcement", "flag": "enforcement", "type": "enum", "required": false, "enumValues": ["observe", "enforce"] }], "hasComplexInput": false, "help": "Set the company autonomy ceiling: a profile (locked_down, balanced, high_autonomy) or custom + max_autonomous_risk. Owner-only, human-gated, and enforced by default." },
49803
+ { "id": "settings.confirmation_policy.get", "namespace": "settings", "action": "confirmation_policy.get", "title": "Get CLI confirmation mode", "description": "Read this company's CLI confirmation mode (DER-1490): careful (the strict default \u2014 the CLI cannot silently auto-approve a confirmation) or trusted_operator (an owner opt-in permitting silent auto-approval of non-dangerous confirmations). Returns metadata only.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read this company's CLI confirmation mode: careful (the strict default \u2014 the CLI cannot silently auto-approve a confirmation) or trusted_operator (an owner opt-in permitting silent auto-approval of non-dangerous confirmations)." },
49804
+ { "id": "settings.confirmation_policy.update", "namespace": "settings", "action": "confirmation_policy.update", "title": "Set CLI confirmation mode", "description": "Set this company's CLI confirmation mode (DER-1490): careful (strict \u2014 no silent CLI auto-approval) or trusted_operator (owner opt-in permitting silent auto-approval of non-dangerous confirmations; dangerous confirmations always require an interactive review). Owner-only and human-gated: choosing trusted_operator establishes an ADR-0018 \xA76 standing authorization.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "cli_mode", "flag": "cli-mode", "type": "enum", "required": true, "enumValues": ["trusted_operator", "careful"] }], "hasComplexInput": false, "help": "Set the CLI confirmation mode to careful or trusted_operator. Owner-only and human-gated; trusted_operator establishes a standing authorization and dangerous confirmations still require an interactive review." },
49649
49805
  { "id": "settings.get", "namespace": "settings", "action": "get", "title": "Get tenant settings", "description": "Read tenant operating settings: plan, status, spend/budgets, integration status, and notification preferences. Returns metadata only.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read tenant operating settings: plan, spend, budgets, integration status, and notification preferences. No secrets." },
49650
49806
  { "id": "settings.product_learning.get", "namespace": "settings", "action": "product_learning.get", "title": "Get product-learning policy", "description": "Read whether product/page/recommendation analytics may be recorded for this tenant. Security and audit logs remain enabled in every mode.", "requiredScope": "tenant", "confirmation": "none", "exposeOverMcp": true, "fields": [], "hasComplexInput": false, "help": "Read whether product/page/recommendation analytics are allowed for the tenant; security and audit logs always stay enabled." },
49651
49807
  { "id": "settings.product_learning.update", "namespace": "settings", "action": "product_learning.update", "title": "Update product-learning policy", "description": "Set tenant product-learning participation. Disabled and enterprise-contract modes block generic product analytics writes, but not security/audit logs.", "requiredScope": "tenant_admin", "confirmation": "human_required", "exposeOverMcp": true, "fields": [{ "name": "mode", "flag": "mode", "type": "enum", "required": true, "enumValues": ["enabled", "disabled", "enterprise_contract"] }], "hasComplexInput": false, "help": "Set tenant product-learning mode to enabled, disabled, or enterprise_contract; the change is human-gated." },
@@ -50623,6 +50779,14 @@ function renderAgentPolicySet(output) {
50623
50779
  const policy = asRecord(asRecord(output).agent_policy);
50624
50780
  return `Set company autonomy ceiling to ${field(policy, "profile")} (max_autonomous_risk=${field(policy, "max_autonomous_risk")}, enforcement=${field(policy, "enforcement")}).`;
50625
50781
  }
50782
+ function renderConfirmationModeGet(output) {
50783
+ const policy = asRecord(asRecord(output).confirmation_policy);
50784
+ return `confirmation_policy cli_mode=${field(policy, "cli_mode")} explicit=${field(policy, "explicit")}`;
50785
+ }
50786
+ function renderConfirmationModeSet(output) {
50787
+ const policy = asRecord(asRecord(output).confirmation_policy);
50788
+ return `Updated CLI confirmation mode to ${field(policy, "cli_mode")}.`;
50789
+ }
50626
50790
  function renderMemberInvite(output) {
50627
50791
  const record2 = asRecord(output);
50628
50792
  return `Invited ${field(record2, "email")} as ${field(record2, "role")} (invite ${field(record2, "invite_id")}).`;
@@ -51186,6 +51350,13 @@ var agentPolicySetHandler = bespoke("settings.agent_policy.update", (parsed) =>
51186
51350
  enforcement: optionalValue(parsed, "enforcement")
51187
51351
  });
51188
51352
  }, renderAgentPolicySet);
51353
+ var confirmationModeSetHandler = bespoke("settings.confirmation_policy.update", (parsed) => {
51354
+ const mode = optionalValue(parsed, "mode");
51355
+ if (mode !== "careful" && mode !== "trusted_operator") {
51356
+ throw new UsageError("Provide --mode careful|trusted_operator.");
51357
+ }
51358
+ return { cli_mode: mode };
51359
+ }, renderConfirmationModeSet);
51189
51360
  var seatDecommissionHandler = (context, rest) => {
51190
51361
  const parsed = parseFlags(rest, /* @__PURE__ */ new Set(["dry-run", "preview", "yes", "y"]));
51191
51362
  if (parsed.flags.has("dry-run") || parsed.flags.has("preview")) {
@@ -51207,6 +51378,13 @@ var settingsAgentPolicyHandler = (context, rest) => dispatch(context, "settings
51207
51378
  },
51208
51379
  set: (ctx, r) => agentPolicySetHandler(ctx, r)
51209
51380
  }, `Usage: ${context.binName} settings agent-policy get|set --profile locked_down|balanced|high_autonomy|custom [--max-autonomous-risk <r>] [--enforcement enforce|observe]`);
51381
+ var settingsConfirmationModeHandler = (context, rest) => dispatch(context, "settings confirmation-mode", rest, {
51382
+ get: (ctx, r) => {
51383
+ const parsed = parseFlags(r, /* @__PURE__ */ new Set(["yes", "y"]));
51384
+ return execute(ctx, parsed, "settings.confirmation_policy.get", {}, renderConfirmationModeGet);
51385
+ },
51386
+ set: (ctx, r) => confirmationModeSetHandler(ctx, r)
51387
+ }, `Usage: ${context.binName} settings confirmation-mode get|set --mode careful|trusted_operator`);
51210
51388
  var agentSetupHandler = (context, rest) => dispatch(context, "agent setup", rest, {
51211
51389
  status: (ctx, r) => {
51212
51390
  const parsed = parseFlags(r, /* @__PURE__ */ new Set(["yes", "y"]));
@@ -51385,6 +51563,7 @@ var GROUP_SPECS = {
51385
51563
  update: { commandId: "settings.update", handler: settingsUpdateHandler, render: renderSettingsUpdate },
51386
51564
  "product-learning": { commandId: "settings.product_learning.get", handler: settingsProductLearningHandler },
51387
51565
  "agent-policy": { commandId: "settings.agent_policy.get", handler: settingsAgentPolicyHandler },
51566
+ "confirmation-mode": { commandId: "settings.confirmation_policy.get", handler: settingsConfirmationModeHandler },
51388
51567
  rename: { commandId: "tenant.rename", handler: settingsRenameHandler, render: renderTenantRename }
51389
51568
  }
51390
51569
  },
@@ -51501,6 +51680,8 @@ var EXTRA_SURFACED = /* @__PURE__ */ new Set([
51501
51680
  // settings product-learning update
51502
51681
  "settings.agent_policy.update",
51503
51682
  // settings agent-policy set
51683
+ "settings.confirmation_policy.update",
51684
+ // settings confirmation-mode set
51504
51685
  "skill.import_github"
51505
51686
  // skills import github
51506
51687
  ]);
@@ -51604,7 +51785,7 @@ function operationUsageLines(bin) {
51604
51785
  `${bin} runner install-service|start|stop|restart|status|logs|uninstall --name <name>`,
51605
51786
  `${bin} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume`,
51606
51787
  `${bin} notification settings|test|errors`,
51607
- `${bin} settings get|update|product-learning|agent-policy|rename`,
51788
+ `${bin} settings get|update|product-learning|agent-policy|confirmation-mode|rename`,
51608
51789
  `${bin} member invite|update|remove`,
51609
51790
  `${bin} agent templates|create|setup|tools|dry-run|go-live|status|run-now|fleet-digest|get-run|show`,
51610
51791
  `${bin} tools list`,
@@ -51922,9 +52103,10 @@ async function executeSkillCommand(context, commandId, body, options = {}) {
51922
52103
  } catch (error51) {
51923
52104
  const pending = context.io.interactive === true ? pendingConfirmationFromError(error51) : null;
51924
52105
  if (pending && pending.commandId === commandId) {
52106
+ renderPendingConfirmationGate2(context.io, pending);
51925
52107
  context.io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the interactive CLI user.
51926
52108
  `);
51927
- const approved = await context.client.execute("confirmation.approve", { confirmation_id: pending.confirmationId });
52109
+ const approved = await context.client.execute("confirmation.approve", { confirmation_id: pending.confirmationId, reviewed: true });
51928
52110
  return asRecord2(approved.output).output ?? approved.output;
51929
52111
  }
51930
52112
  throw error51;
@@ -52170,7 +52352,30 @@ function pendingConfirmationFromError(error51) {
52170
52352
  if (typeof confirmationId !== "string" || confirmationId.length === 0 || typeof commandId !== "string" || commandId.length === 0) {
52171
52353
  return null;
52172
52354
  }
52173
- return { confirmationId, commandId };
52355
+ const summary = typeof details.summary === "string" ? details.summary : void 0;
52356
+ const riskLevel = typeof details.riskLevel === "string" ? details.riskLevel : void 0;
52357
+ return {
52358
+ confirmationId,
52359
+ commandId,
52360
+ ...summary === void 0 ? {} : { summary },
52361
+ ...riskLevel === void 0 ? {} : { riskLevel },
52362
+ ...details.diff === void 0 ? {} : { diff: details.diff }
52363
+ };
52364
+ }
52365
+ function renderPendingConfirmationGate2(io, pending) {
52366
+ if (pending.summary !== void 0) {
52367
+ io.stderr.write(`${redactForLog(pending.summary)}
52368
+ `);
52369
+ }
52370
+ if (pending.riskLevel !== void 0) {
52371
+ io.stderr.write(`Risk level: ${pending.riskLevel}
52372
+ `);
52373
+ }
52374
+ if (pending.diff !== void 0) {
52375
+ io.stderr.write(`Diff:
52376
+ ${redactForLog(pending.diff)}
52377
+ `);
52378
+ }
52174
52379
  }
52175
52380
 
52176
52381
  // src/doctor.ts
@@ -52179,12 +52384,286 @@ import { access as access2 } from "node:fs/promises";
52179
52384
  import { delimiter } from "node:path";
52180
52385
  import { promisify as promisify3 } from "node:util";
52181
52386
 
52387
+ // src/runner-sandbox.ts
52388
+ import { existsSync as existsSync2, realpathSync } from "node:fs";
52389
+ import { readdir, rm as rm2, stat as stat2 } from "node:fs/promises";
52390
+ import { tmpdir } from "node:os";
52391
+ import path3 from "node:path";
52392
+ var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
52393
+ var RUNNER_ASKPASS_DIR = path3.join(tmpdir(), "rost-runner-askpass");
52394
+ var RUNNER_TEMP_PREFIXES = [
52395
+ "rost-runner-mcp-",
52396
+ "rost-runner-sandbox-",
52397
+ "rost-runner-askpass-"
52398
+ ];
52399
+ var RUNNER_TEMP_STARTUP_SWEEP_MAX_AGE_MS = 60 * 60 * 1e3;
52400
+ var RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS = 45 * 60 * 1e3;
52401
+ var SENSITIVE_READ_SUBPATHS = [
52402
+ ".ssh",
52403
+ ".aws",
52404
+ ".gnupg",
52405
+ ".config/gcloud",
52406
+ ".config/gh",
52407
+ ".kube",
52408
+ ".docker",
52409
+ ".npmrc"
52410
+ ];
52411
+ var SENSITIVE_READ_LITERALS = [".netrc"];
52412
+ var SENSITIVE_WRITE_SUBPATHS = [
52413
+ ".ssh",
52414
+ ".aws",
52415
+ ".gnupg",
52416
+ ".config/git",
52417
+ "bin",
52418
+ "Library/LaunchAgents"
52419
+ ];
52420
+ var SENSITIVE_WRITE_LITERALS = [
52421
+ ".gitconfig",
52422
+ ".zshrc",
52423
+ ".zprofile",
52424
+ ".zshenv",
52425
+ ".zlogin",
52426
+ ".bashrc",
52427
+ ".bash_profile",
52428
+ ".bash_login",
52429
+ ".profile"
52430
+ ];
52431
+ function parseSandboxMode(value) {
52432
+ const normalized = (value ?? "auto").trim().toLowerCase();
52433
+ if (normalized === "off" || normalized === "strict" || normalized === "auto" || normalized === "") {
52434
+ return normalized === "" ? "auto" : normalized;
52435
+ }
52436
+ return "auto";
52437
+ }
52438
+ function parseSandboxNetwork(value) {
52439
+ const normalized = (value ?? "").trim().toLowerCase();
52440
+ if (normalized === "" || normalized === "allow") {
52441
+ return { allowNetwork: true, unrecognized: null };
52442
+ }
52443
+ if (normalized === "deny") {
52444
+ return { allowNetwork: false, unrecognized: null };
52445
+ }
52446
+ return { allowNetwork: true, unrecognized: value ?? "" };
52447
+ }
52448
+ function sbplString(value) {
52449
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
52450
+ }
52451
+ function subpath(value) {
52452
+ return `(subpath ${sbplString(value)})`;
52453
+ }
52454
+ function literal2(value) {
52455
+ return `(literal ${sbplString(value)})`;
52456
+ }
52457
+ var CLAUDE_TEMP_SBPL_REGEX = '(regex #"^/private/tmp/claude-[^/]+")';
52458
+ var STRICT_SYSTEM_READ_SUBPATHS = [
52459
+ "/usr",
52460
+ "/bin",
52461
+ "/sbin",
52462
+ "/System",
52463
+ "/Library",
52464
+ "/private/etc",
52465
+ "/private/var/db",
52466
+ "/dev",
52467
+ "/opt",
52468
+ "/nix"
52469
+ ];
52470
+ function buildSeatbeltProfile(input) {
52471
+ const home = input.homeDir.replace(/\/+$/, "");
52472
+ const lines = ["(version 1)"];
52473
+ const denyReadBaseSubpaths = input.denyReadPaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
52474
+ const denyReadLiterals = input.denyReadPaths.filter((p) => !p.endsWith("/")).map(literal2);
52475
+ const denyWriteSubpaths = input.denyWritePaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
52476
+ const denyWriteLiterals = input.denyWritePaths.filter((p) => !p.endsWith("/")).map(literal2);
52477
+ const credentialReadDenies = [
52478
+ ...SENSITIVE_READ_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
52479
+ ...SENSITIVE_READ_LITERALS.map((rel) => literal2(`${home}/${rel}`))
52480
+ ];
52481
+ if (input.strict) {
52482
+ lines.push("(deny default)");
52483
+ lines.push("(allow process*)");
52484
+ lines.push("(allow sysctl-read)");
52485
+ lines.push("(allow mach-lookup)");
52486
+ lines.push("(allow iokit-open)");
52487
+ lines.push("(allow system-socket)");
52488
+ lines.push("(allow signal (target self))");
52489
+ lines.push("(allow file-read-metadata)");
52490
+ const strictReads = [
52491
+ // The root directory NODE itself (not its children — `literal`, never `subpath`).
52492
+ // dyld resolves the `/System/Volumes/Preboot/Cryptexes` firmlink to load the arm64
52493
+ // shared cache at process start, and that resolution reads `/`; without this a
52494
+ // deny-default profile SIGABRTs every spawned runtime (cat/node/claude) before it
52495
+ // runs. `(literal "/")` grants only the root dir listing/metadata (no secret), while
52496
+ // every child path stays individually deny-gated. (Guard never hits this branch.)
52497
+ literal2("/"),
52498
+ ...STRICT_SYSTEM_READ_SUBPATHS.map(subpath),
52499
+ subpath(`${home}/.claude`),
52500
+ subpath(`${home}/.codex`),
52501
+ subpath(input.tmpDir)
52502
+ ];
52503
+ lines.push(`(allow file-read* ${strictReads.join(" ")})`);
52504
+ } else {
52505
+ lines.push("(allow default)");
52506
+ }
52507
+ lines.push("(deny appleevent-send)");
52508
+ if (denyReadBaseSubpaths.length > 0) {
52509
+ lines.push(`(deny file-read* ${denyReadBaseSubpaths.join(" ")})`);
52510
+ }
52511
+ const workspaceReads = [subpath(input.workspaceDir), ...input.allowWritePaths.map(subpath), CLAUDE_TEMP_SBPL_REGEX];
52512
+ lines.push(`(allow file-read* ${workspaceReads.join(" ")})`);
52513
+ lines.push("(deny file-write*)");
52514
+ const allowWrites = [
52515
+ subpath(input.workspaceDir),
52516
+ subpath(input.tmpDir),
52517
+ subpath(`${home}/.claude`),
52518
+ subpath(`${home}/.codex`),
52519
+ ...input.allowWritePaths.map(subpath),
52520
+ CLAUDE_TEMP_SBPL_REGEX,
52521
+ // DER-1398: Claude Code's per-session + per-command temp dirs.
52522
+ subpath("/dev/fd"),
52523
+ literal2("/dev/null"),
52524
+ literal2("/dev/stdout"),
52525
+ literal2("/dev/stderr"),
52526
+ literal2("/dev/tty"),
52527
+ literal2("/dev/dtracehelper")
52528
+ ];
52529
+ lines.push(`(allow file-write* ${allowWrites.join(" ")})`);
52530
+ const lastReadDenies = [
52531
+ ...credentialReadDenies,
52532
+ ...denyReadLiterals,
52533
+ ...input.denyReadLastSubpaths.map((p) => subpath(p.replace(/\/+$/, "")))
52534
+ ];
52535
+ lines.push(`(deny file-read* ${lastReadDenies.join(" ")})`);
52536
+ const denyWrites = [
52537
+ ...SENSITIVE_WRITE_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
52538
+ ...SENSITIVE_WRITE_LITERALS.map((rel) => literal2(`${home}/${rel}`)),
52539
+ // Caller-supplied write-denies (e.g. `<baseRepo>/.git/hooks` + `.git/config`) last so
52540
+ // they override the allowWritePaths re-allow of the shared object store above, while
52541
+ // `.git/{objects,refs,worktrees,logs,index,HEAD}` stay writable for local commits.
52542
+ ...denyWriteSubpaths,
52543
+ ...denyWriteLiterals
52544
+ ];
52545
+ lines.push(`(deny file-write* ${denyWrites.join(" ")})`);
52546
+ lines.push('(deny mach-lookup (global-name "com.apple.coreservices.appleevents"))');
52547
+ if (!input.allowNetwork) {
52548
+ lines.push("(deny network*)");
52549
+ } else if (input.strict) {
52550
+ lines.push("(allow network*)");
52551
+ }
52552
+ return `${lines.join("\n")}
52553
+ `;
52554
+ }
52555
+ function canonical(p) {
52556
+ try {
52557
+ return realpathSync(p);
52558
+ } catch {
52559
+ return p;
52560
+ }
52561
+ }
52562
+ function canonicalFile(p) {
52563
+ return path3.join(canonical(path3.dirname(p)), path3.basename(p));
52564
+ }
52565
+ function isSandboxExecAvailable() {
52566
+ try {
52567
+ return existsSync2(SANDBOX_EXEC_PATH);
52568
+ } catch {
52569
+ return false;
52570
+ }
52571
+ }
52572
+ function resolveSandboxSpec(options) {
52573
+ if (options.mode === "off") {
52574
+ return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
52575
+ }
52576
+ if (options.platform !== "darwin") {
52577
+ return { kind: "none", reason: `no OS sandbox on ${options.platform} (seatbelt is macOS-only; Linux confinement is a follow-up)` };
52578
+ }
52579
+ const available = options.sandboxExecAvailable ?? isSandboxExecAvailable();
52580
+ if (!available) {
52581
+ return { kind: "none", reason: `sandbox-exec not found at ${SANDBOX_EXEC_PATH}` };
52582
+ }
52583
+ const workspaceDir = canonical(options.workspaceDir);
52584
+ const tmpDir = canonical(options.tmpDir);
52585
+ const homeDir = canonical(options.homeDir);
52586
+ const allowNetwork = options.allowNetwork ?? true;
52587
+ const denyReadPaths = [canonicalFile(options.stateFile)];
52588
+ if (options.runnerBaseDir) {
52589
+ denyReadPaths.push(`${canonical(options.runnerBaseDir).replace(/\/+$/, "")}/`);
52590
+ }
52591
+ const denyWritePaths = (options.denyWritePaths ?? []).map(
52592
+ (p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
52593
+ );
52594
+ denyWritePaths.push(`${canonicalFile(RUNNER_ASKPASS_DIR)}/`);
52595
+ const allowWritePaths = (options.allowWritePaths ?? []).map(canonical);
52596
+ const denyReadLastSubpaths = [`${canonicalFile(RUNNER_ASKPASS_DIR)}/`];
52597
+ const profile = buildSeatbeltProfile({
52598
+ workspaceDir,
52599
+ tmpDir,
52600
+ homeDir,
52601
+ denyReadPaths,
52602
+ denyReadLastSubpaths,
52603
+ allowWritePaths,
52604
+ denyWritePaths,
52605
+ allowNetwork,
52606
+ strict: options.mode === "strict"
52607
+ });
52608
+ return {
52609
+ kind: "seatbelt",
52610
+ reason: options.mode === "strict" ? "seatbelt (strict: deny-default reads + write confinement)" : "seatbelt (guard: deny sensitive reads/writes)",
52611
+ profile,
52612
+ allowNetwork
52613
+ };
52614
+ }
52615
+ function forgeBuildUnsupportedReason(platform2) {
52616
+ if (platform2 === "darwin") {
52617
+ return null;
52618
+ }
52619
+ return `Forge build execution is unsupported on this host: no macOS seatbelt (sandbox-exec) confinement is available on '${platform2}', so this runner cannot prove filesystem/egress isolation for an untrusted build turn. The server will not offer Forge-phase work to this runner \u2014 unless its tenant is on the narrow, operator-established trust allowlist (FORGE_SANDBOX_TRUSTED_TENANT_IDS) \u2014 until real Linux confinement (namespaces/Landlock, tracked as a DER-1343 follow-up) ships.`;
52620
+ }
52621
+ function wrapCommandWithSandbox(spec, command, args, profilePath) {
52622
+ if (spec.kind === "none" || profilePath === null) {
52623
+ return { command, args, profilePath: null };
52624
+ }
52625
+ return {
52626
+ command: SANDBOX_EXEC_PATH,
52627
+ args: ["-f", profilePath, command, ...args],
52628
+ profilePath
52629
+ };
52630
+ }
52631
+ async function sweepStaleRunnerTempFiles(dir, options = {}) {
52632
+ const maxAgeMs = options.maxAgeMs ?? RUNNER_TEMP_STARTUP_SWEEP_MAX_AGE_MS;
52633
+ const now = options.now ?? Date.now();
52634
+ let entries;
52635
+ try {
52636
+ entries = await readdir(dir);
52637
+ } catch {
52638
+ return [];
52639
+ }
52640
+ const removed = [];
52641
+ await Promise.all(
52642
+ entries.map(async (name) => {
52643
+ if (!RUNNER_TEMP_PREFIXES.some((prefix) => name.startsWith(prefix))) {
52644
+ return;
52645
+ }
52646
+ const full = path3.join(dir, name);
52647
+ try {
52648
+ const info = await stat2(full);
52649
+ if (now - info.mtimeMs < maxAgeMs) {
52650
+ return;
52651
+ }
52652
+ await rm2(full, { recursive: true, force: true });
52653
+ removed.push(name);
52654
+ } catch {
52655
+ }
52656
+ })
52657
+ );
52658
+ return removed;
52659
+ }
52660
+
52182
52661
  // src/runner-service.ts
52183
52662
  import { execFile as execFileCallback2 } from "node:child_process";
52184
- import { realpathSync } from "node:fs";
52185
- import { access, mkdir as mkdir2, readFile as readFile3, readdir, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
52663
+ import { realpathSync as realpathSync2 } from "node:fs";
52664
+ import { access, mkdir as mkdir2, readFile as readFile3, readdir as readdir2, rm as rm3, writeFile as writeFile2 } from "node:fs/promises";
52186
52665
  import { homedir as homedir2 } from "node:os";
52187
- import path3 from "node:path";
52666
+ import path4 from "node:path";
52188
52667
  import { promisify as promisify2 } from "node:util";
52189
52668
  var execFile2 = promisify2(execFileCallback2);
52190
52669
  var SERVICE_VERBS = ["install-service", "start", "stop", "restart", "status", "logs", "uninstall"];
@@ -52201,9 +52680,9 @@ function isRunnerServiceInvocation(args) {
52201
52680
  function runnerServicePaths(homeDir, runnerName) {
52202
52681
  const safeName = safeRunnerName(runnerName);
52203
52682
  return {
52204
- launchAgentPath: path3.join(homeDir, "Library", "LaunchAgents", `${serviceLabel(safeName)}.plist`),
52205
- stateFile: path3.join(homeDir, "Library", "Application Support", cliBrand.name, `runner-${safeName}.json`),
52206
- logDir: path3.join(homeDir, "Library", "Logs", cliBrand.name, "runner", safeName)
52683
+ launchAgentPath: path4.join(homeDir, "Library", "LaunchAgents", `${serviceLabel(safeName)}.plist`),
52684
+ stateFile: path4.join(homeDir, "Library", "Application Support", cliBrand.name, `runner-${safeName}.json`),
52685
+ logDir: path4.join(homeDir, "Library", "Logs", cliBrand.name, "runner", safeName)
52207
52686
  };
52208
52687
  }
52209
52688
  function renderLaunchdPlist(config2) {
@@ -52245,9 +52724,9 @@ ${config2.allowDevelopmentAppUrl === true ? ` <key>${allowInsecureAppUrlEnv}<
52245
52724
  <key>RunAtLoad</key>
52246
52725
  <true/>
52247
52726
  <key>StandardOutPath</key>
52248
- <string>${escapePlist(path3.join(config2.logDir, "stdout.log"))}</string>
52727
+ <string>${escapePlist(path4.join(config2.logDir, "stdout.log"))}</string>
52249
52728
  <key>StandardErrorPath</key>
52250
- <string>${escapePlist(path3.join(config2.logDir, "stderr.log"))}</string>
52729
+ <string>${escapePlist(path4.join(config2.logDir, "stderr.log"))}</string>
52251
52730
  </dict>
52252
52731
  </plist>
52253
52732
  `;
@@ -52315,8 +52794,8 @@ async function installService(options) {
52315
52794
  const paths = runnerServicePaths(options.homeDir, safeName);
52316
52795
  const label = serviceLabel(safeName);
52317
52796
  await mkdir2(paths.logDir, { recursive: true });
52318
- await mkdir2(path3.dirname(paths.stateFile), { recursive: true });
52319
- await mkdir2(path3.dirname(paths.launchAgentPath), { recursive: true });
52797
+ await mkdir2(path4.dirname(paths.stateFile), { recursive: true });
52798
+ await mkdir2(path4.dirname(paths.launchAgentPath), { recursive: true });
52320
52799
  if (options.userCode !== void 0) {
52321
52800
  const paired = await pairRunnerOnce(options, safeName, paths.stateFile, options.userCode);
52322
52801
  if (!paired) {
@@ -52471,8 +52950,8 @@ async function logsService(options) {
52471
52950
  const safeName = safeRunnerName(options.runnerName);
52472
52951
  const paths = runnerServicePaths(options.homeDir, safeName);
52473
52952
  const label = serviceLabel(safeName);
52474
- const stdoutLog = path3.join(paths.logDir, "stdout.log");
52475
- const stderrLog = path3.join(paths.logDir, "stderr.log");
52953
+ const stdoutLog = path4.join(paths.logDir, "stdout.log");
52954
+ const stderrLog = path4.join(paths.logDir, "stderr.log");
52476
52955
  options.io.stdout.write(
52477
52956
  `${[
52478
52957
  `runner service logs for ${label}:`,
@@ -52503,7 +52982,7 @@ async function uninstallService(options) {
52503
52982
  await bootoutQuietly(options.exec, options.uid, paths.launchAgentPath);
52504
52983
  let removed = false;
52505
52984
  try {
52506
- await rm2(paths.launchAgentPath, { force: true });
52985
+ await rm3(paths.launchAgentPath, { force: true });
52507
52986
  removed = true;
52508
52987
  } catch (error51) {
52509
52988
  options.io.stderr.write(
@@ -52576,10 +53055,10 @@ function serviceTargets(uid, label) {
52576
53055
  }
52577
53056
  async function inspectRunnerServiceInstallations(options = {}) {
52578
53057
  const homeDir = options.homeDir ?? homedir2();
52579
- const launchAgentsDir = path3.join(homeDir, "Library", "LaunchAgents");
53058
+ const launchAgentsDir = path4.join(homeDir, "Library", "LaunchAgents");
52580
53059
  let names = [];
52581
53060
  try {
52582
- const entries = await readdir(launchAgentsDir);
53061
+ const entries = await readdir2(launchAgentsDir);
52583
53062
  names = entries.map((entry) => runnerNameFromPlist(entry)).filter((value) => value !== null).sort((left, right) => left.localeCompare(right));
52584
53063
  } catch {
52585
53064
  return [];
@@ -52705,11 +53184,11 @@ function resolveNodePath() {
52705
53184
  }
52706
53185
  function buildServicePath(nodePath, homeDir) {
52707
53186
  const entries = [
52708
- path3.dirname(nodePath),
53187
+ path4.dirname(nodePath),
52709
53188
  "/opt/homebrew/bin",
52710
53189
  "/usr/local/bin",
52711
- path3.join(homeDir, ".local", "bin"),
52712
- path3.join(homeDir, "bin"),
53190
+ path4.join(homeDir, ".local", "bin"),
53191
+ path4.join(homeDir, "bin"),
52713
53192
  "/usr/bin",
52714
53193
  "/bin",
52715
53194
  "/usr/sbin",
@@ -52758,7 +53237,7 @@ function resolveBinPath() {
52758
53237
  return cliBrand.binName;
52759
53238
  }
52760
53239
  try {
52761
- return realpathSync(entry);
53240
+ return realpathSync2(entry);
52762
53241
  } catch {
52763
53242
  return entry;
52764
53243
  }
@@ -52897,6 +53376,10 @@ async function runDoctor(options) {
52897
53376
  lines.push(`version: ${currentVersion ?? "unknown"}`);
52898
53377
  const pathStatus = await checkPath(env);
52899
53378
  lines.push(pathStatus);
53379
+ const forgeUnsupportedReason = forgeBuildUnsupportedReason(process.platform);
53380
+ if (forgeUnsupportedReason) {
53381
+ lines.push(`forge_build: unsupported - ${forgeUnsupportedReason}`);
53382
+ }
52900
53383
  try {
52901
53384
  const session = await store.read();
52902
53385
  lines.push(`session: ${session ? `present (${store.describe()})` : `missing - run ${cliBrand.binName} login --device`}`);
@@ -53016,9 +53499,8 @@ import { promisify as promisify5 } from "node:util";
53016
53499
  // src/forge-workspace.ts
53017
53500
  import { execFile as execFileCallback4 } from "node:child_process";
53018
53501
  import { createHash } from "node:crypto";
53019
- import { chmod, mkdir as mkdir3, rm as rm3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
53020
- import { tmpdir } from "node:os";
53021
- import path4 from "node:path";
53502
+ import { chmod, lstat, mkdir as mkdir3, mkdtemp, realpath, rm as rm4, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
53503
+ import path5 from "node:path";
53022
53504
  import { setTimeout as sleep2 } from "node:timers/promises";
53023
53505
  import { promisify as promisify4 } from "node:util";
53024
53506
 
@@ -53155,20 +53637,20 @@ function shortRequestId(buildRequestId) {
53155
53637
  return slice.length > 0 ? slice : createHash("sha1").update(buildRequestId).digest("hex").slice(0, 8);
53156
53638
  }
53157
53639
  function requestRoot(baseDir, buildRequestId) {
53158
- return path4.join(baseDir, "work", shortRequestId(buildRequestId));
53640
+ return path5.join(baseDir, "work", shortRequestId(buildRequestId));
53159
53641
  }
53160
53642
  function baseRepoDir(baseDir, buildRequestId) {
53161
- return path4.join(requestRoot(baseDir, buildRequestId), "base");
53643
+ return path5.join(requestRoot(baseDir, buildRequestId), "base");
53162
53644
  }
53163
53645
  function branchName(buildRequestId, changeset) {
53164
53646
  const short = shortRequestId(buildRequestId);
53165
53647
  return changeset.total > 1 ? `forge/${short}-cs${changeset.ordinal}` : `forge/${short}`;
53166
53648
  }
53167
53649
  function buildWorktreeDir(baseDir, buildRequestId, ordinal) {
53168
- return path4.join(requestRoot(baseDir, buildRequestId), `cs${ordinal}`);
53650
+ return path5.join(requestRoot(baseDir, buildRequestId), `cs${ordinal}`);
53169
53651
  }
53170
53652
  function reviewWorktreeDir(baseDir, buildRequestId, ordinal) {
53171
- return path4.join(requestRoot(baseDir, buildRequestId), `review-cs${ordinal}`);
53653
+ return path5.join(requestRoot(baseDir, buildRequestId), `review-cs${ordinal}`);
53172
53654
  }
53173
53655
  function remoteUrlFor(options) {
53174
53656
  if (options.remoteUrl) {
@@ -53178,14 +53660,14 @@ function remoteUrlFor(options) {
53178
53660
  }
53179
53661
  async function pathExists(target) {
53180
53662
  try {
53181
- await stat2(target);
53663
+ await stat3(target);
53182
53664
  return true;
53183
53665
  } catch {
53184
53666
  return false;
53185
53667
  }
53186
53668
  }
53187
53669
  async function isGitRepo(dir) {
53188
- return await pathExists(path4.join(dir, ".git")) || await pathExists(path4.join(dir, "HEAD"));
53670
+ return await pathExists(path5.join(dir, ".git")) || await pathExists(path5.join(dir, "HEAD"));
53189
53671
  }
53190
53672
  async function runGit(args, cwd, timeoutMs = LOCAL_GIT_TIMEOUT_MS) {
53191
53673
  try {
@@ -53237,13 +53719,31 @@ async function withNetworkRetries(op, fn, retryCounter) {
53237
53719
  }
53238
53720
  throw new Error(`${op} exhausted retries: ${lastError instanceof Error ? redactSecrets(lastError.message) : redactSecrets(String(lastError))}`);
53239
53721
  }
53240
- async function openAskpassSession(credential) {
53241
- const helperPath = path4.join(
53242
- // Same temp root the runner uses; the `rost-runner-askpass-` prefix is on the
53243
- // startup sweep's reap list, so a killed turn's orphan is cleaned up.
53244
- tmpdir(),
53245
- `rost-runner-askpass-${createHash("sha256").update(`${Date.now()}:${Math.random()}`).digest("hex").slice(0, 16)}.sh`
53246
- );
53722
+ async function canonicalAskpassBase(baseDir) {
53723
+ return path5.join(await realpath(path5.dirname(baseDir)), path5.basename(baseDir));
53724
+ }
53725
+ async function prepareAskpassTurnDir(baseDir) {
53726
+ await mkdir3(baseDir, { recursive: true, mode: 448 });
53727
+ const baseInfo = await lstat(baseDir);
53728
+ if (baseInfo.isSymbolicLink() || !baseInfo.isDirectory()) {
53729
+ throw new Error(`refusing to use askpass dir ${baseDir}: not a real directory (possible symlink attack)`);
53730
+ }
53731
+ if (typeof process.getuid === "function" && baseInfo.uid !== process.getuid()) {
53732
+ throw new Error(`refusing to use askpass dir ${baseDir}: not owned by this user (possible pre-planted dir)`);
53733
+ }
53734
+ await chmod(baseDir, 448);
53735
+ const turnDir = await mkdtemp(path5.join(baseDir, "rost-runner-askpass-"));
53736
+ const canonicalBase = await canonicalAskpassBase(baseDir);
53737
+ const realTurn = await realpath(turnDir);
53738
+ if (path5.dirname(realTurn) !== canonicalBase) {
53739
+ await rm4(turnDir, { recursive: true, force: true });
53740
+ throw new Error(`refusing to use askpass dir ${turnDir}: resolved outside ${canonicalBase} (possible symlink attack)`);
53741
+ }
53742
+ return turnDir;
53743
+ }
53744
+ async function openAskpassSession(credential, baseDir = RUNNER_ASKPASS_DIR) {
53745
+ const turnDir = await prepareAskpassTurnDir(baseDir);
53746
+ const helperPath = path5.join(turnDir, "askpass.sh");
53247
53747
  const helper = [
53248
53748
  "#!/bin/sh",
53249
53749
  "# One-shot Forge GIT_ASKPASS helper. Contains no secret; reads the token from env.",
@@ -53266,7 +53766,7 @@ async function openAskpassSession(credential) {
53266
53766
  return {
53267
53767
  env,
53268
53768
  cleanup: async () => {
53269
- await rm3(helperPath, { force: true });
53769
+ await rm4(turnDir, { recursive: true, force: true });
53270
53770
  }
53271
53771
  };
53272
53772
  }
@@ -53304,8 +53804,8 @@ async function localBranchExists(baseRepo, branch) {
53304
53804
  async function worktreeRegistered(baseRepo, worktreeDir) {
53305
53805
  try {
53306
53806
  const listing = await runGit(["worktree", "list", "--porcelain"], baseRepo);
53307
- const target = path4.resolve(worktreeDir);
53308
- return listing.split("\n").some((line) => line.startsWith("worktree ") && path4.resolve(line.slice("worktree ".length).trim()) === target);
53807
+ const target = path5.resolve(worktreeDir);
53808
+ return listing.split("\n").some((line) => line.startsWith("worktree ") && path5.resolve(line.slice("worktree ".length).trim()) === target);
53309
53809
  } catch {
53310
53810
  return false;
53311
53811
  }
@@ -53316,7 +53816,7 @@ async function configureForgeIdentity(baseRepo) {
53316
53816
  }
53317
53817
  async function ensureBaseClone(input) {
53318
53818
  const baseRepo = baseRepoDir(input.baseDir, input.buildRequestId);
53319
- await mkdir3(path4.dirname(baseRepo), { recursive: true, mode: 448 });
53819
+ await mkdir3(path5.dirname(baseRepo), { recursive: true, mode: 448 });
53320
53820
  await withLedgerStep(input.ledger, "clone", (retryCounter) => withAskpass(input.credential, async (netEnv) => {
53321
53821
  if (await isGitRepo(baseRepo)) {
53322
53822
  await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", input.url, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
@@ -53365,7 +53865,7 @@ async function prepareMergeWorkspace(options) {
53365
53865
  ledger: options.ledger
53366
53866
  });
53367
53867
  const branch = options.headBranch;
53368
- const worktreeDir = path4.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
53868
+ const worktreeDir = path5.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
53369
53869
  const originDefault = `origin/${options.repo.default_branch}`;
53370
53870
  const originHead = `origin/${branch}`;
53371
53871
  await withLedgerStep(options.ledger, "branch", async () => {
@@ -53402,7 +53902,7 @@ async function prepareReviewWorktree(options, baseRepo) {
53402
53902
  if (await worktreeRegistered(baseRepo, worktreeDir)) {
53403
53903
  await removeWorktree(baseRepo, worktreeDir);
53404
53904
  } else if (await pathExists(worktreeDir)) {
53405
- await rm3(worktreeDir, { recursive: true, force: true });
53905
+ await rm4(worktreeDir, { recursive: true, force: true });
53406
53906
  await runGit(["worktree", "prune"], baseRepo).catch(() => void 0);
53407
53907
  }
53408
53908
  await runGit(["worktree", "add", "--detach", worktreeDir, headSha], baseRepo);
@@ -53735,267 +54235,18 @@ async function removeWorktree(baseRepo, worktreeDir) {
53735
54235
  try {
53736
54236
  await runGit(["worktree", "remove", "--force", worktreeDir], baseRepo);
53737
54237
  } catch {
53738
- await rm3(worktreeDir, { recursive: true, force: true });
54238
+ await rm4(worktreeDir, { recursive: true, force: true });
53739
54239
  }
53740
54240
  await runGit(["worktree", "prune"], baseRepo).catch(() => void 0);
53741
54241
  }
53742
54242
  async function removeWorkspace(ws) {
53743
54243
  if (!await pathExists(ws.baseRepo)) {
53744
- await rm3(ws.worktreeDir, { recursive: true, force: true });
54244
+ await rm4(ws.worktreeDir, { recursive: true, force: true });
53745
54245
  return;
53746
54246
  }
53747
54247
  await removeWorktree(ws.baseRepo, ws.worktreeDir);
53748
54248
  }
53749
54249
 
53750
- // src/runner-sandbox.ts
53751
- import { existsSync as existsSync2, realpathSync as realpathSync2 } from "node:fs";
53752
- import { readdir as readdir2, rm as rm4, stat as stat3 } from "node:fs/promises";
53753
- import path5 from "node:path";
53754
- var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
53755
- var RUNNER_TEMP_PREFIXES = [
53756
- "rost-runner-mcp-",
53757
- "rost-runner-sandbox-",
53758
- "rost-runner-askpass-"
53759
- ];
53760
- var SENSITIVE_READ_SUBPATHS = [
53761
- ".ssh",
53762
- ".aws",
53763
- ".gnupg",
53764
- ".config/gcloud",
53765
- ".config/gh",
53766
- ".kube",
53767
- ".docker",
53768
- ".npmrc"
53769
- ];
53770
- var SENSITIVE_READ_LITERALS = [".netrc"];
53771
- var SENSITIVE_WRITE_SUBPATHS = [
53772
- ".ssh",
53773
- ".aws",
53774
- ".gnupg",
53775
- ".config/git",
53776
- "bin",
53777
- "Library/LaunchAgents"
53778
- ];
53779
- var SENSITIVE_WRITE_LITERALS = [
53780
- ".gitconfig",
53781
- ".zshrc",
53782
- ".zprofile",
53783
- ".zshenv",
53784
- ".zlogin",
53785
- ".bashrc",
53786
- ".bash_profile",
53787
- ".bash_login",
53788
- ".profile"
53789
- ];
53790
- function parseSandboxMode(value) {
53791
- const normalized = (value ?? "auto").trim().toLowerCase();
53792
- if (normalized === "off" || normalized === "strict" || normalized === "auto" || normalized === "") {
53793
- return normalized === "" ? "auto" : normalized;
53794
- }
53795
- return "auto";
53796
- }
53797
- function parseSandboxNetwork(value) {
53798
- const normalized = (value ?? "").trim().toLowerCase();
53799
- if (normalized === "" || normalized === "allow") {
53800
- return { allowNetwork: true, unrecognized: null };
53801
- }
53802
- if (normalized === "deny") {
53803
- return { allowNetwork: false, unrecognized: null };
53804
- }
53805
- return { allowNetwork: true, unrecognized: value ?? "" };
53806
- }
53807
- function sbplString(value) {
53808
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
53809
- }
53810
- function subpath(value) {
53811
- return `(subpath ${sbplString(value)})`;
53812
- }
53813
- function literal2(value) {
53814
- return `(literal ${sbplString(value)})`;
53815
- }
53816
- var CLAUDE_TEMP_SBPL_REGEX = '(regex #"^/private/tmp/claude-[^/]+")';
53817
- var STRICT_SYSTEM_READ_SUBPATHS = [
53818
- "/usr",
53819
- "/bin",
53820
- "/sbin",
53821
- "/System",
53822
- "/Library",
53823
- "/private/etc",
53824
- "/private/var/db",
53825
- "/dev",
53826
- "/opt",
53827
- "/nix"
53828
- ];
53829
- function buildSeatbeltProfile(input) {
53830
- const home = input.homeDir.replace(/\/+$/, "");
53831
- const lines = ["(version 1)"];
53832
- const denyReadBaseSubpaths = input.denyReadPaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
53833
- const denyReadLiterals = input.denyReadPaths.filter((p) => !p.endsWith("/")).map(literal2);
53834
- const denyWriteSubpaths = input.denyWritePaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
53835
- const denyWriteLiterals = input.denyWritePaths.filter((p) => !p.endsWith("/")).map(literal2);
53836
- const credentialReadDenies = [
53837
- ...SENSITIVE_READ_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
53838
- ...SENSITIVE_READ_LITERALS.map((rel) => literal2(`${home}/${rel}`))
53839
- ];
53840
- if (input.strict) {
53841
- lines.push("(deny default)");
53842
- lines.push("(allow process*)");
53843
- lines.push("(allow sysctl-read)");
53844
- lines.push("(allow mach-lookup)");
53845
- lines.push("(allow iokit-open)");
53846
- lines.push("(allow system-socket)");
53847
- lines.push("(allow signal (target self))");
53848
- lines.push("(allow file-read-metadata)");
53849
- const strictReads = [
53850
- ...STRICT_SYSTEM_READ_SUBPATHS.map(subpath),
53851
- subpath(`${home}/.claude`),
53852
- subpath(`${home}/.codex`),
53853
- subpath(input.tmpDir)
53854
- ];
53855
- lines.push(`(allow file-read* ${strictReads.join(" ")})`);
53856
- } else {
53857
- lines.push("(allow default)");
53858
- }
53859
- lines.push("(deny appleevent-send)");
53860
- if (denyReadBaseSubpaths.length > 0) {
53861
- lines.push(`(deny file-read* ${denyReadBaseSubpaths.join(" ")})`);
53862
- }
53863
- const workspaceReads = [subpath(input.workspaceDir), ...input.allowWritePaths.map(subpath), CLAUDE_TEMP_SBPL_REGEX];
53864
- lines.push(`(allow file-read* ${workspaceReads.join(" ")})`);
53865
- lines.push("(deny file-write*)");
53866
- const allowWrites = [
53867
- subpath(input.workspaceDir),
53868
- subpath(input.tmpDir),
53869
- subpath(`${home}/.claude`),
53870
- subpath(`${home}/.codex`),
53871
- ...input.allowWritePaths.map(subpath),
53872
- CLAUDE_TEMP_SBPL_REGEX,
53873
- // DER-1398: Claude Code's per-session + per-command temp dirs.
53874
- subpath("/dev/fd"),
53875
- literal2("/dev/null"),
53876
- literal2("/dev/stdout"),
53877
- literal2("/dev/stderr"),
53878
- literal2("/dev/tty"),
53879
- literal2("/dev/dtracehelper")
53880
- ];
53881
- lines.push(`(allow file-write* ${allowWrites.join(" ")})`);
53882
- lines.push(`(deny file-read* ${[...credentialReadDenies, ...denyReadLiterals].join(" ")})`);
53883
- const denyWrites = [
53884
- ...SENSITIVE_WRITE_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
53885
- ...SENSITIVE_WRITE_LITERALS.map((rel) => literal2(`${home}/${rel}`)),
53886
- // Caller-supplied write-denies (e.g. `<baseRepo>/.git/hooks` + `.git/config`) last so
53887
- // they override the allowWritePaths re-allow of the shared object store above, while
53888
- // `.git/{objects,refs,worktrees,logs,index,HEAD}` stay writable for local commits.
53889
- ...denyWriteSubpaths,
53890
- ...denyWriteLiterals
53891
- ];
53892
- lines.push(`(deny file-write* ${denyWrites.join(" ")})`);
53893
- lines.push('(deny mach-lookup (global-name "com.apple.coreservices.appleevents"))');
53894
- if (!input.allowNetwork) {
53895
- lines.push("(deny network*)");
53896
- } else if (input.strict) {
53897
- lines.push("(allow network*)");
53898
- }
53899
- return `${lines.join("\n")}
53900
- `;
53901
- }
53902
- function canonical(p) {
53903
- try {
53904
- return realpathSync2(p);
53905
- } catch {
53906
- return p;
53907
- }
53908
- }
53909
- function canonicalFile(p) {
53910
- return path5.join(canonical(path5.dirname(p)), path5.basename(p));
53911
- }
53912
- function isSandboxExecAvailable() {
53913
- try {
53914
- return existsSync2(SANDBOX_EXEC_PATH);
53915
- } catch {
53916
- return false;
53917
- }
53918
- }
53919
- function resolveSandboxSpec(options) {
53920
- if (options.mode === "off") {
53921
- return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
53922
- }
53923
- if (options.platform !== "darwin") {
53924
- return { kind: "none", reason: `no OS sandbox on ${options.platform} (seatbelt is macOS-only; Linux confinement is a follow-up)` };
53925
- }
53926
- const available = options.sandboxExecAvailable ?? isSandboxExecAvailable();
53927
- if (!available) {
53928
- return { kind: "none", reason: `sandbox-exec not found at ${SANDBOX_EXEC_PATH}` };
53929
- }
53930
- const workspaceDir = canonical(options.workspaceDir);
53931
- const tmpDir = canonical(options.tmpDir);
53932
- const homeDir = canonical(options.homeDir);
53933
- const allowNetwork = options.allowNetwork ?? true;
53934
- const denyReadPaths = [canonicalFile(options.stateFile)];
53935
- if (options.runnerBaseDir) {
53936
- denyReadPaths.push(`${canonical(options.runnerBaseDir).replace(/\/+$/, "")}/`);
53937
- }
53938
- const denyWritePaths = (options.denyWritePaths ?? []).map(
53939
- (p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
53940
- );
53941
- const allowWritePaths = (options.allowWritePaths ?? []).map(canonical);
53942
- const profile = buildSeatbeltProfile({
53943
- workspaceDir,
53944
- tmpDir,
53945
- homeDir,
53946
- denyReadPaths,
53947
- allowWritePaths,
53948
- denyWritePaths,
53949
- allowNetwork,
53950
- strict: options.mode === "strict"
53951
- });
53952
- return {
53953
- kind: "seatbelt",
53954
- reason: options.mode === "strict" ? "seatbelt (strict: deny-default reads + write confinement)" : "seatbelt (guard: deny sensitive reads/writes)",
53955
- profile,
53956
- allowNetwork
53957
- };
53958
- }
53959
- function wrapCommandWithSandbox(spec, command, args, profilePath) {
53960
- if (spec.kind === "none" || profilePath === null) {
53961
- return { command, args, profilePath: null };
53962
- }
53963
- return {
53964
- command: SANDBOX_EXEC_PATH,
53965
- args: ["-f", profilePath, command, ...args],
53966
- profilePath
53967
- };
53968
- }
53969
- async function sweepStaleRunnerTempFiles(dir, options = {}) {
53970
- const maxAgeMs = options.maxAgeMs ?? 60 * 60 * 1e3;
53971
- const now = options.now ?? Date.now();
53972
- let entries;
53973
- try {
53974
- entries = await readdir2(dir);
53975
- } catch {
53976
- return [];
53977
- }
53978
- const removed = [];
53979
- await Promise.all(
53980
- entries.map(async (name) => {
53981
- if (!RUNNER_TEMP_PREFIXES.some((prefix) => name.startsWith(prefix))) {
53982
- return;
53983
- }
53984
- const full = path5.join(dir, name);
53985
- try {
53986
- const info = await stat3(full);
53987
- if (now - info.mtimeMs < maxAgeMs) {
53988
- return;
53989
- }
53990
- await rm4(full, { force: true });
53991
- removed.push(name);
53992
- } catch {
53993
- }
53994
- })
53995
- );
53996
- return removed;
53997
- }
53998
-
53999
54250
  // src/runner-serve.ts
54000
54251
  var execFile5 = promisify5(execFileCallback5);
54001
54252
  var RUNNER_WORK_ORDER_TOOLS = [
@@ -54011,7 +54262,6 @@ var RUNNER_AICOS_TURN_TOOLS = [
54011
54262
  var CLAUDE_WORKSPACE_WRITE_LOCAL_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
54012
54263
  var CLAUDE_READ_LOCAL_TOOLS = ["Read", "Glob", "Grep"];
54013
54264
  var READ_STUB_TIMEOUT_MS = 18e4;
54014
- var FORGE_TURN_HARD_CEILING_SECONDS = 30 * 60;
54015
54265
  function claudeMcpToolName(name) {
54016
54266
  return name.startsWith("mcp__") ? name : `mcp__rost__${name}`;
54017
54267
  }
@@ -54039,12 +54289,19 @@ ${usage()}
54039
54289
  `);
54040
54290
  if (config2.sandboxNetworkWarning) {
54041
54291
  options.io.stderr.write(`warning: ${config2.sandboxNetworkWarning}
54292
+ `);
54293
+ }
54294
+ const forgeUnsupportedReason = forgeBuildUnsupportedReason(process.platform);
54295
+ if (forgeUnsupportedReason) {
54296
+ options.io.stdout.write(`forge build: ${forgeUnsupportedReason}
54042
54297
  `);
54043
54298
  }
54044
54299
  try {
54045
54300
  const swept = await sweepStaleRunnerTempFiles(tmpdir2());
54046
- if (swept.length > 0) {
54047
- options.io.stdout.write(`swept ${swept.length} stale runner temp file(s)
54301
+ const sweptAskpass = await sweepStaleRunnerTempFiles(RUNNER_ASKPASS_DIR);
54302
+ const total = swept.length + sweptAskpass.length;
54303
+ if (total > 0) {
54304
+ options.io.stdout.write(`swept ${total} stale runner temp file(s)
54048
54305
  `);
54049
54306
  }
54050
54307
  } catch {
@@ -54081,9 +54338,19 @@ ${usage()}
54081
54338
  }
54082
54339
  if (localRuntime) {
54083
54340
  activeSessions += 1;
54341
+ const stopTurnHeartbeat = startTurnHeartbeat({
54342
+ fetchImpl,
54343
+ appUrl: options.appUrl,
54344
+ secret: state.runner_secret,
54345
+ intervalMs: config2.heartbeatMs,
54346
+ capabilities,
54347
+ io: options.io,
54348
+ telemetry: () => detectTelemetry(capabilities, env, activeSessions, cliVersion)
54349
+ });
54084
54350
  try {
54085
54351
  await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
54086
54352
  } finally {
54353
+ stopTurnHeartbeat();
54087
54354
  activeSessions = Math.max(0, activeSessions - 1);
54088
54355
  }
54089
54356
  }
@@ -54115,6 +54382,44 @@ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runt
54115
54382
  }
54116
54383
  }
54117
54384
  }
54385
+ function startTurnHeartbeat(deps) {
54386
+ let stopped = false;
54387
+ let inFlight = false;
54388
+ const timer = setInterval(() => {
54389
+ if (stopped || inFlight) {
54390
+ return;
54391
+ }
54392
+ inFlight = true;
54393
+ const controller = new AbortController();
54394
+ const timeout = setTimeout(() => controller.abort(), deps.intervalMs);
54395
+ timeout.unref?.();
54396
+ void (async () => {
54397
+ try {
54398
+ const response = await post2(deps.fetchImpl, deps.appUrl, "/api/runner/heartbeat", {
54399
+ capabilities: deps.capabilities,
54400
+ telemetry: deps.telemetry()
54401
+ }, deps.secret, controller.signal);
54402
+ if (!stopped && response.status !== 200) {
54403
+ deps.io.stderr.write(`turn heartbeat ${response.status}: ${redactForLog(JSON.stringify(response.json))}
54404
+ `);
54405
+ }
54406
+ } catch (error51) {
54407
+ if (!stopped) {
54408
+ deps.io.stderr.write(`turn heartbeat error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
54409
+ `);
54410
+ }
54411
+ } finally {
54412
+ clearTimeout(timeout);
54413
+ inFlight = false;
54414
+ }
54415
+ })();
54416
+ }, deps.intervalMs);
54417
+ timer.unref?.();
54418
+ return () => {
54419
+ stopped = true;
54420
+ clearInterval(timer);
54421
+ };
54422
+ }
54118
54423
  function resolveAccountAlias(env) {
54119
54424
  return (env.RUNNER_MODEL_ACCOUNT_ALIAS ?? env.ROST_RUNNER_MODEL_ACCOUNT_ALIAS ?? "").trim() || "default";
54120
54425
  }
@@ -54355,7 +54660,7 @@ async function startPairing(client, config2) {
54355
54660
  }
54356
54661
  async function detectCapabilities(cliVersion, env, homeDir) {
54357
54662
  const [claude, codex, git, gh, forgeDiskFreeMb] = await Promise.all([
54358
- probe("claude"),
54663
+ probeClaude(),
54359
54664
  probe("codex"),
54360
54665
  probeGit(),
54361
54666
  probeGh(),
@@ -54416,6 +54721,48 @@ async function probe(command) {
54416
54721
  return { installed: false, version: null, auth_state: "unknown" };
54417
54722
  }
54418
54723
  }
54724
+ async function probeClaude() {
54725
+ const base = await probe("claude");
54726
+ if (!base.installed) {
54727
+ return {
54728
+ ...base,
54729
+ account_alias: null,
54730
+ rate_limited_until: null,
54731
+ resident_session: {
54732
+ status: "unsupported",
54733
+ transport: null,
54734
+ reason: "Claude CLI is not installed on this runner."
54735
+ }
54736
+ };
54737
+ }
54738
+ const streamJsonSupported = await probeClaudeStreamJsonInterface();
54739
+ return {
54740
+ ...base,
54741
+ account_alias: null,
54742
+ rate_limited_until: null,
54743
+ resident_session: streamJsonSupported ? {
54744
+ status: "prototype_gated",
54745
+ transport: "stream-json",
54746
+ reason: "Claude CLI exposes stream-json I/O, but the long-lived resident session path remains explicitly gated until multi-turn subscription behavior is proven on a paired runner."
54747
+ } : {
54748
+ status: "unsupported",
54749
+ transport: null,
54750
+ reason: "Claude CLI on this runner does not expose the required stream-json I/O surface for a resident session prototype."
54751
+ }
54752
+ };
54753
+ }
54754
+ async function probeClaudeStreamJsonInterface() {
54755
+ try {
54756
+ const { stdout, stderr } = await execFile5("claude", ["--help"], { timeout: 1500, maxBuffer: 1024 * 1024 });
54757
+ return supportsClaudeStreamJsonInterface(`${stdout ?? ""}
54758
+ ${stderr ?? ""}`);
54759
+ } catch {
54760
+ return false;
54761
+ }
54762
+ }
54763
+ function supportsClaudeStreamJsonInterface(helpText2) {
54764
+ return helpText2.includes("--input-format") && helpText2.includes("--output-format") && helpText2.includes("stream-json");
54765
+ }
54419
54766
  async function probeGit() {
54420
54767
  try {
54421
54768
  const { stdout, stderr } = await execFile5("git", ["--version"], { timeout: 1500 });
@@ -54453,7 +54800,7 @@ async function probeForgeDiskFreeMb(homeDir) {
54453
54800
  return null;
54454
54801
  }
54455
54802
  }
54456
- async function post2(fetchImpl, appUrl2, pathName, body, token) {
54803
+ async function post2(fetchImpl, appUrl2, pathName, body, token, signal) {
54457
54804
  const headers = { "content-type": "application/json" };
54458
54805
  if (token) {
54459
54806
  headers.authorization = `Bearer ${token}`;
@@ -54461,7 +54808,10 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
54461
54808
  const response = await fetchImpl(`${appUrl2.replace(/\/+$/, "")}${pathName}`, {
54462
54809
  method: "POST",
54463
54810
  headers,
54464
- body: JSON.stringify(body)
54811
+ body: JSON.stringify(body),
54812
+ // Optional per-call abort. Callers that pass no signal are unchanged; only the turn
54813
+ // keepalive uses it to bound a hung POST so its in-flight guard always releases (DER-1484).
54814
+ ...signal ? { signal } : {}
54465
54815
  });
54466
54816
  const text = await response.text();
54467
54817
  let json2;
@@ -54938,6 +55288,13 @@ function buildTurnPrompt(input) {
54938
55288
  `Claimed ${kind === "turn_execution" ? "turn execution" : "work order"} context: ${JSON.stringify(context).slice(0, 8e3)}`
54939
55289
  ].filter((line) => line.length > 0).join("\n");
54940
55290
  }
55291
+ async function sweepOrphanedTurnTempFiles() {
55292
+ try {
55293
+ await sweepStaleRunnerTempFiles(tmpdir2(), { maxAgeMs: RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS });
55294
+ await sweepStaleRunnerTempFiles(RUNNER_ASKPASS_DIR, { maxAgeMs: RUNNER_TEMP_TURN_SWEEP_MAX_AGE_MS });
55295
+ } catch {
55296
+ }
55297
+ }
54941
55298
  async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
54942
55299
  const execution = parseRunnerExecutionContract(workOrder);
54943
55300
  if (execution?.profile === "forge_merge") {
@@ -55020,6 +55377,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
55020
55377
  if (sandboxProfilePath) {
55021
55378
  void rm5(sandboxProfilePath, { force: true });
55022
55379
  }
55380
+ void sweepOrphanedTurnTempFiles();
55023
55381
  if (forgePrep) {
55024
55382
  if (plan.profile === "forge_build" && result.ok && forgePrep.credential.canPush) {
55025
55383
  result = await finalizeForgeBuildPush(ctx, forgePrep, result);
@@ -55211,6 +55569,7 @@ async function spawnMergeConflictModel(ctx, input) {
55211
55569
  if (sandboxProfilePath) {
55212
55570
  void rm5(sandboxProfilePath, { force: true });
55213
55571
  }
55572
+ void sweepOrphanedTurnTempFiles();
55214
55573
  return result;
55215
55574
  }
55216
55575
  async function runForgeMergeTurn(ctx, workOrder, execution, runtime) {
@@ -55308,6 +55667,20 @@ async function executeForgeMerge(ctx, input) {
55308
55667
  function scrubTranscriptSessionIds(value) {
55309
55668
  return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
55310
55669
  }
55670
+ function killProcessTree(child) {
55671
+ const pid = child.pid;
55672
+ if (typeof pid === "number") {
55673
+ try {
55674
+ process.kill(-pid, "SIGKILL");
55675
+ return;
55676
+ } catch {
55677
+ }
55678
+ }
55679
+ try {
55680
+ child.kill("SIGKILL");
55681
+ } catch {
55682
+ }
55683
+ }
55311
55684
  function runModelProcess(input) {
55312
55685
  return new Promise((resolve2) => {
55313
55686
  let settled = false;
@@ -55321,12 +55694,20 @@ function runModelProcess(input) {
55321
55694
  const child = spawn(input.command, input.args, {
55322
55695
  cwd: input.cwd,
55323
55696
  env: input.env,
55324
- stdio: ["ignore", "pipe", "pipe"]
55697
+ stdio: ["ignore", "pipe", "pipe"],
55698
+ // DER-1569 FIX 1: give the turn its OWN process group so a timeout SIGKILL reaps the
55699
+ // WHOLE tree, not just the direct child. sandbox-exec execvp's into the model (pid
55700
+ // preserved), and the model's Bash tool forks build/test grandchildren that inherit this
55701
+ // pgid; a plain `child.kill()` signals only the direct pid and orphans them to init.
55702
+ // `detached` makes the child a group leader (pgid == child.pid) so `process.kill(-pid)`
55703
+ // in killProcessTree hits the model and every descendant. We still await `close`, so the
55704
+ // reference is kept (no `unref()`); the parent lifecycle is unchanged.
55705
+ detached: true
55325
55706
  });
55326
55707
  let out = "";
55327
55708
  let err = "";
55328
55709
  const timer = setTimeout(() => {
55329
- child.kill("SIGKILL");
55710
+ killProcessTree(child);
55330
55711
  const partial2 = scrubTranscriptSessionIds(redactForLog((out || err || "").trim()));
55331
55712
  finish({
55332
55713
  ok: false,
@@ -55544,6 +55925,15 @@ function usage() {
55544
55925
  }
55545
55926
 
55546
55927
  // src/index.ts
55928
+ async function defaultConfirmPrompt(question) {
55929
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
55930
+ try {
55931
+ const answer = (await rl.question(`${question} `)).trim().toLowerCase();
55932
+ return answer === "y" || answer === "yes";
55933
+ } finally {
55934
+ rl.close();
55935
+ }
55936
+ }
55547
55937
  async function main(argv = process.argv.slice(2), options = {}) {
55548
55938
  const io = options.io ?? {
55549
55939
  stdout: process.stdout,
@@ -55552,7 +55942,10 @@ async function main(argv = process.argv.slice(2), options = {}) {
55552
55942
  // and stdout must be TTYs. A piped/headless/agent session fails this and so
55553
55943
  // will not auto-approve its own mint/revoke confirmation unless skip-
55554
55944
  // permissions is explicitly enabled.
55555
- interactive: process.stdin.isTTY === true && process.stdout.isTTY === true
55945
+ interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
55946
+ // DER-1490: a real session prompts the human on stdin before approving a
55947
+ // pending confirmation gate.
55948
+ confirm: defaultConfirmPrompt
55556
55949
  };
55557
55950
  const login = options.login ?? loginWithBrowser;
55558
55951
  const deviceLogin = options.deviceLogin ?? loginWithDeviceCode;
@@ -55982,11 +56375,13 @@ async function executeMcp(io, client, appUrl2, args, env = process.env) {
55982
56375
  const options = parseMcpInstallOptions(args.slice(1));
55983
56376
  const envSkipPermissions = env[`${cliBrand.envPrefix}_CLI_SKIP_PERMISSIONS`] === "1";
55984
56377
  const canAutoApprove = io.interactive === true || options.skipPermissions || envSkipPermissions;
56378
+ const reviewed = io.interactive === true;
55985
56379
  const rendered = await renderMcpInstall({
55986
56380
  client,
55987
56381
  appUrl: appUrl2,
55988
56382
  options,
55989
- canAutoApprove
56383
+ canAutoApprove,
56384
+ reviewed
55990
56385
  });
55991
56386
  io.stdout.write(rendered.stdout);
55992
56387
  if (rendered.stderr) {
@@ -56056,15 +56451,21 @@ async function printCommandOutput(io, client, commandId, body = {}, format, opti
56056
56451
  return 0;
56057
56452
  } catch (error51) {
56058
56453
  const envAutoConfirm = process.env[`${cliBrand.envPrefix}_CLI_AUTO_CONFIRM`] === "1";
56059
- if (io.interactive === true || options.autoConfirm === true || envAutoConfirm) {
56060
- const pending = pendingConfirmationFromError2(error51);
56061
- if (pending && pending.commandId === commandId) {
56062
- const reason = io.interactive === true ? "interactive CLI user" : options.autoConfirm === true ? "explicit CLI auto-confirm" : `${cliBrand.envPrefix}_CLI_AUTO_CONFIRM environment auto-confirm`;
56063
- io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the ${reason}.
56064
- `);
56454
+ const pending = pendingConfirmationFromError2(error51);
56455
+ if (pending && pending.commandId === commandId && (io.interactive === true || options.autoConfirm === true || envAutoConfirm)) {
56456
+ renderPendingConfirmationGate3(io, pending);
56457
+ if (io.interactive === true) {
56458
+ const approved = io.confirm !== void 0 && await io.confirm(`Approve ${pending.commandId} (confirmation ${pending.confirmationId})? [y/N]`);
56459
+ if (!approved) {
56460
+ io.stderr.write("Not approved.\n");
56461
+ return 1;
56462
+ }
56065
56463
  try {
56066
- const approved = await client.execute("confirmation.approve", { confirmation_id: pending.confirmationId });
56067
- const approvedOutput = asRecord3(approved.output).output ?? approved.output;
56464
+ const approvedResult = await client.execute("confirmation.approve", {
56465
+ confirmation_id: pending.confirmationId,
56466
+ reviewed: true
56467
+ });
56468
+ const approvedOutput = asRecord3(approvedResult.output).output ?? approvedResult.output;
56068
56469
  const rendered = format ? format(approvedOutput) : JSON.stringify(approvedOutput, null, 2);
56069
56470
  io.stdout.write(`${rendered}
56070
56471
  `);
@@ -56073,6 +56474,19 @@ async function printCommandOutput(io, client, commandId, body = {}, format, opti
56073
56474
  return printCommandError2(io, approvalError);
56074
56475
  }
56075
56476
  }
56477
+ const reason = options.autoConfirm === true ? "explicit CLI auto-confirm" : `${cliBrand.envPrefix}_CLI_AUTO_CONFIRM environment auto-confirm`;
56478
+ io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the ${reason}.
56479
+ `);
56480
+ try {
56481
+ const approvedResult = await client.execute("confirmation.approve", { confirmation_id: pending.confirmationId });
56482
+ const approvedOutput = asRecord3(approvedResult.output).output ?? approvedResult.output;
56483
+ const rendered = format ? format(approvedOutput) : JSON.stringify(approvedOutput, null, 2);
56484
+ io.stdout.write(`${rendered}
56485
+ `);
56486
+ return 0;
56487
+ } catch (approvalError) {
56488
+ return printCommandError2(io, approvalError);
56489
+ }
56076
56490
  }
56077
56491
  return printCommandError2(io, error51);
56078
56492
  }
@@ -56095,7 +56509,32 @@ function pendingConfirmationFromError2(error51) {
56095
56509
  if (typeof confirmationId !== "string" || confirmationId.length === 0 || typeof commandId !== "string" || commandId.length === 0) {
56096
56510
  return null;
56097
56511
  }
56098
- return { confirmationId, commandId };
56512
+ const summary = typeof details.summary === "string" ? details.summary : void 0;
56513
+ const riskLevel = typeof details.riskLevel === "string" ? details.riskLevel : void 0;
56514
+ return {
56515
+ confirmationId,
56516
+ commandId,
56517
+ ...summary === void 0 ? {} : { summary },
56518
+ ...riskLevel === void 0 ? {} : { riskLevel },
56519
+ ...details.diff === void 0 ? {} : { diff: details.diff }
56520
+ };
56521
+ }
56522
+ function renderPendingConfirmationGate3(io, pending) {
56523
+ io.stderr.write(`Confirmation required: ${pending.commandId} (${pending.confirmationId})
56524
+ `);
56525
+ if (pending.summary !== void 0) {
56526
+ io.stderr.write(`${redactForLog(pending.summary)}
56527
+ `);
56528
+ }
56529
+ if (pending.riskLevel !== void 0) {
56530
+ io.stderr.write(`Risk level: ${pending.riskLevel}
56531
+ `);
56532
+ }
56533
+ if (pending.diff !== void 0) {
56534
+ io.stderr.write(`Diff:
56535
+ ${redactForLog(pending.diff)}
56536
+ `);
56537
+ }
56099
56538
  }
56100
56539
  function printCommandError2(io, error51) {
56101
56540
  if (error51 instanceof CommandClientError && error51.response && !error51.response.ok) {