@sema-agent/server 1.313.0 → 1.315.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-hmac.d.ts +17 -0
- package/dist/approval-hmac.js +27 -0
- package/dist/auth-bridge.d.ts +3 -2
- package/dist/bench/s1/live-deps.js +2 -2
- package/dist/budget.d.ts +4 -76
- package/dist/budget.js +4 -83
- package/dist/capabilities/sandbox-file-send.d.ts +2 -1
- package/dist/config-center/apply-effective.d.ts +22 -0
- package/dist/config-center/apply-effective.js +283 -0
- package/dist/config-center/http-client.d.ts +23 -0
- package/dist/config-center/http-client.js +109 -0
- package/dist/config-center/restart-signal.d.ts +12 -0
- package/dist/config-center/restart-signal.js +70 -0
- package/dist/config-center/skills-mcp.d.ts +13 -0
- package/dist/config-center/skills-mcp.js +113 -0
- package/dist/config-center/types.d.ts +143 -0
- package/dist/config-center/types.js +2 -0
- package/dist/config-types.d.ts +1 -1
- package/dist/elicitation.d.ts +5 -3
- package/dist/elicitation.js +3 -2
- package/dist/fleet/fleet-bus.js +92 -83
- package/dist/fleet-lease.d.ts +1 -1
- package/dist/fleet-lease.js +1 -1
- package/dist/hooks/hook-llm.d.ts +5 -4
- package/dist/hooks/hook-runner.d.ts +1 -1
- package/dist/hooks/hook-runner.js +19 -10
- package/dist/http/server.d.ts +95 -69
- package/dist/http/server.js +75 -86
- package/dist/index.d.ts +1 -1
- package/dist/key-resolver.d.ts +1 -1
- package/dist/key-resolver.js +1 -1
- package/dist/leader/grader-env-factory.d.ts +1 -1
- package/dist/leader/grader-env-factory.js +2 -2
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.d.ts +1 -1
- package/dist/leader/wire.js +170 -166
- package/dist/main.js +487 -476
- package/dist/memory-scope.d.ts +18 -0
- package/dist/memory-scope.js +38 -0
- package/dist/observability/cost-taxonomy.d.ts +34 -0
- package/dist/observability/cost-taxonomy.js +26 -0
- package/dist/observability/prompt-manifest.d.ts +75 -0
- package/dist/observability/prompt-manifest.js +82 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +67 -0
- package/dist/plugins/checkpoint-store-sql.js +224 -0
- package/dist/plugins/image-bake-store-sql.d.ts +53 -0
- package/dist/plugins/image-bake-store-sql.js +463 -0
- package/dist/plugins/k8s-bg-scripts.d.ts +19 -0
- package/dist/plugins/k8s-bg-scripts.js +129 -0
- package/dist/plugins/k8s-exec-protocol.d.ts +20 -0
- package/dist/plugins/k8s-exec-protocol.js +87 -0
- package/dist/plugins/pg-checkpoint-store.d.ts +1 -33
- package/dist/plugins/pg-checkpoint-store.js +1 -189
- package/dist/plugins/pg-cost-quota.js +3 -143
- package/dist/plugins/pg-image-bake.d.ts +1 -40
- package/dist/plugins/pg-image-bake.js +1 -420
- package/dist/plugins/pg-rate-limiter.d.ts +2 -32
- package/dist/plugins/pg-rate-limiter.js +4 -147
- package/dist/plugins/remote-env-k8s.d.ts +5 -38
- package/dist/plugins/remote-env-k8s.js +7 -213
- package/dist/plugins/sql-driver.d.ts +25 -0
- package/dist/plugins/sql-driver.js +59 -0
- package/dist/plugins/tidb-checkpoint-store.d.ts +1 -49
- package/dist/plugins/tidb-checkpoint-store.js +1 -191
- package/dist/plugins/tidb-image-bake.d.ts +1 -38
- package/dist/plugins/tidb-image-bake.js +1 -348
- package/dist/plugins/web-search.d.ts +4 -3
- package/dist/plugins/write-behind-counter.d.ts +14 -3
- package/dist/plugins/write-behind-counter.js +39 -12
- package/dist/principal-jwt.d.ts +44 -0
- package/dist/principal-jwt.js +95 -0
- package/dist/question.d.ts +2 -1
- package/dist/question.js +3 -2
- package/dist/run-local.js +2 -2
- package/dist/runtime-caps-resolver.d.ts +7 -7
- package/dist/runtime-caps-resolver.js +3 -3
- package/dist/security.d.ts +4 -74
- package/dist/security.js +6 -155
- package/dist/sema-registry.d.ts +5 -200
- package/dist/sema-registry.js +4 -567
- package/dist/tool-approval.d.ts +2 -1
- package/dist/tool-approval.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function applyPodSpecPatch(pod, patch) {
|
|
2
|
+
assertHardeningOnly(patch.podSecurityContext, "podSecurityContext");
|
|
3
|
+
assertHardeningOnly(patch.containerSecurityContext, "containerSecurityContext");
|
|
4
|
+
if (patch.labels) {
|
|
5
|
+
const identity = {};
|
|
6
|
+
for (const k of ["app", "managed-by"]) {
|
|
7
|
+
const v = pod.metadata.labels?.[k];
|
|
8
|
+
if (v !== undefined)
|
|
9
|
+
identity[k] = v;
|
|
10
|
+
}
|
|
11
|
+
pod.metadata.labels = { ...pod.metadata.labels, ...patch.labels, ...identity };
|
|
12
|
+
}
|
|
13
|
+
const spec = pod.spec;
|
|
14
|
+
assertVolumesPodLocal(patch.volumes, "volumes");
|
|
15
|
+
if (patch.volumes?.length)
|
|
16
|
+
spec.volumes = [...(spec.volumes ?? []), ...patch.volumes];
|
|
17
|
+
if (patch.podSecurityContext)
|
|
18
|
+
spec.securityContext = { ...(spec.securityContext ?? {}), ...patch.podSecurityContext };
|
|
19
|
+
const container = spec.containers?.[0];
|
|
20
|
+
if (container) {
|
|
21
|
+
if (patch.volumeMounts?.length)
|
|
22
|
+
container.volumeMounts = [...(container.volumeMounts ?? []), ...patch.volumeMounts];
|
|
23
|
+
if (patch.containerSecurityContext)
|
|
24
|
+
container.securityContext = { ...(container.securityContext ?? {}), ...patch.containerSecurityContext };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function assertHardeningOnly(sc, where) {
|
|
28
|
+
if (!sc)
|
|
29
|
+
return;
|
|
30
|
+
const bad = (msg) => {
|
|
31
|
+
throw new Error(`podSpecPatch.${where} rejected — it may only HARDEN, never weaken isolation: ${msg}`);
|
|
32
|
+
};
|
|
33
|
+
if (sc.privileged === true)
|
|
34
|
+
bad("privileged:true");
|
|
35
|
+
if (sc.allowPrivilegeEscalation === true)
|
|
36
|
+
bad("allowPrivilegeEscalation:true");
|
|
37
|
+
if (sc.readOnlyRootFilesystem === false)
|
|
38
|
+
bad("readOnlyRootFilesystem:false");
|
|
39
|
+
if (sc.runAsNonRoot === false)
|
|
40
|
+
bad("runAsNonRoot:false");
|
|
41
|
+
if (sc.runAsUser === 0)
|
|
42
|
+
bad("runAsUser:0 (root)");
|
|
43
|
+
const caps = sc.capabilities;
|
|
44
|
+
if (caps && Array.isArray(caps.add) && caps.add.length > 0)
|
|
45
|
+
bad(`capabilities.add ${JSON.stringify(caps.add)} (a hardening patch may only .drop)`);
|
|
46
|
+
if (sc.procMount === "Unmasked")
|
|
47
|
+
bad('procMount:"Unmasked" (re-exposes masked /proc)');
|
|
48
|
+
const seccomp = sc.seccompProfile;
|
|
49
|
+
if (seccomp && seccomp.type === "Unconfined")
|
|
50
|
+
bad('seccompProfile.type:"Unconfined" (disables syscall filtering)');
|
|
51
|
+
}
|
|
52
|
+
function assertVolumesPodLocal(volumes, where) {
|
|
53
|
+
if (!volumes?.length)
|
|
54
|
+
return;
|
|
55
|
+
const POD_LOCAL = new Set(["emptyDir", "configMap", "secret", "projected", "downwardAPI", "ephemeral"]);
|
|
56
|
+
for (const v of volumes) {
|
|
57
|
+
const vol = v;
|
|
58
|
+
const sources = Object.keys(vol).filter((k) => k !== "name");
|
|
59
|
+
for (const src of sources) {
|
|
60
|
+
if (!POD_LOCAL.has(src)) {
|
|
61
|
+
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("/")}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function execSocketTlsOptions(ca, insecureTls) {
|
|
67
|
+
const caList = ca ? [ca] : undefined;
|
|
68
|
+
return { ca: caList, rejectUnauthorized: !insecureTls, tls: { ca: caList, rejectUnauthorized: !insecureTls } };
|
|
69
|
+
}
|
|
70
|
+
export function exitCodeFromStatus(statusJson) {
|
|
71
|
+
try {
|
|
72
|
+
const s = JSON.parse(statusJson);
|
|
73
|
+
if (s.status === "Success")
|
|
74
|
+
return { exitCode: 0 };
|
|
75
|
+
const cause = s.details?.causes?.find((c) => c.reason === "ExitCode");
|
|
76
|
+
if (cause?.message?.trim()) {
|
|
77
|
+
const code = Number(cause.message);
|
|
78
|
+
if (Number.isFinite(code))
|
|
79
|
+
return { exitCode: code };
|
|
80
|
+
}
|
|
81
|
+
return { error: s.message || s.reason || "exec failed without an exit code" };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { error: `unparseable exec status: ${statusJson.slice(0, 200)}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=k8s-exec-protocol.js.map
|
|
@@ -1,34 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { type Checkpoint, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason } from "@sema-agent/core";
|
|
3
|
-
import { type PendingCheckpoint } from "./tidb-checkpoint-store.js";
|
|
4
|
-
export declare class PgCheckpointStore implements CheckpointStore {
|
|
5
|
-
private readonly pool;
|
|
6
|
-
private readonly logger?;
|
|
7
|
-
constructor(pool: Pool, logger?: {
|
|
8
|
-
info?(msg: string, meta?: unknown): void;
|
|
9
|
-
} | undefined);
|
|
10
|
-
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
|
|
11
|
-
get(token: CheckpointToken): Promise<Checkpoint | null>;
|
|
12
|
-
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
|
|
13
|
-
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
14
|
-
setPendingSteer(token: CheckpointToken, scope: string, steer: {
|
|
15
|
-
text: string;
|
|
16
|
-
trusted: boolean;
|
|
17
|
-
}): Promise<boolean>;
|
|
18
|
-
expire(token: CheckpointToken, scope: string): Promise<boolean>;
|
|
19
|
-
reap(scope: string, cutoff: number): Promise<number>;
|
|
20
|
-
reapExpired(cutoff: number): Promise<number>;
|
|
21
|
-
listExpiredApprovalGates(cutoff: number, limit?: number): Promise<Array<{
|
|
22
|
-
sessionId: string;
|
|
23
|
-
scope: string;
|
|
24
|
-
}>>;
|
|
25
|
-
listPending(scope?: string): Promise<PendingCheckpoint[]>;
|
|
26
|
-
listByScope(scope: string): Promise<CheckpointSummary[]>;
|
|
27
|
-
findPendingTokenBySession(sessionId: string, scope?: string): Promise<CheckpointToken | null>;
|
|
28
|
-
peekPendingScope(sessionId: string): Promise<string | null | undefined>;
|
|
29
|
-
putCtx(sessionId: string, ctx: unknown): Promise<void>;
|
|
30
|
-
getCtx<T = unknown>(sessionId: string): Promise<T | null>;
|
|
31
|
-
reapCtx(cutoff: number): Promise<number>;
|
|
32
|
-
deleteBySession(sessionId: string, owner: string | null): Promise<number>;
|
|
33
|
-
}
|
|
1
|
+
export { PgCheckpointStore } from "./checkpoint-store-sql.js";
|
|
34
2
|
//# sourceMappingURL=pg-checkpoint-store.d.ts.map
|
|
@@ -1,190 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { redactDeep } from "../trace/redact.js";
|
|
3
|
-
import { tokenFingerprint } from "./tidb-checkpoint-store.js";
|
|
4
|
-
import { pgProtocolJsonStringify } from "./pg-safe-json.js";
|
|
5
|
-
import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
|
|
6
|
-
const MAX_TOOL_INPUT_CHARS = 8192;
|
|
7
|
-
const TERMINAL_BACKSTOP_MS = Math.max(60_000, Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) || 30 * 86_400_000);
|
|
8
|
-
const TERMINAL_GRACE_MS = 3_600_000;
|
|
9
|
-
const PG_UNIQUE_VIOLATION = "23505";
|
|
10
|
-
function boundedToolInput(args) {
|
|
11
|
-
if (args === undefined)
|
|
12
|
-
return null;
|
|
13
|
-
const redacted = redactDeep(args);
|
|
14
|
-
const json = JSON.stringify(redacted);
|
|
15
|
-
if (json === undefined)
|
|
16
|
-
return null;
|
|
17
|
-
if (json.length > MAX_TOOL_INPUT_CHARS)
|
|
18
|
-
return { truncated: true, bytes: json.length };
|
|
19
|
-
return redacted;
|
|
20
|
-
}
|
|
21
|
-
export class PgCheckpointStore {
|
|
22
|
-
pool;
|
|
23
|
-
logger;
|
|
24
|
-
constructor(pool, logger) {
|
|
25
|
-
this.pool = pool;
|
|
26
|
-
this.logger = logger;
|
|
27
|
-
}
|
|
28
|
-
async put(token, cp) {
|
|
29
|
-
const pa = cp.pendingAction;
|
|
30
|
-
const version = checkpointVersionOf(cp);
|
|
31
|
-
const toolInput = boundedToolInput(pa?.args);
|
|
32
|
-
try {
|
|
33
|
-
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) " +
|
|
34
|
-
"VALUES ($1,$2,$3,$4,'pending',$5,$6,$7::jsonb,$8::jsonb,$9,$10,$11,$12,$13,$14)", [
|
|
35
|
-
token,
|
|
36
|
-
cp.scope,
|
|
37
|
-
cp.sessionId,
|
|
38
|
-
version,
|
|
39
|
-
pa?.toolName ?? null,
|
|
40
|
-
pa?.toolCallId ?? null,
|
|
41
|
-
toolInput === null ? null : pgProtocolJsonStringify(toolInput, "approval tool_input"),
|
|
42
|
-
pgProtocolJsonStringify(cp, "checkpoint"),
|
|
43
|
-
cp.deadline ?? null,
|
|
44
|
-
cp.createdAt,
|
|
45
|
-
Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS),
|
|
46
|
-
cp.gate?.kind ?? null,
|
|
47
|
-
pa?.boundInputHash ?? null,
|
|
48
|
-
((g) => (g?.riskDescriptor ? pgProtocolJsonStringify(g.riskDescriptor, "risk descriptor") : null))(cp.gate),
|
|
49
|
-
]);
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
if (e?.code === PG_UNIQUE_VIOLATION) {
|
|
53
|
-
throw new CheckpointError("checkpoint.already_exists", `checkpoint token already exists`);
|
|
54
|
-
}
|
|
55
|
-
throw e;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async get(token) {
|
|
59
|
-
const res = await this.pool.query("SELECT version, status, checkpoint, outcome, reopen_reason, rev, pending_steer FROM checkpoint WHERE token = $1", [token]);
|
|
60
|
-
const r = res.rows[0];
|
|
61
|
-
if (!r)
|
|
62
|
-
return null;
|
|
63
|
-
const version = Number(r.version);
|
|
64
|
-
if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
65
|
-
throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
|
|
66
|
-
}
|
|
67
|
-
const cp = parseJson(r.checkpoint);
|
|
68
|
-
cp.status = r.status;
|
|
69
|
-
const outcomeRaw = parseJson(r.outcome);
|
|
70
|
-
if (outcomeRaw)
|
|
71
|
-
cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
|
|
72
|
-
cp.reopenReason = r.reopen_reason ?? undefined;
|
|
73
|
-
cp.rev = r.rev == null ? 0 : Number(r.rev);
|
|
74
|
-
const steerRaw = parseJson(r.pending_steer);
|
|
75
|
-
if (steerRaw && cp.state)
|
|
76
|
-
cp.state.pendingSteer = steerRaw;
|
|
77
|
-
this.logger?.info?.("checkpoint_get", {
|
|
78
|
-
tokenFp: tokenFingerprint(token),
|
|
79
|
-
status: cp.status,
|
|
80
|
-
version,
|
|
81
|
-
hasWorkspaceHandle: !!cp.state?.workspaceHandle,
|
|
82
|
-
snapshotId: cp.state?.workspaceHandle?.snapshotId,
|
|
83
|
-
});
|
|
84
|
-
return cp;
|
|
85
|
-
}
|
|
86
|
-
async resolve(token, scope, outcome, expect) {
|
|
87
|
-
const params = [pgProtocolJsonStringify(outcome, "checkpoint outcome"), Date.now(), token, scope];
|
|
88
|
-
if (expect)
|
|
89
|
-
params.push(expect.rev);
|
|
90
|
-
const res = await this.pool.query("UPDATE checkpoint SET status = 'resolved', outcome = $1::jsonb, decided_at = $2, rev = rev + 1, reopen_reason = NULL WHERE token = $3 AND scope = $4 AND status = 'pending'" +
|
|
91
|
-
(expect ? " AND rev = $5" : ""), params);
|
|
92
|
-
return (res.rowCount ?? 0) === 1;
|
|
93
|
-
}
|
|
94
|
-
async reopen(token, scope, reason) {
|
|
95
|
-
const res = await this.pool.query("UPDATE checkpoint SET status = 'pending', reopen_reason = $1, rev = rev + 1 WHERE token = $2 AND scope = $3 AND status = 'resolved'", [reason, token, scope]);
|
|
96
|
-
return (res.rowCount ?? 0) === 1;
|
|
97
|
-
}
|
|
98
|
-
async setPendingSteer(token, scope, steer) {
|
|
99
|
-
const clean = validatePendingSteer(steer);
|
|
100
|
-
const res = await this.pool.query("UPDATE checkpoint SET pending_steer = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'", [pgProtocolJsonStringify(clean, "pending steer"), token, scope]);
|
|
101
|
-
return (res.rowCount ?? 0) > 0;
|
|
102
|
-
}
|
|
103
|
-
async expire(token, scope) {
|
|
104
|
-
const res = await this.pool.query("UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'", [Date.now(), token, scope]);
|
|
105
|
-
return (res.rowCount ?? 0) === 1;
|
|
106
|
-
}
|
|
107
|
-
async reap(scope, cutoff) {
|
|
108
|
-
const res = await this.pool.query("UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE scope = $2 AND status = 'pending' AND deadline IS NOT NULL AND deadline <= $3", [Date.now(), scope, cutoff]);
|
|
109
|
-
return res.rowCount ?? 0;
|
|
110
|
-
}
|
|
111
|
-
async reapExpired(cutoff) {
|
|
112
|
-
const res = await this.pool.query("UPDATE checkpoint SET status = 'expired', decided_at = $1 " +
|
|
113
|
-
"WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 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 <= $3))", [Date.now(), cutoff, cutoff]);
|
|
114
|
-
return res.rowCount ?? 0;
|
|
115
|
-
}
|
|
116
|
-
async listExpiredApprovalGates(cutoff, limit = 100) {
|
|
117
|
-
const res = await this.pool.query("SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
|
|
118
|
-
"AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= $1 ORDER BY deadline ASC LIMIT $2", [cutoff, limit]);
|
|
119
|
-
return res.rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
|
|
120
|
-
}
|
|
121
|
-
async listPending(scope) {
|
|
122
|
-
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 " +
|
|
123
|
-
"FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
|
|
124
|
-
const res = scope
|
|
125
|
-
? await this.pool.query(`${base} AND c.scope=$1 ORDER BY c.created_at ASC`, [scope])
|
|
126
|
-
: await this.pool.query(`${base} ORDER BY c.created_at ASC`);
|
|
127
|
-
const out = res.rows.map((r) => {
|
|
128
|
-
const toolCallId = r.tool_call_id ?? null;
|
|
129
|
-
return {
|
|
130
|
-
sessionId: String(r.session_id),
|
|
131
|
-
scope: String(r.scope),
|
|
132
|
-
toolName: r.tool_name ?? null,
|
|
133
|
-
toolCallId,
|
|
134
|
-
boundCallId: toolCallId,
|
|
135
|
-
boundInputHash: r.bound_input_hash ?? null,
|
|
136
|
-
taskId: r.task_id ?? null,
|
|
137
|
-
input: r.tool_input ?? null,
|
|
138
|
-
createdAt: Number(r.created_at),
|
|
139
|
-
deadline: r.deadline == null ? null : Number(r.deadline),
|
|
140
|
-
riskDescriptor: parseJson(r.risk_descriptor),
|
|
141
|
-
};
|
|
142
|
-
});
|
|
143
|
-
return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
|
|
144
|
-
}
|
|
145
|
-
async listByScope(scope) {
|
|
146
|
-
const res = await this.pool.query("SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at ASC LIMIT 500", [scope]);
|
|
147
|
-
const out = [];
|
|
148
|
-
for (const r of res.rows) {
|
|
149
|
-
const cp = parseJson(r.checkpoint);
|
|
150
|
-
if (!cp)
|
|
151
|
-
continue;
|
|
152
|
-
cp.status = r.status;
|
|
153
|
-
out.push(summarizeCheckpoint(cp));
|
|
154
|
-
}
|
|
155
|
-
return out;
|
|
156
|
-
}
|
|
157
|
-
async findPendingTokenBySession(sessionId, scope) {
|
|
158
|
-
const res = scope
|
|
159
|
-
? await this.pool.query("SELECT token FROM checkpoint WHERE session_id=$1 AND scope=$2 AND status='pending' LIMIT 1", [sessionId, scope])
|
|
160
|
-
: await this.pool.query("SELECT token FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1", [sessionId]);
|
|
161
|
-
return res.rows[0] ? String(res.rows[0].token) : null;
|
|
162
|
-
}
|
|
163
|
-
async peekPendingScope(sessionId) {
|
|
164
|
-
const res = await this.pool.query("SELECT scope FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1", [sessionId]);
|
|
165
|
-
if (!res.rows[0])
|
|
166
|
-
return undefined;
|
|
167
|
-
return res.rows[0].scope ?? null;
|
|
168
|
-
}
|
|
169
|
-
async putCtx(sessionId, ctx) {
|
|
170
|
-
await this.pool.query("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES ($1,$2::jsonb,$3) " +
|
|
171
|
-
"ON CONFLICT (session_id) DO UPDATE SET ctx = EXCLUDED.ctx, updated_at = EXCLUDED.updated_at", [sessionId, pgProtocolJsonStringify(ctx, "checkpoint ctx"), Date.now()]);
|
|
172
|
-
}
|
|
173
|
-
async getCtx(sessionId) {
|
|
174
|
-
const res = await this.pool.query("SELECT ctx FROM checkpoint_ctx WHERE session_id=$1", [sessionId]);
|
|
175
|
-
return res.rows[0] ? parseJson(res.rows[0].ctx) : null;
|
|
176
|
-
}
|
|
177
|
-
async reapCtx(cutoff) {
|
|
178
|
-
const res = await this.pool.query("DELETE FROM checkpoint_ctx WHERE updated_at < $1 " +
|
|
179
|
-
"AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
|
|
180
|
-
"AND session_id NOT IN (SELECT session_id FROM task_active)", [cutoff]);
|
|
181
|
-
return res.rowCount ?? 0;
|
|
182
|
-
}
|
|
183
|
-
async deleteBySession(sessionId, owner) {
|
|
184
|
-
const ownerGuard = "EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = $1 AND sm.owner IS NOT DISTINCT FROM $2)";
|
|
185
|
-
const res = await this.pool.query(`DELETE FROM checkpoint WHERE session_id = $1 AND ${ownerGuard}`, [sessionId, owner]);
|
|
186
|
-
await this.pool.query(`DELETE FROM checkpoint_ctx WHERE session_id = $1 AND ${ownerGuard}`, [sessionId, owner]);
|
|
187
|
-
return res.rowCount ?? 0;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
1
|
+
export { PgCheckpointStore } from "./checkpoint-store-sql.js";
|
|
190
2
|
//# sourceMappingURL=pg-checkpoint-store.js.map
|
|
@@ -1,151 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
const
|
|
3
|
-
class PgWriteBehindCounter {
|
|
4
|
-
pool;
|
|
5
|
-
windowMs;
|
|
6
|
-
now;
|
|
7
|
-
onDegraded;
|
|
8
|
-
queryTimeoutMs;
|
|
9
|
-
pendingDelta = new Map();
|
|
10
|
-
fleetTotal = new Map();
|
|
11
|
-
timer;
|
|
12
|
-
flushing = false;
|
|
13
|
-
flushStartedAt = 0;
|
|
14
|
-
failStreak = new Map();
|
|
15
|
-
constructor(pool, windowMs, now = () => Date.now(), onDegraded, queryTimeoutMs = DEFAULT_COUNTER_QUERY_TIMEOUT_MS) {
|
|
16
|
-
this.pool = pool;
|
|
17
|
-
this.windowMs = windowMs;
|
|
18
|
-
this.now = now;
|
|
19
|
-
this.onDegraded = onDegraded;
|
|
20
|
-
this.queryTimeoutMs = queryTimeoutMs;
|
|
21
|
-
}
|
|
22
|
-
bump(kind, failed, error) {
|
|
23
|
-
if (!this.onDegraded)
|
|
24
|
-
return;
|
|
25
|
-
const prev = this.failStreak.get(kind) ?? 0;
|
|
26
|
-
if (!failed && prev === 0)
|
|
27
|
-
return;
|
|
28
|
-
const next = failed ? prev + 1 : 0;
|
|
29
|
-
this.failStreak.set(kind, next);
|
|
30
|
-
try {
|
|
31
|
-
this.onDegraded({ table: "cost_quota", kind, streak: next, prevStreak: prev, ...(failed && error !== undefined ? { error: error instanceof Error ? error.message : String(error) } : {}) });
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
bucket() {
|
|
37
|
-
return Math.floor(this.now() / this.windowMs);
|
|
38
|
-
}
|
|
39
|
-
rk(key) {
|
|
40
|
-
return `${key}${SEP}${this.bucket()}`;
|
|
41
|
-
}
|
|
42
|
-
used(key) {
|
|
43
|
-
const rk = this.rk(key);
|
|
44
|
-
return (this.fleetTotal.get(rk) ?? 0) + (this.pendingDelta.get(rk) ?? 0);
|
|
45
|
-
}
|
|
46
|
-
addLocal(key, n) {
|
|
47
|
-
if (n <= 0)
|
|
48
|
-
return;
|
|
49
|
-
const rk = this.rk(key);
|
|
50
|
-
this.pendingDelta.set(rk, (this.pendingDelta.get(rk) ?? 0) + n);
|
|
51
|
-
}
|
|
52
|
-
retryAfterSec() {
|
|
53
|
-
return Math.ceil(((this.bucket() + 1) * this.windowMs - this.now()) / 1000);
|
|
54
|
-
}
|
|
55
|
-
async flush() {
|
|
56
|
-
if (this.flushing) {
|
|
57
|
-
if (this.now() - this.flushStartedAt > 60_000)
|
|
58
|
-
this.bump("stalled", true, "flush round stalled >60s (hung query?)");
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
this.flushing = true;
|
|
62
|
-
this.flushStartedAt = this.now();
|
|
63
|
-
try {
|
|
64
|
-
await this.flushOnce();
|
|
65
|
-
}
|
|
66
|
-
finally {
|
|
67
|
-
this.flushing = false;
|
|
68
|
-
this.bump("stalled", false);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async flushOnce() {
|
|
72
|
-
let attempted = false;
|
|
73
|
-
let failure;
|
|
74
|
-
let hadFailure = false;
|
|
75
|
-
for (const [rk, delta] of [...this.pendingDelta]) {
|
|
76
|
-
if (delta <= 0)
|
|
77
|
-
continue;
|
|
78
|
-
const sep = rk.lastIndexOf(SEP);
|
|
79
|
-
const key = rk.slice(0, sep);
|
|
80
|
-
const windowBucket = Number(rk.slice(sep + 1));
|
|
81
|
-
attempted = true;
|
|
82
|
-
try {
|
|
83
|
-
const q = {
|
|
84
|
-
text: "INSERT INTO cost_quota (scope_key, window_bucket, micro, updated_at) VALUES ($1,$2,$3,$4) " +
|
|
85
|
-
"ON CONFLICT (scope_key, window_bucket) DO UPDATE SET micro = cost_quota.micro + EXCLUDED.micro, updated_at = EXCLUDED.updated_at",
|
|
86
|
-
values: [key, windowBucket, delta, new Date()],
|
|
87
|
-
query_timeout: this.queryTimeoutMs,
|
|
88
|
-
};
|
|
89
|
-
await this.pool.query(q);
|
|
90
|
-
const remaining = (this.pendingDelta.get(rk) ?? 0) - delta;
|
|
91
|
-
if (remaining > 0)
|
|
92
|
-
this.pendingDelta.set(rk, remaining);
|
|
93
|
-
else
|
|
94
|
-
this.pendingDelta.delete(rk);
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
hadFailure = true;
|
|
98
|
-
failure = err;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
if (attempted)
|
|
102
|
-
this.bump("flush", hadFailure, failure);
|
|
103
|
-
await this.refresh();
|
|
104
|
-
await this.reap();
|
|
105
|
-
}
|
|
106
|
-
async refresh() {
|
|
107
|
-
const b = this.bucket();
|
|
108
|
-
try {
|
|
109
|
-
const q = { text: "SELECT scope_key, micro FROM cost_quota WHERE window_bucket = $1", values: [b], query_timeout: this.queryTimeoutMs };
|
|
110
|
-
const res = await this.pool.query(q);
|
|
111
|
-
const next = new Map();
|
|
112
|
-
for (const r of res.rows) {
|
|
113
|
-
next.set(`${String(r.scope_key)}${SEP}${b}`, Number(r.micro));
|
|
114
|
-
}
|
|
115
|
-
this.fleetTotal = next;
|
|
116
|
-
this.bump("refresh", false);
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
this.bump("refresh", true, err);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async reap() {
|
|
123
|
-
try {
|
|
124
|
-
const q = { text: "DELETE FROM cost_quota WHERE window_bucket < $1", values: [this.bucket()], query_timeout: this.queryTimeoutMs };
|
|
125
|
-
const res = await this.pool.query(q);
|
|
126
|
-
return res.rowCount ?? 0;
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return 0;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
startRefresh(intervalMs = 5_000) {
|
|
133
|
-
this.timer ??= setInterval(() => void this.flush(), intervalMs);
|
|
134
|
-
this.timer.unref?.();
|
|
135
|
-
return this;
|
|
136
|
-
}
|
|
137
|
-
stop() {
|
|
138
|
-
if (this.timer)
|
|
139
|
-
clearInterval(this.timer);
|
|
140
|
-
this.timer = undefined;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
1
|
+
import { PgWriteBehindCounter } from "./write-behind-counter.js";
|
|
2
|
+
const COST_QUOTA_TABLE = { table: "cost_quota", keyCol: "scope_key", valCol: "micro" };
|
|
143
3
|
export class PgCostQuota {
|
|
144
4
|
limitMicroUsd;
|
|
145
5
|
counter;
|
|
146
6
|
constructor(pool, limitMicroUsd, windowMs, now, onDegraded, queryTimeoutMs) {
|
|
147
7
|
this.limitMicroUsd = limitMicroUsd;
|
|
148
|
-
this.counter = new PgWriteBehindCounter(pool, windowMs, now, onDegraded, queryTimeoutMs);
|
|
8
|
+
this.counter = new PgWriteBehindCounter(pool, COST_QUOTA_TABLE, windowMs, now, onDegraded, queryTimeoutMs);
|
|
149
9
|
}
|
|
150
10
|
check(scopeKey) {
|
|
151
11
|
const used = this.counter.used(scopeKey);
|
|
@@ -1,42 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
import type { BakeStatus, BakeState, BakeRecord, BakeEvent, CreateBakeInput, BakeTerminal } from "./store-contracts.js";
|
|
1
|
+
export { PgImageBake, PG_IMAGE_BAKE_SCHEMA, ensurePgImageBakeSchema as ensureSchema } from "./image-bake-store-sql.js";
|
|
3
2
|
export type { BakeStatus, BakeState, BakeErrorCode, BakeRecord, BakeEvent, CreateBakeInput, BakeTerminal, } from "./store-contracts.js";
|
|
4
|
-
export declare const PG_IMAGE_BAKE_SCHEMA: string[];
|
|
5
|
-
export declare function ensureSchema(pool: Pool | PoolClient, poolName?: string): Promise<void>;
|
|
6
|
-
export declare class PgImageBake {
|
|
7
|
-
private readonly pool;
|
|
8
|
-
private readonly poolName;
|
|
9
|
-
constructor(pool: Pool, poolName?: string);
|
|
10
|
-
createBake(input: CreateBakeInput): Promise<{
|
|
11
|
-
created: boolean;
|
|
12
|
-
bake: BakeRecord;
|
|
13
|
-
}>;
|
|
14
|
-
createBakeIfIdle(input: CreateBakeInput): Promise<{
|
|
15
|
-
created: boolean;
|
|
16
|
-
bake: BakeRecord | null;
|
|
17
|
-
}>;
|
|
18
|
-
getBake(bakeId: string): Promise<BakeRecord | null>;
|
|
19
|
-
getBakeByIdem(idemKey: string): Promise<BakeRecord | null>;
|
|
20
|
-
findActiveByArgv(argv: string[]): Promise<BakeRecord | null>;
|
|
21
|
-
findActiveAny(): Promise<BakeRecord | null>;
|
|
22
|
-
findNextQueuedId(): Promise<string | null>;
|
|
23
|
-
claimBake(bakeId: string, runnerId: string, leaseMs: number): Promise<BakeRecord | null>;
|
|
24
|
-
appendEvent(bakeId: string, kind: string, data: unknown, opts?: {
|
|
25
|
-
lineOrd?: number | null;
|
|
26
|
-
level?: string | null;
|
|
27
|
-
}): Promise<number | null>;
|
|
28
|
-
private lineOrdExists;
|
|
29
|
-
maxSeq(bakeId: string): Promise<number>;
|
|
30
|
-
retainedFrom(bakeId: string): Promise<number>;
|
|
31
|
-
getEvents(bakeId: string, afterSeq: number): Promise<BakeEvent[]>;
|
|
32
|
-
setStatus(bakeId: string, status: BakeStatus): Promise<void>;
|
|
33
|
-
setState(bakeId: string, state: BakeState): Promise<void>;
|
|
34
|
-
setTerminal(bakeId: string, t: BakeTerminal): Promise<number>;
|
|
35
|
-
setIndexId(bakeId: string, indexId: string): Promise<boolean>;
|
|
36
|
-
heartbeatLease(bakeId: string, runnerId: string, leaseMs: number): Promise<boolean>;
|
|
37
|
-
requestCancel(bakeId: string): Promise<boolean>;
|
|
38
|
-
isCancelRequested(bakeId: string): Promise<boolean>;
|
|
39
|
-
reapStaleBakes(olderThanMs: number): Promise<number>;
|
|
40
|
-
private tx;
|
|
41
|
-
}
|
|
42
3
|
//# sourceMappingURL=pg-image-bake.d.ts.map
|