@kody-ade/kody-engine 0.4.304 → 0.4.306

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/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.304",
18
+ version: "0.4.306",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -1914,6 +1914,7 @@ function parseCapabilityConfig(raw) {
1914
1914
  mentions: stringList(raw.mentions).map((m) => m.replace(/^@/, "")),
1915
1915
  tools,
1916
1916
  capabilityTools: tools,
1917
+ capabilityToolMode: parseCapabilityToolMode(raw.capabilityToolMode),
1917
1918
  implementations,
1918
1919
  executables: stringList(raw.executables),
1919
1920
  role: stringField(raw.role),
@@ -1924,6 +1925,11 @@ function parseCapabilityConfig(raw) {
1924
1925
  workflow: parseWorkflow(raw.workflow)
1925
1926
  };
1926
1927
  }
1928
+ function parseCapabilityToolMode(raw) {
1929
+ if (raw === void 0 || raw === null || raw === "") return void 0;
1930
+ if (raw === "lock" || raw === "append") return raw;
1931
+ return void 0;
1932
+ }
1927
1933
  function parseCapabilityBody(raw, slug2) {
1928
1934
  const trimmed = raw.trim();
1929
1935
  const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
@@ -2414,7 +2420,8 @@ __export(capabilityMcp_exports, {
2414
2420
  parseCapabilityTrustMode: () => parseCapabilityTrustMode,
2415
2421
  readCapabilityTrustMode: () => readCapabilityTrustMode,
2416
2422
  readCheckRuns: () => readCheckRuns,
2417
- readThread: () => readThread
2423
+ readThread: () => readThread,
2424
+ startCapability: () => startCapability
2418
2425
  });
2419
2426
  import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2420
2427
  import { z as z3 } from "zod";
@@ -2663,6 +2670,9 @@ function dispatchWorkflow(workflowFile, capability, issueNumber, repoSlug) {
2663
2670
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
2664
2671
  }
2665
2672
  }
2673
+ function startCapability(workflowFile, name, issue, repoSlug) {
2674
+ return dispatchWorkflow(workflowFile, name, issue, repoSlug);
2675
+ }
2666
2676
  function expectedDispatchTarget(capability) {
2667
2677
  const route = resolveCapabilityAction(capability);
2668
2678
  if (!route) return null;
@@ -2835,9 +2845,31 @@ function capabilityToolDefinitions(opts) {
2835
2845
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2836
2846
  }
2837
2847
  };
