@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.
Files changed (175) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.kxm/agents/coordinator.yaml +9 -0
  3. package/.kxm/agents/critic-arch.yaml +13 -0
  4. package/.kxm/agents/critic-cli.yaml +13 -0
  5. package/.kxm/agents/implementer.yaml +13 -0
  6. package/.kxm/gates.yaml +8 -0
  7. package/.kxm/producers.yaml +22 -0
  8. package/.kxm/project.yaml +15 -0
  9. package/.kxm/roles/writer.yaml +7 -0
  10. package/.kxm/workflows/default.yaml +47 -0
  11. package/CHANGELOG.md +39 -7
  12. package/README.md +1 -0
  13. package/docs/README.md +5 -0
  14. package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
  15. package/docs/agent-skills.md +135 -0
  16. package/docs/architecture.md +1 -1
  17. package/docs/assignment-runner.md +21 -8
  18. package/docs/browser-automation.md +116 -0
  19. package/docs/configuration.md +11 -2
  20. package/docs/getting-started.md +21 -0
  21. package/docs/kb/how-credentials-retrieved-safely.md +31 -0
  22. package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
  23. package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
  24. package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
  25. package/docs/kb/how-to-resume-after-mfa.md +28 -0
  26. package/docs/kb/how-to-take-over-session.md +32 -0
  27. package/docs/kb/why-authentication-disappeared.md +32 -0
  28. package/docs/kb/why-automation-opened-different-browser.md +32 -0
  29. package/docs/kb/why-session-viewer-cannot-control.md +31 -0
  30. package/docs/kxm-handbook.md +3 -3
  31. package/docs/operations.md +24 -0
  32. package/docs/operator-pi-packages.md +63 -0
  33. package/docs/prompts/browser-annotate-feedback.md +41 -0
  34. package/docs/prompts/browser-diagnose-recover.md +38 -0
  35. package/docs/prompts/browser-explore.md +42 -0
  36. package/docs/prompts/browser-repro-fix.md +48 -0
  37. package/docs/prompts/browser-start.md +41 -0
  38. package/docs/prompts/browser-takeover.md +50 -0
  39. package/docs/skills/repo-work-delivery.md +107 -0
  40. package/docs/skills.md +2 -0
  41. package/docs/test-matrix.md +4 -3
  42. package/docs/troubleshooting.md +41 -1
  43. package/docs/vnext/validation.md +9 -0
  44. package/docs/webhook-workflows.md +2 -2
  45. package/examples/README.md +1 -1
  46. package/package.json +16 -17
  47. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  48. package/plugins/kxm/README.md +1 -1
  49. package/plugins/kxm/dist/cli.js +41620 -35578
  50. package/plugins/kxm/dist/core.js +271 -34
  51. package/plugins/kxm/dist/extension.js +7759 -86
  52. package/plugins/kxm/dist/mcp-server.js +75 -21
  53. package/plugins/kxm/dist/runtime.js +8218 -2328
  54. package/plugins/kxm/dist/server.js +3125 -2260
  55. package/plugins/kxm/dist/vnext-runtime-supervisor.js +5961 -661
  56. package/plugins/kxm/package.json +1 -1
  57. package/plugins/kxm/skills/SUITE.md +5 -0
  58. package/plugins/kxm/skills/hints.json +103 -0
  59. package/plugins/kxm/skills/kxm/SKILL.md +30 -83
  60. package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
  61. package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
  62. package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
  63. package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
  64. package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
  65. package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
  66. package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
  67. package/plugins/kxm/skills/kxm-context-memory/SKILL.md +69 -0
  68. package/plugins/kxm/skills/kxm-definitions/SKILL.md +65 -0
  69. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +34 -0
  70. package/plugins/kxm/skills/kxm-harvest/SKILL.md +48 -0
  71. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +43 -0
  72. package/plugins/kxm/skills/kxm-insights/SKILL.md +48 -0
  73. package/plugins/kxm/skills/kxm-mind/SKILL.md +59 -0
  74. package/plugins/kxm/skills/kxm-peer/SKILL.md +110 -0
  75. package/plugins/kxm/skills/kxm-project-setup/SKILL.md +42 -0
  76. package/plugins/kxm/skills/kxm-projects/SKILL.md +43 -0
  77. package/plugins/kxm/skills/kxm-protocol/SKILL.md +66 -0
  78. package/plugins/kxm/skills/kxm-query/SKILL.md +45 -0
  79. package/plugins/kxm/skills/kxm-routing-improve/SKILL.md +30 -0
  80. package/plugins/kxm/skills/kxm-runs/SKILL.md +29 -0
  81. package/plugins/kxm/skills/kxm-setup/SKILL.md +55 -0
  82. package/plugins/kxm/skills/kxm-skill-lifecycle/SKILL.md +31 -0
  83. package/plugins/kxm/skills/kxm-tasks/SKILL.md +33 -0
  84. package/plugins/kxm/skills/kxm-triage/SKILL.md +47 -0
  85. package/plugins/kxm/skills/kxm-work/SKILL.md +44 -0
  86. package/plugins/kxm/skills/kxm-workflow/SKILL.md +45 -0
  87. package/plugins/kxm/src/autocomplete.ts +9 -3
  88. package/plugins/kxm/src/browser.ts +603 -0
  89. package/plugins/kxm/src/cli/context-skills.ts +373 -0
  90. package/plugins/kxm/src/cli/hub.ts +614 -0
  91. package/plugins/kxm/src/cli/roles.ts +615 -0
  92. package/plugins/kxm/src/cli/system.ts +906 -0
  93. package/plugins/kxm/src/cli/tasks.ts +364 -0
  94. package/plugins/kxm/src/cli/types.ts +270 -0
  95. package/plugins/kxm/src/cli/vnext.ts +698 -0
  96. package/plugins/kxm/src/cli/workflows.ts +699 -0
  97. package/plugins/kxm/src/cli.ts +362 -2849
  98. package/plugins/kxm/src/commands.ts +150 -8
  99. package/plugins/kxm/src/completion-install.ts +223 -0
  100. package/plugins/kxm/src/config.ts +7 -4
  101. package/plugins/kxm/src/context-packet.ts +172 -0
  102. package/plugins/kxm/src/database.ts +1 -1
  103. package/plugins/kxm/src/extension.ts +36 -1
  104. package/plugins/kxm/src/external-effects.ts +357 -8
  105. package/plugins/kxm/src/hub-env.ts +193 -0
  106. package/plugins/kxm/src/hub.ts +2 -4
  107. package/plugins/kxm/src/improve.ts +72 -0
  108. package/plugins/kxm/src/init-guide-setup.ts +547 -0
  109. package/plugins/kxm/src/local-snapshot.ts +1 -1
  110. package/plugins/kxm/src/mcp-server.ts +1 -1
  111. package/plugins/kxm/src/model-inventory.ts +127 -0
  112. package/plugins/kxm/src/modes.ts +348 -0
  113. package/plugins/kxm/src/policy-draft.d.mts +55 -0
  114. package/plugins/kxm/src/policy-draft.mjs +565 -0
  115. package/plugins/kxm/src/price-calc.ts +17 -18
  116. package/plugins/kxm/src/prices.ts +32 -16
  117. package/plugins/kxm/src/producers.ts +71 -0
  118. package/plugins/kxm/src/protocol.ts +111 -0
  119. package/plugins/kxm/src/restricted-yaml.d.mts +31 -0
  120. package/plugins/kxm/src/restricted-yaml.mjs +145 -0
  121. package/plugins/kxm/src/role.ts +710 -0
  122. package/plugins/kxm/src/routing.ts +99 -1
  123. package/plugins/kxm/src/runtime.ts +4 -0
  124. package/plugins/kxm/src/safety-integrity.ts +76 -0
  125. package/plugins/kxm/src/session-work.ts +9 -2
  126. package/plugins/kxm/src/sqlite.ts +76 -0
  127. package/plugins/kxm/src/ssh-remote.ts +560 -0
  128. package/plugins/kxm/src/store.ts +1 -1
  129. package/plugins/kxm/src/studio-layout.ts +660 -17
  130. package/plugins/kxm/src/subagent-control.ts +312 -0
  131. package/plugins/kxm/src/suggest.ts +7 -13
  132. package/plugins/kxm/src/telemetry.ts +82 -0
  133. package/plugins/kxm/src/tui.ts +140 -0
  134. package/plugins/kxm/src/vnext-bindings.ts +1 -1
  135. package/plugins/kxm/src/vnext-config.ts +53 -111
  136. package/plugins/kxm/src/vnext-engine-command.ts +2 -0
  137. package/plugins/kxm/src/vnext-engine.ts +214 -62
  138. package/plugins/kxm/src/vnext-harness.ts +336 -84
  139. package/plugins/kxm/src/vnext-oneshot-evidence.ts +117 -0
  140. package/plugins/kxm/src/vnext-oneshot-process.ts +187 -0
  141. package/plugins/kxm/src/vnext-oneshot-producer.ts +182 -224
  142. package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
  143. package/plugins/kxm/src/vnext-runtime-store.ts +36 -2
  144. package/plugins/kxm/src/vnext-runtime-supervisor.ts +122 -5
  145. package/plugins/kxm/src/vnext-runtime.ts +14 -0
  146. package/plugins/kxm/src/workflow-manager.ts +392 -0
  147. package/plugins/kxm/src/workflow-tui.ts +255 -0
  148. package/plugins/kxm/src/workflow.ts +144 -0
  149. package/schemas/policy-draft/README.md +17 -0
  150. package/schemas/policy-draft/model.v2.schema.json +140 -0
  151. package/schemas/policy-draft/role.v2.schema.json +91 -0
  152. package/schemas/vnext/modes.schema.json +56 -0
  153. package/schemas/vnext/role.schema.json +76 -0
  154. package/schemas/vnext/run-event.schema.json +1 -0
  155. package/scripts/assignment-run.d.mts +1 -1
  156. package/scripts/assignment-run.mjs +44 -35
  157. package/scripts/check-generated.mjs +33 -9
  158. package/scripts/emit-codex-artifacts.mjs +255 -11
  159. package/scripts/harness-run.d.mts +12 -4
  160. package/scripts/harness-run.mjs +65 -17
  161. package/scripts/kxm-bump-version.mjs +146 -0
  162. package/scripts/kxm-hub.mjs +150 -2
  163. package/scripts/kxm-publish-npm.mjs +3 -1
  164. package/scripts/kxm-release-github.mjs +3 -1
  165. package/scripts/kxm.mjs +0 -0
  166. package/scripts/native-critic.d.mts +5 -0
  167. package/scripts/native-critic.mjs +60 -0
  168. package/.kxm/config/README.md +0 -5
  169. package/.kxm/config/agents.json +0 -43
  170. package/.kxm/config/env.example +0 -56
  171. package/.kxm/config/update.example.yaml +0 -9
  172. package/.kxm/config/workflows/fix.json +0 -160
  173. package/.kxm/config/workflows/jira-development.json +0 -116
  174. package/.kxm/config/workflows/provenance-quorum.json +0 -150
  175. package/.kxm/config/workflows/v04-dogfood.json +0 -72
@@ -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
- const context = params.contextTokens ?? params.tokensIn ?? 0;
88
- let selectedTier = row.tiers[0];
89
- for (const tier of row.tiers) {
90
- if (tier.upToContextTokens !== void 0 && tier.upToContextTokens !== null && context > tier.upToContextTokens) {
91
- continue;
92
- }
93
- selectedTier = tier;
94
- break;
95
- }
96
- const tokensIn = params.tokensIn ?? 0;
97
- const tokensOut = params.tokensOut ?? 0;
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
- contextTokens: v2?.contextTokens ?? r.tokensIn ?? null
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: (/* @__PURE__ */ new Date()).toISOString(),
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 sessionTokenRaw = env.KXM_SESSION_TOKEN?.trim();
1597
- if (sessionTokenRaw) {
1598
- const session = parseSessionToken(sessionTokenRaw);
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: "KXM_SESSION_TOKEN is malformed" };
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 (existsSync(current)) {
1880
+ if (existsSync2(current)) {
1654
1881
  if (i >= maxFiles) {
1655
1882
  try {
1656
- unlinkSync(current);
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 (existsSync(filePath)) {
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 && existsSync(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
- mkdirSync(dirname(filePath), { recursive: true });
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
  };