@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.
- package/dist/bin/kody.js +294 -42
- 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.
|
|
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
|
|
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
|
|
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
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
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: {
|
|
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
|
-
/**
|
|
4843
|
-
|
|
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
|
|
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
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
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
|
|
5202
|
-
if (!
|
|
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
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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 =
|
|
5501
|
+
const parsed = parseClaimRequest(body);
|
|
5251
5502
|
if ("error" in parsed) return sendJson3(res, 400, { error: parsed.error });
|
|
5252
|
-
const
|
|
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.
|
|
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",
|