@sema-agent/server 1.314.0 → 1.315.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) 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/server.d.ts +95 -69
  16. package/dist/http/server.js +8 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/leader/leader.js +5 -2
  19. package/dist/leader/wire.js +169 -165
  20. package/dist/main.js +462 -451
  21. package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
  22. package/dist/plugins/checkpoint-store-sql.js +224 -0
  23. package/dist/plugins/image-bake-store-sql.d.ts +53 -0
  24. package/dist/plugins/image-bake-store-sql.js +463 -0
  25. package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
  26. package/dist/plugins/k8s-bg-scripts.js +129 -0
  27. package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
  28. package/dist/plugins/k8s-exec-protocol.js +87 -0
  29. package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
  30. package/dist/plugins/pg-checkpoint-store.js +1 -189
  31. package/dist/plugins/pg-cost-quota.js +3 -143
  32. package/dist/plugins/pg-image-bake.d.ts +1 -40
  33. package/dist/plugins/pg-image-bake.js +1 -420
  34. package/dist/plugins/pg-rate-limiter.d.ts +2 -32
  35. package/dist/plugins/pg-rate-limiter.js +4 -147
  36. package/dist/plugins/remote-env-k8s.d.ts +5 -38
  37. package/dist/plugins/remote-env-k8s.js +7 -213
  38. package/dist/plugins/sql-driver.d.ts +25 -0
  39. package/dist/plugins/sql-driver.js +59 -0
  40. package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
  41. package/dist/plugins/tidb-checkpoint-store.js +1 -191
  42. package/dist/plugins/tidb-image-bake.d.ts +1 -38
  43. package/dist/plugins/tidb-image-bake.js +1 -348
  44. package/dist/plugins/write-behind-counter.d.ts +14 -3
  45. package/dist/plugins/write-behind-counter.js +39 -12
  46. package/dist/principal-jwt.d.ts +44 -0
  47. package/dist/principal-jwt.js +95 -0
  48. package/dist/security.d.ts +2 -58
  49. package/dist/security.js +3 -118
  50. package/dist/sema-registry.d.ts +5 -200
  51. package/dist/sema-registry.js +4 -567
  52. package/package.json +1 -1
