@kontextmind/kxm 0.7.167 → 0.7.169

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 (35) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +30 -0
  3. package/docs/contributing/ci-and-release.md +6 -1
  4. package/docs/operations/runtime-sync.md +2 -1
  5. package/docs/operations/troubleshooting.md +4 -0
  6. package/docs/operations/upgrade.md +18 -3
  7. package/docs/operations.md +70 -7
  8. package/docs/reference/cli-reference.md +21 -6
  9. package/docs/reference/config-reference.md +28 -13
  10. package/docs/start/install.md +1 -1
  11. package/package.json +1 -1
  12. package/plugins/kxm/.claude-plugin/plugin.json +1 -1
  13. package/plugins/kxm/dist/claude-hook.js +170 -36
  14. package/plugins/kxm/dist/cli.js +451 -132
  15. package/plugins/kxm/dist/extension.js +184 -53
  16. package/plugins/kxm/dist/mcp-server.js +97 -40
  17. package/plugins/kxm/dist/runtime-supervisor.js +240 -47
  18. package/plugins/kxm/dist/runtime.js +240 -47
  19. package/plugins/kxm/dist/server.js +9 -0
  20. package/plugins/kxm/package.json +1 -1
  21. package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -2
  22. package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +4 -2
  23. package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -4
  24. package/plugins/kxm/src/cli/hub.ts +42 -7
  25. package/plugins/kxm/src/cli/system.ts +3 -1
  26. package/plugins/kxm/src/cli/types.ts +5 -3
  27. package/plugins/kxm/src/cli.ts +2 -1
  28. package/plugins/kxm/src/engine.ts +105 -8
  29. package/plugins/kxm/src/hub-binding.ts +301 -55
  30. package/plugins/kxm/src/hub-identity.ts +17 -9
  31. package/plugins/kxm/src/kxm-update.ts +29 -6
  32. package/plugins/kxm/src/mcp-server.ts +1 -1
  33. package/plugins/kxm/src/project-config.ts +63 -1
  34. package/plugins/kxm/src/runtime-supervisor.ts +22 -4
  35. package/scripts/kxm-publish-npm.mjs +47 -1
@@ -17431,7 +17431,28 @@ var RESOURCE_SCHEMA = Object.freeze({
17431
17431
  var IDENTIFIER2 = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
17432
17432
  var WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³]|conin\$|conout\$|clock\$)$/i;
17433
17433
  var BUILTIN_EXECUTORS = ["local", "ssh", "exe-dev"];
