@kontextmind/kxm 0.6.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/agents/coordinator.yaml +9 -0
- package/.kxm/agents/critic-arch.yaml +13 -0
- package/.kxm/agents/critic-cli.yaml +13 -0
- package/.kxm/agents/implementer.yaml +13 -0
- package/.kxm/gates.yaml +8 -0
- package/.kxm/producers.yaml +22 -0
- package/.kxm/project.yaml +15 -0
- package/.kxm/roles/writer.yaml +7 -0
- package/.kxm/workflows/default.yaml +47 -0
- package/CHANGELOG.md +39 -7
- package/README.md +1 -0
- package/docs/README.md +5 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +135 -0
- package/docs/architecture.md +1 -1
- package/docs/assignment-runner.md +21 -8
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +11 -2
- 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/kxm-handbook.md +3 -3
- package/docs/operations.md +24 -0
- package/docs/operator-pi-packages.md +63 -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 +107 -0
- package/docs/skills.md +2 -0
- package/docs/test-matrix.md +4 -3
- package/docs/troubleshooting.md +41 -1
- package/docs/vnext/validation.md +9 -0
- package/docs/webhook-workflows.md +2 -2
- package/examples/README.md +1 -1
- package/package.json +16 -17
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/README.md +1 -1
- package/plugins/kxm/dist/cli.js +41620 -35578
- package/plugins/kxm/dist/core.js +271 -34
- package/plugins/kxm/dist/extension.js +7759 -86
- package/plugins/kxm/dist/mcp-server.js +75 -21
- package/plugins/kxm/dist/runtime.js +8218 -2328
- package/plugins/kxm/dist/server.js +3125 -2260
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/SUITE.md +5 -0
- package/plugins/kxm/skills/hints.json +103 -0
- package/plugins/kxm/skills/kxm/SKILL.md +30 -83
- 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-context-memory/SKILL.md +69 -0
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
- package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
- package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
- package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
- package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
- package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
- package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
- package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
- package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
- package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
- package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
- package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
- package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
- package/plugins/kxm/src/autocomplete.ts +9 -3
- 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 +362 -2849
- package/plugins/kxm/src/commands.ts +150 -8
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/config.ts +7 -4
- package/plugins/kxm/src/context-packet.ts +172 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/extension.ts +36 -1
- package/plugins/kxm/src/external-effects.ts +357 -8
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/hub.ts +2 -4
- package/plugins/kxm/src/improve.ts +72 -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 +127 -0
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/policy-draft.d.mts +55 -0
- package/plugins/kxm/src/policy-draft.mjs +565 -0
- package/plugins/kxm/src/price-calc.ts +17 -18
- package/plugins/kxm/src/prices.ts +32 -16
- package/plugins/kxm/src/producers.ts +71 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
- package/plugins/kxm/src/restricted-yaml.mjs +145 -0
- package/plugins/kxm/src/role.ts +710 -0
- package/plugins/kxm/src/routing.ts +99 -1
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/session-work.ts +9 -2
- 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/studio-layout.ts +660 -17
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/suggest.ts +7 -13
- package/plugins/kxm/src/telemetry.ts +82 -0
- package/plugins/kxm/src/tui.ts +140 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +53 -111
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +214 -62
- package/plugins/kxm/src/vnext-harness.ts +336 -84
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
- package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
- package/plugins/kxm/src/vnext-runtime.ts +14 -0
- package/plugins/kxm/src/workflow-manager.ts +392 -0
- package/plugins/kxm/src/workflow-tui.ts +255 -0
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/policy-draft/README.md +17 -0
- package/schemas/policy-draft/model.v2.schema.json +140 -0
- package/schemas/policy-draft/role.v2.schema.json +91 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/schemas/vnext/role.schema.json +76 -0
- package/schemas/vnext/run-event.schema.json +1 -0
- package/scripts/assignment-run.d.mts +1 -1
- package/scripts/assignment-run.mjs +44 -35
- package/scripts/check-generated.mjs +33 -9
- package/scripts/emit-codex-artifacts.mjs +255 -11
- package/scripts/harness-run.d.mts +12 -4
- package/scripts/harness-run.mjs +65 -17
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +150 -2
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
- package/scripts/native-critic.d.mts +5 -0
- package/scripts/native-critic.mjs +60 -0
- package/.kxm/config/README.md +0 -5
- package/.kxm/config/agents.json +0 -43
- package/.kxm/config/env.example +0 -56
- package/.kxm/config/update.example.yaml +0 -9
- package/.kxm/config/workflows/fix.json +0 -160
- package/.kxm/config/workflows/jira-development.json +0 -116
- package/.kxm/config/workflows/provenance-quorum.json +0 -150
- package/.kxm/config/workflows/v04-dogfood.json +0 -72
package/plugins/kxm/dist/core.js
CHANGED
|
@@ -59,6 +59,61 @@ function parseBoundedInteger(value, field, fallback, min, max) {
|
|
|
59
59
|
}
|
|
60
60
|
return value;
|
|
61
61
|
}
|
|
62
|
+
var TERMINAL_RECEIPT_SCHEMA = "kxm.terminal-receipt.v1";
|
|
63
|
+
var VALID_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["accepted", "audit_escalation", "rejected", "error"]);
|
|
64
|
+
function validateTerminalReceipt(value) {
|
|
65
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
66
|
+
throw new ProtocolError(400, "terminal receipt must be an object", "invalid_terminal_receipt");
|
|
67
|
+
}
|
|
68
|
+
const record = value;
|
|
69
|
+
if (record.schema !== void 0 && record.schema !== TERMINAL_RECEIPT_SCHEMA) {
|
|
70
|
+
throw new ProtocolError(400, `terminal receipt schema must be ${TERMINAL_RECEIPT_SCHEMA}`, "invalid_terminal_receipt");
|
|
71
|
+
}
|
|
72
|
+
const status = record.status;
|
|
73
|
+
if (!status || !VALID_TERMINAL_STATUSES.has(status)) {
|
|
74
|
+
throw new ProtocolError(
|
|
75
|
+
400,
|
|
76
|
+
`terminal receipt status must be one of: ${Array.from(VALID_TERMINAL_STATUSES).join(", ")}`,
|
|
77
|
+
"invalid_terminal_receipt"
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const seat = requireString(record.seat, "seat", { max: 64 });
|
|
81
|
+
const runId = requireString(record.runId, "runId", { max: 128 });
|
|
82
|
+
const stageId = requireString(record.stageId, "stageId", { max: 128 });
|
|
83
|
+
const timestamp = requireString(record.timestamp, "timestamp", { max: 64 });
|
|
84
|
+
const host = requireString(record.host, "host", { max: 64 });
|
|
85
|
+
const model = requireString(record.model, "model", { max: 128 });
|
|
86
|
+
let evidence;
|
|
87
|
+
if (record.evidence !== void 0) {
|
|
88
|
+
if (!record.evidence || typeof record.evidence !== "object" || Array.isArray(record.evidence)) {
|
|
89
|
+
throw new ProtocolError(400, "terminal receipt evidence must be an object", "invalid_terminal_receipt");
|
|
90
|
+
}
|
|
91
|
+
evidence = record.evidence;
|
|
92
|
+
}
|
|
93
|
+
let metrics;
|
|
94
|
+
if (record.metrics !== void 0) {
|
|
95
|
+
if (!record.metrics || typeof record.metrics !== "object" || Array.isArray(record.metrics)) {
|
|
96
|
+
throw new ProtocolError(400, "terminal receipt metrics must be an object", "invalid_terminal_receipt");
|
|
97
|
+
}
|
|
98
|
+
metrics = record.metrics;
|
|
99
|
+
}
|
|
100
|
+
const escalationReason = optionalString(record.escalationReason, "escalationReason", 1024);
|
|
101
|
+
const ruling = optionalString(record.ruling, "ruling", 2048);
|
|
102
|
+
return {
|
|
103
|
+
schema: TERMINAL_RECEIPT_SCHEMA,
|
|
104
|
+
status,
|
|
105
|
+
seat,
|
|
106
|
+
runId,
|
|
107
|
+
stageId,
|
|
108
|
+
timestamp,
|
|
109
|
+
host,
|
|
110
|
+
model,
|
|
111
|
+
...evidence ? { evidence } : {},
|
|
112
|
+
...metrics ? { metrics } : {},
|
|
113
|
+
...escalationReason ? { escalationReason } : {},
|
|
114
|
+
...ruling ? { ruling } : {}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
62
117
|
|
|
63
118
|
// plugins/kxm/src/routing.ts
|
|
64
119
|
import { createHash } from "node:crypto";
|
|
@@ -68,10 +123,7 @@ function findModelPrice(catalog, model, provider) {
|
|
|
68
123
|
const normalizedModel = model.trim().toLowerCase();
|
|
69
124
|
const normalizedProvider = provider?.trim().toLowerCase();
|
|
70
125
|
for (const entry of catalog.models) {
|
|
71
|
-
if (normalizedProvider && entry.provider.toLowerCase() !== normalizedProvider)
|
|
72
|
-
const matchesAlias = entry.aliases?.some((a) => a.toLowerCase() === normalizedModel);
|
|
73
|
-
if (!matchesAlias) continue;
|
|
74
|
-
}
|
|
126
|
+
if (normalizedProvider && entry.provider.toLowerCase() !== normalizedProvider) continue;
|
|
75
127
|
if (entry.id.toLowerCase() === normalizedModel || entry.model.toLowerCase() === normalizedModel) {
|
|
76
128
|
return entry;
|
|
77
129
|
}
|
|
@@ -84,20 +136,19 @@ function findModelPrice(catalog, model, provider) {
|
|
|
84
136
|
function calculateModelCost(catalog, params) {
|
|
85
137
|
const row = findModelPrice(catalog, params.model, params.provider);
|
|
86
138
|
if (!row || row.tiers.length === 0) return void 0;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
const cacheRead = params.cacheReadTokens ?? 0;
|
|
99
|
-
const cacheWrite = params.cacheWriteTokens ?? 0;
|
|
139
|
+
if (catalog.currency !== void 0 && catalog.currency !== "USD") return void 0;
|
|
140
|
+
const validCount = (n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0;
|
|
141
|
+
const tokensIn = params.tokensIn;
|
|
142
|
+
const tokensOut = params.tokensOut;
|
|
143
|
+
const cacheRead = params.cacheReadTokens;
|
|
144
|
+
const cacheWrite = params.cacheWriteTokens;
|
|
145
|
+
if (!validCount(tokensIn) || !validCount(tokensOut) || !validCount(cacheRead) || !validCount(cacheWrite)) return void 0;
|
|
146
|
+
const context = params.contextTokens;
|
|
147
|
+
if (context != null && !validCount(context)) return void 0;
|
|
148
|
+
const selectedTier = context == null ? row.tiers.length === 1 && row.tiers[0].upToContextTokens == null ? row.tiers[0] : void 0 : row.tiers.find((tier) => tier.upToContextTokens == null || context <= tier.upToContextTokens);
|
|
149
|
+
if (!selectedTier || cacheRead > 0 && selectedTier.cacheReadPerMillion == null || cacheWrite > 0 && selectedTier.cacheWritePerMillion == null) return void 0;
|
|
100
150
|
const cost = tokensIn / 1e6 * selectedTier.inputPerMillion + tokensOut / 1e6 * selectedTier.outputPerMillion + cacheRead / 1e6 * (selectedTier.cacheReadPerMillion ?? 0) + cacheWrite / 1e6 * (selectedTier.cacheWritePerMillion ?? 0);
|
|
151
|
+
if (!Number.isFinite(cost) || cost < 0) return void 0;
|
|
101
152
|
const priceRef = `${catalog.date}#${row.id}`;
|
|
102
153
|
return {
|
|
103
154
|
costUsd: Math.round(cost * 1e6) / 1e6,
|
|
@@ -435,6 +486,63 @@ function computePercentile(sorted, p) {
|
|
|
435
486
|
}
|
|
436
487
|
return sorted[base];
|
|
437
488
|
}
|
|
489
|
+
function computeDecayedWeight(recordedAt, halfLifeDays = 14, now = Date.now()) {
|
|
490
|
+
const ts = typeof recordedAt === "number" ? recordedAt : Date.parse(recordedAt);
|
|
491
|
+
if (!Number.isFinite(ts)) return 1;
|
|
492
|
+
const deltaMs = Math.max(0, now - ts);
|
|
493
|
+
const halfLifeMs = halfLifeDays * 24 * 60 * 60 * 1e3;
|
|
494
|
+
return Math.pow(2, -deltaMs / halfLifeMs);
|
|
495
|
+
}
|
|
496
|
+
function evaluateCircuitBreaker(records, route, config, now = Date.now()) {
|
|
497
|
+
const mode = config?.mode ?? "soft_demotion";
|
|
498
|
+
const failureThreshold = config?.failureThreshold ?? 3;
|
|
499
|
+
const windowSeconds = config?.windowSeconds ?? 3600;
|
|
500
|
+
const penaltyMultiplier = config?.penaltyMultiplier ?? 5;
|
|
501
|
+
const windowMs = windowSeconds * 1e3;
|
|
502
|
+
const matching = records.filter((r) => {
|
|
503
|
+
const isV2 = r.schema === ROUTING_RECORD_V2_SCHEMA;
|
|
504
|
+
const harness = isV2 ? r.harness : r.providerMetadata?.harness;
|
|
505
|
+
const model = isV2 ? r.effectiveModel || r.requestedModel : r.effectiveModel || r.requestedModel;
|
|
506
|
+
if (harness !== route.harness || model !== route.model) return false;
|
|
507
|
+
const recAt = r.recordedAt;
|
|
508
|
+
const ts = typeof recAt === "string" ? Date.parse(recAt) : 0;
|
|
509
|
+
return ts >= now - windowMs;
|
|
510
|
+
});
|
|
511
|
+
matching.sort((a, b) => {
|
|
512
|
+
const ta = typeof a.recordedAt === "string" ? Date.parse(a.recordedAt) : 0;
|
|
513
|
+
const tb = typeof b.recordedAt === "string" ? Date.parse(b.recordedAt) : 0;
|
|
514
|
+
return tb - ta;
|
|
515
|
+
});
|
|
516
|
+
let consecutiveFailures = 0;
|
|
517
|
+
for (const r of matching) {
|
|
518
|
+
const passed = r.verifierOutcome === "passed" || r.finalOutcome === "accepted" || r.finalOutcome === "completed";
|
|
519
|
+
if (passed && (r.retries ?? 0) === 0) {
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
consecutiveFailures++;
|
|
523
|
+
}
|
|
524
|
+
if (consecutiveFailures >= failureThreshold) {
|
|
525
|
+
if (mode === "quarantine") {
|
|
526
|
+
return {
|
|
527
|
+
status: "quarantined",
|
|
528
|
+
consecutiveFailures,
|
|
529
|
+
penaltyMultiplier,
|
|
530
|
+
reason: `Route quarantined after ${consecutiveFailures} consecutive failures within ${windowSeconds}s`
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
status: "demoted",
|
|
535
|
+
consecutiveFailures,
|
|
536
|
+
penaltyMultiplier,
|
|
537
|
+
reason: `Route demoted with ${penaltyMultiplier}x penalty after ${consecutiveFailures} consecutive failures`
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
status: "healthy",
|
|
542
|
+
consecutiveFailures,
|
|
543
|
+
penaltyMultiplier: 1
|
|
544
|
+
};
|
|
545
|
+
}
|
|
438
546
|
function generateRoutingReport(records, options = {}) {
|
|
439
547
|
const generatedAt = options.now ? options.now() : (/* @__PURE__ */ new Date()).toISOString();
|
|
440
548
|
if (records.length === 0) {
|
|
@@ -538,7 +646,8 @@ function generateRoutingReport(records, options = {}) {
|
|
|
538
646
|
tokensIn: r.tokensIn ?? null,
|
|
539
647
|
tokensOut: r.tokensOut ?? null,
|
|
540
648
|
cacheReadTokens: r.cacheReadTokens ?? null,
|
|
541
|
-
|
|
649
|
+
cacheWriteTokens: v2?.cacheWriteTokens ?? null,
|
|
650
|
+
contextTokens: v2?.contextTokens ?? null
|
|
542
651
|
});
|
|
543
652
|
if (calc) {
|
|
544
653
|
equivTotal += calc.costUsd;
|
|
@@ -742,7 +851,10 @@ function redactStringList(values, maxItems = 32) {
|
|
|
742
851
|
}
|
|
743
852
|
|
|
744
853
|
// plugins/kxm/src/commands.ts
|
|
745
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
854
|
+
import { createHash as createHash2, randomUUID as randomUUID2, timingSafeEqual } from "node:crypto";
|
|
855
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
856
|
+
import { homedir } from "node:os";
|
|
857
|
+
import { dirname, join, resolve } from "node:path";
|
|
746
858
|
|
|
747
859
|
// plugins/kxm/src/client.ts
|
|
748
860
|
var HubHttpError = class extends Error {
|
|
@@ -1508,26 +1620,60 @@ function parseAttemptToken(token) {
|
|
|
1508
1620
|
}
|
|
1509
1621
|
return void 0;
|
|
1510
1622
|
}
|
|
1623
|
+
function timingSafeStringCompare(a, b) {
|
|
1624
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
1625
|
+
const hashA = createHash2("sha256").update(a).digest();
|
|
1626
|
+
const hashB = createHash2("sha256").update(b).digest();
|
|
1627
|
+
return timingSafeEqual(hashA, hashB);
|
|
1628
|
+
}
|
|
1511
1629
|
function mintSessionToken(input) {
|
|
1512
1630
|
const toolPolicy = input?.toolPolicy ?? (input?.allowedTools || input?.deniedTools || input?.preset ? {
|
|
1513
1631
|
...input?.preset ? { preset: input.preset } : {},
|
|
1514
|
-
...input?.allowedTools ? { allow: input.allowedTools } : {},
|
|
1515
|
-
...input?.deniedTools ? { deny: input.deniedTools } : {}
|
|
1632
|
+
...input?.allowedTools ? { allow: input.allowedTools, allowedTools: input.allowedTools } : {},
|
|
1633
|
+
...input?.deniedTools ? { deny: input.deniedTools, deniedTools: input.deniedTools } : {}
|
|
1516
1634
|
} : void 0);
|
|
1635
|
+
const issuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1636
|
+
const defaultTtlMs = 24 * 60 * 60 * 1e3;
|
|
1637
|
+
const ttlMs = typeof input?.ttlMs === "number" && input.ttlMs > 0 ? input.ttlMs : defaultTtlMs;
|
|
1638
|
+
const expiresAt = input?.expiresAt ?? new Date(Date.now() + ttlMs).toISOString();
|
|
1517
1639
|
const payload = {
|
|
1518
1640
|
schema: "kxm.session-token.v1",
|
|
1519
1641
|
sessionId: input?.sessionId ?? `session-${randomUUID2()}`,
|
|
1520
|
-
issuedAt
|
|
1642
|
+
issuedAt,
|
|
1643
|
+
expiresAt,
|
|
1521
1644
|
...input?.agentName ? { agentName: input.agentName } : {},
|
|
1522
1645
|
...toolPolicy ? { toolPolicy } : {}
|
|
1523
1646
|
};
|
|
1524
1647
|
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
1525
1648
|
}
|
|
1649
|
+
function isSessionTokenExpired(payloadOrToken) {
|
|
1650
|
+
let payload;
|
|
1651
|
+
if (typeof payloadOrToken === "string") {
|
|
1652
|
+
try {
|
|
1653
|
+
const raw = Buffer.from(payloadOrToken.trim(), "base64url").toString("utf8");
|
|
1654
|
+
payload = JSON.parse(raw);
|
|
1655
|
+
} catch {
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
} else {
|
|
1659
|
+
payload = payloadOrToken;
|
|
1660
|
+
}
|
|
1661
|
+
if (!payload || !payload.expiresAt) return false;
|
|
1662
|
+
const expiryTime = new Date(payload.expiresAt).getTime();
|
|
1663
|
+
if (Number.isNaN(expiryTime)) return true;
|
|
1664
|
+
return Date.now() >= expiryTime;
|
|
1665
|
+
}
|
|
1526
1666
|
function parseSessionToken(token) {
|
|
1527
1667
|
try {
|
|
1528
1668
|
const raw = Buffer.from(token.trim(), "base64url").toString("utf8");
|
|
1529
1669
|
const parsed = JSON.parse(raw);
|
|
1530
1670
|
if (parsed && typeof parsed === "object" && parsed.schema === "kxm.session-token.v1" && typeof parsed.sessionId === "string") {
|
|
1671
|
+
if (parsed.expiresAt) {
|
|
1672
|
+
const expiryTime = new Date(parsed.expiresAt).getTime();
|
|
1673
|
+
if (!Number.isNaN(expiryTime) && Date.now() >= expiryTime) {
|
|
1674
|
+
return void 0;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1531
1677
|
return parsed;
|
|
1532
1678
|
}
|
|
1533
1679
|
} catch {
|
|
@@ -1535,6 +1681,49 @@ function parseSessionToken(token) {
|
|
|
1535
1681
|
}
|
|
1536
1682
|
return void 0;
|
|
1537
1683
|
}
|
|
1684
|
+
function resolveUserConfigDirectory(overrideDir) {
|
|
1685
|
+
if (overrideDir) return resolve(overrideDir);
|
|
1686
|
+
return resolve(process.env.KXM_USER_CONFIG_DIR?.trim() || join(homedir(), ".config", "kxm"));
|
|
1687
|
+
}
|
|
1688
|
+
function sessionTokenPath(userConfigDir) {
|
|
1689
|
+
return join(resolveUserConfigDirectory(userConfigDir), "session.token");
|
|
1690
|
+
}
|
|
1691
|
+
function persistSessionTokenToDisk(token, options) {
|
|
1692
|
+
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
1693
|
+
const dir = dirname(filePath);
|
|
1694
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1695
|
+
const mode = options?.mode ?? 384;
|
|
1696
|
+
writeFileSync(filePath, `${token.trim()}
|
|
1697
|
+
`, { encoding: "utf8", mode });
|
|
1698
|
+
try {
|
|
1699
|
+
chmodSync(filePath, mode);
|
|
1700
|
+
} catch {
|
|
1701
|
+
}
|
|
1702
|
+
return filePath;
|
|
1703
|
+
}
|
|
1704
|
+
function readSessionTokenFromDisk(options) {
|
|
1705
|
+
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
1706
|
+
if (!existsSync(filePath)) return void 0;
|
|
1707
|
+
try {
|
|
1708
|
+
const token = readFileSync(filePath, "utf8").trim();
|
|
1709
|
+
if (!token) return void 0;
|
|
1710
|
+
const payload = parseSessionToken(token);
|
|
1711
|
+
if (!payload) return void 0;
|
|
1712
|
+
return { token, payload };
|
|
1713
|
+
} catch {
|
|
1714
|
+
return void 0;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function clearSessionTokenFromDisk(options) {
|
|
1718
|
+
const filePath = sessionTokenPath(options?.userConfigDir);
|
|
1719
|
+
if (!existsSync(filePath)) return false;
|
|
1720
|
+
try {
|
|
1721
|
+
unlinkSync(filePath);
|
|
1722
|
+
return true;
|
|
1723
|
+
} catch {
|
|
1724
|
+
return false;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1538
1727
|
function matchToolPattern(pattern, toolName) {
|
|
1539
1728
|
if (pattern === "*" || pattern === toolName) return true;
|
|
1540
1729
|
if (pattern.endsWith("*")) {
|
|
@@ -1577,7 +1766,7 @@ function isToolAllowed(commandName, policy) {
|
|
|
1577
1766
|
}
|
|
1578
1767
|
return true;
|
|
1579
1768
|
}
|
|
1580
|
-
function enforceToolPolicy(commandName, env = process.env) {
|
|
1769
|
+
function enforceToolPolicy(commandName, env = process.env, options) {
|
|
1581
1770
|
const attemptTokenRaw = env.KXM_ATTEMPT_TOKEN?.trim();
|
|
1582
1771
|
if (attemptTokenRaw) {
|
|
1583
1772
|
const attempt = parseAttemptToken(attemptTokenRaw);
|
|
@@ -1591,13 +1780,51 @@ function enforceToolPolicy(commandName, env = process.env) {
|
|
|
1591
1780
|
detail: `command ${commandName} is denied by attempt tool policy`
|
|
1592
1781
|
};
|
|
1593
1782
|
}
|
|
1783
|
+
if (commandName === "kxm_promote" || commandName === "promote") {
|
|
1784
|
+
const explicitAllow = attempt.toolPolicy?.allow ?? attempt.toolPolicy?.allowedTools;
|
|
1785
|
+
if (!Array.isArray(explicitAllow) || !explicitAllow.includes("kxm_promote") && !explicitAllow.includes("promote") && !explicitAllow.includes("*")) {
|
|
1786
|
+
return {
|
|
1787
|
+
allowed: false,
|
|
1788
|
+
error: "attempt_token_admin_denied",
|
|
1789
|
+
detail: "AttemptToken worker cannot perform operator state promotion without explicit policy grant"
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (options?.runId && attempt.runId && options.runId !== attempt.runId) {
|
|
1794
|
+
return {
|
|
1795
|
+
allowed: false,
|
|
1796
|
+
error: "attempt_token_scope_violation",
|
|
1797
|
+
detail: `attempt token runId ${attempt.runId} does not match request runId ${options.runId}`
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1594
1800
|
return { allowed: true };
|
|
1595
1801
|
}
|
|
1596
|
-
const
|
|
1597
|
-
if (
|
|
1598
|
-
const session = parseSessionToken(
|
|
1802
|
+
const envSessionRaw = env.KXM_SESSION_TOKEN?.trim();
|
|
1803
|
+
if (envSessionRaw) {
|
|
1804
|
+
const session = parseSessionToken(envSessionRaw);
|
|
1805
|
+
if (!session) {
|
|
1806
|
+
return { allowed: false, error: "session_token_invalid", detail: "KXM_SESSION_TOKEN is malformed or expired" };
|
|
1807
|
+
}
|
|
1808
|
+
if (!isToolAllowed(commandName, session.toolPolicy)) {
|
|
1809
|
+
return {
|
|
1810
|
+
allowed: false,
|
|
1811
|
+
error: "tool_policy_denied",
|
|
1812
|
+
detail: `command ${commandName} is denied by session tool policy`
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
return { allowed: true };
|
|
1816
|
+
}
|
|
1817
|
+
const tokenFile = sessionTokenPath(env.KXM_USER_CONFIG_DIR);
|
|
1818
|
+
if (existsSync(tokenFile)) {
|
|
1819
|
+
let tokenRaw;
|
|
1820
|
+
try {
|
|
1821
|
+
tokenRaw = readFileSync(tokenFile, "utf8").trim();
|
|
1822
|
+
} catch {
|
|
1823
|
+
return { allowed: false, error: "session_token_invalid", detail: "Session token file on disk could not be read" };
|
|
1824
|
+
}
|
|
1825
|
+
const session = tokenRaw ? parseSessionToken(tokenRaw) : void 0;
|
|
1599
1826
|
if (!session) {
|
|
1600
|
-
return { allowed: false, error: "session_token_invalid", detail: "
|
|
1827
|
+
return { allowed: false, error: "session_token_invalid", detail: "Session token on disk is malformed or expired" };
|
|
1601
1828
|
}
|
|
1602
1829
|
if (!isToolAllowed(commandName, session.toolPolicy)) {
|
|
1603
1830
|
return {
|
|
@@ -1612,8 +1839,8 @@ function enforceToolPolicy(commandName, env = process.env) {
|
|
|
1612
1839
|
}
|
|
1613
1840
|
|
|
1614
1841
|
// plugins/kxm/src/logger.ts
|
|
1615
|
-
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
1616
|
-
import { dirname } from "node:path";
|
|
1842
|
+
import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync, statSync, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1843
|
+
import { dirname as dirname2 } from "node:path";
|
|
1617
1844
|
var LOG_LEVEL_PRIORITY = {
|
|
1618
1845
|
debug: 10,
|
|
1619
1846
|
info: 20,
|
|
@@ -1650,10 +1877,10 @@ function redactLogValue(val, key) {
|
|
|
1650
1877
|
function rotateLogFiles(filePath, maxFiles) {
|
|
1651
1878
|
for (let i = maxFiles; i >= 1; i--) {
|
|
1652
1879
|
const current = `${filePath}.${i}`;
|
|
1653
|
-
if (
|
|
1880
|
+
if (existsSync2(current)) {
|
|
1654
1881
|
if (i >= maxFiles) {
|
|
1655
1882
|
try {
|
|
1656
|
-
|
|
1883
|
+
unlinkSync2(current);
|
|
1657
1884
|
} catch {
|
|
1658
1885
|
}
|
|
1659
1886
|
} else {
|
|
@@ -1664,7 +1891,7 @@ function rotateLogFiles(filePath, maxFiles) {
|
|
|
1664
1891
|
}
|
|
1665
1892
|
}
|
|
1666
1893
|
}
|
|
1667
|
-
if (
|
|
1894
|
+
if (existsSync2(filePath)) {
|
|
1668
1895
|
try {
|
|
1669
1896
|
renameSync(filePath, `${filePath}.1`);
|
|
1670
1897
|
} catch {
|
|
@@ -1681,7 +1908,7 @@ function createLogger(options) {
|
|
|
1681
1908
|
const shouldStdout = options.stdout ?? !isDaemon;
|
|
1682
1909
|
const correlationDefaults = options.correlation ?? {};
|
|
1683
1910
|
let currentSize = 0;
|
|
1684
|
-
if (filePath &&
|
|
1911
|
+
if (filePath && existsSync2(filePath)) {
|
|
1685
1912
|
try {
|
|
1686
1913
|
currentSize = statSync(filePath).size;
|
|
1687
1914
|
} catch {
|
|
@@ -1719,7 +1946,7 @@ function createLogger(options) {
|
|
|
1719
1946
|
currentSize = 0;
|
|
1720
1947
|
}
|
|
1721
1948
|
try {
|
|
1722
|
-
|
|
1949
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
1723
1950
|
appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
|
|
1724
1951
|
currentSize += lineBytes;
|
|
1725
1952
|
} catch {
|
|
@@ -1785,13 +2012,17 @@ export {
|
|
|
1785
2012
|
ROUTING_RECORD_SCHEMA,
|
|
1786
2013
|
ROUTING_RECORD_V2_SCHEMA,
|
|
1787
2014
|
ROUTING_REPORT_SCHEMA,
|
|
2015
|
+
TERMINAL_RECEIPT_SCHEMA,
|
|
1788
2016
|
WORKER_RESULT_SCHEMA,
|
|
1789
2017
|
WORKER_SCHEMA,
|
|
1790
2018
|
agentWorker,
|
|
1791
2019
|
behavioralConfigHash,
|
|
2020
|
+
clearSessionTokenFromDisk,
|
|
1792
2021
|
compareRoutingRecords,
|
|
2022
|
+
computeDecayedWeight,
|
|
1793
2023
|
createLogger,
|
|
1794
2024
|
enforceToolPolicy,
|
|
2025
|
+
evaluateCircuitBreaker,
|
|
1795
2026
|
formatRoutingReport,
|
|
1796
2027
|
gateWorker,
|
|
1797
2028
|
generateRoutingReport,
|
|
@@ -1800,6 +2031,7 @@ export {
|
|
|
1800
2031
|
getPiToolDefinitions,
|
|
1801
2032
|
groupByBehavior,
|
|
1802
2033
|
isQuotaExhausted,
|
|
2034
|
+
isSessionTokenExpired,
|
|
1803
2035
|
isToolAllowed,
|
|
1804
2036
|
looksLikeSecret,
|
|
1805
2037
|
mintAttemptToken,
|
|
@@ -1813,11 +2045,16 @@ export {
|
|
|
1813
2045
|
parseRoutingRecord,
|
|
1814
2046
|
parseRoutingRecordV2,
|
|
1815
2047
|
parseSessionToken,
|
|
2048
|
+
persistSessionTokenToDisk,
|
|
2049
|
+
readSessionTokenFromDisk,
|
|
1816
2050
|
reconcileInbox,
|
|
1817
2051
|
redactLogValue,
|
|
1818
2052
|
redactSecrets,
|
|
1819
2053
|
redactStringList,
|
|
1820
2054
|
requireString,
|
|
1821
2055
|
rotateLogFiles,
|
|
2056
|
+
sessionTokenPath,
|
|
2057
|
+
timingSafeStringCompare,
|
|
2058
|
+
validateTerminalReceipt,
|
|
1822
2059
|
workerResult
|
|
1823
2060
|
};
|