@sema-agent/server 7.2.0 → 7.4.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 (140) hide show
  1. package/README.md +2 -1
  2. package/README.zh-CN.md +1 -1
  3. package/USAGE.md +26 -1
  4. package/dist/approval-ask-machine.d.ts +39 -0
  5. package/dist/approval-ask-machine.js +101 -0
  6. package/dist/approval-card.d.ts +244 -0
  7. package/dist/approval-card.js +237 -0
  8. package/dist/approval-deny-reasons.d.ts +56 -0
  9. package/dist/approval-deny-reasons.js +54 -0
  10. package/dist/approval-reconciler.d.ts +174 -0
  11. package/dist/approval-reconciler.js +307 -0
  12. package/dist/boot/coordinators.d.ts +1 -0
  13. package/dist/boot/coordinators.js +39 -4
  14. package/dist/boot/deferred-sandbox-path-env.d.ts +99 -0
  15. package/dist/boot/deferred-sandbox-path-env.js +279 -0
  16. package/dist/boot/execution-env.js +11 -1
  17. package/dist/boot/lexical-path-env.d.ts +10 -0
  18. package/dist/boot/lexical-path-env.js +88 -0
  19. package/dist/boot/reapers.d.ts +34 -0
  20. package/dist/boot/reapers.js +198 -23
  21. package/dist/boot/resolve-spec.js +97 -33
  22. package/dist/capabilities/center-prompts.js +4 -1
  23. package/dist/capabilities/oa-tools.d.ts +15 -0
  24. package/dist/capabilities/oa-tools.js +54 -0
  25. package/dist/config-types.d.ts +68 -1
  26. package/dist/config.d.ts +1 -0
  27. package/dist/config.js +138 -2
  28. package/dist/elicitation.d.ts +4 -0
  29. package/dist/elicitation.js +7 -3
  30. package/dist/finance/cost-taxonomy.d.ts +34 -0
  31. package/dist/finance/cost-taxonomy.js +26 -0
  32. package/dist/hooks/hook-runner.js +32 -0
  33. package/dist/http/routes/capabilities.js +14 -0
  34. package/dist/http/routes/diagnostics.d.ts +84 -0
  35. package/dist/http/routes/diagnostics.js +140 -0
  36. package/dist/http/routes/runs.d.ts +1 -0
  37. package/dist/http/routes/runs.js +548 -16
  38. package/dist/http/routes/tasks.js +175 -12
  39. package/dist/http/server.d.ts +6 -1
  40. package/dist/http/server.js +120 -4
  41. package/dist/http/sse-log.d.ts +51 -0
  42. package/dist/http/sse-log.js +64 -0
  43. package/dist/http/wire-types.d.ts +20 -5
  44. package/dist/main.js +35 -4
  45. package/dist/observability/fail-open.d.ts +98 -0
  46. package/dist/observability/fail-open.js +216 -0
  47. package/dist/observability/prompt-manifest.d.ts +13 -0
  48. package/dist/observability/prompt-manifest.js +8 -0
  49. package/dist/plugins/approval-ask-store-memory.d.ts +38 -0
  50. package/dist/plugins/approval-ask-store-memory.js +299 -0
  51. package/dist/plugins/approval-ask-store-sql.d.ts +341 -0
  52. package/dist/plugins/approval-ask-store-sql.js +705 -0
  53. package/dist/plugins/approval-store-sql.d.ts +116 -0
  54. package/dist/plugins/approval-store-sql.js +151 -0
  55. package/dist/plugins/background-agent-store-sql.js +20 -1
  56. package/dist/plugins/checkpoint-store-sql.d.ts +84 -9
  57. package/dist/plugins/checkpoint-store-sql.js +297 -16
  58. package/dist/plugins/file-workflow-journal-store.d.ts +12 -0
  59. package/dist/plugins/file-workflow-journal-store.js +12 -0
  60. package/dist/plugins/local-checkpoint-store.d.ts +6 -5
  61. package/dist/plugins/local-checkpoint-store.js +4 -0
  62. package/dist/plugins/pg-approval-store.d.ts +9 -0
  63. package/dist/plugins/pg-approval-store.js +9 -0
  64. package/dist/plugins/pg-breaker-state.d.ts +8 -0
  65. package/dist/plugins/pg-breaker-state.js +8 -0
  66. package/dist/plugins/pg-checkpoint-store.d.ts +10 -0
  67. package/dist/plugins/pg-checkpoint-store.js +10 -0
  68. package/dist/plugins/pg-file-snapshot-store.d.ts +8 -0
  69. package/dist/plugins/pg-file-snapshot-store.js +8 -0
  70. package/dist/plugins/pg-image-bake.d.ts +12 -0
  71. package/dist/plugins/pg-image-bake.js +11 -0
  72. package/dist/plugins/pg-image-index.d.ts +12 -0
  73. package/dist/plugins/pg-image-index.js +11 -0
  74. package/dist/plugins/pg-outcome-ledger.d.ts +12 -0
  75. package/dist/plugins/pg-outcome-ledger.js +11 -0
  76. package/dist/plugins/pg-pool.js +11 -0
  77. package/dist/plugins/pg-resume-anchor-store.d.ts +7 -0
  78. package/dist/plugins/pg-resume-anchor-store.js +7 -0
  79. package/dist/plugins/pg-run-store.d.ts +9 -0
  80. package/dist/plugins/pg-run-store.js +9 -0
  81. package/dist/plugins/pg-session-policy-store.d.ts +7 -0
  82. package/dist/plugins/pg-session-policy-store.js +7 -0
  83. package/dist/plugins/pg-session-store.d.ts +12 -0
  84. package/dist/plugins/pg-session-store.js +12 -0
  85. package/dist/plugins/pg-tool-result-store.d.ts +9 -0
  86. package/dist/plugins/pg-tool-result-store.js +9 -0
  87. package/dist/plugins/pg-workflow-journal-store.d.ts +9 -0
  88. package/dist/plugins/pg-workflow-journal-store.js +9 -0
  89. package/dist/plugins/pg-workflow-run-store.d.ts +9 -0
  90. package/dist/plugins/pg-workflow-run-store.js +9 -0
  91. package/dist/plugins/store-backend.d.ts +18 -0
  92. package/dist/plugins/store-backend.js +10 -0
  93. package/dist/plugins/tidb-approval-store.d.ts +8 -0
  94. package/dist/plugins/tidb-approval-store.js +8 -0
  95. package/dist/plugins/tidb-breaker-state.d.ts +7 -0
  96. package/dist/plugins/tidb-breaker-state.js +7 -0
  97. package/dist/plugins/tidb-checkpoint-store.d.ts +9 -0
  98. package/dist/plugins/tidb-checkpoint-store.js +9 -0
  99. package/dist/plugins/tidb-file-snapshot-store.d.ts +8 -0
  100. package/dist/plugins/tidb-file-snapshot-store.js +8 -0
  101. package/dist/plugins/tidb-image-bake.d.ts +12 -0
  102. package/dist/plugins/tidb-image-bake.js +11 -0
  103. package/dist/plugins/tidb-image-index.d.ts +12 -0
  104. package/dist/plugins/tidb-image-index.js +11 -0
  105. package/dist/plugins/tidb-outcome-ledger.d.ts +12 -0
  106. package/dist/plugins/tidb-outcome-ledger.js +12 -0
  107. package/dist/plugins/tidb-pool.js +27 -4
  108. package/dist/plugins/tidb-resume-anchor-store.d.ts +7 -0
  109. package/dist/plugins/tidb-resume-anchor-store.js +7 -0
  110. package/dist/plugins/tidb-run-store.d.ts +10 -0
  111. package/dist/plugins/tidb-run-store.js +9 -0
  112. package/dist/plugins/tidb-session-policy-store.d.ts +7 -0
  113. package/dist/plugins/tidb-session-policy-store.js +7 -0
  114. package/dist/plugins/tidb-tool-result-store.d.ts +8 -0
  115. package/dist/plugins/tidb-tool-result-store.js +10 -0
  116. package/dist/plugins/tidb-workflow-journal-store.d.ts +9 -0
  117. package/dist/plugins/tidb-workflow-journal-store.js +9 -0
  118. package/dist/plugins/tidb-workflow-run-store.d.ts +10 -0
  119. package/dist/plugins/tidb-workflow-run-store.js +10 -0
  120. package/dist/plugins/workflow-journal-limits.d.ts +12 -0
  121. package/dist/plugins/workflow-journal-limits.js +12 -0
  122. package/dist/question.d.ts +21 -14
  123. package/dist/question.js +83 -34
  124. package/dist/runs.d.ts +16 -1
  125. package/dist/runs.js +61 -3
  126. package/dist/runtime-caps-resolver.d.ts +7 -1
  127. package/dist/runtime-caps-resolver.js +65 -3
  128. package/dist/sema-registry.d.ts +41 -0
  129. package/dist/sema-registry.js +40 -0
  130. package/dist/spec-fields.d.ts +4 -0
  131. package/dist/spec-fields.js +6 -0
  132. package/dist/task-settings.d.ts +36 -15
  133. package/dist/task-settings.js +19 -5
  134. package/dist/tool-approval.d.ts +296 -3
  135. package/dist/tool-approval.js +1074 -50
  136. package/dist/trace/core-keyset-guard.d.ts +2 -2
  137. package/dist/trace/ledger-sink.js +14 -1
  138. package/dist/trace/project.d.ts +90 -0
  139. package/dist/trace/project.js +188 -0
  140. package/package.json +5 -4
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Durable approval store for the human-in-the-loop write gate (F4) — SINGLE-FILE DUAL-DIALECT
3
+ * (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBApprovalStore` /
4
+ * `PgApprovalStore` class names survive as thin ctor subclasses so every consumer (store-backend.ts,
5
+ * src/index.ts, the approval/audit/security-coverage suites) is untouched.
6
+ *
7
+ * When a gated tool call is about to run, a `pending` row is recorded here; the instance running the
8
+ * task polls it for a decision. Because the row is durable and shared, an operator's approve/deny can
9
+ * land on ANY instance — critical under async runs + a load balancer, where the deciding request and
10
+ * the waiting run are usually on different instances.
11
+ *
12
+ * Unlike the run/checkpoint PG store (whose schema is owned centrally by pg-pool.ts), this file also
13
+ * exports a self-contained PG schema (`PG_APPROVAL_SCHEMA` + `ensureSchema`) — the `approval` table is
14
+ * DISJOINT from the other PG stores' tables, so a CREATE IF NOT EXISTS here shadows nothing. Central
15
+ * aggregation into pg-pool.ts composes it alongside the others. Kept in lock-step with the TiDB store
16
+ * by the shared real-DB integration suite (test/pg-approval-store-integration.test.ts).
17
+ *
18
+ * ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
19
+ * - `?` placeholders vs `$n`
20
+ * - null-safe scope compare: `<=>` vs `IS NOT DISTINCT FROM` (a NULL scope row — legacy/untenanted
21
+ * run — is matched by a NULL scope arg; one tenant's scope NEVER matches another's — the
22
+ * single-DB fleet read guard, defense-in-depth)
23
+ * - JSON binding: TiDB `JSON.stringify` verbatim vs PG `pgProtocolJsonStringify` (codex R4-H2: the
24
+ * persisted approval `args` is the operator's REVIEW surface and must agree byte-for-byte with the
25
+ * policy-executed args — a lossy/rewriting envelope would let an operator approve one payload while
26
+ * a different one runs)
27
+ * - affectedRows vs rowCount (via SqlDriver)
28
+ * - schema ownership: TiDB DDL lives in tidb-pool.ts SCHEMA_STATEMENTS; PG DDL is this file's own
29
+ * self-contained `PG_APPROVAL_SCHEMA` (see the PG twin's original header — unlike the other PG
30
+ * stores, `approval` is NOT centrally owned by pg-pool.ts's DDL array, only aggregated into its
31
+ * `ensureSchema` composition).
32
+ * - `getStatus` result shape: `RowDataPacket[]` (mysql2 typed query) vs a plain-record `pg` query —
33
+ * normalized to `SqlRow` by the shared driver, so both arms read the same field names.
34
+ */
35
+ import type { Pool as MySqlPool } from "mysql2/promise";
36
+ import type { Pool as PgPool, PoolClient } from "pg";
37
+ import { type SqlDriver } from "./sql-driver.js";
38
+ export type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
39
+ export interface ApprovalRow {
40
+ id: string;
41
+ taskId: string | null;
42
+ sessionId: string | null;
43
+ owner: string | null;
44
+ /** Single-DB fleet scope guard (defense-in-depth): the run's tenant identity, owner-sourced. Reads filter on it. */
45
+ scope: string | null;
46
+ toolName: string;
47
+ args: unknown;
48
+ status: ApprovalStatus;
49
+ reason: string | null;
50
+ decidedBy: string | null;
51
+ createdAt: string;
52
+ decidedAt: string | null;
53
+ }
54
+ /** PG translation of the `approval` DDL in tidb-pool.ts SCHEMA_STATEMENTS (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
55
+ * inline KEY→separate CREATE INDEX).
56
+ *
57
+ * SCHEMA POLICY: see the header of pg-pool.ts — the code is the single source of truth, schema changes are
58
+ * drop-and-recreate, and NO new `ALTER TABLE` seams are added here; fold into the CREATE instead. */
59
+ export declare const PG_APPROVAL_SCHEMA: string[];
60
+ /** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
61
+ export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
62
+ /** Dual-dialect durable approval store. See the file header for the dialect-delta ledger. */
63
+ export declare class SqlApprovalStore {
64
+ protected readonly db: SqlDriver;
65
+ constructor(db: SqlDriver);
66
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
67
+ private q;
68
+ /** JSON column binding: TiDB stores the string verbatim; PG goes through the LOSSLESS protocol envelope
69
+ * (codex R4-H2 — the approval `args` is the operator's review surface and must agree with the
70
+ * policy-executed args at the NUL position). */
71
+ private json;
72
+ createPending(id: string, scope: string | null, ctx: {
73
+ taskId?: string | null;
74
+ sessionId?: string | null;
75
+ owner?: string | null;
76
+ toolName: string;
77
+ args: unknown;
78
+ }): Promise<void>;
79
+ getStatus(id: string, scope: string | null): Promise<{
80
+ status: ApprovalStatus;
81
+ reason: string | null;
82
+ decidedBy: string | null;
83
+ } | undefined>;
84
+ /** Apply a terminal decision, but only to a still-pending row (CAS). Returns whether it applied. The scope
85
+ * guard (null-safe) is carried on the operator path too for consistency with the read paths. */
86
+ decide(id: string, scope: string | null, decision: "approved" | "denied" | "expired", reason: string | null, decidedBy: string | null): Promise<boolean>;
87
+ /** Pending approvals (operator queue), scoped to one tenant (single-DB fleet read guard, null-safe), optionally
88
+ * further filtered to one owner. */
89
+ listPending(scope: string | null, owner?: string): Promise<ApprovalRow[]>;
90
+ get(id: string, scope: string | null): Promise<ApprovalRow | undefined>;
91
+ /** Operator-only CROSS-tenant by-id read (no scope filter). The F4 operator queue serves/decides across
92
+ * tenants by design; the HTTP layer gates this to an operator and then re-binds decide() to the row's OWN
93
+ * scope. Tenants NEVER reach this — they use the scope-bound {@link get}. */
94
+ getById(id: string): Promise<ApprovalRow | undefined>;
95
+ /** Operator-only CROSS-tenant pending list (no scope filter) — the unfiltered operator queue across all
96
+ * tenants. Tenants use the scope-bound {@link listPending}; this is HTTP-gated to an operator.
97
+ * 🔴 identical SQL text on both dialects (no placeholders, nothing dialect-specific) — kept as ONE literal
98
+ * rather than a `q()` pair so a reader isn't misled into looking for a divergence that isn't there. */
99
+ listPendingAll(): Promise<ApprovalRow[]>;
100
+ /** Expire pending approvals older than `olderThanMs` (a never-answered operator request). Deliberately GLOBAL
101
+ * (NOT scope-filtered): staleness expiry is fleet-level maintenance that protects the worker regardless of
102
+ * tenant — a stale row in any scope must be reaped. Scope isolation applies to the per-tenant READ paths, not
103
+ * to this maintenance sweep. */
104
+ expireStale(olderThanMs: number): Promise<number>;
105
+ }
106
+ /** approval row → {@link ApprovalRow} — shared by BOTH dialects. design/158 S8 归位 / A12 收编. */
107
+ export declare function mapRow(r: Record<string, unknown>): ApprovalRow;
108
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
109
+ export declare class TiDBApprovalStore extends SqlApprovalStore {
110
+ constructor(pool: MySqlPool);
111
+ }
112
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
113
+ export declare class PgApprovalStore extends SqlApprovalStore {
114
+ constructor(pool: PgPool);
115
+ }
116
+ //# sourceMappingURL=approval-store-sql.d.ts.map
@@ -0,0 +1,151 @@
1
+ import { parseJsonStrict as parseJson, toIsoOrNull as iso } from "./sql-row-helpers.js";
2
+ import { pgProtocolJsonStringify } from "./pg-safe-json.js";
3
+ import { mysqlDriver, pgDriver } from "./sql-driver.js";
4
+ /** PG translation of the `approval` DDL in tidb-pool.ts SCHEMA_STATEMENTS (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
5
+ * inline KEY→separate CREATE INDEX).
6
+ *
7
+ * SCHEMA POLICY: see the header of pg-pool.ts — the code is the single source of truth, schema changes are
8
+ * drop-and-recreate, and NO new `ALTER TABLE` seams are added here; fold into the CREATE instead. */
9
+ export const PG_APPROVAL_SCHEMA = [
10
+ `CREATE TABLE IF NOT EXISTS approval (
11
+ id VARCHAR(64) NOT NULL,
12
+ task_id VARCHAR(64),
13
+ session_id VARCHAR(64),
14
+ owner VARCHAR(190),
15
+ -- Single-DB fleet scope guard (tenant isolation). NULLABLE on purpose: a NULL-scope row is an untenanted
16
+ -- run, and every read/decide path matches it with \`scope IS NOT DISTINCT FROM $n\` (the PG twin of TiDB
17
+ -- \`<=>\`) so NULL matches only NULL and one tenant's scope NEVER matches another's.
18
+ scope VARCHAR(190),
19
+ tool_name VARCHAR(190) NOT NULL,
20
+ args JSONB,
21
+ status VARCHAR(16) NOT NULL,
22
+ reason TEXT,
23
+ decided_by VARCHAR(190),
24
+ created_at TIMESTAMPTZ(3) NOT NULL,
25
+ decided_at TIMESTAMPTZ(3),
26
+ PRIMARY KEY (id)
27
+ )`,
28
+ `CREATE INDEX IF NOT EXISTS idx_approval_owner_status ON approval (owner, status)`,
29
+ `CREATE INDEX IF NOT EXISTS idx_approval_status ON approval (status)`,
30
+ `CREATE INDEX IF NOT EXISTS idx_approval_scope_status ON approval (scope, status)`,
31
+ ];
32
+ /** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
33
+ export async function ensureSchema(pool) {
34
+ for (const stmt of PG_APPROVAL_SCHEMA)
35
+ await pool.query(stmt);
36
+ }
37
+ /** Dual-dialect durable approval store. See the file header for the dialect-delta ledger. */
38
+ export class SqlApprovalStore {
39
+ db;
40
+ constructor(db) {
41
+ this.db = db;
42
+ }
43
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
44
+ q(tidb, pg) {
45
+ return this.db.dialect === "tidb" ? tidb : pg;
46
+ }
47
+ /** JSON column binding: TiDB stores the string verbatim; PG goes through the LOSSLESS protocol envelope
48
+ * (codex R4-H2 — the approval `args` is the operator's review surface and must agree with the
49
+ * policy-executed args at the NUL position). */
50
+ json(value, label) {
51
+ return this.db.dialect === "tidb" ? JSON.stringify(value) : pgProtocolJsonStringify(value, label);
52
+ }
53
+ async createPending(id, scope, ctx) {
54
+ await this.db.query(this.q("INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
55
+ "VALUES (?,?,?,?,?,?,?,'pending',?)", "INSERT INTO approval (id, task_id, session_id, owner, scope, tool_name, args, status, created_at) " +
56
+ "VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb,'pending',$8)"), [
57
+ id,
58
+ ctx.taskId ?? null,
59
+ ctx.sessionId ?? null,
60
+ ctx.owner ?? null,
61
+ scope,
62
+ ctx.toolName,
63
+ ctx.args == null ? null : this.json(ctx.args, "approval args"),
64
+ new Date(),
65
+ ]);
66
+ }
67
+ async getStatus(id, scope) {
68
+ // null-safe scope match (`<=>` / `IS NOT DISTINCT FROM`): a NULL scope row (legacy / untenanted run) is
69
+ // matched by a NULL scope arg, and a tenant's scope NEVER matches another tenant's — the single-DB fleet
70
+ // read guard (defense-in-depth).
71
+ const { rows } = await this.db.query(this.q("SELECT status, reason, decided_by FROM approval WHERE id = ? AND scope <=> ?", "SELECT status, reason, decided_by FROM approval WHERE id = $1 AND scope IS NOT DISTINCT FROM $2"), [id, scope]);
72
+ const r = rows[0];
73
+ if (!r)
74
+ return undefined;
75
+ return { status: r.status, reason: r.reason ?? null, decidedBy: r.decided_by ?? null };
76
+ }
77
+ /** Apply a terminal decision, but only to a still-pending row (CAS). Returns whether it applied. The scope
78
+ * guard (null-safe) is carried on the operator path too for consistency with the read paths. */
79
+ async decide(id, scope, decision, reason, decidedBy) {
80
+ const res = await this.db.query(this.q("UPDATE approval SET status = ?, reason = ?, decided_by = ?, decided_at = ? WHERE id = ? AND scope <=> ? AND status = 'pending'", "UPDATE approval SET status = $1, reason = $2, decided_by = $3, decided_at = $4 WHERE id = $5 AND scope IS NOT DISTINCT FROM $6 AND status = 'pending'"), [decision, reason, decidedBy, new Date(), id, scope]);
81
+ return res.affected === 1;
82
+ }
83
+ /** Pending approvals (operator queue), scoped to one tenant (single-DB fleet read guard, null-safe), optionally
84
+ * further filtered to one owner. */
85
+ async listPending(scope, owner) {
86
+ const { rows } = owner
87
+ ? await this.db.query(this.q("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? AND owner = ? ORDER BY created_at ASC", "SELECT * FROM approval WHERE status = 'pending' AND scope IS NOT DISTINCT FROM $1 AND owner = $2 ORDER BY created_at ASC"), [scope, owner])
88
+ : await this.db.query(this.q("SELECT * FROM approval WHERE status = 'pending' AND scope <=> ? ORDER BY created_at ASC", "SELECT * FROM approval WHERE status = 'pending' AND scope IS NOT DISTINCT FROM $1 ORDER BY created_at ASC"), [scope]);
89
+ return rows.map(mapRow);
90
+ }
91
+ async get(id, scope) {
92
+ const { rows } = await this.db.query(this.q("SELECT * FROM approval WHERE id = ? AND scope <=> ?", "SELECT * FROM approval WHERE id = $1 AND scope IS NOT DISTINCT FROM $2"), [id, scope]);
93
+ return rows[0] ? mapRow(rows[0]) : undefined;
94
+ }
95
+ /** Operator-only CROSS-tenant by-id read (no scope filter). The F4 operator queue serves/decides across
96
+ * tenants by design; the HTTP layer gates this to an operator and then re-binds decide() to the row's OWN
97
+ * scope. Tenants NEVER reach this — they use the scope-bound {@link get}. */
98
+ async getById(id) {
99
+ const { rows } = await this.db.query(this.q("SELECT * FROM approval WHERE id = ?", "SELECT * FROM approval WHERE id = $1"), [id]);
100
+ return rows[0] ? mapRow(rows[0]) : undefined;
101
+ }
102
+ /** Operator-only CROSS-tenant pending list (no scope filter) — the unfiltered operator queue across all
103
+ * tenants. Tenants use the scope-bound {@link listPending}; this is HTTP-gated to an operator.
104
+ * 🔴 identical SQL text on both dialects (no placeholders, nothing dialect-specific) — kept as ONE literal
105
+ * rather than a `q()` pair so a reader isn't misled into looking for a divergence that isn't there. */
106
+ async listPendingAll() {
107
+ const { rows } = await this.db.query("SELECT * FROM approval WHERE status = 'pending' ORDER BY created_at ASC");
108
+ return rows.map(mapRow);
109
+ }
110
+ /** Expire pending approvals older than `olderThanMs` (a never-answered operator request). Deliberately GLOBAL
111
+ * (NOT scope-filtered): staleness expiry is fleet-level maintenance that protects the worker regardless of
112
+ * tenant — a stale row in any scope must be reaped. Scope isolation applies to the per-tenant READ paths, not
113
+ * to this maintenance sweep. */
114
+ async expireStale(olderThanMs) {
115
+ const cutoff = new Date(Date.now() - olderThanMs);
116
+ const res = await this.db.query(this.q("UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = ? " +
117
+ "WHERE status = 'pending' AND created_at < ?", "UPDATE approval SET status = 'expired', reason = 'timed out before decision', decided_at = $1 " +
118
+ "WHERE status = 'pending' AND created_at < $2"), [new Date(), cutoff]);
119
+ return res.affected;
120
+ }
121
+ }
122
+ /** approval row → {@link ApprovalRow} — shared by BOTH dialects. design/158 S8 归位 / A12 收编. */
123
+ export function mapRow(r) {
124
+ return {
125
+ id: String(r.id),
126
+ taskId: r.task_id ?? null,
127
+ sessionId: r.session_id ?? null,
128
+ owner: r.owner ?? null,
129
+ scope: r.scope ?? null,
130
+ toolName: String(r.tool_name),
131
+ args: parseJson(r.args),
132
+ status: r.status,
133
+ reason: r.reason ?? null,
134
+ decidedBy: r.decided_by ?? null,
135
+ createdAt: iso(r.created_at),
136
+ decidedAt: iso(r.decided_at),
137
+ };
138
+ }
139
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
140
+ export class TiDBApprovalStore extends SqlApprovalStore {
141
+ constructor(pool) {
142
+ super(mysqlDriver(pool));
143
+ }
144
+ }
145
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
146
+ export class PgApprovalStore extends SqlApprovalStore {
147
+ constructor(pool) {
148
+ super(pgDriver(pool));
149
+ }
150
+ }
151
+ //# sourceMappingURL=approval-store-sql.js.map
@@ -9,7 +9,7 @@ const SUMMARY_COLS = "handle, owner, session_scoped, session_id, parent_session_
9
9
  * 类型签名逐字核对过),只是 record_json 全量对象序列化里随手带上的普通字段,不需要专用列——
