@kontextmind/kxm 0.7.37 → 0.7.39
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/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/vision-gate.ts +94 -0
- package/scripts/roster-policy.mjs +15 -6
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
3
3
|
"name": "kxm",
|
|
4
4
|
"displayName": "KXM",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.39",
|
|
6
6
|
"description": "Headless multi-agent orchestration, durable workflows, and a live operator dashboard for Pi and Claude Code",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "KontextMind",
|
|
@@ -17121,7 +17121,7 @@ async function deliverInboxNotification(messageId, delivered, notify) {
|
|
|
17121
17121
|
}
|
|
17122
17122
|
|
|
17123
17123
|
// plugins/kxm/src/mcp-server.ts
|
|
17124
|
-
var VERSION = "0.7.
|
|
17124
|
+
var VERSION = "0.7.39";
|
|
17125
17125
|
var inbox = /* @__PURE__ */ new Map();
|
|
17126
17126
|
var notifiedInbox = /* @__PURE__ */ new Set();
|
|
17127
17127
|
var meshClient;
|
package/plugins/kxm/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { AGENT_COMMANDS_MAP, enforceToolPolicy, getMcpTools, reconcileInbox } fr
|
|
|
8
8
|
import { deliverInboxNotification } from "./inbox.ts";
|
|
9
9
|
import type { HubEvent, MessageRecord } from "./protocol.ts";
|
|
10
10
|
|
|
11
|
-
const VERSION = "0.7.
|
|
11
|
+
const VERSION = "0.7.39";
|
|
12
12
|
const inbox = new Map<string, MessageRecord>();
|
|
13
13
|
const notifiedInbox = new Set<string>();
|
|
14
14
|
let meshClient: HubClient | undefined;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { isRouteAdmitted } from "./routes.ts";
|
|
4
|
+
|
|
5
|
+
/** Deterministic vision gate: judge a UI screenshot through an admitted
|
|
6
|
+
* vision route with a bounded prompt and a strict structured verdict. The
|
|
7
|
+
* gate never free-form chats and never fabricates — an unreadable image, an
|
|
8
|
+
* unadmitted route, or an unparsable verdict returns `verdict: null` with a
|
|
9
|
+
* `divergence` reason. Transport is injectable so tests run without
|
|
10
|
+
* network. */
|
|
11
|
+
|
|
12
|
+
export const VISION_GATE_DEFAULT_ROUTE = "zai-coding-cn/glm-5.3-flash";
|
|
13
|
+
const QUESTION_MAX_CHARS = 500;
|
|
14
|
+
const RAW_MAX_CHARS = 2000;
|
|
15
|
+
|
|
16
|
+
export interface VisionGateOptions {
|
|
17
|
+
projectRoot?: string | undefined;
|
|
18
|
+
/** Absolute or project-relative path to the screenshot. */
|
|
19
|
+
imagePath: string;
|
|
20
|
+
/** What to assert about the screenshot, phrased as a yes/no question. */
|
|
21
|
+
question: string;
|
|
22
|
+
/** Producer id as admitted in routes.yaml. Defaults to the verified
|
|
23
|
+
* vision route. */
|
|
24
|
+
route?: string | undefined;
|
|
25
|
+
timeoutMs?: number | undefined;
|
|
26
|
+
/** Injectable transport: receives the composed prompt, returns CLI stdout. */
|
|
27
|
+
transport?: ((prompt: string) => Promise<string>) | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface VisionGateResult {
|
|
31
|
+
/** true/false when the verdict parsed; null = fail closed. */
|
|
32
|
+
verdict: boolean | null;
|
|
33
|
+
route: string;
|
|
34
|
+
/** Bounded raw output for audit. */
|
|
35
|
+
raw: string;
|
|
36
|
+
divergence?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function visionGatePrompt(question: string, imagePath?: string | undefined): string {
|
|
40
|
+
const bounded = question.trim().slice(0, QUESTION_MAX_CHARS);
|
|
41
|
+
const reference = imagePath ? `Look at the image @${imagePath}.` : "Look at the image attached to this prompt.";
|
|
42
|
+
return [
|
|
43
|
+
"You are judging a UI screenshot for an automated verification gate.",
|
|
44
|
+
reference,
|
|
45
|
+
bounded,
|
|
46
|
+
'Reply with exactly one JSON object and nothing else: {"verdict": true} or {"verdict": false}',
|
|
47
|
+
].join(" ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function parseVisionVerdict(raw: string): boolean | null {
|
|
51
|
+
const match = /"verdict"\s*:\s*(true|false)/i.exec(raw);
|
|
52
|
+
return match ? match[1] === "true" : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultTransport(prompt: string, timeoutMs: number): string {
|
|
56
|
+
return execFileSync(
|
|
57
|
+
"pi",
|
|
58
|
+
["--provider", "zai-coding-cn", "--model", "glm-5.3-flash", "-p", "--no-session", prompt],
|
|
59
|
+
{ encoding: "utf8", timeout: timeoutMs, windowsHide: true },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function runVisionGate(options: VisionGateOptions): Promise<VisionGateResult> {
|
|
64
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
65
|
+
const route = options.route ?? VISION_GATE_DEFAULT_ROUTE;
|
|
66
|
+
const timeoutMs = options.timeoutMs ?? 90_000;
|
|
67
|
+
const base: VisionGateResult = { verdict: null, route, raw: "" };
|
|
68
|
+
|
|
69
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 5_000 || timeoutMs > 300_000) {
|
|
70
|
+
return { ...base, divergence: "timeout_out_of_bounds" };
|
|
71
|
+
}
|
|
72
|
+
if (!options.imagePath || !existsSync(options.imagePath) || !statSync(options.imagePath).isFile()) {
|
|
73
|
+
return { ...base, divergence: "image unreadable" };
|
|
74
|
+
}
|
|
75
|
+
if (!isRouteAdmitted(projectRoot, route)) {
|
|
76
|
+
return { ...base, divergence: "route_not_admitted" };
|
|
77
|
+
}
|
|
78
|
+
const prompt = visionGatePrompt(options.question, options.imagePath);
|
|
79
|
+
|
|
80
|
+
let raw: string;
|
|
81
|
+
try {
|
|
82
|
+
raw = options.transport
|
|
83
|
+
? await options.transport(prompt)
|
|
84
|
+
: defaultTransport(prompt, timeoutMs);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return { ...base, divergence: `transport failed: ${String((error as Error)?.message ?? error).slice(0, 200)}` };
|
|
87
|
+
}
|
|
88
|
+
const boundedRaw = raw.slice(0, RAW_MAX_CHARS);
|
|
89
|
+
const verdict = parseVisionVerdict(boundedRaw);
|
|
90
|
+
if (verdict === null) {
|
|
91
|
+
return { ...base, raw: boundedRaw, divergence: "verdict unreadable" };
|
|
92
|
+
}
|
|
93
|
+
return { ...base, verdict, raw: boundedRaw };
|
|
94
|
+
}
|
|
@@ -97,9 +97,11 @@ export function validateRosterDocument(policy, readBlob) {
|
|
|
97
97
|
text(model, 'origin model'); keys(origin, ['vendor', 'evidence'], 'origin');
|
|
98
98
|
text(origin.vendor, 'origin vendor');
|
|
99
99
|
if (PI_ALLOWED_PROVIDERS.includes(canonical(origin.vendor.toLowerCase()))) refuse('origin/vendor must name the model vendor, not billing provider');
|
|
100
|
-
|
|
100
|
+
const evidenceKeys = origin.evidence.commit === undefined ? ['source', 'sha256'] : ['source', 'commit', 'sha256'];
|
|
101
|
+
keys(origin.evidence, evidenceKeys, 'origin evidence'); digest(origin.evidence.sha256);
|
|
101
102
|
sourcePath(origin.evidence.source);
|
|
102
|
-
|
|
103
|
+
if (origin.evidence.commit !== undefined) objectId(origin.evidence.commit, 'origin evidence commit');
|
|
104
|
+
const bytes = readBlob(origin.evidence.source, origin.evidence.commit);
|
|
103
105
|
if (bytes == null || sha256(bytes) !== origin.evidence.sha256) refuse('origin evidence hash mismatch');
|
|
104
106
|
}
|
|
105
107
|
for (const [id, route] of Object.entries(policy.routes)) {
|
|
@@ -160,17 +162,24 @@ export function validateRosterDocument(policy, readBlob) {
|
|
|
160
162
|
}
|
|
161
163
|
return policy;
|
|
162
164
|
}
|
|
163
|
-
function validate(bytes, commit) {
|
|
165
|
+
function validate(bytes, commit, trusted) {
|
|
164
166
|
let policy;
|
|
165
167
|
try { policy = YAML.parse(bytes.toString('utf8')); } catch { refuse('invalid policy YAML'); }
|
|
166
168
|
if (policy === undefined || policy === null || typeof policy !== 'object' || Array.isArray(policy)) refuse('invalid policy YAML');
|
|
167
|
-
|
|
169
|
+
const readBlob = (source, pinnedCommit) => {
|
|
170
|
+
if (pinnedCommit === undefined) return blobAt(commit, source).bytes;
|
|
171
|
+
// A pinned evidence commit decouples the digest from later edits to the
|
|
172
|
+
// evidence file; the pinned commit itself must be part of trusted history.
|
|
173
|
+
git('merge-base', '--is-ancestor', pinnedCommit, trusted);
|
|
174
|
+
return blobAt(pinnedCommit, source).bytes;
|
|
175
|
+
};
|
|
176
|
+
return validateRosterDocument(policy, readBlob);
|
|
168
177
|
}
|
|
169
178
|
export function loadTrustedRosterPolicy() {
|
|
170
179
|
const snapshot = control();
|
|
171
180
|
const { blob, bytes } = blobAt(snapshot.head, POLICY);
|
|
172
181
|
if (!workingBytes(POLICY).equals(bytes)) refuse('working policy differs from committed bytes');
|
|
173
|
-
const policy = validate(bytes, snapshot.head);
|
|
182
|
+
const policy = validate(bytes, snapshot.head, trusted);
|
|
174
183
|
unchanged(snapshot);
|
|
175
184
|
return frozen({ identity: { commit: snapshot.head, blob, sha256: sha256(bytes) }, policy });
|
|
176
185
|
}
|
|
@@ -181,7 +190,7 @@ export function resolveBoundPolicy(identity) {
|
|
|
181
190
|
git('merge-base', '--is-ancestor', identity.commit, snapshot.trusted);
|
|
182
191
|
const { blob, bytes } = blobAt(identity.commit, POLICY);
|
|
183
192
|
if (blob !== identity.blob || sha256(bytes) !== identity.sha256) refuse('bound identity mismatch');
|
|
184
|
-
const policy = validate(bytes, identity.commit);
|
|
193
|
+
const policy = validate(bytes, identity.commit, snapshot.trusted);
|
|
185
194
|
unchanged(snapshot);
|
|
186
195
|
return frozen({ identity: { ...identity }, policy });
|
|
187
196
|
}
|