@kontextmind/kxm 0.7.0 → 0.7.10
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/.claude-plugin/marketplace.json +1 -1
- package/.kxm/roles/writer.yaml +2 -0
- package/docs/README.md +3 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +19 -2
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +10 -1
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/operations.md +24 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +5 -0
- package/docs/skills.md +2 -0
- package/docs/troubleshooting.md +22 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +41203 -38364
- package/plugins/kxm/dist/core.js +57 -0
- package/plugins/kxm/dist/extension.js +40 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +1582 -81
- package/plugins/kxm/dist/server.js +129 -4
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +221 -41
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +30 -0
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +238 -3791
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/external-effects.ts +1 -1
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +8 -8
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/role.ts +335 -0
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +38 -1
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +16 -0
- package/plugins/kxm/src/vnext-harness.ts +92 -22
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +39 -7
- package/plugins/kxm/src/vnext-oneshot-process.ts +46 -8
- package/plugins/kxm/src/vnext-oneshot-producer.ts +22 -1
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +5 -3
- package/plugins/kxm/src/workflow-tui.ts +1 -1
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +145 -3
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisory agent control, in-process real-time steering, and work-splitting.
|
|
3
|
+
*
|
|
4
|
+
* Implements:
|
|
5
|
+
* - Dual-tool control plane: `agent` (spawn) and `agent_control` (manage, poll, steer, stop).
|
|
6
|
+
* - Strict capability narrowing via `allowed_tools[]` allowlists.
|
|
7
|
+
* - Mid-flight steering instruction injection.
|
|
8
|
+
* - Subagent state lifecycle management (working, blocked, idle, done, error).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { assertCommandSeatbelt } from "./safety-integrity.ts";
|
|
13
|
+
|
|
14
|
+
export type SubagentType = "general" | "Explore" | "Plan" | "Reviewer" | "Auditor";
|
|
15
|
+
export type SubagentStatus = "pending" | "running" | "blocked" | "completed" | "stopped" | "failed";
|
|
16
|
+
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
|
|
17
|
+
|
|
18
|
+
export interface AgentSpawnParams {
|
|
19
|
+
prompt: string;
|
|
20
|
+
description: string;
|
|
21
|
+
type?: SubagentType | undefined;
|
|
22
|
+
model?: string | undefined;
|
|
23
|
+
thinking?: ThinkingLevel | undefined;
|
|
24
|
+
allowed_tools?: string[] | undefined;
|
|
25
|
+
turns?: number | undefined;
|
|
26
|
+
background?: boolean | undefined;
|
|
27
|
+
parent_tools?: readonly string[] | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AgentControlParams {
|
|
31
|
+
action: "info" | "result" | "steer" | "stop";
|
|
32
|
+
kind?: "types" | "models" | "active" | undefined;
|
|
33
|
+
agent_id?: string | undefined;
|
|
34
|
+
message?: string | undefined;
|
|
35
|
+
turns?: number | undefined;
|
|
36
|
+
verbose?: boolean | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SubagentRecord {
|
|
40
|
+
id: string;
|
|
41
|
+
description: string;
|
|
42
|
+
type: SubagentType;
|
|
43
|
+
model: string;
|
|
44
|
+
thinking: ThinkingLevel;
|
|
45
|
+
allowedTools: string[];
|
|
46
|
+
status: SubagentStatus;
|
|
47
|
+
turnsMax: number;
|
|
48
|
+
turnsCompleted: number;
|
|
49
|
+
prompt: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
steeringMessages: Array<{ text: string; injectedAt: string; processed: boolean }>;
|
|
53
|
+
output?: string | undefined;
|
|
54
|
+
error?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SubagentExecutionReceipt {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
action: string;
|
|
60
|
+
agent_id?: string | undefined;
|
|
61
|
+
status?: SubagentStatus | undefined;
|
|
62
|
+
activeAgents?: Array<{ id: string; description: string; type: SubagentType; status: SubagentStatus }> | undefined;
|
|
63
|
+
types?: readonly SubagentType[] | undefined;
|
|
64
|
+
result?: string | undefined;
|
|
65
|
+
steered?: boolean | undefined;
|
|
66
|
+
stopped?: boolean | undefined;
|
|
67
|
+
error?: string | undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const SUBAGENT_TYPES: readonly SubagentType[] = Object.freeze([
|
|
71
|
+
"general",
|
|
72
|
+
"Explore",
|
|
73
|
+
"Plan",
|
|
74
|
+
"Reviewer",
|
|
75
|
+
"Auditor",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
export const DEFAULT_SUBAGENT_MODELS: Readonly<Record<SubagentType, string>> = Object.freeze({
|
|
79
|
+
general: "grok/grok-4.6",
|
|
80
|
+
Explore: "openrouter/qwen/qwen3-coder-plus",
|
|
81
|
+
Plan: "claude/fable",
|
|
82
|
+
Reviewer: "openai/gpt-5.6-sol",
|
|
83
|
+
Auditor: "claude/fable",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates and intersects child allowed tools against the parent tool surface.
|
|
88
|
+
* Enforces the safety invariant that a child can only narrow, never expand authority.
|
|
89
|
+
*/
|
|
90
|
+
export function narrowSubagentTools(
|
|
91
|
+
requestedTools: readonly string[] | undefined,
|
|
92
|
+
parentTools?: readonly string[] | undefined,
|
|
93
|
+
): string[] {
|
|
94
|
+
if (!requestedTools || requestedTools.length === 0) {
|
|
95
|
+
return parentTools ? [...parentTools] : ["read", "grep", "find"];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!parentTools || parentTools.length === 0) {
|
|
99
|
+
return [...requestedTools];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parentSet = new Set(parentTools);
|
|
103
|
+
const narrowed: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const tool of requestedTools) {
|
|
106
|
+
if (parentSet.has(tool)) {
|
|
107
|
+
narrowed.push(tool);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (narrowed.length === 0 && requestedTools.length > 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Cannot spawn subagent: requested tools [${requestedTools.join(", ")}] expand beyond parent capabilities.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return narrowed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* In-Process Subagent Manager.
|
|
122
|
+
*/
|
|
123
|
+
export class SubagentManager {
|
|
124
|
+
private readonly subagents = new Map<string, SubagentRecord>();
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spawns a new managed subagent with strict capability boundaries.
|
|
128
|
+
*/
|
|
129
|
+
spawn(params: AgentSpawnParams): SubagentRecord {
|
|
130
|
+
if (!params.prompt || !params.prompt.trim()) {
|
|
131
|
+
throw new Error('Subagent "prompt" is required.');
|
|
132
|
+
}
|
|
133
|
+
if (!params.description || !params.description.trim()) {
|
|
134
|
+
throw new Error('Subagent "description" is required.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const type = params.type ?? "general";
|
|
138
|
+
const model = params.model ?? DEFAULT_SUBAGENT_MODELS[type] ?? "grok/grok-4.6";
|
|
139
|
+
const thinking = params.thinking ?? "low";
|
|
140
|
+
const allowedTools = narrowSubagentTools(params.allowed_tools, params.parent_tools);
|
|
141
|
+
const id = `ag_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
142
|
+
const now = new Date().toISOString();
|
|
143
|
+
|
|
144
|
+
const record: SubagentRecord = {
|
|
145
|
+
id,
|
|
146
|
+
description: params.description.trim(),
|
|
147
|
+
type,
|
|
148
|
+
model,
|
|
149
|
+
thinking,
|
|
150
|
+
allowedTools,
|
|
151
|
+
status: params.background === false ? "completed" : "running",
|
|
152
|
+
turnsMax: params.turns ?? 10,
|
|
153
|
+
turnsCompleted: 0,
|
|
154
|
+
prompt: params.prompt.trim(),
|
|
155
|
+
createdAt: now,
|
|
156
|
+
updatedAt: now,
|
|
157
|
+
steeringMessages: [],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
this.subagents.set(id, record);
|
|
161
|
+
return record;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Dispatches an action from the `agent_control` tool.
|
|
166
|
+
*/
|
|
167
|
+
control(params: AgentControlParams): SubagentExecutionReceipt {
|
|
168
|
+
const action = params.action;
|
|
169
|
+
|
|
170
|
+
// 1. Action: "info"
|
|
171
|
+
if (action === "info") {
|
|
172
|
+
const kind = params.kind ?? "active";
|
|
173
|
+
if (kind === "types") {
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
action: "info",
|
|
177
|
+
types: SUBAGENT_TYPES,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (kind === "models") {
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
action: "info",
|
|
184
|
+
result: JSON.stringify(DEFAULT_SUBAGENT_MODELS),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const active = Array.from(this.subagents.values()).map((ag) => ({
|
|
188
|
+
id: ag.id,
|
|
189
|
+
description: ag.description,
|
|
190
|
+
type: ag.type,
|
|
191
|
+
status: ag.status,
|
|
192
|
+
}));
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
action: "info",
|
|
196
|
+
activeAgents: active,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!params.agent_id) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
action,
|
|
204
|
+
error: 'Parameter "agent_id" is required for result, steer, and stop actions.',
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const agent = this.subagents.get(params.agent_id);
|
|
209
|
+
if (!agent) {
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
action,
|
|
213
|
+
agent_id: params.agent_id,
|
|
214
|
+
error: `Subagent "${params.agent_id}" not found.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. Action: "result"
|
|
219
|
+
if (action === "result") {
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
action: "result",
|
|
223
|
+
agent_id: agent.id,
|
|
224
|
+
status: agent.status,
|
|
225
|
+
result: agent.output ?? `[Subagent is currently ${agent.status}]`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. Action: "steer"
|
|
230
|
+
if (action === "steer") {
|
|
231
|
+
if (!params.message || !params.message.trim()) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
action: "steer",
|
|
235
|
+
agent_id: agent.id,
|
|
236
|
+
error: 'Parameter "message" is required for steer action.',
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (agent.status === "completed" || agent.status === "stopped" || agent.status === "failed") {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
action: "steer",
|
|
244
|
+
agent_id: agent.id,
|
|
245
|
+
error: `Cannot steer subagent "${agent.id}" because it is already ${agent.status}.`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
agent.steeringMessages.push({
|
|
250
|
+
text: params.message.trim(),
|
|
251
|
+
injectedAt: new Date().toISOString(),
|
|
252
|
+
processed: false,
|
|
253
|
+
});
|
|
254
|
+
agent.updatedAt = new Date().toISOString();
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
action: "steer",
|
|
259
|
+
agent_id: agent.id,
|
|
260
|
+
status: agent.status,
|
|
261
|
+
steered: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 4. Action: "stop"
|
|
266
|
+
if (action === "stop") {
|
|
267
|
+
agent.status = "stopped";
|
|
268
|
+
agent.updatedAt = new Date().toISOString();
|
|
269
|
+
return {
|
|
270
|
+
ok: true,
|
|
271
|
+
action: "stop",
|
|
272
|
+
agent_id: agent.id,
|
|
273
|
+
status: "stopped",
|
|
274
|
+
stopped: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
action,
|
|
281
|
+
error: `Unknown control action "${action}".`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Retrieves an agent record by ID.
|
|
287
|
+
*/
|
|
288
|
+
get(agentId: string): SubagentRecord | undefined {
|
|
289
|
+
return this.subagents.get(agentId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Marks a subagent as completed with output.
|
|
294
|
+
*/
|
|
295
|
+
complete(agentId: string, output: string): void {
|
|
296
|
+
const agent = this.subagents.get(agentId);
|
|
297
|
+
if (agent) {
|
|
298
|
+
agent.status = "completed";
|
|
299
|
+
agent.output = output;
|
|
300
|
+
agent.updatedAt = new Date().toISOString();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Returns all active running subagents.
|
|
306
|
+
*/
|
|
307
|
+
listActive(): SubagentRecord[] {
|
|
308
|
+
return Array.from(this.subagents.values()).filter(
|
|
309
|
+
(ag) => ag.status === "running" || ag.status === "pending" || ag.status === "blocked",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
writeFileSync,
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
|
-
import { DatabaseSync } from "
|
|
18
|
+
import { DatabaseSync } from "./sqlite.ts";
|
|
19
19
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
20
20
|
import {
|
|
21
21
|
VnextConfigError,
|
|
@@ -1033,6 +1033,7 @@ function validateBundle(
|
|
|
1033
1033
|
environments: readonly VnextResource[],
|
|
1034
1034
|
options: VnextConfigOptions,
|
|
1035
1035
|
gateRegistry?: VnextResource,
|
|
1036
|
+
projectRoot?: string,
|
|
1036
1037
|
): VnextConfigIssue[] {
|
|
1037
1038
|
const issues: VnextConfigIssue[] = [];
|
|
1038
1039
|
const entries = valuesOf(project.value, "repositories").map((candidate) => objectValue(candidate)).filter((candidate): candidate is JsonObject => Boolean(candidate));
|
|
@@ -1105,6 +1106,42 @@ function validateBundle(
|
|
|
1105
1106
|
const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
|
|
1106
1107
|
if (!workflows.has(defaultWorkflow)) issues.push(issue("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
|
|
1107
1108
|
for (const workflow of workflows.values()) validateWorkflow(workflow, agents, models, repositoryIds, gates, issues);
|
|
1109
|
+
|
|
1110
|
+
if (projectRoot) {
|
|
1111
|
+
const writerRolePath = join(projectRoot, ".kxm", "roles", "writer.yaml");
|
|
1112
|
+
const implementerAgent = agents.get("implementer") ?? agents.get("writer");
|
|
1113
|
+
if (existsSync(writerRolePath) && implementerAgent) {
|
|
1114
|
+
try {
|
|
1115
|
+
const rawRole = parseRestrictedYaml(readFileSync(writerRolePath, "utf8"));
|
|
1116
|
+
const roleObj = objectValue(rawRole);
|
|
1117
|
+
const rosterEntries = valuesOf(roleObj ?? {}, "roster")
|
|
1118
|
+
.map((candidate) => objectValue(candidate))
|
|
1119
|
+
.filter((entry): entry is JsonObject => Boolean(entry));
|
|
1120
|
+
const enabledRosterModels = rosterEntries
|
|
1121
|
+
.filter((entry) => entry.enabled !== false)
|
|
1122
|
+
.map((entry) => stringValue(entry.model))
|
|
1123
|
+
.filter((m): m is string => Boolean(m));
|
|
1124
|
+
|
|
1125
|
+
const agentModelObj = objectValue(implementerAgent.value.model);
|
|
1126
|
+
const agentModelStr = stringValue(implementerAgent.value.model);
|
|
1127
|
+
const agentProvider = agentModelObj ? stringValue(agentModelObj.provider) : undefined;
|
|
1128
|
+
const agentModel = agentModelObj ? stringValue(agentModelObj.model) : agentModelStr;
|
|
1129
|
+
const canonicalAgentModel = agentProvider && agentModel ? `${agentProvider}/${agentModel}` : agentModel;
|
|
1130
|
+
|
|
1131
|
+
if (enabledRosterModels.length > 0 && canonicalAgentModel) {
|
|
1132
|
+
const matches = enabledRosterModels.some((rm) => rm === canonicalAgentModel || rm === agentModel || rm.endsWith(`/${agentModel}`));
|
|
1133
|
+
if (!matches) {
|
|
1134
|
+
issues.push(issue("semantic", "role_roster_conflicts_with_agent", ".kxm/roles/writer.yaml", `role roster in .kxm/roles/writer.yaml does not include agent model ${canonicalAgentModel} from ${implementerAgent.logicalPath}`));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
if (error instanceof VnextConfigError) {
|
|
1139
|
+
issues.push(...error.issues);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1108
1145
|
return sortIssues(issues);
|
|
1109
1146
|
}
|
|
1110
1147
|
|
|
@@ -1306,7 +1343,7 @@ export function loadVnextProject(projectRoot: string, options: VnextConfigOption
|
|
|
1306
1343
|
}
|
|
1307
1344
|
if (loadIssues.length > 0) throw new VnextConfigError(loadIssues);
|
|
1308
1345
|
|
|
1309
|
-
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry);
|
|
1346
|
+
const issues = validateBundle(project, repositories, agents, models, workflows, environments, options, gateRegistry, root);
|
|
1310
1347
|
if (issues.length > 0) throw new VnextConfigError(issues);
|
|
1311
1348
|
const resources = [project, ...repositories.values(), ...agents.values(), ...models.values(), ...workflows.values(), ...environments, ...(gateRegistry ? [gateRegistry] : [])]
|
|
1312
1349
|
.sort((left, right) => compareCodeUnits(left.logicalPath, right.logicalPath));
|
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import type { VnextGateObservationInput } from "./vnext-engine-gate-records.ts";
|
|
4
4
|
import type { VnextGateErrorClass, VnextGateStopCause } from "./vnext-runtime-store.ts";
|
|
5
5
|
import { runtimeError } from "./vnext-runtime-store.ts";
|
|
6
|
+
import { assertCommandSeatbelt } from "./safety-integrity.ts";
|
|
6
7
|
|
|
7
8
|
const MAX_DIRECT_TIMER_MS = 2_147_483_647;
|
|
8
9
|
export const COMMAND_TERM_GRACE_MS = 2000;
|
|
@@ -176,6 +177,7 @@ class CommandObserver implements VnextCommandObserver {
|
|
|
176
177
|
) {
|
|
177
178
|
throw runtimeError("run_events_illegal", "command", "command timeoutMs exceeds the direct timer bound");
|
|
178
179
|
}
|
|
180
|
+
assertCommandSeatbelt(this.definition.argv.join(" "));
|
|
179
181
|
this.startedAt = new Date().toISOString();
|
|
180
182
|
let child: ChildProcess;
|
|
181
183
|
try {
|
|
@@ -94,6 +94,21 @@ import {
|
|
|
94
94
|
parseRoutingRecordV2,
|
|
95
95
|
behavioralConfigHash,
|
|
96
96
|
} from "./routing.ts";
|
|
97
|
+
import {
|
|
98
|
+
assertCommandSeatbelt,
|
|
99
|
+
DESTRUCTIVE_COMMAND_PATTERNS,
|
|
100
|
+
isRtkBypassRequired,
|
|
101
|
+
assertPinnedSshHostKeyPolicy,
|
|
102
|
+
type RtkBypassContext,
|
|
103
|
+
} from "./safety-integrity.ts";
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
assertCommandSeatbelt,
|
|
107
|
+
DESTRUCTIVE_COMMAND_PATTERNS,
|
|
108
|
+
isRtkBypassRequired,
|
|
109
|
+
assertPinnedSshHostKeyPolicy,
|
|
110
|
+
type RtkBypassContext,
|
|
111
|
+
};
|
|
97
112
|
|
|
98
113
|
const trustedProducers = new WeakSet<object>();
|
|
99
114
|
const CAPABILITY_PREFIX = "kxm-attempt-capability\0";
|
|
@@ -557,6 +572,7 @@ async function runPreparedCommandGate(
|
|
|
557
572
|
definition: { readonly kind: "command"; readonly argv: readonly string[]; readonly timeoutMs: number; readonly cwd?: "control" },
|
|
558
573
|
token: string,
|
|
559
574
|
): Promise<VnextRunDriveResult> {
|
|
575
|
+
assertCommandSeatbelt(definition.argv.join(" "));
|
|
560
576
|
const storePath = context.eventStore.path;
|
|
561
577
|
const observer = createCommandObserver({
|
|
562
578
|
definition,
|
|
@@ -58,14 +58,22 @@ export interface HarnessCatalogEntry {
|
|
|
58
58
|
oneShot?: HarnessOneShotConfig | undefined;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
export type HarnessRunCommand = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult;
|
|
62
|
+
export type HarnessRunCommandAsync = (command: string, args: readonly string[], timeoutMs: number) => HarnessCommandResult | Promise<HarnessCommandResult>;
|
|
63
|
+
|
|
61
64
|
export interface HarnessProbeOptions {
|
|
62
65
|
env?: NodeJS.ProcessEnv | undefined;
|
|
63
|
-
runCommand?:
|
|
66
|
+
runCommand?: HarnessRunCommand | undefined;
|
|
64
67
|
timeoutMs?: number | undefined;
|
|
65
68
|
platform?: NodeJS.Platform | undefined;
|
|
66
69
|
existsSync?: ((path: string) => boolean) | undefined;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
export interface AsyncHarnessProbeOptions extends Omit<HarnessProbeOptions, "runCommand"> {
|
|
73
|
+
signal?: AbortSignal | undefined;
|
|
74
|
+
runCommand?: HarnessRunCommandAsync | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
69
77
|
export interface HarnessDispatchStatus {
|
|
70
78
|
status: "yes" | "no";
|
|
71
79
|
supported: boolean;
|
|
@@ -414,6 +422,8 @@ const READ_ONLY_ONESHOT_ARGS = Object.freeze({
|
|
|
414
422
|
claude: Object.freeze(["--tools", "Read,Glob,Grep", "--restricted", "--safe-mode", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--disable-slash-commands", "--no-session-persistence"]),
|
|
415
423
|
codex: Object.freeze(["--sandbox", "read-only", "--ignore-user-config", "-c", 'approval_policy="never"']),
|
|
416
424
|
grok: Object.freeze(["--sandbox", "read-only", "--permission-mode", "plan", "--tools", "Read,Glob,Grep", "--no-subagents", "--disable-web-search"]),
|
|
425
|
+
agy: Object.freeze(["--mode", "plan", "--sandbox", "--disable-slash-commands"]),
|
|
426
|
+
kimi: Object.freeze(["--plan"]),
|
|
417
427
|
});
|
|
418
428
|
|
|
419
429
|
export function oneShotReadOnlyArgs(harness: string): readonly string[] | undefined {
|
|
@@ -469,7 +479,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
469
479
|
authArgs: ["provider", "list"],
|
|
470
480
|
update: { self: ["upgrade"] },
|
|
471
481
|
oneShot: {
|
|
472
|
-
argv: ["--output-format", "stream-json", "-p"],
|
|
482
|
+
argv: [...oneShotReadOnlyArgs("kimi")!, "--output-format", "stream-json", "-p"],
|
|
473
483
|
promptVia: "arg",
|
|
474
484
|
outputFormat: "stream-json",
|
|
475
485
|
usageParser: parseKimiOneShotUsage,
|
|
@@ -532,7 +542,7 @@ export const BUILTIN_HARNESSES: readonly HarnessCatalogEntry[] = Object.freeze([
|
|
|
532
542
|
authArgs: ["models"],
|
|
533
543
|
update: { self: ["update"] },
|
|
534
544
|
oneShot: {
|
|
535
|
-
argv: ["--output-format", "json", "-p"],
|
|
545
|
+
argv: [...oneShotReadOnlyArgs("agy")!, "--output-format", "json", "-p"],
|
|
536
546
|
promptVia: "arg",
|
|
537
547
|
outputFormat: "json",
|
|
538
548
|
usageParser: parseAgyOneShotUsage,
|
|
@@ -596,7 +606,7 @@ export function findWinNpmInnerExe(
|
|
|
596
606
|
return undefined;
|
|
597
607
|
}
|
|
598
608
|
|
|
599
|
-
function defaultRunner(env: NodeJS.ProcessEnv):
|
|
609
|
+
function defaultRunner(env: NodeJS.ProcessEnv): HarnessRunCommand {
|
|
600
610
|
return (command, args, timeoutMs) => {
|
|
601
611
|
try {
|
|
602
612
|
const result = spawnSync(command, [...args], {
|
|
@@ -622,6 +632,37 @@ function defaultRunner(env: NodeJS.ProcessEnv): (command: string, args: readonly
|
|
|
622
632
|
};
|
|
623
633
|
}
|
|
624
634
|
|
|
635
|
+
function commandResultFromSpawn(result: Awaited<ReturnType<typeof defaultSpawn>>): HarnessCommandResult {
|
|
636
|
+
const errno = result.error && "code" in result.error
|
|
637
|
+
? (result.error as NodeJS.ErrnoException).code
|
|
638
|
+
: undefined;
|
|
639
|
+
const error = (typeof errno === "string" && errno ? errno : undefined)
|
|
640
|
+
?? result.error?.message
|
|
641
|
+
?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
|
|
642
|
+
return {
|
|
643
|
+
ok: result.code === 0 && !error,
|
|
644
|
+
code: result.code,
|
|
645
|
+
stdout: result.stdout,
|
|
646
|
+
stderr: result.stderr,
|
|
647
|
+
...(error ? { error } : {}),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function defaultAsyncRunner(env: NodeJS.ProcessEnv, signal?: AbortSignal): HarnessRunCommandAsync {
|
|
652
|
+
return async (command, args, timeoutMs) => {
|
|
653
|
+
try {
|
|
654
|
+
return commandResultFromSpawn(await defaultSpawn(command, args, {
|
|
655
|
+
env,
|
|
656
|
+
timeoutMs,
|
|
657
|
+
signal,
|
|
658
|
+
shell: harnessSpawnUsesShell(command),
|
|
659
|
+
}));
|
|
660
|
+
} catch (error) {
|
|
661
|
+
return { ok: false, code: null, stdout: "", stderr: "", error: error instanceof Error ? error.message : "spawn_failed" };
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
625
666
|
function firstLine(text: string): string | undefined {
|
|
626
667
|
const line = text.split(/\r?\n/).map((candidate) => candidate.trim()).find(Boolean);
|
|
627
668
|
return line && line.length <= 200 ? line : undefined;
|
|
@@ -856,6 +897,18 @@ export function probeHarnesses(options: HarnessProbeOptions = {}): HarnessInvent
|
|
|
856
897
|
};
|
|
857
898
|
}
|
|
858
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Inventory probe that never uses spawnSync. Product CLI/hub/extension paths
|
|
902
|
+
* must await this so capability/auth checks cannot block the event loop.
|
|
903
|
+
*/
|
|
904
|
+
export async function probeHarnessesAsync(options: AsyncHarnessProbeOptions = {}): Promise<HarnessInventory> {
|
|
905
|
+
return replayHarnessProbe(
|
|
906
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
907
|
+
(runCommand) => probeHarnesses({ ...options, runCommand }),
|
|
908
|
+
256,
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
859
912
|
export function eligibleHarnesses(inventory: HarnessInventory): readonly string[] {
|
|
860
913
|
const eligible = inventory.harnesses
|
|
861
914
|
.filter((entry) => entry !== undefined && entry.detected && entry.authenticated === true)
|
|
@@ -1116,7 +1169,7 @@ export function probeHarnessAssignment(options: HarnessAssignmentProbeOptions):
|
|
|
1116
1169
|
|
|
1117
1170
|
export interface AsyncHarnessAssignmentProbeOptions extends Omit<HarnessAssignmentProbeOptions, "runCommand"> {
|
|
1118
1171
|
signal?: AbortSignal | undefined;
|
|
1119
|
-
runCommand?:
|
|
1172
|
+
runCommand?: HarnessRunCommandAsync | undefined;
|
|
1120
1173
|
}
|
|
1121
1174
|
|
|
1122
1175
|
class PendingHarnessCommand {
|
|
@@ -1132,37 +1185,43 @@ class PendingHarnessCommand {
|
|
|
1132
1185
|
}
|
|
1133
1186
|
}
|
|
1134
1187
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
*/
|
|
1141
|
-
export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
|
|
1188
|
+
async function replayHarnessProbe<T>(
|
|
1189
|
+
run: HarnessRunCommandAsync,
|
|
1190
|
+
execute: (runCommand: HarnessRunCommand) => T,
|
|
1191
|
+
limit: number,
|
|
1192
|
+
): Promise<T> {
|
|
1142
1193
|
const observed = new Map<string, HarnessCommandResult>();
|
|
1143
|
-
|
|
1144
|
-
const result = await defaultSpawn(command, args, { env: options.env, timeoutMs, signal: options.signal });
|
|
1145
|
-
const error = result.error?.message ?? (result.observedChildExit !== true ? "auth_probe_exit_unobserved" : undefined);
|
|
1146
|
-
return { ok: result.code === 0 && !error, code: result.code, stdout: result.stdout, stderr: result.stderr,
|
|
1147
|
-
...(error ? { error } : {}) };
|
|
1148
|
-
});
|
|
1149
|
-
for (let commands = 0; commands <= 32; commands++) {
|
|
1194
|
+
for (let commands = 0; commands <= limit; commands++) {
|
|
1150
1195
|
try {
|
|
1151
|
-
return
|
|
1196
|
+
return execute((command, args, timeoutMs) => {
|
|
1152
1197
|
const key = JSON.stringify([command, args, timeoutMs]);
|
|
1153
1198
|
const result = observed.get(key);
|
|
1154
1199
|
if (result) return result;
|
|
1155
1200
|
throw new PendingHarnessCommand(command, args, timeoutMs, key);
|
|
1156
|
-
}
|
|
1201
|
+
});
|
|
1157
1202
|
} catch (pending) {
|
|
1158
1203
|
if (!(pending instanceof PendingHarnessCommand)) throw pending;
|
|
1159
|
-
if (commands ===
|
|
1204
|
+
if (commands === limit) throw new Error("auth_probe_command_limit");
|
|
1160
1205
|
observed.set(pending.key, await run(pending.command, pending.args, pending.timeoutMs));
|
|
1161
1206
|
}
|
|
1162
1207
|
}
|
|
1163
1208
|
throw new Error("auth_probe_command_limit");
|
|
1164
1209
|
}
|
|
1165
1210
|
|
|
1211
|
+
/**
|
|
1212
|
+
* Execute the existing probe policy without blocking the Runtime event loop.
|
|
1213
|
+
* Replay pure policy decisions from per-call command observations, yielding for
|
|
1214
|
+
* every missing observation. This reuses the exact auth/model/brake parsers,
|
|
1215
|
+
* deduplicates repeated detection, and never caches auth across assignments.
|
|
1216
|
+
*/
|
|
1217
|
+
export async function probeHarnessAssignmentAsync(options: AsyncHarnessAssignmentProbeOptions): Promise<HarnessStatus> {
|
|
1218
|
+
return replayHarnessProbe(
|
|
1219
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
1220
|
+
(runCommand) => probeHarnessAssignment({ ...options, runCommand }),
|
|
1221
|
+
32,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1166
1225
|
export function probeHarnessesForModel(
|
|
1167
1226
|
modelSpec: HarnessModelSpec,
|
|
1168
1227
|
options: HarnessProbeOptions = {},
|
|
@@ -1187,6 +1246,17 @@ export function probeHarnessesForModel(
|
|
|
1187
1246
|
};
|
|
1188
1247
|
}
|
|
1189
1248
|
|
|
1249
|
+
export async function probeHarnessesForModelAsync(
|
|
1250
|
+
modelSpec: HarnessModelSpec,
|
|
1251
|
+
options: AsyncHarnessProbeOptions = {},
|
|
1252
|
+
): Promise<HarnessInventory> {
|
|
1253
|
+
return replayHarnessProbe(
|
|
1254
|
+
options.runCommand ?? defaultAsyncRunner(options.env ?? process.env, options.signal),
|
|
1255
|
+
(runCommand) => probeHarnessesForModel(modelSpec, { ...options, runCommand }),
|
|
1256
|
+
256,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1190
1260
|
function scopesFor(scope: HarnessUpdateScope, entry: HarnessCatalogEntry): Exclude<HarnessUpdateScope, "all">[] {
|
|
1191
1261
|
if (scope === "self") return ["self"];
|
|
1192
1262
|
if (scope === "extensions") return entry.update.extensions ? ["extensions"] : [];
|