@rosthq/cli 0.7.56 → 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/doctor.d.ts.map +1 -1
- package/dist/forge-workspace.d.ts +1 -0
- package/dist/forge-workspace.d.ts.map +1 -1
- package/dist/generated/command-manifest.d.ts.map +1 -1
- package/dist/generator/groups.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +741 -367
- package/dist/index.js.map +4 -4
- package/dist/mcp-install.d.ts +1 -0
- package/dist/mcp-install.d.ts.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/runner-sandbox.d.ts +5 -0
- package/dist/runner-sandbox.d.ts.map +1 -1
- package/dist/runner-serve.d.ts +73 -0
- package/dist/runner-serve.d.ts.map +1 -1
- package/package.json +1 -1
- package/prompts/how-tos/human-confirmations.md +2 -1
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
|
|
@@ -39211,6 +39212,13 @@ var AGENT_POLICY_DEFAULT = {
|
|
|
39211
39212
|
max_autonomous_risk: AGENT_POLICY_PROFILE_CEILINGS.balanced
|
|
39212
39213
|
};
|
|
39213
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
|
+
|
|
39214
39222
|
// ../../packages/protocol/src/aicos.ts
|
|
39215
39223
|
var uuidSchema5 = external_exports.string().uuid();
|
|
39216
39224
|
var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
@@ -39457,17 +39465,38 @@ var AICOS_ATTACHMENT_CONTENT_TYPES = ["image/png", "image/jpeg", "image/webp", "
|
|
|
39457
39465
|
var AICOS_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
39458
39466
|
var AICOS_MAX_ATTACHMENTS_PER_TURN = 4;
|
|
39459
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);
|
|
39460
39470
|
var aicosAttachmentSchema = external_exports.object({
|
|
39461
39471
|
id: uuidSchema5,
|
|
39462
39472
|
contentType: aicosAttachmentContentTypeSchema,
|
|
39463
39473
|
sizeBytes: external_exports.number().int().positive(),
|
|
39464
39474
|
width: external_exports.number().int().positive().nullable(),
|
|
39465
39475
|
height: external_exports.number().int().positive().nullable(),
|
|
39466
|
-
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")
|
|
39467
39479
|
}).strict();
|
|
39468
39480
|
var aicosAttachmentUploadResultSchema = external_exports.object({
|
|
39469
39481
|
attachment: aicosAttachmentSchema
|
|
39470
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();
|
|
39471
39500
|
var aicosMessageSchema = external_exports.object({
|
|
39472
39501
|
id: uuidSchema5,
|
|
39473
39502
|
sessionId: uuidSchema5,
|
|
@@ -39509,6 +39538,7 @@ var aicosAgentBuilderMetadataSchema = external_exports.object({
|
|
|
39509
39538
|
agentBuilder: agentBuilderCompilationSchema
|
|
39510
39539
|
}).strict();
|
|
39511
39540
|
var aicosTurnRequestSchema = external_exports.object({
|
|
39541
|
+
requestId: uuidSchema5,
|
|
39512
39542
|
sessionId: uuidSchema5.nullable().optional(),
|
|
39513
39543
|
purpose: aicosConversationPurposeSchema.default("general"),
|
|
39514
39544
|
// Empty is permitted only when attachments are present (an image-only turn);
|
|
@@ -41176,6 +41206,8 @@ var MCP_ONBOARDING_TOOL_NAMES = {
|
|
|
41176
41206
|
setCompass: `${MCP_TOOL_PREFIX}_set_compass`,
|
|
41177
41207
|
staffSeat: `${MCP_TOOL_PREFIX}_staff_seat`
|
|
41178
41208
|
};
|
|
41209
|
+
var FORGE_TURN_HARD_CEILING_SECONDS = 30 * 60;
|
|
41210
|
+
var FORGE_TURN_PHASE_OVERHEAD_SECONDS = 20 * 60;
|
|
41179
41211
|
|
|
41180
41212
|
// ../../packages/protocol/src/mcp-onboarding.ts
|
|
41181
41213
|
var MCP_TOKEN_ABSOLUTE_MAX_EXPIRY_MS = (MCP_TOKEN_NO_EXPIRY_SENTINEL_DAYS + 1) * 24 * 60 * 60 * 1e3;
|
|
@@ -41363,7 +41395,15 @@ var pendingConfirmationSchema = external_exports.object({
|
|
|
41363
41395
|
}).strict()
|
|
41364
41396
|
}).strict();
|
|
41365
41397
|
var confirmationApproveInputSchema = external_exports.object({
|
|
41366
|
-
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()
|
|
41367
41407
|
}).strict();
|
|
41368
41408
|
var confirmationRejectInputSchema = external_exports.object({
|
|
41369
41409
|
confirmation_id: external_exports.string().min(1),
|
|
@@ -41910,7 +41950,7 @@ var fleetDigestAgentSchema = fleetOverviewRowSchema.extend({
|
|
|
41910
41950
|
spend_7d_usd: external_exports.string(),
|
|
41911
41951
|
spend_30d_usd: external_exports.string(),
|
|
41912
41952
|
cost_drift_percent: external_exports.number().int().nullable(),
|
|
41913
|
-
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"]),
|
|
41914
41954
|
health_reasons: external_exports.array(external_exports.string()),
|
|
41915
41955
|
recent_failed_runs: external_exports.array(fleetDigestRunSchema),
|
|
41916
41956
|
unresolved_errors: external_exports.array(fleetDigestErrorSchema),
|
|
@@ -42150,6 +42190,7 @@ var runnerExecutionStateSchema = external_exports.enum([
|
|
|
42150
42190
|
"processing",
|
|
42151
42191
|
"idle_unproven",
|
|
42152
42192
|
"queued_unclaimed",
|
|
42193
|
+
"model_account_exhausted",
|
|
42153
42194
|
"missing_runtime",
|
|
42154
42195
|
"not_connected",
|
|
42155
42196
|
"revoked",
|
|
@@ -42165,8 +42206,9 @@ var runnerExecutionReadinessSchema = external_exports.object({
|
|
|
42165
42206
|
recent_completed_at: isoDateTime4.nullable(),
|
|
42166
42207
|
recent_failed_at: isoDateTime4.nullable()
|
|
42167
42208
|
}).strict();
|
|
42209
|
+
var runnerSandboxPostureSchema = external_exports.enum(["guard", "strict", "off", "none", "unknown"]);
|
|
42168
42210
|
var runnerPostureSchema = external_exports.object({
|
|
42169
|
-
sandbox:
|
|
42211
|
+
sandbox: runnerSandboxPostureSchema,
|
|
42170
42212
|
network: external_exports.enum(["allow", "deny", "unknown"]),
|
|
42171
42213
|
capability_set: external_exports.array(external_exports.string())
|
|
42172
42214
|
}).strict();
|
|
@@ -42408,6 +42450,17 @@ var agentPolicyUpdateInputSchema = external_exports.object({
|
|
|
42408
42450
|
});
|
|
42409
42451
|
}
|
|
42410
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();
|
|
42411
42464
|
var tenantSpendStatusSchema = external_exports.object({
|
|
42412
42465
|
spend_usd: external_exports.string(),
|
|
42413
42466
|
soft_cap_usd: external_exports.string().nullable(),
|
|
@@ -42903,6 +42956,20 @@ var softwareTerminationBoundSchema = external_exports.object({
|
|
|
42903
42956
|
max_wall_clock_seconds: external_exports.number().int().positive().max(7 * 24 * 60 * 60).optional(),
|
|
42904
42957
|
cost_budget_usd: external_exports.number().nonnegative().max(1e6).optional()
|
|
42905
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
|
+
});
|
|
42906
42973
|
var softwareBuildRequestSummarySchema = external_exports.object({
|
|
42907
42974
|
id: uuid10,
|
|
42908
42975
|
software_project_id: uuid10,
|
|
@@ -42917,7 +42984,8 @@ var softwareBuildRequestSummarySchema = external_exports.object({
|
|
|
42917
42984
|
max_wall_clock_seconds: external_exports.number().int().nullable(),
|
|
42918
42985
|
cost_budget_usd: external_exports.string().nullable(),
|
|
42919
42986
|
iterations_used: external_exports.number().int(),
|
|
42920
|
-
created_at: external_exports.string()
|
|
42987
|
+
created_at: external_exports.string(),
|
|
42988
|
+
scheduler_state: softwarePhaseSchedulerStateSchema.nullable()
|
|
42921
42989
|
});
|
|
42922
42990
|
var softwarePhaseRunSummarySchema = external_exports.object({
|
|
42923
42991
|
id: uuid10,
|
|
@@ -42926,7 +42994,8 @@ var softwarePhaseRunSummarySchema = external_exports.object({
|
|
|
42926
42994
|
attempt_no: external_exports.number().int(),
|
|
42927
42995
|
entered_at: external_exports.string().nullable(),
|
|
42928
42996
|
exited_at: external_exports.string().nullable(),
|
|
42929
|
-
created_at: external_exports.string()
|
|
42997
|
+
created_at: external_exports.string(),
|
|
42998
|
+
scheduler_state: softwarePhaseSchedulerStateSchema.nullable()
|
|
42930
42999
|
});
|
|
42931
43000
|
var softwareGateSummarySchema = external_exports.object({
|
|
42932
43001
|
id: uuid10,
|
|
@@ -43077,7 +43146,8 @@ var softwareRequestCreateOutputSchema = external_exports.object({
|
|
|
43077
43146
|
software_project_id: uuid10,
|
|
43078
43147
|
status: softwareRequestStatusSchema,
|
|
43079
43148
|
current_phase: softwarePhaseSchema,
|
|
43080
|
-
phase_run_id: uuid10
|
|
43149
|
+
phase_run_id: uuid10,
|
|
43150
|
+
scheduler_state: softwarePhaseSchedulerStateSchema
|
|
43081
43151
|
});
|
|
43082
43152
|
var softwareRequestListInputSchema = external_exports.object({
|
|
43083
43153
|
limit: external_exports.number().int().positive().max(200).optional()
|
|
@@ -44198,6 +44268,13 @@ var issueEventPayloadSchema = external_exports.object({
|
|
|
44198
44268
|
severity: external_exports.enum(["low", "med", "high", "critical"])
|
|
44199
44269
|
}).strict();
|
|
44200
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
|
+
|
|
44201
44278
|
// ../../packages/protocol/src/operating.ts
|
|
44202
44279
|
var uuidSchema17 = external_exports.string().uuid();
|
|
44203
44280
|
var taskListInputSchema = external_exports.object({}).strict();
|
|
@@ -44363,7 +44440,12 @@ var escalationDecisionOutputSchema = external_exports.object({
|
|
|
44363
44440
|
status: escalationStatusSchema,
|
|
44364
44441
|
decision_id: uuidSchema18,
|
|
44365
44442
|
decided_by: uuidSchema18,
|
|
44366
|
-
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()
|
|
44367
44449
|
}).strict();
|
|
44368
44450
|
|
|
44369
44451
|
// ../../packages/protocol/src/index.ts
|
|
@@ -44789,7 +44871,7 @@ The Compass is drafted, then activated by a human through supersession.
|
|
|
44789
44871
|
order: 15,
|
|
44790
44872
|
title: "AICOS chat guide",
|
|
44791
44873
|
summary: "How the AI Chief of Staff chat works in the authenticated app shell and what it can safely do today.",
|
|
44792
|
-
version: "2026-07-
|
|
44874
|
+
version: "2026-07-10.2",
|
|
44793
44875
|
public: true,
|
|
44794
44876
|
audiences: ["human", "in_app_agent"],
|
|
44795
44877
|
stages: ["company_setup", "operating_rhythm"],
|
|
@@ -44857,10 +44939,12 @@ Each turn carries server-normalized page context: route template, current path,
|
|
|
44857
44939
|
|
|
44858
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.
|
|
44859
44941
|
|
|
44860
|
-
AICOS also has an explicit brain selection. Cloud can use the included managed allowance or a tenant Anthropic key (BYOK). Runner
|
|
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.
|
|
44861
44943
|
|
|
44862
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.
|
|
44863
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
|
+
|
|
44864
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.
|
|
44865
44949
|
|
|
44866
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.
|
|
@@ -44875,7 +44959,7 @@ You can attach images to a message from the composer. {{brand}} accepts PNG, JPE
|
|
|
44875
44959
|
|
|
44876
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.
|
|
44877
44961
|
|
|
44878
|
-
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.
|
|
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.
|
|
44879
44963
|
|
|
44880
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.
|
|
44881
44965
|
|
|
@@ -44938,7 +45022,7 @@ Treat AICOS as a coordinator over the operating system. It can become more usefu
|
|
|
44938
45022
|
order: 20,
|
|
44939
45023
|
title: "Responsibility Graph playbook",
|
|
44940
45024
|
summary: "How to build a functions-first graph with seats, owners, Stewards, vacancies, and clean authority.",
|
|
44941
|
-
version: "2026-07-
|
|
45025
|
+
version: "2026-07-10.1",
|
|
44942
45026
|
public: true,
|
|
44943
45027
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
44944
45028
|
stages: ["graph_design", "staffing"],
|
|
@@ -44982,7 +45066,7 @@ The graph always resolves to a single canonical root seat (Founder & CEO in the
|
|
|
44982
45066
|
|
|
44983
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.
|
|
44984
45068
|
|
|
44985
|
-
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.
|
|
44986
45070
|
|
|
44987
45071
|
## Forge Developer Team seats are pinned
|
|
44988
45072
|
|
|
@@ -45015,7 +45099,9 @@ The graph also carries a mission-control panel. It pulls the same operational st
|
|
|
45015
45099
|
|
|
45016
45100
|
## An agent seat's profile
|
|
45017
45101
|
|
|
45018
|
-
A staffed agent's seat page
|
|
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.
|
|
45019
45105
|
|
|
45020
45106
|
## Review an agent seat's delivered work
|
|
45021
45107
|
|
|
@@ -45598,7 +45684,7 @@ Use the lower-level commands after health names a finding: \`agent.get_run\` for
|
|
|
45598
45684
|
order: 46,
|
|
45599
45685
|
title: "Tool access and vault",
|
|
45600
45686
|
summary: "How to give agents access to tools without exposing raw credentials or expanding authority by accident.",
|
|
45601
|
-
version: "2026-07-02.
|
|
45687
|
+
version: "2026-07-02.4",
|
|
45602
45688
|
public: true,
|
|
45603
45689
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
45604
45690
|
stages: ["staffing"],
|
|
@@ -45656,7 +45742,7 @@ Google can be connected from Settings once the workspace OAuth app is configured
|
|
|
45656
45742
|
|
|
45657
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.
|
|
45658
45744
|
|
|
45659
|
-
\`integration.connect_rest\`
|
|
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.
|
|
45660
45746
|
|
|
45661
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.
|
|
45662
45748
|
|
|
@@ -45676,7 +45762,7 @@ There is exactly one way to give a connected tool its credential, and it is the
|
|
|
45676
45762
|
|
|
45677
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.
|
|
45678
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.
|
|
45679
|
-
- Store a secret: \`credential.ingress\`
|
|
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.)
|
|
45680
45766
|
- Sign the manifest: \`charter.sign_manifest\` / \`rost_sign_charter_manifest\` requests human confirmation for the seat's permission manifest.
|
|
45681
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\`.
|
|
45682
45768
|
|
|
@@ -45744,7 +45830,7 @@ External connectors are being rolled out provider by provider, conservatively (r
|
|
|
45744
45830
|
order: 48,
|
|
45745
45831
|
title: "CLI and MCP installation guide",
|
|
45746
45832
|
summary: "Install the public CLI, register remote token-backed MCP clients, and find the full command and tool catalog.",
|
|
45747
|
-
version: "2026-07-
|
|
45833
|
+
version: "2026-07-10.2",
|
|
45748
45834
|
public: true,
|
|
45749
45835
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
45750
45836
|
stages: ["company_setup", "staffing"],
|
|
@@ -46105,7 +46191,7 @@ A leaked tenant-admin token can administer the whole tenant, not just one seat.
|
|
|
46105
46191
|
|
|
46106
46192
|
### Storing the Anthropic key and other credentials
|
|
46107
46193
|
|
|
46108
|
-
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
|
|
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.
|
|
46109
46195
|
|
|
46110
46196
|
## When to use MCP or CLI
|
|
46111
46197
|
|
|
@@ -46210,7 +46296,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
|
|
|
46210
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\` |
|
|
46211
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\` |
|
|
46212
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\` |
|
|
46213
|
-
| \`{{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"\` |
|
|
46214
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\` |
|
|
46215
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\` |
|
|
46216
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\` |
|
|
@@ -46223,7 +46309,7 @@ The signed-in app exposes the same Skill command surface at **Skills**, linked f
|
|
|
46223
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\` |
|
|
46224
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\` |
|
|
46225
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\` |
|
|
46226
|
-
| \`{{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.
|
|
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"\` |
|
|
46227
46313
|
|
|
46228
46314
|
Skills wrapper help:
|
|
46229
46315
|
|
|
@@ -46316,8 +46402,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
|
|
|
46316
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. |
|
|
46317
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. |
|
|
46318
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. |
|
|
46319
|
-
| \`rost_approve_pending_confirmation\` | \`confirmation.approve\` | Approve a pending confirmation. | Tenant | Call with \`{"confirmation_id":"<confirmation-id>"}\`. |
|
|
46320
|
-
| \`rost_reject_pending_confirmation\` | \`confirmation.reject\` | Reject a pending confirmation. | Tenant | Call with \`{"confirmation_id":"<confirmation-id>"}\`. |
|
|
46321
46405
|
| \`rost_draft_compass\` | \`compass.draft\` | Create a Compass draft. | Tenant | Call with a concise Compass document. |
|
|
46322
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. |
|
|
46323
46407
|
| \`rost_approve_compass_version\` | \`compass.approve_version\` | Activate a Compass version by supersession. | Tenant | Call after human review. |
|
|
@@ -46327,7 +46411,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
|
|
|
46327
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\`. |
|
|
46328
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. |
|
|
46329
46413
|
| \`rost_staff_seat\` | \`staffing.assign\` | Compatibility helper for human, agent, or hybrid staffing. | Seat or tenant-admin | Call with \`seat_id\` and \`occupant\`. |
|
|
46330
|
-
| \`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. |
|
|
46331
46414
|
| \`rost_go_live\` | \`agent.go_live\` | Promote a dry-run agent seat to live. | Seat or tenant-admin | Call after human approval. |
|
|
46332
46415
|
| \`rost_create_mcp_token\` | \`mcp_token.create\` | Mint a tenant-admin or seat-scoped MCP token. | Tenant | Prefer \`{{cli}} mcp install\` for users. |
|
|
46333
46416
|
| \`rost_revoke_mcp_token\` | \`mcp_token.revoke\` | Revoke an MCP token immediately. | Tenant | Call with \`token_id\`. |
|
|
@@ -46381,7 +46464,6 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
|
|
|
46381
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\`. |
|
|
46382
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\`. |
|
|
46383
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. |
|
|
46384
|
-
| \`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. |
|
|
46385
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. |
|
|
46386
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>"}\`. |
|
|
46387
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. |
|
|
@@ -46397,8 +46479,10 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
|
|
|
46397
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"}\`. |
|
|
46398
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. |
|
|
46399
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. |
|
|
46400
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. |
|
|
46401
|
-
| \`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":"
|
|
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. |
|
|
46402
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. |
|
|
46403
46487
|
| \`rost_get_signal\` | \`signal.get\` | Read a measurable with its full reading history. | Seat or tenant-admin | Call with \`{"measurable_id":"<id>"}\`. |
|
|
46404
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>"}\`. |
|
|
@@ -46465,13 +46549,13 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
|
|
|
46465
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. |
|
|
46466
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 \`{}\`. |
|
|
46467
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. |
|
|
46468
|
-
| \`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>", ...}\`). |
|
|
46469
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. |
|
|
46470
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. |
|
|
46471
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. |
|
|
46472
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\`. |
|
|
46473
|
-
| \`rost_list_forge_build_requests\` | \`software_factory.request.list\` | The Forge control-room board: build requests with status, current phase, and
|
|
46474
|
-
| \`rost_show_a_forge_build_request\` | \`software_factory.request.show\` | The Forge request detail: the request, current phase, phase-run history, and
|
|
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>"}\`. |
|
|
46475
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"}\`. |
|
|
46476
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. |
|
|
46477
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}\`. |
|
|
@@ -46495,7 +46579,7 @@ Several rows here are seat-operating commands (\`task.create\`, the \`signal.*\`
|
|
|
46495
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. |
|
|
46496
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>"}\`. |
|
|
46497
46581
|
|
|
46498
|
-
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.
|
|
46499
46583
|
|
|
46500
46584
|
#### Forge GitHub App access
|
|
46501
46585
|
|
|
@@ -47095,7 +47179,7 @@ Agents can suggest commitments and report progress. They should not create a new
|
|
|
47095
47179
|
order: 61,
|
|
47096
47180
|
title: "Signal guide",
|
|
47097
47181
|
summary: "How to define and read measurables so the company runs on evidence instead of status theater.",
|
|
47098
|
-
version: "2026-07-
|
|
47182
|
+
version: "2026-07-08.1",
|
|
47099
47183
|
public: true,
|
|
47100
47184
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
47101
47185
|
stages: ["operating_rhythm"],
|
|
@@ -47158,6 +47242,10 @@ Avoid vanity numbers, manual-only status fields, and metrics nobody can act on.
|
|
|
47158
47242
|
|
|
47159
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.
|
|
47160
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
|
+
|
|
47161
47249
|
## Import a scorecard from CSV
|
|
47162
47250
|
|
|
47163
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.
|
|
@@ -47519,11 +47607,11 @@ Do not claim that a live charge happened unless Stripe test/live evidence proves
|
|
|
47519
47607
|
order: 72,
|
|
47520
47608
|
title: "Settings guide",
|
|
47521
47609
|
summary: "How to use Settings as the control plane for company access, channels, providers, tokens, and operating defaults.",
|
|
47522
|
-
version: "2026-07-
|
|
47610
|
+
version: "2026-07-09.2",
|
|
47523
47611
|
public: true,
|
|
47524
47612
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
47525
47613
|
stages: ["company_setup", "staffing"],
|
|
47526
|
-
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"],
|
|
47527
47615
|
legal: { publicRisk: "low", notes: ["{{brand}}-native settings guidance."] },
|
|
47528
47616
|
sources: [
|
|
47529
47617
|
{
|
|
@@ -47544,7 +47632,7 @@ Start with members and invites, then provider and channel connections, then MCP
|
|
|
47544
47632
|
|
|
47545
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.
|
|
47546
47634
|
|
|
47547
|
-
|
|
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.
|
|
47548
47636
|
|
|
47549
47637
|
## What belongs in Settings
|
|
47550
47638
|
|
|
@@ -47600,7 +47688,7 @@ Agents that run on {{brand}}-managed inference draw against a tenant inference b
|
|
|
47600
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.
|
|
47601
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.
|
|
47602
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.
|
|
47603
|
-
- 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
|
|
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.
|
|
47604
47692
|
|
|
47605
47693
|
## Company autonomy ceiling (Company Guardrails)
|
|
47606
47694
|
|
|
@@ -47613,6 +47701,15 @@ The company autonomy ceiling is a single tenant-wide dial that caps how much ANY
|
|
|
47613
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.
|
|
47614
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.
|
|
47615
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
|
+
|
|
47616
47713
|
## Agent guidance
|
|
47617
47714
|
|
|
47618
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.`
|
|
@@ -47662,7 +47759,7 @@ When a user asks to add a person, clarify whether they mean app access, seat occ
|
|
|
47662
47759
|
order: 74,
|
|
47663
47760
|
title: "Notifications guide",
|
|
47664
47761
|
summary: "How {{brand}} should notify humans about decisions, escalations, stale work, and agent boundaries.",
|
|
47665
|
-
version: "2026-
|
|
47762
|
+
version: "2026-07-09.1",
|
|
47666
47763
|
public: true,
|
|
47667
47764
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
47668
47765
|
stages: ["operating_rhythm"],
|
|
@@ -47687,6 +47784,7 @@ Notifications should move decisions to the right human without turning {{brand}}
|
|
|
47687
47784
|
- A Signal is broken or stale.
|
|
47688
47785
|
- A Friction item needs a decision.
|
|
47689
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).
|
|
47690
47788
|
|
|
47691
47789
|
## Diagnose failed deliveries
|
|
47692
47790
|
|
|
@@ -47701,7 +47799,7 @@ Every notification should include the seat, cause, evidence, and requested decis
|
|
|
47701
47799
|
order: 75,
|
|
47702
47800
|
title: "Local runner guide",
|
|
47703
47801
|
summary: "How local agent sessions and runner surfaces should operate through {{brand}} without bypassing Charters or audit.",
|
|
47704
|
-
version: "2026-07-
|
|
47802
|
+
version: "2026-07-10.1",
|
|
47705
47803
|
public: true,
|
|
47706
47804
|
audiences: ["human", "cli", "mcp", "in_app_agent"],
|
|
47707
47805
|
stages: ["staffing", "operating_rhythm"],
|
|
@@ -47753,11 +47851,11 @@ The runner bearer secret is stored locally and is never displayed. Each claimed
|
|
|
47753
47851
|
|
|
47754
47852
|
## Interactive AICOS runner turns
|
|
47755
47853
|
|
|
47756
|
-
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
|
|
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.
|
|
47757
47855
|
|
|
47758
47856
|
## Forge scheduler work
|
|
47759
47857
|
|
|
47760
|
-
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
|
|
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.
|
|
47761
47859
|
|
|
47762
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.
|
|
47763
47861
|
|
|
@@ -48639,7 +48737,7 @@ This worked document **omits** \`unanswered_boundaries\` and \`seat_type_recomme
|
|
|
48639
48737
|
order: 43,
|
|
48640
48738
|
title: "Agent builder guide",
|
|
48641
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.",
|
|
48642
|
-
version: "2026-
|
|
48740
|
+
version: "2026-07-10.1",
|
|
48643
48741
|
public: true,
|
|
48644
48742
|
audiences: ["cli", "mcp", "in_app_agent"],
|
|
48645
48743
|
stages: ["staffing"],
|
|
@@ -48683,7 +48781,7 @@ Building teams of controlled agent workers is the product's core differentiator.
|
|
|
48683
48781
|
## The full setup sequence
|
|
48684
48782
|
|
|
48685
48783
|
1. **Seat** \u2014 the agent occupies a seat in the Responsibility Graph (a function, not a person). Create or pick the seat first.
|
|
48686
|
-
2. **Steward** \u2014 every agent needs a human steward chain (invariant: no orphan agents).
|
|
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.
|
|
48687
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).
|
|
48688
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.
|
|
48689
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.
|
|
@@ -48972,6 +49070,13 @@ var approveViaSchema = external_exports.object({
|
|
|
48972
49070
|
var pendingConfirmationSchema2 = external_exports.object({
|
|
48973
49071
|
confirmationId: external_exports.string().min(1),
|
|
48974
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(),
|
|
48975
49080
|
approveVia: approveViaSchema.optional().catch(void 0)
|
|
48976
49081
|
}).passthrough();
|
|
48977
49082
|
var confirmationApprovalOutputSchema = external_exports.object({
|
|
@@ -49088,16 +49193,22 @@ function brandEnvKey3(suffix) {
|
|
|
49088
49193
|
async function renderMcpInstall(input) {
|
|
49089
49194
|
const stderrLines = [];
|
|
49090
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
|
+
};
|
|
49091
49202
|
if (input.options.rotateTokenId === void 0 && input.options.scope === void 0) {
|
|
49092
49203
|
throw new Error(MISSING_SCOPE_MESSAGE);
|
|
49093
49204
|
}
|
|
49094
|
-
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,
|
|
49095
|
-
const token = await createMcpToken(input.client, mcpTokenCreateBody(mintScope, input.options.expiry),
|
|
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);
|
|
49096
49207
|
let rotationOldRevoked = null;
|
|
49097
49208
|
if (input.options.rotateTokenId !== void 0) {
|
|
49098
49209
|
const oldTokenId = input.options.rotateTokenId;
|
|
49099
49210
|
try {
|
|
49100
|
-
const output = await executeWithConfirmation(input.client, "mcp_token.revoke", { token_id: oldTokenId },
|
|
49211
|
+
const output = await executeWithConfirmation(input.client, "mcp_token.revoke", { token_id: oldTokenId }, approval);
|
|
49101
49212
|
const revoke = mcpTokenRevokeOutputSchema2.safeParse(output);
|
|
49102
49213
|
rotationOldRevoked = revoke.success && revoke.data.revoked === true;
|
|
49103
49214
|
if (!rotationOldRevoked) {
|
|
@@ -49178,9 +49289,9 @@ function mcpTokenCreateBody(mint, expiry) {
|
|
|
49178
49289
|
}
|
|
49179
49290
|
return base;
|
|
49180
49291
|
}
|
|
49181
|
-
async function resolveRotationScope(client, options,
|
|
49292
|
+
async function resolveRotationScope(client, options, approval) {
|
|
49182
49293
|
const oldTokenId = options.rotateTokenId;
|
|
49183
|
-
const listOutput = await executeWithConfirmation(client, "mcp_token.list", { include_revoked: true },
|
|
49294
|
+
const listOutput = await executeWithConfirmation(client, "mcp_token.list", { include_revoked: true }, approval);
|
|
49184
49295
|
const parsed = mcpTokenListOutputSchema2.safeParse(listOutput);
|
|
49185
49296
|
if (!parsed.success) {
|
|
49186
49297
|
throw new Error("Could not read the token list to rotate \u2014 mcp_token.list returned an unexpected shape.");
|
|
@@ -49243,11 +49354,11 @@ function revokeFailureReason(error51) {
|
|
|
49243
49354
|
}
|
|
49244
49355
|
return "unknown error";
|
|
49245
49356
|
}
|
|
49246
|
-
async function createMcpToken(client, body,
|
|
49247
|
-
const output = await executeWithConfirmation(client, "mcp_token.create", body,
|
|
49357
|
+
async function createMcpToken(client, body, approval) {
|
|
49358
|
+
const output = await executeWithConfirmation(client, "mcp_token.create", body, approval);
|
|
49248
49359
|
return mcpTokenOutputSchema.parse(output);
|
|
49249
49360
|
}
|
|
49250
|
-
async function executeWithConfirmation(client, commandId, body,
|
|
49361
|
+
async function executeWithConfirmation(client, commandId, body, approval) {
|
|
49251
49362
|
try {
|
|
49252
49363
|
const result = await client.execute(commandId, body);
|
|
49253
49364
|
return result.output;
|
|
@@ -49256,7 +49367,7 @@ async function executeWithConfirmation(client, commandId, body, canAutoApprove)
|
|
|
49256
49367
|
if (!pending) {
|
|
49257
49368
|
throw error51;
|
|
49258
49369
|
}
|
|
49259
|
-
if (!canAutoApprove) {
|
|
49370
|
+
if (!approval.canAutoApprove) {
|
|
49260
49371
|
throw new PendingConfirmationError({
|
|
49261
49372
|
confirmationId: pending.confirmationId,
|
|
49262
49373
|
commandId: pending.commandId,
|
|
@@ -49264,13 +49375,33 @@ async function executeWithConfirmation(client, commandId, body, canAutoApprove)
|
|
|
49264
49375
|
...pending.approveVia?.webUrl === void 0 ? {} : { approveUrl: pending.approveVia.webUrl }
|
|
49265
49376
|
});
|
|
49266
49377
|
}
|
|
49378
|
+
approval.pushStderr(renderPendingConfirmationGate(pending));
|
|
49267
49379
|
const approved = await client.execute("confirmation.approve", {
|
|
49268
|
-
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 } : {}
|
|
49269
49386
|
});
|
|
49270
49387
|
const approvalOutput = confirmationApprovalOutputSchema.parse(approved.output);
|
|
49271
49388
|
return approvalOutput.output;
|
|
49272
49389
|
}
|
|
49273
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
|
+
}
|
|
49274
49405
|
function pendingCommandConfirmation(error51, commandId) {
|
|
49275
49406
|
if (!(error51 instanceof CommandClientError) || error51.status !== 409 || !error51.response || error51.response.ok) {
|
|
49276
49407
|
return null;
|
|
@@ -49592,9 +49723,9 @@ var COMMAND_MANIFEST = [
|
|
|
49592
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." },
|
|
49593
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." },
|
|
49594
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." },
|
|
49595
|
-
{ "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." },
|
|
49596
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." },
|
|
49597
|
-
{ "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":
|
|
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." },
|
|
49598
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." },
|
|
49599
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." },
|
|
49600
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." },
|
|
@@ -49627,7 +49758,7 @@ var COMMAND_MANIFEST = [
|
|
|
49627
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." },
|
|
49628
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." },
|
|
49629
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." },
|
|
49630
|
-
{ "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":
|
|
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." },
|
|
49631
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." },
|
|
49632
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." },
|
|
49633
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." },
|
|
@@ -49669,6 +49800,8 @@ var COMMAND_MANIFEST = [
|
|
|
49669
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." },
|
|
49670
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." },
|
|
49671
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." },
|
|
49672
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." },
|
|
49673
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." },
|
|
49674
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." },
|
|
@@ -50646,6 +50779,14 @@ function renderAgentPolicySet(output) {
|
|
|
50646
50779
|
const policy = asRecord(asRecord(output).agent_policy);
|
|
50647
50780
|
return `Set company autonomy ceiling to ${field(policy, "profile")} (max_autonomous_risk=${field(policy, "max_autonomous_risk")}, enforcement=${field(policy, "enforcement")}).`;
|
|
50648
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
|
+
}
|
|
50649
50790
|
function renderMemberInvite(output) {
|
|
50650
50791
|
const record2 = asRecord(output);
|
|
50651
50792
|
return `Invited ${field(record2, "email")} as ${field(record2, "role")} (invite ${field(record2, "invite_id")}).`;
|
|
@@ -51209,6 +51350,13 @@ var agentPolicySetHandler = bespoke("settings.agent_policy.update", (parsed) =>
|
|
|
51209
51350
|
enforcement: optionalValue(parsed, "enforcement")
|
|
51210
51351
|
});
|
|
51211
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);
|
|
51212
51360
|
var seatDecommissionHandler = (context, rest) => {
|
|
51213
51361
|
const parsed = parseFlags(rest, /* @__PURE__ */ new Set(["dry-run", "preview", "yes", "y"]));
|
|
51214
51362
|
if (parsed.flags.has("dry-run") || parsed.flags.has("preview")) {
|
|
@@ -51230,6 +51378,13 @@ var settingsAgentPolicyHandler = (context, rest) => dispatch(context, "settings
|
|
|
51230
51378
|
},
|
|
51231
51379
|
set: (ctx, r) => agentPolicySetHandler(ctx, r)
|
|
51232
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`);
|
|
51233
51388
|
var agentSetupHandler = (context, rest) => dispatch(context, "agent setup", rest, {
|
|
51234
51389
|
status: (ctx, r) => {
|
|
51235
51390
|
const parsed = parseFlags(r, /* @__PURE__ */ new Set(["yes", "y"]));
|
|
@@ -51408,6 +51563,7 @@ var GROUP_SPECS = {
|
|
|
51408
51563
|
update: { commandId: "settings.update", handler: settingsUpdateHandler, render: renderSettingsUpdate },
|
|
51409
51564
|
"product-learning": { commandId: "settings.product_learning.get", handler: settingsProductLearningHandler },
|
|
51410
51565
|
"agent-policy": { commandId: "settings.agent_policy.get", handler: settingsAgentPolicyHandler },
|
|
51566
|
+
"confirmation-mode": { commandId: "settings.confirmation_policy.get", handler: settingsConfirmationModeHandler },
|
|
51411
51567
|
rename: { commandId: "tenant.rename", handler: settingsRenameHandler, render: renderTenantRename }
|
|
51412
51568
|
}
|
|
51413
51569
|
},
|
|
@@ -51524,6 +51680,8 @@ var EXTRA_SURFACED = /* @__PURE__ */ new Set([
|
|
|
51524
51680
|
// settings product-learning update
|
|
51525
51681
|
"settings.agent_policy.update",
|
|
51526
51682
|
// settings agent-policy set
|
|
51683
|
+
"settings.confirmation_policy.update",
|
|
51684
|
+
// settings confirmation-mode set
|
|
51527
51685
|
"skill.import_github"
|
|
51528
51686
|
// skills import github
|
|
51529
51687
|
]);
|
|
@@ -51627,7 +51785,7 @@ function operationUsageLines(bin) {
|
|
|
51627
51785
|
`${bin} runner install-service|start|stop|restart|status|logs|uninstall --name <name>`,
|
|
51628
51786
|
`${bin} forge project-create|project-list|request-create|request-list|request-show|request-pause|request-cancel|request-resume`,
|
|
51629
51787
|
`${bin} notification settings|test|errors`,
|
|
51630
|
-
`${bin} settings get|update|product-learning|agent-policy|rename`,
|
|
51788
|
+
`${bin} settings get|update|product-learning|agent-policy|confirmation-mode|rename`,
|
|
51631
51789
|
`${bin} member invite|update|remove`,
|
|
51632
51790
|
`${bin} agent templates|create|setup|tools|dry-run|go-live|status|run-now|fleet-digest|get-run|show`,
|
|
51633
51791
|
`${bin} tools list`,
|
|
@@ -51945,9 +52103,10 @@ async function executeSkillCommand(context, commandId, body, options = {}) {
|
|
|
51945
52103
|
} catch (error51) {
|
|
51946
52104
|
const pending = context.io.interactive === true ? pendingConfirmationFromError(error51) : null;
|
|
51947
52105
|
if (pending && pending.commandId === commandId) {
|
|
52106
|
+
renderPendingConfirmationGate2(context.io, pending);
|
|
51948
52107
|
context.io.stderr.write(`Approving ${pending.commandId} confirmation ${pending.confirmationId} as the interactive CLI user.
|
|
51949
52108
|
`);
|
|
51950
|
-
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 });
|
|
51951
52110
|
return asRecord2(approved.output).output ?? approved.output;
|
|
51952
52111
|
}
|
|
51953
52112
|
throw error51;
|
|
@@ -52193,7 +52352,30 @@ function pendingConfirmationFromError(error51) {
|
|
|
52193
52352
|
if (typeof confirmationId !== "string" || confirmationId.length === 0 || typeof commandId !== "string" || commandId.length === 0) {
|
|
52194
52353
|
return null;
|
|
52195
52354
|
}
|
|
52196
|
-
|
|
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
|
+
}
|
|
52197
52379
|
}
|
|
52198
52380
|
|
|
52199
52381
|
// src/doctor.ts
|
|
@@ -52202,12 +52384,286 @@ import { access as access2 } from "node:fs/promises";
|
|
|
52202
52384
|
import { delimiter } from "node:path";
|
|
52203
52385
|
import { promisify as promisify3 } from "node:util";
|
|
52204
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
|
+
|
|
52205
52661
|
// src/runner-service.ts
|
|
52206
52662
|
import { execFile as execFileCallback2 } from "node:child_process";
|
|
52207
|
-
import { realpathSync } from "node:fs";
|
|
52208
|
-
import { access, mkdir as mkdir2, readFile as readFile3, readdir, rm as
|
|
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";
|
|
52209
52665
|
import { homedir as homedir2 } from "node:os";
|
|
52210
|
-
import
|
|
52666
|
+
import path4 from "node:path";
|
|
52211
52667
|
import { promisify as promisify2 } from "node:util";
|
|
52212
52668
|
var execFile2 = promisify2(execFileCallback2);
|
|
52213
52669
|
var SERVICE_VERBS = ["install-service", "start", "stop", "restart", "status", "logs", "uninstall"];
|
|
@@ -52224,9 +52680,9 @@ function isRunnerServiceInvocation(args) {
|
|
|
52224
52680
|
function runnerServicePaths(homeDir, runnerName) {
|
|
52225
52681
|
const safeName = safeRunnerName(runnerName);
|
|
52226
52682
|
return {
|
|
52227
|
-
launchAgentPath:
|
|
52228
|
-
stateFile:
|
|
52229
|
-
logDir:
|
|
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)
|
|
52230
52686
|
};
|
|
52231
52687
|
}
|
|
52232
52688
|
function renderLaunchdPlist(config2) {
|
|
@@ -52268,9 +52724,9 @@ ${config2.allowDevelopmentAppUrl === true ? ` <key>${allowInsecureAppUrlEnv}<
|
|
|
52268
52724
|
<key>RunAtLoad</key>
|
|
52269
52725
|
<true/>
|
|
52270
52726
|
<key>StandardOutPath</key>
|
|
52271
|
-
<string>${escapePlist(
|
|
52727
|
+
<string>${escapePlist(path4.join(config2.logDir, "stdout.log"))}</string>
|
|
52272
52728
|
<key>StandardErrorPath</key>
|
|
52273
|
-
<string>${escapePlist(
|
|
52729
|
+
<string>${escapePlist(path4.join(config2.logDir, "stderr.log"))}</string>
|
|
52274
52730
|
</dict>
|
|
52275
52731
|
</plist>
|
|
52276
52732
|
`;
|
|
@@ -52338,8 +52794,8 @@ async function installService(options) {
|
|
|
52338
52794
|
const paths = runnerServicePaths(options.homeDir, safeName);
|
|
52339
52795
|
const label = serviceLabel(safeName);
|
|
52340
52796
|
await mkdir2(paths.logDir, { recursive: true });
|
|
52341
|
-
await mkdir2(
|
|
52342
|
-
await mkdir2(
|
|
52797
|
+
await mkdir2(path4.dirname(paths.stateFile), { recursive: true });
|
|
52798
|
+
await mkdir2(path4.dirname(paths.launchAgentPath), { recursive: true });
|
|
52343
52799
|
if (options.userCode !== void 0) {
|
|
52344
52800
|
const paired = await pairRunnerOnce(options, safeName, paths.stateFile, options.userCode);
|
|
52345
52801
|
if (!paired) {
|
|
@@ -52494,8 +52950,8 @@ async function logsService(options) {
|
|
|
52494
52950
|
const safeName = safeRunnerName(options.runnerName);
|
|
52495
52951
|
const paths = runnerServicePaths(options.homeDir, safeName);
|
|
52496
52952
|
const label = serviceLabel(safeName);
|
|
52497
|
-
const stdoutLog =
|
|
52498
|
-
const stderrLog =
|
|
52953
|
+
const stdoutLog = path4.join(paths.logDir, "stdout.log");
|
|
52954
|
+
const stderrLog = path4.join(paths.logDir, "stderr.log");
|
|
52499
52955
|
options.io.stdout.write(
|
|
52500
52956
|
`${[
|
|
52501
52957
|
`runner service logs for ${label}:`,
|
|
@@ -52526,7 +52982,7 @@ async function uninstallService(options) {
|
|
|
52526
52982
|
await bootoutQuietly(options.exec, options.uid, paths.launchAgentPath);
|
|
52527
52983
|
let removed = false;
|
|
52528
52984
|
try {
|
|
52529
|
-
await
|
|
52985
|
+
await rm3(paths.launchAgentPath, { force: true });
|
|
52530
52986
|
removed = true;
|
|
52531
52987
|
} catch (error51) {
|
|
52532
52988
|
options.io.stderr.write(
|
|
@@ -52599,10 +53055,10 @@ function serviceTargets(uid, label) {
|
|
|
52599
53055
|
}
|
|
52600
53056
|
async function inspectRunnerServiceInstallations(options = {}) {
|
|
52601
53057
|
const homeDir = options.homeDir ?? homedir2();
|
|
52602
|
-
const launchAgentsDir =
|
|
53058
|
+
const launchAgentsDir = path4.join(homeDir, "Library", "LaunchAgents");
|
|
52603
53059
|
let names = [];
|
|
52604
53060
|
try {
|
|
52605
|
-
const entries = await
|
|
53061
|
+
const entries = await readdir2(launchAgentsDir);
|
|
52606
53062
|
names = entries.map((entry) => runnerNameFromPlist(entry)).filter((value) => value !== null).sort((left, right) => left.localeCompare(right));
|
|
52607
53063
|
} catch {
|
|
52608
53064
|
return [];
|
|
@@ -52728,11 +53184,11 @@ function resolveNodePath() {
|
|
|
52728
53184
|
}
|
|
52729
53185
|
function buildServicePath(nodePath, homeDir) {
|
|
52730
53186
|
const entries = [
|
|
52731
|
-
|
|
53187
|
+
path4.dirname(nodePath),
|
|
52732
53188
|
"/opt/homebrew/bin",
|
|
52733
53189
|
"/usr/local/bin",
|
|
52734
|
-
|
|
52735
|
-
|
|
53190
|
+
path4.join(homeDir, ".local", "bin"),
|
|
53191
|
+
path4.join(homeDir, "bin"),
|
|
52736
53192
|
"/usr/bin",
|
|
52737
53193
|
"/bin",
|
|
52738
53194
|
"/usr/sbin",
|
|
@@ -52781,7 +53237,7 @@ function resolveBinPath() {
|
|
|
52781
53237
|
return cliBrand.binName;
|
|
52782
53238
|
}
|
|
52783
53239
|
try {
|
|
52784
|
-
return
|
|
53240
|
+
return realpathSync2(entry);
|
|
52785
53241
|
} catch {
|
|
52786
53242
|
return entry;
|
|
52787
53243
|
}
|
|
@@ -52920,6 +53376,10 @@ async function runDoctor(options) {
|
|
|
52920
53376
|
lines.push(`version: ${currentVersion ?? "unknown"}`);
|
|
52921
53377
|
const pathStatus = await checkPath(env);
|
|
52922
53378
|
lines.push(pathStatus);
|
|
53379
|
+
const forgeUnsupportedReason = forgeBuildUnsupportedReason(process.platform);
|
|
53380
|
+
if (forgeUnsupportedReason) {
|
|
53381
|
+
lines.push(`forge_build: unsupported - ${forgeUnsupportedReason}`);
|
|
53382
|
+
}
|
|
52923
53383
|
try {
|
|
52924
53384
|
const session = await store.read();
|
|
52925
53385
|
lines.push(`session: ${session ? `present (${store.describe()})` : `missing - run ${cliBrand.binName} login --device`}`);
|
|
@@ -53039,9 +53499,8 @@ import { promisify as promisify5 } from "node:util";
|
|
|
53039
53499
|
// src/forge-workspace.ts
|
|
53040
53500
|
import { execFile as execFileCallback4 } from "node:child_process";
|
|
53041
53501
|
import { createHash } from "node:crypto";
|
|
53042
|
-
import { chmod, mkdir as mkdir3, rm as
|
|
53043
|
-
import
|
|
53044
|
-
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";
|
|
53045
53504
|
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
53046
53505
|
import { promisify as promisify4 } from "node:util";
|
|
53047
53506
|
|
|
@@ -53178,20 +53637,20 @@ function shortRequestId(buildRequestId) {
|
|
|
53178
53637
|
return slice.length > 0 ? slice : createHash("sha1").update(buildRequestId).digest("hex").slice(0, 8);
|
|
53179
53638
|
}
|
|
53180
53639
|
function requestRoot(baseDir, buildRequestId) {
|
|
53181
|
-
return
|
|
53640
|
+
return path5.join(baseDir, "work", shortRequestId(buildRequestId));
|
|
53182
53641
|
}
|
|
53183
53642
|
function baseRepoDir(baseDir, buildRequestId) {
|
|
53184
|
-
return
|
|
53643
|
+
return path5.join(requestRoot(baseDir, buildRequestId), "base");
|
|
53185
53644
|
}
|
|
53186
53645
|
function branchName(buildRequestId, changeset) {
|
|
53187
53646
|
const short = shortRequestId(buildRequestId);
|
|
53188
53647
|
return changeset.total > 1 ? `forge/${short}-cs${changeset.ordinal}` : `forge/${short}`;
|
|
53189
53648
|
}
|
|
53190
53649
|
function buildWorktreeDir(baseDir, buildRequestId, ordinal) {
|
|
53191
|
-
return
|
|
53650
|
+
return path5.join(requestRoot(baseDir, buildRequestId), `cs${ordinal}`);
|
|
53192
53651
|
}
|
|
53193
53652
|
function reviewWorktreeDir(baseDir, buildRequestId, ordinal) {
|
|
53194
|
-
return
|
|
53653
|
+
return path5.join(requestRoot(baseDir, buildRequestId), `review-cs${ordinal}`);
|
|
53195
53654
|
}
|
|
53196
53655
|
function remoteUrlFor(options) {
|
|
53197
53656
|
if (options.remoteUrl) {
|
|
@@ -53201,14 +53660,14 @@ function remoteUrlFor(options) {
|
|
|
53201
53660
|
}
|
|
53202
53661
|
async function pathExists(target) {
|
|
53203
53662
|
try {
|
|
53204
|
-
await
|
|
53663
|
+
await stat3(target);
|
|
53205
53664
|
return true;
|
|
53206
53665
|
} catch {
|
|
53207
53666
|
return false;
|
|
53208
53667
|
}
|
|
53209
53668
|
}
|
|
53210
53669
|
async function isGitRepo(dir) {
|
|
53211
|
-
return await pathExists(
|
|
53670
|
+
return await pathExists(path5.join(dir, ".git")) || await pathExists(path5.join(dir, "HEAD"));
|
|
53212
53671
|
}
|
|
53213
53672
|
async function runGit(args, cwd, timeoutMs = LOCAL_GIT_TIMEOUT_MS) {
|
|
53214
53673
|
try {
|
|
@@ -53260,13 +53719,31 @@ async function withNetworkRetries(op, fn, retryCounter) {
|
|
|
53260
53719
|
}
|
|
53261
53720
|
throw new Error(`${op} exhausted retries: ${lastError instanceof Error ? redactSecrets(lastError.message) : redactSecrets(String(lastError))}`);
|
|
53262
53721
|
}
|
|
53263
|
-
async function
|
|
53264
|
-
|
|
53265
|
-
|
|
53266
|
-
|
|
53267
|
-
|
|
53268
|
-
|
|
53269
|
-
)
|
|
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");
|
|
53270
53747
|
const helper = [
|
|
53271
53748
|
"#!/bin/sh",
|
|
53272
53749
|
"# One-shot Forge GIT_ASKPASS helper. Contains no secret; reads the token from env.",
|
|
@@ -53289,7 +53766,7 @@ async function openAskpassSession(credential) {
|
|
|
53289
53766
|
return {
|
|
53290
53767
|
env,
|
|
53291
53768
|
cleanup: async () => {
|
|
53292
|
-
await
|
|
53769
|
+
await rm4(turnDir, { recursive: true, force: true });
|
|
53293
53770
|
}
|
|
53294
53771
|
};
|
|
53295
53772
|
}
|
|
@@ -53327,8 +53804,8 @@ async function localBranchExists(baseRepo, branch) {
|
|
|
53327
53804
|
async function worktreeRegistered(baseRepo, worktreeDir) {
|
|
53328
53805
|
try {
|
|
53329
53806
|
const listing = await runGit(["worktree", "list", "--porcelain"], baseRepo);
|
|
53330
|
-
const target =
|
|
53331
|
-
return listing.split("\n").some((line) => line.startsWith("worktree ") &&
|
|
53807
|
+
const target = path5.resolve(worktreeDir);
|
|
53808
|
+
return listing.split("\n").some((line) => line.startsWith("worktree ") && path5.resolve(line.slice("worktree ".length).trim()) === target);
|
|
53332
53809
|
} catch {
|
|
53333
53810
|
return false;
|
|
53334
53811
|
}
|
|
@@ -53339,7 +53816,7 @@ async function configureForgeIdentity(baseRepo) {
|
|
|
53339
53816
|
}
|
|
53340
53817
|
async function ensureBaseClone(input) {
|
|
53341
53818
|
const baseRepo = baseRepoDir(input.baseDir, input.buildRequestId);
|
|
53342
|
-
await mkdir3(
|
|
53819
|
+
await mkdir3(path5.dirname(baseRepo), { recursive: true, mode: 448 });
|
|
53343
53820
|
await withLedgerStep(input.ledger, "clone", (retryCounter) => withAskpass(input.credential, async (netEnv) => {
|
|
53344
53821
|
if (await isGitRepo(baseRepo)) {
|
|
53345
53822
|
await runGitNetwork("git fetch", ["fetch", "--prune", "--quiet", input.url, "+refs/heads/*:refs/remotes/origin/*"], netEnv, baseRepo, FETCH_TIMEOUT_MS, retryCounter);
|
|
@@ -53388,7 +53865,7 @@ async function prepareMergeWorkspace(options) {
|
|
|
53388
53865
|
ledger: options.ledger
|
|
53389
53866
|
});
|
|
53390
53867
|
const branch = options.headBranch;
|
|
53391
|
-
const worktreeDir =
|
|
53868
|
+
const worktreeDir = path5.join(requestRoot(options.baseDir, options.buildRequestId), "merge");
|
|
53392
53869
|
const originDefault = `origin/${options.repo.default_branch}`;
|
|
53393
53870
|
const originHead = `origin/${branch}`;
|
|
53394
53871
|
await withLedgerStep(options.ledger, "branch", async () => {
|
|
@@ -53425,7 +53902,7 @@ async function prepareReviewWorktree(options, baseRepo) {
|
|
|
53425
53902
|
if (await worktreeRegistered(baseRepo, worktreeDir)) {
|
|
53426
53903
|
await removeWorktree(baseRepo, worktreeDir);
|
|
53427
53904
|
} else if (await pathExists(worktreeDir)) {
|
|
53428
|
-
await
|
|
53905
|
+
await rm4(worktreeDir, { recursive: true, force: true });
|
|
53429
53906
|
await runGit(["worktree", "prune"], baseRepo).catch(() => void 0);
|
|
53430
53907
|
}
|
|
53431
53908
|
await runGit(["worktree", "add", "--detach", worktreeDir, headSha], baseRepo);
|
|
@@ -53758,267 +54235,18 @@ async function removeWorktree(baseRepo, worktreeDir) {
|
|
|
53758
54235
|
try {
|
|
53759
54236
|
await runGit(["worktree", "remove", "--force", worktreeDir], baseRepo);
|
|
53760
54237
|
} catch {
|
|
53761
|
-
await
|
|
54238
|
+
await rm4(worktreeDir, { recursive: true, force: true });
|
|
53762
54239
|
}
|
|
53763
54240
|
await runGit(["worktree", "prune"], baseRepo).catch(() => void 0);
|
|
53764
54241
|
}
|
|
53765
54242
|
async function removeWorkspace(ws) {
|
|
53766
54243
|
if (!await pathExists(ws.baseRepo)) {
|
|
53767
|
-
await
|
|
54244
|
+
await rm4(ws.worktreeDir, { recursive: true, force: true });
|
|
53768
54245
|
return;
|
|
53769
54246
|
}
|
|
53770
54247
|
await removeWorktree(ws.baseRepo, ws.worktreeDir);
|
|
53771
54248
|
}
|
|
53772
54249
|
|
|
53773
|
-
// src/runner-sandbox.ts
|
|
53774
|
-
import { existsSync as existsSync2, realpathSync as realpathSync2 } from "node:fs";
|
|
53775
|
-
import { readdir as readdir2, rm as rm4, stat as stat3 } from "node:fs/promises";
|
|
53776
|
-
import path5 from "node:path";
|
|
53777
|
-
var SANDBOX_EXEC_PATH = "/usr/bin/sandbox-exec";
|
|
53778
|
-
var RUNNER_TEMP_PREFIXES = [
|
|
53779
|
-
"rost-runner-mcp-",
|
|
53780
|
-
"rost-runner-sandbox-",
|
|
53781
|
-
"rost-runner-askpass-"
|
|
53782
|
-
];
|
|
53783
|
-
var SENSITIVE_READ_SUBPATHS = [
|
|
53784
|
-
".ssh",
|
|
53785
|
-
".aws",
|
|
53786
|
-
".gnupg",
|
|
53787
|
-
".config/gcloud",
|
|
53788
|
-
".config/gh",
|
|
53789
|
-
".kube",
|
|
53790
|
-
".docker",
|
|
53791
|
-
".npmrc"
|
|
53792
|
-
];
|
|
53793
|
-
var SENSITIVE_READ_LITERALS = [".netrc"];
|
|
53794
|
-
var SENSITIVE_WRITE_SUBPATHS = [
|
|
53795
|
-
".ssh",
|
|
53796
|
-
".aws",
|
|
53797
|
-
".gnupg",
|
|
53798
|
-
".config/git",
|
|
53799
|
-
"bin",
|
|
53800
|
-
"Library/LaunchAgents"
|
|
53801
|
-
];
|
|
53802
|
-
var SENSITIVE_WRITE_LITERALS = [
|
|
53803
|
-
".gitconfig",
|
|
53804
|
-
".zshrc",
|
|
53805
|
-
".zprofile",
|
|
53806
|
-
".zshenv",
|
|
53807
|
-
".zlogin",
|
|
53808
|
-
".bashrc",
|
|
53809
|
-
".bash_profile",
|
|
53810
|
-
".bash_login",
|
|
53811
|
-
".profile"
|
|
53812
|
-
];
|
|
53813
|
-
function parseSandboxMode(value) {
|
|
53814
|
-
const normalized = (value ?? "auto").trim().toLowerCase();
|
|
53815
|
-
if (normalized === "off" || normalized === "strict" || normalized === "auto" || normalized === "") {
|
|
53816
|
-
return normalized === "" ? "auto" : normalized;
|
|
53817
|
-
}
|
|
53818
|
-
return "auto";
|
|
53819
|
-
}
|
|
53820
|
-
function parseSandboxNetwork(value) {
|
|
53821
|
-
const normalized = (value ?? "").trim().toLowerCase();
|
|
53822
|
-
if (normalized === "" || normalized === "allow") {
|
|
53823
|
-
return { allowNetwork: true, unrecognized: null };
|
|
53824
|
-
}
|
|
53825
|
-
if (normalized === "deny") {
|
|
53826
|
-
return { allowNetwork: false, unrecognized: null };
|
|
53827
|
-
}
|
|
53828
|
-
return { allowNetwork: true, unrecognized: value ?? "" };
|
|
53829
|
-
}
|
|
53830
|
-
function sbplString(value) {
|
|
53831
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
53832
|
-
}
|
|
53833
|
-
function subpath(value) {
|
|
53834
|
-
return `(subpath ${sbplString(value)})`;
|
|
53835
|
-
}
|
|
53836
|
-
function literal2(value) {
|
|
53837
|
-
return `(literal ${sbplString(value)})`;
|
|
53838
|
-
}
|
|
53839
|
-
var CLAUDE_TEMP_SBPL_REGEX = '(regex #"^/private/tmp/claude-[^/]+")';
|
|
53840
|
-
var STRICT_SYSTEM_READ_SUBPATHS = [
|
|
53841
|
-
"/usr",
|
|
53842
|
-
"/bin",
|
|
53843
|
-
"/sbin",
|
|
53844
|
-
"/System",
|
|
53845
|
-
"/Library",
|
|
53846
|
-
"/private/etc",
|
|
53847
|
-
"/private/var/db",
|
|
53848
|
-
"/dev",
|
|
53849
|
-
"/opt",
|
|
53850
|
-
"/nix"
|
|
53851
|
-
];
|
|
53852
|
-
function buildSeatbeltProfile(input) {
|
|
53853
|
-
const home = input.homeDir.replace(/\/+$/, "");
|
|
53854
|
-
const lines = ["(version 1)"];
|
|
53855
|
-
const denyReadBaseSubpaths = input.denyReadPaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
|
|
53856
|
-
const denyReadLiterals = input.denyReadPaths.filter((p) => !p.endsWith("/")).map(literal2);
|
|
53857
|
-
const denyWriteSubpaths = input.denyWritePaths.filter((p) => p.endsWith("/")).map((p) => subpath(p.replace(/\/+$/, "")));
|
|
53858
|
-
const denyWriteLiterals = input.denyWritePaths.filter((p) => !p.endsWith("/")).map(literal2);
|
|
53859
|
-
const credentialReadDenies = [
|
|
53860
|
-
...SENSITIVE_READ_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
|
|
53861
|
-
...SENSITIVE_READ_LITERALS.map((rel) => literal2(`${home}/${rel}`))
|
|
53862
|
-
];
|
|
53863
|
-
if (input.strict) {
|
|
53864
|
-
lines.push("(deny default)");
|
|
53865
|
-
lines.push("(allow process*)");
|
|
53866
|
-
lines.push("(allow sysctl-read)");
|
|
53867
|
-
lines.push("(allow mach-lookup)");
|
|
53868
|
-
lines.push("(allow iokit-open)");
|
|
53869
|
-
lines.push("(allow system-socket)");
|
|
53870
|
-
lines.push("(allow signal (target self))");
|
|
53871
|
-
lines.push("(allow file-read-metadata)");
|
|
53872
|
-
const strictReads = [
|
|
53873
|
-
...STRICT_SYSTEM_READ_SUBPATHS.map(subpath),
|
|
53874
|
-
subpath(`${home}/.claude`),
|
|
53875
|
-
subpath(`${home}/.codex`),
|
|
53876
|
-
subpath(input.tmpDir)
|
|
53877
|
-
];
|
|
53878
|
-
lines.push(`(allow file-read* ${strictReads.join(" ")})`);
|
|
53879
|
-
} else {
|
|
53880
|
-
lines.push("(allow default)");
|
|
53881
|
-
}
|
|
53882
|
-
lines.push("(deny appleevent-send)");
|
|
53883
|
-
if (denyReadBaseSubpaths.length > 0) {
|
|
53884
|
-
lines.push(`(deny file-read* ${denyReadBaseSubpaths.join(" ")})`);
|
|
53885
|
-
}
|
|
53886
|
-
const workspaceReads = [subpath(input.workspaceDir), ...input.allowWritePaths.map(subpath), CLAUDE_TEMP_SBPL_REGEX];
|
|
53887
|
-
lines.push(`(allow file-read* ${workspaceReads.join(" ")})`);
|
|
53888
|
-
lines.push("(deny file-write*)");
|
|
53889
|
-
const allowWrites = [
|
|
53890
|
-
subpath(input.workspaceDir),
|
|
53891
|
-
subpath(input.tmpDir),
|
|
53892
|
-
subpath(`${home}/.claude`),
|
|
53893
|
-
subpath(`${home}/.codex`),
|
|
53894
|
-
...input.allowWritePaths.map(subpath),
|
|
53895
|
-
CLAUDE_TEMP_SBPL_REGEX,
|
|
53896
|
-
// DER-1398: Claude Code's per-session + per-command temp dirs.
|
|
53897
|
-
subpath("/dev/fd"),
|
|
53898
|
-
literal2("/dev/null"),
|
|
53899
|
-
literal2("/dev/stdout"),
|
|
53900
|
-
literal2("/dev/stderr"),
|
|
53901
|
-
literal2("/dev/tty"),
|
|
53902
|
-
literal2("/dev/dtracehelper")
|
|
53903
|
-
];
|
|
53904
|
-
lines.push(`(allow file-write* ${allowWrites.join(" ")})`);
|
|
53905
|
-
lines.push(`(deny file-read* ${[...credentialReadDenies, ...denyReadLiterals].join(" ")})`);
|
|
53906
|
-
const denyWrites = [
|
|
53907
|
-
...SENSITIVE_WRITE_SUBPATHS.map((rel) => subpath(`${home}/${rel}`)),
|
|
53908
|
-
...SENSITIVE_WRITE_LITERALS.map((rel) => literal2(`${home}/${rel}`)),
|
|
53909
|
-
// Caller-supplied write-denies (e.g. `<baseRepo>/.git/hooks` + `.git/config`) last so
|
|
53910
|
-
// they override the allowWritePaths re-allow of the shared object store above, while
|
|
53911
|
-
// `.git/{objects,refs,worktrees,logs,index,HEAD}` stay writable for local commits.
|
|
53912
|
-
...denyWriteSubpaths,
|
|
53913
|
-
...denyWriteLiterals
|
|
53914
|
-
];
|
|
53915
|
-
lines.push(`(deny file-write* ${denyWrites.join(" ")})`);
|
|
53916
|
-
lines.push('(deny mach-lookup (global-name "com.apple.coreservices.appleevents"))');
|
|
53917
|
-
if (!input.allowNetwork) {
|
|
53918
|
-
lines.push("(deny network*)");
|
|
53919
|
-
} else if (input.strict) {
|
|
53920
|
-
lines.push("(allow network*)");
|
|
53921
|
-
}
|
|
53922
|
-
return `${lines.join("\n")}
|
|
53923
|
-
`;
|
|
53924
|
-
}
|
|
53925
|
-
function canonical(p) {
|
|
53926
|
-
try {
|
|
53927
|
-
return realpathSync2(p);
|
|
53928
|
-
} catch {
|
|
53929
|
-
return p;
|
|
53930
|
-
}
|
|
53931
|
-
}
|
|
53932
|
-
function canonicalFile(p) {
|
|
53933
|
-
return path5.join(canonical(path5.dirname(p)), path5.basename(p));
|
|
53934
|
-
}
|
|
53935
|
-
function isSandboxExecAvailable() {
|
|
53936
|
-
try {
|
|
53937
|
-
return existsSync2(SANDBOX_EXEC_PATH);
|
|
53938
|
-
} catch {
|
|
53939
|
-
return false;
|
|
53940
|
-
}
|
|
53941
|
-
}
|
|
53942
|
-
function resolveSandboxSpec(options) {
|
|
53943
|
-
if (options.mode === "off") {
|
|
53944
|
-
return { kind: "none", reason: "RUNNER_SANDBOX=off (loosened fallback - OS confinement disabled)" };
|
|
53945
|
-
}
|
|
53946
|
-
if (options.platform !== "darwin") {
|
|
53947
|
-
return { kind: "none", reason: `no OS sandbox on ${options.platform} (seatbelt is macOS-only; Linux confinement is a follow-up)` };
|
|
53948
|
-
}
|
|
53949
|
-
const available = options.sandboxExecAvailable ?? isSandboxExecAvailable();
|
|
53950
|
-
if (!available) {
|
|
53951
|
-
return { kind: "none", reason: `sandbox-exec not found at ${SANDBOX_EXEC_PATH}` };
|
|
53952
|
-
}
|
|
53953
|
-
const workspaceDir = canonical(options.workspaceDir);
|
|
53954
|
-
const tmpDir = canonical(options.tmpDir);
|
|
53955
|
-
const homeDir = canonical(options.homeDir);
|
|
53956
|
-
const allowNetwork = options.allowNetwork ?? true;
|
|
53957
|
-
const denyReadPaths = [canonicalFile(options.stateFile)];
|
|
53958
|
-
if (options.runnerBaseDir) {
|
|
53959
|
-
denyReadPaths.push(`${canonical(options.runnerBaseDir).replace(/\/+$/, "")}/`);
|
|
53960
|
-
}
|
|
53961
|
-
const denyWritePaths = (options.denyWritePaths ?? []).map(
|
|
53962
|
-
(p) => p.endsWith("/") ? `${canonical(p.replace(/\/+$/, ""))}/` : canonicalFile(p)
|
|
53963
|
-
);
|
|
53964
|
-
const allowWritePaths = (options.allowWritePaths ?? []).map(canonical);
|
|
53965
|
-
const profile = buildSeatbeltProfile({
|
|
53966
|
-
workspaceDir,
|
|
53967
|
-
tmpDir,
|
|
53968
|
-
homeDir,
|
|
53969
|
-
denyReadPaths,
|
|
53970
|
-
allowWritePaths,
|
|
53971
|
-
denyWritePaths,
|
|
53972
|
-
allowNetwork,
|
|
53973
|
-
strict: options.mode === "strict"
|
|
53974
|
-
});
|
|
53975
|
-
return {
|
|
53976
|
-
kind: "seatbelt",
|
|
53977
|
-
reason: options.mode === "strict" ? "seatbelt (strict: deny-default reads + write confinement)" : "seatbelt (guard: deny sensitive reads/writes)",
|
|
53978
|
-
profile,
|
|
53979
|
-
allowNetwork
|
|
53980
|
-
};
|
|
53981
|
-
}
|
|
53982
|
-
function wrapCommandWithSandbox(spec, command, args, profilePath) {
|
|
53983
|
-
if (spec.kind === "none" || profilePath === null) {
|
|
53984
|
-
return { command, args, profilePath: null };
|
|
53985
|
-
}
|
|
53986
|
-
return {
|
|
53987
|
-
command: SANDBOX_EXEC_PATH,
|
|
53988
|
-
args: ["-f", profilePath, command, ...args],
|
|
53989
|
-
profilePath
|
|
53990
|
-
};
|
|
53991
|
-
}
|
|
53992
|
-
async function sweepStaleRunnerTempFiles(dir, options = {}) {
|
|
53993
|
-
const maxAgeMs = options.maxAgeMs ?? 60 * 60 * 1e3;
|
|
53994
|
-
const now = options.now ?? Date.now();
|
|
53995
|
-
let entries;
|
|
53996
|
-
try {
|
|
53997
|
-
entries = await readdir2(dir);
|
|
53998
|
-
} catch {
|
|
53999
|
-
return [];
|
|
54000
|
-
}
|
|
54001
|
-
const removed = [];
|
|
54002
|
-
await Promise.all(
|
|
54003
|
-
entries.map(async (name) => {
|
|
54004
|
-
if (!RUNNER_TEMP_PREFIXES.some((prefix) => name.startsWith(prefix))) {
|
|
54005
|
-
return;
|
|
54006
|
-
}
|
|
54007
|
-
const full = path5.join(dir, name);
|
|
54008
|
-
try {
|
|
54009
|
-
const info = await stat3(full);
|
|
54010
|
-
if (now - info.mtimeMs < maxAgeMs) {
|
|
54011
|
-
return;
|
|
54012
|
-
}
|
|
54013
|
-
await rm4(full, { force: true });
|
|
54014
|
-
removed.push(name);
|
|
54015
|
-
} catch {
|
|
54016
|
-
}
|
|
54017
|
-
})
|
|
54018
|
-
);
|
|
54019
|
-
return removed;
|
|
54020
|
-
}
|
|
54021
|
-
|
|
54022
54250
|
// src/runner-serve.ts
|
|
54023
54251
|
var execFile5 = promisify5(execFileCallback5);
|
|
54024
54252
|
var RUNNER_WORK_ORDER_TOOLS = [
|
|
@@ -54034,7 +54262,6 @@ var RUNNER_AICOS_TURN_TOOLS = [
|
|
|
54034
54262
|
var CLAUDE_WORKSPACE_WRITE_LOCAL_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
54035
54263
|
var CLAUDE_READ_LOCAL_TOOLS = ["Read", "Glob", "Grep"];
|
|
54036
54264
|
var READ_STUB_TIMEOUT_MS = 18e4;
|
|
54037
|
-
var FORGE_TURN_HARD_CEILING_SECONDS = 30 * 60;
|
|
54038
54265
|
function claudeMcpToolName(name) {
|
|
54039
54266
|
return name.startsWith("mcp__") ? name : `mcp__rost__${name}`;
|
|
54040
54267
|
}
|
|
@@ -54062,12 +54289,19 @@ ${usage()}
|
|
|
54062
54289
|
`);
|
|
54063
54290
|
if (config2.sandboxNetworkWarning) {
|
|
54064
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}
|
|
54065
54297
|
`);
|
|
54066
54298
|
}
|
|
54067
54299
|
try {
|
|
54068
54300
|
const swept = await sweepStaleRunnerTempFiles(tmpdir2());
|
|
54069
|
-
|
|
54070
|
-
|
|
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)
|
|
54071
54305
|
`);
|
|
54072
54306
|
}
|
|
54073
54307
|
} catch {
|
|
@@ -54104,9 +54338,19 @@ ${usage()}
|
|
|
54104
54338
|
}
|
|
54105
54339
|
if (localRuntime) {
|
|
54106
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
|
+
});
|
|
54107
54350
|
try {
|
|
54108
54351
|
await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
|
|
54109
54352
|
} finally {
|
|
54353
|
+
stopTurnHeartbeat();
|
|
54110
54354
|
activeSessions = Math.max(0, activeSessions - 1);
|
|
54111
54355
|
}
|
|
54112
54356
|
}
|
|
@@ -54138,6 +54382,44 @@ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runt
|
|
|
54138
54382
|
}
|
|
54139
54383
|
}
|
|
54140
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
|
+
}
|
|
54141
54423
|
function resolveAccountAlias(env) {
|
|
54142
54424
|
return (env.RUNNER_MODEL_ACCOUNT_ALIAS ?? env.ROST_RUNNER_MODEL_ACCOUNT_ALIAS ?? "").trim() || "default";
|
|
54143
54425
|
}
|
|
@@ -54518,7 +54800,7 @@ async function probeForgeDiskFreeMb(homeDir) {
|
|
|
54518
54800
|
return null;
|
|
54519
54801
|
}
|
|
54520
54802
|
}
|
|
54521
|
-
async function post2(fetchImpl, appUrl2, pathName, body, token) {
|
|
54803
|
+
async function post2(fetchImpl, appUrl2, pathName, body, token, signal) {
|
|
54522
54804
|
const headers = { "content-type": "application/json" };
|
|
54523
54805
|
if (token) {
|
|
54524
54806
|
headers.authorization = `Bearer ${token}`;
|
|
@@ -54526,7 +54808,10 @@ async function post2(fetchImpl, appUrl2, pathName, body, token) {
|
|
|
54526
54808
|
const response = await fetchImpl(`${appUrl2.replace(/\/+$/, "")}${pathName}`, {
|
|
54527
54809
|
method: "POST",
|
|
54528
54810
|
headers,
|
|
54529
|
-
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 } : {}
|
|
54530
54815
|
});
|
|
54531
54816
|
const text = await response.text();
|
|
54532
54817
|
let json2;
|
|
@@ -55003,6 +55288,13 @@ function buildTurnPrompt(input) {
|
|
|
55003
55288
|
`Claimed ${kind === "turn_execution" ? "turn execution" : "work order"} context: ${JSON.stringify(context).slice(0, 8e3)}`
|
|
55004
55289
|
].filter((line) => line.length > 0).join("\n");
|
|
55005
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
|
+
}
|
|
55006
55298
|
async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
|
|
55007
55299
|
const execution = parseRunnerExecutionContract(workOrder);
|
|
55008
55300
|
if (execution?.profile === "forge_merge") {
|
|
@@ -55085,6 +55377,7 @@ async function spawnRunnerTurn(ctx, workOrder, runtime, kind) {
|
|
|
55085
55377
|
if (sandboxProfilePath) {
|
|
55086
55378
|
void rm5(sandboxProfilePath, { force: true });
|
|
55087
55379
|
}
|
|
55380
|
+
void sweepOrphanedTurnTempFiles();
|
|
55088
55381
|
if (forgePrep) {
|
|
55089
55382
|
if (plan.profile === "forge_build" && result.ok && forgePrep.credential.canPush) {
|
|
55090
55383
|
result = await finalizeForgeBuildPush(ctx, forgePrep, result);
|
|
@@ -55276,6 +55569,7 @@ async function spawnMergeConflictModel(ctx, input) {
|
|
|
55276
55569
|
if (sandboxProfilePath) {
|
|
55277
55570
|
void rm5(sandboxProfilePath, { force: true });
|
|
55278
55571
|
}
|
|
55572
|
+
void sweepOrphanedTurnTempFiles();
|
|
55279
55573
|
return result;
|
|
55280
55574
|
}
|
|
55281
55575
|
async function runForgeMergeTurn(ctx, workOrder, execution, runtime) {
|
|
@@ -55373,6 +55667,20 @@ async function executeForgeMerge(ctx, input) {
|
|
|
55373
55667
|
function scrubTranscriptSessionIds(value) {
|
|
55374
55668
|
return value.replace(/("?(?:session_id|sessionId)"?\s*[:=]\s*"?)[A-Za-z0-9._-]+("?)/gi, "$1[redacted]$2");
|
|
55375
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
|
+
}
|
|
55376
55684
|
function runModelProcess(input) {
|
|
55377
55685
|
return new Promise((resolve2) => {
|
|
55378
55686
|
let settled = false;
|
|
@@ -55386,12 +55694,20 @@ function runModelProcess(input) {
|
|
|
55386
55694
|
const child = spawn(input.command, input.args, {
|
|
55387
55695
|
cwd: input.cwd,
|
|
55388
55696
|
env: input.env,
|
|
55389
|
-
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
|
|
55390
55706
|
});
|
|
55391
55707
|
let out = "";
|
|
55392
55708
|
let err = "";
|
|
55393
55709
|
const timer = setTimeout(() => {
|
|
55394
|
-
child
|
|
55710
|
+
killProcessTree(child);
|
|
55395
55711
|
const partial2 = scrubTranscriptSessionIds(redactForLog((out || err || "").trim()));
|
|
55396
55712
|
finish({
|
|
55397
55713
|
ok: false,
|
|
@@ -55609,6 +55925,15 @@ function usage() {
|
|
|
55609
55925
|
}
|
|
55610
55926
|
|
|
55611
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
|
+
}
|
|
55612
55937
|
async function main(argv = process.argv.slice(2), options = {}) {
|
|
55613
55938
|
const io = options.io ?? {
|
|
55614
55939
|
stdout: process.stdout,
|
|
@@ -55617,7 +55942,10 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
55617
55942
|
// and stdout must be TTYs. A piped/headless/agent session fails this and so
|
|
55618
55943
|
// will not auto-approve its own mint/revoke confirmation unless skip-
|
|
55619
55944
|
// permissions is explicitly enabled.
|
|
55620
|
-
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
|
|
55621
55949
|
};
|
|
55622
55950
|
const login = options.login ?? loginWithBrowser;
|
|
55623
55951
|
const deviceLogin = options.deviceLogin ?? loginWithDeviceCode;
|
|
@@ -56047,11 +56375,13 @@ async function executeMcp(io, client, appUrl2, args, env = process.env) {
|
|
|
56047
56375
|
const options = parseMcpInstallOptions(args.slice(1));
|
|
56048
56376
|
const envSkipPermissions = env[`${cliBrand.envPrefix}_CLI_SKIP_PERMISSIONS`] === "1";
|
|
56049
56377
|
const canAutoApprove = io.interactive === true || options.skipPermissions || envSkipPermissions;
|
|
56378
|
+
const reviewed = io.interactive === true;
|
|
56050
56379
|
const rendered = await renderMcpInstall({
|
|
56051
56380
|
client,
|
|
56052
56381
|
appUrl: appUrl2,
|
|
56053
56382
|
options,
|
|
56054
|
-
canAutoApprove
|
|
56383
|
+
canAutoApprove,
|
|
56384
|
+
reviewed
|
|
56055
56385
|
});
|
|
56056
56386
|
io.stdout.write(rendered.stdout);
|
|
56057
56387
|
if (rendered.stderr) {
|
|
@@ -56121,15 +56451,21 @@ async function printCommandOutput(io, client, commandId, body = {}, format, opti
|
|
|
56121
56451
|
return 0;
|
|
56122
56452
|
} catch (error51) {
|
|
56123
56453
|
const envAutoConfirm = process.env[`${cliBrand.envPrefix}_CLI_AUTO_CONFIRM`] === "1";
|
|
56124
|
-
|
|
56125
|
-
|
|
56126
|
-
|
|
56127
|
-
|
|
56128
|
-
io.
|
|
56129
|
-
|
|
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
|
+
}
|
|
56130
56463
|
try {
|
|
56131
|
-
const
|
|
56132
|
-
|
|
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;
|
|
56133
56469
|
const rendered = format ? format(approvedOutput) : JSON.stringify(approvedOutput, null, 2);
|
|
56134
56470
|
io.stdout.write(`${rendered}
|
|
56135
56471
|
`);
|
|
@@ -56138,6 +56474,19 @@ async function printCommandOutput(io, client, commandId, body = {}, format, opti
|
|
|
56138
56474
|
return printCommandError2(io, approvalError);
|
|
56139
56475
|
}
|
|
56140
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
|
+
}
|
|
56141
56490
|
}
|
|
56142
56491
|
return printCommandError2(io, error51);
|
|
56143
56492
|
}
|
|
@@ -56160,7 +56509,32 @@ function pendingConfirmationFromError2(error51) {
|
|
|
56160
56509
|
if (typeof confirmationId !== "string" || confirmationId.length === 0 || typeof commandId !== "string" || commandId.length === 0) {
|
|
56161
56510
|
return null;
|
|
56162
56511
|
}
|
|
56163
|
-
|
|
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
|
+
}
|
|
56164
56538
|
}
|
|
56165
56539
|
function printCommandError2(io, error51) {
|
|
56166
56540
|
if (error51 instanceof CommandClientError && error51.response && !error51.response.ok) {
|