@kal-elsam/kairo-runtime 0.11.0 → 0.12.0

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.
Files changed (64) hide show
  1. package/global-template/components/agent-skills/LICENSE +21 -0
  2. package/global-template/components/agent-skills/PROVENANCE.md +26 -0
  3. package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
  4. package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
  5. package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
  6. package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
  7. package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
  8. package/global-template/components/catalog.json +29 -0
  9. package/package.json +5 -2
  10. package/src/cli.js +136 -8
  11. package/src/global/component-builders.js +3 -1
  12. package/src/global/components/agent-skills.js +27 -0
  13. package/src/global/ink/cockpit-control-center.js +104 -4
  14. package/src/global/ink/cockpit-scan.js +20 -2
  15. package/src/global/ink/ecosystem-updates-display.js +37 -0
  16. package/src/global/ink/launch-input.js +32 -1
  17. package/src/global/ink/orchestrator-app.js +2 -1
  18. package/src/global/ink/orchestrator-state.js +17 -2
  19. package/src/global/ink/system-resources-display.js +109 -0
  20. package/src/global/ink/use-orchestrator-data.js +40 -4
  21. package/src/global/ink/ux/live-overview.js +11 -1
  22. package/src/global/mcp/kairo-mcp.js +230 -0
  23. package/src/global/observability/build-companion-snapshot.js +281 -0
  24. package/src/global/observability/build-observability-snapshot.js +24 -0
  25. package/src/global/observability/ecosystem-updates.js +224 -0
  26. package/src/global/observability/gentle-bundle-export.js +71 -0
  27. package/src/global/observability/gentle-bundle-import.js +122 -0
  28. package/src/global/observability/gentle-probe.js +155 -0
  29. package/src/global/observability/graphify-ops.js +133 -0
  30. package/src/global/observability/graphify-parse-cache.js +90 -0
  31. package/src/global/observability/graphify-probe.js +185 -0
  32. package/src/global/observability/hermes-activity.js +163 -0
  33. package/src/global/observability/hermes-probe.js +171 -0
  34. package/src/global/observability/index.js +85 -0
  35. package/src/global/observability/passive-snapshot-flight.js +93 -0
  36. package/src/global/observability/probe-contract.js +38 -0
  37. package/src/global/observability/probe-registry.js +30 -0
  38. package/src/global/observability/resource-advisor.js +71 -0
  39. package/src/global/observability/system-resources.js +171 -0
  40. package/src/global/runtime/alerts/alert-cli.js +31 -0
  41. package/src/global/runtime/alerts/alert-store.js +29 -6
  42. package/src/global/runtime/alerts/alert-validate.js +25 -1
  43. package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
  44. package/src/global/runtime/execution-adapters/claude.js +2 -1
  45. package/src/global/runtime/execution-adapters/codex.js +2 -1
  46. package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
  47. package/src/global/runtime/execution-adapters/cursor.js +2 -1
  48. package/src/global/runtime/execution-adapters/opencode.js +2 -1
  49. package/src/global/runtime/execution-adapters/pi.js +2 -1
  50. package/src/global/runtime/review/index.js +1 -1
  51. package/src/global/runtime/review/review-cli.js +113 -3
  52. package/src/global/runtime/review/review-git.js +142 -11
  53. package/src/global/runtime/review/review-patch.js +2 -0
  54. package/src/global/runtime/review/review-receipts.js +12 -7
  55. package/src/global/runtime/review/review-runner.js +2 -2
  56. package/src/global/runtime/review/review-types.js +8 -5
  57. package/src/global/runtime/review/review-validate.js +5 -1
  58. package/src/global/runtime/run-cli.js +2 -0
  59. package/src/global/runtime/run-manager.js +39 -18
  60. package/src/global/runtime/run-permissions.js +231 -0
  61. package/src/global/runtime/run-profile.js +2 -0
  62. package/src/global/runtime/run-supervisor.js +77 -37
  63. package/src/global/runtime/run-types.js +2 -0
  64. package/src/global/updates-cli.js +41 -0
