@sema-agent/server 1.314.0 → 1.316.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 (146) hide show
  1. package/dist/approval-hmac.d.ts +17 -0
  2. package/dist/approval-hmac.js +27 -0
  3. package/dist/config-center/apply-effective.d.ts +22 -0
  4. package/dist/config-center/apply-effective.js +283 -0
  5. package/dist/config-center/http-client.d.ts +23 -0
  6. package/dist/config-center/http-client.js +109 -0
  7. package/dist/config-center/restart-signal.d.ts +12 -0
  8. package/dist/config-center/restart-signal.js +70 -0
  9. package/dist/config-center/skills-mcp.d.ts +13 -0
  10. package/dist/config-center/skills-mcp.js +113 -0
  11. package/dist/config-center/types.d.ts +143 -0
  12. package/dist/config-center/types.js +2 -0
  13. package/dist/fleet/fleet-bus.js +92 -83
  14. package/dist/hooks/hook-runner.js +18 -9
  15. package/dist/http/idempotency.d.ts +1 -0
  16. package/dist/http/idempotency.js +3 -0
  17. package/dist/http/principal-gate.d.ts +12 -0
  18. package/dist/http/principal-gate.js +52 -0
  19. package/dist/http/route-ctx.d.ts +67 -0
  20. package/dist/http/route-ctx.js +2 -0
  21. package/dist/http/routes/attachments.d.ts +4 -0
  22. package/dist/http/routes/attachments.js +106 -0
  23. package/dist/http/routes/capabilities.d.ts +4 -0
  24. package/dist/http/routes/capabilities.js +111 -0
  25. package/dist/http/routes/images.d.ts +20 -0
  26. package/dist/http/routes/images.js +572 -0
  27. package/dist/http/routes/memory-policy.d.ts +4 -0
  28. package/dist/http/routes/memory-policy.js +105 -0
  29. package/dist/http/routes/observability.d.ts +4 -0
  30. package/dist/http/routes/observability.js +63 -0
  31. package/dist/http/routes/session-sync.d.ts +36 -0
  32. package/dist/http/routes/session-sync.js +513 -0
  33. package/dist/http/routes/sessions-list.d.ts +4 -0
  34. package/dist/http/routes/sessions-list.js +117 -0
  35. package/dist/http/routes/sessions.d.ts +17 -0
  36. package/dist/http/routes/sessions.js +746 -0
  37. package/dist/http/send.d.ts +6 -0
  38. package/dist/http/send.js +19 -0
  39. package/dist/http/server.d.ts +101 -76
  40. package/dist/http/server.js +129 -2410
  41. package/dist/http/sse-log.d.ts +25 -0
  42. package/dist/http/sse-log.js +59 -0
  43. package/dist/http/workspace-content.d.ts +3 -0
  44. package/dist/http/workspace-content.js +20 -0
  45. package/dist/index.d.ts +1 -1
  46. package/dist/leader/leader.js +5 -2
  47. package/dist/leader/wire.js +169 -165
  48. package/dist/main.js +462 -451
  49. package/dist/plugins/approval-store-sql.d.ts +52 -0
  50. package/dist/plugins/approval-store-sql.js +119 -0
  51. package/dist/plugins/breaker-state-sql.d.ts +29 -0
  52. package/dist/plugins/breaker-state-sql.js +104 -0
  53. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  54. package/dist/plugins/checkpoint-store-sql.js +224 -0
  55. package/dist/plugins/file-snapshot-store-sql.d.ts +38 -0
  56. package/dist/plugins/file-snapshot-store-sql.js +210 -0
  57. package/dist/plugins/image-bake-store-sql.d.ts +53 -0
  58. package/dist/plugins/image-bake-store-sql.js +463 -0
  59. package/dist/plugins/image-index-sql.d.ts +33 -0
  60. package/dist/plugins/image-index-sql.js +328 -0
  61. package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
  62. package/dist/plugins/k8s-bg-scripts.js +129 -0
  63. package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
  64. package/dist/plugins/k8s-exec-protocol.js +87 -0
  65. package/dist/plugins/outcome-ledger-sql.d.ts +70 -0
  66. package/dist/plugins/outcome-ledger-sql.js +158 -0
  67. package/dist/plugins/pg-approval-store.d.ts +1 -27
  68. package/dist/plugins/pg-approval-store.js +1 -83
  69. package/dist/plugins/pg-breaker-state.d.ts +1 -19
  70. package/dist/plugins/pg-breaker-state.js +1 -88
  71. package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
  72. package/dist/plugins/pg-checkpoint-store.js +1 -189
  73. package/dist/plugins/pg-cost-quota.js +3 -143
  74. package/dist/plugins/pg-file-snapshot-store.d.ts +1 -27
  75. package/dist/plugins/pg-file-snapshot-store.js +1 -190
  76. package/dist/plugins/pg-image-bake.d.ts +1 -40
  77. package/dist/plugins/pg-image-bake.js +1 -420
  78. package/dist/plugins/pg-image-index.d.ts +1 -21
  79. package/dist/plugins/pg-image-index.js +1 -217
  80. package/dist/plugins/pg-outcome-ledger.d.ts +2 -22
  81. package/dist/plugins/pg-outcome-ledger.js +1 -104
  82. package/dist/plugins/pg-rate-limiter.d.ts +2 -32
  83. package/dist/plugins/pg-rate-limiter.js +4 -147
  84. package/dist/plugins/pg-resume-anchor-store.d.ts +1 -13
  85. package/dist/plugins/pg-resume-anchor-store.js +1 -22
  86. package/dist/plugins/pg-run-store.d.ts +1 -75
  87. package/dist/plugins/pg-run-store.js +1 -287
  88. package/dist/plugins/pg-session-policy-store.d.ts +1 -11
  89. package/dist/plugins/pg-session-policy-store.js +1 -90
  90. package/dist/plugins/pg-tool-result-store.d.ts +1 -15
  91. package/dist/plugins/pg-tool-result-store.js +1 -52
  92. package/dist/plugins/pg-workflow-journal-store.d.ts +1 -20
  93. package/dist/plugins/pg-workflow-journal-store.js +1 -34
  94. package/dist/plugins/pg-workflow-run-store.d.ts +1 -55
  95. package/dist/plugins/pg-workflow-run-store.js +1 -220
  96. package/dist/plugins/remote-env-k8s.d.ts +5 -38
  97. package/dist/plugins/remote-env-k8s.js +7 -213
  98. package/dist/plugins/resume-anchor-store-sql.d.ts +23 -0
  99. package/dist/plugins/resume-anchor-store-sql.js +38 -0
  100. package/dist/plugins/run-store-sql.d.ts +89 -0
  101. package/dist/plugins/run-store-sql.js +397 -0
  102. package/dist/plugins/session-policy-store-sql.d.ts +22 -0
  103. package/dist/plugins/session-policy-store-sql.js +109 -0
  104. package/dist/plugins/sql-driver.d.ts +25 -0
  105. package/dist/plugins/sql-driver.js +59 -0
  106. package/dist/plugins/tidb-approval-store.d.ts +1 -39
  107. package/dist/plugins/tidb-approval-store.js +1 -71
  108. package/dist/plugins/tidb-breaker-state.d.ts +1 -17
  109. package/dist/plugins/tidb-breaker-state.js +1 -74
  110. package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
  111. package/dist/plugins/tidb-checkpoint-store.js +1 -191
  112. package/dist/plugins/tidb-file-snapshot-store.d.ts +1 -27
  113. package/dist/plugins/tidb-file-snapshot-store.js +1 -192
  114. package/dist/plugins/tidb-image-bake.d.ts +1 -38
  115. package/dist/plugins/tidb-image-bake.js +1 -348
  116. package/dist/plugins/tidb-image-index.d.ts +1 -19
  117. package/dist/plugins/tidb-image-index.js +1 -149
  118. package/dist/plugins/tidb-outcome-ledger.d.ts +1 -57
  119. package/dist/plugins/tidb-outcome-ledger.js +1 -97
  120. package/dist/plugins/tidb-resume-anchor-store.d.ts +1 -13
  121. package/dist/plugins/tidb-resume-anchor-store.js +1 -22
  122. package/dist/plugins/tidb-run-store.d.ts +1 -75
  123. package/dist/plugins/tidb-run-store.js +1 -306
  124. package/dist/plugins/tidb-session-policy-store.d.ts +1 -11
  125. package/dist/plugins/tidb-session-policy-store.js +1 -86
  126. package/dist/plugins/tidb-tool-result-store.d.ts +1 -13
  127. package/dist/plugins/tidb-tool-result-store.js +1 -38
  128. package/dist/plugins/tidb-workflow-journal-store.d.ts +1 -20
  129. package/dist/plugins/tidb-workflow-journal-store.js +1 -34
  130. package/dist/plugins/tidb-workflow-run-store.d.ts +1 -61
  131. package/dist/plugins/tidb-workflow-run-store.js +1 -231
  132. package/dist/plugins/tool-result-store-sql.d.ts +25 -0
  133. package/dist/plugins/tool-result-store-sql.js +69 -0
  134. package/dist/plugins/workflow-journal-store-sql.d.ts +30 -0
  135. package/dist/plugins/workflow-journal-store-sql.js +53 -0
  136. package/dist/plugins/workflow-run-store-sql.d.ts +85 -0
  137. package/dist/plugins/workflow-run-store-sql.js +299 -0
  138. package/dist/plugins/write-behind-counter.d.ts +14 -3
  139. package/dist/plugins/write-behind-counter.js +39 -12
  140. package/dist/principal-jwt.d.ts +44 -0
  141. package/dist/principal-jwt.js +95 -0
  142. package/dist/security.d.ts +2 -58
  143. package/dist/security.js +3 -118
  144. package/dist/sema-registry.d.ts +5 -200
  145. package/dist/sema-registry.js +4 -567
  146. package/package.json +1 -1