@@ -18,133 +18,8 @@ const K8S_BG_MAX_TIMEOUT_SEC = Math.max(K8S_BG_DEFAULT_TIMEOUT_SEC, Math.floor(N
18
18
  const K8S_BG_READ_CAP = Math.max(64 * 1024, Math.floor(Number(process.env.K8S_BG_READ_CAP ?? "")) || 1024 * 1024);
19
19
  const K8S_BG_FILE_CAP = Math.max(K8S_BG_READ_CAP, Math.floor(Number(process.env.K8S_BG_FILE_CAP ?? "")) || 64 * 1024 * 1024);
20
20
  const K8S_BG_MEM_CAP = Math.max(64 * 1024, Math.floor(Number(process.env.K8S_BG_MEM_CAP ?? "")) || 8 * 1024 * 1024);
21
- export function buildBgRunnerScript(dir) {
22
- const q = (name) => shellQuote(`${dir}/${name}`);
23
- return [
24
- `echo $$ > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`,
25
- `sh ${q("cmd.sh")} > ${q("out")} 2> ${q("err")} < /dev/null`,
26
- `__RC=$?`,
27
- `echo $__RC > ${q("exit.tmp")} 2>/dev/null && mv ${q("exit.tmp")} ${q("exit")} 2>/dev/null`,
28
- ].join("\n");
29
- }
30
- export function buildBgLauncherScript(dir, cmdShB64, runnerShB64) {
31
- const d = shellQuote(dir);
32
- const q = (name) => shellQuote(`${dir}/${name}`);
33
- return [
34
- `mkdir -p ${d} && chmod 700 ${d}`,
35
- `printf %s '${cmdShB64}' | base64 -d > ${q("cmd.sh")}`,
36
- `printf %s '${runnerShB64}' | base64 -d > ${q("runner.sh")}`,
37
- `setsid sh ${q("runner.sh")} >/dev/null 2>&1 &`,
38
- `__i=0; while [ ! -s ${q("pgid")} ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
39
- `if [ -s ${q("pgid")} ]; then echo SEMABG_LAUNCHED; else echo SEMABG_NOPGID; fi`,
40
- ].join("\n");
41
- }
42
- export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
43
- const d = shellQuote(dir);
44
- const slice = (file, cursor) => [
45
- `__N=$((__T - ${cursor}))`,
46
- `if [ "$__N" -gt 0 ]; then`,
47
- ` if [ "$__N" -le ${readCap} ]; then tail -c +$(( ${cursor} + 1 )) ${d}/${file} 2>/dev/null | head -c "$__N" | base64;`,
48
- ` else tail -c +$(( __T - ${readCap} + 1 )) ${d}/${file} 2>/dev/null | head -c ${readCap} | base64; fi`,
49
- `fi`,
50
- ].join("\n");
51
- return [
52
- `if [ ! -d ${d} ]; then echo SEMABG_GONE; exit 0; fi`,
53
- `__OT=$(wc -c < ${d}/out 2>/dev/null); __OT=$(( \${__OT:-0} + 0 ))`,
54
- `__ET=$(wc -c < ${d}/err 2>/dev/null); __ET=$(( \${__ET:-0} + 0 ))`,
55
- `__EX=$( [ -f ${d}/exit ] && cat ${d}/exit 2>/dev/null || echo - )`,
56
- `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
57
- `if [ "$__PG" = "-" ]; then __AL=1; elif kill -0 -"$__PG" 2>/dev/null; then __AL=1; else __AL=0; fi`,
58
- `echo "SEMABG_META o=$__OT e=$__ET x=$__EX a=$__AL"`,
59
- `echo SEMABG_O`,
60
- `__T=$__OT`,
61
- slice("out", stdoutCursor),
62
- `echo SEMABG_E`,
63
- `__T=$__ET`,
64
- slice("err", stderrCursor),
65
- `echo SEMABG_X`,
66
- ].join("\n");
67
- }
68
- export function buildBgKillScript(dir) {
69
- const d = shellQuote(dir);
70
- return [
71
- `__i=0; while [ ! -s ${d}/pgid ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
72
- `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
73
- `if [ "$__PG" != "-" ]; then kill -TERM -"$__PG" 2>/dev/null || kill -TERM "$__PG" 2>/dev/null; sleep 1; kill -KILL -"$__PG" 2>/dev/null || kill -KILL "$__PG" 2>/dev/null; fi`,
74
- `echo SEMABG_KILLED`,
75
- ].join("\n");
76
- }
77
- export function buildBgDisposeScript(dir) {
78
- const d = shellQuote(dir);
79
- return [
80
- `__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
81
- `if [ "$__PG" != "-" ]; then kill -KILL -"$__PG" 2>/dev/null || kill -KILL "$__PG" 2>/dev/null; fi`,
82
- `rm -rf ${d} 2>/dev/null`,
83
- `echo SEMABG_DISPOSED`,
84
- ].join("\n");
85
- }
86
- export function buildDetachCapableExec(dir, command) {
87
- const d = shellQuote(dir);
88
- const q = (name) => shellQuote(`${dir}/${name}`);
89
- const cmdB64 = Buffer.from(command, "utf8").toString("base64");
90
- const writePgid = (val) => `printf %s ${val} > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`;
91
- return [
92
- `__cmd=$(printf %s '${cmdB64}' | base64 -d)`,
93
- `mkdir -p ${d} 2>/dev/null`,
94
- `trap 'rm -rf ${d} 2>/dev/null' EXIT`,
95
- `if command -v setsid >/dev/null 2>&1; then`,
96
- ` setsid sh -c "$__cmd" &`,
97
- ` __pg=$!`,
98
- ` ${writePgid('"$__pg"')}`,
99
- ` wait "$__pg"; __rc=$?`,
100
- `else`,
101
- ` ${writePgid('"$$"')}`,
102
- ` sh -c "$__cmd"; __rc=$?`,
103
- `fi`,
104
- `exit $__rc`,
105
- ].join("\n");
106
- }
107
- export function parseBgPollOutput(raw) {
108
- if (raw.includes("SEMABG_GONE")) {
109
- return { gone: true, outTotal: 0, errTotal: 0, alive: false, stdout: "", stdoutBytes: 0, stderr: "", stderrBytes: 0 };
110
- }
111
- const lines = raw.split("\n");
112
- const metaIdx = lines.findIndex((l) => l.startsWith("SEMABG_META "));
113
- if (metaIdx < 0)
114
- return undefined;
115
- const oIdx = lines.indexOf("SEMABG_O", metaIdx + 1);
116
- const eIdx = oIdx >= 0 ? lines.indexOf("SEMABG_E", oIdx + 1) : -1;
117
- const xIdx = eIdx >= 0 ? lines.indexOf("SEMABG_X", eIdx + 1) : -1;
118
- if (oIdx < 0 || eIdx < 0 || xIdx < 0)
119
- return undefined;
120
- const m = /o=(\d+) e=(\d+) x=(\S+) a=([01])/.exec(lines[metaIdx].slice("SEMABG_META ".length));
121
- if (!m)
122
- return undefined;
123
- const outTotal = parseInt(m[1], 10);
124
- const errTotal = parseInt(m[2], 10);
125
- const exTok = m[3];
126
- const alive = m[4] === "1";
127
- const exitParsed = exTok === "-" ? NaN : parseInt(exTok, 10);
128
- const exitCode = Number.isFinite(exitParsed) ? exitParsed : undefined;
129
- const decode = (from, to) => {
130
- const b64 = lines.slice(from, to).join("").replace(/\s+/g, "");
131
- const buf = Buffer.from(b64, "base64");
132
- return { text: buf.toString("utf8"), bytes: buf.length };
133
- };
134
- const o = decode(oIdx + 1, eIdx);
135
- const e = decode(eIdx + 1, xIdx);
136
- return {
137
- gone: false,
138
- outTotal,
139
- errTotal,
140
- ...(exitCode !== undefined ? { exitCode } : {}),
141
- alive,
142
- stdout: o.text,
143
- stdoutBytes: o.bytes,
144
- stderr: e.text,
145
- stderrBytes: e.bytes,
146
- };
147
- }
21
+ import { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
22
+ export { buildBgRunnerScript, buildBgLauncherScript, buildBgPollScript, buildBgKillScript, buildBgDisposeScript, buildDetachCapableExec, parseBgPollOutput, } from "./k8s-bg-scripts.js";
148
23
  const ok = (value) => ({ ok: true, value });
149
24
  const unsupported = (op) => ({
150
25
  ok: false,
@@ -153,98 +28,17 @@ const unsupported = (op) => ({
153
28
  const SA_DIR = "/var/run/secrets/kubernetes.io/serviceaccount";
154
29
  const EXEC_SUBPROTOCOLS = ["v5.channel.k8s.io", "v4.channel.k8s.io"];
155
30
  const WRITE_CHUNK_BYTES = 64 * 1024;
156
- export function applyPodSpecPatch(pod, patch) {
157
- assertHardeningOnly(patch.podSecurityContext, "podSecurityContext");
158
- assertHardeningOnly(patch.containerSecurityContext, "containerSecurityContext");
159
- if (patch.labels) {
160
- const identity = {};
161
- for (const k of ["app", "managed-by"]) {
162
- const v = pod.metadata.labels?.[k];
163
- if (v !== undefined)
164
- identity[k] = v;
165
- }
166
- pod.metadata.labels = { ...pod.metadata.labels, ...patch.labels, ...identity };
167
- }
168
- const spec = pod.spec;
169
- assertVolumesPodLocal(patch.volumes, "volumes");
170
- if (patch.volumes?.length)
171
- spec.volumes = [...(spec.volumes ?? []), ...patch.volumes];
172
- if (patch.podSecurityContext)
173
- spec.securityContext = { ...(spec.securityContext ?? {}), ...patch.podSecurityContext };
174
- const container = spec.containers?.[0];
175
- if (container) {
176
- if (patch.volumeMounts?.length)
177
- container.volumeMounts = [...(container.volumeMounts ?? []), ...patch.volumeMounts];
178
- if (patch.containerSecurityContext)
179
- container.securityContext = { ...(container.securityContext ?? {}), ...patch.containerSecurityContext };
180
- }
181
- }
182
- function assertHardeningOnly(sc, where) {
183
- if (!sc)
184
- return;
185
- const bad = (msg) => {
186
- throw new Error(`podSpecPatch.${where} rejected — it may only HARDEN, never weaken isolation: ${msg}`);
187
- };
188
- if (sc.privileged === true)
189
- bad("privileged:true");
190
- if (sc.allowPrivilegeEscalation === true)
191
- bad("allowPrivilegeEscalation:true");
192
- if (sc.readOnlyRootFilesystem === false)
193
- bad("readOnlyRootFilesystem:false");
194
- if (sc.runAsNonRoot === false)
195
- bad("runAsNonRoot:false");
196
- if (sc.runAsUser === 0)
197
- bad("runAsUser:0 (root)");
198
- const caps = sc.capabilities;
199
- if (caps && Array.isArray(caps.add) && caps.add.length > 0)
200
- bad(`capabilities.add ${JSON.stringify(caps.add)} (a hardening patch may only .drop)`);
201
- if (sc.procMount === "Unmasked")
202
- bad('procMount:"Unmasked" (re-exposes masked /proc)');
203
- const seccomp = sc.seccompProfile;
204
- if (seccomp && seccomp.type === "Unconfined")
205
- bad('seccompProfile.type:"Unconfined" (disables syscall filtering)');
206
- }
207
- function assertVolumesPodLocal(volumes, where) {
208
- if (!volumes?.length)
209
- return;
210
- const POD_LOCAL = new Set(["emptyDir", "configMap", "secret", "projected", "downwardAPI", "ephemeral"]);
211
- for (const v of volumes) {
212
- const vol = v;
213
- const sources = Object.keys(vol).filter((k) => k !== "name");
214
- for (const src of sources) {
215
- if (!POD_LOCAL.has(src)) {
216
- throw new Error(`podSpecPatch.${where} rejected — volume source ${JSON.stringify(src)} is not pod-local (host-backed sources like hostPath/csi/flexVolume cross the Kata VM boundary into the node FS). Allowed: ${[...POD_LOCAL].join("/")}`);
217
- }
218
- }
219
- }
220
- }
221
- export function execSocketTlsOptions(ca, insecureTls) {
222
- const caList = ca ? [ca] : undefined;
223
- return { ca: caList, rejectUnauthorized: !insecureTls, tls: { ca: caList, rejectUnauthorized: !insecureTls } };
224
- }
31
+ import { applyPodSpecPatch } from "./k8s-exec-protocol.js";
32
+ export { applyPodSpecPatch } from "./k8s-exec-protocol.js";
33
+ import { execSocketTlsOptions, exitCodeFromStatus } from "./k8s-exec-protocol.js";
34
+ export { execSocketTlsOptions } from "./k8s-exec-protocol.js";
225
35
  export function errorMessageOf(e) {
226
36
  if (e instanceof Error)
227
37
  return e.message;
228
38
  const m = e?.message;
229
39
  return typeof m === "string" && m ? m : String(e);
230
40
  }
231
- export function exitCodeFromStatus(statusJson) {
232
- try {
233
- const s = JSON.parse(statusJson);
234
- if (s.status === "Success")
235
- return { exitCode: 0 };
236
- const cause = s.details?.causes?.find((c) => c.reason === "ExitCode");
237
- if (cause?.message?.trim()) {
238
- const code = Number(cause.message);
239
- if (Number.isFinite(code))
240
- return { exitCode: code };
241
- }
242
- return { error: s.message || s.reason || "exec failed without an exit code" };
243
- }
244
- catch {
245
- return { error: `unparseable exec status: ${statusJson.slice(0, 200)}` };
246
- }
247
- }
41
+ export { exitCodeFromStatus } from "./k8s-exec-protocol.js";
248
42
  export class RemoteK8sExecutionEnv {
249
43
  capabilities;
250
44
  cwd;
@@ -0,0 +1,25 @@
1
+ import type { Pool as MySqlPool } from "mysql2/promise";
2
+ import type { Pool as PgPool } from "pg";
3
+ export type SqlDialect = "tidb" | "pg";
4
+ export type SqlRow = Record<string, unknown>;
5
+ export interface SqlResult {
6
+ rows: SqlRow[];
7
+ affected: number;
8
+ }
9
+ export interface SqlExec {
10
+ query(sql: string, params?: unknown[]): Promise<SqlResult>;
11
+ }
12
+ export interface SqlTxConn extends SqlExec {
13
+ begin(): Promise<void>;
14
+ beginPessimistic(): Promise<void>;
15
+ commit(): Promise<void>;
16
+ rollback(): Promise<void>;
17
+ release(): void;
18
+ }
19
+ export interface SqlDriver extends SqlExec {
20
+ readonly dialect: SqlDialect;
21
+ connect(): Promise<SqlTxConn>;
22
+ }
23
+ export declare function mysqlDriver(pool: MySqlPool): SqlDriver;
24
+ export declare function pgDriver(pool: PgPool): SqlDriver;
25
+ //# sourceMappingURL=sql-driver.d.ts.map
@@ -0,0 +1,59 @@
1
+ function mysqlResult(r) {
2
+ if (Array.isArray(r))
3
+ return { rows: r, affected: 0 };
4
+ const h = r;
5
+ return { rows: [], affected: Number(h?.affectedRows ?? 0) };
6
+ }
7
+ async function mysqlQuery(q, sql, params) {
8
+ const [r] = (await q.query(sql, params ?? []));
9
+ return mysqlResult(r);
10
+ }
11
+ export function mysqlDriver(pool) {
12
+ return {
13
+ dialect: "tidb",
14
+ query: (sql, params) => mysqlQuery(pool, sql, params),
15
+ async connect() {
16
+ const c = await pool.getConnection();
17
+ return {
18
+ query: (sql, params) => mysqlQuery(c, sql, params),
19
+ begin: () => c.beginTransaction(),
20
+ beginPessimistic: async () => {
21
+ await c.query("BEGIN PESSIMISTIC");
22
+ },
23
+ commit: () => c.commit(),
24
+ rollback: () => c.rollback(),
25
+ release: () => c.release(),
26
+ };
27
+ },
28
+ };
29
+ }
30
+ async function pgQuery(q, sql, params) {
31
+ const res = await q.query(sql, params ?? []);
32
+ return { rows: (res.rows ?? []), affected: res.rowCount ?? 0 };
33
+ }
34
+ export function pgDriver(pool) {
35
+ return {
36
+ dialect: "pg",
37
+ query: (sql, params) => pgQuery(pool, sql, params),
38
+ async connect() {
39
+ const c = await pool.connect();
40
+ return {
41
+ query: (sql, params) => pgQuery(c, sql, params),
42
+ begin: async () => {
43
+ await c.query("BEGIN");
44
+ },
45
+ beginPessimistic: async () => {
46
+ await c.query("BEGIN");
47
+ },
48
+ commit: async () => {
49
+ await c.query("COMMIT");
50
+ },
51
+ rollback: async () => {
52
+ await c.query("ROLLBACK");
53
+ },
54
+ release: () => c.release(),
55
+ };
56
+ },
57
+ };
58
+ }
59
+ //# sourceMappingURL=sql-driver.js.map
@@ -1,50 +1,2 @@
1
- import type { Pool } from "mysql2/promise";
2
- import { type Checkpoint, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
3
- export declare const TERMINAL_BACKSTOP_MS: number;
4
- export declare const TERMINAL_GRACE_MS = 3600000;
5
- export declare function tokenFingerprint(token: string): string;
6
- export interface PendingCheckpoint {
7
- sessionId: string;
8
- scope: string;
9
- toolName: string | null;
10
- toolCallId: string | null;
11
- boundCallId: string | null;
12
- boundInputHash: string | null;
13
- taskId: string | null;
14
- input: unknown;
15
- createdAt: number;
16
- deadline: number | null;
17
- riskDescriptor: RiskDescriptor | null;
18
- }
19
- export declare function boundedToolInput(args: unknown): unknown;
20
- export declare class TiDBCheckpointStore implements CheckpointStore {
21
- private readonly pool;
22
- private readonly logger?;
23
- constructor(pool: Pool, logger?: {
24
- info?(msg: string, meta?: unknown): void;
25
- } | undefined);
26
- put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
27
- get(token: CheckpointToken): Promise<Checkpoint | null>;
28
- resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
29
- reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
30
- setPendingSteer(token: CheckpointToken, scope: string, steer: {
31
- text: string;
32
- trusted: boolean;
33
- }): Promise<boolean>;
34
- expire(token: CheckpointToken, scope: string): Promise<boolean>;
35
- reap(scope: string, cutoff: number): Promise<number>;
36
- reapExpired(cutoff: number): Promise<number>;
37
- listExpiredApprovalGates(cutoff: number, limit?: number): Promise<Array<{
38
- sessionId: string;
39
- scope: string;
40
- }>>;
41
- listPending(scope?: string): Promise<PendingCheckpoint[]>;
42
- listByScope(scope: string): Promise<CheckpointSummary[]>;
43
- findPendingTokenBySession(sessionId: string, scope?: string): Promise<CheckpointToken | null>;
44
- peekPendingScope(sessionId: string): Promise<string | null | undefined>;
45
- putCtx(sessionId: string, ctx: unknown): Promise<void>;
46
- getCtx<T = unknown>(sessionId: string): Promise<T | null>;
47
- reapCtx(cutoff: number): Promise<number>;
48
- deleteBySession(sessionId: string, owner: string | null): Promise<number>;
49
- }
1
+ export { TiDBCheckpointStore, SqlCheckpointStore, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS, tokenFingerprint, boundedToolInput, type PendingCheckpoint, } from "./checkpoint-store-sql.js";
50
2
  //# sourceMappingURL=tidb-checkpoint-store.d.ts.map
@@ -1,192 +1,2 @@
1
- import { createHash } from "node:crypto";
2
- import { CheckpointError, validatePendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION } from "@sema-agent/core";
3
- import { redactDeep } from "../trace/redact.js";
4
- import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
5
- const MAX_TOOL_INPUT_CHARS = 8192;
6
- export const TERMINAL_BACKSTOP_MS = Math.max(60_000, Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) || 30 * 86_400_000);
7
- export const TERMINAL_GRACE_MS = 3_600_000;
8
- export function tokenFingerprint(token) {
9
- return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
10
- }
11
- export function boundedToolInput(args) {
12
- if (args === undefined)
13
- return null;
14
- const redacted = redactDeep(args);
15
- const json = JSON.stringify(redacted);
16
- if (json === undefined)
17
- return null;
18
- if (json.length > MAX_TOOL_INPUT_CHARS)
19
- return { truncated: true, bytes: json.length };
20
- return redacted;
21
- }
22
- const DUP_ENTRY = 1062;
23
- export class TiDBCheckpointStore {
24
- pool;
25
- logger;
26
- constructor(pool, logger) {
27
- this.pool = pool;
28
- this.logger = logger;
29
- }
30
- async put(token, cp) {
31
- const pa = cp.pendingAction;
32
- const version = checkpointVersionOf(cp);
33
- const toolInput = boundedToolInput(pa?.args);
34
- try {
35
- await this.pool.query("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at, terminal_at, gate_kind, bound_input_hash, risk_descriptor) " +
36
- "VALUES (?,?,?,?,'pending',?,?,?,?,?,?,?,?,?,?)", [
37
- token,
38
- cp.scope,
39
- cp.sessionId,
40
- version,
41
- pa?.toolName ?? null,
42
- pa?.toolCallId ?? null,
43
- toolInput === null ? null : JSON.stringify(toolInput),
44
- JSON.stringify(cp),
45
- cp.deadline ?? null,
46
- cp.createdAt,
47
- Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS),
48
- cp.gate?.kind ?? null,
49
- pa?.boundInputHash ?? null,
50
- ((g) => (g?.riskDescriptor ? JSON.stringify(g.riskDescriptor) : null))(cp.gate),
51
- ]);
52
- }
53
- catch (e) {
54
- if (e?.errno === DUP_ENTRY) {
55
- throw new CheckpointError("checkpoint.already_exists", `checkpoint token already exists`);
56
- }
57
- throw e;
58
- }
59
- }
60
- async get(token) {
61
- const [rows] = await this.pool.query("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer FROM checkpoint WHERE token = ?", [token]);
62
- const r = rows[0];
63
- if (!r)
64
- return null;
65
- const version = Number(r.version);
66
- if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
67
- throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
68
- }
69
- const cp = parseJson(r.checkpoint);
70
- cp.status = r.status;
71
- const outcomeRaw = parseJson(r.outcome);
72
- if (outcomeRaw)
73
- cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
74
- cp.reopenReason = r.reopen_reason ?? undefined;
75
- cp.rev = r.rev == null ? 0 : Number(r.rev);
76
- const steerRaw = parseJson(r.pending_steer);
77
- if (steerRaw && cp.state)
78
- cp.state.pendingSteer = steerRaw;
79
- this.logger?.info?.("checkpoint_get", {
80
- tokenFp: tokenFingerprint(token),
81
- status: cp.status,
82
- version,
83
- hasWorkspaceHandle: !!cp.state?.workspaceHandle,
84
- snapshotId: cp.state?.workspaceHandle?.snapshotId,
85
- });
86
- return cp;
87
- }
88
- async resolve(token, scope, outcome, expect) {
89
- const params = [JSON.stringify(outcome), Date.now(), token, scope];
90
- if (expect)
91
- params.push(expect.rev);
92
- const [res] = await this.pool.query("UPDATE checkpoint SET status = 'resolved', outcome = ?, decided_at = ?, rev = rev + 1, reopen_reason = NULL WHERE token = ? AND scope = ? AND status = 'pending'" +
93
- (expect ? " AND rev = ?" : ""), params);
94
- return res.affectedRows === 1;
95
- }
96
- async reopen(token, scope, reason) {
97
- const [res] = await this.pool.query("UPDATE checkpoint SET status = 'pending', reopen_reason = ?, rev = rev + 1 WHERE token = ? AND scope = ? AND status = 'resolved'", [reason, token, scope]);
98
- return res.affectedRows === 1;
99
- }
100
- async setPendingSteer(token, scope, steer) {
101
- const clean = validatePendingSteer(steer);
102
- const [res] = await this.pool.query("UPDATE checkpoint SET pending_steer = ? WHERE token = ? AND scope = ? AND status = 'pending'", [JSON.stringify(clean), token, scope]);
103
- return res.affectedRows > 0;
104
- }
105
- async expire(token, scope) {
106
- const [res] = await this.pool.query("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE token = ? AND scope = ? AND status = 'pending'", [Date.now(), token, scope]);
107
- return res.affectedRows === 1;
108
- }
109
- async reap(scope, cutoff) {
110
- const [res] = await this.pool.query("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE scope = ? AND status = 'pending' AND deadline IS NOT NULL AND deadline <= ?", [Date.now(), scope, cutoff]);
111
- return res.affectedRows;
112
- }
113
- async reapExpired(cutoff) {
114
- const [res] = await this.pool.query("UPDATE checkpoint SET status = 'expired', decided_at = ? " +
115
- "WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= ?))", [Date.now(), cutoff, cutoff]);
116
- return res.affectedRows;
117
- }
118
- async listExpiredApprovalGates(cutoff, limit = 100) {
119
- const [rows] = await this.pool.query("SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
120
- "AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= ? ORDER BY deadline ASC LIMIT ?", [cutoff, limit]);
121
- return rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
122
- }
123
- async listPending(scope) {
124
- const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, ta.task_id " +
125
- "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
126
- const [rows] = scope
127
- ? await this.pool.query(`${base} AND c.scope=? ORDER BY c.created_at ASC`, [scope])
128
- : await this.pool.query(`${base} ORDER BY c.created_at ASC`);
129
- const out = rows.map((r) => {
130
- const toolCallId = r.tool_call_id ?? null;
131
- return {
132
- sessionId: String(r.session_id),
133
- scope: String(r.scope),
134
- toolName: r.tool_name ?? null,
135
- toolCallId,
136
- boundCallId: toolCallId,
137
- boundInputHash: r.bound_input_hash ?? null,
138
- taskId: r.task_id ?? null,
139
- input: r.tool_input ?? null,
140
- createdAt: Number(r.created_at),
141
- deadline: r.deadline == null ? null : Number(r.deadline),
142
- riskDescriptor: parseJson(r.risk_descriptor),
143
- };
144
- });
145
- return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
146
- }
147
- async listByScope(scope) {
148
- const [rows] = await this.pool.query("SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at ASC LIMIT 500", [scope]);
149
- const out = [];
150
- for (const r of rows) {
151
- const cp = parseJson(r.checkpoint);
152
- if (!cp)
153
- continue;
154
- cp.status = r.status;
155
- out.push(summarizeCheckpoint(cp));
156
- }
157
- return out;
158
- }
159
- async findPendingTokenBySession(sessionId, scope) {
160
- const [rows] = scope
161
- ? await this.pool.query("SELECT token FROM checkpoint WHERE session_id=? AND scope=? AND status='pending' LIMIT 1", [sessionId, scope])
162
- : await this.pool.query("SELECT token FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", [sessionId]);
163
- return rows[0] ? String(rows[0].token) : null;
164
- }
165
- async peekPendingScope(sessionId) {
166
- const [rows] = await this.pool.query("SELECT scope FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", [sessionId]);
167
- if (!rows[0])
168
- return undefined;
169
- return rows[0].scope ?? null;
170
- }
171
- async putCtx(sessionId, ctx) {
172
- await this.pool.query("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES (?,?,?) " +
173
- "ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at = VALUES(updated_at)", [sessionId, JSON.stringify(ctx), Date.now()]);
174
- }
175
- async getCtx(sessionId) {
176
- const [rows] = await this.pool.query("SELECT ctx FROM checkpoint_ctx WHERE session_id=?", [sessionId]);
177
- return rows[0] ? parseJson(rows[0].ctx) : null;
178
- }
179
- async reapCtx(cutoff) {
180
- const [res] = await this.pool.query("DELETE FROM checkpoint_ctx WHERE updated_at < ? " +
181
- "AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
182
- "AND session_id NOT IN (SELECT session_id FROM task_active)", [cutoff]);
183
- return res.affectedRows;
184
- }
185
- async deleteBySession(sessionId, owner) {
186
- const ownerGuard = "EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = ? AND sm.owner <=> ?)";
187
- const [res] = await this.pool.query(`DELETE FROM checkpoint WHERE session_id = ? AND ${ownerGuard}`, [sessionId, sessionId, owner]);
188
- await this.pool.query(`DELETE FROM checkpoint_ctx WHERE session_id = ? AND ${ownerGuard}`, [sessionId, sessionId, owner]);
189
- return res.affectedRows;
190
- }
191
- }
1
+ export { TiDBCheckpointStore, SqlCheckpointStore, TERMINAL_BACKSTOP_MS, TERMINAL_GRACE_MS, tokenFingerprint, boundedToolInput, } from "./checkpoint-store-sql.js";
192
2
  //# sourceMappingURL=tidb-checkpoint-store.js.map
@@ -1,41 +1,4 @@
1
- import type { Pool } from "mysql2/promise";
2
- import { type BakeStatus, type BakeState, type BakeRecord, type BakeEvent, type CreateBakeInput, type BakeTerminal } from "./store-contracts.js";
1
+ export { TiDBImageBake, SqlImageBake } from "./image-bake-store-sql.js";
3
2
  export type { BakeStatus, BakeState, BakeErrorCode, BakeRecord, BakeEvent, CreateBakeInput, BakeTerminal, BakeRow, } from "./store-contracts.js";
4
3
  export { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS } from "./store-contracts.js";
5
- export declare class TiDBImageBake {
6
- private readonly pool;
7
- private readonly poolName;
8
- constructor(pool: Pool, poolName?: string);
9
- createBake(input: CreateBakeInput): Promise<{
10
- created: boolean;
11
- bake: BakeRecord;
12
- }>;
13
- createBakeIfIdle(input: CreateBakeInput): Promise<{
14
- created: boolean;
15
- bake: BakeRecord | null;
16
- }>;
17
- getBake(bakeId: string): Promise<BakeRecord | null>;
18
- getBakeByIdem(idemKey: string): Promise<BakeRecord | null>;
19
- findActiveByArgv(argv: string[]): Promise<BakeRecord | null>;
20
- findActiveAny(): Promise<BakeRecord | null>;
21
- findNextQueuedId(): Promise<string | null>;
22
- claimBake(bakeId: string, runnerId: string, leaseMs: number): Promise<BakeRecord | null>;
23
- appendEvent(bakeId: string, kind: string, data: unknown, opts?: {
24
- lineOrd?: number | null;
25
- level?: string | null;
26
- }): Promise<number | null>;
27
- private lineOrdExists;
28
- maxSeq(bakeId: string): Promise<number>;
29
- retainedFrom(bakeId: string): Promise<number>;
30
- getEvents(bakeId: string, afterSeq: number): Promise<BakeEvent[]>;
31
- setStatus(bakeId: string, status: BakeStatus): Promise<void>;
32
- setState(bakeId: string, state: BakeState): Promise<void>;
33
- setTerminal(bakeId: string, t: BakeTerminal): Promise<number>;
34
- setIndexId(bakeId: string, indexId: string): Promise<boolean>;
35
- heartbeatLease(bakeId: string, runnerId: string, leaseMs: number): Promise<boolean>;
36
- requestCancel(bakeId: string): Promise<boolean>;
37
- isCancelRequested(bakeId: string): Promise<boolean>;
38
- reapStaleBakes(olderThanMs: number): Promise<number>;
39
- private tx;
40
- }
41
4
  //# sourceMappingURL=tidb-image-bake.d.ts.map