@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
@@ -0,0 +1,572 @@
1
+ import { RateLimiter } from "../../observability/rate-limit.js";
2
+ import { redactSecrets, redactDeep } from "../../trace/redact.js";
3
+ import { pgHasUnstorable } from "../../plugins/pg-safe-json.js";
4
+ import { manifestToIndexEntry } from "../../images/manifest.js";
5
+ import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
6
+ import { streamSseLog } from "../sse-log.js";
7
+ import { sendJson, sendError } from "../send.js";
8
+ import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk, explicitOperator } from "../principal-gate.js";
9
+ import { scopedIdempotencyKey } from "../idempotency.js";
10
+ export function createImagesLocal(deps) {
11
+ return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
12
+ }
13
+ export function coarseStatusForState(state) {
14
+ if (state === "COMPLETE")
15
+ return "done";
16
+ if (state === "FAILED" || state === "CANCELLED")
17
+ return "failed";
18
+ return "running";
19
+ }
20
+ export function errorCodeForExit(exitCode, cancelled) {
21
+ if (cancelled)
22
+ return "cancelled";
23
+ switch (exitCode) {
24
+ case 9:
25
+ return "band-conflict";
26
+ case 10:
27
+ return "duplicate";
28
+ case 124:
29
+ return "timeout";
30
+ case 507:
31
+ return "disk";
32
+ default:
33
+ return null;
34
+ }
35
+ }
36
+ const RUNNER_ERROR_CODES = new Set(["band-conflict", "duplicate", "disk", "timeout", "cancelled"]);
37
+ const SANDBOX_BASE_PROFILE = "sandbox-base";
38
+ export const IMAGE_DIGEST_RE = /^\/v1\/images\/digests\/([^/]+)$/;
39
+ export const IMAGE_PROFILE_RE = /^\/v1\/images\/([^/]+)$/;
40
+ export const BAKE_ID_RE = /^\/v1\/images\/bakes\/([^/]+)$/;
41
+ export const BAKE_EVENTS_RE = /^\/v1\/images\/bakes\/([^/]+)\/events$/;
42
+ export const BAKE_CANCEL_RE = /^\/v1\/images\/bakes\/([^/]+)\/cancel$/;
43
+ export const BAKE_CLAIM_RE = /^\/v1\/images\/bakes\/([^/]+)\/claim$/;
44
+ export const BAKE_INGEST_RE = /^\/v1\/images\/bakes\/([^/]+)\/ingest$/;
45
+ export async function handleImages(req, res, url, ctx) {
46
+ const miss = { fell: false };
47
+ await handleImagesBody(req, res, url, ctx, miss);
48
+ return !miss.fell;
49
+ }
50
+ async function handleImagesBody(req, res, url, ctx, miss) {
51
+ const { deps } = ctx;
52
+ const { readJson } = ctx.helpers;
53
+ const { source } = ctx.req;
54
+ const { bakeSubmitLimiter } = ctx.local.images;
55
+ if (deps.imageBakes && url.startsWith("/v1/images/bakes")) {
56
+ const bakes = deps.imageBakes;
57
+ const idx = deps.imageIndex;
58
+ const principal = gatedPrincipal(req, deps.config);
59
+ const operators = deps.config.operatorPrincipals;
60
+ const explicitOperator = explicitOperatorOk(principal, operators);
61
+ const cfg = deps.config.imageBakes;
62
+ const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
63
+ if (req.method === "POST" && url === "/v1/images/bakes") {
64
+ if (!explicitOperator) {
65
+ sendJson(res, 403, { error: "operator only" });
66
+ return;
67
+ }
68
+ const rl = bakeSubmitLimiter.check(`bake:${principal}`);
69
+ if (!rl.allowed) {
70
+ res.setHeader("retry-after", String(rl.retryAfterSec));
71
+ sendJson(res, 429, { error: "bake submission rate exceeded", retryAfterSec: rl.retryAfterSec });
72
+ return;
73
+ }
74
+ const body = (await readJson(req));
75
+ const v = validateBake(body);
76
+ if (!v.ok) {
77
+ sendJson(res, v.status, { error: v.error, ...(v.conflictBands ? { errorCode: "band-conflict", conflictBands: v.conflictBands } : {}) });
78
+ return;
79
+ }
80
+ let effectiveBaseRef;
81
+ if (v.baseRef) {
82
+ const baseEntry = idx ? await idx.getByDigest(v.baseRef) : null;
83
+ if (!baseEntry || baseEntry.status !== "published" || baseEntry.profile !== SANDBOX_BASE_PROFILE) {
84
+ sendJson(res, 400, { error: "baseRef is not a known published sandbox-base digest" });
85
+ return;
86
+ }
87
+ effectiveBaseRef = v.baseRef;
88
+ }
89
+ else {
90
+ effectiveBaseRef = cfg.defaultBaseRef ?? (idx ? (await idx.latestPublished(SANDBOX_BASE_PROFILE, { operator: true }))?.digest ?? null : null);
91
+ }
92
+ if (!v.dryRun && !effectiveBaseRef) {
93
+ sendJson(res, 503, { error: "no sandbox-base available to build from (set SANDBOX_BASE_REF or publish a sandbox-base image)" });
94
+ return;
95
+ }
96
+ const argv = buildBakeArgv(v, { baseRef: effectiveBaseRef, cacheBase: cfg.cacheBase });
97
+ const rawIdem = headerStr(req.headers["idempotency-key"]) ?? (typeof body.idempotencyKey === "string" ? body.idempotencyKey : undefined);
98
+ const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, principal) ?? null : null;
99
+ const createInput = {
100
+ profile: v.profile,
101
+ bands: v.bands,
102
+ baseRef: effectiveBaseRef,
103
+ push: v.push,
104
+ dryRun: v.dryRun,
105
+ logs: v.logs,
106
+ argv,
107
+ idemKey,
108
+ requestedBy: principal ?? null,
109
+ };
110
+ if (!v.dryRun) {
111
+ const sameArgv = await bakes.findActiveByArgv(argv);
112
+ if (sameArgv) {
113
+ sendJson(res, 202, { bakeId: sameArgv.bakeId, eventsUrl: `/v1/images/bakes/${sameArgv.bakeId}/events`, status: sameArgv.status, state: sameArgv.state });
114
+ return;
115
+ }
116
+ const r = await bakes.createBakeIfIdle(createInput);
117
+ if (!r.created) {
118
+ const sameNow = await bakes.findActiveByArgv(argv);
119
+ if (sameNow) {
120
+ sendJson(res, 202, { bakeId: sameNow.bakeId, eventsUrl: `/v1/images/bakes/${sameNow.bakeId}/events`, status: sameNow.status, state: sameNow.state });
121
+ return;
122
+ }
123
+ const busy = await bakes.findActiveAny();
124
+ sendJson(res, 409, { error: "a bake is already in progress", ...(busy ? { activeBakeId: busy.bakeId, eventsUrl: `/v1/images/bakes/${busy.bakeId}/events` } : {}) });
125
+ return;
126
+ }
127
+ sendJson(res, 202, { bakeId: r.bake.bakeId, eventsUrl: `/v1/images/bakes/${r.bake.bakeId}/events`, status: r.bake.status, state: r.bake.state });
128
+ return;
129
+ }
130
+ const { bake } = await bakes.createBake(createInput);
131
+ sendJson(res, 202, { bakeId: bake.bakeId, eventsUrl: `/v1/images/bakes/${bake.bakeId}/events`, status: bake.status, state: bake.state });
132
+ return;
133
+ }
134
+ const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
135
+ if (evMatch) {
136
+ if (!explicitOperator) {
137
+ sendJson(res, 403, { error: "operator only" });
138
+ return;
139
+ }
140
+ const bakeId = decodeURIComponent(evMatch[1]);
141
+ const bake = await bakes.getBake(bakeId);
142
+ if (!bake) {
143
+ sendJson(res, 404, { error: "bake not found" });
144
+ return;
145
+ }
146
+ await streamBakeEvents(req, res, bakes, bakeId, cfg.staleMs);
147
+ return;
148
+ }
149
+ const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
150
+ if (cancelMatch) {
151
+ if (!explicitOperator) {
152
+ sendJson(res, 403, { error: "operator only" });
153
+ return;
154
+ }
155
+ const bakeId = decodeURIComponent(cancelMatch[1]);
156
+ const bake = await bakes.getBake(bakeId);
157
+ if (!bake) {
158
+ sendJson(res, 404, { error: "bake not found" });
159
+ return;
160
+ }
161
+ const flagged = await bakes.requestCancel(bakeId);
162
+ sendJson(res, 202, { bakeId, cancelRequested: true, note: flagged ? "cancel requested — the runner kills the build at the next heartbeat" : "bake already terminal — cancel is a no-op" });
163
+ return;
164
+ }
165
+ if (req.method === "POST" && url === "/v1/images/bakes/claim") {
166
+ if (!isRunner) {
167
+ sendJson(res, 403, { error: "bake-runner only" });
168
+ return;
169
+ }
170
+ const body = (await readJson(req));
171
+ const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
172
+ let claimed = null;
173
+ for (let attempt = 0; attempt < 4 && !claimed; attempt++) {
174
+ const nextId = await bakes.findNextQueuedId();
175
+ if (!nextId)
176
+ break;
177
+ claimed = await bakes.claimBake(nextId, runnerId, cfg.leaseMs);
178
+ }
179
+ if (!claimed) {
180
+ res.writeHead(204).end();
181
+ return;
182
+ }
183
+ sendJson(res, 200, claimResponse(claimed));
184
+ return;
185
+ }
186
+ const claimMatch = req.method === "POST" ? BAKE_CLAIM_RE.exec(url) : null;
187
+ if (claimMatch) {
188
+ if (!isRunner) {
189
+ sendJson(res, 403, { error: "bake-runner only" });
190
+ return;
191
+ }
192
+ const bakeId = decodeURIComponent(claimMatch[1]);
193
+ const body = (await readJson(req));
194
+ const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
195
+ const claimed = await bakes.claimBake(bakeId, runnerId, cfg.leaseMs);
196
+ if (!claimed) {
197
+ sendJson(res, 409, { error: "bake not claimable (already claimed or the build lease is held)" });
198
+ return;
199
+ }
200
+ sendJson(res, 200, claimResponse(claimed));
201
+ return;
202
+ }
203
+ const ingestMatch = req.method === "POST" ? BAKE_INGEST_RE.exec(url) : null;
204
+ if (ingestMatch) {
205
+ if (!isRunner) {
206
+ sendJson(res, 403, { error: "bake-runner only" });
207
+ return;
208
+ }
209
+ const bakeId = decodeURIComponent(ingestMatch[1]);
210
+ const bake = await bakes.getBake(bakeId);
211
+ if (!bake) {
212
+ sendJson(res, 404, { error: "bake not found" });
213
+ return;
214
+ }
215
+ const presented = headerStr(req.headers["x-bake-ingest-secret"]);
216
+ if (!bake.ingestSecret || !presented || !safeEqual(presented, bake.ingestSecret)) {
217
+ sendJson(res, 403, { error: "invalid ingest secret" });
218
+ return;
219
+ }
220
+ const line = (await readJson(req));
221
+ if (line.event === "heartbeat") {
222
+ const fresh = await bakes.getBake(bakeId);
223
+ if (!fresh || fresh.status !== "running") {
224
+ sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh?.cancelRequested ?? false });
225
+ return;
226
+ }
227
+ const ok = bake.runnerId ? await bakes.heartbeatLease(bakeId, bake.runnerId, cfg.leaseMs) : false;
228
+ if (!ok) {
229
+ sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh.cancelRequested });
230
+ return;
231
+ }
232
+ sendJson(res, 200, { accepted: true, leaseValid: true, cancelRequested: fresh.cancelRequested });
233
+ return;
234
+ }
235
+ const fresh = await bakes.getBake(bakeId);
236
+ if (!fresh || fresh.status !== "running") {
237
+ sendJson(res, 409, { accepted: false, leaseValid: false, cancelRequested: fresh?.cancelRequested ?? false });
238
+ return;
239
+ }
240
+ if (fresh.runnerId) {
241
+ await bakes.heartbeatLease(bakeId, fresh.runnerId, cfg.leaseMs).catch(() => undefined);
242
+ }
243
+ const result = await ingestBakeLine(bakes, idx, fresh, line, cfg.registry);
244
+ const after = await bakes.getBake(bakeId);
245
+ sendJson(res, result.status, { ...result.body, cancelRequested: after?.cancelRequested ?? false, leaseValid: true });
246
+ return;
247
+ }
248
+ const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
249
+ if (idMatch) {
250
+ if (!explicitOperator) {
251
+ sendJson(res, 403, { error: "operator only" });
252
+ return;
253
+ }
254
+ const bakeId = decodeURIComponent(idMatch[1]);
255
+ const bake = await bakes.getBake(bakeId);
256
+ if (!bake) {
257
+ sendJson(res, 404, { error: "bake not found" });
258
+ return;
259
+ }
260
+ sendJson(res, 200, { bake: bakeView(bake) });
261
+ return;
262
+ }
263
+ sendJson(res, 404, { error: "not found" });
264
+ return;
265
+ }
266
+ if (deps.imageIndex && url.startsWith("/v1/images")) {
267
+ const idx = deps.imageIndex;
268
+ const principal = gatedPrincipal(req, deps.config);
269
+ const operator = explicitOperator(principal, deps.config.operatorPrincipals);
270
+ const viewer = { operator, tenantId: principal ?? null };
271
+ const q = new URL(req.url ?? "", "http://x").searchParams;
272
+ if (req.method === "GET" && url === "/v1/images") {
273
+ const limRaw = q.get("limit");
274
+ const { entries, nextCursor } = await idx.list({
275
+ ...viewer,
276
+ profile: q.get("profile") ?? undefined,
277
+ status: q.get("status") ?? undefined,
278
+ capability: q.get("capability") ?? undefined,
279
+ latestOnly: q.get("latest") === "true",
280
+ limit: limRaw ? Number(limRaw) : undefined,
281
+ cursor: q.get("cursor") ?? undefined,
282
+ });
283
+ sendJson(res, 200, { images: entries, nextCursor });
284
+ return;
285
+ }
286
+ if (req.method === "POST" && url === "/v1/images/select") {
287
+ const body = (await readJson(req));
288
+ const profile = typeof body.profile === "string" ? body.profile : "";
289
+ if (!profile) {
290
+ sendJson(res, 400, { error: "profile is required" });
291
+ return;
292
+ }
293
+ const entry = await idx.latestPublished(profile, viewer);
294
+ if (!entry) {
295
+ sendJson(res, 404, { error: `no published image for profile '${profile}' visible to this principal` });
296
+ return;
297
+ }
298
+ const needed = Array.isArray(body.capabilitiesNeeded)
299
+ ? body.capabilitiesNeeded.filter((c) => typeof c === "string")
300
+ : [];
301
+ const caps = entry.capabilities;
302
+ const missing = needed.filter((c) => !caps[c]);
303
+ if (missing.length > 0) {
304
+ sendJson(res, 409, { error: "capabilities not satisfied by the image", missing });
305
+ return;
306
+ }
307
+ sendJson(res, 200, {
308
+ profile: entry.profile,
309
+ digest: entry.digest,
310
+ repo: entry.repo,
311
+ ref: `${entry.repo}@${entry.digest}`,
312
+ podContract: entry.podContract,
313
+ capabilities: entry.capabilities,
314
+ manifestSha: entry.manifestSha,
315
+ });
316
+ return;
317
+ }
318
+ if (req.method === "POST" && url === "/v1/images/register") {
319
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
320
+ sendJson(res, 403, { error: "operator only" });
321
+ return;
322
+ }
323
+ const b = (await readJson(req));
324
+ if (typeof b.profile !== "string" || typeof b.repo !== "string" || typeof b.digest !== "string") {
325
+ sendJson(res, 400, { error: "profile, repo, digest are required" });
326
+ return;
327
+ }
328
+ const id = await idx.registerBuilding({
329
+ profile: b.profile,
330
+ bands: Array.isArray(b.bands) ? b.bands : [],
331
+ repo: b.repo,
332
+ tag: typeof b.tag === "string" ? b.tag : "",
333
+ digest: b.digest,
334
+ toolchainVersions: b.toolchainVersions ?? {},
335
+ capabilities: b.capabilities ?? {},
336
+ podContract: b.podContract ?? {},
337
+ sizeBytes: typeof b.sizeBytes === "number" ? b.sizeBytes : null,
338
+ status: (typeof b.status === "string" ? b.status : "building"),
339
+ visibility: b.visibility === "tenant" ? "tenant" : "public",
340
+ tenantId: typeof b.tenantId === "string" ? b.tenantId : null,
341
+ manifestSha: typeof b.manifestSha === "string" ? b.manifestSha : null,
342
+ recipeGitSha: typeof b.recipeGitSha === "string" ? b.recipeGitSha : null,
343
+ generatorVersion: typeof b.generatorVersion === "string" ? b.generatorVersion : null,
344
+ buildDate: typeof b.buildDate === "string" ? b.buildDate : null,
345
+ supersedes: typeof b.supersedes === "string" ? b.supersedes : null,
346
+ signed: b.signed === true,
347
+ });
348
+ sendJson(res, 200, { id });
349
+ return;
350
+ }
351
+ const digMatch = req.method === "GET" ? IMAGE_DIGEST_RE.exec(url) : null;
352
+ if (digMatch) {
353
+ const entry = await idx.getByDigest(decodeURIComponent(digMatch[1]));
354
+ if (!entry || (!viewer.operator && entry.visibility === "tenant" && entry.tenantId !== viewer.tenantId)) {
355
+ sendJson(res, 404, { error: "not found" });
356
+ return;
357
+ }
358
+ sendJson(res, 200, { image: entry });
359
+ return;
360
+ }
361
+ const profMatch = req.method === "GET" ? IMAGE_PROFILE_RE.exec(url) : null;
362
+ if (profMatch) {
363
+ const entry = await idx.latestPublished(decodeURIComponent(profMatch[1]), viewer);
364
+ if (!entry) {
365
+ sendJson(res, 404, { error: "not found" });
366
+ return;
367
+ }
368
+ sendJson(res, 200, { image: entry });
369
+ return;
370
+ }
371
+ }
372
+ miss.fell = true;
373
+ }
374
+ async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
375
+ await streamSseLog(req, res, {
376
+ statusOf: async (id) => {
377
+ const bake = await bakes.getBake(id);
378
+ return bake ? { status: bake.status, updatedAt: bake.updatedAt } : undefined;
379
+ },
380
+ getEvents: (id, after) => bakes.getEvents(id, after),
381
+ retainedFrom: (id) => bakes.retainedFrom(id),
382
+ formatEvent: (ev) => ({
383
+ id: ev.seq,
384
+ event: ev.kind,
385
+ data: { kind: ev.kind, seq: ev.seq, ts: ev.ts, ...(ev.level ? { level: ev.level } : {}), ...(ev.data ?? {}) },
386
+ }),
387
+ staleFrame: () => ({ event: "done", data: { kind: "done", status: "FAILED", error: "bake-runner lost (no heartbeat)", digest: null, indexId: null, repo: null, ref: null } }),
388
+ }, bakeId, staleMs);
389
+ }
390
+ function bakeView(b) {
391
+ return {
392
+ bakeId: b.bakeId,
393
+ status: b.status,
394
+ state: b.state,
395
+ profile: b.profile,
396
+ bands: b.bands,
397
+ baseRef: b.baseRef,
398
+ push: b.push,
399
+ dryRun: b.dryRun,
400
+ logs: b.logs,
401
+ digest: b.digest,
402
+ repo: b.repo,
403
+ ref: b.ref,
404
+ indexId: b.indexId,
405
+ exitCode: b.exitCode,
406
+ error: b.error,
407
+ errorCode: b.errorCode,
408
+ manifestSha: b.manifestSha,
409
+ tag: b.tag,
410
+ requestedBy: b.requestedBy,
411
+ createdAt: b.createdAt,
412
+ updatedAt: b.updatedAt,
413
+ };
414
+ }
415
+ function claimResponse(claimed) {
416
+ return {
417
+ bakeId: claimed.bakeId,
418
+ argv: claimed.argv,
419
+ push: claimed.push,
420
+ dryRun: claimed.dryRun,
421
+ logs: claimed.logs,
422
+ profile: claimed.profile,
423
+ bands: claimed.bands,
424
+ ingestSecret: claimed.ingestSecret,
425
+ leaseUntil: claimed.leaseUntil,
426
+ };
427
+ }
428
+ async function ingestBakeLine(bakes, idx, bake, line, registry) {
429
+ const bakeId = bake.bakeId;
430
+ const event = typeof line.event === "string" ? line.event : "";
431
+ const lineOrd = typeof line.lineOrd === "number" ? line.lineOrd : typeof line.line_ord === "number" ? line.line_ord : null;
432
+ if (event !== "done") {
433
+ if (event === "state") {
434
+ const state = typeof line.state === "string" ? line.state : null;
435
+ if (state)
436
+ await bakes.setState(bakeId, state);
437
+ const seq = await bakes.appendEvent(bakeId, "state", { state }, { lineOrd });
438
+ return { status: 200, body: { accepted: true, seq } };
439
+ }
440
+ const kind = event === "manifest" ? "manifest" : event;
441
+ const level = event === "log" && line.level === "raw" ? "raw" : null;
442
+ const { event: _e, lineOrd: _lo, line_ord: _li, ...data } = line;
443
+ const seq = await bakes.appendEvent(bakeId, kind, redactDeep(data), { lineOrd, level });
444
+ return { status: 200, body: { accepted: true, seq } };
445
+ }
446
+ const doneStatus = typeof line.status === "string" ? line.status : "FAILED";
447
+ const exitCode = typeof line.exitCode === "number" ? line.exitCode : doneStatus === "COMPLETE" ? 0 : null;
448
+ const cancelled = doneStatus === "CANCELLED";
449
+ const errorText = typeof line.error === "string" ? redactSecrets(line.error) : null;
450
+ const tag = typeof line.tag === "string" ? line.tag : bake.tag;
451
+ let digest = typeof line.digest === "string" && line.digest.length > 0 ? line.digest : null;
452
+ let indexId = bake.indexId;
453
+ let repo = bake.repo;
454
+ let ref = bake.ref;
455
+ let manifestSha = bake.manifestSha;
456
+ let finalStatus = doneStatus;
457
+ let finalError = errorText;
458
+ let finalExit = exitCode;
459
+ let registerDowngraded = false;
460
+ const registerSteps = [];
461
+ if (doneStatus === "COMPLETE" && digest && idx) {
462
+ const manifestBody = (line.manifestBody ?? line.manifest);
463
+ const fresh = await bakes.getBake(bakeId);
464
+ const nonTerminal = !!fresh && (fresh.status === "queued" || fresh.status === "running");
465
+ if (manifestBody && typeof manifestBody === "object" && nonTerminal) {
466
+ const profile = bake.profile && bake.profile !== "custom" ? bake.profile : (manifestBody.profile && manifestBody.profile !== "custom" ? manifestBody.profile : "custom");
467
+ repo = registry ? `${registry}/${profile}` : `sema-images/${profile}`;
468
+ manifestSha = typeof line.manifestSha === "string" ? line.manifestSha : (typeof line.manifest?.sha256 === "string" ? String(line.manifest.sha256) : bake.manifestSha);
469
+ if (pgHasUnstorable(digest) || pgHasUnstorable(repo) || pgHasUnstorable(manifestSha)) {
470
+ indexId = null;
471
+ registerSteps.push({ step: "register", status: "skipped", reason: "identity fields contained unstorable bytes" });
472
+ }
473
+ else {
474
+ registerSteps.push({ step: "register", status: "running" });
475
+ try {
476
+ const entry = manifestToIndexEntry(manifestBody, { repo, digest, status: "building", manifestSha, sizeBytes: manifestBody.sizeBytes ?? null, signed: false });
477
+ entry.visibility = "public";
478
+ entry.tenantId = null;
479
+ indexId = await idx.registerBuilding(entry);
480
+ ref = `${repo}@${digest}`;
481
+ const bound = await bakes.setIndexId(bakeId, indexId);
482
+ if (!bound)
483
+ indexId = null;
484
+ registerSteps.push({ step: "register", status: "ok" });
485
+ }
486
+ catch (err) {
487
+ indexId = null;
488
+ ref = `${repo}@${digest}`;
489
+ finalStatus = "FAILED";
490
+ registerDowngraded = true;
491
+ finalExit = exitCode ?? 0;
492
+ finalError = redactSecrets(`index register failed after a COMPLETE build (built-but-unregistered partial): ${String(err?.message ?? err)}`);
493
+ registerSteps.push({ step: "register", status: "fail", reason: "index register failed (TiDB?)" });
494
+ }
495
+ }
496
+ }
497
+ }
498
+ const suppliedCode = typeof line.errorCode === "string" && RUNNER_ERROR_CODES.has(line.errorCode)
499
+ ? line.errorCode
500
+ : undefined;
501
+ const errorCode = cancelled
502
+ ? "cancelled"
503
+ : suppliedCode !== undefined && !registerDowngraded
504
+ ? suppliedCode
505
+ : errorCodeForExit(finalExit, cancelled);
506
+ const conflictBands = errorCode === "band-conflict" && Array.isArray(line.conflictBands)
507
+ ? line.conflictBands.filter((b) => typeof b === "string")
508
+ : null;
509
+ const identityInvalid = (typeof digest === "string" && pgHasUnstorable(digest)) ||
510
+ (typeof repo === "string" && pgHasUnstorable(repo)) ||
511
+ (typeof ref === "string" && pgHasUnstorable(ref)) ||
512
+ (typeof manifestSha === "string" && pgHasUnstorable(manifestSha));
513
+ if (identityInvalid) {
514
+ finalStatus = "FAILED";
515
+ digest = null;
516
+ repo = null;
517
+ ref = null;
518
+ manifestSha = null;
519
+ indexId = null;
520
+ finalError = finalError ? `${finalError}; identity fields contained unstorable bytes` : "terminal frame identity fields contained unstorable bytes";
521
+ }
522
+ const finalState = (finalStatus === "COMPLETE" ? "COMPLETE" : finalStatus === "CANCELLED" ? "CANCELLED" : "FAILED");
523
+ const coarse = coarseStatusForState(finalState);
524
+ let affectedRows = 0;
525
+ const terminal = {
526
+ status: coarse === "done" ? "done" : "failed",
527
+ state: finalState,
528
+ digest,
529
+ repo,
530
+ ref,
531
+ indexId,
532
+ exitCode: finalExit,
533
+ error: finalError,
534
+ errorCode,
535
+ manifestSha,
536
+ tag,
537
+ };
538
+ let lastErr;
539
+ for (let attempt = 0; attempt < 3; attempt += 1) {
540
+ try {
541
+ affectedRows = await bakes.setTerminal(bakeId, terminal);
542
+ lastErr = undefined;
543
+ break;
544
+ }
545
+ catch (err) {
546
+ lastErr = err;
547
+ }
548
+ }
549
+ if (lastErr !== undefined) {
550
+ return { status: 200, body: { accepted: true, terminal: false, needsFlip: true } };
551
+ }
552
+ if (affectedRows >= 1) {
553
+ for (const s of registerSteps)
554
+ await bakes.appendEvent(bakeId, "step", s);
555
+ await bakes.appendEvent(bakeId, "done", {
556
+ status: finalStatus,
557
+ digest,
558
+ repo,
559
+ ref,
560
+ indexId,
561
+ tag,
562
+ manifestSha,
563
+ sizeBytes: typeof line.sizeBytes === "number" ? line.sizeBytes : null,
564
+ exitCode: finalExit,
565
+ error: finalError,
566
+ errorCode,
567
+ ...(conflictBands ? { conflictBands } : {}),
568
+ }, { lineOrd });
569
+ }
570
+ return { status: 200, body: { accepted: true, terminal: affectedRows >= 1, indexId } };
571
+ }
572
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1,4 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { RouteCtx } from "../route-ctx.js";
3
+ export declare function handleMemoryPolicy(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
4
+ //# sourceMappingURL=memory-policy.d.ts.map
@@ -0,0 +1,105 @@
1
+ import { formatUserScope } from "@sema-agent/core";
2
+ import { parseMemorySyncRequest } from "../../memory-sync.js";
3
+ import { sendJson, sendError } from "../send.js";
4
+ import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
5
+ export async function handleMemoryPolicy(req, res, url, ctx) {
6
+ const miss = { fell: false };
7
+ await handleMemoryPolicyBody(req, res, url, ctx, miss);
8
+ return !miss.fell;
9
+ }
10
+ async function handleMemoryPolicyBody(req, res, url, ctx, miss) {
11
+ const { deps } = ctx;
12
+ const { readJson } = ctx.helpers;
13
+ if (url === "/v1/memory/export" && req.method === "GET") {
14
+ const principal = gatedPrincipal(req, deps.config);
15
+ if (!principal) {
16
+ sendJson(res, 401, { error: "unauthorized" });
17
+ return;
18
+ }
19
+ if (deps.config.memoryEngineBackend === "file" || !deps.memoryExport) {
20
+ sendJson(res, 501, { error: "memory export requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)" });
21
+ return;
22
+ }
23
+ const scope = new URL(req.url ?? "", "http://x").searchParams.get("scope");
24
+ if (!scope) {
25
+ sendJson(res, 400, { error: "missing ?scope=<key>" });
26
+ return;
27
+ }
28
+ if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
29
+ sendJson(res, 404, { error: "not found" });
30
+ return;
31
+ }
32
+ try {
33
+ const entries = await deps.memoryExport(scope);
34
+ sendJson(res, 200, { scope, exportedAt: new Date().toISOString(), entries });
35
+ }
36
+ catch (err) {
37
+ deps.logger?.warn?.("memory_export_failed", { scope, err: String(err) });
38
+ sendJson(res, 500, { error: "memory export failed" });
39
+ }
40
+ return;
41
+ }
42
+ if (req.method === "POST" && url.startsWith("/v1/memory/sync/")) {
43
+ const principal = gatedPrincipal(req, deps.config);
44
+ if (!principal) {
45
+ sendJson(res, 401, { error: "unauthorized" });
46
+ return;
47
+ }
48
+ if (deps.config.memoryEngineBackend === "file" || !deps.memorySync) {
49
+ sendJson(res, 501, { error: "memory sync requires a DB memory backend (MEMORY_ENGINE_BACKEND=pg|tidb)" });
50
+ return;
51
+ }
52
+ let scope;
53
+ try {
54
+ scope = decodeURIComponent(url.slice("/v1/memory/sync/".length));
55
+ }
56
+ catch {
57
+ sendError(res, 400, "memory_sync_invalid_scope", "malformed percent-encoding in :scope", { code: "memory_sync_invalid_scope" });
58
+ return;
59
+ }
60
+ if (scope.length === 0) {
61
+ sendError(res, 400, "memory_sync_invalid_scope", "missing :scope", { code: "memory_sync_invalid_scope" });
62
+ return;
63
+ }
64
+ if (scope !== formatUserScope(principal) && !explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
65
+ sendJson(res, 404, { error: "not found" });
66
+ return;
67
+ }
68
+ const body = await readJson(req);
69
+ const parsed = parseMemorySyncRequest(body, scope);
70
+ if (!parsed.ok) {
71
+ sendError(res, 422, "memory_sync_invalid_body", parsed.error, { code: "memory_sync_invalid_body" });
72
+ return;
73
+ }
74
+ try {
75
+ const out = await deps.memorySync(scope, parsed.value);
76
+ sendJson(res, 200, out);
77
+ }
78
+ catch (err) {
79
+ deps.logger?.warn?.("memory_sync_failed", { scope, peer: parsed.value.peer, err: String(err) });
80
+ sendJson(res, 500, { error: "memory sync failed" });
81
+ }
82
+ return;
83
+ }
84
+ if (url === "/v1/policy" && req.method === "GET") {
85
+ const principal = gatedPrincipal(req, deps.config);
86
+ if (deps.config.requirePrincipal && !principal) {
87
+ sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
88
+ return;
89
+ }
90
+ sendJson(res, 200, {
91
+ autonomy: deps.config.autonomy ?? null,
92
+ commandPolicy: deps.config.commandPolicy ?? [],
93
+ approvalRequire: deps.config.approvalRequire,
94
+ limits: {
95
+ maxTaskCostUsd: deps.config.maxTaskCostUsd,
96
+ maxTaskTokens: deps.config.maxTaskTokens,
97
+ maxPrincipalCostUsd: deps.config.maxPrincipalCostUsd,
98
+ costQuotaWindowSec: deps.config.costQuotaWindowSec,
99
+ },
100
+ });
101
+ return;
102
+ }
103
+ miss.fell = true;
104
+ }
105
+ //# sourceMappingURL=memory-policy.js.map