@kody-ade/kody-engine 0.4.116 → 0.4.117
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 +206 -30
- 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.117",
|
|
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",
|
|
@@ -4785,6 +4785,7 @@ import { createServer as createServer3 } from "http";
|
|
|
4785
4785
|
var FLY_API_BASE = "https://api.machines.dev/v1";
|
|
4786
4786
|
var POOL_METADATA_KEY = "kody_pool";
|
|
4787
4787
|
var POOL_METADATA_VALUE = "1";
|
|
4788
|
+
var POOL_REPO_METADATA_KEY = "kody_pool_repo";
|
|
4788
4789
|
var FlyClient = class {
|
|
4789
4790
|
constructor(opts) {
|
|
4790
4791
|
this.opts = opts;
|
|
@@ -4814,7 +4815,7 @@ var FlyClient = class {
|
|
|
4814
4815
|
if (!raw.trim()) return null;
|
|
4815
4816
|
return JSON.parse(raw);
|
|
4816
4817
|
}
|
|
4817
|
-
/** Create + start a pooled machine in serve mode
|
|
4818
|
+
/** Create + start a pooled machine in serve mode, tagged for `repoTag`. */
|
|
4818
4819
|
async createPooled(input) {
|
|
4819
4820
|
const body = {
|
|
4820
4821
|
region: input.region,
|
|
@@ -4824,7 +4825,10 @@ var FlyClient = class {
|
|
|
4824
4825
|
auto_destroy: true,
|
|
4825
4826
|
restart: { policy: "no" },
|
|
4826
4827
|
init: { entrypoint: ["/usr/local/bin/entrypoint-serve.sh"] },
|
|
4827
|
-
metadata: {
|
|
4828
|
+
metadata: {
|
|
4829
|
+
[POOL_METADATA_KEY]: POOL_METADATA_VALUE,
|
|
4830
|
+
[POOL_REPO_METADATA_KEY]: input.repoTag
|
|
4831
|
+
},
|
|
4828
4832
|
env: {
|
|
4829
4833
|
RUNNER_API_KEY: input.runnerApiKey,
|
|
4830
4834
|
KODY_LITELLM_URL: input.litellmUrl,
|
|
@@ -4839,11 +4843,14 @@ var FlyClient = class {
|
|
|
4839
4843
|
async get(id) {
|
|
4840
4844
|
return this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}`, { allow404: true });
|
|
4841
4845
|
}
|
|
4842
|
-
/**
|
|
4843
|
-
|
|
4846
|
+
/**
|
|
4847
|
+
* List pooled machines for `repoTag` (kody_pool + matching repo tag),
|
|
4848
|
+
* excluding destroyed/destroying. Each repo's pool sees only its own.
|
|
4849
|
+
*/
|
|
4850
|
+
async listPooled(repoTag) {
|
|
4844
4851
|
const all = await this.call(`/apps/${enc(this.opts.app)}/machines`, { allow404: true }) ?? [];
|
|
4845
4852
|
return all.filter(
|
|
4846
|
-
(m) => m.config?.metadata?.[POOL_METADATA_KEY] === POOL_METADATA_VALUE && m.state !== "destroyed" && m.state !== "destroying"
|
|
4853
|
+
(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
4854
|
);
|
|
4848
4855
|
}
|
|
4849
4856
|
/** Suspend (freeze) a machine — wakes in ~1s from the snapshot. */
|
|
@@ -4913,7 +4920,7 @@ var PoolManager = class {
|
|
|
4913
4920
|
* free; anything else is left to finish/auto-destroy. Then refill to `min`.
|
|
4914
4921
|
*/
|
|
4915
4922
|
async reconcile() {
|
|
4916
|
-
const machines = await this.deps.fly.listPooled();
|
|
4923
|
+
const machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
|
|
4917
4924
|
this.free = [];
|
|
4918
4925
|
for (const m of machines) {
|
|
4919
4926
|
if ((m.state === "suspended" || m.state === "suspending") && m.private_ip) {
|
|
@@ -4981,7 +4988,7 @@ var PoolManager = class {
|
|
|
4981
4988
|
async resync() {
|
|
4982
4989
|
let machines;
|
|
4983
4990
|
try {
|
|
4984
|
-
machines = await this.deps.fly.listPooled();
|
|
4991
|
+
machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
|
|
4985
4992
|
} catch (err) {
|
|
4986
4993
|
this.log(`resync: listPooled failed: ${errMsg2(err)}`);
|
|
4987
4994
|
return;
|
|
@@ -5031,6 +5038,7 @@ var PoolManager = class {
|
|
|
5031
5038
|
guest: cfg.guest,
|
|
5032
5039
|
runnerApiKey: cfg.runnerApiKey,
|
|
5033
5040
|
litellmUrl: cfg.litellmUrl,
|
|
5041
|
+
repoTag: cfg.repoTag,
|
|
5034
5042
|
port: cfg.port
|
|
5035
5043
|
});
|
|
5036
5044
|
if (!m.private_ip) {
|
|
@@ -5078,6 +5086,160 @@ function errMsg2(err) {
|
|
|
5078
5086
|
return err instanceof Error ? err.message : String(err);
|
|
5079
5087
|
}
|
|
5080
5088
|
|
|
5089
|
+
// src/pool/vault.ts
|
|
5090
|
+
import { createDecipheriv } from "crypto";
|
|
5091
|
+
var GITHUB_API = "https://api.github.com";
|
|
5092
|
+
var VAULT_PATH = ".kody/secrets.enc";
|
|
5093
|
+
var CACHE_TTL_MS = 6e4;
|
|
5094
|
+
var cache = /* @__PURE__ */ new Map();
|
|
5095
|
+
function decryptVault(payload, masterKey) {
|
|
5096
|
+
const parts = payload.split(":");
|
|
5097
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
5098
|
+
throw new Error("invalid vault payload format");
|
|
5099
|
+
}
|
|
5100
|
+
const [, ivB64, ctB64, tagB64] = parts;
|
|
5101
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
5102
|
+
const ct = Buffer.from(ctB64, "base64");
|
|
5103
|
+
const tag = Buffer.from(tagB64, "base64");
|
|
5104
|
+
const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
|
|
5105
|
+
decipher.setAuthTag(tag);
|
|
5106
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
|
|
5107
|
+
}
|
|
5108
|
+
async function readVaultSecrets(opts) {
|
|
5109
|
+
const key = `${opts.owner}/${opts.repo}`.toLowerCase();
|
|
5110
|
+
const hit = cache.get(key);
|
|
5111
|
+
if (hit && hit.expiresAt > Date.now()) return hit.secrets;
|
|
5112
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
5113
|
+
const res = await doFetch(
|
|
5114
|
+
`${GITHUB_API}/repos/${encodeURIComponent(opts.owner)}/${encodeURIComponent(opts.repo)}/contents/${VAULT_PATH}`,
|
|
5115
|
+
{
|
|
5116
|
+
headers: {
|
|
5117
|
+
Authorization: `Bearer ${opts.githubToken}`,
|
|
5118
|
+
Accept: "application/vnd.github+json",
|
|
5119
|
+
"User-Agent": "kody-pool-serve"
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
);
|
|
5123
|
+
if (res.status === 404) {
|
|
5124
|
+
cache.set(key, { secrets: {}, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
5125
|
+
return {};
|
|
5126
|
+
}
|
|
5127
|
+
if (!res.ok) {
|
|
5128
|
+
throw new Error(`vault read ${res.status} for ${key}: ${(await res.text().catch(() => "")).slice(0, 160)}`);
|
|
5129
|
+
}
|
|
5130
|
+
const body = await res.json();
|
|
5131
|
+
if (!body.content) {
|
|
5132
|
+
cache.set(key, { secrets: {}, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
5133
|
+
return {};
|
|
5134
|
+
}
|
|
5135
|
+
const ciphertext = Buffer.from(body.content, body.encoding ?? "base64").toString("utf8").trim();
|
|
5136
|
+
const doc = JSON.parse(decryptVault(ciphertext, opts.masterKey));
|
|
5137
|
+
const flat = {};
|
|
5138
|
+
for (const [name, entry] of Object.entries(doc.secrets ?? {})) {
|
|
5139
|
+
if (entry && typeof entry.value === "string") flat[name] = entry.value;
|
|
5140
|
+
}
|
|
5141
|
+
cache.set(key, { secrets: flat, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
5142
|
+
return flat;
|
|
5143
|
+
}
|
|
5144
|
+
async function readRepoSecret(opts) {
|
|
5145
|
+
const secrets = await readVaultSecrets(opts);
|
|
5146
|
+
const v = secrets[opts.name];
|
|
5147
|
+
return v && v.trim() ? v : null;
|
|
5148
|
+
}
|
|
5149
|
+
async function readRepoSecrets(opts) {
|
|
5150
|
+
return readVaultSecrets(opts);
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
// src/pool/registry.ts
|
|
5154
|
+
var PoolRegistry = class {
|
|
5155
|
+
constructor(cfg) {
|
|
5156
|
+
this.cfg = cfg;
|
|
5157
|
+
this.log = cfg.log ?? (() => {
|
|
5158
|
+
});
|
|
5159
|
+
this.resolveFlyToken = cfg.resolveFlyToken ?? ((owner, repo) => readRepoSecret({
|
|
5160
|
+
githubToken: cfg.githubToken,
|
|
5161
|
+
masterKey: cfg.masterKey,
|
|
5162
|
+
owner,
|
|
5163
|
+
repo,
|
|
5164
|
+
name: "FLY_API_TOKEN"
|
|
5165
|
+
}));
|
|
5166
|
+
}
|
|
5167
|
+
cfg;
|
|
5168
|
+
pools = /* @__PURE__ */ new Map();
|
|
5169
|
+
resolveFlyToken;
|
|
5170
|
+
log;
|
|
5171
|
+
key(owner, repo) {
|
|
5172
|
+
return `${owner}/${repo}`.toLowerCase();
|
|
5173
|
+
}
|
|
5174
|
+
/** Get-or-create the pool for a repo, or null if the repo has no Fly token. */
|
|
5175
|
+
async getPool(owner, repo) {
|
|
5176
|
+
const repoTag = this.key(owner, repo);
|
|
5177
|
+
const existing = this.pools.get(repoTag);
|
|
5178
|
+
if (existing) return existing;
|
|
5179
|
+
let flyToken;
|
|
5180
|
+
try {
|
|
5181
|
+
flyToken = await this.resolveFlyToken(owner, repo);
|
|
5182
|
+
} catch (err) {
|
|
5183
|
+
this.log(`registry: vault read failed for ${repoTag}: ${err instanceof Error ? err.message : String(err)}`);
|
|
5184
|
+
return null;
|
|
5185
|
+
}
|
|
5186
|
+
if (!flyToken) {
|
|
5187
|
+
this.log(`registry: ${repoTag} has no FLY_API_TOKEN \u2014 no pool`);
|
|
5188
|
+
return null;
|
|
5189
|
+
}
|
|
5190
|
+
const fly = new FlyClient({ token: flyToken, app: this.cfg.base.app });
|
|
5191
|
+
const pm = new PoolManager({
|
|
5192
|
+
fly,
|
|
5193
|
+
config: { ...this.cfg.base, repoTag },
|
|
5194
|
+
log: (m) => this.log(`[${repoTag}] ${m}`)
|
|
5195
|
+
});
|
|
5196
|
+
this.pools.set(repoTag, pm);
|
|
5197
|
+
void pm.reconcile().catch((err) => this.log(`[${repoTag}] reconcile: ${err instanceof Error ? err.message : String(err)}`));
|
|
5198
|
+
return pm;
|
|
5199
|
+
}
|
|
5200
|
+
async claim(owner, repo, req) {
|
|
5201
|
+
const pm = await this.getPool(owner, repo);
|
|
5202
|
+
if (!pm) return { ok: false, reason: "repo has no FLY_API_TOKEN (no pool)" };
|
|
5203
|
+
let allSecrets = {};
|
|
5204
|
+
try {
|
|
5205
|
+
const vault = await readRepoSecrets({
|
|
5206
|
+
githubToken: this.cfg.githubToken,
|
|
5207
|
+
masterKey: this.cfg.masterKey,
|
|
5208
|
+
owner,
|
|
5209
|
+
repo
|
|
5210
|
+
});
|
|
5211
|
+
allSecrets = Object.fromEntries(Object.entries(vault).filter(([k]) => k !== "FLY_API_TOKEN"));
|
|
5212
|
+
} catch (err) {
|
|
5213
|
+
this.log(`[${this.key(owner, repo)}] vault secrets read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5214
|
+
}
|
|
5215
|
+
const job = {
|
|
5216
|
+
jobId: req.jobId,
|
|
5217
|
+
repo: `${owner}/${repo}`,
|
|
5218
|
+
issueNumber: req.issueNumber,
|
|
5219
|
+
githubToken: this.cfg.githubToken,
|
|
5220
|
+
ref: req.ref,
|
|
5221
|
+
allSecrets,
|
|
5222
|
+
model: req.model,
|
|
5223
|
+
sessionId: req.sessionId,
|
|
5224
|
+
dashboardUrl: req.dashboardUrl
|
|
5225
|
+
};
|
|
5226
|
+
return pm.claim(job);
|
|
5227
|
+
}
|
|
5228
|
+
/** Status for a single repo's pool, or null if none exists yet. */
|
|
5229
|
+
status(owner, repo) {
|
|
5230
|
+
return this.pools.get(this.key(owner, repo))?.status() ?? null;
|
|
5231
|
+
}
|
|
5232
|
+
/** Resync every active repo pool (periodic self-heal). */
|
|
5233
|
+
async resyncAll() {
|
|
5234
|
+
for (const [repoTag, pm] of this.pools) {
|
|
5235
|
+
await pm.resync().catch((err) => this.log(`[${repoTag}] resync: ${err instanceof Error ? err.message : String(err)}`));
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
activeRepos() {
|
|
5239
|
+
return [...this.pools.keys()];
|
|
5240
|
+
}
|
|
5241
|
+
};
|
|
5242
|
+
|
|
5081
5243
|
// src/pool/keys.ts
|
|
5082
5244
|
import { hkdfSync } from "crypto";
|
|
5083
5245
|
var POOL_API_KEY_INFO = "kody-pool-api:v1";
|
|
@@ -5149,7 +5311,7 @@ function readJsonBody3(req) {
|
|
|
5149
5311
|
req.on("error", reject);
|
|
5150
5312
|
});
|
|
5151
5313
|
}
|
|
5152
|
-
function
|
|
5314
|
+
function parseClaimRequest(body) {
|
|
5153
5315
|
if (typeof body !== "object" || body === null) return { error: "body must be an object" };
|
|
5154
5316
|
const b = body;
|
|
5155
5317
|
const jobId = typeof b.jobId === "string" ? b.jobId.trim() : "";
|
|
@@ -5158,15 +5320,12 @@ function parsePoolJob(body) {
|
|
|
5158
5320
|
if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
|
|
5159
5321
|
const issueNumber = Number(b.issueNumber);
|
|
5160
5322
|
if (!Number.isInteger(issueNumber) || issueNumber <= 0) return { error: "issueNumber required" };
|
|
5161
|
-
const
|
|
5162
|
-
if (
|
|
5163
|
-
|
|
5164
|
-
if (typeof b.
|
|
5165
|
-
if (typeof b.
|
|
5166
|
-
|
|
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 };
|
|
5323
|
+
const req = { jobId, repo, issueNumber };
|
|
5324
|
+
if (typeof b.ref === "string" && b.ref.trim()) req.ref = b.ref.trim();
|
|
5325
|
+
if (typeof b.model === "string" && b.model.trim()) req.model = b.model.trim();
|
|
5326
|
+
if (typeof b.sessionId === "string" && b.sessionId.trim()) req.sessionId = b.sessionId.trim();
|
|
5327
|
+
if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) req.dashboardUrl = b.dashboardUrl.trim();
|
|
5328
|
+
return { req };
|
|
5170
5329
|
}
|
|
5171
5330
|
function superviseLitellm() {
|
|
5172
5331
|
if (process.env.POOL_DISABLE_LITELLM === "1") return null;
|
|
@@ -5198,8 +5357,8 @@ var poolServe = async (ctx) => {
|
|
|
5198
5357
|
ctx.skipAgent = true;
|
|
5199
5358
|
const masterRaw = process.env.KODY_MASTER_KEY?.trim();
|
|
5200
5359
|
if (!masterRaw) throw new Error("KODY_MASTER_KEY required for pool-serve");
|
|
5201
|
-
const
|
|
5202
|
-
if (!
|
|
5360
|
+
const githubToken = process.env.GITHUB_TOKEN?.trim();
|
|
5361
|
+
if (!githubToken) throw new Error("GITHUB_TOKEN required for pool-serve (reads per-repo vaults)");
|
|
5203
5362
|
const master = masterKeyBytes(masterRaw);
|
|
5204
5363
|
const poolApiKey = derivePoolApiKey(master);
|
|
5205
5364
|
const runnerApiKey = deriveRunnerApiKey(master);
|
|
@@ -5213,23 +5372,36 @@ var poolServe = async (ctx) => {
|
|
|
5213
5372
|
const apiPort = envInt("POOL_API_PORT", 4100);
|
|
5214
5373
|
const healthTimeoutMs = envInt("POOL_HEALTH_TIMEOUT_MS", 12e4);
|
|
5215
5374
|
const litellm = superviseLitellm();
|
|
5216
|
-
const
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5375
|
+
const registry = new PoolRegistry({
|
|
5376
|
+
githubToken,
|
|
5377
|
+
masterKey: master,
|
|
5378
|
+
base: {
|
|
5379
|
+
min,
|
|
5380
|
+
image: process.env.FLY_RUNNER_IMAGE ?? "registry.fly.io/kody-runner:latest",
|
|
5381
|
+
region,
|
|
5382
|
+
guest,
|
|
5383
|
+
runnerApiKey,
|
|
5384
|
+
litellmUrl,
|
|
5385
|
+
port: runnerPort,
|
|
5386
|
+
healthTimeoutMs,
|
|
5387
|
+
app
|
|
5388
|
+
},
|
|
5220
5389
|
log
|
|
5221
5390
|
});
|
|
5222
|
-
manager.reconcile().catch((err) => log(`reconcile failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
5223
5391
|
const refillMs = envInt("POOL_REFILL_INTERVAL_MS", 6e4);
|
|
5224
5392
|
const tick = setInterval(() => {
|
|
5225
|
-
|
|
5393
|
+
registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
5226
5394
|
}, refillMs);
|
|
5227
5395
|
const server = createServer3(async (req, res) => {
|
|
5228
5396
|
try {
|
|
5229
5397
|
if (!req.method || !req.url) return sendJson3(res, 400, { error: "bad request" });
|
|
5230
5398
|
const url = new URL(req.url, "http://localhost");
|
|
5231
5399
|
if (req.method === "GET" && url.pathname === "/healthz") {
|
|
5232
|
-
return sendJson3(res, 200, {
|
|
5400
|
+
return sendJson3(res, 200, {
|
|
5401
|
+
ok: true,
|
|
5402
|
+
litellm: litellm ? "supervised" : "off",
|
|
5403
|
+
repos: registry.activeRepos()
|
|
5404
|
+
});
|
|
5233
5405
|
}
|
|
5234
5406
|
const authed = bearerOk(
|
|
5235
5407
|
req.headers["authorization"],
|
|
@@ -5238,7 +5410,10 @@ var poolServe = async (ctx) => {
|
|
|
5238
5410
|
);
|
|
5239
5411
|
if (!authed) return sendJson3(res, 401, { error: "unauthorized" });
|
|
5240
5412
|
if (req.method === "GET" && url.pathname === "/pool/status") {
|
|
5241
|
-
|
|
5413
|
+
const repoParam = (url.searchParams.get("repo") ?? "").trim();
|
|
5414
|
+
const [owner, repo] = repoParam.split("/");
|
|
5415
|
+
if (!owner || !repo) return sendJson3(res, 400, { error: "repo query (owner/name) required" });
|
|
5416
|
+
return sendJson3(res, 200, { status: registry.status(owner, repo) });
|
|
5242
5417
|
}
|
|
5243
5418
|
if (req.method === "POST" && url.pathname === "/pool/claim") {
|
|
5244
5419
|
let body;
|
|
@@ -5247,9 +5422,10 @@ var poolServe = async (ctx) => {
|
|
|
5247
5422
|
} catch {
|
|
5248
5423
|
return sendJson3(res, 400, { error: "invalid JSON body" });
|
|
5249
5424
|
}
|
|
5250
|
-
const parsed =
|
|
5425
|
+
const parsed = parseClaimRequest(body);
|
|
5251
5426
|
if ("error" in parsed) return sendJson3(res, 400, { error: parsed.error });
|
|
5252
|
-
const
|
|
5427
|
+
const [owner, repo] = parsed.req.repo.split("/");
|
|
5428
|
+
const result = await registry.claim(owner, repo, parsed.req);
|
|
5253
5429
|
if (result.ok) return sendJson3(res, 200, { ok: true, machineId: result.machineId });
|
|
5254
5430
|
return sendJson3(res, 503, { ok: false, reason: result.reason ?? "pool unavailable" });
|
|
5255
5431
|
}
|
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.117",
|
|
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",
|