@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,4 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { RouteCtx } from "../route-ctx.js";
3
+ export declare function handleObservability(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
4
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { sendJson } from "../send.js";
2
+ import { gatedPrincipal, isOperator, explicitOperatorOk } from "../principal-gate.js";
3
+ export async function handleObservability(req, res, url, ctx) {
4
+ const miss = { fell: false };
5
+ await handleObservabilityBody(req, res, url, ctx, miss);
6
+ return !miss.fell;
7
+ }
8
+ async function handleObservabilityBody(req, res, url, ctx, miss) {
9
+ const { deps } = ctx;
10
+ if (url === "/v1/outcomes" && req.method === "GET") {
11
+ if (deps.config.requirePrincipal) {
12
+ const principal = gatedPrincipal(req, deps.config);
13
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
14
+ sendJson(res, 403, { error: "outcomes view is operator-only on a multi-tenant worker" });
15
+ return;
16
+ }
17
+ }
18
+ if (!deps.outcomeSink?.summary) {
19
+ sendJson(res, 501, { error: "no queryable outcome ledger on this worker (File sink — read the JSONL dataset directly)" });
20
+ return;
21
+ }
22
+ const q = new URL(req.url ?? "/", "http://localhost").searchParams;
23
+ const sig = q.get("signature") ?? undefined;
24
+ try {
25
+ const rows = await deps.outcomeSink.summary({ ...(sig ? { taskSignature: sig } : {}), includeAborted: q.get("includeAborted") === "true" });
26
+ sendJson(res, 200, { outcomes: rows });
27
+ }
28
+ catch (err) {
29
+ deps.logger?.warn?.("outcomes_summary_failed", { err: String(err) });
30
+ sendJson(res, 500, { error: "outcome summary failed" });
31
+ }
32
+ return;
33
+ }
34
+ if (url === "/v1/usage" && req.method === "GET") {
35
+ const principal = gatedPrincipal(req, deps.config);
36
+ if (!principal) {
37
+ sendJson(res, 401, { error: `missing principal header '${deps.config.principalHeader}'` });
38
+ return;
39
+ }
40
+ const windowSec = deps.config.costQuotaWindowSec;
41
+ if (!deps.costQuota) {
42
+ sendJson(res, 200, { enabled: false, windowSec, maxTaskCostUsd: deps.config.maxTaskCostUsd, maxTaskTokens: deps.config.maxTaskTokens });
43
+ return;
44
+ }
45
+ const d = deps.costQuota.check(principal);
46
+ sendJson(res, 200, {
47
+ enabled: true,
48
+ windowSec,
49
+ usedMicroUsd: d.usedMicroUsd,
50
+ usedUsd: d.usedMicroUsd / 1_000_000,
51
+ limitMicroUsd: d.limitMicroUsd,
52
+ limitUsd: d.limitMicroUsd / 1_000_000,
53
+ remainingMicroUsd: Math.max(0, d.limitMicroUsd - d.usedMicroUsd),
54
+ overLimit: !d.allowed,
55
+ retryAfterSec: d.retryAfterSec,
56
+ maxTaskCostUsd: deps.config.maxTaskCostUsd,
57
+ maxTaskTokens: deps.config.maxTaskTokens,
58
+ });
59
+ return;
60
+ }
61
+ miss.fell = true;
62
+ }
63
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1,36 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type SessionRulesRecord } from "@sema-agent/core";
3
+ import { type StagingHandle } from "../../session-sync.js";
4
+ import type { RouteCtx } from "../route-ctx.js";
5
+ export type PendingStaging = {
6
+ handle: StagingHandle;
7
+ realSessionId: string;
8
+ owner: string | null;
9
+ resolution?: "overwrite-dst";
10
+ operatorOk: boolean;
11
+ snapshots: Array<{
12
+ key: string;
13
+ manifest: Array<[string, string]>;
14
+ }>;
15
+ policy: SessionRulesRecord[];
16
+ anchors: Array<{
17
+ eventId: string;
18
+ entryId: string;
19
+ owner: string | null;
20
+ }>;
21
+ declaredEntryIds: string[];
22
+ openedBy: string | null;
23
+ openedFleetWide: boolean;
24
+ };
25
+ export interface SessionSyncLocal {
26
+ pendingStagings: Map<string, PendingStaging>;
27
+ importLeases: Map<string, {
28
+ at: number;
29
+ stagingId: string;
30
+ }>;
31
+ touchImportLease(sessionId: string): void;
32
+ cleanupStaging(stagingId: string): void;
33
+ }
34
+ export declare function createSessionSyncLocal(): SessionSyncLocal;
35
+ export declare function handleSessionSync(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
36
+ //# sourceMappingURL=session-sync.d.ts.map
@@ -0,0 +1,513 @@
1
+ import { once } from "node:events";
2
+ import { randomBytes, createHash } from "node:crypto";
3
+ import { sessionLogDigest, sessionLogDigestsComparable, uuidv7, StreamingImportValidator, SessionError, SessionPolicyError } from "@sema-agent/core";
4
+ import { StringDecoder } from "node:string_decoder";
5
+ import { exportSession, exportSessionManifest, importSession, classifySyncRelationshipByIds, SyncConflictError, stagingIdFor } from "../../session-sync.js";
6
+ import { isUuidV7, isUuidShape } from "../../security.js";
7
+ import { sendJson, sendError, msg } from "../send.js";
8
+ import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
9
+ const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
10
+ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
11
+ const SYNC_IMPORT_BATCH = 500;
12
+ const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
13
+ export function createSessionSyncLocal() {
14
+ const pendingStagings = new Map();
15
+ const importLeases = new Map();
16
+ const touchImportLease = (sessionId) => {
17
+ const cur = importLeases.get(sessionId);
18
+ if (cur)
19
+ importLeases.set(sessionId, { ...cur, at: Date.now() });
20
+ };
21
+ const cleanupStaging = (stagingId) => {
22
+ const p = pendingStagings.get(stagingId);
23
+ if (p)
24
+ importLeases.delete(p.realSessionId);
25
+ pendingStagings.delete(stagingId);
26
+ };
27
+ return { pendingStagings, importLeases, touchImportLease, cleanupStaging };
28
+ }
29
+ export async function handleSessionSync(req, res, url, ctx) {
30
+ const miss = { fell: false };
31
+ await handleSessionSyncBody(req, res, url, ctx, miss);
32
+ return !miss.fell;
33
+ }
34
+ async function handleSessionSyncBody(req, res, url, ctx, miss) {
35
+ const { deps } = ctx;
36
+ const { readJson, readRawBody, safeDecode, sessionOwnerScopeForWrite } = ctx.helpers;
37
+ const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
38
+ {
39
+ const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|snapshots\/([^/]+)\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
40
+ if (sm) {
41
+ const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
42
+ if (!deps.backend || !ownerOf) {
43
+ sendJson(res, 501, { error: "session sync requires a durable session store" });
44
+ return;
45
+ }
46
+ const scope = sessionOwnerScopeForWrite(req);
47
+ if (!scope.fleetWide && scope.gateOwner === null) {
48
+ sendJson(res, 401, { error: "unauthorized" });
49
+ return;
50
+ }
51
+ const sessionId = safeDecode(sm[1]);
52
+ if (sessionId === null || !isUuidShape(sessionId)) {
53
+ sendJson(res, 400, { error: "invalid session id (must be a canonical uuid)" });
54
+ return;
55
+ }
56
+ const owner = await ownerOf(sessionId);
57
+ const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
58
+ const pullGateOk = () => {
59
+ if (owner === undefined) {
60
+ sendJson(res, 404, { error: "session not found" });
61
+ return false;
62
+ }
63
+ if (!ownsOrFleet(owner)) {
64
+ sendJson(res, 404, { error: "session not found" });
65
+ return false;
66
+ }
67
+ return true;
68
+ };
69
+ const pushGateOk = () => {
70
+ if (owner !== undefined && !ownsOrFleet(owner)) {
71
+ sendJson(res, 404, { error: "session not found" });
72
+ return false;
73
+ }
74
+ return true;
75
+ };
76
+ if (sm[2] === "manifest" && req.method === "GET") {
77
+ if (!pullGateOk())
78
+ return;
79
+ const out = await exportSessionManifest(sessionId, deps.backend);
80
+ if (out === null) {
81
+ sendJson(res, 404, { error: "session not found" });
82
+ return;
83
+ }
84
+ sendJson(res, 200, { manifest: out.manifest });
85
+ return;
86
+ }
87
+ if (sm[2] === "entries" && req.method === "GET") {
88
+ if (!pullGateOk())
89
+ return;
90
+ const afterSeqRaw = new URL(req.url ?? "", "http://x").searchParams.get("afterSeq");
91
+ let afterSeq;
92
+ if (afterSeqRaw !== null) {
93
+ const n = Number(afterSeqRaw);
94
+ if (!Number.isInteger(n) || n < 0) {
95
+ sendJson(res, 400, { error: "afterSeq must be a non-negative integer" });
96
+ return;
97
+ }
98
+ afterSeq = n;
99
+ }
100
+ const stream = deps.sessionStorage?.exportEntriesStream?.bind(deps.sessionStorage);
101
+ if (!stream) {
102
+ sendJson(res, 501, { error: "session entry streaming requires a durable session store" });
103
+ return;
104
+ }
105
+ const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
106
+ if (it === null) {
107
+ sendJson(res, 404, { error: "session not found" });
108
+ return;
109
+ }
110
+ res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });
111
+ const write = async (line) => { if (!res.write(line))
112
+ await once(res, "drain"); };
113
+ let count = 0;
114
+ try {
115
+ await write(`${JSON.stringify({ __sync: "begin", sessionId })}\n`);
116
+ for await (const entry of it) {
117
+ await write(`${JSON.stringify(entry)}\n`);
118
+ count++;
119
+ }
120
+ await write(`${JSON.stringify({ __sync: "end", count })}\n`);
121
+ res.end();
122
+ }
123
+ catch (e) {
124
+ deps.logger?.error?.("sync entries stream failed", { sessionId, count, error: msg(e) });
125
+ res.end();
126
+ }
127
+ return;
128
+ }
129
+ if (sm[4] !== undefined && req.method === "GET") {
130
+ if (!pullGateOk())
131
+ return;
132
+ const key = safeDecode(sm[4]);
133
+ const hash = safeDecode(sm[5]);
134
+ if (key === null || hash === null) {
135
+ sendJson(res, 400, { error: "invalid snapshot key or blob hash" });
136
+ return;
137
+ }
138
+ if (!SHA256_HEX_RE.test(hash)) {
139
+ sendJson(res, 400, { error: "blob hash must be 64 lowercase-hex chars" });
140
+ return;
141
+ }
142
+ const fs = deps.fileSnapshotStore;
143
+ const manifest = await fs?.exportManifest?.(sessionId, key);
144
+ if (!manifest || ![...manifest.values()].includes(hash)) {
145
+ sendJson(res, 404, { error: "blob not found" });
146
+ return;
147
+ }
148
+ const bytes = await fs?.getBlob?.(hash);
149
+ if (!bytes) {
150
+ sendJson(res, 404, { error: "blob not found" });
151
+ return;
152
+ }
153
+ res.writeHead(200, { "content-type": "application/octet-stream" });
154
+ res.end(Buffer.from(bytes));
155
+ return;
156
+ }
157
+ if (sm[3] !== undefined && req.method === "PUT") {
158
+ if (!pushGateOk())
159
+ return;
160
+ const hash = safeDecode(sm[3]);
161
+ if (hash === null || !SHA256_HEX_RE.test(hash)) {
162
+ sendJson(res, 400, { error: "blob hash must be 64 lowercase-hex chars" });
163
+ return;
164
+ }
165
+ const fs = deps.fileSnapshotStore;
166
+ if (typeof fs?.putBlob !== "function") {
167
+ sendJson(res, 501, { error: "blob upload requires a durable snapshot store" });
168
+ return;
169
+ }
170
+ const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
171
+ if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
172
+ sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`, { code: "blob_too_large_for_sql" });
173
+ return;
174
+ }
175
+ if (createHash("sha256").update(body).digest("hex") !== hash) {
176
+ sendJson(res, 400, { error: "blob hash mismatch" });
177
+ return;
178
+ }
179
+ const put = await fs.putBlob(hash, new Uint8Array(body));
180
+ if (!put.ok) {
181
+ sendError(res, 502, put.error.code, "blob store write failed", { code: put.error.code });
182
+ return;
183
+ }
184
+ res.writeHead(204).end();
185
+ return;
186
+ }
187
+ if (sm[2] === "import" && req.method === "POST") {
188
+ if (!pushGateOk())
189
+ return;
190
+ if (deps.sessionMirrorRuling && !scope.fleetWide && scope.gateOwner !== null) {
191
+ const auditPrincipal = scope.gateOwner;
192
+ void deps.sessionMirrorRuling(auditPrincipal).then((mirror) => {
193
+ if (mirror?.required) {
194
+ deps.metrics?.inc("session_mirror_required_push_total");
195
+ deps.logger?.info("session_mirror_required_push_observed", { principal: auditPrincipal, engineUrl: mirror.engineUrl, sessionId });
196
+ }
197
+ }).catch(() => { });
198
+ }
199
+ if (!deps.sessionStorage?.beginImportStaging) {
200
+ sendJson(res, 501, { error: "staged session import requires a durable session store" });
201
+ return;
202
+ }
203
+ let body;
204
+ try {
205
+ body = (await readJson(req));
206
+ }
207
+ catch {
208
+ sendJson(res, 400, { error: "invalid JSON body" });
209
+ return;
210
+ }
211
+ if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
212
+ sendJson(res, 400, { error: "entryIds must be an array of strings" });
213
+ return;
214
+ }
215
+ const snapshotsOk = Array.isArray(body.snapshots) && body.snapshots.every((s) => typeof s === "object" && s !== null && typeof s.key === "string" && Array.isArray(s.manifest));
216
+ if (!snapshotsOk || !Array.isArray(body.policy) || !Array.isArray(body.anchors)) {
217
+ sendJson(res, 400, { error: "malformed import metadata" });
218
+ return;
219
+ }
220
+ if (body.resolution !== undefined && body.resolution !== "overwrite-dst") {
221
+ sendJson(res, 400, { error: "resolution must be 'overwrite-dst'" });
222
+ return;
223
+ }
224
+ const resolution = body.resolution;
225
+ const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
226
+ const entryIds = body.entryIds;
227
+ const snapshots = body.snapshots;
228
+ const policy = body.policy;
229
+ const anchors = body.anchors;
230
+ const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
231
+ const relA = classifySyncRelationshipByIds(entryIds, dstIds);
232
+ if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
233
+ sendError(res, 409, "conflict", "sync conflict", { relation: relA });
234
+ return;
235
+ }
236
+ let payloadVerified = false;
237
+ let digestSaysDiffer = false;
238
+ if (relA.relation === "identical" && srcLogDigest !== undefined) {
239
+ const dstEntries = await deps.sessionStorage.exportEntries?.(sessionId).catch(() => null) ?? null;
240
+ const dstDigest = dstEntries === null ? undefined : sessionLogDigest(dstEntries);
241
+ if (sessionLogDigestsComparable(srcLogDigest, dstDigest)) {
242
+ if (srcLogDigest === dstDigest)
243
+ payloadVerified = true;
244
+ else
245
+ digestSaysDiffer = true;
246
+ }
247
+ if (!payloadVerified)
248
+ digestSaysDiffer = true;
249
+ }
250
+ if (relA.relation === "identical" && !digestSaysDiffer) {
251
+ const fss = deps.fileSnapshotStore;
252
+ if (fss?.importManifest && fss.getBlob && snapshots.length > 0) {
253
+ const getBlobA = (h) => fss.getBlob(h);
254
+ for (const snap of snapshots) {
255
+ const r = await fss.importManifest(sessionId, snap.key, new Map(snap.manifest), getBlobA);
256
+ if (!r.ok) {
257
+ sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
258
+ return;
259
+ }
260
+ }
261
+ }
262
+ const opOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
263
+ if (deps.sessionPolicyStore)
264
+ for (const rec of policy)
265
+ await deps.sessionPolicyStore.putRules(sessionId, rec.principal, rec.rules, { operator: opOk });
266
+ const importingP = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
267
+ if (deps.resumeAnchorStore)
268
+ for (const a of anchors)
269
+ await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
270
+ sendJson(res, 200, {
271
+ relation: "identical",
272
+ basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
273
+ payloadVerified,
274
+ });
275
+ return;
276
+ }
277
+ const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
278
+ if (activeTaskId) {
279
+ sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
280
+ return;
281
+ }
282
+ const heldLease = importLeases.get(sessionId);
283
+ if (heldLease) {
284
+ const staleSecRaw = deps.config.syncImportLeaseStaleSec;
285
+ const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
286
+ const staleMs = staleSec * 1000;
287
+ const ageMs = Date.now() - heldLease.at;
288
+ if (staleMs > 0 && ageMs >= staleMs) {
289
+ deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId, ageMs, staleMs, stagingId: heldLease.stagingId });
290
+ cleanupStaging(heldLease.stagingId);
291
+ importLeases.delete(sessionId);
292
+ }
293
+ else {
294
+ const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
295
+ sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
296
+ + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
297
+ return;
298
+ }
299
+ }
300
+ const fss = deps.fileSnapshotStore;
301
+ const canImportSnapshots = typeof fss?.importManifest === "function";
302
+ if (canImportSnapshots) {
303
+ const distinct = new Set();
304
+ for (const snap of snapshots)
305
+ for (const [, hash] of snap.manifest)
306
+ distinct.add(hash);
307
+ const all = [...distinct];
308
+ if (typeof fss?.hasBlobs === "function") {
309
+ const present = await fss.hasBlobs(all);
310
+ const missing = all.find((h) => !present.has(h));
311
+ if (missing !== undefined) {
312
+ sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
313
+ return;
314
+ }
315
+ }
316
+ else {
317
+ for (const hash of all) {
318
+ if (!(await deps.fileSnapshotStore.getBlob(hash))) {
319
+ sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
320
+ return;
321
+ }
322
+ }
323
+ }
324
+ }
325
+ const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
326
+ const operatorOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
327
+ const token = randomBytes(8).toString("hex");
328
+ const handle = deps.sessionStorage.beginImportStaging(sessionId, token);
329
+ const stagingId = handle.stagingId;
330
+ importLeases.set(sessionId, { at: Date.now(), stagingId });
331
+ pendingStagings.set(stagingId, {
332
+ handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
333
+ snapshots, policy, anchors, declaredEntryIds: entryIds, openedBy: scope.gateOwner, openedFleetWide: scope.fleetWide,
334
+ });
335
+ sendJson(res, 200, { stagingId, relation: relA.relation });
336
+ return;
337
+ }
338
+ if (sm[6] !== undefined && req.method === "POST") {
339
+ const stagingId = safeDecode(sm[6]);
340
+ if (stagingId === null) {
341
+ sendJson(res, 400, { error: "invalid staging id" });
342
+ return;
343
+ }
344
+ const pending = pendingStagings.get(stagingId);
345
+ if (!pending || pending.realSessionId !== sessionId) {
346
+ sendJson(res, 404, { error: "staging not found" });
347
+ return;
348
+ }
349
+ if (!pending.openedFleetWide && !scope.fleetWide && pending.openedBy !== scope.gateOwner) {
350
+ sendJson(res, 404, { error: "staging not found" });
351
+ return;
352
+ }
353
+ const getBlob = (h) => deps.fileSnapshotStore.getBlob(h);
354
+ const canImportSnapshots = typeof deps.fileSnapshotStore?.importManifest === "function";
355
+ const validator = new StreamingImportValidator();
356
+ let lineBuf = "";
357
+ let pendingBatch = [];
358
+ const flush = async () => { if (pendingBatch.length) {
359
+ await pending.handle.appendBatch(pendingBatch);
360
+ pendingBatch = [];
361
+ } };
362
+ class PrecheckError extends Error {
363
+ status;
364
+ constructor(status, msg) {
365
+ super(msg);
366
+ this.status = status;
367
+ }
368
+ }
369
+ const streamedIds = new Set();
370
+ const onEntry = (e) => {
371
+ if (!e || typeof e !== "object" || typeof e.id !== "string")
372
+ throw new PrecheckError(422, "malformed entry (missing id)");
373
+ streamedIds.add(e.id);
374
+ validator.step(e);
375
+ };
376
+ const utf8 = new StringDecoder("utf8");
377
+ try {
378
+ for await (const chunk of req) {
379
+ touchImportLease(pending.realSessionId);
380
+ lineBuf += utf8.write(chunk);
381
+ if (Buffer.byteLength(lineBuf, "utf8") > SYNC_NDJSON_LINE_MAX_BYTES)
382
+ throw new PrecheckError(413, "NDJSON line too large");
383
+ let nl;
384
+ while ((nl = lineBuf.indexOf("\n")) >= 0) {
385
+ const line = lineBuf.slice(0, nl).trim();
386
+ lineBuf = lineBuf.slice(nl + 1);
387
+ if (line.length === 0)
388
+ continue;
389
+ let e;
390
+ try {
391
+ e = JSON.parse(line);
392
+ }
393
+ catch {
394
+ throw new PrecheckError(400, "malformed NDJSON line");
395
+ }
396
+ onEntry(e);
397
+ pendingBatch.push(e);
398
+ if (pendingBatch.length >= SYNC_IMPORT_BATCH)
399
+ await flush();
400
+ }
401
+ }
402
+ lineBuf += utf8.end();
403
+ const tail = lineBuf.trim();
404
+ if (tail.length > 0) {
405
+ let e;
406
+ try {
407
+ e = JSON.parse(tail);
408
+ }
409
+ catch {
410
+ throw new PrecheckError(400, "malformed NDJSON line");
411
+ }
412
+ onEntry(e);
413
+ pendingBatch.push(e);
414
+ }
415
+ await flush();
416
+ validator.finish();
417
+ if (streamedIds.size !== pending.declaredEntryIds.length || !pending.declaredEntryIds.every((id) => streamedIds.has(id))) {
418
+ throw new PrecheckError(422, "streamed entries do not match the Phase-A declared entryIds");
419
+ }
420
+ }
421
+ catch (e) {
422
+ if (e instanceof PrecheckError) {
423
+ await pending.handle.abort().catch(() => undefined);
424
+ cleanupStaging(stagingId);
425
+ sendError(res, e.status, "invalid_entries", e.message);
426
+ return;
427
+ }
428
+ if (e instanceof SessionError && e.code === "invalid_session") {
429
+ await pending.handle.abort().catch(() => undefined);
430
+ cleanupStaging(stagingId);
431
+ sendError(res, 422, "invalid_entries", "staged session log failed validation");
432
+ return;
433
+ }
434
+ await pending.handle.abort().catch(() => undefined);
435
+ cleanupStaging(stagingId);
436
+ throw e;
437
+ }
438
+ const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
439
+ if (activeAtCommit) {
440
+ await pending.handle.abort().catch(() => undefined);
441
+ cleanupStaging(stagingId);
442
+ sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
443
+ return;
444
+ }
445
+ try {
446
+ const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
447
+ if (canImportSnapshots) {
448
+ const importManifest = deps.fileSnapshotStore.importManifest.bind(deps.fileSnapshotStore);
449
+ for (const snap of pending.snapshots) {
450
+ const r = await importManifest(sessionId, snap.key, new Map(snap.manifest), getBlob);
451
+ if (!r.ok) {
452
+ sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
453
+ cleanupStaging(stagingId);
454
+ return;
455
+ }
456
+ }
457
+ }
458
+ const policyStore = deps.sessionPolicyStore;
459
+ if (policyStore)
460
+ for (const rec of pending.policy)
461
+ await policyStore.putRules(sessionId, rec.principal, rec.rules, { operator: pending.operatorOk });
462
+ const anchorStore = deps.resumeAnchorStore;
463
+ if (anchorStore)
464
+ for (const a of pending.anchors)
465
+ await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
466
+ deps.sessionStorage.evict?.(sessionId);
467
+ cleanupStaging(stagingId);
468
+ sendJson(res, 200, { relation: committed.relation });
469
+ }
470
+ catch (e) {
471
+ if (e instanceof SyncConflictError) {
472
+ await pending.handle.abort().catch(() => undefined);
473
+ cleanupStaging(stagingId);
474
+ sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
475
+ return;
476
+ }
477
+ if (e instanceof SessionPolicyError) {
478
+ cleanupStaging(stagingId);
479
+ sendError(res, e.code === "loosen_forbidden" ? 403 : 409, e.code, e.message);
480
+ return;
481
+ }
482
+ cleanupStaging(stagingId);
483
+ throw e;
484
+ }
485
+ return;
486
+ }
487
+ if (sm[2] === "plan" && req.method === "POST") {
488
+ if (!pushGateOk())
489
+ return;
490
+ let body;
491
+ try {
492
+ body = (await readJson(req));
493
+ }
494
+ catch {
495
+ sendJson(res, 400, { error: "invalid JSON body" });
496
+ return;
497
+ }
498
+ if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
499
+ sendJson(res, 400, { error: "entryIds must be an array of strings" });
500
+ return;
501
+ }
502
+ const dst = await deps.sessionStorage?.exportEntries?.(sessionId);
503
+ const relation = classifySyncRelationshipByIds(body.entryIds, dst ? dst.map((e) => e.id) : null);
504
+ sendJson(res, 200, relation);
505
+ return;
506
+ }
507
+ sendJson(res, 404, { error: "not found" });
508
+ return;
509
+ }
510
+ }
511
+ miss.fell = true;
512
+ }
513
+ //# sourceMappingURL=session-sync.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 handleSessionsList(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
4
+ //# sourceMappingURL=sessions-list.d.ts.map