@sema-agent/server 1.313.0 → 1.315.0

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 (83) hide show
  1. package/dist/approval-hmac.d.ts +17 -0
  2. package/dist/approval-hmac.js +27 -0
  3. package/dist/auth-bridge.d.ts +3 -2
  4. package/dist/bench/s1/live-deps.js +2 -2
  5. package/dist/budget.d.ts +4 -76
  6. package/dist/budget.js +4 -83
  7. package/dist/capabilities/sandbox-file-send.d.ts +2 -1
  8. package/dist/config-center/apply-effective.d.ts +22 -0
  9. package/dist/config-center/apply-effective.js +283 -0
  10. package/dist/config-center/http-client.d.ts +23 -0
  11. package/dist/config-center/http-client.js +109 -0
  12. package/dist/config-center/restart-signal.d.ts +12 -0
  13. package/dist/config-center/restart-signal.js +70 -0
  14. package/dist/config-center/skills-mcp.d.ts +13 -0
  15. package/dist/config-center/skills-mcp.js +113 -0
  16. package/dist/config-center/types.d.ts +143 -0
  17. package/dist/config-center/types.js +2 -0
  18. package/dist/config-types.d.ts +1 -1
  19. package/dist/elicitation.d.ts +5 -3
  20. package/dist/elicitation.js +3 -2
  21. package/dist/fleet/fleet-bus.js +92 -83
  22. package/dist/fleet-lease.d.ts +1 -1
  23. package/dist/fleet-lease.js +1 -1
  24. package/dist/hooks/hook-llm.d.ts +5 -4
  25. package/dist/hooks/hook-runner.d.ts +1 -1
  26. package/dist/hooks/hook-runner.js +19 -10
  27. package/dist/http/server.d.ts +95 -69
  28. package/dist/http/server.js +75 -86
  29. package/dist/index.d.ts +1 -1
  30. package/dist/key-resolver.d.ts +1 -1
  31. package/dist/key-resolver.js +1 -1
  32. package/dist/leader/grader-env-factory.d.ts +1 -1
  33. package/dist/leader/grader-env-factory.js +2 -2
  34. package/dist/leader/leader.js +5 -2
  35. package/dist/leader/wire.d.ts +1 -1
  36. package/dist/leader/wire.js +170 -166
  37. package/dist/main.js +487 -476
  38. package/dist/memory-scope.d.ts +18 -0
  39. package/dist/memory-scope.js +38 -0
  40. package/dist/observability/cost-taxonomy.d.ts +34 -0
  41. package/dist/observability/cost-taxonomy.js +26 -0
  42. package/dist/observability/prompt-manifest.d.ts +75 -0
  43. package/dist/observability/prompt-manifest.js +82 -0
  44. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  45. package/dist/plugins/checkpoint-store-sql.js +224 -0
  46. package/dist/plugins/image-bake-store-sql.d.ts +53 -0
  47. package/dist/plugins/image-bake-store-sql.js +463 -0
  48. package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
  49. package/dist/plugins/k8s-bg-scripts.js +129 -0
  50. package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
  51. package/dist/plugins/k8s-exec-protocol.js +87 -0
  52. package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
  53. package/dist/plugins/pg-checkpoint-store.js +1 -189
  54. package/dist/plugins/pg-cost-quota.js +3 -143
  55. package/dist/plugins/pg-image-bake.d.ts +1 -40
  56. package/dist/plugins/pg-image-bake.js +1 -420
  57. package/dist/plugins/pg-rate-limiter.d.ts +2 -32
  58. package/dist/plugins/pg-rate-limiter.js +4 -147
  59. package/dist/plugins/remote-env-k8s.d.ts +5 -38
  60. package/dist/plugins/remote-env-k8s.js +7 -213
  61. package/dist/plugins/sql-driver.d.ts +25 -0
  62. package/dist/plugins/sql-driver.js +59 -0
  63. package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
  64. package/dist/plugins/tidb-checkpoint-store.js +1 -191
  65. package/dist/plugins/tidb-image-bake.d.ts +1 -38
  66. package/dist/plugins/tidb-image-bake.js +1 -348
  67. package/dist/plugins/web-search.d.ts +4 -3
  68. package/dist/plugins/write-behind-counter.d.ts +14 -3
  69. package/dist/plugins/write-behind-counter.js +39 -12
  70. package/dist/principal-jwt.d.ts +44 -0
  71. package/dist/principal-jwt.js +95 -0
  72. package/dist/question.d.ts +2 -1
  73. package/dist/question.js +3 -2
  74. package/dist/run-local.js +2 -2
  75. package/dist/runtime-caps-resolver.d.ts +7 -7
  76. package/dist/runtime-caps-resolver.js +3 -3
  77. package/dist/security.d.ts +4 -74
  78. package/dist/security.js +6 -155
  79. package/dist/sema-registry.d.ts +5 -200
  80. package/dist/sema-registry.js +4 -567
  81. package/dist/tool-approval.d.ts +2 -1
  82. package/dist/tool-approval.js +3 -2
  83. package/package.json +1 -1