@@ -35,16 +35,20 @@ function snapshotProvenance(snapshot) {
35
35
  commit: snapshot.commit ?? null,
36
36
  fingerprint: snapshot.fingerprint,
37
37
  totals: snapshot.totals,
38
- files: (snapshot.files ?? []).map((f) => ({
39
- path: f.path,
40
- sourcePath: f.sourcePath ?? null,
41
- status: f.status, hash: f.hash, changedLines: f.changedLines
42
- })),
38
+ files: (snapshot.files ?? []).map((f) => {
39
+ const entry = {
40
+ path: f.path,
41
+ sourcePath: f.sourcePath ?? null,
42
+ status: f.status, hash: f.hash, changedLines: f.changedLines
43
+ };
44
+ if (f.mode != null) entry.mode = f.mode;
45
+ return entry;
46
+ }),
43
47
  excluded: (snapshot.excluded ?? []).map((e) => ({ path: e.path, reason: e.reason }))
44
48
  };
45
49
  }
46
50
 
47
- /** Build a v1 receipt: findings + provenance only (no prompt/diff/transcript/raw). */
51
+ /** Build a secret-free receipt (v2 for staged candidates; v1 otherwise). */
48
52
  export function buildReviewReceipt({
49
53
  reviewId,
50
54
  agentId,
@@ -59,8 +63,9 @@ export function buildReviewReceipt({
59
63
  createdAt = null
60
64
  } = {}) {
61
65
  assertSafeReviewId(reviewId);
66
+ const version = snapshot?.mode === "staged" ? 2 : 1;
62
67
  const receipt = {
63
- version: 1,
68
+ version,
64
69
  reviewId,
65
70
  agentId,
66
71
  model,
@@ -60,7 +60,7 @@ function classifyAgentError(error) {
60
60
  * Never returns/persists prompt, patch, JSONL, or transcript.
61
61
  */
62
62
  export async function runReview({
63
- cwd, agent, base = null, commit = null, model = null,
63
+ cwd, agent, base = null, commit = null, staged = false, model = null,
64
64
  includePrivate = false, privateConfirmed = false, failOn = null,
65
65
  homeDir, cliVersion = null,
66
66
  resolveSnapshot = resolveReviewSnapshot,
@@ -73,7 +73,7 @@ export async function runReview({
73
73
  const reviewId = createId();
74
74
  const startedAt = now();
75
75
  const snapshot = await resolveSnapshot({
76
- cwd, base, commit, includePrivate, privateConfirmed
76
+ cwd, base, commit, staged, includePrivate, privateConfirmed
77
77
  });
78
78
 
79
79
  let state = REVIEW_STATES.COMPLETED;
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
 
3
3
  export const REVIEW_SCOPE_MODES = Object.freeze({
4
- WORKING_TREE: "working-tree", BASE: "base", COMMIT: "commit"
4
+ WORKING_TREE: "working-tree", BASE: "base", COMMIT: "commit", STAGED: "staged"
5
5
  });
6
6
  export const REVIEW_SEVERITIES = Object.freeze({ HIGH: "high", MEDIUM: "medium", LOW: "low" });
7
7
  export const REVIEW_STATES = Object.freeze({
@@ -18,7 +18,8 @@ export const REVIEW_SNAPSHOT_ERROR_CODES = Object.freeze({
18
18
  INVALID_SCOPE: "invalid_scope",
19
19
  INVALID_PATH: "invalid_path",
20
20
  LIMIT_EXCEEDED: "limit_exceeded",
21
- PRIVATE_CONSENT_REQUIRED: "private_consent_required"
21
+ PRIVATE_CONSENT_REQUIRED: "private_consent_required",
22
+ TRUNCATED_OBJECT_ID: "truncated_object_id"
22
23
  });
23
24
 
24
25
  export class ReviewSnapshotError extends Error {
@@ -30,12 +31,14 @@ export class ReviewSnapshotError extends Error {
30
31
  }
31
32
  }
32
33
 
33
- export function resolveReviewScopeMode({ base = null, commit = null } = {}) {
34
- if (base && commit) {
35
- throw new ReviewSnapshotError("--base and --commit are mutually exclusive.", {
34
+ export function resolveReviewScopeMode({ base = null, commit = null, staged = false } = {}) {
35
+ const selected = [Boolean(base), Boolean(commit), Boolean(staged)].filter(Boolean).length;
36
+ if (selected > 1) {
37
+ throw new ReviewSnapshotError("--base, --commit, and --staged are mutually exclusive.", {
36
38
  code: REVIEW_SNAPSHOT_ERROR_CODES.INVALID_SCOPE
37
39
  });
38
40
  }
41
+ if (staged) return REVIEW_SCOPE_MODES.STAGED;
39
42
  if (base) return REVIEW_SCOPE_MODES.BASE;
40
43
  if (commit) return REVIEW_SCOPE_MODES.COMMIT;
41
44
  return REVIEW_SCOPE_MODES.WORKING_TREE;
@@ -32,7 +32,10 @@ const RECEIPT_SHAPE = Object.freeze({
32
32
  commit: "string?",
33
33
  fingerprint: "string",
34
34
  totals: { fileCount: "number", changedLines: "number", diffBytes: "number" },
35
- files: [{ path: "string", sourcePath: "string?", status: "string", hash: "string", changedLines: "number" }],
35
+ files: [{
36
+ path: "string", sourcePath: "string?", status: "string", hash: "string",
37
+ mode: "string?", changedLines: "number"
38
+ }],
36
39
  excluded: [{ path: "string", reason: "string" }]
37
40
  },
38
41
  findings: [{
@@ -173,6 +176,7 @@ function assertMatchesShape(value, shape, path) {
173
176
  }
174
177
  for (const [key, childShape] of Object.entries(shape)) {
175
178
  if (!(key in body)) {
179
+ if (typeof childShape === "string" && isOptionalScalar(childShape)) continue;
176
180
  throw new ReviewValidationError(`Missing field "${key}" at ${path}.`, {
177
181
  code: REVIEW_VALIDATION_ERROR_CODES.INVALID_OUTPUT, details: { path, key }
178
182
  });
@@ -42,6 +42,8 @@ export async function runGlobalRun(options, packageManifest, { startRunImpl = st
42
42
  cwd: options.cwd,
43
43
  model: options.model ?? runtime.model,
44
44
  permissions,
45
+ allowUnsafePermissions: Boolean(options.allowUnsafePermissions),
46
+ permissionSource: "cli",
45
47
  captureTranscript,
46
48
  cliVersion: packageManifest.version,
47
49
  profile: profileResolved,
@@ -13,6 +13,7 @@ import {
13
13
  import { resolveExecutionAdapter } from "./execution-adapters/index.js";
14
14
  import { resolveProfileAgents } from "../profile.js";
15
15
  import { resolveRuntimeOptions } from "./run-profile.js";
16
+ import { authorizeRunPermissions } from "./run-permissions.js";
16
17
  import { cleanupStaleHandoffs, deleteRunHandoff, writeRunHandoff } from "./run-handoff.js";
17
18
  import { isRunAlive } from "./run-liveness.js";
18
19
  import { readSupervisorLock, writeSupervisorLock } from "./run-supervisor-lock.js";
@@ -93,6 +94,9 @@ async function prepareRun({
93
94
  cwd,
94
95
  model = null,
95
96
  permissions = [],
97
+ allowUnsafePermissions = false,
98
+ permissionSource = "cli",
99
+ permissionConsentType = null,
96
100
  captureTranscript = false,
97
101
  cliVersion,
98
102
  profile = null,
@@ -113,6 +117,14 @@ async function prepareRun({
113
117
  );
114
118
  }
115
119
 
120
+ const authorized = authorizeRunPermissions({
121
+ permissions,
122
+ agentId,
123
+ allowUnsafePermissions,
124
+ source: permissionSource,
125
+ consentType: permissionConsentType
126
+ });
127
+
116
128
  if (normalizedStrategy === RUN_STRATEGIES.ORCHESTRATED) {
117
129
  await assertManagedMinionExtension(homeDir);
118
130
  }
@@ -126,7 +138,8 @@ async function prepareRun({
126
138
  model,
127
139
  task,
128
140
  cwd,
129
- permissions,
141
+ permissions: authorized.permissions,
142
+ permissionAuthority: authorized.permissionAuthority,
130
143
  captureTranscript,
131
144
  cliVersion,
132
145
  profileSources: profile?.sources ?? null,
@@ -141,7 +154,7 @@ async function prepareRun({
141
154
  task,
142
155
  cwd,
143
156
  model,
144
- permissions,
157
+ permissions: authorized.permissions,
145
158
  captureTranscript,
146
159
  cliVersion,
147
160
  profile: profile?.profile ?? null,
@@ -173,6 +186,9 @@ export async function startRun({
173
186
  cwd,
174
187
  model = null,
175
188
  permissions = [],
189
+ allowUnsafePermissions = false,
190
+ permissionSource = "cli",
191
+ permissionConsentType = null,
176
192
  captureTranscript = false,
177
193
  cliVersion,
178
194
  profile = null,
@@ -190,6 +206,9 @@ export async function startRun({
190
206
  cwd,
191
207
  model,
192
208
  permissions,
209
+ allowUnsafePermissions,
210
+ permissionSource,
211
+ permissionConsentType,
193
212
  captureTranscript,
194
213
  cliVersion,
195
214
  profile,
@@ -275,22 +294,8 @@ export async function stopRun(homeDir, runId, { signal = "SIGTERM" } = {}) {
275
294
  return state;
276
295
  }
277
296
 
278
- const lock = await readSupervisorLock(homeDir, runId);
279
- const child = activeProcesses.get(runId);
280
-
281
- if (child) {
282
- child.kill(signal);
283
- } else {
284
- const targets = [lock?.agentPid, state.pid, lock?.supervisorPid].filter(Boolean);
285
- for (const pid of targets) {
286
- try {
287
- process.kill(pid, signal);
288
- } catch {
289
- // Process may already be gone.
290
- }
291
- }
292
- }
293
-
297
+ // Persist cancel markers BEFORE kill/close so the supervisor close handler
298
+ // cannot lose the race and finalize as FAILED while promises stay pending.
294
299
  await writeCancelSignal(homeDir, runId, {
295
300
  requested: true,
296
301
  signal,
@@ -309,6 +314,22 @@ export async function stopRun(homeDir, runId, { signal = "SIGTERM" } = {}) {
309
314
  data: { signal }
310
315
  }));
311
316
 
317
+ const lock = await readSupervisorLock(homeDir, runId);
318
+ const child = activeProcesses.get(runId);
319
+
320
+ if (child) {
321
+ child.kill(signal);
322
+ } else {
323
+ const targets = [lock?.agentPid, state.pid, lock?.supervisorPid].filter(Boolean);
324
+ for (const pid of targets) {
325
+ try {
326
+ process.kill(pid, signal);
327
+ } catch {
328
+ // Process may already be gone.
329
+ }
330
+ }
331
+ }
332
+
312
333
  activeProcesses.delete(runId);
313
334
  return metadata;
314
335
  }
@@ -0,0 +1,231 @@
1
+ export const PERMISSION_MODES = Object.freeze({
2
+ NORMAL: "normal",
3
+ SAFE: "safe",
4
+ UNSAFE: "unsafe"
5
+ });
6
+
7
+ export const CONSENT_TYPES = Object.freeze({
8
+ NONE: "none",
9
+ ALLOW_UNSAFE_PERMISSIONS: "allow-unsafe-permissions",
10
+ COCKPIT_UNSAFE_CONFIRM: "cockpit-unsafe-confirm",
11
+ CLI_CONFIRM_IMPORT: "cli-confirm-import",
12
+ COCKPIT_ALERT_RESOLVE: "cockpit-alert-resolve",
13
+ COCKPIT_ALERT_DISMISS: "cockpit-alert-dismiss",
14
+ CLI_CONFIRM_ALERT_RESOLVE: "cli-confirm-alert-resolve",
15
+ CLI_CONFIRM_ALERT_DISMISS: "cli-confirm-alert-dismiss"
16
+ });
17
+
18
+ /** Closed set of non-agent unsafe operations (import, controlled alerts). */
19
+ export const UNSAFE_OPERATIONS = Object.freeze({
20
+ GENTLE_BUNDLE_IMPORT: "gentle-bundle-import",
21
+ ALERT_RESOLVE: "alert-resolve",
22
+ ALERT_DISMISS: "alert-dismiss"
23
+ });
24
+
25
+ /** Exact (operation, source, consent) triples — reject all other combinations. */
26
+ export const UNSAFE_CONSENT_BINDINGS = Object.freeze([
27
+ [UNSAFE_OPERATIONS.GENTLE_BUNDLE_IMPORT, "cli", CONSENT_TYPES.CLI_CONFIRM_IMPORT],
28
+ [UNSAFE_OPERATIONS.ALERT_RESOLVE, "cli", CONSENT_TYPES.CLI_CONFIRM_ALERT_RESOLVE],
29
+ [UNSAFE_OPERATIONS.ALERT_RESOLVE, "cockpit", CONSENT_TYPES.COCKPIT_ALERT_RESOLVE],
30
+ [UNSAFE_OPERATIONS.ALERT_DISMISS, "cli", CONSENT_TYPES.CLI_CONFIRM_ALERT_DISMISS],
31
+ [UNSAFE_OPERATIONS.ALERT_DISMISS, "cockpit", CONSENT_TYPES.COCKPIT_ALERT_DISMISS]
32
+ ].map(([operation, source, consent]) => Object.freeze({ operation, source, consent })));
33
+
34
+ const UNSAFE = new Set(["force", "yolo"]);
35
+ const SAFE = new Set(["read-only"]);
36
+ const UNSAFE_SOURCES = new Set(["cli", "cockpit"]);
37
+
38
+ /** Canonical permission modes each adapter may accept (empty = normal always ok). */
39
+ export const ADAPTER_PERMISSION_MODES = Object.freeze({
40
+ pi: Object.freeze(["read-only"]),
41
+ codex: Object.freeze(["yolo"]),
42
+ claude: Object.freeze(["force", "yolo"]),
43
+ cursor: Object.freeze(["force", "yolo"]),
44
+ opencode: Object.freeze(["force"])
45
+ });
46
+
47
+ export class PermissionAuthorityError extends Error {
48
+ constructor(message, { code = "permission_authority", details = null } = {}) {
49
+ super(message);
50
+ this.name = "PermissionAuthorityError";
51
+ this.code = code;
52
+ this.details = details;
53
+ }
54
+ }
55
+
56
+ function canonicalizeToken(raw) {
57
+ const token = String(raw ?? "").trim().toLowerCase();
58
+ if (!token) return null;
59
+ if (token === "all" || token === "force") return "force";
60
+ if (token === "yolo" || token.startsWith("dangerously-")) return "yolo";
61
+ if (token === "read-only") return "read-only";
62
+ throw new PermissionAuthorityError(`Unknown permission "${raw}".`, {
63
+ code: "unknown_permission", details: { permission: raw }
64
+ });
65
+ }
66
+
67
+ /** Normalize aliases; reject unknown tokens. Dedupes preserving force/yolo/read-only order. */
68
+ export function normalizePermissions(permissions = []) {
69
+ if (!Array.isArray(permissions)) {
70
+ throw new PermissionAuthorityError("Permissions must be an array.", {
71
+ code: "invalid_permissions"
72
+ });
73
+ }
74
+ const seen = new Set();
75
+ const out = [];
76
+ for (const entry of permissions) {
77
+ const token = canonicalizeToken(entry);
78
+ if (!token || seen.has(token)) continue;
79
+ seen.add(token);
80
+ out.push(token);
81
+ }
82
+ return out;
83
+ }
84
+
85
+ export function classifyPermissionMode(normalized = []) {
86
+ if (normalized.some((p) => UNSAFE.has(p))) return PERMISSION_MODES.UNSAFE;
87
+ if (normalized.some((p) => SAFE.has(p))) return PERMISSION_MODES.SAFE;
88
+ return PERMISSION_MODES.NORMAL;
89
+ }
90
+
91
+ /** Profile defaults may only be empty or safe — never force/yolo/aliases. */
92
+ export function validateDefaultPermissions(defaultPermissions) {
93
+ if (defaultPermissions == null) return [];
94
+ const normalized = normalizePermissions(defaultPermissions);
95
+ if (normalized.some((p) => UNSAFE.has(p))) {
96
+ throw new PermissionAuthorityError(
97
+ "Profile defaultPermissions cannot include unsafe modes (force/yolo/all/dangerously-*). "
98
+ + "Pass them per run with --allow-unsafe-permissions.",
99
+ { code: "unsafe_default_permissions", details: { defaultPermissions: normalized } }
100
+ );
101
+ }
102
+ return normalized;
103
+ }
104
+
105
+ function resolveConsent({ mode, source, allowUnsafePermissions, consentType }) {
106
+ if (mode !== PERMISSION_MODES.UNSAFE) return CONSENT_TYPES.NONE;
107
+ if (consentType) return consentType;
108
+ if (source === "cockpit") return CONSENT_TYPES.COCKPIT_UNSAFE_CONFIRM;
109
+ if (allowUnsafePermissions) return CONSENT_TYPES.ALLOW_UNSAFE_PERMISSIONS;
110
+ return CONSENT_TYPES.NONE;
111
+ }
112
+
113
+ /**
114
+ * Fail-closed permission authority for startRun.
115
+ * Unsafe modes require explicit allowUnsafePermissions (CLI) or cockpit confirm.
116
+ */
117
+ export function authorizeRunPermissions({
118
+ permissions = [],
119
+ agentId,
120
+ allowUnsafePermissions = false,
121
+ source = "cli",
122
+ consentType = null
123
+ } = {}) {
124
+ const normalized = normalizePermissions(permissions);
125
+ const mode = classifyPermissionMode(normalized);
126
+
127
+ if (mode === PERMISSION_MODES.UNSAFE && !allowUnsafePermissions) {
128
+ throw new PermissionAuthorityError(
129
+ `Unsafe permissions (${normalized.join(", ")}) require --allow-unsafe-permissions `
130
+ + "(or an explicit Cockpit unsafe confirmation).",
131
+ { code: "unsafe_consent_required", details: { permissions: normalized, source } }
132
+ );
133
+ }
134
+
135
+ const supported = new Set(ADAPTER_PERMISSION_MODES[agentId] ?? []);
136
+ for (const token of normalized) {
137
+ if (!supported.has(token)) {
138
+ throw new PermissionAuthorityError(
139
+ `Permission "${token}" is not supported by adapter "${agentId}". `
140
+ + `Supported: ${[...supported].join(", ") || "(none)"}.`,
141
+ {
142
+ code: "unsupported_permission",
143
+ details: { permission: token, agentId, supported: [...supported] }
144
+ }
145
+ );
146
+ }
147
+ }
148
+
149
+ const consent = resolveConsent({ mode, source, allowUnsafePermissions, consentType });
150
+ return {
151
+ permissions: normalized,
152
+ permissionAuthority: {
153
+ mode,
154
+ source: source === "cockpit" ? "cockpit" : "cli",
155
+ consent
156
+ }
157
+ };
158
+ }
159
+
160
+ /** Map normalized permissions to common CLI flags (force/yolo). */
161
+ export function buildPermissionsArgs(permissions = []) {
162
+ const normalized = normalizePermissions(permissions);
163
+ if (normalized.includes("force")) return ["--force"];
164
+ if (normalized.includes("yolo")) return ["--dangerously-skip-permissions"];
165
+ return [];
166
+ }
167
+
168
+ /** Fail-closed unsafe-op authority: closed bindings + frozen process-local issuance. */
169
+ const ISSUED_UNSAFE_AUTHORITIES = new WeakMap();
170
+
171
+ function resolveConsentBinding(operation, source, consentType) {
172
+ const match = UNSAFE_SOURCES.has(source)
173
+ ? UNSAFE_CONSENT_BINDINGS.find((b) => b.operation === operation && b.source === source)
174
+ : null;
175
+ if (!match || (consentType != null && consentType !== match.consent)) {
176
+ throw new PermissionAuthorityError(
177
+ `Invalid consent binding for "${operation}" / "${source}"`
178
+ + (consentType != null ? ` / "${consentType}"` : "") + ".",
179
+ { code: "invalid_unsafe_consent", details: { operation, source, consentType } }
180
+ );
181
+ }
182
+ return match.consent;
183
+ }
184
+
185
+ export function assertUnsafePermissionAuthority(permissionAuthority, { expectedOperation } = {}) {
186
+ const pa = permissionAuthority;
187
+ if (!pa || typeof pa !== "object" || Array.isArray(pa) || pa.mode !== PERMISSION_MODES.UNSAFE) {
188
+ throw new PermissionAuthorityError("Unsafe mutation requires permissionAuthority.", {
189
+ code: pa ? "invalid_unsafe_consent" : "permission_authority_required",
190
+ details: pa ? { permissionAuthority: pa } : null
191
+ });
192
+ }
193
+ const issued = ISSUED_UNSAFE_AUTHORITIES.get(pa);
194
+ if (!issued) {
195
+ throw new PermissionAuthorityError("permissionAuthority was not issued by authorizeUnsafeOperation.", {
196
+ code: "permission_authority_forged", details: { operation: pa.operation, source: pa.source }
197
+ });
198
+ }
199
+ if (expectedOperation && issued.operation !== expectedOperation) {
200
+ throw new PermissionAuthorityError(
201
+ `permissionAuthority.operation "${issued.operation}" does not match "${expectedOperation}".`,
202
+ { code: "invalid_unsafe_consent", details: { operation: issued.operation, expectedOperation } }
203
+ );
204
+ }
205
+ resolveConsentBinding(issued.operation, issued.source, issued.consent);
206
+ return issued;
207
+ }
208
+
209
+ export function authorizeUnsafeOperation({
210
+ operation, confirmed = false, source = "cli", consentType = null
211
+ } = {}) {
212
+ if (!Object.values(UNSAFE_OPERATIONS).includes(operation)) {
213
+ throw new PermissionAuthorityError(`Unknown unsafe operation "${operation}".`, {
214
+ code: "unknown_unsafe_operation", details: { operation }
215
+ });
216
+ }
217
+ if (!confirmed) {
218
+ const code = operation === UNSAFE_OPERATIONS.GENTLE_BUNDLE_IMPORT
219
+ ? "import_consent_required" : "unsafe_consent_required";
220
+ throw new PermissionAuthorityError(
221
+ `Unsafe operation "${operation}" requires explicit confirmation.`,
222
+ { code, details: { operation, source } }
223
+ );
224
+ }
225
+ const consent = resolveConsentBinding(operation, source, consentType);
226
+ const permissionAuthority = Object.freeze({
227
+ mode: PERMISSION_MODES.UNSAFE, source, consent, operation
228
+ });
229
+ ISSUED_UNSAFE_AUTHORITIES.set(permissionAuthority, permissionAuthority);
230
+ return { operation, permissionAuthority };
231
+ }
@@ -1,5 +1,6 @@
1
1
  import { resolveProfileAgents } from "../profile.js";
2
2
  import { EXECUTION_ADAPTER_IDS } from "./execution-adapters/index.js";
3
+ import { validateDefaultPermissions } from "./run-permissions.js";
3
4
 
4
5
  export function resolveRuntimeOptions(profileResolved, overrides = {}) {
5
6
  const profile = profileResolved?.profile ?? profileResolved ?? {};
@@ -56,6 +57,7 @@ export function validateRuntimeProfile(profile) {
56
57
  if (defaultPermissions != null && !Array.isArray(defaultPermissions)) {
57
58
  throw new Error("Profile defaultPermissions must be an array.");
58
59
  }
60
+ validateDefaultPermissions(defaultPermissions);
59
61
 
60
62
  if (
61
63
  profile.defaultRuntimeAgent != null
@@ -36,6 +36,17 @@ async function shouldPreserveCancelledState(homeDir, runId) {
36
36
  return null;
37
37
  }
38
38
 
39
+ let afterMissedCancelCheckForTests = null;
40
+
41
+ /** Test seam: runs after a cancel-preserve miss, before writing FAILED/COMPLETED. */
42
+ export function setAfterMissedCancelCheckForTests(fn) {
43
+ afterMissedCancelCheckForTests = fn;
44
+ }
45
+
46
+ export function resetAfterMissedCancelCheckForTests() {
47
+ afterMissedCancelCheckForTests = null;
48
+ }
49
+
39
50
  const workerPath = fileURLToPath(new URL("./run-supervisor-worker.js", import.meta.url));
40
51
 
41
52
  export function spawnDetachedSupervisor({ homeDir, runId, spawnImpl = spawn }) {
@@ -182,34 +193,73 @@ export async function supervisePreparedRun({
182
193
  };
183
194
 
184
195
  const completion = new Promise((resolve, reject) => {
196
+ let settled = false;
197
+ const settleResolve = (value) => {
198
+ if (settled) return;
199
+ settled = true;
200
+ resolve(value);
201
+ };
202
+ const settleReject = (error) => {
203
+ if (settled) return;
204
+ settled = true;
205
+ reject(error);
206
+ };
207
+
208
+ const settleCancelled = async () => {
209
+ cancelledRuns?.delete(runId);
210
+ try {
211
+ const fresh = await readRunState(homeDir, runId);
212
+ if (fresh?.state === RUN_STATES.CANCELLED) {
213
+ settleResolve(fresh);
214
+ return;
215
+ }
216
+ settleResolve({ ...(fresh ?? metadata), state: RUN_STATES.CANCELLED });
217
+ } catch {
218
+ settleResolve({ ...metadata, state: RUN_STATES.CANCELLED });
219
+ }
220
+ };
221
+
222
+ const preferCancelled = async () => {
223
+ const preserved = await shouldPreserveCancelledState(homeDir, runId);
224
+ if (preserved || cancelledRuns?.has(runId)) {
225
+ await settleCancelled();
226
+ return true;
227
+ }
228
+ return false;
229
+ };
230
+
185
231
  child.on("error", async (error) => {
186
232
  if (timeoutHandle) clearTimeout(timeoutHandle);
187
233
  activeProcesses?.delete(runId);
188
234
 
189
- await serializeStateWrite(async () => {
190
- const preserved = await shouldPreserveCancelledState(homeDir, runId);
191
- if (preserved) {
192
- cancelledRuns?.delete(runId);
193
- resolve(preserved.state === RUN_STATES.CANCELLED
194
- ? preserved
195
- : { ...preserved, state: RUN_STATES.CANCELLED });
196
- return;
197
- }
235
+ try {
236
+ await serializeStateWrite(async () => {
237
+ if (await preferCancelled()) return;
198
238
 
199
- metadata = transitionRunState(metadata, RUN_STATES.FAILED, {
200
- error: error.message
239
+ if (afterMissedCancelCheckForTests) {
240
+ await afterMissedCancelCheckForTests({ runId, homeDir, phase: "error" });
241
+ }
242
+ if (await preferCancelled()) return;
243
+
244
+ metadata = transitionRunState(metadata, RUN_STATES.FAILED, {
245
+ error: error.message
246
+ });
247
+ await writeRunState(homeDir, metadata);
248
+ await appendRunEvent(homeDir, createRunEvent({
249
+ runId,
250
+ type: "run.failed",
251
+ data: { error: error.message }
252
+ }), { captureTranscript: shouldPersistTranscript(captureTranscript) });
201
253
  });
202
- await writeRunState(homeDir, metadata);
203
- await appendRunEvent(homeDir, createRunEvent({
204
- runId,
205
- type: "run.failed",
206
- data: { error: error.message }
207
- }), { captureTranscript: shouldPersistTranscript(captureTranscript) });
208
- });
209
- reject(error);
254
+ if (!settled) settleReject(error);
255
+ } catch (handlerError) {
256
+ settleReject(handlerError);
257
+ }
210
258
  });
211
259
 
212
260
  child.on("close", async (exitCode) => {
261
+ if (settled) return;
262
+
213
263
  try {
214
264
  if (timeoutHandle) clearTimeout(timeoutHandle);
215
265
  activeProcesses?.delete(runId);
@@ -230,24 +280,14 @@ export async function supervisePreparedRun({
230
280
  await stateWrites;
231
281
 
232
282
  await serializeStateWrite(async () => {
233
- const preserved = await shouldPreserveCancelledState(homeDir, runId);
234
- if (preserved) {
235
- cancelledRuns?.delete(runId);
236
- resolve(preserved.state === RUN_STATES.CANCELLED
237
- ? preserved
238
- : { ...preserved, state: RUN_STATES.CANCELLED });
239
- return;
240
- }
283
+ if (settled) return;
284
+ if (await preferCancelled()) return;
241
285
 
242
- if (cancelledRuns?.has(runId)) {
243
- cancelledRuns.delete(runId);
244
- try {
245
- resolve(await readRunState(homeDir, runId));
246
- } catch {
247
- resolve({ ...metadata, state: RUN_STATES.CANCELLED });
248
- }
249
- return;
286
+ if (afterMissedCancelCheckForTests) {
287
+ await afterMissedCancelCheckForTests({ runId, homeDir, phase: "close", exitCode });
250
288
  }
289
+ if (settled) return;
290
+ if (await preferCancelled()) return;
251
291
 
252
292
  const failed = exitCode !== 0;
253
293
  const nextState = failed ? RUN_STATES.FAILED : RUN_STATES.COMPLETED;
@@ -264,10 +304,10 @@ export async function supervisePreparedRun({
264
304
  if (!failed && strategy === RUN_STRATEGIES.ORCHESTRATED) {
265
305
  await finalizeOrchState(runId, { homeDir, recovered: false });
266
306
  }
267
- resolve(metadata);
307
+ settleResolve(metadata);
268
308
  });
269
309
  } catch (error) {
270
- reject(error);
310
+ settleReject(error);
271
311
  }
272
312
  });
273
313
  });
@@ -81,6 +81,7 @@ export function createRunMetadata({
81
81
  task,
82
82
  cwd,
83
83
  permissions = [],
84
+ permissionAuthority = null,
84
85
  captureTranscript = false,
85
86
  cliVersion,
86
87
  profileSources = null,
@@ -99,6 +100,7 @@ export function createRunMetadata({
99
100
  taskLength,
100
101
  cwd,
101
102
  permissions,
103
+ permissionAuthority,
102
104
  captureTranscript,
103
105
  cliVersion,
104
106
  profileSources,