17434
- var BUILTIN_TOOL_PRESETS = ["coordinator", "read-only", "workspace-writer", "tests-writer"];
17434
+ var TOOL_PRESET_PROFILES = Object.freeze({
17435
+ coordinator: "read-only",
17436
+ "read-only": "read-only",
17437
+ "workspace-writer": "edit",
17438
+ "tests-writer": "edit"
17439
+ });
17440
+ var BUILTIN_TOOL_PRESETS = Object.keys(TOOL_PRESET_PROFILES);
17441
+ function toolPresetProfile(preset) {
17442
+ if (!Object.hasOwn(TOOL_PRESET_PROFILES, preset)) return void 0;
17443
+ return TOOL_PRESET_PROFILES[preset];
17444
+ }
17445
+ function toolPresetExceedsRoleMessage(preset, roleId, rolePermission, rolePreset) {
17446
+ const profile = toolPresetProfile(preset);
17447
+ if (profile !== "edit") return void 0;
17448
+ if (rolePermission === "read-only") {
17449
+ return `tools.preset ${preset} exceeds role ${roleId} permission read-only`;
17450
+ }
17451
+ if (rolePreset !== void 0 && toolPresetProfile(rolePreset) === "read-only") {
17452
+ return `tools.preset ${preset} exceeds role ${roleId} preset ${rolePreset}`;
17453
+ }
17454
+ return void 0;
17455
+ }
17435
17456
  var SECRET_VALUE_PATTERNS = [
17436
17457
  /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----/,
17437
17458
  /\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b/,
@@ -17831,6 +17852,15 @@ function objectValue(value) {
17831
17852
  function stringValue(value) {
17832
17853
  return typeof value === "string" ? value : void 0;
17833
17854
  }
17855
+ function roleById(roles, roleId) {
17856
+ const direct = roles.get(roleId);
17857
+ if (direct) return direct;
17858
+ const folded = roleId.toLocaleLowerCase("en-US");
17859
+ for (const role of roles.values()) {
17860
+ if ((role.id ?? "").toLocaleLowerCase("en-US") === folded) return role;
17861
+ }
17862
+ return void 0;
17863
+ }
17834
17864
  function numberValue(value, fallback) {
17835
17865
  return typeof value === "number" ? value : fallback;
17836
17866
  }
@@ -18345,10 +18375,29 @@ function validateBundle(project, repositories, agents, models, workflows, enviro
18345
18375
  if (executor && !executors.has(executor)) issues.push(issue3("reference", "executor_unknown", agent.logicalPath, `executor ${executor} is not registered`));
18346
18376
  const preset = stringValue(objectValue(agent.value.tools)?.preset);
18347
18377
  if (preset && !presets.has(preset)) issues.push(issue3("reference", "tool_preset_unknown", agent.logicalPath, `tool preset ${preset} is not registered`));
18378
+ const roleId = stringValue(agent.value.role);
18379
+ const role = roleId ? roleById(roles, roleId) : void 0;
18380
+ const rolePermission = stringValue(role?.value.permission);
18381
+ if (preset && role && (rolePermission === "edit" || rolePermission === "read-only")) {
18382
+ const message = toolPresetExceedsRoleMessage(
18383
+ preset,
18384
+ role.id ?? roleId ?? "role",
18385
+ rolePermission,
18386
+ stringValue(objectValue(role.value.tools)?.preset)
18387
+ );
18388
+ if (message) issues.push(issue3("semantic", "tool_preset_exceeds_role", agent.logicalPath, message));
18389
+ }
18348
18390
  for (const repositoryId of Object.keys(objectValue(agent.value.repositories) ?? {})) {
18349
18391
  if (!repositoryIds.has(repositoryId)) issues.push(issue3("reference", "repository_unknown", agent.logicalPath, `references unknown repository ${repositoryId}`));
18350
18392
  }
18351
18393
  }
18394
+ for (const role of roles.values()) {
18395
+ const permission = stringValue(role.value.permission);
18396
+ const rolePreset = stringValue(objectValue(role.value.tools)?.preset);
18397
+ if (permission === "read-only" && rolePreset && toolPresetProfile(rolePreset) === "edit") {
18398
+ issues.push(issue3("semantic", "tool_preset_exceeds_role", role.logicalPath, `tools.preset ${rolePreset} exceeds permission read-only`));
18399
+ }
18400
+ }
18352
18401
  validateModelReferences(models, issues);
18353
18402
  const defaultWorkflow = stringValue(project.value.defaultWorkflow) ?? "default";
18354
18403
  if (!lookupKxmResource(workflows.values(), defaultWorkflow, "workflow")) issues.push(issue3("reference", "default_workflow_unknown", project.logicalPath, `default workflow ${defaultWorkflow} does not exist`));
@@ -30125,6 +30174,68 @@ function unsupportedAgentStepTimeout(step, limitMs) {
30125
30174
  detail: "step timeoutMs is wider than project limits.agentStepTimeoutMs"
30126
30175
  };
30127
30176
  }
30177
+ function narrowerPermission(left, right) {
30178
+ return left === "read-only" || right === "read-only" ? "read-only" : "edit";
30179
+ }
30180
+ function stepRepositoryPermission(step) {
30181
+ return Object.values(step.repositories).some((access) => access === "write") ? "edit" : "read-only";
30182
+ }
30183
+ function stepAccessDetail(step) {
30184
+ const entries = Object.entries(step.repositories);
30185
+ if (entries.length === 0) return "none";
30186
+ return entries.map(([id, access]) => `${id} ${access}`).sort((left, right) => left < right ? -1 : left > right ? 1 : 0).join(", ");
30187
+ }
30188
+ function yamlToolsPreset(doc) {
30189
+ const tools = doc?.tools;
30190
+ if (!tools || typeof tools !== "object" || Array.isArray(tools)) return void 0;
30191
+ const preset = tools.preset;
30192
+ return typeof preset === "string" && preset.length > 0 ? preset : void 0;
30193
+ }
30194
+ function resolveToolDispatch(projectRoot, step, agentId) {
30195
+ const stepPermission = stepRepositoryPermission(step);
30196
+ if (step.kind !== "agent" && step.kind !== "moa") return { permission: stepPermission };
30197
+ const agentFile = findYamlBasename(join18(projectRoot, ".kxm", "agents"), agentId, "agent") ?? agentId;
30198
+ const agent = readYamlFile(join18(projectRoot, ".kxm", "agents", `${agentFile}.yaml`));
30199
+ const roleId = typeof agent?.role === "string" ? agent.role : "";
30200
+ if (!roleId) return { permission: stepPermission };
30201
+ const roleDoc = readYamlFile(join18(projectRoot, ".kxm", "roles", `${roleId}.yaml`));
30202
+ const rawPermission = roleDoc?.permission;
30203
+ const rolePermission = rawPermission === "edit" || rawPermission === "read-only" ? rawPermission : void 0;
30204
+ if (!rolePermission) return { permission: stepPermission };
30205
+ const rolePreset = yamlToolsPreset(roleDoc);
30206
+ const agentPreset = yamlToolsPreset(agent);
30207
+ const rolePresetProfile = rolePreset ? toolPresetProfile(rolePreset) : void 0;
30208
+ if (rolePreset && rolePresetProfile === "edit" && rolePermission === "read-only") {
30209
+ return {
30210
+ error: {
30211
+ reason: "step_unsupported",
30212
+ field: "tools.preset",
30213
+ detail: `tools.preset ${rolePreset} exceeds role ${roleId} permission read-only`
30214
+ }
30215
+ };
30216
+ }
30217
+ if (agentPreset) {
30218
+ const exceeds = toolPresetExceedsRoleMessage(agentPreset, roleId, rolePermission, rolePreset);
30219
+ if (exceeds) {
30220
+ return { error: { reason: "step_unsupported", field: "tools.preset", detail: exceeds } };
30221
+ }
30222
+ }
30223
+ let chain = rolePermission;
30224
+ if (rolePresetProfile) chain = narrowerPermission(chain, rolePresetProfile);
30225
+ const agentProfile = agentPreset ? toolPresetProfile(agentPreset) : void 0;
30226
+ if (agentProfile) chain = narrowerPermission(chain, agentProfile);
30227
+ const permission = narrowerPermission(chain, stepPermission);
30228
+ if (stepPermission === "edit" && permission === "read-only") {
30229
+ return {
30230
+ error: {
30231
+ reason: "step_unsupported",
30232
+ field: "permission",
30233
+ detail: `role permission ${rolePermission} resolves to ${chain}; step access ${stepAccessDetail(step)}`
30234
+ }
30235
+ };
30236
+ }
30237
+ return { permission };
30238
+ }
30128
30239
  function executingSingletonAttempt(plan, state) {
30129
30240
  const current = state.currentStep;
30130
30241
  if (!current || current.panel.order.length !== 1) return void 0;
@@ -30317,10 +30428,17 @@ function prepareDispatch(context, runId, producerId, dispatchSources) {
30317
30428
  if (unsupported) return { kind: "return", state, handoff: { ...unsupported, stepId } };
30318
30429
  const wideTimeout = unsupportedAgentStepTimeout(step, projectAgentStepTimeoutMs(context, run));
30319
30430
  if (wideTimeout) return { kind: "return", state, handoff: { ...wideTimeout, stepId } };
30431
+ const firstListed = step.assignments.allowedAgents[0];
30432
+ const firstAgentId = step.kind === "agent" || step.kind === "moa" ? firstListed ? firstListed : step.agent : void 0;
30433
+ if (firstAgentId) {
30434
+ const toolDispatch = resolveToolDispatch(context.projectRoot, step, firstAgentId);
30435
+ if ("error" in toolDispatch) {
30436
+ return { kind: "return", state, handoff: { ...toolDispatch.error, stepId } };
30437
+ }
30438
+ }
30320
30439
  let resolvedRoute;
30321
30440
  if (producerId !== "driver-simulated") {
30322
- const allowed = step.assignments.allowedAgents;
30323
- const agentId = allowed && allowed.length > 0 && allowed[0] ? allowed[0] : step.kind === "agent" || step.kind === "moa" ? step.agent : "coordinator";
30441
+ const agentId = firstAgentId ?? (step.kind === "agent" || step.kind === "moa" ? step.agent : "coordinator");
30324
30442
  const routeResult = resolveProducerRoute(context.projectRoot, step, agentId);
30325
30443
  if ("error" in routeResult) {
30326
30444
  return { kind: "return", state, handoff: { ...routeResult.error, stepId } };
@@ -30440,6 +30558,10 @@ function birthMember(context, input) {
30440
30558
  const born = folded.currentStep?.panel.order.length ?? 0;
30441
30559
  const allowed = input.step.assignments.allowedAgents;
30442
30560
  const agentId = allowed && allowed.length > born && allowed[born] ? allowed[born] : input.step.kind === "agent" || input.step.kind === "moa" ? input.step.agent : "coordinator";
30561
+ const toolDispatch = resolveToolDispatch(context.projectRoot, input.step, agentId);
30562
+ if ("error" in toolDispatch) {
30563
+ return { handoff: { ...toolDispatch.error, stepId: input.stepId } };
30564
+ }
30443
30565
  let resolvedRoute = input.resolvedRoute;
30444
30566
  if (!resolvedRoute && input.producerId && input.producerId !== "driver-simulated") {
30445
30567
  const routeResult = resolveProducerRoute(context.projectRoot, input.step, agentId);
@@ -30529,7 +30651,7 @@ function birthMember(context, input) {
30529
30651
  stepAttempt: input.stepAttempt,
30530
30652
  objective: promptText,
30531
30653
  allowedOutcomes: [...input.step.outcomes],
30532
- permissionCeiling: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only"
30654
+ permissionCeiling: toolDispatch.permission
30533
30655
  },
30534
30656
  acceptanceCriteria: input.step.requiredEvidence.map((ev) => ({
30535
30657
  id: ev.key,
@@ -30574,7 +30696,7 @@ function birthMember(context, input) {
30574
30696
  prompt: input.step.instructions ? `${input.step.instructions}
30575
30697
 
30576
30698
  ${generatedPrompt}` : generatedPrompt,
30577
- permission: Object.values(input.step.repositories).some((access) => access === "write") ? "edit" : "read-only",
30699
+ permission: toolDispatch.permission,
30578
30700
  ...(input.step.kind === "agent" || input.step.kind === "moa") && input.step.timeoutMs !== void 0 ? { timeoutMs: input.step.timeoutMs } : {},
30579
30701
  contextPacket,
30580
30702
  ...resolvedRoute?.effort !== void 0 ? { thinking: resolvedRoute.effort } : {},
@@ -32416,6 +32538,9 @@ function resolveUserStateRoot(env) {
32416
32538
  function hubBindingFile(env = process.env) {
32417
32539
  return join19(resolveUserStateRoot(env), "hub-binding.json");
32418
32540
  }
32541
+ function hubBindingCloudFile(env = process.env) {
32542
+ return join19(resolveUserStateRoot(env), "hub-binding.cloud.json");
32543
+ }
32419
32544
  function validateHubUrl(raw) {
32420
32545
  let parsed;
32421
32546
  try {
@@ -32525,33 +32650,37 @@ function isIsoTimestamp(value) {
32525
32650
  if (Number.isNaN(Date.parse(value))) return false;
32526
32651
  return value === new Date(value).toISOString();
32527
32652
  }
32528
- function readHubBinding(env = process.env) {
32529
- const file = hubBindingFile(env);
32530
- if (!existsSync16(file)) return void 0;
32653
+ var LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
32654
+ var BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
32655
+ function bindingFallbackWarning(file, reason) {
32656
+ return `hub binding at ${file} ${reason}; falling back to the local hub. Restart the Runtime supervisor, the hub, and workers after upgrading and before a cloud bind.`;
32657
+ }
32658
+ function writeBindingJson(file, value) {
32659
+ mkdirSync7(dirname9(file), { recursive: true, mode: 448 });
32660
+ const temporary = join19(dirname9(file), `.${file.endsWith("cloud.json") ? "hub-binding-cloud" : "hub-binding"}-${process.pid}.tmp`);
32661
+ try {
32662
+ writeFileSync8(temporary, `${JSON.stringify(value, null, 2)}
32663
+ `, { encoding: "utf8", mode: 384 });
32664
+ renameSync2(temporary, file);
32665
+ } finally {
32666
+ rmSync2(temporary, { force: true });
32667
+ }
32668
+ }
32669
+ function parseBindingObject(file) {
32670
+ if (!existsSync16(file)) return new HubBindingError(`missing hub binding at ${file}`);
32531
32671
  let parsed;
32532
32672
  try {
32533
32673
  parsed = JSON.parse(readFileSync14(file, "utf8"));
32534
32674
  } catch {
32535
- throw new HubBindingError(`malformed hub binding at ${file}`);
32675
+ return new HubBindingError(`malformed hub binding at ${file}`);
32536
32676
  }
32537
32677
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
32538
- throw new HubBindingError(`malformed hub binding at ${file}`);
32539
- }
32540
- const row = parsed;
32541
- const keys = Object.keys(row);
32542
- const allowed = /* @__PURE__ */ new Set(["schema", "url", "boundAt", "cloud", "tokenEnv", "tokenCommand"]);
32543
- const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
32544
- if (keys.some((key) => !allowed.has(key)) || !cloudish && keys.length !== 3 || row.schema !== HUB_BINDING_SCHEMA || typeof row.url !== "string" || typeof row.boundAt !== "string" || !isIsoTimestamp(row.boundAt)) {
32545
- throw new HubBindingError(`malformed hub binding at ${file}`);
32678
+ return new HubBindingError(`malformed hub binding at ${file}`);
32546
32679
  }
32547
- let url;
32548
- try {
32549
- url = validateHubUrl(row.url);
32550
- } catch {
32551
- throw new HubBindingError(`malformed hub binding at ${file}`);
32552
- }
32553
- if (!cloudish) return { schema: HUB_BINDING_SCHEMA, url, boundAt: row.boundAt };
32554
- if (row.cloud !== true) throw new HubBindingError(`malformed hub binding at ${file}`);
32680
+ return parsed;
32681
+ }
32682
+ function cloudRecordFromRow(file, row, url, boundAt) {
32683
+ if (row.cloud !== true) return void 0;
32555
32684
  let tokenEnv;
32556
32685
  let tokenCommand;
32557
32686
  if (row.tokenEnv !== void 0) {
@@ -32573,12 +32702,65 @@ function readHubBinding(env = process.env) {
32573
32702
  return {
32574
32703
  schema: HUB_BINDING_SCHEMA,
32575
32704
  url,
32576
- boundAt: row.boundAt,
32705
+ boundAt,
32577
32706
  cloud: true,
32578
32707
  ...tokenEnv ? { tokenEnv } : {},
32579
32708
  ...tokenCommand ? { tokenCommand } : {}
32580
32709
  };
32581
32710
  }
32711
+ function interpretBindingFile(file) {
32712
+ if (!existsSync16(file)) return {};
32713
+ const parsed = parseBindingObject(file);
32714
+ if (parsed instanceof HubBindingError) throw parsed;
32715
+ const keys = Object.keys(parsed);
32716
+ const unknown = keys.filter((key) => !BINDING_KEYS.has(key));
32717
+ if (parsed.schema !== HUB_BINDING_SCHEMA || unknown.length > 0) {
32718
+ return { warning: bindingFallbackWarning(file, `is newer than this build understands (${String(parsed.schema)})`) };
32719
+ }
32720
+ if (typeof parsed.url !== "string" || typeof parsed.boundAt !== "string" || !isIsoTimestamp(parsed.boundAt)) {
32721
+ throw new HubBindingError(`malformed hub binding at ${file}`);
32722
+ }
32723
+ let url;
32724
+ try {
32725
+ url = validateHubUrl(parsed.url);
32726
+ } catch {
32727
+ throw new HubBindingError(`malformed hub binding at ${file}`);
32728
+ }
32729
+ const cloudish = keys.some((key) => key === "cloud" || key === "tokenEnv" || key === "tokenCommand");
32730
+ if (!cloudish) {
32731
+ if (keys.length !== 3) throw new HubBindingError(`malformed hub binding at ${file}`);
32732
+ return { record: { schema: HUB_BINDING_SCHEMA, url, boundAt: parsed.boundAt } };
32733
+ }
32734
+ const record2 = cloudRecordFromRow(file, parsed, url, parsed.boundAt);
32735
+ if (!record2) throw new HubBindingError(`malformed hub binding at ${file}`);
32736
+ return { record: record2, legacyCloud: true };
32737
+ }
32738
+ function loadHubBinding(env = process.env) {
32739
+ const cloudFile = hubBindingCloudFile(env);
32740
+ const cloud = interpretBindingFile(cloudFile);
32741
+ if (cloud.warning && !cloud.record) {
32742
+ const local = existsSync16(hubBindingFile(env)) ? interpretBindingFile(hubBindingFile(env)) : {};
32743
+ if (local.record && !local.record.cloud) return { record: local.record, warning: cloud.warning };
32744
+ return { warning: cloud.warning };
32745
+ }
32746
+ if (cloud.record?.cloud) return { record: cloud.record, ...cloud.warning ? { warning: cloud.warning } : {} };
32747
+ const main = interpretBindingFile(hubBindingFile(env));
32748
+ if (main.legacyCloud && main.record) {
32749
+ writeBindingJson(cloudFile, main.record);
32750
+ rmSync2(hubBindingFile(env), { force: true });
32751
+ return {
32752
+ record: main.record,
32753
+ warning: bindingFallbackWarning(
32754
+ hubBindingFile(env),
32755
+ "stored a cloud bind in the pre-0.7.160 file"
32756
+ )
32757
+ };
32758
+ }
32759
+ return { ...main.record ? { record: main.record } : {}, ...main.warning ? { warning: main.warning } : {} };
32760
+ }
32761
+ function readHubBinding(env = process.env) {
32762
+ return loadHubBinding(env).record;
32763
+ }
32582
32764
 
32583
32765
  // plugins/kxm/src/hub-env.ts
32584
32766
  import { existsSync as existsSync18, mkdirSync as mkdirSync8, readFileSync as readFileSync16, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync9 } from "node:fs";
@@ -32654,15 +32836,6 @@ function resolveProjectIdentity(cwd, env = process.env, explicit) {
32654
32836
  if (fromPackage) return { project: fromPackage, source: "package.json", sourceLabel: "package.json name" };
32655
32837
  return { project: basename4(cwd), source: "directory", sourceLabel: "directory name" };
32656
32838
  }
32657
- function opReferenceFromTokenCommand(command) {
32658
- try {
32659
- const args = splitTokenCommand(command);
32660
- if (args[0] === "op" && args[1] === "read" && args[2] && isHubOpReference(args[2])) return args[2];
32661
- } catch {
32662
- return void 0;
32663
- }
32664
- return void 0;
32665
- }
32666
32839
  function defaultOpRead(reference, env) {
32667
32840
  if (!isHubOpReference(reference)) {
32668
32841
  throw new CloudTokenError("cloud_token_command_invalid", `refusing to resolve ${reference}; an op:// reference is required`);
@@ -32707,18 +32880,24 @@ function resolveKeyReference(ref, env, opRead = defaultOpRead) {
32707
32880
  }
32708
32881
  function keySourceFor(input) {
32709
32882
  const ref = input.endpoint?.key;
32710
- if (input.mode === "local" && input.env.KXM_AUTH_TOKEN?.trim()) return "env:KXM_AUTH_TOKEN";
32711
- if (ref?.env && input.env[ref.env]?.trim()) return `env:${ref.env}`;
32712
- if (ref?.op) return `op:${ref.op}`;
32713
- if (ref?.env) return `env:${ref.env}`;
32883
+ const envName = (name) => {
32884
+ if (!name) return void 0;
32885
+ return input.env[name]?.trim() ? `env:${name}` : void 0;
32886
+ };
32714
32887
  if (input.mode === "cloud") {
32715
- if (input.binding?.tokenEnv) return `env:${input.binding.tokenEnv}`;
32716
- if (input.binding?.tokenCommand) {
32717
- const op = opReferenceFromTokenCommand(input.binding.tokenCommand);
32718
- return op ? `op:${op}` : "command";
32719
- }
32888
+ const fromEnv2 = envName(ref?.env);
32889
+ if (fromEnv2) return fromEnv2;
32890
+ if (ref?.op) return `op:${ref.op}`;
32891
+ const fromBindingEnv = envName(input.binding?.tokenEnv);
32892
+ if (fromBindingEnv) return fromBindingEnv;
32893
+ if (input.binding?.tokenCommand) return "token-command";
32720
32894
  return "missing";
32721
32895
  }
32896
+ const fromAuth = envName("KXM_AUTH_TOKEN");
32897
+ if (fromAuth) return fromAuth;
32898
+ const fromEnv = envName(ref?.env);
32899
+ if (fromEnv) return fromEnv;
32900
+ if (ref?.op) return `op:${ref.op}`;
32722
32901
  if (input.hasSavedProjectToken?.(input.project)) return "hub-env";
32723
32902
  return "missing";
32724
32903
  }
@@ -33379,8 +33558,17 @@ async function syncKxmOutbox(eventStore, client, options = {}) {
33379
33558
  }
33380
33559
  }
33381
33560
  }
33382
- function runtimeHubClientFor(context, env) {
33383
- const serverUrl = env.KXM_SERVER_URL?.trim() || readHubBinding(env)?.url;
33561
+ function runtimeHubClientFor(context, env, onBindingWarning) {
33562
+ let recordUrl;
33563
+ try {
33564
+ const loaded = loadHubBinding(env);
33565
+ if (loaded.warning) onBindingWarning?.(loaded.warning);
33566
+ recordUrl = loaded.record?.url;
33567
+ } catch (error) {
33568
+ if (!(error instanceof HubBindingError)) throw error;
33569
+ onBindingWarning?.(`${error.message}; falling back to the local hub so sync can continue`);
33570
+ }
33571
+ const serverUrl = env.KXM_SERVER_URL?.trim() || recordUrl;
33384
33572
  if (!serverUrl) return void 0;
33385
33573
  const project = context.projectId;
33386
33574
  const authToken = resolveClientHubAuthToken(env, project);
@@ -33908,6 +34096,7 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
33908
34096
  else logger.info(entry);
33909
34097
  };
33910
34098
  let syncing = false;
34099
+ let bindingWarning;
33911
34100
  const syncTimer = setInterval(() => {
33912
34101
  if (syncing || stopping) return;
33913
34102
  syncing = true;
@@ -33925,7 +34114,11 @@ async function startKxmRuntimeSupervisorInner(paths, requestedPortOption, now, e
33925
34114
  consecutiveFailures: 0
33926
34115
  };
33927
34116
  try {
33928
- const client = runtimeHubClientFor(context, env);
34117
+ const client = runtimeHubClientFor(context, env, (message) => {
34118
+ if (bindingWarning === message) return;
34119
+ bindingWarning = message;
34120
+ logger.warn({ event: "hub_binding_fallback", message });
34121
+ });
33929
34122
  if (!client) {
33930
34123
  recordSyncStatus(context, { ...base, state: "no_hub" });
33931
34124
  continue;
@@ -15542,6 +15542,13 @@ var RESOURCE_SCHEMA = Object.freeze({
15542
15542
  workflow: { identity: "kxm.workflow.v1", file: "workflow.schema.json" },
15543
15543
  "gate-registry": { identity: "kxm.gate-registry.v1", file: "gate-registry.schema.json" }
15544
15544
  });
15545
+ var TOOL_PRESET_PROFILES = Object.freeze({
15546
+ coordinator: "read-only",
15547
+ "read-only": "read-only",
15548
+ "workspace-writer": "edit",
15549
+ "tests-writer": "edit"
15550
+ });
15551
+ var BUILTIN_TOOL_PRESETS = Object.keys(TOOL_PRESET_PROFILES);
15545
15552
  function compareCodeUnits(left, right) {
15546
15553
  return left < right ? -1 : left > right ? 1 : 0;
15547
15554
  }
@@ -23491,6 +23498,8 @@ var CloudTokenError = class extends Error {
23491
23498
  this.code = code;
23492
23499
  }
23493
23500
  };
23501
+ var LEGACY_BINDING_KEYS = ["schema", "url", "boundAt"];
23502
+ var BINDING_KEYS = /* @__PURE__ */ new Set([...LEGACY_BINDING_KEYS, "cloud", "tokenEnv", "tokenCommand"]);
23494
23503
 
23495
23504
  // plugins/kxm/src/hub-identity.ts
23496
23505
  var OP_TIMEOUT_MS = 15e3;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin",
3
- "version": "0.7.167",
3
+ "version": "0.7.169",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,8 +12,9 @@ or permission to execute an assignment.
12
12
 
13
13
  Dispatch reads `.kxm/roles/*.yaml` and `.kxm/models/*.yaml`. An agent binds
14
14
  a role with `role:` and does not pin `harness` or `model`. An agent
15
- `tools.preset` must be a registered preset. Narrowing that preset against
16
- the role preset is not validated yet (P3). `kxm routes` prints
15
+ `tools.preset` must be a registered preset and may only narrow the role.
16
+ A wider preset is refused at load (`tool_preset_exceeds_role`) and again
17
+ at dispatch (`step_unsupported`). `kxm routes` prints
17
18
  admission from `.kxm/routes.yaml` and membership from the role files.
18
19
  The developer runner builds its policy object from those same files at
19
20
  `refs/remotes/origin/main`.
@@ -39,8 +39,10 @@ still blocks the tools.
39
39
  | `kxm update [harness] --extensions` | Update only extensions and plugins (Pi packages, the Claude kxm plugin) |
40
40
  | `kxm update [harness] --models` | Refresh model catalogs where the harness supports it |
41
41
 
42
- - `kxm update --kxm` downloads the GitHub release tarball by default, so it
43
- needs an authenticated `gh`.
42
+ - `kxm update --kxm` downloads the GitHub release tarball by default. It uses
43
+ `gh` when that CLI answers `gh --version`, and otherwise curl with the same
44
+ sha256 check. A host with neither can follow the curl commands in
45
+ docs/operations/upgrade.md.
44
46
  - `kxm update claude --extensions` updates only a user-scope plugin install;
45
47
  its dry run prints `claude extensions: would claude plugin update kxm -y`.
46
48
  A project-scope install is the operator's `claude plugin update kxm@kxm --scope project`.
@@ -77,10 +77,15 @@ kxm restore .kxm/backups/pre-upgrade/manifest.json
77
77
  `hub_bind_unauthenticated` unless a credential for the current project
78
78
  resolves from `KXM_AUTH_TOKEN` or the persisted `hub-env.json`.
79
79
  - `kxm hub bind --cloud` is the bind for a loopback SSH forward of a remote
80
- hub. It never falls back to this machine's hub-env token. Pass
81
- `--token-env KXMD_HUB_TOKEN`, `--key-op op://Private/kxmd/project-token`,
82
- or `--token-command "op read op://..."`. The non-secret fields are written
83
- to `.kxm/config.yaml`. See docs/operations.md, section "Cross-box peer attach".
80
+ hub (`http://127.0.0.1:17331`). `https://hub.kxmd.dev` is an Authentik login
81
+ page, not a client URL. Bind refuses an HTML 200 or a 30x (`hub_not_kxm`)
82
+ unless `--force` is set. It never falls back to this machine's hub-env
83
+ token. Pass `--token-env KXMD_HUB_TOKEN`, `--key-op op://Private/kxmd/project-token`,
84
+ or `--token-command "op read op://..."`. `--token-command` is stored only in
85
+ the machine-wide `hub-binding.cloud.json`, not in `.kxm/config.yaml`.
86
+ `kxm hub view` names the source that would supply the token. See
87
+ docs/operations.md, section "Cross-box peer attach". Restart the supervisor,
88
+ hub, and workers after an upgrade and before a cloud bind.
84
89
  - Hub PID claims record the wrapper and server child PID. A dead wrapper's
85
90
  claim is reclaimed automatically, an orphaned server is terminated first,
86
91
  and `kxm hub stop` recovers such orphans directly.
@@ -13,8 +13,10 @@ import {
13
13
  HUB_BINDING_SCHEMA,
14
14
  CloudTokenError,
15
15
  HubBindingError,
16
+ hubBindingCloudFile,
16
17
  hubBindingFile,
17
18
  probeHubHealth,
19
+ readHubEndpoint,
18
20
  hubBindingScope,
19
21
  effectiveHubBindingScope,
20
22
  isCloudTokenEnvName,
@@ -181,8 +183,17 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
181
183
  }
182
184
  if (!(error instanceof HubBindingError)) throw error;
183
185
  }
184
- const health = await hubGet(`${runtime.serverUrl}/health`, runtime.fetchImpl, headers);
185
- const ready = await hubGet(`${runtime.serverUrl}/ready`, runtime.fetchImpl, headers);
186
+ const health = await readHubEndpoint(runtime.serverUrl, "health", runtime.fetchImpl, headers ? { headers } : {});
187
+ const ready = await readHubEndpoint(runtime.serverUrl, "ready", runtime.fetchImpl, headers ? { headers } : {});
188
+ const proxyKind = [health.kind, ready.kind].find((kind) => kind === "redirect" || kind === "auth_proxy");
189
+ const notHub = !proxyKind && (health.kind === "not_hub" || ready.kind === "not_hub");
190
+ const probeNote = proxyKind === "redirect"
191
+ ? " · auth proxy or redirect"
192
+ : proxyKind === "auth_proxy"
193
+ ? " · auth proxy (HTML login page)"
194
+ : notHub
195
+ ? " · not a kxm hub"
196
+ : "";
186
197
  // Scope on the status line deliberately: "attached across a network" and "attached on
187
198
  // this box" are otherwise indistinguishable, and only one of them ships a token.
188
199
  // Scope is a property of the URL actually contacted, not of whichever file the
@@ -210,7 +221,7 @@ export async function cmdStatus(runtime: Runtime): Promise<number> {
210
221
  ready: ready.body,
211
222
  };
212
223
  print(runtime.io, runtime.json, payload,
213
- `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}`);
224
+ `hub health=${health.ok} ready=${ready.ok} · ${effectiveScope} hub${overridden ? " (KXM_SERVER_URL)" : ""} · mode=${identity.mode} project=${identity.project} (${connection.sourceLabel}) key=${identity.keySource}${probeNote}`);
214
225
  return payload.ok ? 0 : 1;
215
226
  }
216
227
 
@@ -314,6 +325,7 @@ export interface HubBindOptions {
314
325
  tokenEnv?: string | undefined;
315
326
  tokenCommand?: string | undefined;
316
327
  keyOp?: string | undefined;
328
+ force?: boolean | undefined;
317
329
  }
318
330
 
319
331
  export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubBindOptions = {}): Promise<number> {
@@ -444,7 +456,7 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
444
456
  return 2;
445
457
  }
446
458
  }
447
- const file = hubBindingFile(runtime.env);
459
+ const file = cloud ? hubBindingCloudFile(runtime.env) : hubBindingFile(runtime.env);
448
460
  const bindingCommand = tokenCommand ?? (cloud && keyOp ? `op read ${keyOp}` : undefined);
449
461
  const cloudFields = cloud
450
462
  ? { cloud: true as const, ...(tokenEnv ? { tokenEnv } : {}), ...(bindingCommand ? { tokenCommand: bindingCommand } : {}) }
@@ -471,6 +483,27 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
471
483
  );
472
484
  return 0;
473
485
  }
486
+ const probe = await probeHubHealth(url, runtime.fetchImpl);
487
+ const acceptable = probe.kind === "hub" || probe.kind === "timeout" || probe.kind === "unreachable";
488
+ if (!acceptable && options.force !== true) {
489
+ print(
490
+ runtime.io,
491
+ runtime.json,
492
+ {
493
+ ok: false,
494
+ command: "hub bind",
495
+ error: "hub_not_kxm",
496
+ url,
497
+ scope,
498
+ kind: probe.kind,
499
+ status: probe.status,
500
+ detail: probe.detail,
501
+ nextAction: "bind_a_kxm_hub_or_pass_force",
502
+ },
503
+ `refusing to bind ${url}: ${probe.detail}; no binding was written. Pass --force to bind anyway`,
504
+ );
505
+ return 2;
506
+ }
474
507
  try {
475
508
  writeHubEndpoint(runtime.dirs.workdir, {
476
509
  mode: cloud ? "cloud" : "local",
@@ -491,9 +524,11 @@ export async function cmdHubBind(runtime: Runtime, rawUrl: string, options: HubB
491
524
  throw error;
492
525
  }
493
526
  writeHubBinding({ ...record, boundAt: new Date().toISOString() }, runtime.env);
494
- const { health, probeMs } = await probeHubHealth(url, runtime.fetchImpl);
495
- print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health, probeMs, ...cloudFields },
496
- `bound hub ${url} · ${scope} · ${formatHubBindHealth(health)}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
527
+ const healthText = acceptable
528
+ ? formatHubBindHealth(probe.health)
529
+ : `${probe.detail} (--force)`;
530
+ print(runtime.io, runtime.json, { ok: true, command: "hub bind", url, scope, file, configFile, project: identity.project, health: probe.health, probeMs: probe.probeMs, kind: probe.kind, ...cloudFields },
531
+ `bound hub ${url} · ${scope} · ${healthText}${cloud ? cloudNote : scope === "remote" ? " · token leaves this machine" : ""}`);
497
532
  return 0;
498
533
  }
499
534
 
@@ -75,6 +75,7 @@ import {
75
75
  import {
76
76
  installedKxmVersion,
77
77
  fetchLatestKxmVersion,
78
+ ghReleaseDownloadAvailable,
78
79
  kxmReleaseAssetName,
79
80
  noticeFromVersions,
80
81
  planKxmPackageUpdate,
@@ -159,7 +160,8 @@ export function applyKxmPackageUpdate(runtime: Runtime, notice: KxmUpdateNotice)
159
160
  }
160
161
  const releaseDir = mkdtempSync(join(tmpdir(), "kxm-pkg-update-"));
161
162
  try {
162
- const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset);
163
+ const downloader = notice.source === "github" && !ghReleaseDownloadAvailable(runtime.env) ? "curl" : "gh";
164
+ const planned = planKxmPackageUpdate(notice.source, notice.latest, releaseDir, notice.asset, downloader);
163
165
  if (runtime.dryRun) {
164
166
  return { ok: true, detail: planned.map(formatPackageUpdateStep).join(" && ") };
165
167
  }
@@ -10,7 +10,7 @@ import {
10
10
  HubBindingError,
11
11
  effectiveHubBindingScope,
12
12
  hubBindingFile,
13
- readHubBinding,
13
+ loadHubBinding,
14
14
  } from "../hub-binding.ts";
15
15
  import { describeHubConnection } from "../hub-identity.ts";
16
16
  import type { InstallProbe } from "../kxm-install-kind.ts";
@@ -280,10 +280,12 @@ export function runtimeFrom(ctx: CliContext, command: Command): Runtime {
280
280
  const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
281
281
  let boundHubUrl: string | undefined;
282
282
  try {
283
- boundHubUrl = readHubBinding(ctx.env)?.url;
283
+ const loaded = loadHubBinding(ctx.env);
284
+ if (loaded.warning) ctx.io.stderr(`kxm: ${loaded.warning}\n`);
285
+ boundHubUrl = loaded.record?.url;
284
286
  } catch (error) {
285
287
  if (error instanceof HubBindingError) {
286
- ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again\n`);
288
+ ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; falling back to the local hub. Run kxm hub bind <url> again\n`);
287
289
  } else {
288
290
  throw error;
289
291
  }
@@ -1374,7 +1374,8 @@ function createProgram(ctx: CliContext, result: { code: number }, argv: readonly
1374
1374
  .option("--token-env <name>", "Environment variable that holds the hub token; the token is not stored")
1375
1375
  .option("--token-command <command>", "Program that prints the hub token; the token is not stored")
1376
1376
  .option("--key-op <ref>", "op:// reference for the hub token; resolved with op read, never stored")
1377
- .action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string; keyOp?: string }) {
1377
+ .option("--force", "Bind even when the URL is not a kxm hub")
1378
+ .action(async function hubBindAction(this: Command, url: string, options: { cloud?: boolean; tokenEnv?: string; tokenCommand?: string; keyOp?: string; force?: boolean }) {
1378
1379
  result.code = await cmdHubBind(runtimeFrom(ctx, this), url, options);
1379
1380
  });
1380
1381
  addGlobalOptions(hub.command("unbind").description("Remove this machine's hub binding")).action(bind(cmdHubUnbind));