@kody-ade/kody-engine 0.4.116 → 0.4.118

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 (2) hide show
  1. package/dist/bin/kody.js +294 -42
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -880,7 +880,7 @@ var init_loadPriorArt = __esm({
880
880
  // package.json
881
881
  var package_default = {
882
882
  name: "@kody-ade/kody-engine",
883
- version: "0.4.116",
883
+ version: "0.4.118",
884
884
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
885
885
  license: "MIT",
886
886
  type: "module",
@@ -1227,9 +1227,43 @@ function loadConfig(projectDir = process.cwd()) {
1227
1227
  classify: parseClassifyConfig(raw.classify),
1228
1228
  release: parseReleaseConfig(raw.release),
1229
1229
  jobs: parseJobsConfig(raw.jobs),
1230
+ access: parseAccessConfig(raw.access),
1230
1231
  qa: parseQaConfig(raw.qa)
1231
1232
  };
1232
1233
  }
1234
+ var GITHUB_AUTHOR_ASSOCIATIONS = [
1235
+ "OWNER",
1236
+ "MEMBER",
1237
+ "COLLABORATOR",
1238
+ "CONTRIBUTOR",
1239
+ "FIRST_TIME_CONTRIBUTOR",
1240
+ "FIRST_TIMER",
1241
+ "MANNEQUIN",
1242
+ "NONE"
1243
+ ];
1244
+ function parseAccessConfig(raw) {
1245
+ if (!raw || typeof raw !== "object") return void 0;
1246
+ const r = raw;
1247
+ if (r.allowedAssociations === void 0) return void 0;
1248
+ if (!Array.isArray(r.allowedAssociations)) {
1249
+ throw new Error(`kody.config.json: access.allowedAssociations must be an array of strings`);
1250
+ }
1251
+ const valid = new Set(GITHUB_AUTHOR_ASSOCIATIONS);
1252
+ const out = [];
1253
+ for (const v of r.allowedAssociations) {
1254
+ if (typeof v !== "string") {
1255
+ throw new Error(`kody.config.json: access.allowedAssociations entries must be strings`);
1256
+ }
1257
+ const up = v.trim().toUpperCase();
1258
+ if (!valid.has(up)) {
1259
+ throw new Error(
1260
+ `kody.config.json: access.allowedAssociations contains "${v}" \u2014 must be one of ${GITHUB_AUTHOR_ASSOCIATIONS.join(", ")}`
1261
+ );
1262
+ }
1263
+ out.push(up);
1264
+ }
1265
+ return out.length > 0 ? { allowedAssociations: out } : void 0;
1266
+ }
1233
1267
  function parseQaConfig(raw) {
1234
1268
  if (!raw || typeof raw !== "object") return void 0;
1235
1269
  const r = raw;
@@ -2496,6 +2530,7 @@ function autoDispatch(opts) {
2496
2530
  const authorType = String(event.comment?.user?.type ?? "");
2497
2531
  if (!rawBody.toLowerCase().includes("@kody")) return null;
2498
2532
  if (authorLogin === "kody-bot" || authorType === "Bot") return null;
2533
+ if (!associationAllowed(event, opts?.config)) return null;
2499
2534
  const body = rawBody.toLowerCase();
2500
2535
  const targetNum = Number(event.issue?.number ?? 0);
2501
2536
  const isPr = !!event.issue?.pull_request;
@@ -2571,6 +2606,10 @@ function autoDispatchTyped(opts) {
2571
2606
  if (authorLogin === "kody-bot" || authorType === "Bot") {
2572
2607
  return { kind: "silent", reason: `bot-authored comment (${authorLogin || authorType})` };
2573
2608
  }
2609
+ if (!associationAllowed(event, opts?.config)) {
2610
+ const assoc = String(event.comment?.author_association ?? "").toUpperCase() || "<none>";
2611
+ return { kind: "silent", reason: `commenter association '${assoc}' not in access.allowedAssociations` };
2612
+ }
2574
2613
  const targetNum = Number(event.issue?.number ?? 0);
2575
2614
  const isPr = !!event.issue?.pull_request;
2576
2615
  if (!targetNum) {
@@ -2622,6 +2661,12 @@ function dispatchScheduledWatches(opts) {
2622
2661
  }
2623
2662
  return out;
2624
2663
  }
2664
+ function associationAllowed(event, config) {
2665
+ const allowed = config?.access?.allowedAssociations;
2666
+ if (!allowed || allowed.length === 0) return true;
2667
+ const assoc = String(event.comment?.author_association ?? "").toUpperCase();
2668
+ return allowed.includes(assoc);
2669
+ }
2625
2670
  function extractAfterTag(body) {
2626
2671
  const idx = body.indexOf("@kody");
2627
2672
  if (idx === -1) return body;
@@ -4635,13 +4680,23 @@ function parseJob(body) {
4635
4680
  if (!jobId) return { error: "jobId required" };
4636
4681
  const repo = typeof b.repo === "string" ? b.repo.trim() : "";
4637
4682
  if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
4638
- const issueNumber = Number(b.issueNumber);
4639
- if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
4640
- return { error: "issueNumber (positive integer) required" };
4641
- }
4642
4683
  const githubToken = typeof b.githubToken === "string" ? b.githubToken.trim() : "";
4643
4684
  if (!githubToken) return { error: "githubToken required" };
4644
- const job = { jobId, repo, issueNumber, githubToken };
4685
+ const mode = b.mode === "interactive" ? "interactive" : "issue";
4686
+ const job = { jobId, repo, githubToken, mode };
4687
+ if (mode === "issue") {
4688
+ const issueNumber = Number(b.issueNumber);
4689
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
4690
+ return { error: "issueNumber (positive integer) required for issue mode" };
4691
+ }
4692
+ job.issueNumber = issueNumber;
4693
+ } else {
4694
+ const sessionId = typeof b.sessionId === "string" ? b.sessionId.trim() : "";
4695
+ if (!sessionId) return { error: "sessionId required for interactive mode" };
4696
+ job.sessionId = sessionId;
4697
+ if (Number.isFinite(Number(b.idleExitMs))) job.idleExitMs = Number(b.idleExitMs);
4698
+ if (Number.isFinite(Number(b.hardCapMs))) job.hardCapMs = Number(b.hardCapMs);
4699
+ }
4645
4700
  if (typeof b.ref === "string" && b.ref.trim()) job.ref = b.ref.trim();
4646
4701
  if (typeof b.model === "string" && b.model.trim()) job.model = b.model.trim();
4647
4702
  if (typeof b.sessionId === "string" && b.sessionId.trim()) job.sessionId = b.sessionId.trim();
@@ -4658,16 +4713,21 @@ async function defaultRunJob(job) {
4658
4713
  fs19.rmSync(workdir, { recursive: true, force: true });
4659
4714
  fs19.mkdirSync(workdir, { recursive: true });
4660
4715
  const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
4716
+ const interactive = job.mode === "interactive";
4661
4717
  const childEnv = {
4662
4718
  ...process.env,
4663
4719
  REPO: job.repo,
4664
4720
  REF: branch,
4665
4721
  GITHUB_TOKEN: job.githubToken,
4666
- ISSUE_NUMBER: String(job.issueNumber),
4722
+ // Issue mode bakes ISSUE_NUMBER → `kody run --issue N`. Interactive mode
4723
+ // leaves it empty and sets SESSION_ID so the engine boots a chat session.
4724
+ ISSUE_NUMBER: interactive ? "" : String(job.issueNumber),
4667
4725
  ALL_SECRETS: allSecrets,
4668
4726
  SESSION_ID: job.sessionId ?? "",
4669
4727
  DASHBOARD_URL: job.dashboardUrl ?? "",
4670
- MODEL: job.model ?? ""
4728
+ MODEL: job.model ?? "",
4729
+ ...interactive && job.idleExitMs ? { KODY_IDLE_EXIT_MS: String(job.idleExitMs) } : {},
4730
+ ...interactive && job.hardCapMs ? { KODY_HARD_CAP_MS: String(job.hardCapMs) } : {}
4671
4731
  };
4672
4732
  const run = (cmd, args, cwd) => new Promise((resolve4) => {
4673
4733
  const child = spawn3(cmd, args, { stdio: "inherit", env: childEnv, cwd });
@@ -4699,9 +4759,12 @@ async function defaultRunJob(job) {
4699
4759
  const authorEmail = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
4700
4760
  await run("git", ["config", "user.name", authorName], workdir);
4701
4761
  await run("git", ["config", "user.email", authorEmail], workdir);
4702
- process.stdout.write(`[runner-serve] job ${job.jobId}: running issue #${job.issueNumber}
4703
- `);
4704
- const runCode = await run("kody", ["run", "--issue", String(job.issueNumber)], workdir);
4762
+ const runArgs = interactive ? [] : ["run", "--issue", String(job.issueNumber)];
4763
+ process.stdout.write(
4764
+ `[runner-serve] job ${job.jobId}: ${interactive ? `interactive session ${job.sessionId}` : `running issue #${job.issueNumber}`}
4765
+ `
4766
+ );
4767
+ const runCode = await run("kody", runArgs, workdir);
4705
4768
  process.stdout.write(`[runner-serve] job ${job.jobId}: finished (exit ${runCode})
4706
4769
  `);
4707
4770
  process.exit(runCode);
@@ -4785,6 +4848,7 @@ import { createServer as createServer3 } from "http";
4785
4848
  var FLY_API_BASE = "https://api.machines.dev/v1";
4786
4849
  var POOL_METADATA_KEY = "kody_pool";
4787
4850
  var POOL_METADATA_VALUE = "1";
4851
+ var POOL_REPO_METADATA_KEY = "kody_pool_repo";
4788
4852
  var FlyClient = class {
4789
4853
  constructor(opts) {
4790
4854
  this.opts = opts;
@@ -4814,7 +4878,7 @@ var FlyClient = class {
4814
4878
  if (!raw.trim()) return null;
4815
4879
  return JSON.parse(raw);
4816
4880
  }
4817
- /** Create + start a pooled machine in serve mode. */
4881
+ /** Create + start a pooled machine in serve mode, tagged for `repoTag`. */
4818
4882
  async createPooled(input) {
4819
4883
  const body = {
4820
4884
  region: input.region,
@@ -4824,7 +4888,10 @@ var FlyClient = class {
4824
4888
  auto_destroy: true,
4825
4889
  restart: { policy: "no" },
4826
4890
  init: { entrypoint: ["/usr/local/bin/entrypoint-serve.sh"] },
4827
- metadata: { [POOL_METADATA_KEY]: POOL_METADATA_VALUE },
4891
+ metadata: {
4892
+ [POOL_METADATA_KEY]: POOL_METADATA_VALUE,
4893
+ [POOL_REPO_METADATA_KEY]: input.repoTag
4894
+ },
4828
4895
  env: {
4829
4896
  RUNNER_API_KEY: input.runnerApiKey,
4830
4897
  KODY_LITELLM_URL: input.litellmUrl,
@@ -4839,11 +4906,14 @@ var FlyClient = class {
4839
4906
  async get(id) {
4840
4907
  return this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}`, { allow404: true });
4841
4908
  }
4842
- /** List the app's pooled (kody_pool) machines, excluding destroyed/destroying. */
4843
- async listPooled() {
4909
+ /**
4910
+ * List pooled machines for `repoTag` (kody_pool + matching repo tag),
4911
+ * excluding destroyed/destroying. Each repo's pool sees only its own.
4912
+ */
4913
+ async listPooled(repoTag) {
4844
4914
  const all = await this.call(`/apps/${enc(this.opts.app)}/machines`, { allow404: true }) ?? [];
4845
4915
  return all.filter(
4846
- (m) => m.config?.metadata?.[POOL_METADATA_KEY] === POOL_METADATA_VALUE && m.state !== "destroyed" && m.state !== "destroying"
4916
+ (m) => m.config?.metadata?.[POOL_METADATA_KEY] === POOL_METADATA_VALUE && m.config?.metadata?.[POOL_REPO_METADATA_KEY] === repoTag && m.state !== "destroyed" && m.state !== "destroying"
4847
4917
  );
4848
4918
  }
4849
4919
  /** Suspend (freeze) a machine — wakes in ~1s from the snapshot. */
@@ -4913,7 +4983,7 @@ var PoolManager = class {
4913
4983
  * free; anything else is left to finish/auto-destroy. Then refill to `min`.
4914
4984
  */
4915
4985
  async reconcile() {
4916
- const machines = await this.deps.fly.listPooled();
4986
+ const machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
4917
4987
  this.free = [];
4918
4988
  for (const m of machines) {
4919
4989
  if ((m.state === "suspended" || m.state === "suspending") && m.private_ip) {
@@ -4981,7 +5051,7 @@ var PoolManager = class {
4981
5051
  async resync() {
4982
5052
  let machines;
4983
5053
  try {
4984
- machines = await this.deps.fly.listPooled();
5054
+ machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
4985
5055
  } catch (err) {
4986
5056
  this.log(`resync: listPooled failed: ${errMsg2(err)}`);
4987
5057
  return;
@@ -5031,6 +5101,7 @@ var PoolManager = class {
5031
5101
  guest: cfg.guest,
5032
5102
  runnerApiKey: cfg.runnerApiKey,
5033
5103
  litellmUrl: cfg.litellmUrl,
5104
+ repoTag: cfg.repoTag,
5034
5105
  port: cfg.port
5035
5106
  });
5036
5107
  if (!m.private_ip) {
@@ -5078,6 +5149,163 @@ function errMsg2(err) {
5078
5149
  return err instanceof Error ? err.message : String(err);
5079
5150
  }
5080
5151
 
5152
+ // src/pool/vault.ts
5153
+ import { createDecipheriv } from "crypto";
5154
+ var GITHUB_API = "https://api.github.com";
5155
+ var VAULT_PATH = ".kody/secrets.enc";
5156
+ var CACHE_TTL_MS = 6e4;
5157
+ var cache = /* @__PURE__ */ new Map();
5158
+ function decryptVault(payload, masterKey) {
5159
+ const parts = payload.split(":");
5160
+ if (parts.length !== 4 || parts[0] !== "v1") {
5161
+ throw new Error("invalid vault payload format");
5162
+ }
5163
+ const [, ivB64, ctB64, tagB64] = parts;
5164
+ const iv = Buffer.from(ivB64, "base64");
5165
+ const ct = Buffer.from(ctB64, "base64");
5166
+ const tag = Buffer.from(tagB64, "base64");
5167
+ const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
5168
+ decipher.setAuthTag(tag);
5169
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
5170
+ }
5171
+ async function readVaultSecrets(opts) {
5172
+ const key = `${opts.owner}/${opts.repo}`.toLowerCase();
5173
+ const hit = cache.get(key);
5174
+ if (hit && hit.expiresAt > Date.now()) return hit.secrets;
5175
+ const doFetch = opts.fetchImpl ?? fetch;
5176
+ const res = await doFetch(
5177
+ `${GITHUB_API}/repos/${encodeURIComponent(opts.owner)}/${encodeURIComponent(opts.repo)}/contents/${VAULT_PATH}`,
5178
+ {
5179
+ headers: {
5180
+ Authorization: `Bearer ${opts.githubToken}`,
5181
+ Accept: "application/vnd.github+json",
5182
+ "User-Agent": "kody-pool-serve"
5183
+ }
5184
+ }
5185
+ );
5186
+ if (res.status === 404) {
5187
+ cache.set(key, { secrets: {}, expiresAt: Date.now() + CACHE_TTL_MS });
5188
+ return {};
5189
+ }
5190
+ if (!res.ok) {
5191
+ throw new Error(`vault read ${res.status} for ${key}: ${(await res.text().catch(() => "")).slice(0, 160)}`);
5192
+ }
5193
+ const body = await res.json();
5194
+ if (!body.content) {
5195
+ cache.set(key, { secrets: {}, expiresAt: Date.now() + CACHE_TTL_MS });
5196
+ return {};
5197
+ }
5198
+ const ciphertext = Buffer.from(body.content, body.encoding ?? "base64").toString("utf8").trim();
5199
+ const doc = JSON.parse(decryptVault(ciphertext, opts.masterKey));
5200
+ const flat = {};
5201
+ for (const [name, entry] of Object.entries(doc.secrets ?? {})) {
5202
+ if (entry && typeof entry.value === "string") flat[name] = entry.value;
5203
+ }
5204
+ cache.set(key, { secrets: flat, expiresAt: Date.now() + CACHE_TTL_MS });
5205
+ return flat;
5206
+ }
5207
+ async function readRepoSecret(opts) {
5208
+ const secrets = await readVaultSecrets(opts);
5209
+ const v = secrets[opts.name];
5210
+ return v && v.trim() ? v : null;
5211
+ }
5212
+ async function readRepoSecrets(opts) {
5213
+ return readVaultSecrets(opts);
5214
+ }
5215
+
5216
+ // src/pool/registry.ts
5217
+ var PoolRegistry = class {
5218
+ constructor(cfg) {
5219
+ this.cfg = cfg;
5220
+ this.log = cfg.log ?? (() => {
5221
+ });
5222
+ this.resolveFlyToken = cfg.resolveFlyToken ?? ((owner, repo) => readRepoSecret({
5223
+ githubToken: cfg.githubToken,
5224
+ masterKey: cfg.masterKey,
5225
+ owner,
5226
+ repo,
5227
+ name: "FLY_API_TOKEN"
5228
+ }));
5229
+ }
5230
+ cfg;
5231
+ pools = /* @__PURE__ */ new Map();
5232
+ resolveFlyToken;
5233
+ log;
5234
+ key(owner, repo) {
5235
+ return `${owner}/${repo}`.toLowerCase();
5236
+ }
5237
+ /** Get-or-create the pool for a repo, or null if the repo has no Fly token. */
5238
+ async getPool(owner, repo) {
5239
+ const repoTag = this.key(owner, repo);
5240
+ const existing = this.pools.get(repoTag);
5241
+ if (existing) return existing;
5242
+ let flyToken;
5243
+ try {
5244
+ flyToken = await this.resolveFlyToken(owner, repo);
5245
+ } catch (err) {
5246
+ this.log(`registry: vault read failed for ${repoTag}: ${err instanceof Error ? err.message : String(err)}`);
5247
+ return null;
5248
+ }
5249
+ if (!flyToken) {
5250
+ this.log(`registry: ${repoTag} has no FLY_API_TOKEN \u2014 no pool`);
5251
+ return null;
5252
+ }
5253
+ const fly = new FlyClient({ token: flyToken, app: this.cfg.base.app });
5254
+ const pm = new PoolManager({
5255
+ fly,
5256
+ config: { ...this.cfg.base, repoTag },
5257
+ log: (m) => this.log(`[${repoTag}] ${m}`)
5258
+ });
5259
+ this.pools.set(repoTag, pm);
5260
+ void pm.reconcile().catch((err) => this.log(`[${repoTag}] reconcile: ${err instanceof Error ? err.message : String(err)}`));
5261
+ return pm;
5262
+ }
5263
+ async claim(owner, repo, req) {
5264
+ const pm = await this.getPool(owner, repo);
5265
+ if (!pm) return { ok: false, reason: "repo has no FLY_API_TOKEN (no pool)" };
5266
+ let allSecrets = {};
5267
+ try {
5268
+ const vault = await readRepoSecrets({
5269
+ githubToken: this.cfg.githubToken,
5270
+ masterKey: this.cfg.masterKey,
5271
+ owner,
5272
+ repo
5273
+ });
5274
+ allSecrets = Object.fromEntries(Object.entries(vault).filter(([k]) => k !== "FLY_API_TOKEN"));
5275
+ } catch (err) {
5276
+ this.log(`[${this.key(owner, repo)}] vault secrets read failed: ${err instanceof Error ? err.message : String(err)}`);
5277
+ }
5278
+ const job = {
5279
+ jobId: req.jobId,
5280
+ repo: `${owner}/${repo}`,
5281
+ githubToken: this.cfg.githubToken,
5282
+ mode: req.mode ?? "issue",
5283
+ issueNumber: req.issueNumber,
5284
+ sessionId: req.sessionId,
5285
+ idleExitMs: req.idleExitMs,
5286
+ hardCapMs: req.hardCapMs,
5287
+ ref: req.ref,
5288
+ allSecrets,
5289
+ model: req.model,
5290
+ dashboardUrl: req.dashboardUrl
5291
+ };
5292
+ return pm.claim(job);
5293
+ }
5294
+ /** Status for a single repo's pool, or null if none exists yet. */
5295
+ status(owner, repo) {
5296
+ return this.pools.get(this.key(owner, repo))?.status() ?? null;
5297
+ }
5298
+ /** Resync every active repo pool (periodic self-heal). */
5299
+ async resyncAll() {
5300
+ for (const [repoTag, pm] of this.pools) {
5301
+ await pm.resync().catch((err) => this.log(`[${repoTag}] resync: ${err instanceof Error ? err.message : String(err)}`));
5302
+ }
5303
+ }
5304
+ activeRepos() {
5305
+ return [...this.pools.keys()];
5306
+ }
5307
+ };
5308
+
5081
5309
  // src/pool/keys.ts
5082
5310
  import { hkdfSync } from "crypto";
5083
5311
  var POOL_API_KEY_INFO = "kody-pool-api:v1";
@@ -5149,24 +5377,31 @@ function readJsonBody3(req) {
5149
5377
  req.on("error", reject);
5150
5378
  });
5151
5379
  }
5152
- function parsePoolJob(body) {
5380
+ function parseClaimRequest(body) {
5153
5381
  if (typeof body !== "object" || body === null) return { error: "body must be an object" };
5154
5382
  const b = body;
5155
5383
  const jobId = typeof b.jobId === "string" ? b.jobId.trim() : "";
5156
5384
  if (!jobId) return { error: "jobId required" };
5157
5385
  const repo = typeof b.repo === "string" ? b.repo.trim() : "";
5158
5386
  if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
5159
- const issueNumber = Number(b.issueNumber);
5160
- if (!Number.isInteger(issueNumber) || issueNumber <= 0) return { error: "issueNumber required" };
5161
- const githubToken = typeof b.githubToken === "string" ? b.githubToken.trim() : "";
5162
- if (!githubToken) return { error: "githubToken required" };
5163
- const job = { jobId, repo, issueNumber, githubToken };
5164
- if (typeof b.ref === "string" && b.ref.trim()) job.ref = b.ref.trim();
5165
- if (typeof b.model === "string" && b.model.trim()) job.model = b.model.trim();
5166
- if (typeof b.sessionId === "string" && b.sessionId.trim()) job.sessionId = b.sessionId.trim();
5167
- if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) job.dashboardUrl = b.dashboardUrl.trim();
5168
- if (b.allSecrets && typeof b.allSecrets === "object") job.allSecrets = b.allSecrets;
5169
- return { job };
5387
+ const mode = b.mode === "interactive" ? "interactive" : "issue";
5388
+ const req = { jobId, repo, mode };
5389
+ if (mode === "issue") {
5390
+ const issueNumber = Number(b.issueNumber);
5391
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) return { error: "issueNumber required for issue mode" };
5392
+ req.issueNumber = issueNumber;
5393
+ } else {
5394
+ const sessionId = typeof b.sessionId === "string" ? b.sessionId.trim() : "";
5395
+ if (!sessionId) return { error: "sessionId required for interactive mode" };
5396
+ req.sessionId = sessionId;
5397
+ if (Number.isFinite(Number(b.idleExitMs))) req.idleExitMs = Number(b.idleExitMs);
5398
+ if (Number.isFinite(Number(b.hardCapMs))) req.hardCapMs = Number(b.hardCapMs);
5399
+ }
5400
+ if (typeof b.ref === "string" && b.ref.trim()) req.ref = b.ref.trim();
5401
+ if (typeof b.model === "string" && b.model.trim()) req.model = b.model.trim();
5402
+ if (typeof b.sessionId === "string" && b.sessionId.trim()) req.sessionId = b.sessionId.trim();
5403
+ if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) req.dashboardUrl = b.dashboardUrl.trim();
5404
+ return { req };
5170
5405
  }
5171
5406
  function superviseLitellm() {
5172
5407
  if (process.env.POOL_DISABLE_LITELLM === "1") return null;
@@ -5198,8 +5433,8 @@ var poolServe = async (ctx) => {
5198
5433
  ctx.skipAgent = true;
5199
5434
  const masterRaw = process.env.KODY_MASTER_KEY?.trim();
5200
5435
  if (!masterRaw) throw new Error("KODY_MASTER_KEY required for pool-serve");
5201
- const flyToken = process.env.FLY_API_TOKEN?.trim();
5202
- if (!flyToken) throw new Error("FLY_API_TOKEN required for pool-serve");
5436
+ const githubToken = process.env.GITHUB_TOKEN?.trim();
5437
+ if (!githubToken) throw new Error("GITHUB_TOKEN required for pool-serve (reads per-repo vaults)");
5203
5438
  const master = masterKeyBytes(masterRaw);
5204
5439
  const poolApiKey = derivePoolApiKey(master);
5205
5440
  const runnerApiKey = deriveRunnerApiKey(master);
@@ -5213,23 +5448,36 @@ var poolServe = async (ctx) => {
5213
5448
  const apiPort = envInt("POOL_API_PORT", 4100);
5214
5449
  const healthTimeoutMs = envInt("POOL_HEALTH_TIMEOUT_MS", 12e4);
5215
5450
  const litellm = superviseLitellm();
5216
- const fly = new FlyClient({ token: flyToken, app });
5217
- const manager = new PoolManager({
5218
- fly,
5219
- config: { min, image: process.env.FLY_RUNNER_IMAGE ?? "registry.fly.io/kody-runner:latest", region, guest, runnerApiKey, litellmUrl, port: runnerPort, healthTimeoutMs },
5451
+ const registry = new PoolRegistry({
5452
+ githubToken,
5453
+ masterKey: master,
5454
+ base: {
5455
+ min,
5456
+ image: process.env.FLY_RUNNER_IMAGE ?? "registry.fly.io/kody-runner:latest",
5457
+ region,
5458
+ guest,
5459
+ runnerApiKey,
5460
+ litellmUrl,
5461
+ port: runnerPort,
5462
+ healthTimeoutMs,
5463
+ app
5464
+ },
5220
5465
  log
5221
5466
  });
5222
- manager.reconcile().catch((err) => log(`reconcile failed: ${err instanceof Error ? err.message : String(err)}`));
5223
5467
  const refillMs = envInt("POOL_REFILL_INTERVAL_MS", 6e4);
5224
5468
  const tick = setInterval(() => {
5225
- manager.resync().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
5469
+ registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
5226
5470
  }, refillMs);
5227
5471
  const server = createServer3(async (req, res) => {
5228
5472
  try {
5229
5473
  if (!req.method || !req.url) return sendJson3(res, 400, { error: "bad request" });
5230
5474
  const url = new URL(req.url, "http://localhost");
5231
5475
  if (req.method === "GET" && url.pathname === "/healthz") {
5232
- return sendJson3(res, 200, { ok: true, litellm: litellm ? "supervised" : "off", pool: manager.status() });
5476
+ return sendJson3(res, 200, {
5477
+ ok: true,
5478
+ litellm: litellm ? "supervised" : "off",
5479
+ repos: registry.activeRepos()
5480
+ });
5233
5481
  }
5234
5482
  const authed = bearerOk(
5235
5483
  req.headers["authorization"],
@@ -5238,7 +5486,10 @@ var poolServe = async (ctx) => {
5238
5486
  );
5239
5487
  if (!authed) return sendJson3(res, 401, { error: "unauthorized" });
5240
5488
  if (req.method === "GET" && url.pathname === "/pool/status") {
5241
- return sendJson3(res, 200, manager.status());
5489
+ const repoParam = (url.searchParams.get("repo") ?? "").trim();
5490
+ const [owner, repo] = repoParam.split("/");
5491
+ if (!owner || !repo) return sendJson3(res, 400, { error: "repo query (owner/name) required" });
5492
+ return sendJson3(res, 200, { status: registry.status(owner, repo) });
5242
5493
  }
5243
5494
  if (req.method === "POST" && url.pathname === "/pool/claim") {
5244
5495
  let body;
@@ -5247,9 +5498,10 @@ var poolServe = async (ctx) => {
5247
5498
  } catch {
5248
5499
  return sendJson3(res, 400, { error: "invalid JSON body" });
5249
5500
  }
5250
- const parsed = parsePoolJob(body);
5501
+ const parsed = parseClaimRequest(body);
5251
5502
  if ("error" in parsed) return sendJson3(res, 400, { error: parsed.error });
5252
- const result = await manager.claim(parsed.job);
5503
+ const [owner, repo] = parsed.req.repo.split("/");
5504
+ const result = await registry.claim(owner, repo, parsed.req);
5253
5505
  if (result.ok) return sendJson3(res, 200, { ok: true, machineId: result.machineId });
5254
5506
  return sendJson3(res, 503, { ok: false, reason: result.reason ?? "pool unavailable" });
5255
5507
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.116",
3
+ "version": "0.4.118",
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",