@@ -1,421 +1,2 @@
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 { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS } from "./store-contracts.js";
6
- export const PG_IMAGE_BAKE_SCHEMA = [
7
- `CREATE TABLE IF NOT EXISTS image_bake (
8
- bake_id VARCHAR(64) NOT NULL,
9
- profile VARCHAR(128) NOT NULL,
10
- bands JSONB NULL,
11
- base_ref VARCHAR(255) NULL,
12
- push SMALLINT NOT NULL DEFAULT 0,
13
- dry_run SMALLINT NOT NULL DEFAULT 0,
14
- logs SMALLINT NOT NULL DEFAULT 0,
15
- argv JSONB NOT NULL,
16
- status VARCHAR(16) NOT NULL,
17
- state VARCHAR(16) NULL,
18
- digest VARCHAR(255) NULL,
19
- repo VARCHAR(255) NULL,
20
- ref VARCHAR(320) NULL,
21
- index_id VARCHAR(64) NULL,
22
- exit_code INT NULL,
23
- error TEXT NULL,
24
- error_code VARCHAR(32) NULL,
25
- manifest_sha VARCHAR(64) NULL,
26
- tag VARCHAR(255) NULL,
27
- idem_key VARCHAR(255) NULL,
28
- ingest_secret VARCHAR(64) NULL,
29
- runner_id VARCHAR(64) NULL,
30
- lease_until TIMESTAMPTZ(3) NULL,
31
- cancel_requested SMALLINT NOT NULL DEFAULT 0,
32
- requested_by VARCHAR(128) NULL,
33
- created_at TIMESTAMPTZ(3) NOT NULL,
34
- updated_at TIMESTAMPTZ(3) NOT NULL,
35
- PRIMARY KEY (bake_id),
36
- CONSTRAINT uq_image_bake_idem UNIQUE (idem_key)
37
- )`,
38
- `CREATE INDEX IF NOT EXISTS idx_image_bake_status ON image_bake (status)`,
39
- `CREATE INDEX IF NOT EXISTS idx_image_bake_created ON image_bake (created_at)`,
40
- `CREATE TABLE IF NOT EXISTS image_bake_event (
41
- bake_id VARCHAR(64) NOT NULL,
42
- seq INT NOT NULL,
43
- line_ord INT NULL,
44
- kind VARCHAR(24) NOT NULL,
45
- level VARCHAR(8) NULL,
46
- data JSONB NULL,
47
- ts TIMESTAMPTZ(3) NOT NULL,
48
- PRIMARY KEY (bake_id, seq),
49
- CONSTRAINT uq_image_bake_event_line UNIQUE (bake_id, line_ord)
50
- )`,
51
- `CREATE TABLE IF NOT EXISTS image_bake_lease (
52
- pool VARCHAR(32) NOT NULL DEFAULT 'standard',
53
- bake_id VARCHAR(64) NOT NULL,
54
- acquired_at TIMESTAMPTZ(3) NOT NULL,
55
- expires_at TIMESTAMPTZ(3) NOT NULL,
56
- PRIMARY KEY (pool)
57
- )`,
58
- `CREATE TABLE IF NOT EXISTS image_bake_admit (
59
- pool VARCHAR(32) NOT NULL,
60
- PRIMARY KEY (pool)
61
- )`,
62
- ];
63
- export async function ensureSchema(pool, poolName = "standard") {
64
- for (const stmt of PG_IMAGE_BAKE_SCHEMA)
65
- await pool.query(stmt);
66
- await pool.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [poolName]);
67
- }
68
- function isDupKey(err) {
69
- return Boolean(err) && err.code === "23505";
70
- }
71
- function dupKeyName(err) {
72
- const c = String(err?.constraint ?? "");
73
- if (c === "uq_image_bake_event_line")
74
- return "uq_line";
75
- if (/pkey$/i.test(c))
76
- return "primary";
77
- return "other";
78
- }
79
- export class PgImageBake {
80
- pool;
81
- poolName;
82
- constructor(pool, poolName = "standard") {
83
- this.pool = pool;
84
- this.poolName = poolName;
85
- }
86
- async createBake(input) {
87
- const bakeId = uuidv7();
88
- const now = new Date();
89
- try {
90
- await this.pool.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
91
- "idem_key, cancel_requested, requested_by, created_at, updated_at) " +
92
- "VALUES ($1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12)", [
93
- bakeId,
94
- input.profile,
95
- input.bands == null ? null : JSON.stringify(input.bands),
96
- input.baseRef,
97
- input.push ? 1 : 0,
98
- input.dryRun ? 1 : 0,
99
- input.logs ? 1 : 0,
100
- JSON.stringify(input.argv),
101
- input.idemKey,
102
- input.requestedBy,
103
- now,
104
- now,
105
- ]);
106
- }
107
- catch (err) {
108
- if (isDupKey(err) && input.idemKey) {
109
- const existing = await this.getBakeByIdem(input.idemKey);
110
- if (existing)
111
- return { created: false, bake: existing };
112
- }
113
- throw err;
114
- }
115
- const bake = await this.getBake(bakeId);
116
- if (!bake)
117
- throw new Error("createBake: row vanished after insert");
118
- return { created: true, bake };
119
- }
120
- async createBakeIfIdle(input) {
121
- const bakeId = uuidv7();
122
- const now = new Date();
123
- const conn = await this.pool.connect();
124
- try {
125
- await conn.query("BEGIN");
126
- await conn.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [this.poolName]);
127
- await conn.query("SELECT pool FROM image_bake_admit WHERE pool = $1 FOR UPDATE", [this.poolName]);
128
- const res = await conn.query("INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
129
- "idem_key, cancel_requested, requested_by, created_at, updated_at) " +
130
- "SELECT $1,$2,$3::jsonb,$4,$5,$6,$7,$8::jsonb,'queued',$9,0,$10,$11,$12 WHERE NOT EXISTS " +
131
- "(SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0)", [
132
- bakeId,
133
- input.profile,
134
- input.bands == null ? null : JSON.stringify(input.bands),
135
- input.baseRef,
136
- input.push ? 1 : 0,
137
- input.dryRun ? 1 : 0,
138
- input.logs ? 1 : 0,
139
- JSON.stringify(input.argv),
140
- input.idemKey,
141
- input.requestedBy,
142
- now,
143
- now,
144
- ]);
145
- const inserted = (res.rowCount ?? 0) > 0;
146
- await conn.query("COMMIT");
147
- if (!inserted)
148
- return { created: false, bake: null };
149
- }
150
- catch (err) {
151
- try {
152
- await conn.query("ROLLBACK");
153
- }
154
- catch {
155
- }
156
- throw err;
157
- }
158
- finally {
159
- conn.release();
160
- }
161
- const bake = await this.getBake(bakeId);
162
- if (!bake)
163
- throw new Error("createBakeIfIdle: row vanished after insert");
164
- return { created: true, bake };
165
- }
166
- async getBake(bakeId) {
167
- const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE bake_id = $1 LIMIT 1`, [bakeId]);
168
- return res.rows[0] ? mapRow(res.rows[0]) : null;
169
- }
170
- async getBakeByIdem(idemKey) {
171
- const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = $1 LIMIT 1`, [idemKey]);
172
- return res.rows[0] ? mapRow(res.rows[0]) : null;
173
- }
174
- async findActiveByArgv(argv) {
175
- const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = $1::jsonb ` +
176
- "ORDER BY created_at DESC LIMIT 1", [JSON.stringify(argv)]);
177
- return res.rows[0] ? mapRow(res.rows[0]) : null;
178
- }
179
- async findActiveAny() {
180
- const res = await this.pool.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
181
- "ORDER BY created_at ASC LIMIT 1");
182
- return res.rows[0] ? mapRow(res.rows[0]) : null;
183
- }
184
- async findNextQueuedId() {
185
- const res = await this.pool.query("SELECT bake_id FROM image_bake WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1");
186
- return res.rows[0] ? String(res.rows[0].bake_id) : null;
187
- }
188
- async claimBake(bakeId, runnerId, leaseMs) {
189
- const conn = await this.pool.connect();
190
- try {
191
- await conn.query("BEGIN");
192
- const now = new Date();
193
- const leaseUntil = new Date(now.getTime() + leaseMs);
194
- const ingestSecret = randomBytes(24).toString("hex");
195
- const bakeRes = await conn.query("UPDATE image_bake SET status='running', state='PENDING', runner_id=$1, ingest_secret=$2, " +
196
- "lease_until=$3, updated_at=$4 WHERE bake_id=$5 AND status='queued'", [runnerId, ingestSecret, leaseUntil, now, bakeId]);
197
- if ((bakeRes.rowCount ?? 0) !== 1) {
198
- await conn.query("ROLLBACK");
199
- return null;
200
- }
201
- const dryRes = await conn.query("SELECT dry_run FROM image_bake WHERE bake_id=$1", [bakeId]);
202
- if (Boolean(dryRes.rows[0]?.dry_run)) {
203
- await conn.query("COMMIT");
204
- return (await this.getBake(bakeId)) ?? null;
205
- }
206
- const leaseSel = await conn.query("SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=$1", [this.poolName]);
207
- const cur = leaseSel.rows[0];
208
- if (!cur) {
209
- try {
210
- await conn.query("INSERT INTO image_bake_lease (pool, bake_id, acquired_at, expires_at) VALUES ($1,$2,$3,$4)", [this.poolName, bakeId, now, leaseUntil]);
211
- }
212
- catch (err) {
213
- if (isDupKey(err)) {
214
- await conn.query("ROLLBACK");
215
- return null;
216
- }
217
- throw err;
218
- }
219
- }
220
- else {
221
- if (new Date(cur.expires_at).getTime() >= now.getTime()) {
222
- await conn.query("ROLLBACK");
223
- return null;
224
- }
225
- const holderId = String(cur.bake_id ?? "");
226
- const holderRes = await conn.query("SELECT status FROM image_bake WHERE bake_id=$1", [holderId]);
227
- const holderStatus = String(holderRes.rows[0]?.status ?? "");
228
- if (holderStatus === "queued" || holderStatus === "running") {
229
- await conn.query("ROLLBACK");
230
- return null;
231
- }
232
- const stealRes = await conn.query("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]);
233
- if ((stealRes.rowCount ?? 0) === 0) {
234
- await conn.query("ROLLBACK");
235
- return null;
236
- }
237
- }
238
- const check = await conn.query("SELECT bake_id FROM image_bake_lease WHERE pool=$1", [this.poolName]);
239
- if (String(check.rows[0]?.bake_id ?? "") !== bakeId) {
240
- await conn.query("ROLLBACK");
241
- return null;
242
- }
243
- await conn.query("COMMIT");
244
- return (await this.getBake(bakeId)) ?? null;
245
- }
246
- catch (err) {
247
- try {
248
- await conn.query("ROLLBACK");
249
- }
250
- catch {
251
- }
252
- throw err;
253
- }
254
- finally {
255
- conn.release();
256
- }
257
- }
258
- async appendEvent(bakeId, kind, data, opts = {}) {
259
- const lineOrd = opts.lineOrd ?? null;
260
- if (lineOrd !== null && (await this.lineOrdExists(bakeId, lineOrd)))
261
- return null;
262
- const payload = data == null ? null : pgSafeJsonStringify(data);
263
- for (let attempt = 0; attempt < 50; attempt += 1) {
264
- const seq = (await this.maxSeq(bakeId)) + 1;
265
- try {
266
- await this.pool.query("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()]);
267
- return seq;
268
- }
269
- catch (err) {
270
- if (!isDupKey(err))
271
- throw err;
272
- const which = dupKeyName(err);
273
- if (which === "uq_line" && lineOrd !== null)
274
- return null;
275
- await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5)));
276
- }
277
- }
278
- throw new Error(`appendEvent: seq allocation lost the (bake_id,seq) race after retries (bakeId=${bakeId})`);
279
- }
280
- async lineOrdExists(bakeId, lineOrd) {
281
- const res = await this.pool.query("SELECT 1 FROM image_bake_event WHERE bake_id = $1 AND line_ord = $2 LIMIT 1", [bakeId, lineOrd]);
282
- return (res.rowCount ?? 0) > 0;
283
- }
284
- async maxSeq(bakeId) {
285
- const res = await this.pool.query("SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = $1", [bakeId]);
286
- const m = res.rows[0]?.m;
287
- return m == null ? 0 : Number(m);
288
- }
289
- async retainedFrom(bakeId) {
290
- const res = await this.pool.query("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = $1", [bakeId]);
291
- const m = res.rows[0]?.m;
292
- return m == null ? 0 : Number(m);
293
- }
294
- async getEvents(bakeId, afterSeq) {
295
- const res = await this.pool.query("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]);
296
- return res.rows.map((r) => ({
297
- seq: Number(r.seq),
298
- lineOrd: r.line_ord === null ? null : Number(r.line_ord),
299
- kind: String(r.kind),
300
- level: r.level ?? null,
301
- data: parseJson(r.data, null),
302
- ts: iso(r.ts),
303
- }));
304
- }
305
- async setStatus(bakeId, status) {
306
- await this.pool.query("UPDATE image_bake SET status = $1, updated_at = $2 WHERE bake_id = $3", [
307
- status,
308
- new Date(),
309
- bakeId,
310
- ]);
311
- }
312
- async setState(bakeId, state) {
313
- await this.pool.query("UPDATE image_bake SET state = $1, updated_at = $2 WHERE bake_id = $3", [
314
- state,
315
- new Date(),
316
- bakeId,
317
- ]);
318
- }
319
- async setTerminal(bakeId, t) {
320
- let affectedRows = 0;
321
- await this.tx(async (conn) => {
322
- const invalidIdentity = (t.digest != null && pgHasUnstorable(t.digest)) ||
323
- (t.manifestSha != null && pgHasUnstorable(t.manifestSha)) ||
324
- (t.repo != null && pgHasUnstorable(t.repo)) ||
325
- (t.ref != null && pgHasUnstorable(t.ref));
326
- const res = await conn.query("UPDATE image_bake SET status=$1, state=$2, digest=$3, repo=$4, ref=$5, index_id=$6, exit_code=$7, error=$8, " +
327
- "error_code=$9, manifest_sha=$10, tag=$11, ingest_secret=NULL, updated_at=$12 WHERE bake_id=$13 AND status IN ('queued','running')", [
328
- invalidIdentity ? "failed" : t.status,
329
- t.state,
330
- invalidIdentity ? null : t.digest ?? null,
331
- invalidIdentity ? null : t.repo ?? null,
332
- invalidIdentity ? null : t.ref ?? null,
333
- t.indexId ?? null,
334
- t.exitCode ?? null,
335
- t.error == null ? null : pgSanitizeText(t.error),
336
- t.errorCode == null ? null : pgSanitizeText(t.errorCode),
337
- invalidIdentity ? null : t.manifestSha ?? null,
338
- t.tag == null ? null : pgSanitizeText(t.tag),
339
- new Date(),
340
- bakeId,
341
- ]);
342
- affectedRows = res.rowCount ?? 0;
343
- await conn.query("DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2", [this.poolName, bakeId]);
344
- });
345
- return affectedRows;
346
- }
347
- async setIndexId(bakeId, indexId) {
348
- const res = await this.pool.query("UPDATE image_bake SET index_id = $1, updated_at = $2 WHERE bake_id = $3 AND status IN ('queued','running')", [indexId, new Date(), bakeId]);
349
- return (res.rowCount ?? 0) >= 1;
350
- }
351
- async heartbeatLease(bakeId, runnerId, leaseMs) {
352
- const now = new Date();
353
- const leaseUntil = new Date(now.getTime() + leaseMs);
354
- const res = await this.pool.query("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]);
355
- if ((res.rowCount ?? 0) < 1)
356
- return false;
357
- const dryRes = await this.pool.query("SELECT dry_run FROM image_bake WHERE bake_id = $1", [bakeId]);
358
- if (Boolean(dryRes.rows[0]?.dry_run))
359
- return true;
360
- const leaseRes = await this.pool.query("UPDATE image_bake_lease SET expires_at = $1 WHERE pool = $2 AND bake_id = $3", [leaseUntil, this.poolName, bakeId]);
361
- return (leaseRes.rowCount ?? 0) >= 1;
362
- }
363
- async requestCancel(bakeId) {
364
- const res = await this.pool.query("UPDATE image_bake SET cancel_requested = 1, updated_at = $1 WHERE bake_id = $2 AND status IN ('queued','running')", [new Date(), bakeId]);
365
- return (res.rowCount ?? 0) > 0;
366
- }
367
- async isCancelRequested(bakeId) {
368
- const res = await this.pool.query("SELECT cancel_requested FROM image_bake WHERE bake_id = $1", [bakeId]);
369
- return Boolean(res.rows[0]?.cancel_requested);
370
- }
371
- async reapStaleBakes(olderThanMs) {
372
- const cutoff = new Date(Date.now() - olderThanMs);
373
- const victimsRes = await this.pool.query("SELECT bake_id FROM image_bake WHERE status = 'running' AND " +
374
- "(lease_until IS NULL OR lease_until < $1) AND updated_at < $2", [cutoff, cutoff]);
375
- let reaped = 0;
376
- for (const v of victimsRes.rows) {
377
- const bakeId = String(v.bake_id);
378
- const res = await this.pool.query("UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
379
- "ingest_secret=NULL, index_id=NULL, updated_at=$1 WHERE bake_id=$2 AND status='running'", [new Date(), bakeId]);
380
- if ((res.rowCount ?? 0) === 1) {
381
- reaped += 1;
382
- try {
383
- await this.appendEvent(bakeId, "done", {
384
- status: "FAILED",
385
- digest: null,
386
- repo: null,
387
- ref: null,
388
- indexId: null,
389
- exitCode: null,
390
- error: "bake-runner lost (lease expired)",
391
- errorCode: null,
392
- });
393
- }
394
- catch {
395
- }
396
- await this.pool.query("DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2", [this.poolName, bakeId]);
397
- }
398
- }
399
- return reaped;
400
- }
401
- async tx(fn) {
402
- const conn = await this.pool.connect();
403
- try {
404
- await conn.query("BEGIN");
405
- await fn(conn);
406
- await conn.query("COMMIT");
407
- }
408
- catch (err) {
409
- try {
410
- await conn.query("ROLLBACK");
411
- }
412
- catch {
413
- }
414
- throw err;
415
- }
416
- finally {
417
- conn.release();
418
- }
419
- }
420
- }
1
+ export { PgImageBake, PG_IMAGE_BAKE_SCHEMA, ensurePgImageBakeSchema as ensureSchema } from "./image-bake-store-sql.js";
421
2
  //# sourceMappingURL=pg-image-bake.js.map
@@ -1,23 +1,3 @@
1
- import type { Pool, PoolClient } from "pg";
2
- import { type ImageStatus, type ImageIndexEntry, type ImageIndexUpsert, type ImageViewer, type ImageListFilter } from "./store-contracts.js";
1
+ export { PgImageIndex, PG_IMAGE_INDEX_SCHEMA, ensureSchema } from "./image-index-sql.js";
3
2
  export type { ImageNestedBuildMode, ImageCapabilities, ImagePodContract, ImageStatus, ImageVisibility, ImageIndexEntry, ImageIndexUpsert, ImageViewer, ImageListFilter, } from "./store-contracts.js";
4
- export declare const PG_IMAGE_INDEX_SCHEMA: string[];
5
- export declare function ensureSchema(pool: Pool | PoolClient): Promise<void>;
6
- export declare class PgImageIndex {
7
- private readonly pool;
8
- constructor(pool: Pool);
9
- static idFor(repo: string, digest: string): string;
10
- upsert(e: ImageIndexUpsert): Promise<string>;
11
- registerBuilding(e: ImageIndexUpsert): Promise<string>;
12
- getById(id: string): Promise<ImageIndexEntry | null>;
13
- getByDigest(digest: string): Promise<ImageIndexEntry | null>;
14
- latestPublished(profile: string, viewer: ImageViewer): Promise<ImageIndexEntry | null>;
15
- list(filter: ImageListFilter): Promise<{
16
- entries: ImageIndexEntry[];
17
- nextCursor: string | null;
18
- }>;
19
- setStatus(id: string, status: ImageStatus, opts?: {
20
- supersedes?: string | null;
21
- }): Promise<void>;
22
- }
23
3
  //# sourceMappingURL=pg-image-index.d.ts.map
@@ -1,218 +1,2 @@
1
- import { createHash } from "node:crypto";
2
- import { pgSafeJsonStringify, pgSanitizeText, pgHasUnstorable, PgUnstorableError } from "./pg-safe-json.js";
3
- import { mapImageRow as mapRow, } from "./store-contracts.js";
4
- export const PG_IMAGE_INDEX_SCHEMA = [
5
- `CREATE TABLE IF NOT EXISTS sandbox_image_index (
6
- id VARCHAR(64) NOT NULL,
7
- profile VARCHAR(64) NOT NULL,
8
- bands JSONB,
9
- repo VARCHAR(255) NOT NULL,
10
- tag VARCHAR(128) NOT NULL,
11
- digest VARCHAR(80) NOT NULL,
12
- toolchain_versions JSONB,
13
- capabilities JSONB,
14
- pod_contract JSONB,
15
- size_bytes BIGINT,
16
- status VARCHAR(16) NOT NULL DEFAULT 'building',
17
- visibility VARCHAR(16) NOT NULL DEFAULT 'public',
18
- tenant_id VARCHAR(64),
19
- manifest_sha VARCHAR(64),
20
- recipe_git_sha VARCHAR(64),
21
- generator_version VARCHAR(32),
22
- build_date TIMESTAMPTZ(3),
23
- supersedes VARCHAR(64),
24
- signed SMALLINT NOT NULL DEFAULT 0,
25
- created_at TIMESTAMPTZ(3) NOT NULL,
26
- updated_at TIMESTAMPTZ(3) NOT NULL,
27
- PRIMARY KEY (id)
28
- )`,
29
- `CREATE UNIQUE INDEX IF NOT EXISTS uq_repo_digest ON sandbox_image_index (repo, digest)`,
30
- `CREATE INDEX IF NOT EXISTS idx_profile_status ON sandbox_image_index (profile, status, visibility)`,
31
- `CREATE INDEX IF NOT EXISTS idx_digest ON sandbox_image_index (digest)`,
32
- `CREATE INDEX IF NOT EXISTS idx_tenant ON sandbox_image_index (tenant_id, status)`,
33
- ];
34
- export async function ensureSchema(pool) {
35
- for (const stmt of PG_IMAGE_INDEX_SCHEMA)
36
- await pool.query(stmt);
37
- }
38
- const MAX_LIMIT = 200;
39
- const DEFAULT_LIMIT = 50;
40
- function visibilityClause(viewer, startIdx) {
41
- if (viewer.operator)
42
- return { sql: "1=1", params: [], nextIdx: startIdx };
43
- if (viewer.tenantId)
44
- return {
45
- sql: `(visibility = 'public' OR tenant_id = $${startIdx})`,
46
- params: [viewer.tenantId],
47
- nextIdx: startIdx + 1,
48
- };
49
- return { sql: "visibility = 'public'", params: [], nextIdx: startIdx };
50
- }
51
- function encodeCursor(e) {
52
- return Buffer.from(`${e.createdAt}|${e.id}`, "utf8").toString("base64url");
53
- }
54
- function decodeCursor(c) {
55
- try {
56
- const [createdAt, id] = Buffer.from(c, "base64url").toString("utf8").split("|");
57
- if (!createdAt || !id)
58
- return null;
59
- return { createdAt, id };
60
- }
61
- catch {
62
- return null;
63
- }
64
- }
65
- const UPSERT_COLS = "(id, profile, bands, repo, tag, digest, toolchain_versions, capabilities, pod_contract, size_bytes, " +
66
- "status, visibility, tenant_id, manifest_sha, recipe_git_sha, generator_version, build_date, supersedes, " +
67
- "signed, created_at, updated_at)";
68
- const UPSERT_VALUES = "($1,$2,$3::jsonb,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)";
69
- function upsertParams(e, id, now) {
70
- return [
71
- id,
72
- pgSanitizeText(e.profile),
73
- pgSafeJsonStringify(e.bands ?? []),
74
- e.repo,
75
- pgSanitizeText(e.tag),
76
- e.digest,
77
- pgSafeJsonStringify(e.toolchainVersions ?? {}),
78
- pgSafeJsonStringify(e.capabilities ?? {}),
79
- pgSafeJsonStringify(e.podContract ?? {}),
80
- e.sizeBytes,
81
- e.status,
82
- e.visibility,
83
- e.tenantId ?? null,
84
- e.manifestSha ?? null,
85
- e.recipeGitSha == null ? null : pgSanitizeText(e.recipeGitSha),
86
- e.generatorVersion == null ? null : pgSanitizeText(e.generatorVersion),
87
- e.buildDate ? new Date(e.buildDate) : null,
88
- e.supersedes == null ? null : pgSanitizeText(e.supersedes),
89
- e.signed ? 1 : 0,
90
- now,
91
- now,
92
- ];
93
- }
94
- export class PgImageIndex {
95
- pool;
96
- constructor(pool) {
97
- this.pool = pool;
98
- }
99
- static idFor(repo, digest) {
100
- return createHash("sha256").update(`${repo}@${digest}`).digest("hex").slice(0, 64);
101
- }
102
- async upsert(e) {
103
- if (pgHasUnstorable(e.repo))
104
- throw new PgUnstorableError("image repo (identity)");
105
- if (e.tenantId != null && pgHasUnstorable(e.tenantId))
106
- throw new PgUnstorableError("image tenantId (identity)");
107
- const id = PgImageIndex.idFor(e.repo, e.digest);
108
- const now = new Date();
109
- await this.pool.query(`INSERT INTO sandbox_image_index ${UPSERT_COLS}
110
- VALUES ${UPSERT_VALUES}
111
- ON CONFLICT (id) DO UPDATE SET
112
- profile = EXCLUDED.profile, bands = EXCLUDED.bands, tag = EXCLUDED.tag,
113
- toolchain_versions = EXCLUDED.toolchain_versions, capabilities = EXCLUDED.capabilities,
114
- pod_contract = EXCLUDED.pod_contract, size_bytes = EXCLUDED.size_bytes,
115
- status = EXCLUDED.status, visibility = EXCLUDED.visibility, tenant_id = EXCLUDED.tenant_id,
116
- manifest_sha = EXCLUDED.manifest_sha, recipe_git_sha = EXCLUDED.recipe_git_sha,
117
- generator_version = EXCLUDED.generator_version, build_date = EXCLUDED.build_date,
118
- supersedes = EXCLUDED.supersedes, signed = EXCLUDED.signed, updated_at = EXCLUDED.updated_at`, upsertParams(e, id, now));
119
- return id;
120
- }
121
- async registerBuilding(e) {
122
- if (pgHasUnstorable(e.repo))
123
- throw new PgUnstorableError("image repo (identity)");
124
- if (e.tenantId != null && pgHasUnstorable(e.tenantId))
125
- throw new PgUnstorableError("image tenantId (identity)");
126
- const id = PgImageIndex.idFor(e.repo, e.digest);
127
- const now = new Date();
128
- await this.pool.query(`INSERT INTO sandbox_image_index ${UPSERT_COLS}
129
- VALUES ${UPSERT_VALUES}
130
- ON CONFLICT (id) DO UPDATE SET
131
- profile = EXCLUDED.profile, bands = EXCLUDED.bands, tag = EXCLUDED.tag,
132
- toolchain_versions = EXCLUDED.toolchain_versions, capabilities = EXCLUDED.capabilities,
133
- pod_contract = EXCLUDED.pod_contract, size_bytes = EXCLUDED.size_bytes,
134
- status = CASE WHEN sandbox_image_index.status = 'published'
135
- THEN sandbox_image_index.status ELSE EXCLUDED.status END,
136
- visibility = CASE WHEN sandbox_image_index.status = 'published'
137
- THEN sandbox_image_index.visibility ELSE EXCLUDED.visibility END,
138
- tenant_id = CASE WHEN sandbox_image_index.status = 'published'
139
- THEN sandbox_image_index.tenant_id ELSE EXCLUDED.tenant_id END,
140
- manifest_sha = EXCLUDED.manifest_sha, recipe_git_sha = EXCLUDED.recipe_git_sha,
141
- generator_version = EXCLUDED.generator_version, build_date = EXCLUDED.build_date,
142
- supersedes = EXCLUDED.supersedes,
143
- signed = CASE WHEN sandbox_image_index.status = 'published'
144
- THEN sandbox_image_index.signed ELSE EXCLUDED.signed END,
145
- updated_at = EXCLUDED.updated_at`, upsertParams(e, id, now));
146
- return id;
147
- }
148
- async getById(id) {
149
- const res = await this.pool.query("SELECT * FROM sandbox_image_index WHERE id = $1 LIMIT 1", [id]);
150
- return res.rows[0] ? mapRow(res.rows[0]) : null;
151
- }
152
- async getByDigest(digest) {
153
- const res = await this.pool.query("SELECT * FROM sandbox_image_index WHERE digest = $1 ORDER BY updated_at DESC LIMIT 1", [digest]);
154
- return res.rows[0] ? mapRow(res.rows[0]) : null;
155
- }
156
- async latestPublished(profile, viewer) {
157
- const vis = visibilityClause(viewer, 2);
158
- const res = await this.pool.query(`SELECT * FROM sandbox_image_index
159
- WHERE profile = $1 AND status = 'published' AND ${vis.sql}
160
- ORDER BY build_date DESC NULLS LAST, created_at DESC LIMIT 1`, [profile, ...vis.params]);
161
- return res.rows[0] ? mapRow(res.rows[0]) : null;
162
- }
163
- async list(filter) {
164
- const limit = Math.min(Math.max(1, filter.limit ?? DEFAULT_LIMIT), MAX_LIMIT);
165
- const vis = visibilityClause(filter, 1);
166
- const where = [vis.sql];
167
- const params = [...vis.params];
168
- let idx = vis.nextIdx;
169
- if (filter.profile) {
170
- where.push(`profile = $${idx++}`);
171
- params.push(filter.profile);
172
- }
173
- if (filter.status) {
174
- where.push(`status = $${idx++}`);
175
- params.push(filter.status);
176
- }
177
- if (filter.capability) {
178
- where.push(`capabilities -> $${idx++} = 'true'::jsonb`);
179
- params.push(filter.capability);
180
- }
181
- if (filter.cursor) {
182
- const c = decodeCursor(filter.cursor);
183
- if (c) {
184
- where.push(`(created_at < $${idx} OR (created_at = $${idx + 1} AND id < $${idx + 2}))`);
185
- params.push(new Date(c.createdAt), new Date(c.createdAt), c.id);
186
- idx += 3;
187
- }
188
- }
189
- const sql = `SELECT * FROM sandbox_image_index WHERE ${where.join(" AND ")} ` +
190
- `ORDER BY created_at DESC, id DESC LIMIT $${idx}`;
191
- params.push(limit + 1);
192
- const res = await this.pool.query(sql, params);
193
- let entries = res.rows.map(mapRow);
194
- let nextCursor = null;
195
- if (entries.length > limit) {
196
- entries = entries.slice(0, limit);
197
- const last = entries[entries.length - 1];
198
- nextCursor = encodeCursor({ createdAt: last.createdAt, id: last.id });
199
- }
200
- if (filter.latestOnly) {
201
- const seen = new Set();
202
- entries = entries.filter((e) => (seen.has(e.profile) ? false : (seen.add(e.profile), true)));
203
- }
204
- return { entries, nextCursor };
205
- }
206
- async setStatus(id, status, opts) {
207
- const sets = ["status = $1", "updated_at = $2"];
208
- const params = [status, new Date()];
209
- let idx = 3;
210
- if (opts && "supersedes" in opts) {
211
- sets.push(`supersedes = $${idx++}`);
212
- params.push(opts.supersedes ?? null);
213
- }
214
- params.push(id);
215
- await this.pool.query(`UPDATE sandbox_image_index SET ${sets.join(", ")} WHERE id = $${idx}`, params);
216
- }
217
- }
1
+ export { PgImageIndex, PG_IMAGE_INDEX_SCHEMA, ensureSchema } from "./image-index-sql.js";
218
2
  //# sourceMappingURL=pg-image-index.js.map