2848
+ const startCapabilityTool = {
2849
+ name: "start_capability",
2850
+ description: "Start a known Kody capability on an issue or PR through workflow_dispatch. Use this instead of shelling out to `gh workflow run` or posting bot-authored `@kody` command comments. E.g. start_capability({name:'qa-engineer', issue:<n>}). Returns {ok} or {ok:false,error}.",
2851
+ inputSchema: {
2852
+ name: z3.string().min(1).describe("Capability action to start (e.g. 'qa-engineer', 'run', 'sync')."),
2853
+ issue: z3.number().int().positive().optional().describe("Issue or PR number forwarded as issue_number."),
2854
+ issueNumber: z3.number().int().positive().optional().describe("Deprecated alias for issue.")
2855
+ },
2856
+ handler: async (args) => {
2857
+ const name = String(args.name ?? "");
2858
+ const issue = Number(args.issue ?? args.issueNumber);
2859
+ if (!Number.isFinite(issue) || issue <= 0) {
2860
+ return { content: [{ type: "text", text: "Start failed: `issue` is required and must be a positive number." }] };
2861
+ }
2862
+ if (isDispatchGated(name, readCapabilityTrustMode(opts.state, opts.repoSlug, opts.capabilitySlug))) {
2863
+ return { content: [{ type: "text", text: trustRefusal(opts.capabilitySlug) }] };
2864
+ }
2865
+ const result = startCapability(workflowFile, name, issue, opts.repoSlug);
2866
+ const text = result.ok ? `Started capability \`${name}\` on #${issue} via workflow_dispatch.` : `Start failed for capability \`${name}\` on #${issue}: ${result.error}`;
2867
+ return { content: [{ type: "text", text }] };
2868
+ }
2869
+ };
2838
2870
  const dispatchTool = {
2839
2871
  name: "dispatch_workflow",
2840
- description: "Dispatch a kody.yml workflow_dispatch run for a capability action against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({capability:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
2872
+ description: "Legacy alias for start_capability. Dispatches a kody.yml workflow_dispatch run for a capability action against an issue. Prefer start_capability({name:'run', issue:<n>}). Returns {ok} or {ok:false,error}.",
2841
2873
  inputSchema: {
2842
2874
  capability: z3.string().min(1).optional().describe("Capability action to run (e.g. 'run')."),
2843
2875
  executable: z3.string().min(1).optional().describe("Deprecated alias for capability."),
@@ -2849,7 +2881,7 @@ function capabilityToolDefinitions(opts) {
2849
2881
  if (isDispatchGated(capability, readCapabilityTrustMode(opts.state, opts.repoSlug, opts.capabilitySlug))) {
2850
2882
  return { content: [{ type: "text", text: trustRefusal(opts.capabilitySlug) }] };
2851
2883
  }
2852
- const result = dispatchWorkflow(workflowFile, capability, issueNumber, opts.repoSlug);
2884
+ const result = startCapability(workflowFile, capability, issueNumber, opts.repoSlug);
2853
2885
  const text = result.ok ? `Dispatched capability \`${capability}\` on #${issueNumber} via workflow_dispatch.` : `Dispatch failed for capability \`${capability}\` on #${issueNumber}: ${result.error}`;
2854
2886
  return { content: [{ type: "text", text }] };
2855
2887
  }
@@ -2869,6 +2901,7 @@ function capabilityToolDefinitions(opts) {
2869
2901
  readThreadTool,
2870
2902
  ensureIssueTool,
2871
2903
  ensureCommentTool,
2904
+ startCapabilityTool,
2872
2905
  dispatchTool,
2873
2906
  ...cmsTools
2874
2907
  ];
@@ -2918,6 +2951,7 @@ var init_capabilityMcp = __esm({
2918
2951
  "read_thread",
2919
2952
  "ensure_issue",
2920
2953
  "ensure_comment",
2954
+ "start_capability",
2921
2955
  "dispatch_workflow",
2922
2956
  ...DASHBOARD_CMS_MCP_TOOL_NAMES
2923
2957
  ];
@@ -4264,6 +4298,7 @@ function loadProfile(profilePath) {
4264
4298
  describe: typeof r.describe === "string" ? r.describe : base.describe,
4265
4299
  agent: typeof r.agent === "string" && r.agent.trim() ? r.agent.trim() : base.agent,
4266
4300
  capabilityTools: parseStringArray2(r.capabilityTools ?? r.capabilityTools ?? r.tools) ?? base.capabilityTools,
4301
+ capabilityToolMode: parseCapabilityToolMode2(profilePath, r.capabilityToolMode) ?? base.capabilityToolMode,
4267
4302
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : base.mentions
4268
4303
  };
4269
4304
  }
@@ -4319,6 +4354,7 @@ function loadProfile(profilePath) {
4319
4354
  agent: typeof r.agent === "string" && r.agent.trim() ? r.agent.trim() : void 0,
4320
4355
  // Locked-toolbox palette + mentions from folder-capability profile metadata.
4321
4356
  capabilityTools: parseStringArray2(r.capabilityTools ?? r.capabilityTools ?? r.tools),
4357
+ capabilityToolMode: parseCapabilityToolMode2(profilePath, r.capabilityToolMode),
4322
4358
  mentions: Array.isArray(r.mentions) ? r.mentions.map((m) => String(m).trim()).filter(Boolean) : void 0,
4323
4359
  role,
4324
4360
  kind,
@@ -4360,12 +4396,7 @@ function loadProfile(profilePath) {
4360
4396
  const preNames = new Set(profile.scripts.preflight.map((e) => e.script).filter(Boolean));
4361
4397
  const postNames = profile.scripts.postflight.map((e) => e.script).filter(Boolean);
4362
4398
  const needsState = postNames.includes("writeJobStateFile") || postNames.includes("parseJobStateFromAgentResult");
4363
- const STATE_LOADERS = [
4364
- "loadCapabilityState",
4365
- "loadJobFromFile",
4366
- "runTickScript",
4367
- "runScheduledExecutableTick"
4368
- ];
4399
+ const STATE_LOADERS = ["loadCapabilityState", "loadJobFromFile", "runTickScript", "runScheduledExecutableTick"];
4369
4400
  if (needsState && !STATE_LOADERS.some((s) => preNames.has(s))) {
4370
4401
  throw new ProfileError(
4371
4402
  profilePath,
@@ -4375,6 +4406,11 @@ function loadProfile(profilePath) {
4375
4406
  profile.subagentTemplates = captureSubagentTemplates(profile);
4376
4407
  return profile;
4377
4408
  }
4409
+ function parseCapabilityToolMode2(profilePath, raw) {
4410
+ if (raw === void 0 || raw === null || raw === "") return void 0;
4411
+ if (raw === "lock" || raw === "append") return raw;
4412
+ throw new ProfileError(profilePath, `"capabilityToolMode" must be "lock" or "append"`);
4413
+ }
4378
4414
  function readPromptTemplates(dir) {
4379
4415
  const out = {};
4380
4416
  const read = (p) => {
@@ -4694,6 +4730,7 @@ var init_profile = __esm({
4694
4730
  "capabilityTools",
4695
4731
  "capabilityTools",
4696
4732
  "tools",
4733
+ "capabilityToolMode",
4697
4734
  "mentions",
4698
4735
  "stage",
4699
4736
  "readsFrom",
@@ -13074,10 +13111,16 @@ var init_loadCapabilityState = __esm({
13074
13111
  `loadCapabilityState: capability '${slug2}' declared capabilityTools not in the kody-capability palette: ${unknown.join(", ")}. Available: ${[...CAPABILITY_MCP_TOOL_NAMES].join(", ")}`
13075
13112
  );
13076
13113
  }
13114
+ const mode = profile.capabilityToolMode ?? "lock";
13077
13115
  ctx.data.capabilityTools = declaredTools;
13116
+ ctx.data.capabilityToolMode = mode;
13078
13117
  ctx.data.capabilityToolsList = declaredTools.map((name) => `- \`${name}\``).join("\n");
13079
13118
  ctx.data.capabilityOperatorMention = mentions;
13080
13119
  const mcpToolNames = declaredTools.map((name) => `mcp__kody-capability__${name}`);
13120
+ if (mode === "append") {
13121
+ profile.claudeCode.tools = [.../* @__PURE__ */ new Set([...profile.claudeCode.tools ?? [], ...mcpToolNames])];
13122
+ return;
13123
+ }
13081
13124
  profile.claudeCode.tools = [...mcpToolNames, "mcp__kody-submit__submit_state"];
13082
13125
  profile.claudeCode.enableSubmitTool = true;
13083
13126
  }
@@ -13289,8 +13332,8 @@ var CAPABILITY_TOOL_PALETTE2, loadJobFromFile;
13289
13332
  var init_loadJobFromFile = __esm({
13290
13333
  "src/scripts/loadJobFromFile.ts"() {
13291
13334
  "use strict";
13292
- init_capabilityMcp();
13293
13335
  init_agents();
13336
+ init_capabilityMcp();
13294
13337
  init_registry();
13295
13338
  init_jobState();
13296
13339
  CAPABILITY_TOOL_PALETTE2 = new Set(CAPABILITY_MCP_TOOL_NAMES);
@@ -13304,9 +13347,7 @@ var init_loadJobFromFile = __esm({
13304
13347
  }
13305
13348
  const capability = resolveCapabilityFolder(slug2, path33.join(ctx.cwd, jobsDir));
13306
13349
  if (!capability) {
13307
- throw new Error(
13308
- `loadJobFromFile: capability folder not found or incomplete: ${path33.join(ctx.cwd, jobsDir, slug2)}`
13309
- );
13350
+ throw new Error(`loadJobFromFile: capability folder not found or incomplete: ${path33.join(ctx.cwd, jobsDir, slug2)}`);
13310
13351
  }
13311
13352
  const { title, body, config } = capability;
13312
13353
  const mentions = (config.mentions ?? []).map((login) => `@${login}`).join(" ");
@@ -13351,10 +13392,16 @@ var init_loadJobFromFile = __esm({
13351
13392
  );
13352
13393
  }
13353
13394
  const mcpToolNames = declaredTools.map((name) => `mcp__kody-capability__${name}`);
13354
- profile.claudeCode.tools = [...mcpToolNames, "mcp__kody-submit__submit_state"];
13395
+ const mode = config.capabilityToolMode ?? "lock";
13396
+ if (mode === "append") {
13397
+ profile.claudeCode.tools = [.../* @__PURE__ */ new Set([...profile.claudeCode.tools ?? [], ...mcpToolNames])];
13398
+ } else {
13399
+ profile.claudeCode.tools = [...mcpToolNames, "mcp__kody-submit__submit_state"];
13400
+ ctx.data.promptTemplate = "prompts/locked.md";
13401
+ }
13355
13402
  ctx.data.capabilityTools = declaredTools;
13403
+ ctx.data.capabilityToolMode = mode;
13356
13404
  ctx.data.capabilityOperatorMention = mentions;
13357
- ctx.data.promptTemplate = "prompts/locked.md";
13358
13405
  ctx.data.capabilityToolsList = declaredTools.map((name) => `- \`${name}\``).join("\n");
13359
13406
  }
13360
13407
  };
@@ -15826,165 +15873,6 @@ var init_runFlow = __esm({
15826
15873
  }
15827
15874
  });
15828
15875
 
15829
- // src/scripts/previewBuildHelpers.ts
15830
- import { createDecipheriv, createHash as createHash3 } from "crypto";
15831
- function shortHash(s) {
15832
- return createHash3("sha256").update(s).digest("hex").slice(0, 6);
15833
- }
15834
- function previewAppName(repo, pr) {
15835
- const [owner, name] = repo.split("/");
15836
- if (!owner || !name) {
15837
- throw new Error(`invalid repo "${repo}", expected "owner/name"`);
15838
- }
15839
- return `kp-${shortHash(owner)}-${shortHash(name)}-pr-${pr}`;
15840
- }
15841
- function basePreviewAppName(repo) {
15842
- const [owner, name] = repo.split("/");
15843
- if (!owner || !name) {
15844
- throw new Error(`invalid repo "${repo}", expected "owner/name"`);
15845
- }
15846
- return `kp-${shortHash(owner)}-${shortHash(name)}-base`;
15847
- }
15848
- function decryptVaultPayload(payload, keyRaw) {
15849
- const parts = payload.split(":");
15850
- if (parts.length !== 4 || parts[0] !== "v1") {
15851
- throw new Error("invalid vault payload format");
15852
- }
15853
- const [, ivB64, ctB64, tagB64] = parts;
15854
- const key = /^[0-9a-fA-F]{64}$/.test(keyRaw) ? Buffer.from(keyRaw, "hex") : Buffer.from(keyRaw, "base64");
15855
- if (key.length !== 32) {
15856
- throw new Error("KODY_MASTER_KEY must decode to 32 bytes");
15857
- }
15858
- const iv = Buffer.from(ivB64, "base64");
15859
- const ct = Buffer.from(ctB64, "base64");
15860
- const tag = Buffer.from(tagB64, "base64");
15861
- const decipher = createDecipheriv("aes-256-gcm", key, iv);
15862
- decipher.setAuthTag(tag);
15863
- return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
15864
- }
15865
- function buildEnvFromVault(doc) {
15866
- const buildEnv = {};
15867
- for (const [name, entry] of Object.entries(doc.secrets ?? {})) {
15868
- if (!entry?.value) continue;
15869
- if (NEVER_PASS_TO_BUILD.has(name)) continue;
15870
- buildEnv[name] = entry.value;
15871
- }
15872
- const raw = doc.secrets?.KODY_PREVIEW_BUILD_MODE?.value;
15873
- const buildMode = raw?.toLowerCase().trim() === "dev" ? "dev" : "prod";
15874
- return { buildEnv, buildMode };
15875
- }
15876
- function formatPreviewComment(args) {
15877
- const url = `https://${args.appName}.fly.dev`;
15878
- return [
15879
- "<!-- kody-fly-preview -->",
15880
- `\u2705 **Preview ready:** ${url}`,
15881
- "",
15882
- `<sub>App: \`${args.appName}\` \xB7 Commit: \`${args.ref.slice(0, 7)}\` \xB7 Updated: ${args.nowIso}</sub>`
15883
- ].join("\n");
15884
- }
15885
- function defaultImageTag(repo, ref) {
15886
- return createHash3("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
15887
- }
15888
- var NEVER_PASS_TO_BUILD;
15889
- var init_previewBuildHelpers = __esm({
15890
- "src/scripts/previewBuildHelpers.ts"() {
15891
- "use strict";
15892
- NEVER_PASS_TO_BUILD = /* @__PURE__ */ new Set([
15893
- "FLY_API_TOKEN",
15894
- "FLY_ORG_SLUG",
15895
- "FLY_DEFAULT_REGION",
15896
- "KODY_MASTER_KEY",
15897
- // Preview-config knob; consumed by the dispatcher before spawn.
15898
- "KODY_PREVIEW_BUILD_MODE"
15899
- ]);
15900
- }
15901
- });
15902
-
15903
- // src/scripts/previewBuildRun.ts
15904
- import { spawn as spawn4 } from "child_process";
15905
- async function runCmd(cmd, args, opts = {}) {
15906
- await new Promise((resolve10, reject) => {
15907
- const child = spawn4(cmd, args, {
15908
- cwd: opts.cwd,
15909
- env: { ...process.env, ...opts.env ?? {} },
15910
- stdio: opts.input ? ["pipe", "inherit", "inherit"] : "inherit"
15911
- });
15912
- if (opts.input && child.stdin) {
15913
- child.stdin.write(opts.input);
15914
- child.stdin.end();
15915
- }
15916
- child.on("error", reject);
15917
- child.on("close", (code) => {
15918
- if (code === 0) resolve10();
15919
- else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
15920
- });
15921
- });
15922
- }
15923
- var init_previewBuildRun = __esm({
15924
- "src/scripts/previewBuildRun.ts"() {
15925
- "use strict";
15926
- }
15927
- });
15928
-
15929
- // src/scripts/previewBuildNamespace.ts
15930
- async function fetchGithubOidcToken(audience) {
15931
- const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL?.trim();
15932
- const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN?.trim();
15933
- if (!url || !requestToken) return null;
15934
- const res = await fetch(`${url}&audience=${encodeURIComponent(audience)}`, {
15935
- headers: { Authorization: `Bearer ${requestToken}` },
15936
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
15937
- });
15938
- if (!res.ok) {
15939
- throw new Error(`OIDC token request failed: ${res.status} ${res.statusText}`);
15940
- }
15941
- const data = await res.json();
15942
- if (!data.value) throw new Error("OIDC token response missing `value`");
15943
- return data.value;
15944
- }
15945
- async function setupNamespaceBuilder(opts) {
15946
- try {
15947
- await runCmd("bash", ["-c", NSC_INSTALL]);
15948
- const jwt = await fetchGithubOidcToken(NSC_OIDC_AUDIENCE);
15949
- if (!jwt) {
15950
- console.warn("[preview-build] no GitHub OIDC token (id-token: write missing?) \u2014 local docker build");
15951
- return null;
15952
- }
15953
- await runCmd("nsc", ["auth", "exchange-oidc-token", "--tenant_id", opts.tenantId, "--token", jwt]);
15954
- await runCmd("nsc", ["docker", "buildx", "setup", "--name", opts.builderName]);
15955
- console.log(`[preview-build] Namespace remote builder ready (${opts.builderName})`);
15956
- return opts.builderName;
15957
- } catch (err) {
15958
- console.warn(
15959
- "[preview-build] Namespace setup failed \u2014 falling back to local docker:",
15960
- err instanceof Error ? err.message : String(err)
15961
- );
15962
- return null;
15963
- }
15964
- }
15965
- var NSC_OIDC_AUDIENCE, REQ_TIMEOUT_MS, NSC_INSTALL;
15966
- var init_previewBuildNamespace = __esm({
15967
- "src/scripts/previewBuildNamespace.ts"() {
15968
- "use strict";
15969
- init_previewBuildRun();
15970
- NSC_OIDC_AUDIENCE = "https://namespace.so";
15971
- REQ_TIMEOUT_MS = 15e3;
15972
- NSC_INSTALL = `
15973
- set -euo pipefail
15974
- if [ ! -x /usr/local/bin/nsc ]; then
15975
- ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
15976
- OS=$(uname -s | tr '[:upper:]' '[:lower:]')
15977
- curl -fsSL "https://get.namespace.so/packages/nsc/latest?arch=\${ARCH}&os=\${OS}" -o /tmp/nsc.tar.gz
15978
- mkdir -p /tmp/nsc-extract
15979
- tar -xzf /tmp/nsc.tar.gz -C /tmp/nsc-extract
15980
- NSC_BIN=$(find /tmp/nsc-extract -type f -name nsc | head -1)
15981
- sudo install -m 0755 "$NSC_BIN" /usr/local/bin/nsc
15982
- fi
15983
- /usr/local/bin/nsc version
15984
- `;
15985
- }
15986
- });
15987
-
15988
15876
  // src/stateRepoGithub.ts
15989
15877
  import * as fs39 from "fs";
15990
15878
  import * as path37 from "path";
@@ -16004,7 +15892,7 @@ async function githubContentsFile(opts) {
16004
15892
  "X-GitHub-Api-Version": "2022-11-28",
16005
15893
  "User-Agent": "kody-engine"
16006
15894
  },
16007
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
15895
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
16008
15896
  });
16009
15897
  if (res.status === 404) return null;
16010
15898
  if (!res.ok) {
@@ -16103,7 +15991,7 @@ async function writeGithubStateTextWithConfig(opts) {
16103
15991
  "User-Agent": "kody-engine"
16104
15992
  },
16105
15993
  body: JSON.stringify(payload),
16106
- signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
15994
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS)
16107
15995
  });
16108
15996
  if (!res.ok) {
16109
15997
  throw new Error(
@@ -16157,13 +16045,196 @@ async function persistJsonlFileToGithubState(opts) {
16157
16045
  }
16158
16046
  }
16159
16047
  }
16160
- var GITHUB_API, REQ_TIMEOUT_MS2;
16048
+ var GITHUB_API, REQ_TIMEOUT_MS;
16161
16049
  var init_stateRepoGithub = __esm({
16162
16050
  "src/stateRepoGithub.ts"() {
16163
16051
  "use strict";
16164
16052
  init_stateRepo();
16165
16053
  GITHUB_API = "https://api.github.com";
16166
- REQ_TIMEOUT_MS2 = 3e4;
16054
+ REQ_TIMEOUT_MS = 3e4;
16055
+ }
16056
+ });
16057
+
16058
+ // src/scripts/previewBuildHelpers.ts
16059
+ import { createDecipheriv, createHash as createHash3, hkdfSync } from "crypto";
16060
+ function shortHash(s) {
16061
+ return createHash3("sha256").update(s).digest("hex").slice(0, 6);
16062
+ }
16063
+ function previewAppName(repo, pr) {
16064
+ const [owner, name] = repo.split("/");
16065
+ if (!owner || !name) {
16066
+ throw new Error(`invalid repo "${repo}", expected "owner/name"`);
16067
+ }
16068
+ return `kp-${shortHash(owner)}-${shortHash(name)}-pr-${pr}`;
16069
+ }
16070
+ function basePreviewAppName(repo) {
16071
+ const [owner, name] = repo.split("/");
16072
+ if (!owner || !name) {
16073
+ throw new Error(`invalid repo "${repo}", expected "owner/name"`);
16074
+ }
16075
+ return `kp-${shortHash(owner)}-${shortHash(name)}-base`;
16076
+ }
16077
+ function decryptVaultPayload(payload, keyRaw) {
16078
+ const parts = payload.split(":");
16079
+ if (parts.length !== 4 || parts[0] !== "v1") {
16080
+ throw new Error("invalid vault payload format");
16081
+ }
16082
+ const [, ivB64, ctB64, tagB64] = parts;
16083
+ const key = decodeMasterKey(keyRaw);
16084
+ if (key.length !== 32) {
16085
+ throw new Error("KODY_MASTER_KEY must decode to 32 bytes");
16086
+ }
16087
+ const iv = Buffer.from(ivB64, "base64");
16088
+ const ct = Buffer.from(ctB64, "base64");
16089
+ const tag = Buffer.from(tagB64, "base64");
16090
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
16091
+ decipher.setAuthTag(tag);
16092
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
16093
+ }
16094
+ function decodeMasterKey(keyRaw) {
16095
+ if (/^[0-9a-fA-F]{64}$/.test(keyRaw)) return Buffer.from(keyRaw, "hex");
16096
+ return Buffer.from(keyRaw.replace(/-/g, "+").replace(/_/g, "/"), "base64");
16097
+ }
16098
+ function derivePreviewVerifyKey(masterKeyRaw) {
16099
+ const masterKey = decodeMasterKey(masterKeyRaw);
16100
+ if (masterKey.length !== 32) {
16101
+ throw new Error("KODY_MASTER_KEY must decode to 32 bytes");
16102
+ }
16103
+ return Buffer.from(hkdfSync("sha256", masterKey, Buffer.alloc(0), PREVIEW_KEY_INFO, 32)).toString("hex");
16104
+ }
16105
+ function previewRuntimeEnv(args) {
16106
+ return {
16107
+ ...args.buildEnv,
16108
+ KODY_PREVIEW_VERIFY_KEY: derivePreviewVerifyKey(args.masterKey),
16109
+ KODY_REPO_CONTEXT: args.repo,
16110
+ KODY_PR: String(args.pr)
16111
+ };
16112
+ }
16113
+ function buildEnvFromVault(doc) {
16114
+ const buildEnv = {};
16115
+ for (const [name, entry] of Object.entries(doc.secrets ?? {})) {
16116
+ if (!entry?.value) continue;
16117
+ if (NEVER_PASS_TO_BUILD.has(name)) continue;
16118
+ buildEnv[name] = entry.value;
16119
+ }
16120
+ const raw = doc.secrets?.KODY_PREVIEW_BUILD_MODE?.value;
16121
+ const buildMode = raw?.toLowerCase().trim() === "dev" ? "dev" : "prod";
16122
+ return { buildEnv, buildMode };
16123
+ }
16124
+ function formatPreviewComment(args) {
16125
+ const url = `https://${args.appName}.fly.dev`;
16126
+ return [
16127
+ "<!-- kody-fly-preview -->",
16128
+ `\u2705 **Preview ready:** ${url}`,
16129
+ "",
16130
+ `<sub>App: \`${args.appName}\` \xB7 Commit: \`${args.ref.slice(0, 7)}\` \xB7 Updated: ${args.nowIso}</sub>`
16131
+ ].join("\n");
16132
+ }
16133
+ function defaultImageTag(repo, ref) {
16134
+ return createHash3("sha256").update(`${repo}@${ref}`).digest("hex").slice(0, 12);
16135
+ }
16136
+ var NEVER_PASS_TO_BUILD, PREVIEW_KEY_INFO;
16137
+ var init_previewBuildHelpers = __esm({
16138
+ "src/scripts/previewBuildHelpers.ts"() {
16139
+ "use strict";
16140
+ NEVER_PASS_TO_BUILD = /* @__PURE__ */ new Set([
16141
+ "FLY_API_TOKEN",
16142
+ "FLY_ORG_SLUG",
16143
+ "FLY_DEFAULT_REGION",
16144
+ "KODY_MASTER_KEY",
16145
+ // Preview-config knob; consumed by the dispatcher before spawn.
16146
+ "KODY_PREVIEW_BUILD_MODE",
16147
+ "KODY_PREVIEW_VERIFY_KEY",
16148
+ "KODY_REPO_CONTEXT",
16149
+ "KODY_PR",
16150
+ "KODY_BRANCH"
16151
+ ]);
16152
+ PREVIEW_KEY_INFO = "kody-preview:v1";
16153
+ }
16154
+ });
16155
+
16156
+ // src/scripts/previewBuildRun.ts
16157
+ import { spawn as spawn4 } from "child_process";
16158
+ async function runCmd(cmd, args, opts = {}) {
16159
+ await new Promise((resolve10, reject) => {
16160
+ const child = spawn4(cmd, args, {
16161
+ cwd: opts.cwd,
16162
+ env: { ...process.env, ...opts.env ?? {} },
16163
+ stdio: opts.input ? ["pipe", "inherit", "inherit"] : "inherit"
16164
+ });
16165
+ if (opts.input && child.stdin) {
16166
+ child.stdin.write(opts.input);
16167
+ child.stdin.end();
16168
+ }
16169
+ child.on("error", reject);
16170
+ child.on("close", (code) => {
16171
+ if (code === 0) resolve10();
16172
+ else reject(new Error(`${cmd} ${args.join(" ")} exited ${code}`));
16173
+ });
16174
+ });
16175
+ }
16176
+ var init_previewBuildRun = __esm({
16177
+ "src/scripts/previewBuildRun.ts"() {
16178
+ "use strict";
16179
+ }
16180
+ });
16181
+
16182
+ // src/scripts/previewBuildNamespace.ts
16183
+ async function fetchGithubOidcToken(audience) {
16184
+ const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL?.trim();
16185
+ const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN?.trim();
16186
+ if (!url || !requestToken) return null;
16187
+ const res = await fetch(`${url}&audience=${encodeURIComponent(audience)}`, {
16188
+ headers: { Authorization: `Bearer ${requestToken}` },
16189
+ signal: AbortSignal.timeout(REQ_TIMEOUT_MS2)
16190
+ });
16191
+ if (!res.ok) {
16192
+ throw new Error(`OIDC token request failed: ${res.status} ${res.statusText}`);
16193
+ }
16194
+ const data = await res.json();
16195
+ if (!data.value) throw new Error("OIDC token response missing `value`");
16196
+ return data.value;
16197
+ }
16198
+ async function setupNamespaceBuilder(opts) {
16199
+ try {
16200
+ await runCmd("bash", ["-c", NSC_INSTALL]);
16201
+ const jwt = await fetchGithubOidcToken(NSC_OIDC_AUDIENCE);
16202
+ if (!jwt) {
16203
+ console.warn("[preview-build] no GitHub OIDC token (id-token: write missing?) \u2014 local docker build");
16204
+ return null;
16205
+ }
16206
+ await runCmd("nsc", ["auth", "exchange-oidc-token", "--tenant_id", opts.tenantId, "--token", jwt]);
16207
+ await runCmd("nsc", ["docker", "buildx", "setup", "--name", opts.builderName]);
16208
+ console.log(`[preview-build] Namespace remote builder ready (${opts.builderName})`);
16209
+ return opts.builderName;
16210
+ } catch (err) {
16211
+ console.warn(
16212
+ "[preview-build] Namespace setup failed \u2014 falling back to local docker:",
16213
+ err instanceof Error ? err.message : String(err)
16214
+ );
16215
+ return null;
16216
+ }
16217
+ }
16218
+ var NSC_OIDC_AUDIENCE, REQ_TIMEOUT_MS2, NSC_INSTALL;
16219
+ var init_previewBuildNamespace = __esm({
16220
+ "src/scripts/previewBuildNamespace.ts"() {
16221
+ "use strict";
16222
+ init_previewBuildRun();
16223
+ NSC_OIDC_AUDIENCE = "https://namespace.so";
16224
+ REQ_TIMEOUT_MS2 = 15e3;
16225
+ NSC_INSTALL = `
16226
+ set -euo pipefail
16227
+ if [ ! -x /usr/local/bin/nsc ]; then
16228
+ ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
16229
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
16230
+ curl -fsSL "https://get.namespace.so/packages/nsc/latest?arch=\${ARCH}&os=\${OS}" -o /tmp/nsc.tar.gz
16231
+ mkdir -p /tmp/nsc-extract
16232
+ tar -xzf /tmp/nsc.tar.gz -C /tmp/nsc-extract
16233
+ NSC_BIN=$(find /tmp/nsc-extract -type f -name nsc | head -1)
16234
+ sudo install -m 0755 "$NSC_BIN" /usr/local/bin/nsc
16235
+ fi
16236
+ /usr/local/bin/nsc version
16237
+ `;
16167
16238
  }
16168
16239
  });
16169
16240
 
@@ -16375,10 +16446,10 @@ var FLY_MACHINES, FLY_GRAPHQL, REQ_TIMEOUT_MS3, runPreviewBuild;
16375
16446
  var init_runPreviewBuild = __esm({
16376
16447
  "src/scripts/runPreviewBuild.ts"() {
16377
16448
  "use strict";
16449
+ init_stateRepoGithub();
16378
16450
  init_previewBuildHelpers();
16379
16451
  init_previewBuildNamespace();
16380
16452
  init_previewBuildRun();
16381
- init_stateRepoGithub();
16382
16453
  FLY_MACHINES = "https://api.machines.dev/v1";
16383
16454
  FLY_GRAPHQL = "https://api.fly.io/graphql";
16384
16455
  REQ_TIMEOUT_MS3 = 3e4;
@@ -16503,7 +16574,7 @@ var init_runPreviewBuild = __esm({
16503
16574
  appName,
16504
16575
  region,
16505
16576
  image: `registry.fly.io/${appName}:${tag}`,
16506
- env: buildEnv
16577
+ env: previewRuntimeEnv({ buildEnv, masterKey, pr, repo })
16507
16578
  },
16508
16579
  flyToken
16509
16580
  );
@@ -23060,7 +23131,7 @@ async function runCapabilityFallbackTick(deps) {
23060
23131
  }
23061
23132
 
23062
23133
  // src/pool/keys.ts
23063
- import { hkdfSync } from "crypto";
23134
+ import { hkdfSync as hkdfSync2 } from "crypto";
23064
23135
  var POOL_API_KEY_INFO = "kody-pool-api:v1";
23065
23136
  var RUNNER_API_KEY_INFO = "kody-runner-api:v1";
23066
23137
  function masterKeyBytes(raw) {
@@ -23072,7 +23143,7 @@ function masterKeyBytes(raw) {
23072
23143
  return Buffer.from(v.replace(/-/g, "+").replace(/_/g, "/"), "base64");
23073
23144
  }
23074
23145
  function deriveKey(master, info, length = 32) {
23075
- return Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), info, length)).toString("hex");
23146
+ return Buffer.from(hkdfSync2("sha256", master, Buffer.alloc(0), info, length)).toString("hex");
23076
23147
  }
23077
23148
  function derivePoolApiKey(master) {
23078
23149
  return deriveKey(master, POOL_API_KEY_INFO);
@@ -76,13 +76,20 @@ export interface Profile {
76
76
  */
77
77
  kind: "oneshot" | "scheduled"
78
78
  /**
79
- * Locked-toolbox palette (unified successor to a markdown capability's `tools:`
79
+ * Capability MCP palette (unified successor to a markdown capability's `tools:`
80
80
  * metadata). When non-empty, loadCapabilityState sets ctx.data.capabilityTools so the
81
- * executor spins up the in-process kody-capability MCP server and the agent runs
82
- * MCP-only (Bash/Read revoked unless also in claudeCode.tools). Absent →
83
- * normal SDK tools.
81
+ * executor spins up the in-process kody-capability MCP server. By default this
82
+ * is locked mode: Bash/Read are revoked and only the declared MCP tools plus
83
+ * submit_state remain.
84
84
  */
85
85
  capabilityTools?: string[]
86
+ /**
87
+ * `lock` (default) replaces the normal toolbox with capability MCP tools.
88
+ * `append` keeps the normal toolbox and adds the declared MCP tools. Use append
89
+ * only for coordinator capabilities that still own repo state edits but must use
90
+ * an engine primitive for a narrow side effect such as starting another capability.
91
+ */
92
+ capabilityToolMode?: "lock" | "append"
86
93
  /**
87
94
  * GitHub logins (no leading `@`) this capability's output should mention. Rendered
88
95
  * to `@a @b` and exposed to the prompt as {{mentions}} (and as the capability-MCP
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.304",
3
+ "version": "0.4.306",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,28 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
+ "scripts": {
16
+ "kody:run": "tsx bin/kody.ts",
17
+ "serve": "tsx bin/kody.ts serve",
18
+ "serve:vscode": "tsx bin/kody.ts serve vscode",
19
+ "serve:claude": "tsx bin/kody.ts serve claude",
20
+ "clean:dist": "node scripts/clean-dist.cjs",
21
+ "build": "pnpm clean:dist && tsup && node scripts/copy-assets.cjs",
22
+ "check:modularity": "tsx scripts/check-script-modularity.ts",
23
+ "pretest": "pnpm check:modularity",
24
+ "test": "vitest run tests/unit tests/int --coverage",
25
+ "posttest": "tsx scripts/check-coverage-floor.ts",
26
+ "test:smoke": "vitest run tests/smoke --no-coverage",
27
+ "test:e2e": "vitest run tests/e2e --no-coverage",
28
+ "test:all": "vitest run tests --no-coverage",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "biome check",
31
+ "lint:fix": "biome check --write",
32
+ "format": "biome format --write",
33
+ "verify:package": "node scripts/verify-package-tarball.cjs",
34
+ "brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner",
35
+ "prepublishOnly": "pnpm typecheck && vitest run tests/unit tests/int --no-coverage && pnpm build && pnpm verify:package"
36
+ },
15
37
  "dependencies": {
16
38
  "@actions/cache": "^6.0.0",
17
39
  "@anthropic-ai/claude-agent-sdk": "0.2.119",
@@ -35,26 +57,5 @@
35
57
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
36
58
  },
37
59
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
38
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
39
- "scripts": {
40
- "kody:run": "tsx bin/kody.ts",
41
- "serve": "tsx bin/kody.ts serve",
42
- "serve:vscode": "tsx bin/kody.ts serve vscode",
43
- "serve:claude": "tsx bin/kody.ts serve claude",
44
- "clean:dist": "node scripts/clean-dist.cjs",
45
- "build": "pnpm clean:dist && tsup && node scripts/copy-assets.cjs",
46
- "check:modularity": "tsx scripts/check-script-modularity.ts",
47
- "pretest": "pnpm check:modularity",
48
- "test": "vitest run tests/unit tests/int --coverage",
49
- "posttest": "tsx scripts/check-coverage-floor.ts",
50
- "test:smoke": "vitest run tests/smoke --no-coverage",
51
- "test:e2e": "vitest run tests/e2e --no-coverage",
52
- "test:all": "vitest run tests --no-coverage",
53
- "typecheck": "tsc --noEmit",
54
- "lint": "biome check",
55
- "lint:fix": "biome check --write",
56
- "format": "biome format --write",
57
- "verify:package": "node scripts/verify-package-tarball.cjs",
58
- "brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner"
59
- }
60
- }
60
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
61
+ }