10
10
  * record_json 的写法是 `json(stored)`(stored = `{...record, handle, scope, rev}` 整个对象展开),
11
11
  * 任何 record 上有的字段(含 writerEpoch)天然随之落盘/读回,无需逐字段维护投影列表。 */
12
- const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, record_json";
12
+ const PROJ_COLS = "scope, owner, session_scoped, session_id, parent_session_id, root_session_id, session_anchor, name, agent_type, status, spawned_at_ms, updated_at_ms, settled_at_ms, usage_json, rev, parked_checkpoint_token, park_claim_id, admitted_org_scopes, admitted_org_write_scope, record_json";
13
13
  const PROJ_N = PROJ_COLS.split(", ").length; // F5: WHERE 占位符序号从列数派生,列序变更单点同步
14
14
  function assertScope(record) {
15
15
  if (record.scope === undefined || record.scope === "") {
@@ -43,6 +43,16 @@ function projParams(stored, json, text) {
43
43
  stored.rev,
44
44
  stored.parkedCheckpointToken ?? null,
45
45
  stored.parkClaimId ?? null,
46
+ // core 5.14.0 #22([2854] 提货单⑤):每条腿在 injector-ready 栅上写下自己被裁定的 org 准入结果。
47
+ // 🔴 三态必须区分,这是 core 契约试剂盒硬钉的那条:**缺席(undefined)≠ 空数组**。
48
+ // · 字段缺席 ⇒ 列 NULL(「没有记录」)。老行免回填,读空即零准入 fail-closed。
49
+ // · `[]` / `null` ⇒ 列上是 JSON "[]" / 真 NULL 写域 —— 「裁定过,结果是什么都没有」。
50
+ // 两列是**写侧投影**(真源恒是 record_json,`get()` 只读它)——语义上不可能漂移:缺席的键根本
51
+ // 不进 JSON.stringify,所以「缺席读回缺席」由真源结构保证,不靠这两列。
52
+ // 📌 记账(诚实清单):本仓今天**没有**读这两列的消费者;它们存在是为了让 org 治理审计能按列
53
+ // 查询(「哪些后台子代拿到过 org:acme 的写权」),不必全表扫 JSON。谁要接,先上黑板对表。
54
+ stored.admittedOrgScopes !== undefined ? json(stored.admittedOrgScopes) : null,
55
+ stored.admittedOrgWriteScope !== undefined ? stored.admittedOrgWriteScope : null,
46
56
  json(stored),
47
57
  ];
48
58
  }
@@ -171,6 +181,12 @@ export async function ensureTiDBBackgroundAgentSchema(pool) {
171
181
  -- 行状态原子求值,不能只靠 record_json blob(那要求先读后写,读写之间就是 CAS 本要堵的竞态窗)。
172
182
  parked_checkpoint_token VARCHAR(190) NULL,
173
183
  park_claim_id VARCHAR(190) NULL,
184
+ -- core 5.14.0 #22:该腿被裁定的 org 准入结果(写侧投影,真源恒是 record_json)。
185
+ -- NULL = 字段缺席(「没有记录」,老行天然如此,免回填);JSON "[]" = 裁定过、结果为空。
186
+ -- admitted_org_write_scope 的 NULL 兼表「缺席」与「裁定为无写域」——两者在 core 侧都落 fail-closed
187
+ -- 的零写权,列上不必再分,真源(record_json)里仍然分得清 undefined 与 null。
188
+ admitted_org_scopes TEXT NULL,
189
+ admitted_org_write_scope VARCHAR(190) NULL,
174
190
  record_json LONGTEXT NOT NULL,
175
191
  PRIMARY KEY (scope_key, handle),
176
192
  KEY idx_bga_anchor (scope_key, session_anchor, spawned_at_ms),
@@ -201,6 +217,9 @@ export async function ensurePgBackgroundAgentSchema(q) {
201
217
  -- core 1.383([1565] / design/153 件3a)ε(TiDB 同案注):updateIf 的 CAS 守卫专用列
202
218
  parked_checkpoint_token VARCHAR(190),
203
219
  park_claim_id VARCHAR(190),
220
+ -- core 5.14.0 #22(TiDB 同案注:三态与「写侧投影」口径见 MySQL 孪生的行内注)。
221
+ admitted_org_scopes TEXT,
222
+ admitted_org_write_scope VARCHAR(190),
204
223
  record_json TEXT NOT NULL,
205
224
  PRIMARY KEY (scope_key, handle)
206
225
  )`);
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MySqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import { type Checkpoint, type CheckpointGate, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
3
+ import { type Checkpoint, type CheckpointGate, type CheckpointState, type CheckpointStore, type CheckpointSummary, type CheckpointToken, type PendingSteerInput, type ResumeOutcome, type ResolveExpectation, type ReopenReason, type RiskDescriptor } from "@sema-agent/core";
4
4
  import { type SqlDriver } from "./sql-driver.js";
5
5
  /**
6
6
  * design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
@@ -67,6 +67,42 @@ export interface PendingCheckpoint {
67
67
  * matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
68
68
  * Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */
69
69
  export declare function boundedToolInput(args: unknown): unknown;
70
+ /**
71
+ * 🔴 #151 车5 §9 C4:`findCheckpointCandidatesForAsk` 的 typed 候选行。刻意只有对账收敛器判据 1 真需要的
72
+ * 五件——**没有 token 之外的任何凭据面**,`token` 本身是 resume 凭据、只在服务端内部流转(同
73
+ * `findPendingTokenBySession`:它从不出网,§12-C「checkpointToken 从不外发」)。
74
+ */
75
+ export interface CheckpointAskCandidate {
76
+ token: string;
77
+ /** `checkpoint.status` 列(权威;`pending` | `resolved` | `expired`)。不做过滤,由收敛器判活性。 */
78
+ status: string;
79
+ createdAtMs: number;
80
+ /** 该 park 绑定的 tool call(= `pendingAction.toolCallId`)。`unparseable` 行为 null。 */
81
+ boundCallId: string | null;
82
+ /** 服务端铸的入参摘要——判据 1 的第二道硬等式(与 `approval_asks.bound_input_hash` 相等才算命中)。 */
83
+ boundInputHash: string | null;
84
+ /** 在场 ⇒ 这行读不出(blob 版本超前 / JSON 坏)。收敛器视同不匹配;**从不是** false,缺席即可读。 */
85
+ unparseable?: true;
86
+ }
87
+ /** `pending_steer` 列承载的两个 CheckpointState 字段(= core `appendPendingSteer` / `readPendingSteerQueue`
88
+ * 的入参形)。列是**唯一**权威(suspend 时写的 blob 从不带它们)。 */
89
+ type SteerColumnState = Pick<CheckpointState, "pendingSteer" | "pendingSteerQueue">;
90
+ /** 读列结果。`lossy` = 列里有东西但**没能完整读出来**(JSON 坏 / 条目形不认)。两个调用点对它的处置
91
+ * 刻意相反,见各自的旁注:`get()` 容忍(坏列不许把一条挂起的 run 变成不可 resume),`setPendingSteer`
92
+ * fail-loud(往一份读不全的队列上追加 = 把别人 park 的指令悄悄写没,正是 #147 要消灭的那件事)。 */
93
+ interface SteerColumnRead {
94
+ state: SteerColumnState;
95
+ lossy: boolean;
96
+ }
97
+ /**
98
+ * 两个 steering 列 → `{pendingSteer?, pendingSteerQueue?}`(= core `appendPendingSteer`/`readPendingSteerQueue`
99
+ * 的入参形)。**两列各管一形**,不是一列两形:
100
+ * · `pending_steer`(旧列,本服务只读不写)—— 裸单座 `{text, trusted}`。来源有二:队列化之前的存量行,
101
+ * 以及**滚动升级窗里仍在跑的旧副本**写下的座。core 的 `readPendingSteerQueue` 把它折成 member 0。
102
+ * · `pending_steer_queue`(新列)—— `PendingSteerEntry[]`。
103
+ * 任一列有内容却读不出 ⇒ `lossy`(两个调用点对它的处置见 {@link SteerColumnRead})。
104
+ */
105
+ export declare function readPendingSteerColumns(seatRaw: unknown, queueRaw: unknown): SteerColumnRead;
70
106
  /** Dual-dialect durable CheckpointStore. See the file header for the dialect-delta ledger. */
71
107
  export declare class SqlCheckpointStore implements CheckpointStore {
72
108
  protected readonly db: SqlDriver;
@@ -123,15 +159,30 @@ export declare class SqlCheckpointStore implements CheckpointStore {
123
159
  */
124
160
  reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
125
161
  /**
126
- * design/80 D-A (durable steering): park a steer message on a STILL-PENDING checkpoint (last-writer-wins),
127
- * surfaced via get() as `state.pendingSteer` and injected on resume. CAS on `status='pending'` (+ scope) so a
128
- * steer NEVER lands on a resolved/expired checkpoint and NEVER touches status/the resolve path (§3 inv #4).
129
- * Returns the CAS winner (`true` = landed on a live pending checkpoint; `false` = no pending row to steer).
162
+ * design/80 D-A (durable steering) —— **core 5.14.0(#147 BREAKING)起是「追加进一条有界有序队列」**,
163
+ * 不再是「盖掉单座」。两个人同时给一条挂起的 run 转向,旧语义会让第二条**静默销毁**第一条(一条没人
164
+ * 知道丢了的操作员指令);新语义是两条都在,resume `seq` 全数投递。
165
+ *
166
+ * 语义与边界**全部铸在 core**,本 store 一个都不自铸(契约原文:`appendPendingSteer` is "the shared
167
+ * append every backend must build its next queue with: bounds + `seq` mint live there, not once per store"):
168
+ * · `validatePendingSteer` —— 每个 impl 内部必跑的 fail-closed 不变式(M2,接入审计 2026-07-08):内容
169
+ * 越狱/控制字符 `steering.invalid_content`;**未知字段现在是 REFUSE 而不是丢弃**(领先一个版本的
170
+ * producer 必须知道自己的新字段活不过这条挂起的腿);缺席的 `inputId` 在这里铸(uuidv7)。
171
+ * · `appendPendingSteer` —— 铸 `seq`、判 `inputId` 幂等(同 id 同载荷 = no-op 返回 true;同 id **不同**
172
+ * 载荷 = `steering.duplicate_input_id`,吞掉它就等于把队列要消灭的那种静默丢失原样请回来)、
173
+ * 执行两条上界(条数 + 48000 字节整信封),越界一律 `steering.queue_full` fail-loud,**绝不驱逐**
174
+ * 一条已被接受的指令。
175
+ *
176
+ * 🔴 从「一条无条件 UPDATE」变成「读-改-写」,中间那道窗必须自己有 CAS:两个并发 steer 读到同一份
177
+ * 队列、后写者整个盖掉前者 —— 就是 #147 要消灭的静默丢失换了个位置复发。守卫是 `pending_steer_rev`
178
+ * **专用整数列**(不是拿旧列值比:本表无显式 COLLATE,*_ci 排序规则会把只差大小写的两份信封判等 ⇒
179
+ * CAS 假命中;也不是复用 resolve 的 `rev`:bump 它会让并发 resume 的 `resolve(expect)` 白白落败)。
180
+ * CAS 输了就重读重算(队列只有 3 个位子,争用者天然极少);给足冗余仍全败 ⇒ **抛**,不返回 false ——
181
+ * `false` 在调用方的语义是「没有 pending 行可转向」(HTTP 409「已不再挂起」),把争用报成那个是撒谎。
182
+ *
183
+ * 返回:`true` = 落在一条活的 pending 行上(含幂等重放);`false` = 没有该 (token,scope) 的 pending 行。
130
184
  */
131
- setPendingSteer(token: CheckpointToken, scope: string, steer: {
132
- text: string;
133
- trusted: boolean;
134
- }): Promise<boolean>;
185
+ setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
135
186
  /**
136
187
  * CAS-expire a SINGLE checkpoint by token (core 1.71 `CheckpointStore.expire`, for `TaskStream.destroy`'s
137
188
  * suspended-worker reap — core [R33] {delta}). `pending → expired`, returns the CAS winner (true = we
@@ -199,6 +250,29 @@ export declare class SqlCheckpointStore implements CheckpointStore {
199
250
  * can only resolve within its own tenant. Returns null if there is no pending checkpoint for that session/scope.
200
251
  */
201
252
  findPendingTokenBySession(sessionId: string, scope?: string): Promise<CheckpointToken | null>;
253
+ /**
254
+ * 🔴 #151 车5 §9 C4 —— 流内审批对账收敛器判据 1 的**窄读口**(店面承载;收敛器本体不在本车)。
255
+ *
256
+ * 「这条 PARKING 的 ask 究竟 park 成了哪张 checkpoint?」的唯一读法。为什么不是「按 session 翻历史页」:
257
+ * 分页宽读会漏匹配,而漏匹配在收敛器那侧的后果是**假阴性 ⇒ 落一条不可逆的 DENIED**。所以这里改成
258
+ * 谓词精确查——`(scope, session_id, tool_call_id, created_at ≥ sinceMs)` 这组条件下的行数天然极小,
259
+ * 一次全量返回,结构上没有分页假阴性。
260
+ *
261
+ * 三条口径,逐条都是判据:
262
+ * - **`scope` 必填**(不是 optional):对账是跨腿读,租户门是读口自己的责任,不靠调用方记得过滤
263
+ * (同 `listPendingBySession` 姿势;`resolve`/`reap` 的 scope enforced 同族)。
264
+ * - **不按 status 过滤**:已 `resolved`/`expired` 的行照样返回。后到的 resume 会把行翻成 resolved,
265
+ * 若只看 pending 就会把「park 成功过」这个既成事实读成「没 park 过」⇒ 又是一条假阴性 DENIED
266
+ * (对账三约束③)。park 成功是事实,gate 的活性由 `status` 表达,交给收敛器自己判。
267
+ * - **单行读不出 ⇒ 标记返回,不抛**:`unparseable` 的行由收敛器视同「不匹配」,一条坏行不许打断整段
268
+ * 扫描(§8 C-6)。什么算读不出:blob 的 `version` 超出本 build 支持(`get()` 那条前向兼容门在这里
269
+ * 不能 throw,否则一条超前行会让整个 session 的对账永久卡死)、或 blob JSON 坏。
270
+ *
271
+ * 匹配是两段的:`tool_call_id` **列**是 `put()` 从 `pendingAction.toolCallId` 盖下来的权威投影,命中它
272
+ * 的行零解析即候选;列为 NULL 的行(无工具动作的 park,或列存在之前的旧行)才回落去解 blob——解得出且
273
+ * 相等才算候选,解不出就标 `unparseable`。
274
+ */
275
+ findCheckpointCandidatesForAsk(scope: string, sessionId: string, toolCallId: string, sinceMs: number): Promise<CheckpointAskCandidate[]>;
202
276
  /**
203
277
  * The owner SCOPE of a session's pending checkpoint (the multi-tenant key === the owner principal in the
204
278
  * BFF/non-operator flow, the same key listPending filters by). For the /decide owner-gate: a non-operator
@@ -251,4 +325,5 @@ export declare class PgCheckpointStore extends SqlCheckpointStore {
251
325
  info?(msg: string, meta?: unknown): void;
252
326
  });
253
327
  }
328
+ export {};
254
329
  //# sourceMappingURL=checkpoint-store-sql.d.ts.map