@@ -0,0 +1,463 @@
1
+ import { uuidv7 } from "@sema-agent/core";
2
+ import { randomBytes } from "node:crypto";
3
+ import { pgSanitizeText, pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
4
+ import { parseJsonOr as parseJson, toIso as iso } from "./sql-row-helpers.js";
5
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
+ import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS, } from "./store-contracts.js";
7
+ export const PG_IMAGE_BAKE_SCHEMA = [
8
+ `CREATE TABLE IF NOT EXISTS image_bake (
9
+ bake_id VARCHAR(64) NOT NULL,
10
+ profile VARCHAR(128) NOT NULL,
11
+ bands JSONB NULL,
12
+ base_ref VARCHAR(255) NULL,
13
+ push SMALLINT NOT NULL DEFAULT 0,
14
+ dry_run SMALLINT NOT NULL DEFAULT 0,
15
+ logs SMALLINT NOT NULL DEFAULT 0,
16
+ argv JSONB NOT NULL,
17
+ status VARCHAR(16) NOT NULL,
18
+ state VARCHAR(16) NULL,
19
+ digest VARCHAR(255) NULL,
20
+ repo VARCHAR(255) NULL,
21
+ ref VARCHAR(320) NULL,
22
+ index_id VARCHAR(64) NULL,
23
+ exit_code INT NULL,
24
+ error TEXT NULL,
25
+ error_code VARCHAR(32) NULL,
26
+ manifest_sha VARCHAR(64) NULL,
27
+ tag VARCHAR(255) NULL,
28
+ idem_key VARCHAR(255) NULL,
29
+ ingest_secret VARCHAR(64) NULL,
30
+ runner_id VARCHAR(64) NULL,
31
+ lease_until TIMESTAMPTZ(3) NULL,
32
+ cancel_requested SMALLINT NOT NULL DEFAULT 0,
33
+ requested_by VARCHAR(128) NULL,
34
+ created_at TIMESTAMPTZ(3) NOT NULL,
35
+ updated_at TIMESTAMPTZ(3) NOT NULL,
36
+ PRIMARY KEY (bake_id),
37
+ CONSTRAINT uq_image_bake_idem UNIQUE (idem_key)
38
+ )`,
39
+ `CREATE INDEX IF NOT EXISTS idx_image_bake_status ON image_bake (status)`,
40
+ `CREATE INDEX IF NOT EXISTS idx_image_bake_created ON image_bake (created_at)`,
41
+ `CREATE TABLE IF NOT EXISTS image_bake_event (
42
+ bake_id VARCHAR(64) NOT NULL,
43
+ seq INT NOT NULL,
44
+ line_ord INT NULL,
45
+ kind VARCHAR(24) NOT NULL,
46
+ level VARCHAR(8) NULL,
47
+ data JSONB NULL,
48
+ ts TIMESTAMPTZ(3) NOT NULL,
49
+ PRIMARY KEY (bake_id, seq),
50
+ CONSTRAINT uq_image_bake_event_line UNIQUE (bake_id, line_ord)
51
+ )`,
52
+ `CREATE TABLE IF NOT EXISTS image_bake_lease (
53
+ pool VARCHAR(32) NOT NULL DEFAULT 'standard',
54
+ bake_id VARCHAR(64) NOT NULL,
55
+ acquired_at TIMESTAMPTZ(3) NOT NULL,
56
+ expires_at TIMESTAMPTZ(3) NOT NULL,
57
+ PRIMARY KEY (pool)
58
+ )`,
59
+ `CREATE TABLE IF NOT EXISTS image_bake_admit (
60
+ pool VARCHAR(32) NOT NULL,
61
+ PRIMARY KEY (pool)
62
+ )`,
63
+ ];
64
+ export async function ensurePgImageBakeSchema(pool, poolName = "standard") {
65
+ for (const stmt of PG_IMAGE_BAKE_SCHEMA)
66
+ await pool.query(stmt);
67
+ await pool.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [poolName]);
68
+ }
69
+ function isDupKey(dialect, err) {
70
+ const code = err?.code;
71
+ return Boolean(err) && code === (dialect === "tidb" ? "ER_DUP_ENTRY" : "23505");
72
+ }
73
+ function dupKeyName(dialect, err) {
74
+ if (dialect === "tidb") {
75
+ const msg = String(err?.sqlMessage ?? err?.message ?? "");
76
+ if (/uq_line/i.test(msg))
77
+ return "uq_line";
78
+ if (/PRIMARY/i.test(msg))
79
+ return "primary";
80
+ return "other";
81
+ }
82
+ const c = String(err?.constraint ?? "");
83
+ if (c === "uq_image_bake_event_line")
84
+ return "uq_line";
85
+ if (/pkey$/i.test(c))
86
+ return "primary";
87
+ return "other";
88
+ }
89
+ const INSERT_BAKE_COLS = "INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
90
+ "idem_key, cancel_requested, requested_by, created_at, updated_at) ";
91
+ export class SqlImageBake {
92
+ db;
93
+ poolName;
94
+ constructor(db, poolName = "standard") {
95
+ this.db = db;
96
+ this.poolName = poolName;
97
+ }
98
+ q(tidb, pg) {
99
+ return this.db.dialect === "tidb" ? tidb : pg;
100
+ }
101
+ insertParams(bakeId, input, now) {
102
+ return [
103
+ bakeId,
104
+ input.profile,
105
+ input.bands == null ? null : JSON.stringify(input.bands),
106
+ input.baseRef,
107
+ input.push ? 1 : 0,
108
+ input.dryRun ? 1 : 0,
109
+ input.logs ? 1 : 0,
110
+ JSON.stringify(input.argv),
111
+ input.idemKey,
112
+ input.requestedBy,
113
+ now,
114
+ now,
115
+ ];
116
+ }
117
+ async createBake(input) {
118
+ const bakeId = uuidv7();
119
+ const now = new Date();
120
+ try {
121
+ await this.db.query(this.q(INSERT_BAKE_COLS + "VALUES (?,?,?,?,?,?,?,?,'queued',?,0,?,?,?)", INSERT_BAKE_COLS + "VALUES ($1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12)"), this.insertParams(bakeId, input, now));
122
+ }
123
+ catch (err) {
124
+ if (isDupKey(this.db.dialect, err) && input.idemKey) {
125
+ const existing = await this.getBakeByIdem(input.idemKey);
126
+ if (existing)
127
+ return { created: false, bake: existing };
128
+ }
129
+ throw err;
130
+ }
131
+ const bake = await this.getBake(bakeId);
132
+ if (!bake)
133
+ throw new Error("createBake: row vanished after insert");
134
+ return { created: true, bake };
135
+ }
136
+ async createBakeIfIdle(input) {
137
+ const bakeId = uuidv7();
138
+ const now = new Date();
139
+ const params = this.insertParams(bakeId, input, now);
140
+ const conn = await this.db.connect();
141
+ let inserted = false;
142
+ try {
143
+ if (this.db.dialect === "tidb") {
144
+ await conn.beginPessimistic();
145
+ await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES (?)", [this.poolName]);
146
+ await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
147
+ const busy = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
148
+ if (busy.rows.length === 0) {
149
+ await conn.query(INSERT_BAKE_COLS + "VALUES (?,?,?,?,?,?,?,?,'queued',?,0,?,?,?)", params);
150
+ inserted = true;
151
+ }
152
+ await conn.commit();
153
+ }
154
+ else {
155
+ await conn.begin();
156
+ await conn.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [this.poolName]);
157
+ await conn.query("SELECT pool FROM image_bake_admit WHERE pool = $1 FOR UPDATE", [this.poolName]);
158
+ const res = await conn.query(INSERT_BAKE_COLS +
159
+ "SELECT $1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12 WHERE NOT EXISTS " +
160
+ "(SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0)", params);
161
+ inserted = res.affected > 0;
162
+ await conn.commit();
163
+ }
164
+ }
165
+ catch (err) {
166
+ try {
167
+ await conn.rollback();
168
+ }
169
+ catch {
170
+ }
171
+ throw err;
172
+ }
173
+ finally {
174
+ conn.release();
175
+ }
176
+ if (!inserted)
177
+ return { created: false, bake: null };
178
+ const bake = await this.getBake(bakeId);
179
+ if (!bake)
180
+ throw new Error("createBakeIfIdle: row vanished after insert");
181
+ return { created: true, bake };
182
+ }
183
+ async getBake(bakeId) {
184
+ const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE bake_id = ? LIMIT 1`, `SELECT ${SELECT_COLS} FROM image_bake WHERE bake_id = $1 LIMIT 1`), [bakeId]);
185
+ return rows[0] ? mapRow(rows[0]) : null;
186
+ }
187
+ async getBakeByIdem(idemKey) {
188
+ const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = ? LIMIT 1`, `SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = $1 LIMIT 1`), [idemKey]);
189
+ return rows[0] ? mapRow(rows[0]) : null;
190
+ }
191
+ async findActiveByArgv(argv) {
192
+ const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = CAST(? AS JSON) ` +
193
+ "ORDER BY created_at DESC LIMIT 1", `SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = $1::jsonb ` +
194
+ "ORDER BY created_at DESC LIMIT 1"), [JSON.stringify(argv)]);
195
+ return rows[0] ? mapRow(rows[0]) : null;
196
+ }
197
+ async findActiveAny() {
198
+ const { rows } = await this.db.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
199
+ "ORDER BY created_at ASC LIMIT 1", []);
200
+ return rows[0] ? mapRow(rows[0]) : null;
201
+ }
202
+ async findNextQueuedId() {
203
+ const { rows } = await this.db.query("SELECT bake_id FROM image_bake WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1", []);
204
+ return rows[0] ? String(rows[0].bake_id) : null;
205
+ }
206
+ async claimBake(bakeId, runnerId, leaseMs) {
207
+ const conn = await this.db.connect();
208
+ try {
209
+ await conn.begin();
210
+ const now = new Date();
211
+ const leaseUntil = new Date(now.getTime() + leaseMs);
212
+ const ingestSecret = randomBytes(24).toString("hex");
213
+ const bakeRes = await conn.query(this.q("UPDATE image_bake SET status='running', state='PENDING', runner_id=?, ingest_secret=?, " +
214
+ "lease_until=?, updated_at=? WHERE bake_id=? AND status='queued'", "UPDATE image_bake SET status='running', state='PENDING', runner_id=$1, ingest_secret=$2, " +
215
+ "lease_until=$3, updated_at=$4 WHERE bake_id=$5 AND status='queued'"), [runnerId, ingestSecret, leaseUntil, now, bakeId]);
216
+ if (bakeRes.affected !== 1) {
217
+ await conn.rollback();
218
+ return null;
219
+ }
220
+ const dry = await conn.query(this.q("SELECT dry_run FROM image_bake WHERE bake_id=?", "SELECT dry_run FROM image_bake WHERE bake_id=$1"), [bakeId]);
221
+ if (Boolean(dry.rows[0]?.dry_run)) {
222
+ await conn.commit();
223
+ return (await this.getBake(bakeId)) ?? null;
224
+ }
225
+ const leaseSel = await conn.query(this.q("SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=?", "SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=$1"), [this.poolName]);
226
+ const cur = leaseSel.rows[0];
227
+ if (!cur) {
228
+ try {
229
+ await conn.query(this.q("INSERT INTO image_bake_lease (pool, bake_id, acquired_at, expires_at) VALUES (?,?,?,?)", "INSERT INTO image_bake_lease (pool, bake_id, acquired_at, expires_at) VALUES ($1,$2,$3,$4)"), [this.poolName, bakeId, now, leaseUntil]);
230
+ }
231
+ catch (err) {
232
+ if (isDupKey(this.db.dialect, err)) {
233
+ await conn.rollback();
234
+ return null;
235
+ }
236
+ throw err;
237
+ }
238
+ }
239
+ else {
240
+ if (new Date(cur.expires_at).getTime() >= now.getTime()) {
241
+ await conn.rollback();
242
+ return null;
243
+ }
244
+ const holderId = String(cur.bake_id ?? "");
245
+ const holder = await conn.query(this.q("SELECT status FROM image_bake WHERE bake_id=?", "SELECT status FROM image_bake WHERE bake_id=$1"), [holderId]);
246
+ const holderStatus = String(holder.rows[0]?.status ?? "");
247
+ if (holderStatus === "queued" || holderStatus === "running") {
248
+ await conn.rollback();
249
+ return null;
250
+ }
251
+ const stealRes = await conn.query(this.q("UPDATE image_bake_lease SET bake_id=?, acquired_at=?, expires_at=? WHERE pool=? AND bake_id=? AND expires_at=?", "UPDATE image_bake_lease SET bake_id=$1, acquired_at=$2, expires_at=$3 WHERE pool=$4 AND bake_id=$5 AND expires_at=$6"), [bakeId, now, leaseUntil, this.poolName, holderId, cur.expires_at]);
252
+ if (stealRes.affected === 0) {
253
+ await conn.rollback();
254
+ return null;
255
+ }
256
+ }
257
+ const check = await conn.query(this.q("SELECT bake_id FROM image_bake_lease WHERE pool=?", "SELECT bake_id FROM image_bake_lease WHERE pool=$1"), [this.poolName]);
258
+ if (String(check.rows[0]?.bake_id ?? "") !== bakeId) {
259
+ await conn.rollback();
260
+ return null;
261
+ }
262
+ await conn.commit();
263
+ return (await this.getBake(bakeId)) ?? null;
264
+ }
265
+ catch (err) {
266
+ try {
267
+ await conn.rollback();
268
+ }
269
+ catch {
270
+ }
271
+ throw err;
272
+ }
273
+ finally {
274
+ conn.release();
275
+ }
276
+ }
277
+ async appendEvent(bakeId, kind, data, opts = {}) {
278
+ const lineOrd = opts.lineOrd ?? null;
279
+ if (lineOrd !== null && (await this.lineOrdExists(bakeId, lineOrd)))
280
+ return null;
281
+ const payload = data == null ? null : this.db.dialect === "tidb" ? JSON.stringify(data) : pgSafeJsonStringify(data);
282
+ for (let attempt = 0; attempt < 50; attempt += 1) {
283
+ const seq = (await this.maxSeq(bakeId)) + 1;
284
+ try {
285
+ await this.db.query(this.q("INSERT INTO image_bake_event (bake_id, seq, line_ord, kind, level, data, ts) VALUES (?,?,?,?,?,?,?)", "INSERT INTO image_bake_event (bake_id, seq, line_ord, kind, level, data, ts) VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7)"), [bakeId, seq, lineOrd, kind, opts.level ?? null, payload, new Date()]);
286
+ return seq;
287
+ }
288
+ catch (err) {
289
+ if (!isDupKey(this.db.dialect, err))
290
+ throw err;
291
+ const which = dupKeyName(this.db.dialect, err);
292
+ if (which === "uq_line" && lineOrd !== null)
293
+ return null;
294
+ await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5)));
295
+ }
296
+ }
297
+ throw new Error(`appendEvent: seq allocation lost the (bake_id,seq) race after retries (bakeId=${bakeId})`);
298
+ }
299
+ async lineOrdExists(bakeId, lineOrd) {
300
+ const { rows } = await this.db.query(this.q("SELECT 1 FROM image_bake_event WHERE bake_id = ? AND line_ord = ? LIMIT 1", "SELECT 1 FROM image_bake_event WHERE bake_id = $1 AND line_ord = $2 LIMIT 1"), [bakeId, lineOrd]);
301
+ return rows.length > 0;
302
+ }
303
+ async maxSeq(bakeId) {
304
+ const { rows } = await this.db.query(this.q("SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
305
+ const m = rows[0]?.m;
306
+ return m == null ? 0 : Number(m);
307
+ }
308
+ async retainedFrom(bakeId) {
309
+ const { rows } = await this.db.query(this.q("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
310
+ const m = rows[0]?.m;
311
+ return m == null ? 0 : Number(m);
312
+ }
313
+ async getEvents(bakeId, afterSeq) {
314
+ const { rows } = await this.db.query(this.q("SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = ? AND seq > ? ORDER BY seq ASC", "SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = $1 AND seq > $2 ORDER BY seq ASC"), [bakeId, afterSeq]);
315
+ return rows.map((r) => ({
316
+ seq: Number(r.seq),
317
+ lineOrd: r.line_ord === null ? null : Number(r.line_ord),
318
+ kind: String(r.kind),
319
+ level: r.level ?? null,
320
+ data: parseJson(r.data, null),
321
+ ts: iso(r.ts),
322
+ }));
323
+ }
324
+ async setStatus(bakeId, status) {
325
+ await this.db.query(this.q("UPDATE image_bake SET status = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET status = $1, updated_at = $2 WHERE bake_id = $3"), [status, new Date(), bakeId]);
326
+ }
327
+ async setState(bakeId, state) {
328
+ await this.db.query(this.q("UPDATE image_bake SET state = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET state = $1, updated_at = $2 WHERE bake_id = $3"), [state, new Date(), bakeId]);
329
+ }
330
+ async setTerminal(bakeId, t) {
331
+ let affectedRows = 0;
332
+ await this.tx(async (conn) => {
333
+ const res = await conn.query(this.q("UPDATE image_bake SET status=?, state=?, digest=?, repo=?, ref=?, index_id=?, exit_code=?, error=?, " +
334
+ "error_code=?, manifest_sha=?, tag=?, ingest_secret=NULL, updated_at=? WHERE bake_id=? AND status IN ('queued','running')", "UPDATE image_bake SET status=$1, state=$2, digest=$3, repo=$4, ref=$5, index_id=$6, exit_code=$7, error=$8, " +
335
+ "error_code=$9, manifest_sha=$10, tag=$11, ingest_secret=NULL, updated_at=$12 WHERE bake_id=$13 AND status IN ('queued','running')"), this.db.dialect === "tidb" ? this.terminalParamsTidb(bakeId, t) : this.terminalParamsPg(bakeId, t));
336
+ affectedRows = res.affected;
337
+ await conn.query(this.q("DELETE FROM image_bake_lease WHERE pool = ? AND bake_id = ?", "DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2"), [this.poolName, bakeId]);
338
+ });
339
+ return affectedRows;
340
+ }
341
+ terminalParamsTidb(bakeId, t) {
342
+ return [
343
+ t.status,
344
+ t.state,
345
+ t.digest ?? null,
346
+ t.repo ?? null,
347
+ t.ref ?? null,
348
+ t.indexId ?? null,
349
+ t.exitCode ?? null,
350
+ t.error ?? null,
351
+ t.errorCode ?? null,
352
+ t.manifestSha ?? null,
353
+ t.tag ?? null,
354
+ new Date(),
355
+ bakeId,
356
+ ];
357
+ }
358
+ terminalParamsPg(bakeId, t) {
359
+ const invalidIdentity = (t.digest != null && pgHasUnstorable(t.digest)) ||
360
+ (t.manifestSha != null && pgHasUnstorable(t.manifestSha)) ||
361
+ (t.repo != null && pgHasUnstorable(t.repo)) ||
362
+ (t.ref != null && pgHasUnstorable(t.ref));
363
+ return [
364
+ invalidIdentity ? "failed" : t.status,
365
+ t.state,
366
+ invalidIdentity ? null : t.digest ?? null,
367
+ invalidIdentity ? null : t.repo ?? null,
368
+ invalidIdentity ? null : t.ref ?? null,
369
+ t.indexId ?? null,
370
+ t.exitCode ?? null,
371
+ t.error == null ? null : pgSanitizeText(t.error),
372
+ t.errorCode == null ? null : pgSanitizeText(t.errorCode),
373
+ invalidIdentity ? null : t.manifestSha ?? null,
374
+ t.tag == null ? null : pgSanitizeText(t.tag),
375
+ new Date(),
376
+ bakeId,
377
+ ];
378
+ }
379
+ async setIndexId(bakeId, indexId) {
380
+ const res = await this.db.query(this.q("UPDATE image_bake SET index_id = ?, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", "UPDATE image_bake SET index_id = $1, updated_at = $2 WHERE bake_id = $3 AND status IN ('queued','running')"), [indexId, new Date(), bakeId]);
381
+ return res.affected >= 1;
382
+ }
383
+ async heartbeatLease(bakeId, runnerId, leaseMs) {
384
+ const now = new Date();
385
+ const leaseUntil = new Date(now.getTime() + leaseMs);
386
+ const res = await this.db.query(this.q("UPDATE image_bake SET lease_until = ?, updated_at = ? WHERE bake_id = ? AND runner_id = ? AND status = 'running'", "UPDATE image_bake SET lease_until = $1, updated_at = $2 WHERE bake_id = $3 AND runner_id = $4 AND status = 'running'"), [leaseUntil, now, bakeId, runnerId]);
387
+ if (res.affected < 1)
388
+ return false;
389
+ const dry = await this.db.query(this.q("SELECT dry_run FROM image_bake WHERE bake_id = ?", "SELECT dry_run FROM image_bake WHERE bake_id = $1"), [bakeId]);
390
+ if (Boolean(dry.rows[0]?.dry_run))
391
+ return true;
392
+ const leaseRes = await this.db.query(this.q("UPDATE image_bake_lease SET expires_at = ? WHERE pool = ? AND bake_id = ?", "UPDATE image_bake_lease SET expires_at = $1 WHERE pool = $2 AND bake_id = $3"), [leaseUntil, this.poolName, bakeId]);
393
+ return leaseRes.affected >= 1;
394
+ }
395
+ async requestCancel(bakeId) {
396
+ const res = await this.db.query(this.q("UPDATE image_bake SET cancel_requested = 1, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", "UPDATE image_bake SET cancel_requested = 1, updated_at = $1 WHERE bake_id = $2 AND status IN ('queued','running')"), [new Date(), bakeId]);
397
+ return res.affected > 0;
398
+ }
399
+ async isCancelRequested(bakeId) {
400
+ const { rows } = await this.db.query(this.q("SELECT cancel_requested FROM image_bake WHERE bake_id = ?", "SELECT cancel_requested FROM image_bake WHERE bake_id = $1"), [bakeId]);
401
+ return Boolean(rows[0]?.cancel_requested);
402
+ }
403
+ async reapStaleBakes(olderThanMs) {
404
+ const cutoff = new Date(Date.now() - olderThanMs);
405
+ const victims = await this.db.query(this.q("SELECT bake_id FROM image_bake WHERE status = 'running' AND " + "(lease_until IS NULL OR lease_until < ?) AND updated_at < ?", "SELECT bake_id FROM image_bake WHERE status = 'running' AND " + "(lease_until IS NULL OR lease_until < $1) AND updated_at < $2"), [cutoff, cutoff]);
406
+ let reaped = 0;
407
+ for (const v of victims.rows) {
408
+ const bakeId = String(v.bake_id);
409
+ const res = await this.db.query(this.q("UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
410
+ "ingest_secret=NULL, index_id=NULL, updated_at=? WHERE bake_id=? AND status='running'", "UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
411
+ "ingest_secret=NULL, index_id=NULL, updated_at=$1 WHERE bake_id=$2 AND status='running'"), [new Date(), bakeId]);
412
+ if (res.affected === 1) {
413
+ reaped += 1;
414
+ try {
415
+ await this.appendEvent(bakeId, "done", {
416
+ status: "FAILED",
417
+ digest: null,
418
+ repo: null,
419
+ ref: null,
420
+ indexId: null,
421
+ exitCode: null,
422
+ error: "bake-runner lost (lease expired)",
423
+ errorCode: null,
424
+ });
425
+ }
426
+ catch {
427
+ }
428
+ await this.db.query(this.q("DELETE FROM image_bake_lease WHERE pool = ? AND bake_id = ?", "DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2"), [this.poolName, bakeId]);
429
+ }
430
+ }
431
+ return reaped;
432
+ }
433
+ async tx(fn) {
434
+ const conn = await this.db.connect();
435
+ try {
436
+ await conn.begin();
437
+ await fn(conn);
438
+ await conn.commit();
439
+ }
440
+ catch (err) {
441
+ try {
442
+ await conn.rollback();
443
+ }
444
+ catch {
445
+ }
446
+ throw err;
447
+ }
448
+ finally {
449
+ conn.release();
450
+ }
451
+ }
452
+ }
453
+ export class TiDBImageBake extends SqlImageBake {
454
+ constructor(pool, poolName = "standard") {
455
+ super(mysqlDriver(pool), poolName);
456
+ }
457
+ }
458
+ export class PgImageBake extends SqlImageBake {
459
+ constructor(pool, poolName = "standard") {
460
+ super(pgDriver(pool), poolName);
461
+ }
462
+ }
463
+ //# sourceMappingURL=image-bake-store-sql.js.map
@@ -0,0 +1,19 @@
1
+ export declare function buildBgRunnerScript(dir: string): string;
2
+ export declare function buildBgLauncherScript(dir: string, cmdShB64: string, runnerShB64: string): string;
3
+ export declare function buildBgPollScript(dir: string, stdoutCursor: number, stderrCursor: number, readCap: number): string;
4
+ export declare function buildBgKillScript(dir: string): string;
5
+ export declare function buildBgDisposeScript(dir: string): string;
6
+ export declare function buildDetachCapableExec(dir: string, command: string): string;
7
+ export interface ParsedBgPoll {
8
+ gone: boolean;
9
+ outTotal: number;
10
+ errTotal: number;
11
+ exitCode?: number;
12
+ alive: boolean;
13
+ stdout: string;
14
+ stdoutBytes: number;
15
+ stderr: string;
16
+ stderrBytes: number;
17
+ }
18
+ export declare function parseBgPollOutput(raw: string): ParsedBgPoll | undefined;
19
+ //# sourceMappingURL=k8s-bg-scripts.d.ts.map
@@ -0,0 +1,129 @@
1
+ import { shellQuote } from "./remote-shell.js";
2
+ export function buildBgRunnerScript(dir) {
3
+ const q = (name) => shellQuote(`${dir}/${name}`);
4
+ return [
5
+ `echo $$ > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`,
6
+ `sh ${q("cmd.sh")} > ${q("out")} 2> ${q("err")} < /dev/null`,
7
+ `__RC=$?`,
8
+ `echo $__RC > ${q("exit.tmp")} 2>/dev/null && mv ${q("exit.tmp")} ${q("exit")} 2>/dev/null`,
9
+ ].join("\n");
10
+ }
11
+ export function buildBgLauncherScript(dir, cmdShB64, runnerShB64) {
12
+ const d = shellQuote(dir);
13
+ const q = (name) => shellQuote(`${dir}/${name}`);
14
+ return [
15
+ `mkdir -p ${d} && chmod 700 ${d}`,
16
+ `printf %s '${cmdShB64}' | base64 -d > ${q("cmd.sh")}`,
17
+ `printf %s '${runnerShB64}' | base64 -d > ${q("runner.sh")}`,
18
+ `setsid sh ${q("runner.sh")} >/dev/null 2>&1 &`,
19
+ `__i=0; while [ ! -s ${q("pgid")} ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
20
+ `if [ -s ${q("pgid")} ]; then echo SEMABG_LAUNCHED; else echo SEMABG_NOPGID; fi`,
21
+ ].join("\n");
22
+ }
23
+ export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
24
+ const d = shellQuote(dir);
25
+ const slice = (file, cursor) => [
26
+ `__N=$((__T - ${cursor}))`,
27
+ `if [ "$__N" -gt 0 ]; then`,
28
+ ` if [ "$__N" -le ${readCap} ]; then tail -c +$(( ${cursor} + 1 )) ${d}/${file} 2>/dev/null | head -c "$__N" | base64;`,
29
+ ` else tail -c +$(( __T - ${readCap} + 1 )) ${d}/${file} 2>/dev/null | head -c ${readCap} | base64; fi`,
30
+ `fi`,
31
+ ].join("\n");
32
+ return [
33
+ `if [ ! -d ${d} ]; then echo SEMABG_GONE; exit 0; fi`,
34
+ `__OT=$(wc -c < ${d}/out 2>/dev/null); __OT=$(( \${__OT:-0} + 0 ))`,
35
+ `__ET=$(wc -c < ${d}/err 2>/dev/null); __ET=$(( \${__ET:-0} + 0 ))`,
36
+ `__EX=$( [ -f ${d}/exit ] && cat ${d}/exit 2>/dev/null || echo - )`,
37
+ `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
38
+ `if [ "$__PG" = "-" ]; then __AL=1; elif kill -0 -"$__PG" 2>/dev/null; then __AL=1; else __AL=0; fi`,
39
+ `echo "SEMABG_META o=$__OT e=$__ET x=$__EX a=$__AL"`,
40
+ `echo SEMABG_O`,
41
+ `__T=$__OT`,
42
+ slice("out", stdoutCursor),
43
+ `echo SEMABG_E`,
44
+ `__T=$__ET`,
45
+ slice("err", stderrCursor),
46
+ `echo SEMABG_X`,
47
+ ].join("\n");
48
+ }
49
+ export function buildBgKillScript(dir) {
50
+ const d = shellQuote(dir);
51
+ return [
52
+ `__i=0; while [ ! -s ${d}/pgid ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
53
+ `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
54
+ `if [ "$__PG" != "-" ]; then kill -TERM -"$__PG" 2>/dev/null || kill -TERM "$__PG" 2>/dev/null; sleep 1; kill -KILL -"$__PG" 2>/dev/null || kill -KILL "$__PG" 2>/dev/null; fi`,
55
+ `echo SEMABG_KILLED`,
56
+ ].join("\n");
57
+ }
58
+ export function buildBgDisposeScript(dir) {
59
+ const d = shellQuote(dir);
60
+ return [
61
+ `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
62
+ `if [ "$__PG" != "-" ]; then kill -KILL -"$__PG" 2>/dev/null || kill -KILL "$__PG" 2>/dev/null; fi`,
63
+ `rm -rf ${d} 2>/dev/null`,
64
+ `echo SEMABG_DISPOSED`,
65
+ ].join("\n");
66
+ }
67
+ export function buildDetachCapableExec(dir, command) {
68
+ const d = shellQuote(dir);
69
+ const q = (name) => shellQuote(`${dir}/${name}`);
70
+ const cmdB64 = Buffer.from(command, "utf8").toString("base64");
71
+ const writePgid = (val) => `printf %s ${val} > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`;
72
+ return [
73
+ `__cmd=$(printf %s '${cmdB64}' | base64 -d)`,
74
+ `mkdir -p ${d} 2>/dev/null`,
75
+ `trap 'rm -rf ${d} 2>/dev/null' EXIT`,
76
+ `if command -v setsid >/dev/null 2>&1; then`,
77
+ ` setsid sh -c "$__cmd" &`,
78
+ ` __pg=$!`,
79
+ ` ${writePgid('"$__pg"')}`,
80
+ ` wait "$__pg"; __rc=$?`,
81
+ `else`,
82
+ ` ${writePgid('"$$"')}`,
83
+ ` sh -c "$__cmd"; __rc=$?`,
84
+ `fi`,
85
+ `exit $__rc`,
86
+ ].join("\n");
87
+ }
88
+ export function parseBgPollOutput(raw) {
89
+ if (raw.includes("SEMABG_GONE")) {
90
+ return { gone: true, outTotal: 0, errTotal: 0, alive: false, stdout: "", stdoutBytes: 0, stderr: "", stderrBytes: 0 };
91
+ }
92
+ const lines = raw.split("\n");
93
+ const metaIdx = lines.findIndex((l) => l.startsWith("SEMABG_META "));
94
+ if (metaIdx < 0)
95
+ return undefined;
96
+ const oIdx = lines.indexOf("SEMABG_O", metaIdx + 1);
97
+ const eIdx = oIdx >= 0 ? lines.indexOf("SEMABG_E", oIdx + 1) : -1;
98
+ const xIdx = eIdx >= 0 ? lines.indexOf("SEMABG_X", eIdx + 1) : -1;
99
+ if (oIdx < 0 || eIdx < 0 || xIdx < 0)
100
+ return undefined;
101
+ const m = /o=(\d+) e=(\d+) x=(\S+) a=([01])/.exec(lines[metaIdx].slice("SEMABG_META ".length));
102
+ if (!m)
103
+ return undefined;
104
+ const outTotal = parseInt(m[1], 10);
105
+ const errTotal = parseInt(m[2], 10);
106
+ const exTok = m[3];
107
+ const alive = m[4] === "1";
108
+ const exitParsed = exTok === "-" ? NaN : parseInt(exTok, 10);
109
+ const exitCode = Number.isFinite(exitParsed) ? exitParsed : undefined;
110
+ const decode = (from, to) => {
111
+ const b64 = lines.slice(from, to).join("").replace(/\s+/g, "");
112
+ const buf = Buffer.from(b64, "base64");
113
+ return { text: buf.toString("utf8"), bytes: buf.length };
114
+ };
115
+ const o = decode(oIdx + 1, eIdx);
116
+ const e = decode(eIdx + 1, xIdx);
117
+ return {
118
+ gone: false,
119
+ outTotal,
120
+ errTotal,
121
+ ...(exitCode !== undefined ? { exitCode } : {}),
122
+ alive,
123
+ stdout: o.text,
124
+ stdoutBytes: o.bytes,
125
+ stderr: e.text,
126
+ stderrBytes: e.bytes,
127
+ };
128
+ }
129
+ //# sourceMappingURL=k8s-bg-scripts.js.map
@@ -0,0 +1,20 @@
1
+ export interface PodSpecPatch {
2
+ volumes?: unknown[];
3
+ volumeMounts?: unknown[];
4
+ podSecurityContext?: Record<string, unknown>;
5
+ containerSecurityContext?: Record<string, unknown>;
6
+ labels?: Record<string, string>;
7
+ }
8
+ export declare function applyPodSpecPatch(pod: {
9
+ metadata: {
10
+ labels?: Record<string, string>;
11
+ };
12
+ spec: Record<string, unknown>;
13
+ }, patch: PodSpecPatch): void;
14
+ export declare function execSocketTlsOptions(ca: string | undefined, insecureTls: boolean): Record<string, unknown>;
15
+ export declare function exitCodeFromStatus(statusJson: string): {
16
+ exitCode: number;
17
+ } | {
18
+ error: string;
19
+ };
20
+ //# sourceMappingURL=k8s-exec-protocol.d.ts.map