@kal-elsam/kairo-runtime 0.11.0 → 0.13.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.
- package/global-template/components/agent-skills/LICENSE +21 -0
- package/global-template/components/agent-skills/PROVENANCE.md +26 -0
- package/global-template/components/agent-skills/skills/context-engineering/SKILL.md +289 -0
- package/global-template/components/agent-skills/skills/frontend-ui-engineering/SKILL.md +328 -0
- package/global-template/components/agent-skills/skills/observability-and-instrumentation/SKILL.md +203 -0
- package/global-template/components/agent-skills/skills/performance-optimization/SKILL.md +396 -0
- package/global-template/components/agent-skills/skills/source-driven-development/SKILL.md +194 -0
- package/global-template/components/catalog.json +29 -0
- package/package.json +5 -2
- package/scripts/cockpit-smoke.mjs +2 -1
- package/src/cli.js +136 -8
- package/src/global/component-builders.js +3 -1
- package/src/global/components/agent-skills.js +27 -0
- package/src/global/ink/cockpit-control-center.js +107 -4
- package/src/global/ink/cockpit-scan.js +20 -2
- package/src/global/ink/ecosystem-updates-display.js +37 -0
- package/src/global/ink/launch-input.js +32 -1
- package/src/global/ink/obsidian-vault-display.js +37 -0
- package/src/global/ink/orchestrator-app.js +2 -1
- package/src/global/ink/orchestrator-state.js +17 -2
- package/src/global/ink/system-resources-display.js +109 -0
- package/src/global/ink/use-orchestrator-data.js +40 -4
- package/src/global/ink/ux/live-overview.js +11 -1
- package/src/global/mcp/kairo-mcp.js +230 -0
- package/src/global/observability/build-companion-snapshot.js +302 -0
- package/src/global/observability/build-observability-snapshot.js +24 -0
- package/src/global/observability/ecosystem-updates.js +224 -0
- package/src/global/observability/gentle-bundle-export.js +71 -0
- package/src/global/observability/gentle-bundle-import.js +122 -0
- package/src/global/observability/gentle-probe.js +155 -0
- package/src/global/observability/graphify-ops.js +133 -0
- package/src/global/observability/graphify-parse-cache.js +90 -0
- package/src/global/observability/graphify-probe.js +185 -0
- package/src/global/observability/hermes-activity.js +163 -0
- package/src/global/observability/hermes-probe.js +171 -0
- package/src/global/observability/index.js +124 -0
- package/src/global/observability/obsidian-knowledge-preview.js +214 -0
- package/src/global/observability/obsidian-knowledge-views.js +227 -0
- package/src/global/observability/obsidian-publisher.js +181 -0
- package/src/global/observability/obsidian-status.js +76 -0
- package/src/global/observability/obsidian-vault.js +259 -0
- package/src/global/observability/passive-snapshot-flight.js +93 -0
- package/src/global/observability/probe-contract.js +38 -0
- package/src/global/observability/probe-registry.js +30 -0
- package/src/global/observability/resource-advisor.js +71 -0
- package/src/global/observability/system-resources.js +171 -0
- package/src/global/runtime/alerts/alert-cli.js +31 -0
- package/src/global/runtime/alerts/alert-store.js +29 -6
- package/src/global/runtime/alerts/alert-validate.js +25 -1
- package/src/global/runtime/alerts/controlled-alert-actions.js +56 -0
- package/src/global/runtime/execution-adapters/claude.js +2 -1
- package/src/global/runtime/execution-adapters/codex.js +2 -1
- package/src/global/runtime/execution-adapters/create-execution-adapter.js +3 -14
- package/src/global/runtime/execution-adapters/cursor.js +2 -1
- package/src/global/runtime/execution-adapters/opencode.js +2 -1
- package/src/global/runtime/execution-adapters/pi.js +2 -1
- package/src/global/runtime/review/index.js +1 -1
- package/src/global/runtime/review/review-cli.js +113 -3
- package/src/global/runtime/review/review-git.js +142 -11
- package/src/global/runtime/review/review-patch.js +2 -0
- package/src/global/runtime/review/review-receipts.js +12 -7
- package/src/global/runtime/review/review-runner.js +2 -2
- package/src/global/runtime/review/review-types.js +8 -5
- package/src/global/runtime/review/review-validate.js +5 -1
- package/src/global/runtime/run-cli.js +2 -0
- package/src/global/runtime/run-manager.js +39 -18
- package/src/global/runtime/run-permissions.js +231 -0
- package/src/global/runtime/run-profile.js +2 -0
- package/src/global/runtime/run-supervisor.js +77 -37
- package/src/global/runtime/run-types.js +2 -0
- package/src/global/updates-cli.js +41 -0
|
@@ -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
|
-
|
|
35
|
-
|
|
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: [{
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
234
|
-
if (
|
|
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 (
|
|
243
|
-
|
|
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
|
-
|
|
307
|
+
settleResolve(metadata);
|
|
268
308
|
});
|
|
269
309
|
} catch (error) {
|
|
270
|
-
|
|
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,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { loadEcosystemUpdates as defaultLoadEcosystemUpdates } from "./observability/ecosystem-updates.js";
|
|
2
|
+
import { commandHeader } from "./brand/index.js";
|
|
3
|
+
import { resolveHomeDir } from "./paths.js";
|
|
4
|
+
|
|
5
|
+
/** Read-only ecosystem update check — never applies upgrades. */
|
|
6
|
+
export async function runEcosystemUpdatesCheck(
|
|
7
|
+
options = {},
|
|
8
|
+
packageManifest = {},
|
|
9
|
+
{ loadEcosystemUpdates = defaultLoadEcosystemUpdates } = {}
|
|
10
|
+
) {
|
|
11
|
+
const action = options.updatesAction ?? "check";
|
|
12
|
+
if (action !== "check") {
|
|
13
|
+
throw new Error(`Unknown updates action "${action}". Use: kairo updates check`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const report = await loadEcosystemUpdates({
|
|
17
|
+
packageName: packageManifest.name ?? "@kal-elsam/kairo-runtime",
|
|
18
|
+
installedVersion: packageManifest.version ?? "0.0.0",
|
|
19
|
+
homeDir: resolveHomeDir(options.env ?? process.env),
|
|
20
|
+
forceRefresh: options.force === true,
|
|
21
|
+
env: options.env ?? process.env
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (options.json) {
|
|
25
|
+
console.log(JSON.stringify({ command: "updates", action: "check", ...report }, null, 2));
|
|
26
|
+
return report;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(commandHeader("updates check — ecosystem (read-only)"));
|
|
30
|
+
console.log(`State: ${report.state}${report.cacheHit ? " · cache" : ""}`);
|
|
31
|
+
if (report.checkedAt) console.log(`Checked: ${report.checkedAt}`);
|
|
32
|
+
for (const id of ["kairo", "hermes", "gentle", "skills"]) {
|
|
33
|
+
const tool = report.tools?.[id] ?? {};
|
|
34
|
+
const flag = tool.updateAvailable ? "UPDATE" : "ok";
|
|
35
|
+
const err = tool.error ? ` · ${tool.error}` : "";
|
|
36
|
+
console.log(`[${flag}] ${id.padEnd(7)} installed: ${tool.installed ?? "—"} latest: ${tool.latest ?? "—"}${err}`);
|
|
37
|
+
}
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("No updates were applied. Consent-gated apply lands in a later slice.");
|
|
40
|
+
return report;
|
|
41
|
+
}
|