@suwujs/king-ai 0.2.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/README.md +96 -0
- package/dist/src/agent-config-validation.d.ts +9 -0
- package/dist/src/agent-config-validation.js +30 -0
- package/dist/src/api.d.ts +4 -0
- package/dist/src/api.js +48 -0
- package/dist/src/attachments.d.ts +45 -0
- package/dist/src/attachments.js +322 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +1697 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +20 -0
- package/dist/src/cron.d.ts +11 -0
- package/dist/src/cron.js +65 -0
- package/dist/src/daemon.d.ts +36 -0
- package/dist/src/daemon.js +373 -0
- package/dist/src/engine.d.ts +32 -0
- package/dist/src/engine.js +1014 -0
- package/dist/src/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat.js +28 -0
- package/dist/src/host-api.d.ts +40 -0
- package/dist/src/host-api.js +59 -0
- package/dist/src/host-control.d.ts +48 -0
- package/dist/src/host-control.js +1279 -0
- package/dist/src/host-export.d.ts +50 -0
- package/dist/src/host-export.js +187 -0
- package/dist/src/host-feedback.d.ts +78 -0
- package/dist/src/host-feedback.js +178 -0
- package/dist/src/host-home.d.ts +13 -0
- package/dist/src/host-home.js +54 -0
- package/dist/src/host-ledger.d.ts +261 -0
- package/dist/src/host-ledger.js +554 -0
- package/dist/src/host-loop-events.d.ts +69 -0
- package/dist/src/host-loop-events.js +288 -0
- package/dist/src/host-permission.d.ts +36 -0
- package/dist/src/host-permission.js +180 -0
- package/dist/src/host-policy.d.ts +15 -0
- package/dist/src/host-policy.js +36 -0
- package/dist/src/host-run-executor.d.ts +13 -0
- package/dist/src/host-run-executor.js +221 -0
- package/dist/src/host-run-heartbeat.d.ts +40 -0
- package/dist/src/host-run-heartbeat.js +103 -0
- package/dist/src/host-run-layout.d.ts +17 -0
- package/dist/src/host-run-layout.js +387 -0
- package/dist/src/host-run-meta.d.ts +41 -0
- package/dist/src/host-run-meta.js +115 -0
- package/dist/src/host-run-spec.d.ts +149 -0
- package/dist/src/host-run-spec.js +465 -0
- package/dist/src/host-runs.d.ts +77 -0
- package/dist/src/host-runs.js +195 -0
- package/dist/src/host-sdk.d.ts +412 -0
- package/dist/src/host-sdk.js +628 -0
- package/dist/src/host-server.d.ts +26 -0
- package/dist/src/host-server.js +921 -0
- package/dist/src/host-timeline.d.ts +24 -0
- package/dist/src/host-timeline.js +161 -0
- package/dist/src/jsonl.d.ts +13 -0
- package/dist/src/jsonl.js +47 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/message-routing.d.ts +32 -0
- package/dist/src/message-routing.js +119 -0
- package/dist/src/paths.d.ts +19 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/project-profile.d.ts +49 -0
- package/dist/src/project-profile.js +356 -0
- package/dist/src/remediation.d.ts +14 -0
- package/dist/src/remediation.js +114 -0
- package/dist/src/remote-devices.d.ts +41 -0
- package/dist/src/remote-devices.js +156 -0
- package/dist/src/remote-diagnostics.d.ts +39 -0
- package/dist/src/remote-diagnostics.js +199 -0
- package/dist/src/remote-ssh.d.ts +39 -0
- package/dist/src/remote-ssh.js +129 -0
- package/dist/src/run-stream.d.ts +57 -0
- package/dist/src/run-stream.js +119 -0
- package/dist/src/runner.d.ts +131 -0
- package/dist/src/runner.js +1161 -0
- package/dist/src/runtime-data.d.ts +68 -0
- package/dist/src/runtime-data.js +172 -0
- package/dist/src/service.d.ts +114 -0
- package/dist/src/service.js +631 -0
- package/dist/src/shared-skills.d.ts +26 -0
- package/dist/src/shared-skills.js +85 -0
- package/dist/src/shim.d.ts +1 -0
- package/dist/src/shim.js +64 -0
- package/dist/src/skill-check.d.ts +17 -0
- package/dist/src/skill-check.js +158 -0
- package/dist/src/sse.d.ts +9 -0
- package/dist/src/sse.js +36 -0
- package/dist/src/team-routing.d.ts +55 -0
- package/dist/src/team-routing.js +131 -0
- package/dist/src/team-workflow.d.ts +78 -0
- package/dist/src/team-workflow.js +253 -0
- package/dist/src/text.d.ts +7 -0
- package/dist/src/text.js +27 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +1 -0
- package/dist/src/usage.d.ts +116 -0
- package/dist/src/usage.js +350 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +56 -0
- package/dist/src/worktree.d.ts +47 -0
- package/dist/src/worktree.js +201 -0
- package/package.json +63 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
export const HOST_LOOP_RESULTS_HEADER = [
|
|
4
|
+
"run_id",
|
|
5
|
+
"loop",
|
|
6
|
+
"timestamp",
|
|
7
|
+
"classification",
|
|
8
|
+
"tasks_created",
|
|
9
|
+
"tasks_done",
|
|
10
|
+
"artifacts_created",
|
|
11
|
+
"pending_messages",
|
|
12
|
+
"completion_rate",
|
|
13
|
+
"notes"
|
|
14
|
+
].join("\t") + "\n";
|
|
15
|
+
export async function appendHostLoopEvent(input) {
|
|
16
|
+
const file = resolveLoopEventsPath(input);
|
|
17
|
+
await mkdir(dirname(file), { recursive: true });
|
|
18
|
+
await appendFile(file, `${JSON.stringify(dropUndefined({ ...input.event }))}\n`, "utf8");
|
|
19
|
+
return file;
|
|
20
|
+
}
|
|
21
|
+
export async function readHostLoopEvents(input = {}) {
|
|
22
|
+
const file = resolveLoopEventsPath(input);
|
|
23
|
+
const text = await readFile(file, "utf8").catch((err) => {
|
|
24
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT")
|
|
25
|
+
return "";
|
|
26
|
+
throw err;
|
|
27
|
+
});
|
|
28
|
+
const events = parseLoopEvents(text);
|
|
29
|
+
const filtered = events.filter((event) => matchesLoopEvent(event, input));
|
|
30
|
+
const tail = normalizeTail(input.tail);
|
|
31
|
+
const display = filtered.slice(-tail);
|
|
32
|
+
const results = await maybeWriteLoopResults(events, input, file);
|
|
33
|
+
return {
|
|
34
|
+
file,
|
|
35
|
+
events: display,
|
|
36
|
+
totalEvents: events.length,
|
|
37
|
+
filteredEvents: filtered.length,
|
|
38
|
+
summary: summarizeLoopEvents(filtered),
|
|
39
|
+
results
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function readHostLoopResults(input = {}) {
|
|
43
|
+
const eventsFile = resolveLoopEventsPath(input);
|
|
44
|
+
const file = resolveLoopResultsPath(input, eventsFile);
|
|
45
|
+
const existing = await readFile(file, "utf8").catch((err) => {
|
|
46
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT")
|
|
47
|
+
return undefined;
|
|
48
|
+
throw err;
|
|
49
|
+
});
|
|
50
|
+
if (existing !== undefined) {
|
|
51
|
+
return {
|
|
52
|
+
file,
|
|
53
|
+
rows: parseLoopResultsTable(existing),
|
|
54
|
+
written: false,
|
|
55
|
+
text: existing
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const eventsText = await readFile(eventsFile, "utf8").catch((err) => {
|
|
59
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT")
|
|
60
|
+
return "";
|
|
61
|
+
throw err;
|
|
62
|
+
});
|
|
63
|
+
const rows = buildLoopResultsRows(parseLoopEvents(eventsText));
|
|
64
|
+
return {
|
|
65
|
+
file,
|
|
66
|
+
rows,
|
|
67
|
+
written: false,
|
|
68
|
+
text: formatLoopResultsTable(rows)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function formatHostLoopEvents(result) {
|
|
72
|
+
if (result.events.length === 0) {
|
|
73
|
+
return [
|
|
74
|
+
`host run events: ${result.file}`,
|
|
75
|
+
"no loop events"
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
const lines = [
|
|
79
|
+
`host run events: ${result.file}`,
|
|
80
|
+
`${result.events.length} shown (${result.filteredEvents} matched, ${result.totalEvents} total)`
|
|
81
|
+
];
|
|
82
|
+
for (const event of result.events) {
|
|
83
|
+
lines.push(formatHostLoopEvent(event));
|
|
84
|
+
}
|
|
85
|
+
if (result.summary.loops > 0) {
|
|
86
|
+
lines.push(`summary: ${result.summary.loops} classified loops, productive=${result.summary.productiveRate}%`);
|
|
87
|
+
}
|
|
88
|
+
if (result.results.written) {
|
|
89
|
+
lines.push(`results: ${result.results.file} (${result.results.rows.length} rows)`);
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
function resolveLoopEventsPath(input) {
|
|
94
|
+
if (input.file && input.file.trim())
|
|
95
|
+
return resolve(input.file);
|
|
96
|
+
const outputDir = input.outputDir && input.outputDir.trim() ? input.outputDir : "deliverables";
|
|
97
|
+
return resolve(join(outputDir, "loop-events.ndjson"));
|
|
98
|
+
}
|
|
99
|
+
function resolveLoopResultsPath(input, eventsFile) {
|
|
100
|
+
if (input.resultsFile && input.resultsFile.trim())
|
|
101
|
+
return resolve(input.resultsFile);
|
|
102
|
+
const outputDir = input.outputDir && input.outputDir.trim() ? input.outputDir : dirname(eventsFile);
|
|
103
|
+
return resolve(join(outputDir, "results.tsv"));
|
|
104
|
+
}
|
|
105
|
+
function parseLoopEvents(text) {
|
|
106
|
+
const events = [];
|
|
107
|
+
for (const line of text.split(/\r?\n/)) {
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (!trimmed)
|
|
110
|
+
continue;
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(trimmed);
|
|
113
|
+
if (parsed && typeof parsed === "object")
|
|
114
|
+
events.push(parsed);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Ignore malformed lines so a partially written NDJSON file stays readable.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return events;
|
|
121
|
+
}
|
|
122
|
+
function matchesLoopEvent(event, input) {
|
|
123
|
+
if (input.runId && event.runId !== input.runId)
|
|
124
|
+
return false;
|
|
125
|
+
if (input.agent && event.agent !== input.agent)
|
|
126
|
+
return false;
|
|
127
|
+
if (input.type && event.type !== input.type)
|
|
128
|
+
return false;
|
|
129
|
+
if (input.classification && event.classification !== input.classification)
|
|
130
|
+
return false;
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
function summarizeLoopEvents(events) {
|
|
134
|
+
const classifications = {};
|
|
135
|
+
for (const event of events) {
|
|
136
|
+
if (event.type !== "loop.classified" || typeof event.classification !== "string")
|
|
137
|
+
continue;
|
|
138
|
+
classifications[event.classification] = (classifications[event.classification] ?? 0) + 1;
|
|
139
|
+
}
|
|
140
|
+
const loops = Object.values(classifications).reduce((sum, count) => sum + count, 0);
|
|
141
|
+
const productive = classifications.productive ?? 0;
|
|
142
|
+
return {
|
|
143
|
+
loops,
|
|
144
|
+
classifications,
|
|
145
|
+
productiveRate: loops > 0 ? Math.round((productive / loops) * 100) : 0
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function maybeWriteLoopResults(events, input, eventsFile) {
|
|
149
|
+
const file = resolveLoopResultsPath(input, eventsFile);
|
|
150
|
+
const rows = buildLoopResultsRows(events);
|
|
151
|
+
if (input.writeResults === false || (events.length === 0 && input.writeResults !== true))
|
|
152
|
+
return { file, rows, written: false };
|
|
153
|
+
await mkdir(dirname(file), { recursive: true });
|
|
154
|
+
await writeFile(file, formatLoopResultsTable(rows), "utf8");
|
|
155
|
+
return { file, rows, written: true };
|
|
156
|
+
}
|
|
157
|
+
export function buildLoopResultsRows(events) {
|
|
158
|
+
const rows = [];
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
if (event.type !== "loop.classified" || typeof event.classification !== "string")
|
|
161
|
+
continue;
|
|
162
|
+
const runId = typeof event.runId === "string" && event.runId.trim() ? event.runId.trim() : "";
|
|
163
|
+
const loop = numberValue(event.loop);
|
|
164
|
+
if (loop === undefined)
|
|
165
|
+
continue;
|
|
166
|
+
const loopEvents = events.filter((entry) => sameLoop(entry, runId, loop));
|
|
167
|
+
rows.push({
|
|
168
|
+
runId,
|
|
169
|
+
loop,
|
|
170
|
+
timestamp: typeof event.timestamp === "string" ? event.timestamp : "",
|
|
171
|
+
classification: event.classification,
|
|
172
|
+
tasksCreated: loopEvents.filter((entry) => entry.type === "task.created").length,
|
|
173
|
+
tasksDone: loopEvents.filter((entry) => entry.type === "task.transition" && doneStatus(entry.to)).length,
|
|
174
|
+
artifactsCreated: loopEvents.filter((entry) => entry.type === "artifact.created").length,
|
|
175
|
+
pendingMessages: maxNumber(loopEvents.map((entry) => numberValue(entry.pendingMessages) ?? numberValue(entry.pending))),
|
|
176
|
+
completionRate: completionRateValue(event),
|
|
177
|
+
notes: notesValue(event)
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return rows.sort((a, b) => a.runId.localeCompare(b.runId) || a.loop - b.loop || a.timestamp.localeCompare(b.timestamp));
|
|
181
|
+
}
|
|
182
|
+
export function formatLoopResultsTable(rows) {
|
|
183
|
+
return HOST_LOOP_RESULTS_HEADER + rows.map((row) => [
|
|
184
|
+
row.runId,
|
|
185
|
+
String(row.loop),
|
|
186
|
+
row.timestamp,
|
|
187
|
+
row.classification,
|
|
188
|
+
String(row.tasksCreated),
|
|
189
|
+
String(row.tasksDone),
|
|
190
|
+
String(row.artifactsCreated),
|
|
191
|
+
String(row.pendingMessages),
|
|
192
|
+
row.completionRate,
|
|
193
|
+
row.notes
|
|
194
|
+
].map(tsvCell).join("\t")).join("\n") + (rows.length ? "\n" : "");
|
|
195
|
+
}
|
|
196
|
+
export function parseLoopResultsTable(text) {
|
|
197
|
+
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
|
198
|
+
if (lines.length <= 1)
|
|
199
|
+
return [];
|
|
200
|
+
const header = lines[0]?.split("\t") ?? [];
|
|
201
|
+
const index = new Map(header.map((name, i) => [name, i]));
|
|
202
|
+
return lines.slice(1).map((line) => {
|
|
203
|
+
const cells = line.split("\t");
|
|
204
|
+
return {
|
|
205
|
+
runId: cell(cells, index, "run_id"),
|
|
206
|
+
loop: Number.parseInt(cell(cells, index, "loop") || "0", 10) || 0,
|
|
207
|
+
timestamp: cell(cells, index, "timestamp"),
|
|
208
|
+
classification: cell(cells, index, "classification"),
|
|
209
|
+
tasksCreated: Number.parseInt(cell(cells, index, "tasks_created") || "0", 10) || 0,
|
|
210
|
+
tasksDone: Number.parseInt(cell(cells, index, "tasks_done") || "0", 10) || 0,
|
|
211
|
+
artifactsCreated: Number.parseInt(cell(cells, index, "artifacts_created") || "0", 10) || 0,
|
|
212
|
+
pendingMessages: Number.parseInt(cell(cells, index, "pending_messages") || "0", 10) || 0,
|
|
213
|
+
completionRate: cell(cells, index, "completion_rate"),
|
|
214
|
+
notes: cell(cells, index, "notes")
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function cell(cells, index, name) {
|
|
219
|
+
const i = index.get(name);
|
|
220
|
+
return i === undefined ? "" : cells[i] ?? "";
|
|
221
|
+
}
|
|
222
|
+
function sameLoop(event, runId, loop) {
|
|
223
|
+
const eventRunId = typeof event.runId === "string" ? event.runId : "";
|
|
224
|
+
return eventRunId === runId && numberValue(event.loop) === loop;
|
|
225
|
+
}
|
|
226
|
+
function doneStatus(value) {
|
|
227
|
+
return value === "done" || value === "completed" || value === "complete";
|
|
228
|
+
}
|
|
229
|
+
function completionRateValue(event) {
|
|
230
|
+
const value = numberValue(event.completionRate) ?? numberValue(event.completion_rate);
|
|
231
|
+
return value === undefined ? "" : String(value);
|
|
232
|
+
}
|
|
233
|
+
function notesValue(event) {
|
|
234
|
+
if (Array.isArray(event.reasons))
|
|
235
|
+
return event.reasons.map(String).join("; ");
|
|
236
|
+
if (typeof event.notes === "string")
|
|
237
|
+
return event.notes;
|
|
238
|
+
if (typeof event.reason === "string")
|
|
239
|
+
return event.reason;
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
function maxNumber(values) {
|
|
243
|
+
const numeric = values.filter((value) => value !== undefined);
|
|
244
|
+
return numeric.length ? Math.max(...numeric) : 0;
|
|
245
|
+
}
|
|
246
|
+
function numberValue(value) {
|
|
247
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
248
|
+
return value;
|
|
249
|
+
if (typeof value === "string" && value.trim()) {
|
|
250
|
+
const parsed = Number(value);
|
|
251
|
+
if (Number.isFinite(parsed))
|
|
252
|
+
return parsed;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
function tsvCell(value) {
|
|
257
|
+
return value.replace(/\t/g, " ").replace(/\r?\n/g, " ").trim();
|
|
258
|
+
}
|
|
259
|
+
function formatHostLoopEvent(event) {
|
|
260
|
+
const ts = typeof event.timestamp === "string" ? event.timestamp : "no-time";
|
|
261
|
+
const loop = event.loop === undefined ? "?" : String(event.loop);
|
|
262
|
+
const type = typeof event.type === "string" ? event.type : "unknown";
|
|
263
|
+
if (type === "loop.classified") {
|
|
264
|
+
const reasons = Array.isArray(event.reasons) ? event.reasons.join("; ") : "";
|
|
265
|
+
return `${ts} loop=${loop} ${type} ${event.classification ?? "unknown"}${reasons ? ` ${reasons}` : ""}`;
|
|
266
|
+
}
|
|
267
|
+
if (type === "task.transition") {
|
|
268
|
+
return `${ts} loop=${loop} ${type} task=${event.taskId ?? "?"} ${event.from ?? "?"}->${event.to ?? "?"}`;
|
|
269
|
+
}
|
|
270
|
+
if (type === "queue.backlog") {
|
|
271
|
+
return `${ts} loop=${loop} ${type} agent=${event.agent ?? "?"} pending=${event.pendingMessages ?? event.pending ?? "?"}`;
|
|
272
|
+
}
|
|
273
|
+
if (type === "agent.spawned") {
|
|
274
|
+
return `${ts} loop=${loop} ${type} agent=${event.agent ?? "?"} trigger=${event.trigger ?? "?"}`;
|
|
275
|
+
}
|
|
276
|
+
return `${ts} loop=${loop} ${type} ${JSON.stringify(event).slice(0, 120)}`;
|
|
277
|
+
}
|
|
278
|
+
function dropUndefined(value) {
|
|
279
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
280
|
+
}
|
|
281
|
+
function normalizeTail(value) {
|
|
282
|
+
if (value === undefined || value === null || value === "")
|
|
283
|
+
return 20;
|
|
284
|
+
const tail = typeof value === "number" ? value : Number.parseInt(String(value), 10);
|
|
285
|
+
if (!Number.isFinite(tail) || tail < 1)
|
|
286
|
+
throw new Error("loop event tail must be a positive integer");
|
|
287
|
+
return Math.floor(tail);
|
|
288
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type KingTeamPermissionAction, type KingTeamPermissionRule, type KingTeamSpec } from "./team-workflow.js";
|
|
2
|
+
export interface HostPermissionRequest {
|
|
3
|
+
actorRole?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HostPermissionDeps {
|
|
6
|
+
teamSpec?: () => KingTeamSpec | null | undefined;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
}
|
|
9
|
+
export interface HostPermissionOutcome {
|
|
10
|
+
enforced: boolean;
|
|
11
|
+
role?: string;
|
|
12
|
+
action: KingTeamPermissionAction | null;
|
|
13
|
+
decision?: "allow" | "deny" | "human-decision";
|
|
14
|
+
rule?: KingTeamPermissionRule;
|
|
15
|
+
}
|
|
16
|
+
export declare function hostCommandPermissionAction(command: string, input?: unknown): KingTeamPermissionAction | null;
|
|
17
|
+
export declare function resolveActorRole(request: HostPermissionRequest, env?: NodeJS.ProcessEnv): string | undefined;
|
|
18
|
+
export declare function resolveTeamSpec(deps?: HostPermissionDeps): KingTeamSpec;
|
|
19
|
+
/**
|
|
20
|
+
* Evaluate whether `request.actorRole` may run `command` under the team governance policy.
|
|
21
|
+
* Governance is opt-in: when no role is resolved (request field or KING_AI_TEAM_ROLE), trusted local
|
|
22
|
+
* automation proceeds exactly as before. This is not the host security boundary.
|
|
23
|
+
*/
|
|
24
|
+
export declare function evaluateHostCommandPermission(command: string, input: unknown, request: HostPermissionRequest, deps?: HostPermissionDeps): HostPermissionOutcome;
|
|
25
|
+
export interface HumanApprovalOutcome {
|
|
26
|
+
approved: boolean;
|
|
27
|
+
approver?: string;
|
|
28
|
+
reason?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Evaluate a human-decision marker supplied on a re-issued command. The marker is only valid when
|
|
32
|
+
* `approvedBy` is present AND differs from the requesting role, so a role cannot clear its own
|
|
33
|
+
* governance gate by simply setting `humanApproved=true`. This is still an automation-friendly
|
|
34
|
+
* governance/audit control, not a cryptographic identity check.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveHumanApproval(input: unknown, requesterRole?: string): HumanApprovalOutcome;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { checkTeamPermission, defaultTeamSpec, normalizeTeamRoleId } from "./team-workflow.js";
|
|
2
|
+
/**
|
|
3
|
+
* Map an allowlisted host command (and its input) to the team governance action it represents.
|
|
4
|
+
* Returns null when the command has no role-governance concern (read-only, observability, or run
|
|
5
|
+
* lifecycle plumbing already gated by other policy).
|
|
6
|
+
*/
|
|
7
|
+
const PERMISSION_ACTIONS = [
|
|
8
|
+
"assign-task",
|
|
9
|
+
"claim-task",
|
|
10
|
+
"create-artifact",
|
|
11
|
+
"create-decision",
|
|
12
|
+
"approve-decision",
|
|
13
|
+
"close-task",
|
|
14
|
+
"change-scope",
|
|
15
|
+
"deploy-release",
|
|
16
|
+
"view-audit",
|
|
17
|
+
"manage-queue",
|
|
18
|
+
"view-cost"
|
|
19
|
+
];
|
|
20
|
+
export function hostCommandPermissionAction(command, input) {
|
|
21
|
+
// An explicit, valid `permissionAction` on the input is authoritative — callers that know their
|
|
22
|
+
// intent can opt out of the heuristic inference below. It can only tighten authorization (the
|
|
23
|
+
// action is still checked against the role policy), never bypass it.
|
|
24
|
+
const explicit = explicitPermissionAction(input);
|
|
25
|
+
if (explicit)
|
|
26
|
+
return explicit;
|
|
27
|
+
switch (command) {
|
|
28
|
+
case "timeline":
|
|
29
|
+
return "view-audit";
|
|
30
|
+
case "usage":
|
|
31
|
+
case "expenses":
|
|
32
|
+
return "view-cost";
|
|
33
|
+
case "task-create":
|
|
34
|
+
case "initiative-create":
|
|
35
|
+
case "handoff-create":
|
|
36
|
+
case "capsule-create":
|
|
37
|
+
return "assign-task";
|
|
38
|
+
case "task-update":
|
|
39
|
+
return ledgerMutationAction(input);
|
|
40
|
+
case "capsule-update":
|
|
41
|
+
return "claim-task";
|
|
42
|
+
case "artifact-create":
|
|
43
|
+
case "review-create":
|
|
44
|
+
return "create-artifact";
|
|
45
|
+
case "decision-create":
|
|
46
|
+
return "create-decision";
|
|
47
|
+
case "workflow-create":
|
|
48
|
+
return workflowCreateAction(input);
|
|
49
|
+
case "workflow-update":
|
|
50
|
+
return workflowUpdateAction(input);
|
|
51
|
+
case "submit-run":
|
|
52
|
+
case "cancel-run":
|
|
53
|
+
case "update-run":
|
|
54
|
+
case "compact-ledger":
|
|
55
|
+
return "manage-queue";
|
|
56
|
+
case "execute-run":
|
|
57
|
+
return "deploy-release";
|
|
58
|
+
case "export":
|
|
59
|
+
return "create-artifact";
|
|
60
|
+
default:
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function resolveActorRole(request, env = process.env) {
|
|
65
|
+
return cleanString(request.actorRole) ?? cleanString(env.KING_AI_TEAM_ROLE);
|
|
66
|
+
}
|
|
67
|
+
export function resolveTeamSpec(deps = {}) {
|
|
68
|
+
const provided = deps.teamSpec?.();
|
|
69
|
+
if (provided)
|
|
70
|
+
return provided;
|
|
71
|
+
const raw = (deps.env ?? process.env).KING_AI_TEAM_SPEC;
|
|
72
|
+
if (raw && raw.trim()) {
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (isTeamSpec(parsed))
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// fall through to the built-in default team spec
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return defaultTeamSpec();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate whether `request.actorRole` may run `command` under the team governance policy.
|
|
86
|
+
* Governance is opt-in: when no role is resolved (request field or KING_AI_TEAM_ROLE), trusted local
|
|
87
|
+
* automation proceeds exactly as before. This is not the host security boundary.
|
|
88
|
+
*/
|
|
89
|
+
export function evaluateHostCommandPermission(command, input, request, deps = {}) {
|
|
90
|
+
const role = resolveActorRole(request, deps.env ?? process.env);
|
|
91
|
+
if (!role)
|
|
92
|
+
return { enforced: false, action: null };
|
|
93
|
+
const action = hostCommandPermissionAction(command, input);
|
|
94
|
+
if (!action)
|
|
95
|
+
return { enforced: false, role, action: null };
|
|
96
|
+
const team = resolveTeamSpec(deps);
|
|
97
|
+
const normalizedRole = normalizeTeamRoleId(role);
|
|
98
|
+
const result = checkTeamPermission(team, normalizedRole, action);
|
|
99
|
+
return { enforced: true, role: normalizedRole, action, decision: result.decision, rule: result.rule };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Evaluate a human-decision marker supplied on a re-issued command. The marker is only valid when
|
|
103
|
+
* `approvedBy` is present AND differs from the requesting role, so a role cannot clear its own
|
|
104
|
+
* governance gate by simply setting `humanApproved=true`. This is still an automation-friendly
|
|
105
|
+
* governance/audit control, not a cryptographic identity check.
|
|
106
|
+
*/
|
|
107
|
+
export function resolveHumanApproval(input, requesterRole) {
|
|
108
|
+
if (!input || typeof input !== "object")
|
|
109
|
+
return { approved: false, reason: "no approval provided" };
|
|
110
|
+
const record = input;
|
|
111
|
+
const flagged = record.humanApproved === true || record.humanDecision === "approved";
|
|
112
|
+
if (!flagged)
|
|
113
|
+
return { approved: false, reason: "set humanApproved=true to grant approval" };
|
|
114
|
+
const approver = cleanString(record.approvedBy);
|
|
115
|
+
if (!approver)
|
|
116
|
+
return { approved: false, reason: "approvedBy is required (the human or role granting approval)" };
|
|
117
|
+
if (requesterRole && approver === requesterRole) {
|
|
118
|
+
return { approved: false, approver, reason: "approver must differ from the requesting role" };
|
|
119
|
+
}
|
|
120
|
+
return { approved: true, approver };
|
|
121
|
+
}
|
|
122
|
+
function explicitPermissionAction(input) {
|
|
123
|
+
if (!input || typeof input !== "object")
|
|
124
|
+
return null;
|
|
125
|
+
const value = input.permissionAction;
|
|
126
|
+
return typeof value === "string" && PERMISSION_ACTIONS.includes(value)
|
|
127
|
+
? value
|
|
128
|
+
: null;
|
|
129
|
+
}
|
|
130
|
+
function ledgerMutationAction(input) {
|
|
131
|
+
const status = stringField(input, "status");
|
|
132
|
+
if (status === "done" || status === "cancelled")
|
|
133
|
+
return "close-task";
|
|
134
|
+
if (hasField(input, "acceptance") || hasField(input, "dependsOn"))
|
|
135
|
+
return "change-scope";
|
|
136
|
+
return "claim-task";
|
|
137
|
+
}
|
|
138
|
+
function workflowCreateAction(input) {
|
|
139
|
+
switch (stringField(input, "kind")) {
|
|
140
|
+
case "decision":
|
|
141
|
+
return "create-decision";
|
|
142
|
+
case "artifact":
|
|
143
|
+
case "review":
|
|
144
|
+
return "create-artifact";
|
|
145
|
+
default:
|
|
146
|
+
return "assign-task";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function workflowUpdateAction(input) {
|
|
150
|
+
const id = stringField(input, "id") ?? "";
|
|
151
|
+
const kind = stringField(input, "kind");
|
|
152
|
+
if (kind === "decision" || id.startsWith("decision-") || id.startsWith("decision:"))
|
|
153
|
+
return "approve-decision";
|
|
154
|
+
if (hasField(input, "acceptance") || hasField(input, "dependsOn"))
|
|
155
|
+
return "change-scope";
|
|
156
|
+
const status = stringField(input, "status");
|
|
157
|
+
if (status === "done" || status === "cancelled")
|
|
158
|
+
return "close-task";
|
|
159
|
+
return "claim-task";
|
|
160
|
+
}
|
|
161
|
+
function isTeamSpec(value) {
|
|
162
|
+
if (!value || typeof value !== "object")
|
|
163
|
+
return false;
|
|
164
|
+
const record = value;
|
|
165
|
+
return typeof record.id === "string"
|
|
166
|
+
&& Array.isArray(record.roles)
|
|
167
|
+
&& Boolean(record.permissionPolicy)
|
|
168
|
+
&& Array.isArray(record.permissionPolicy.rules);
|
|
169
|
+
}
|
|
170
|
+
function stringField(input, key) {
|
|
171
|
+
if (!input || typeof input !== "object")
|
|
172
|
+
return undefined;
|
|
173
|
+
return cleanString(input[key]);
|
|
174
|
+
}
|
|
175
|
+
function hasField(input, key) {
|
|
176
|
+
return Boolean(input && typeof input === "object" && input[key] !== undefined);
|
|
177
|
+
}
|
|
178
|
+
function cleanString(value) {
|
|
179
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
180
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type HostPolicyDecision = "allow" | "confirm_required";
|
|
2
|
+
export interface HostPolicyCheck {
|
|
3
|
+
command: string;
|
|
4
|
+
destructive: boolean;
|
|
5
|
+
decision: HostPolicyDecision;
|
|
6
|
+
reason: string;
|
|
7
|
+
requiredConfirmation?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HostPolicyInput {
|
|
10
|
+
confirmed?: unknown;
|
|
11
|
+
confirmation?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare function requiredHostConfirmation(command: string): string;
|
|
14
|
+
export declare function checkHostCommandPolicy(command: string, destructive: boolean, input?: HostPolicyInput): HostPolicyCheck;
|
|
15
|
+
export declare function formatHostPolicyCheck(check: HostPolicyCheck): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function requiredHostConfirmation(command) {
|
|
2
|
+
return `allow:${command}`;
|
|
3
|
+
}
|
|
4
|
+
export function checkHostCommandPolicy(command, destructive, input) {
|
|
5
|
+
if (!destructive) {
|
|
6
|
+
return {
|
|
7
|
+
command,
|
|
8
|
+
destructive,
|
|
9
|
+
decision: "allow",
|
|
10
|
+
reason: "read-only host command"
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const requiredConfirmation = requiredHostConfirmation(command);
|
|
14
|
+
if (input?.confirmed === true || input?.confirmation === requiredConfirmation) {
|
|
15
|
+
return {
|
|
16
|
+
command,
|
|
17
|
+
destructive,
|
|
18
|
+
decision: "allow",
|
|
19
|
+
reason: "destructive host command explicitly confirmed",
|
|
20
|
+
requiredConfirmation
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
command,
|
|
25
|
+
destructive,
|
|
26
|
+
decision: "confirm_required",
|
|
27
|
+
reason: "destructive host command requires explicit confirmation",
|
|
28
|
+
requiredConfirmation
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function formatHostPolicyCheck(check) {
|
|
32
|
+
if (check.decision === "allow") {
|
|
33
|
+
return `${check.command}: allowed (${check.reason})`;
|
|
34
|
+
}
|
|
35
|
+
return `${check.command}: confirmation required (${check.reason}; confirmation=${check.requiredConfirmation})`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HostRunRequest } from "./host-runs.js";
|
|
2
|
+
import type { HostCommandResult, HostCommandRunnerDeps } from "./host-control.js";
|
|
3
|
+
export interface HostRunExecuteInput {
|
|
4
|
+
id?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface HostRunExecuteResult {
|
|
7
|
+
request?: HostRunRequest;
|
|
8
|
+
commandResult?: HostCommandResult;
|
|
9
|
+
summary: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function listSafeHostExecutorCommands(): string[];
|
|
12
|
+
export declare function executeNextHostRunRequest(input?: HostRunExecuteInput, deps?: HostCommandRunnerDeps): Promise<HostRunExecuteResult>;
|
|
13
|
+
export declare function formatHostRunExecuteResult(result: HostRunExecuteResult): string;
|