@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +30 -0
- package/docs/contributing/ci-and-release.md +6 -1
- package/docs/operations/runtime-sync.md +2 -1
- package/docs/operations/troubleshooting.md +4 -0
- package/docs/operations/upgrade.md +18 -3
- package/docs/operations.md +70 -7
- package/docs/reference/cli-reference.md +21 -6
- package/docs/reference/config-reference.md +28 -13
- package/docs/start/install.md +1 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/claude-hook.js +170 -36
- package/plugins/kxm/dist/cli.js +451 -132
- package/plugins/kxm/dist/extension.js +184 -53
- package/plugins/kxm/dist/mcp-server.js +97 -40
- package/plugins/kxm/dist/runtime-supervisor.js +240 -47
- package/plugins/kxm/dist/runtime.js +240 -47
- package/plugins/kxm/dist/server.js +9 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/kxm-definitions/SKILL.md +3 -2
- package/plugins/kxm/skills/kxm-harness-auth/SKILL.md +4 -2
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -4
- package/plugins/kxm/src/cli/hub.ts +42 -7
- package/plugins/kxm/src/cli/system.ts +3 -1
- package/plugins/kxm/src/cli/types.ts +5 -3
- package/plugins/kxm/src/cli.ts +2 -1
- package/plugins/kxm/src/engine.ts +105 -8
- package/plugins/kxm/src/hub-binding.ts +301 -55
- package/plugins/kxm/src/hub-identity.ts +17 -9
- package/plugins/kxm/src/kxm-update.ts +29 -6
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/project-config.ts +63 -1
- package/plugins/kxm/src/runtime-supervisor.ts +22 -4
- 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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
32529
|
-
|
|
32530
|
-
|
|
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
|
-
|
|
32675
|
+
return new HubBindingError(`malformed hub binding at ${file}`);
|
|
32536
32676
|
}
|
|
32537
32677
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32538
|
-
|
|
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
|
-
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
|
|
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
|
|
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
|
-
|
|
32711
|
-
|
|
32712
|
-
|
|
32713
|
-
|
|
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
|
-
|
|
32716
|
-
if (
|
|
32717
|
-
|
|
32718
|
-
|
|
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
|
-
|
|
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;
|
package/plugins/kxm/package.json
CHANGED
|
@@ -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
|
|
16
|
-
|
|
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
|
|
43
|
-
|
|
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.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
185
|
-
const ready = await
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)};
|
|
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
|
}
|
package/plugins/kxm/src/cli.ts
CHANGED
|
@@ -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
|
-
.
|
|
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));
|