@kontextmind/kxm 0.7.98 → 0.7.100
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/CHANGELOG.md +19 -0
- package/docs/contributing/test-matrix.md +2 -0
- package/docs/guides/peer-messaging.md +2 -2
- package/docs/reference/cli-reference.md +10 -7
- package/docs/reference/config-reference.md +4 -2
- package/docs/reference/http-api.md +1 -0
- package/docs/reference/tools.md +3 -3
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/claude-hook.js +4 -1
- package/plugins/kxm/dist/cli.js +115 -57
- package/plugins/kxm/dist/client.js +9 -0
- package/plugins/kxm/dist/core.js +4 -1
- package/plugins/kxm/dist/extension.js +13 -1
- package/plugins/kxm/dist/mcp-server.js +14 -2
- package/plugins/kxm/dist/runtime-supervisor.js +19 -4
- package/plugins/kxm/dist/runtime.js +19 -4
- package/plugins/kxm/dist/server.js +12 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-peer/SKILL.md +5 -3
- package/plugins/kxm/src/cli/workflows.ts +34 -9
- package/plugins/kxm/src/cli.ts +3 -1
- package/plugins/kxm/src/client.ts +10 -0
- package/plugins/kxm/src/commands.ts +10 -1
- package/plugins/kxm/src/extension.ts +1 -0
- package/plugins/kxm/src/hub.ts +12 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/project-config.ts +45 -3
|
@@ -16611,10 +16611,13 @@ function readResource(registry, root, file, logicalPath, kind, id, containmentRo
|
|
|
16611
16611
|
parent = dirname2(parent);
|
|
16612
16612
|
}
|
|
16613
16613
|
if (!stat.isFile()) fail2("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
16614
|
-
|
|
16614
|
+
return { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
|
|
16615
|
+
}
|
|
16616
|
+
function resourceValue(registry, input, label, kind) {
|
|
16617
|
+
const value = parseRestrictedYaml2(input, label);
|
|
16615
16618
|
const issues = registry.validate(kind, value, label);
|
|
16616
16619
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
16617
|
-
return
|
|
16620
|
+
return value;
|
|
16618
16621
|
}
|
|
16619
16622
|
function readTemplateProvenance(registry, root) {
|
|
16620
16623
|
const file = join2(root, ".kxm", "template-provenance.yaml");
|
|
@@ -16663,7 +16666,7 @@ function readTemplateProvenance(registry, root) {
|
|
|
16663
16666
|
}
|
|
16664
16667
|
return value;
|
|
16665
16668
|
}
|
|
16666
|
-
function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
16669
|
+
function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
|
|
16667
16670
|
const resources = /* @__PURE__ */ new Map();
|
|
16668
16671
|
if (!existsSync3(directory)) return resources;
|
|
16669
16672
|
const stat = lstatSync(directory);
|
|
@@ -16684,6 +16687,7 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
|
16684
16687
|
}
|
|
16685
16688
|
const id = basename(entry.name, ".yaml");
|
|
16686
16689
|
if (kind === "model" && id === "inventory") continue;
|
|
16690
|
+
if (id === replacedId) continue;
|
|
16687
16691
|
if (!resourceIdentifier(id)) {
|
|
16688
16692
|
issues.push(issue2("path", "resource_id_invalid", displayPath(root, join2(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
16689
16693
|
continue;
|
|
@@ -17284,6 +17288,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17284
17288
|
}
|
|
17285
17289
|
}
|
|
17286
17290
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17291
|
+
return loadProjectBundle(projectRoot, options);
|
|
17292
|
+
}
|
|
17293
|
+
function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
17287
17294
|
assertNoRegisteredGates(options);
|
|
17288
17295
|
const root = resolve(projectRoot);
|
|
17289
17296
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17317,7 +17324,15 @@ function loadKxmProject(projectRoot, options = {}) {
|
|
|
17317
17324
|
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
17318
17325
|
const agents = listNamedResources(registry, root, join2(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
17319
17326
|
const models = listNamedResources(registry, root, join2(root, ".kxm", "models"), ".kxm/models", "model");
|
|
17320
|
-
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
17327
|
+
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
17328
|
+
if (workflowCandidate) {
|
|
17329
|
+
const { id, document } = workflowCandidate;
|
|
17330
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17331
|
+
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17332
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17333
|
+
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17334
|
+
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17335
|
+
}
|
|
17321
17336
|
const gatePath = join2(root, ".kxm", "gates.yaml");
|
|
17322
17337
|
const gateRegistry = existsSync3(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : void 0;
|
|
17323
17338
|
const environments = [];
|
|
@@ -16781,10 +16781,13 @@ function readResource(registry, root, file, logicalPath, kind, id, containmentRo
|
|
|
16781
16781
|
parent = dirname2(parent);
|
|
16782
16782
|
}
|
|
16783
16783
|
if (!stat.isFile()) fail2("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
16784
|
-
|
|
16784
|
+
return { kind, ...id === void 0 ? {} : { id }, file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
|
|
16785
|
+
}
|
|
16786
|
+
function resourceValue(registry, input, label, kind) {
|
|
16787
|
+
const value = parseRestrictedYaml2(input, label);
|
|
16785
16788
|
const issues = registry.validate(kind, value, label);
|
|
16786
16789
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
16787
|
-
return
|
|
16790
|
+
return value;
|
|
16788
16791
|
}
|
|
16789
16792
|
function readTemplateProvenance(registry, root) {
|
|
16790
16793
|
const file = join2(root, ".kxm", "template-provenance.yaml");
|
|
@@ -16833,7 +16836,7 @@ function readTemplateProvenance(registry, root) {
|
|
|
16833
16836
|
}
|
|
16834
16837
|
return value;
|
|
16835
16838
|
}
|
|
16836
|
-
function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
16839
|
+
function listNamedResources(registry, root, directory, logicalDirectory, kind, replacedId) {
|
|
16837
16840
|
const resources = /* @__PURE__ */ new Map();
|
|
16838
16841
|
if (!existsSync3(directory)) return resources;
|
|
16839
16842
|
const stat = lstatSync(directory);
|
|
@@ -16854,6 +16857,7 @@ function listNamedResources(registry, root, directory, logicalDirectory, kind) {
|
|
|
16854
16857
|
}
|
|
16855
16858
|
const id = basename(entry.name, ".yaml");
|
|
16856
16859
|
if (kind === "model" && id === "inventory") continue;
|
|
16860
|
+
if (id === replacedId) continue;
|
|
16857
16861
|
if (!resourceIdentifier(id)) {
|
|
16858
16862
|
issues.push(issue2("path", "resource_id_invalid", displayPath(root, join2(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
16859
16863
|
continue;
|
|
@@ -17454,6 +17458,9 @@ function assertNoRegisteredGates(options) {
|
|
|
17454
17458
|
}
|
|
17455
17459
|
}
|
|
17456
17460
|
function loadKxmProject(projectRoot, options = {}) {
|
|
17461
|
+
return loadProjectBundle(projectRoot, options);
|
|
17462
|
+
}
|
|
17463
|
+
function loadProjectBundle(projectRoot, options, workflowCandidate) {
|
|
17457
17464
|
assertNoRegisteredGates(options);
|
|
17458
17465
|
const root = resolve(projectRoot);
|
|
17459
17466
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -17487,7 +17494,15 @@ function loadKxmProject(projectRoot, options = {}) {
|
|
|
17487
17494
|
if (earlyIssues.length > 0) throw new KxmConfigError(earlyIssues);
|
|
17488
17495
|
const agents = listNamedResources(registry, root, join2(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
17489
17496
|
const models = listNamedResources(registry, root, join2(root, ".kxm", "models"), ".kxm/models", "model");
|
|
17490
|
-
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
17497
|
+
const workflows = listNamedResources(registry, root, join2(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
17498
|
+
if (workflowCandidate) {
|
|
17499
|
+
const { id, document } = workflowCandidate;
|
|
17500
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
17501
|
+
if (!resourceIdentifier(id)) fail2("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
17502
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
17503
|
+
if (collision) fail2("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
17504
|
+
workflows.set(id, { kind: "workflow", id, file: join2(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
17505
|
+
}
|
|
17491
17506
|
const gatePath = join2(root, ".kxm", "gates.yaml");
|
|
17492
17507
|
const gateRegistry = existsSync3(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : void 0;
|
|
17493
17508
|
const environments = [];
|
|
@@ -17223,7 +17223,10 @@ var AGENT_COMMANDS = [
|
|
|
17223
17223
|
await reconcileInbox(client, context.inbox, context.notifiedInbox);
|
|
17224
17224
|
return { messages: [...context.inbox.values()] };
|
|
17225
17225
|
}
|
|
17226
|
-
return { messages:
|
|
17226
|
+
if (context?.hubInbox) return { messages: await client.listInbox() };
|
|
17227
|
+
throw new Error(
|
|
17228
|
+
"kxm_inbox is not available in this session: it activates each inbound request as a turn, and that turn's final response is the reply"
|
|
17229
|
+
);
|
|
17227
17230
|
}
|
|
17228
17231
|
},
|
|
17229
17232
|
{
|
|
@@ -22526,6 +22529,14 @@ data: ${JSON.stringify({ type: "ops", project, topic: "agents", at: nowIso() })}
|
|
|
22526
22529
|
json(response, 200, { agent: publicAgent(current, staleAfterMs) });
|
|
22527
22530
|
return;
|
|
22528
22531
|
}
|
|
22532
|
+
const inboxMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)\/inbox$/);
|
|
22533
|
+
if (method === "GET" && inboxMatch) {
|
|
22534
|
+
const current = requireAgent(request, decodeURIComponent(inboxMatch[1]));
|
|
22535
|
+
requireProjectAuth(request, current.project);
|
|
22536
|
+
expireMessages();
|
|
22537
|
+
json(response, 200, { messages: store.getPendingMessages(current.id) });
|
|
22538
|
+
return;
|
|
22539
|
+
}
|
|
22529
22540
|
const agentMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)$/);
|
|
22530
22541
|
if (method === "DELETE" && agentMatch) {
|
|
22531
22542
|
const current = requireAgent(request, decodeURIComponent(agentMatch[1]));
|
package/plugins/kxm/package.json
CHANGED
|
@@ -42,9 +42,11 @@ reading of its heartbeat lease: `online` holds the lease, `stale` has passed
|
|
|
42
42
|
peer the hub has retired. Offline peers are listed only with
|
|
43
43
|
`--include-offline`. The host label is a reading aid, never a permission.
|
|
44
44
|
|
|
45
|
-
`kxm peer inbox`
|
|
46
|
-
|
|
47
|
-
that
|
|
45
|
+
`kxm peer inbox` lists the requests addressed to the CLI agent's own name, so
|
|
46
|
+
run it with a stable `KXM_AGENT_NAME` and answer each with `kxm peer reply`
|
|
47
|
+
under that name. The default `cli-<pid>` is a new agent on every call, and its
|
|
48
|
+
inbox is always empty. In Claude Code, `kxm_inbox` lists the session's inbox; a
|
|
49
|
+
Pi session receives each request as a turn instead, and its `kxm_inbox` refuses.
|
|
48
50
|
|
|
49
51
|
## Examples
|
|
50
52
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { stringify } from "yaml";
|
|
4
5
|
import { openReadOnlyDatabase } from "../sqlite.ts";
|
|
5
6
|
import { buildRetrospective, writeRetrospective } from "../retrospective.ts";
|
|
6
7
|
import { redactSecrets } from "../redact.ts";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
addWorkflowDefinition,
|
|
11
12
|
removeWorkflowDefinition,
|
|
12
13
|
modifyWorkflowDefinition,
|
|
14
|
+
parseWorkflowFile,
|
|
13
15
|
scaffoldWorkflowDefinition,
|
|
14
16
|
WORKFLOW_TEMPLATES,
|
|
15
17
|
} from "../workflow-manager.ts";
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
type WorkflowJournalEntry,
|
|
22
24
|
type WorkflowRun,
|
|
23
25
|
} from "../workflow.ts";
|
|
24
|
-
import { discoverKxmProjectRoot } from "../project-config.ts";
|
|
26
|
+
import { discoverKxmProjectRoot, kxmWorkflowWriteIssues } from "../project-config.ts";
|
|
25
27
|
import { ensureKxmSupervisor, kxmRuntimeRequest } from "../runtime-supervisor.ts";
|
|
26
28
|
import { projectRuntimeOwnsRun } from "../runtime-store.ts";
|
|
27
29
|
import type { WorkerOutcome } from "../envelope.ts";
|
|
@@ -213,12 +215,12 @@ export async function cmdWorkflowAdd(
|
|
|
213
215
|
},
|
|
214
216
|
): Promise<number> {
|
|
215
217
|
const scope = options.scope ?? "local";
|
|
218
|
+
const refuse = (error: string, text: string): number => {
|
|
219
|
+
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
220
|
+
return 2;
|
|
221
|
+
};
|
|
216
222
|
let content: Record<string, unknown> | string | undefined;
|
|
217
223
|
if (options.template !== undefined) {
|
|
218
|
-
const refuse = (error: string, text: string): number => {
|
|
219
|
-
print(runtime.io, runtime.json, { ok: false, command: "workflow add", error }, `workflow add failed: ${text}`);
|
|
220
|
-
return 2;
|
|
221
|
-
};
|
|
222
224
|
if (options.file !== undefined || options.pick !== undefined) {
|
|
223
225
|
return refuse("workflow_add_conflict", "--template cannot be combined with --file or --pick");
|
|
224
226
|
}
|
|
@@ -238,8 +240,10 @@ export async function cmdWorkflowAdd(
|
|
|
238
240
|
if (scope === "local") {
|
|
239
241
|
const globalDefs = listWorkflowDefinitions({ scope: "global", userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
240
242
|
for (const gd of globalDefs) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
// Read the listed file itself: it may be `.yml`, which a lookup by `<id>.yaml` misses.
|
|
244
|
+
const definition = parseWorkflowFile(gd.filePath);
|
|
245
|
+
if (definition && !candidates.some((c) => c.id === gd.id)) {
|
|
246
|
+
candidates.push({ id: gd.id, description: gd.description, label: "global", payload: definition });
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
}
|
|
@@ -268,9 +272,30 @@ export async function cmdWorkflowAdd(
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
try {
|
|
271
|
-
const
|
|
275
|
+
const document = typeof content === "string" ? content : stringify(content);
|
|
276
|
+
// A local workflow is read by the project loader, which refuses the whole
|
|
277
|
+
// project over one bad file, so it is checked by that loader before it lands.
|
|
278
|
+
let repoRoot = runtime.cwd;
|
|
279
|
+
if (scope === "local") {
|
|
280
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
281
|
+
if (!projectRoot) {
|
|
282
|
+
return refuse("project_not_found", "local workflows belong to a KXM project; run kxm init at the repository root, or pass --scope global");
|
|
283
|
+
}
|
|
284
|
+
const issues = kxmWorkflowWriteIssues(projectRoot, workflowId!, document);
|
|
285
|
+
if (issues.length > 0) {
|
|
286
|
+
print(
|
|
287
|
+
runtime.io,
|
|
288
|
+
runtime.json,
|
|
289
|
+
{ ok: false, command: "workflow add", error: "workflow_invalid", issues },
|
|
290
|
+
`workflow add failed: with this workflow the project would not load, so nothing was written\n${issues.map((entry) => ` ${entry.file}: ${entry.code}: ${entry.message}`).join("\n")}`,
|
|
291
|
+
);
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
repoRoot = projectRoot;
|
|
295
|
+
}
|
|
296
|
+
const res = addWorkflowDefinition(workflowId!, document, {
|
|
272
297
|
scope,
|
|
273
|
-
repoRoot
|
|
298
|
+
repoRoot,
|
|
274
299
|
userConfigDir: runtime.env.KXM_USER_CONFIG_DIR,
|
|
275
300
|
overwrite: options.overwrite,
|
|
276
301
|
dryRun: runtime.dryRun,
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -349,7 +349,9 @@ async function dispatchAgentCliCommand(
|
|
|
349
349
|
let client: HubClient | undefined;
|
|
350
350
|
try {
|
|
351
351
|
client = await ensureCliClient(runtime);
|
|
352
|
-
|
|
352
|
+
// A one-shot call keeps no inbox, so `peer inbox` reads this agent's open requests from
|
|
353
|
+
// the hub; with a stable KXM_AGENT_NAME those include ones queued while it was offline.
|
|
354
|
+
const output = await cmd.execute(client, args, { hubInbox: true });
|
|
353
355
|
const payload = (output && typeof output === "object" ? output : { result: output }) as object;
|
|
354
356
|
print(runtime.io, runtime.json, payload, JSON.stringify(output, null, 2));
|
|
355
357
|
return 0;
|
|
@@ -325,6 +325,16 @@ export class HubClient {
|
|
|
325
325
|
}));
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/** This agent's open inbound requests (queued or delivered), oldest first. A read: nothing
|
|
329
|
+
* is acknowledged. */
|
|
330
|
+
async listInbox(): Promise<MessageRecord[]> {
|
|
331
|
+
if (!this.agent) throw new Error("hub client is not registered");
|
|
332
|
+
const result = await this.request<{ messages: MessageRecord[] }>(
|
|
333
|
+
`/v1/agents/${encodeURIComponent(this.agent.id)}/inbox`,
|
|
334
|
+
);
|
|
335
|
+
return result.messages;
|
|
336
|
+
}
|
|
337
|
+
|
|
328
338
|
async getMessage(messageId: string): Promise<MessageRecord> {
|
|
329
339
|
const result = await this.request<{ message: MessageRecord }>(`/v1/messages/${encodeURIComponent(messageId)}`);
|
|
330
340
|
return result.message;
|
|
@@ -72,8 +72,12 @@ export interface SessionTokenPayload {
|
|
|
72
72
|
|
|
73
73
|
export interface CommandExecutionContext {
|
|
74
74
|
signal?: AbortSignal | undefined;
|
|
75
|
+
/** An event-fed inbox the caller keeps (the MCP server); `kxm_inbox` reconciles it. */
|
|
75
76
|
inbox?: Map<string, MessageRecord> | undefined;
|
|
76
77
|
notifiedInbox?: Set<string> | undefined;
|
|
78
|
+
/** The caller keeps no inbox and reads its open requests from the hub (a one-shot CLI
|
|
79
|
+
* call). A caller with neither this nor `inbox` activates inbound requests itself (Pi). */
|
|
80
|
+
hubInbox?: boolean | undefined;
|
|
77
81
|
/** Inbound requests this session is handling: the Pi extension's active request, the MCP
|
|
78
82
|
* server's open inbox. A request sent meanwhile is one more hop along their chain. */
|
|
79
83
|
handling?: readonly Pick<MessageRecord, "hops" | "maxHops">[] | undefined;
|
|
@@ -452,7 +456,12 @@ export const AGENT_COMMANDS: readonly AgentCommand[] = [
|
|
|
452
456
|
await reconcileInbox(client, context.inbox, context.notifiedInbox);
|
|
453
457
|
return { messages: [...context.inbox.values()] };
|
|
454
458
|
}
|
|
455
|
-
return { messages:
|
|
459
|
+
if (context?.hubInbox) return { messages: await client.listInbox() };
|
|
460
|
+
// Listing would offer requests this session's own activation queue is about to hand
|
|
461
|
+
// it as turns, and an empty list would hide them, so it refuses.
|
|
462
|
+
throw new Error(
|
|
463
|
+
"kxm_inbox is not available in this session: it activates each inbound request as a turn, and that turn's final response is the reply",
|
|
464
|
+
);
|
|
456
465
|
},
|
|
457
466
|
},
|
|
458
467
|
{
|
|
@@ -898,6 +898,7 @@ export default function piMeshExtension(pi: ExtensionAPI): void | Promise<void>
|
|
|
898
898
|
throw new Error(`tool_policy_denied: ${policy.detail ?? policy.error}`);
|
|
899
899
|
}
|
|
900
900
|
// A request sent while handling the active inbound request continues its hop chain.
|
|
901
|
+
// No inbox and no hubInbox: `pending` owns activation, so kxm_inbox refuses here.
|
|
901
902
|
const handling = activeInbound ? [activeInbound] : [];
|
|
902
903
|
return result(
|
|
903
904
|
await workflowCall(() =>
|
package/plugins/kxm/src/hub.ts
CHANGED
|
@@ -2520,6 +2520,18 @@ export function createMeshHub(options: MeshHubOptions = {}): MeshHub {
|
|
|
2520
2520
|
return;
|
|
2521
2521
|
}
|
|
2522
2522
|
|
|
2523
|
+
// An agent's open inbound requests, queued or delivered, in delivery order. A read
|
|
2524
|
+
// for a caller that holds no event-fed inbox of its own (a one-shot CLI call): it
|
|
2525
|
+
// acknowledges nothing and moves no cursor, so push delivery is unchanged.
|
|
2526
|
+
const inboxMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)\/inbox$/);
|
|
2527
|
+
if (method === "GET" && inboxMatch) {
|
|
2528
|
+
const current = requireAgent(request, decodeURIComponent(inboxMatch[1]!));
|
|
2529
|
+
requireProjectAuth(request, current.project);
|
|
2530
|
+
expireMessages();
|
|
2531
|
+
json(response, 200, { messages: store.getPendingMessages(current.id) });
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2523
2535
|
const agentMatch = url.pathname.match(/^\/v1\/agents\/([^/]+)$/);
|
|
2524
2536
|
if (method === "DELETE" && agentMatch) {
|
|
2525
2537
|
const current = requireAgent(request, decodeURIComponent(agentMatch[1]!));
|
|
@@ -11,7 +11,7 @@ import { deliverInboxNotification } from "./inbox.ts";
|
|
|
11
11
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
12
12
|
import { sessionTokenFixHint } from "./session-token-hint.ts";
|
|
13
13
|
|
|
14
|
-
const VERSION = "0.7.
|
|
14
|
+
const VERSION = "0.7.100";
|
|
15
15
|
const CONFIGURE_PLUGIN = "/plugin configure kxm@kxm";
|
|
16
16
|
const inbox = new Map<string, MessageRecord>();
|
|
17
17
|
const notifiedInbox = new Set<string>();
|
|
@@ -453,10 +453,14 @@ function readResource(
|
|
|
453
453
|
parent = dirname(parent);
|
|
454
454
|
}
|
|
455
455
|
if (!stat.isFile()) fail("path", "resource_not_file", label, "configuration resource must be a regular file");
|
|
456
|
-
|
|
456
|
+
return { kind, ...(id === undefined ? {} : { id }), file, logicalPath, value: resourceValue(registry, readFileSync(file), label, kind) };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function resourceValue(registry: KxmSchemaRegistry, input: string | Uint8Array, label: string, kind: KxmResourceKind): JsonObject {
|
|
460
|
+
const value = parseRestrictedYaml(input, label);
|
|
457
461
|
const issues = registry.validate(kind, value, label);
|
|
458
462
|
if (issues.length > 0) throw new KxmConfigError(issues);
|
|
459
|
-
return
|
|
463
|
+
return value;
|
|
460
464
|
}
|
|
461
465
|
|
|
462
466
|
function readTemplateProvenance(registry: KxmSchemaRegistry, root: string): JsonObject | undefined {
|
|
@@ -513,6 +517,7 @@ function listNamedResources(
|
|
|
513
517
|
directory: string,
|
|
514
518
|
logicalDirectory: string,
|
|
515
519
|
kind: "agent" | "model" | "workflow",
|
|
520
|
+
replacedId?: string,
|
|
516
521
|
): Map<string, KxmResource> {
|
|
517
522
|
const resources = new Map<string, KxmResource>();
|
|
518
523
|
if (!existsSync(directory)) return resources;
|
|
@@ -534,6 +539,7 @@ function listNamedResources(
|
|
|
534
539
|
}
|
|
535
540
|
const id = basename(entry.name, ".yaml");
|
|
536
541
|
if (kind === "model" && id === "inventory") continue;
|
|
542
|
+
if (id === replacedId) continue;
|
|
537
543
|
if (!resourceIdentifier(id)) {
|
|
538
544
|
issues.push(issue("path", "resource_id_invalid", displayPath(root, join(directory, entry.name)), `filename-derived identity ${id} is invalid or platform-reserved`));
|
|
539
545
|
continue;
|
|
@@ -1284,6 +1290,34 @@ export function assertNoRegisteredGates(options: object): void {
|
|
|
1284
1290
|
|
|
1285
1291
|
/** Load and semantically validate one complete, path-derived KXM configuration bundle. */
|
|
1286
1292
|
export function loadKxmProject(projectRoot: string, options: KxmConfigOptions = {}): KxmProjectBundle {
|
|
1293
|
+
return loadProjectBundle(projectRoot, options);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* What `loadKxmProject` would refuse once `.kxm/workflows/<workflowId>.yaml` held
|
|
1298
|
+
* `document`, found without writing it: the same parser, schema and bundle rules,
|
|
1299
|
+
* with `document` standing in for any file of that name.
|
|
1300
|
+
*/
|
|
1301
|
+
export function kxmWorkflowWriteIssues(
|
|
1302
|
+
projectRoot: string,
|
|
1303
|
+
workflowId: string,
|
|
1304
|
+
document: string,
|
|
1305
|
+
options: KxmConfigOptions = {},
|
|
1306
|
+
): readonly KxmConfigIssue[] {
|
|
1307
|
+
try {
|
|
1308
|
+
loadProjectBundle(projectRoot, options, { id: workflowId, document });
|
|
1309
|
+
return [];
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (error instanceof KxmConfigError) return error.issues;
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function loadProjectBundle(
|
|
1317
|
+
projectRoot: string,
|
|
1318
|
+
options: KxmConfigOptions,
|
|
1319
|
+
workflowCandidate?: { id: string; document: string },
|
|
1320
|
+
): KxmProjectBundle {
|
|
1287
1321
|
assertNoRegisteredGates(options);
|
|
1288
1322
|
const root = resolve(projectRoot);
|
|
1289
1323
|
const legacyPresent = legacyConfigFilesAt(root);
|
|
@@ -1323,7 +1357,15 @@ export function loadKxmProject(projectRoot: string, options: KxmConfigOptions =
|
|
|
1323
1357
|
|
|
1324
1358
|
const agents = listNamedResources(registry, root, join(root, ".kxm", "agents"), ".kxm/agents", "agent");
|
|
1325
1359
|
const models = listNamedResources(registry, root, join(root, ".kxm", "models"), ".kxm/models", "model");
|
|
1326
|
-
const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow");
|
|
1360
|
+
const workflows = listNamedResources(registry, root, join(root, ".kxm", "workflows"), ".kxm/workflows", "workflow", workflowCandidate?.id);
|
|
1361
|
+
if (workflowCandidate) {
|
|
1362
|
+
const { id, document } = workflowCandidate;
|
|
1363
|
+
const logicalPath = `.kxm/workflows/${id}.yaml`;
|
|
1364
|
+
if (!resourceIdentifier(id)) fail("path", "resource_id_invalid", logicalPath, `filename-derived identity ${id} is invalid or platform-reserved`);
|
|
1365
|
+
const collision = [...workflows.keys()].find((candidate) => candidate.toLocaleLowerCase("en-US") === id.toLocaleLowerCase("en-US"));
|
|
1366
|
+
if (collision) fail("path", "resource_id_collision", logicalPath, `${id} case-folds to existing ${collision}`);
|
|
1367
|
+
workflows.set(id, { kind: "workflow", id, file: join(root, logicalPath), logicalPath, value: resourceValue(registry, document, logicalPath, "workflow") });
|
|
1368
|
+
}
|
|
1327
1369
|
const gatePath = join(root, ".kxm", "gates.yaml");
|
|
1328
1370
|
const gateRegistry = existsSync(gatePath) ? readResource(registry, root, gatePath, ".kxm/gates.yaml", "gate-registry") : undefined;
|
|
1329
1371
|
const environments: KxmResource[] = [];
|