@sema-agent/server 7.93.4 → 7.93.6
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/bake-runner/main.d.ts +28 -1
- package/dist/bake-runner/main.js +122 -38
- package/dist/bake-runner/runner.d.ts +85 -7
- package/dist/bake-runner/runner.js +116 -27
- package/dist/boot/exit-boundary.d.ts +22 -0
- package/dist/boot/exit-boundary.js +73 -0
- package/dist/boot/shutdown.d.ts +2 -2
- package/dist/boot/shutdown.js +5 -62
- package/dist/hooks/hook-runner.js +5 -1
- package/dist/observability/fail-open.d.ts +16 -0
- package/dist/observability/fail-open.js +16 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/approval-ask-store-sql.js +36 -19
- package/dist/plugins/background-agent-store-sql.js +36 -16
- package/dist/plugins/checkpoint-store-sql.d.ts +5 -1
- package/dist/plugins/checkpoint-store-sql.js +111 -66
- package/dist/plugins/device-store-sql.d.ts +10 -0
- package/dist/plugins/device-store-sql.js +12 -6
- package/dist/plugins/host-platform.d.ts +70 -6
- package/dist/plugins/host-platform.js +14 -3
- package/dist/plugins/leader-run-store-sql.js +16 -6
- package/dist/plugins/one-time-migrations.d.ts +3 -0
- package/dist/plugins/one-time-migrations.js +16 -4
- package/dist/plugins/permission-rule-store-sql.d.ts +1 -0
- package/dist/plugins/permission-rule-store-sql.js +37 -28
- package/dist/plugins/remote-env-host.js +9 -21
- package/dist/plugins/run-store-sql.d.ts +1 -1
- package/dist/plugins/run-store-sql.js +41 -10
- package/dist/plugins/session-policy-store-sql.d.ts +24 -0
- package/dist/plugins/session-policy-store-sql.js +17 -18
- package/dist/plugins/sql-json-column.d.ts +177 -0
- package/dist/plugins/sql-json-column.js +74 -0
- package/dist/plugins/task-list-store-sql.d.ts +13 -1
- package/dist/plugins/task-list-store-sql.js +23 -10
- package/dist/plugins/workflow-run-store-sql.js +14 -4
- package/dist/run-local.d.ts +15 -0
- package/dist/run-local.js +32 -0
- package/package.json +1 -1
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
|
|
2
2
|
import { acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
|
|
3
|
+
import { jsonColumnTextExpr } from "./sql-json-column.js";
|
|
3
4
|
export const NO_SQL_MIGRATIONS = {
|
|
4
5
|
retiredSnapshotTable: "absent",
|
|
5
6
|
retiredSnapshotRows: 0,
|
|
6
|
-
retiredSteerSeatColumn: "absent",
|
|
7
7
|
retiredSteerSeatStaleRows: 0,
|
|
8
|
+
retiredSteerSeatColumn: "absent",
|
|
9
|
+
auditDetailJsonNullRows: 0,
|
|
8
10
|
};
|
|
9
11
|
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
10
12
|
const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
|
|
11
13
|
const DELIVERABLE_STEER_SEAT_WHERE = "(status = 'pending' OR (status = 'resolved' AND execution_outcome IS NULL))";
|
|
12
14
|
const RETIRED_ASK_ORIGIN = { from: "unresolvable", to: "ancestor_marked" };
|
|
13
15
|
const GATE_BEARING_LEDGER_EVENT_TYPE = "tool_end";
|
|
16
|
+
const AUDIT_TABLE = "device_audit";
|
|
17
|
+
const AUDIT_DETAIL_COLUMN = "detail";
|
|
14
18
|
export class RetiredSteerSeatError extends Error {
|
|
15
19
|
rows;
|
|
16
20
|
code = "checkpoint.pending_steer_seat_retired";
|
|
@@ -130,6 +134,11 @@ async function migrateOn(db, dialect) {
|
|
|
130
134
|
`WHERE type = '${GATE_BEARING_LEDGER_EVENT_TYPE}' AND data->'gate'->>'origin' = '${RETIRED_ASK_ORIGIN.from}'`)) +
|
|
131
135
|
(await db.exec(`UPDATE checkpoint SET execution_outcome = jsonb_set(execution_outcome, '{origin}', '"${RETIRED_ASK_ORIGIN.to}"') ` +
|
|
132
136
|
`WHERE execution_outcome->>'origin' = '${RETIRED_ASK_ORIGIN.from}'`));
|
|
137
|
+
const auditTablePresent = await tableExists(db, dialect, AUDIT_TABLE);
|
|
138
|
+
const auditDetailJsonNullRows = !auditTablePresent
|
|
139
|
+
? 0
|
|
140
|
+
: await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
|
|
141
|
+
`WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
|
|
133
142
|
let retiredSnapshotTable = "absent";
|
|
134
143
|
let retiredSnapshotRows = 0;
|
|
135
144
|
let retiredSteerSeatColumn = "absent";
|
|
@@ -160,7 +169,7 @@ async function migrateOn(db, dialect) {
|
|
|
160
169
|
throw new Error(`one-time migration failed AFTER completing destructive steps — record these before retrying, a retry cannot ` +
|
|
161
170
|
`report them again: ${destroyed.join("; ")}. Underlying failure: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
162
171
|
}
|
|
163
|
-
return { retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated };
|
|
172
|
+
return { retiredSnapshotTable, retiredSnapshotRows, retiredSteerSeatColumn, retiredSteerSeatStaleRows, askOriginRowsMigrated, auditDetailJsonNullRows };
|
|
164
173
|
}
|
|
165
174
|
export function migrateRetiredAskOrigin(type, data) {
|
|
166
175
|
if (type !== GATE_BEARING_LEDGER_EVENT_TYPE)
|
|
@@ -177,7 +186,7 @@ export function migrateRetiredAskOrigin(type, data) {
|
|
|
177
186
|
return { ...d, gate: { ...g, origin: RETIRED_ASK_ORIGIN.to } };
|
|
178
187
|
}
|
|
179
188
|
export function logOneTimeMigrations(log, backend, report) {
|
|
180
|
-
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0;
|
|
189
|
+
const changed = report.retiredSnapshotTable === "dropped" || report.retiredSteerSeatColumn === "dropped" || report.askOriginRowsMigrated > 0 || report.auditDetailJsonNullRows > 0;
|
|
181
190
|
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
182
191
|
backend,
|
|
183
192
|
retiredSnapshotTable: report.retiredSnapshotTable,
|
|
@@ -185,8 +194,11 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
185
194
|
retiredSteerSeatColumn: report.retiredSteerSeatColumn,
|
|
186
195
|
retiredSteerSeatStaleRows: report.retiredSteerSeatStaleRows,
|
|
187
196
|
askOriginRowsMigrated: report.askOriginRowsMigrated,
|
|
197
|
+
auditDetailJsonNullRows: report.auditDetailJsonNullRows,
|
|
188
198
|
note: "one-time persisted-data migrations for this version's retirements (retired whole-tree snapshot index table " +
|
|
189
|
-
"dropped; retired single-seat steering column dropped; retired ask-origin spelling rewritten in place
|
|
199
|
+
"dropped; retired single-seat steering column dropped; retired ask-origin spelling rewritten in place; audit " +
|
|
200
|
+
"details written as the JSON text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly " +
|
|
201
|
+
"one on-disk spelling). This " +
|
|
190
202
|
"whole migration step is removed in the next minor — start this version at least once before upgrading past it.",
|
|
191
203
|
});
|
|
192
204
|
}
|
|
@@ -157,6 +157,7 @@ export declare class SqlRuleApprovalRecordStore implements RuleApprovalRecordSto
|
|
|
157
157
|
private q;
|
|
158
158
|
private enc;
|
|
159
159
|
get(id: string): Promise<RuleApprovalRecord | StaleRuleApprovalRecord | undefined>;
|
|
160
|
+
private readApprovalRow;
|
|
160
161
|
create(record: RuleApprovalRecord): Promise<void>;
|
|
161
162
|
/**
|
|
162
163
|
* 丢弃一条**从未被确认**的记录(codex 交叉复审 round8 [medium],可控半场)。
|
|
@@ -6,6 +6,7 @@ import { isMissingColumnError } from "./sql-errors.js";
|
|
|
6
6
|
import { canonicalJson } from "../canonical-json.js";
|
|
7
7
|
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, isPersistedRuleTool, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
8
8
|
import { ensureIndexes } from "./ensure-index.js";
|
|
9
|
+
import { createZodColumnReader, decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn } from "./sql-json-column.js";
|
|
9
10
|
export function writerOfSqlRuleStore(store) {
|
|
10
11
|
const w = Reflect.get(store, PERMISSION_RULE_WRITER);
|
|
11
12
|
if (w === null || typeof w !== "object")
|
|
@@ -211,7 +212,7 @@ export async function ensureTiDBPermissionRuleSchema(pool) {
|
|
|
211
212
|
}
|
|
212
213
|
export async function assertPermissionRuleApprovalSchema(query, dialect) {
|
|
213
214
|
try {
|
|
214
|
-
await query(`SELECT command, edited_json, version, offers_json, selected_offer FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE 1=0`);
|
|
215
|
+
await query(`SELECT command, ${jsonColumnText(dialect, "edited_json")}, version, ${jsonColumnText(dialect, "offers_json")}, selected_offer FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE 1=0`);
|
|
215
216
|
}
|
|
216
217
|
catch (err) {
|
|
217
218
|
if (!isMissingColumnError(err, dialect))
|
|
@@ -231,7 +232,7 @@ export async function assertPermissionRuleApprovalSchema(query, dialect) {
|
|
|
231
232
|
}
|
|
232
233
|
export async function assertPermissionRuleTicketSchema(query, dialect) {
|
|
233
234
|
try {
|
|
234
|
-
await query(`SELECT redeemed_outcome, claim_id, purpose, settled_at_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE 1=0`);
|
|
235
|
+
await query(`SELECT ${jsonColumnText(dialect, "redeemed_outcome")}, claim_id, purpose, settled_at_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE 1=0`);
|
|
235
236
|
}
|
|
236
237
|
catch (err) {
|
|
237
238
|
if (!isMissingColumnError(err, dialect))
|
|
@@ -332,21 +333,21 @@ export function buildRuleBucketChecksum(body) {
|
|
|
332
333
|
}
|
|
333
334
|
const NEXT_DOT_MAX_ATTEMPTS = 8;
|
|
334
335
|
const EMPTY_BUCKET = { actor: "", counter: 0, rev: 0, rules: [], tombstones: [], quarantined: [] };
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
const readRulesJson = createZodColumnReader(z.array(PersistedRuleRowSchema));
|
|
337
|
+
const readTombstonesJson = createZodColumnReader(z.array(RuleTombstoneSchema));
|
|
338
|
+
const readQuarantinedJson = createZodColumnReader(z.array(QuarantinedRuleAddRowSchema));
|
|
339
|
+
const readObservedVectorJson = createZodColumnReader(FrontierSchema);
|
|
340
|
+
const readCandidatesJson = createZodColumnReader(z.array(RuleCandidateSchema));
|
|
341
|
+
const readOffersJson = createZodColumnReader(z.array(RuleOffer2Schema));
|
|
342
|
+
const readRedeemedDotsJson = createZodColumnReader(RedeemedDotsSchema);
|
|
343
|
+
const readEditedJson = createZodColumnReader(RuleEditedSchema);
|
|
344
|
+
async function refusingUnreadableColumns(what, fn) {
|
|
340
345
|
try {
|
|
341
|
-
|
|
346
|
+
return await fn();
|
|
342
347
|
}
|
|
343
|
-
catch (
|
|
344
|
-
throw new Error(
|
|
348
|
+
catch (e) {
|
|
349
|
+
throw foldJsonColumn(e, (m) => new Error(`${m} — the ${what} does not carry a readable shape; refusing the whole of it rather than report a partial one`));
|
|
345
350
|
}
|
|
346
|
-
const parsed = schema.safeParse(json);
|
|
347
|
-
if (!parsed.success)
|
|
348
|
-
throw new Error(`permission_rule.${column} for bucket ${ownerKey} does not carry a readable shape: ${parsed.error.message}`);
|
|
349
|
-
return parsed.data;
|
|
350
351
|
}
|
|
351
352
|
class SqlDurableRulePartition {
|
|
352
353
|
db;
|
|
@@ -368,14 +369,18 @@ class SqlDurableRulePartition {
|
|
|
368
369
|
return this.db.dialect === "tidb" ? tidbSql : pgSql;
|
|
369
370
|
}
|
|
370
371
|
async read() {
|
|
371
|
-
|
|
372
|
+
return refusingUnreadableColumns(`bucket ${this.ownerKey}`, () => this.readBucketRow());
|
|
373
|
+
}
|
|
374
|
+
async readBucketRow() {
|
|
375
|
+
const { rows } = await this.db.query(this.q(`SELECT actor, counter, rev, ${jsonColumnText("tidb", "rules_json")}, ${jsonColumnText("tidb", "tombstones_json")}, ${jsonColumnText("tidb", "quarantined_json")}, ${jsonColumnText("tidb", "observed_vector_json")}, checksum FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = ?`, `SELECT actor, counter, rev, ${jsonColumnText("pg", "rules_json")}, ${jsonColumnText("pg", "tombstones_json")}, ${jsonColumnText("pg", "quarantined_json")}, ${jsonColumnText("pg", "observed_vector_json")}, checksum FROM ${PERMISSION_RULE_TABLE} WHERE owner_key = $1`), [this.ownerKey]);
|
|
372
376
|
const row = rows[0];
|
|
373
377
|
if (row === undefined)
|
|
374
378
|
return { ...EMPTY_BUCKET, rules: [], tombstones: [], quarantined: [] };
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
const
|
|
379
|
+
const at = `${PERMISSION_RULE_TABLE} bucket ${this.ownerKey}`;
|
|
380
|
+
const rulesRaw = decodeJsonColumn(row.rules_json, readRulesJson, `${at}.rules_json`);
|
|
381
|
+
const tombstonesRaw = decodeJsonColumn(row.tombstones_json, readTombstonesJson, `${at}.tombstones_json`);
|
|
382
|
+
const quarantinedRaw = decodeJsonColumn(row.quarantined_json, readQuarantinedJson, `${at}.quarantined_json`);
|
|
383
|
+
const observedVector = row.observed_vector_json === null || row.observed_vector_json === undefined ? undefined : decodeJsonColumn(row.observed_vector_json, readObservedVectorJson, `${at}.observed_vector_json`);
|
|
379
384
|
const expected = buildRuleBucketChecksum({ rules: rulesRaw, tombstones: tombstonesRaw, quarantined: quarantinedRaw, ...(observedVector !== undefined ? { observedVector } : {}) });
|
|
380
385
|
if (String(row.checksum ?? "") !== expected) {
|
|
381
386
|
throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
|
|
@@ -548,7 +553,10 @@ export class SqlRuleApprovalRecordStore {
|
|
|
548
553
|
return dialectProtocolJsonEncoder(this.db.dialect)(v, label);
|
|
549
554
|
}
|
|
550
555
|
async get(id) {
|
|
551
|
-
|
|
556
|
+
return refusingUnreadableColumns(`approval record ${id}`, () => this.readApprovalRow(id));
|
|
557
|
+
}
|
|
558
|
+
async readApprovalRow(id) {
|
|
559
|
+
const { rows } = await this.db.query(this.q(`SELECT record_id, owner_kind, principal, kind, state, version, rev, ${jsonColumnText("tidb", "candidates_json")}, ${jsonColumnText("tidb", "offers_json")}, selected_offer, ${jsonColumnText("tidb", "redeemed_dots_json")}, tool_call_id, bound_input_hash, command, ${jsonColumnText("tidb", "edited_json")}, created_at_iso FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = ?`, `SELECT record_id, owner_kind, principal, kind, state, version, rev, ${jsonColumnText("pg", "candidates_json")}, ${jsonColumnText("pg", "offers_json")}, selected_offer, ${jsonColumnText("pg", "redeemed_dots_json")}, tool_call_id, bound_input_hash, command, ${jsonColumnText("pg", "edited_json")}, created_at_iso FROM ${PERMISSION_RULE_APPROVAL_TABLE} WHERE record_id = $1`), [id]);
|
|
552
560
|
const row = rows[0];
|
|
553
561
|
if (row === undefined)
|
|
554
562
|
return undefined;
|
|
@@ -558,10 +566,11 @@ export class SqlRuleApprovalRecordStore {
|
|
|
558
566
|
if (Number(row.version) !== RULE_APPROVAL_RECORD_SCHEMA) {
|
|
559
567
|
return { staleSchema: true, id: String(row.record_id), ...identity };
|
|
560
568
|
}
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
const
|
|
569
|
+
const at = `${PERMISSION_RULE_APPROVAL_TABLE} row ${id}`;
|
|
570
|
+
const candidates = decodeJsonColumn(row.candidates_json, readCandidatesJson, `${at}.candidates_json`);
|
|
571
|
+
const offers = decodeJsonColumn(row.offers_json, readOffersJson, `${at}.offers_json`);
|
|
572
|
+
const dotsRaw = row.redeemed_dots_json === null || row.redeemed_dots_json === undefined ? undefined : decodeJsonColumn(row.redeemed_dots_json, readRedeemedDotsJson, `${at}.redeemed_dots_json`);
|
|
573
|
+
const edited = row.edited_json === null || row.edited_json === undefined ? undefined : decodeJsonColumn(row.edited_json, readEditedJson, `${at}.edited_json`);
|
|
565
574
|
const kind = z.enum(["card", "import", "starter"]).safeParse(row.kind);
|
|
566
575
|
const state = z.enum(["pending", "approved", "redeemed"]).safeParse(row.state);
|
|
567
576
|
if (!kind.success || !state.success) {
|
|
@@ -716,10 +725,10 @@ export class SqlRuleImportTicketStore {
|
|
|
716
725
|
if (row.redeemedOutcome !== undefined) {
|
|
717
726
|
let outcome;
|
|
718
727
|
try {
|
|
719
|
-
outcome =
|
|
728
|
+
outcome = decodeJsonColumn(row.redeemedOutcome, readJsonObjectColumn, `${PERMISSION_RULE_TICKET_TABLE}.redeemed_outcome for ticket ${ticketId}`);
|
|
720
729
|
}
|
|
721
|
-
catch (
|
|
722
|
-
throw new Error(`${
|
|
730
|
+
catch (e) {
|
|
731
|
+
throw foldJsonColumn(e, (m) => new Error(`${m} — refusing rather than read a settled ticket as still open (which re-runs a redemption that already ran)`));
|
|
723
732
|
}
|
|
724
733
|
return { state: "settled", outcome };
|
|
725
734
|
}
|
|
@@ -738,7 +747,7 @@ export class SqlRuleImportTicketStore {
|
|
|
738
747
|
return affected === 1 ? { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId } : { ok: false };
|
|
739
748
|
}
|
|
740
749
|
async readRow(ticketId) {
|
|
741
|
-
const { rows } = await this.db.query(this.q(`SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, redeemed_outcome, ${SQL_DB_NOW_MS.tidb} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = ?`, `SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, redeemed_outcome, ${SQL_DB_NOW_MS.pg} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = $1`), [ticketId]);
|
|
750
|
+
const { rows } = await this.db.query(this.q(`SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, ${jsonColumnText("tidb", "redeemed_outcome")}, ${SQL_DB_NOW_MS.tidb} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = ?`, `SELECT owner_key, approval_id, payload_hash, purpose, expires_at_ms, consumed_at_ms, claim_id, ${jsonColumnText("pg", "redeemed_outcome")}, ${SQL_DB_NOW_MS.pg} AS db_now_ms FROM ${PERMISSION_RULE_TICKET_TABLE} WHERE ticket_id = $1`), [ticketId]);
|
|
742
751
|
const row = rows[0];
|
|
743
752
|
if (row === undefined)
|
|
744
753
|
return undefined;
|
|
@@ -7,7 +7,7 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
|
|
|
7
7
|
import { StringDecoder } from "node:string_decoder";
|
|
8
8
|
import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
|
|
9
9
|
import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
|
|
10
|
-
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys,
|
|
10
|
+
import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
|
|
11
11
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
12
12
|
import { FileError, ExecutionError, RemoteExecutionError, scrubSecretEnv, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
13
13
|
import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
|
|
@@ -403,15 +403,12 @@ export class RemoteHostExecutionEnv {
|
|
|
403
403
|
env: this.mergeEnv(options?.env),
|
|
404
404
|
...spawnGroupOptions(),
|
|
405
405
|
});
|
|
406
|
-
const ticket =
|
|
407
|
-
if (ticket)
|
|
408
|
-
child.once("exit", () => untrackHostForegroundChild(ticket));
|
|
406
|
+
const ticket = adoptHostForegroundChild(child);
|
|
409
407
|
const finish = (r) => {
|
|
410
408
|
if (settled)
|
|
411
409
|
return;
|
|
412
410
|
settled = true;
|
|
413
|
-
|
|
414
|
-
untrackHostForegroundChild(ticket);
|
|
411
|
+
ticket?.release();
|
|
415
412
|
clearTimeout(timer);
|
|
416
413
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
417
414
|
removeDetachListener?.();
|
|
@@ -460,8 +457,7 @@ export class RemoteHostExecutionEnv {
|
|
|
460
457
|
if (forceSettleTimer)
|
|
461
458
|
clearTimeout(forceSettleTimer);
|
|
462
459
|
options?.abortSignal?.removeEventListener("abort", onAbort);
|
|
463
|
-
|
|
464
|
-
untrackHostForegroundChild(ticket);
|
|
460
|
+
ticket?.release();
|
|
465
461
|
settled = true;
|
|
466
462
|
resolve(ok({ stdout: seedOut, stderr: seedErr, exitCode: 0, detached: { shellId } }));
|
|
467
463
|
};
|
|
@@ -621,8 +617,7 @@ export class RemoteHostExecutionEnv {
|
|
|
621
617
|
clearInterval(pump);
|
|
622
618
|
pump = undefined;
|
|
623
619
|
}
|
|
624
|
-
|
|
625
|
-
untrackHostForegroundChild(ticket);
|
|
620
|
+
ticket?.release();
|
|
626
621
|
if (drainTimer) {
|
|
627
622
|
clearTimeout(drainTimer);
|
|
628
623
|
drainTimer = undefined;
|
|
@@ -675,8 +670,7 @@ export class RemoteHostExecutionEnv {
|
|
|
675
670
|
cleanupSpool();
|
|
676
671
|
throw e;
|
|
677
672
|
}
|
|
678
|
-
|
|
679
|
-
ticket = trackHostForegroundChild(child.pid);
|
|
673
|
+
ticket = adoptHostForegroundChild(child);
|
|
680
674
|
closeSpoolFds();
|
|
681
675
|
child.on("error", (e) => {
|
|
682
676
|
finish({ ok: false, error: new ExecutionError("spawn_error", `host spawn failed: ${e.message}`) });
|
|
@@ -765,8 +759,6 @@ export class RemoteHostExecutionEnv {
|
|
|
765
759
|
settleFromTermination(code, signal);
|
|
766
760
|
});
|
|
767
761
|
child.on("exit", (code, signal) => {
|
|
768
|
-
if (ticket)
|
|
769
|
-
untrackHostForegroundChild(ticket);
|
|
770
762
|
exited = true;
|
|
771
763
|
drainTimer = setTimeout(() => settleFromTermination(code, signal), EXIT_SETTLE_DRAIN_MS);
|
|
772
764
|
drainTimer.unref?.();
|
|
@@ -811,9 +803,7 @@ export class RemoteHostExecutionEnv {
|
|
|
811
803
|
const signalReady = () => { const w = wake; wake = undefined; w?.(); };
|
|
812
804
|
const sh = hostShell();
|
|
813
805
|
const child = spawn(sh.shell, [...sh.args, command], { cwd, env: this.mergeEnv(options?.env), ...spawnGroupOptions() });
|
|
814
|
-
const ticket =
|
|
815
|
-
if (ticket)
|
|
816
|
-
child.once("exit", () => untrackHostForegroundChild(ticket));
|
|
806
|
+
const ticket = adoptHostForegroundChild(child);
|
|
817
807
|
let pipeDestroyTimer;
|
|
818
808
|
const kill = () => {
|
|
819
809
|
try {
|
|
@@ -848,8 +838,7 @@ export class RemoteHostExecutionEnv {
|
|
|
848
838
|
child.on("close", (code, signal) => {
|
|
849
839
|
if (pipeDestroyTimer)
|
|
850
840
|
clearTimeout(pipeDestroyTimer);
|
|
851
|
-
|
|
852
|
-
untrackHostForegroundChild(ticket);
|
|
841
|
+
ticket?.release();
|
|
853
842
|
if (finished)
|
|
854
843
|
return;
|
|
855
844
|
exitCode = code ?? (signal ? 128 + (signalNumber(signal) ?? 0) : 1);
|
|
@@ -925,8 +914,7 @@ export class RemoteHostExecutionEnv {
|
|
|
925
914
|
if (wallTimer)
|
|
926
915
|
clearTimeout(wallTimer);
|
|
927
916
|
options?.signal?.removeEventListener("abort", onAbort);
|
|
928
|
-
|
|
929
|
-
untrackHostForegroundChild(ticket);
|
|
917
|
+
ticket?.release();
|
|
930
918
|
}
|
|
931
919
|
}
|
|
932
920
|
async absolutePath(p) {
|
|
@@ -124,7 +124,7 @@ export declare class SqlRunStore {
|
|
|
124
124
|
getActiveTaskId(sessionId: string): Promise<string | undefined>;
|
|
125
125
|
/** Shared row→record mapper — byte-identical between dialects (both drivers hand back the same column
|
|
126
126
|
* names + comparable JS types after SqlDriver's result normalization).
|
|
127
|
-
* 🔴 **读它的每一条查询都必须选 {@link
|
|
127
|
+
* 🔴 **读它的每一条查询都必须选 {@link runRowCols}**(S-230):映射器对「没被 SELECT 的列」是
|
|
128
128
|
* 结构性失明的 —— `undefined ?? null` 与 `Boolean(undefined)` 都会安静地铸出一个**看上去合法的假值**。
|
|
129
129
|
* 修前 `listRuns` 的两条方言臂就漏选 `instance_id`,于是列表面的每一行 `instanceId` 恒 `null`
|
|
130
130
|
* (今天没有消费点,所以没人发现)。列表收成一个常量 = 这类漏选从此不可能发生。 */
|
|
@@ -3,11 +3,42 @@ import { outwardPlane } from "../terminal.js";
|
|
|
3
3
|
import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
|
|
4
4
|
import { escapeLike } from "./sql-escape.js";
|
|
5
5
|
import { pgSanitizeText } from "./pg-safe-json.js";
|
|
6
|
-
import {
|
|
6
|
+
import { toIso as iso } from "./sql-row-helpers.js";
|
|
7
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
8
|
+
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readJsonObjectColumn, readNonNullJsonValueColumn } from "./sql-json-column.js";
|
|
7
9
|
import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
|
|
8
10
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
9
11
|
import { isDupKeyError } from "./sql-errors.js";
|
|
10
|
-
|
|
12
|
+
function runRowCols(dialect) {
|
|
13
|
+
return (`task_id, session_id, owner, status, ${jsonColumnText(dialect, "result")}, error, error_code, job_id, source, ` +
|
|
14
|
+
"instance_id, cancel_requested, objective_preview, created_at, updated_at");
|
|
15
|
+
}
|
|
16
|
+
const readPersistedTaskResultBody = createObjectColumnReader();
|
|
17
|
+
function decodeResultColumn(v, taskId) {
|
|
18
|
+
return decodeNullableJsonColumn(v, readPersistedTaskResultBody, `task_run.result for run ${taskId}`);
|
|
19
|
+
}
|
|
20
|
+
function refusing(what, read) {
|
|
21
|
+
try {
|
|
22
|
+
return read();
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
throw foldJsonColumn(e, (m) => new Error(`SqlRunStore: ${m} — refusing rather than ${what}`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function usageResultBody(v) {
|
|
29
|
+
try {
|
|
30
|
+
return decodeJsonColumn(v, readJsonObjectColumn, "task_run.result (usage scan row)");
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
if (!isJsonColumnBodyProblem(e))
|
|
34
|
+
throw e;
|
|
35
|
+
recordFailOpen("server.sql-json.task-run.usage-result-unreadable");
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function decodeEventDataColumn(v, taskId, seq) {
|
|
40
|
+
return decodeNullableJsonColumn(v, readNonNullJsonValueColumn, `task_event.data for run ${taskId} seq ${seq}`);
|
|
41
|
+
}
|
|
11
42
|
export class SqlRunStore {
|
|
12
43
|
db;
|
|
13
44
|
constructor(db) {
|
|
@@ -178,7 +209,7 @@ export class SqlRunStore {
|
|
|
178
209
|
sessionId: String(r.session_id),
|
|
179
210
|
owner: r.owner ?? null,
|
|
180
211
|
status: r.status,
|
|
181
|
-
result:
|
|
212
|
+
result: refusing("serve a finished run as if it had produced nothing", () => decodeResultColumn(r.result, String(r.task_id))) ?? null,
|
|
182
213
|
error: r.error ?? null,
|
|
183
214
|
errorCode: r.error_code ?? null,
|
|
184
215
|
jobId: r.job_id ?? null,
|
|
@@ -191,7 +222,7 @@ export class SqlRunStore {
|
|
|
191
222
|
};
|
|
192
223
|
}
|
|
193
224
|
async getRun(taskId) {
|
|
194
|
-
const { rows } = await this.db.query(this.q(`SELECT ${
|
|
225
|
+
const { rows } = await this.db.query(this.q(`SELECT ${runRowCols("tidb")} FROM task_run WHERE task_id = ?`, `SELECT ${runRowCols("pg")} FROM task_run WHERE task_id = $1`), [taskId]);
|
|
195
226
|
return rows[0] ? this.rowToRun(rows[0]) : undefined;
|
|
196
227
|
}
|
|
197
228
|
async listRuns(opts) {
|
|
@@ -220,7 +251,7 @@ export class SqlRunStore {
|
|
|
220
251
|
where.push("(created_at < ? OR (created_at = ? AND task_id < ?))");
|
|
221
252
|
params.push(new Date(opts.cursor.createdAt), new Date(opts.cursor.createdAt), opts.cursor.taskId);
|
|
222
253
|
}
|
|
223
|
-
const sql = `SELECT ${
|
|
254
|
+
const sql = `SELECT ${runRowCols("tidb")} FROM task_run` +
|
|
224
255
|
(where.length ? " WHERE " + where.join(" AND ") : "") +
|
|
225
256
|
" ORDER BY created_at DESC, task_id DESC LIMIT ?";
|
|
226
257
|
params.push(opts.limit);
|
|
@@ -247,7 +278,7 @@ export class SqlRunStore {
|
|
|
247
278
|
const t = p(opts.cursor.taskId);
|
|
248
279
|
where.push(`(created_at < ${a} OR (created_at = ${b} AND task_id < ${t}))`);
|
|
249
280
|
}
|
|
250
|
-
const sql = `SELECT ${
|
|
281
|
+
const sql = `SELECT ${runRowCols("pg")} FROM task_run` +
|
|
251
282
|
(where.length ? " WHERE " + where.join(" AND ") : "") +
|
|
252
283
|
` ORDER BY created_at DESC, task_id DESC LIMIT ${p(opts.limit)}`;
|
|
253
284
|
const { rows } = await this.db.query(sql, params);
|
|
@@ -359,7 +390,7 @@ export class SqlRunStore {
|
|
|
359
390
|
where.push(this.q("owner = ?", "owner = $3"));
|
|
360
391
|
params.push(opts.owner);
|
|
361
392
|
}
|
|
362
|
-
const { rows } = await this.db.query(`SELECT owner, created_at, status, result FROM task_run WHERE ${where.join(" AND ")} ORDER BY created_at LIMIT ${limit + 1}`, params);
|
|
393
|
+
const { rows } = await this.db.query(`SELECT owner, created_at, status, ${jsonColumnText(this.db.dialect, "result")} FROM task_run WHERE ${where.join(" AND ")} ORDER BY created_at LIMIT ${limit + 1}`, params);
|
|
363
394
|
const truncated = rows.length > limit;
|
|
364
395
|
const page = truncated ? rows.slice(0, limit) : rows;
|
|
365
396
|
return {
|
|
@@ -368,7 +399,7 @@ export class SqlRunStore {
|
|
|
368
399
|
owner: r.owner ?? null,
|
|
369
400
|
createdAtMs: new Date(r.created_at).getTime(),
|
|
370
401
|
status: String(r.status),
|
|
371
|
-
...(r.result
|
|
402
|
+
...(r.result === null ? {} : { stats: projectUsageStats(usageResultBody(r.result)) }),
|
|
372
403
|
})),
|
|
373
404
|
};
|
|
374
405
|
}
|
|
@@ -378,11 +409,11 @@ export class SqlRunStore {
|
|
|
378
409
|
return m == null ? 0 : Number(m);
|
|
379
410
|
}
|
|
380
411
|
async getEvents(taskId, afterSeq) {
|
|
381
|
-
const { rows } = await this.db.query(this.q(
|
|
412
|
+
const { rows } = await this.db.query(this.q(`SELECT seq, type, ${jsonColumnText("tidb", "data")}, ts FROM task_event WHERE task_id = ? AND seq > ? ORDER BY seq ASC`, `SELECT seq, type, ${jsonColumnText("pg", "data")}, ts FROM task_event WHERE task_id = $1 AND seq > $2 ORDER BY seq ASC`), [taskId, afterSeq]);
|
|
382
413
|
return rows.map((r) => ({
|
|
383
414
|
seq: Number(r.seq),
|
|
384
415
|
type: String(r.type),
|
|
385
|
-
data:
|
|
416
|
+
data: refusing("replay it as an empty ledger frame", () => decodeEventDataColumn(r.data, taskId, Number(r.seq))) ?? null,
|
|
386
417
|
ts: iso(r.ts),
|
|
387
418
|
}));
|
|
388
419
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type SessionPolicyStore, type SessionPermissionRules, type StoredSessionRules, type SessionRulesRecord, type PutRulesOptions } from "@sema-agent/core";
|
|
2
2
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
3
3
|
import type { Pool as PgPool } from "pg";
|
|
4
|
+
import { type JsonColumnShape } from "./sql-json-column.js";
|
|
4
5
|
import { type SqlDriver } from "./sql-driver.js";
|
|
5
6
|
/** `session_policy.policy_key` 的**唯一铸造点**(core 复合键的 sha256)。
|
|
6
7
|
*
|
|
@@ -8,6 +9,29 @@ import { type SqlDriver } from "./sql-driver.js";
|
|
|
8
9
|
* 逐行重算这把键(身份在 hash 的**原像**里,等值 UPDATE 够不着)。两处各写一遍 sha256 表达式的下场是
|
|
9
10
|
* 某天有人在这里改了 key 公式而收编腿还在按旧式算 —— 迁完的行从此谁也读不到,且**静默**。 */
|
|
10
11
|
export declare const policyKeyFor: (sessionId: string, principal: string | undefined) => string;
|
|
12
|
+
/**
|
|
13
|
+
* 一行的 **`rules` 体**(已按文本取出的那一串)→ 收窄好的 `SessionPermissionRules`(C1)。读不出形 ⇒
|
|
14
|
+
* `SessionPolicyError("corrupt")`,与 rev / gen 两道拒同口径、同一个码。
|
|
15
|
+
*
|
|
16
|
+
* 形判据复用**现成的那一只**读者 `readSessionPermissionRules`(五桶 `string[]` 的单一属主;未知键照旧忽略,
|
|
17
|
+
* 与 core 的 `normalizeRules` 同口径),不在这里铸第二份键集。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 **恰好解一次码,而这是由读腿的投影保证的**(codex 对抗复审两轮,两条读法各自实测被推翻后的终形):
|
|
20
|
+
* · 「体是什么就直接判形」——**错**:MariaDB(本仓 README 声明支持的 MySQL 协议后端之一)的 `JSON` 是
|
|
21
|
+
* `LONGTEXT` 的**别名**,驱动只对真 JSON 传输类型自动解码 ⇒ 那台机器上每一条**合法**规则都以文本到达,
|
|
22
|
+
* 会被判成 `corrupt`(三个读点全挂,一台好机器变砖);
|
|
23
|
+
* · 「体是串就再解一次」——**也错**:`JSON`/`JSONB` 列上驱动已经解过一次,于是一行体为 `"{}"`(一个把
|
|
24
|
+
* `JSON.stringify` 多调了一次的迁移脚本的典型产物)会被解成 `{}` 并当作**空规则**端出去 —— 与本道拒
|
|
25
|
+
* 要关的 `null` 洞逐字同形,只是换了个入口。
|
|
26
|
+
* 两条读法错在**同一个地方**:从值本身分不出「驱动解过了」还是「没解过」。⇒ 让这个歧义在**构造上**不存在:
|
|
27
|
+
* 三个读点都把这一列**按文本**投影(`CAST(rules AS CHAR)` / `::text`),于是到这里恒是一只串、恰好解一次。
|
|
28
|
+
* JSON 列里的字符串标量按文本取出来**带着引号**(真库实测两方言同形:`"\"{}\""`)⇒ 解一次得到一只串 ⇒ 判坏;
|
|
29
|
+
* 文本承载的列取出来就是它自己 ⇒ 解一次得到对象 ⇒ 好体。**一条规则,两种承载,零方言分叉,不依赖驱动行为。**
|
|
30
|
+
*
|
|
31
|
+
* 🔴 S-550:上面这一整段现在是**整仓**的口径,实现搬进唯一属主 `sql-json-column.ts`(投影 `jsonColumnText`
|
|
32
|
+
* + 恰好解一次 `decodeJsonColumn` + 折码 `foldJsonColumn`);本店只留「形判据是哪一只」与「坏体折成哪个码」。
|
|
33
|
+
*/
|
|
34
|
+
export declare function readSessionRulesBody(body: unknown): JsonColumnShape<SessionPermissionRules>;
|
|
11
35
|
/** Dual-dialect durable SessionPolicyStore. See the file header for the dialect-delta ledger. */
|
|
12
36
|
export declare class SqlSessionPolicyStore implements SessionPolicyStore {
|
|
13
37
|
private readonly db;
|
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
|
|
3
3
|
import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
|
|
4
|
+
import { decodeJsonColumn, foldJsonColumn, jsonColumnText } from "./sql-json-column.js";
|
|
4
5
|
import { pgHasUnstorable } from "./pg-safe-json.js";
|
|
5
6
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
6
7
|
import { isDeadlockError, isDupKeyError } from "./sql-errors.js";
|
|
7
8
|
export const policyKeyFor = (sessionId, principal) => createHash("sha256").update(JSON.stringify([sessionId, principal ?? null])).digest("hex");
|
|
8
9
|
const policyKey = policyKeyFor;
|
|
9
10
|
const RETRY_LIMIT = 8;
|
|
11
|
+
export function readSessionRulesBody(body) {
|
|
12
|
+
const read = readSessionPermissionRules(body);
|
|
13
|
+
return read.problem !== undefined ? { problem: describeSessionPermissionRulesProblem(read.problem) } : { value: read.rules };
|
|
14
|
+
}
|
|
10
15
|
function readRulesBody(text, where) {
|
|
11
|
-
let body;
|
|
12
16
|
try {
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
throw new SessionPolicyError("corrupt", `session rules row ${where} carries a rules body that is not JSON (a torn or hand-edited write) — refusing rather than serve a row nobody can read as rules`);
|
|
17
|
+
return decodeJsonColumn(text, readSessionRulesBody, where);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw new SessionPolicyError("corrupt", `session rules row ${where} carries a malformed rules body (${describeSessionPermissionRulesProblem(read.problem)}) — refusing rather than serve it as an EMPTY rule set, which would silently drop every tightening this session carries`);
|
|
19
|
+
catch (e) {
|
|
20
|
+
throw foldJsonColumn(e, (m) => new SessionPolicyError("corrupt", `${m} — an unreadable rules body served as an EMPTY rule set would silently drop every tightening this session carries`));
|
|
21
21
|
}
|
|
22
|
-
return read.rules;
|
|
23
22
|
}
|
|
24
23
|
function rowToStored(r, where) {
|
|
25
24
|
const rev = Number(r.rev);
|
|
26
25
|
if (!Number.isFinite(rev) || !Number.isInteger(rev) || rev < 1) {
|
|
27
26
|
throw new SessionPolicyError("corrupt", `session rules row ${where} carries rev ${String(r.rev)} — a stored row's rev is always ≥ 1 (the stamp is floor+1), and rev 0 is the live reader's synthetic empty seed; refusing rather than serve it as rules`);
|
|
28
27
|
}
|
|
29
|
-
|
|
28
|
+
if (r.gen === undefined || r.gen === null) {
|
|
29
|
+
throw new Error(`session_policy row ${where} came back without a \`gen\` column — a read leg did not project it (the column is BIGINT NOT NULL on both dialects). ` +
|
|
30
|
+
"Refusing rather than judge this row's lineage as generation 0, which is a value nothing minted.");
|
|
31
|
+
}
|
|
32
|
+
const gen = Number(r.gen);
|
|
30
33
|
if (!Number.isFinite(gen) || !Number.isInteger(gen) || gen < 0) {
|
|
31
34
|
throw new SessionPolicyError("corrupt", `session rules row ${where} carries lineage gen ${String(r.gen)} — a generation is a whole number ≥ 0; refusing rather than let a reader judge lineage by a value nothing minted`);
|
|
32
35
|
}
|
|
33
|
-
|
|
34
|
-
if (typeof raw !== "string") {
|
|
35
|
-
throw new Error(`session rules row ${where}: the read did not project the rules column as TEXT (got ${typeof raw}) — the SELECT and rowToStored have drifted apart`);
|
|
36
|
-
}
|
|
37
|
-
return { ...readRulesBody(raw, where), rev, gen };
|
|
36
|
+
return { ...readRulesBody(r.rules, `session_policy.rules ${where}`), rev, gen };
|
|
38
37
|
}
|
|
39
38
|
export class SqlSessionPolicyStore {
|
|
40
39
|
db;
|
|
@@ -48,13 +47,13 @@ export class SqlSessionPolicyStore {
|
|
|
48
47
|
return isDupKeyError(this.db.dialect, e);
|
|
49
48
|
}
|
|
50
49
|
async getRules(sessionId, principal) {
|
|
51
|
-
const { rows } = await this.db.query(this.q(
|
|
50
|
+
const { rows } = await this.db.query(this.q(`SELECT ${jsonColumnText("tidb", "rules")}, rev, gen FROM session_policy WHERE policy_key = ?`, `SELECT ${jsonColumnText("pg", "rules")}, rev, gen FROM session_policy WHERE policy_key = $1`), [policyKey(sessionId, principal)]);
|
|
52
51
|
if (!rows[0])
|
|
53
52
|
return null;
|
|
54
53
|
return rowToStored(rows[0], `for session ${JSON.stringify(sessionId)}`);
|
|
55
54
|
}
|
|
56
55
|
async listBySession(sessionId) {
|
|
57
|
-
const { rows } = await this.db.query(this.q(
|
|
56
|
+
const { rows } = await this.db.query(this.q(`SELECT principal, ${jsonColumnText("tidb", "rules")}, rev, gen FROM session_policy WHERE session_id = ?`, `SELECT principal, ${jsonColumnText("pg", "rules")}, rev, gen FROM session_policy WHERE session_id = $1`), [sessionId]);
|
|
58
57
|
return rows.map((r) => ({
|
|
59
58
|
principal: r.principal == null ? undefined : String(r.principal),
|
|
60
59
|
rules: rowToStored(r, `for session ${JSON.stringify(sessionId)} (enumeration)`),
|
|
@@ -63,7 +62,7 @@ export class SqlSessionPolicyStore {
|
|
|
63
62
|
async putRules(sessionId, principal, rules, opts) {
|
|
64
63
|
const key = policyKey(sessionId, principal);
|
|
65
64
|
return this.retrying(() => this.tx(async (conn) => {
|
|
66
|
-
const { rows } = await conn.query(this.q(
|
|
65
|
+
const { rows } = await conn.query(this.q(`SELECT ${jsonColumnText("tidb", "rules")}, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE`, `SELECT ${jsonColumnText("pg", "rules")}, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE`), [key]);
|
|
67
66
|
const prior = rows[0] ? rowToStored(rows[0], `for session ${JSON.stringify(sessionId)} (pre-write snapshot)`) : undefined;
|
|
68
67
|
const priorRev = prior?.rev ?? 0;
|
|
69
68
|
if (opts?.expectedRev !== undefined && priorRev !== opts.expectedRev) {
|