@sema-agent/server 7.138.0 → 7.140.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/MIGRATION.md +457 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +91 -41
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/runner.js +1 -1
- package/dist/adoption/sql.js +1 -1
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +26 -26
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/live-deps.js +3 -2
- package/dist/bench/s1/row.d.ts +5 -2
- package/dist/bench/s1/row.js +2 -22
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/adoption.js +1 -1
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +28 -34
- package/dist/boot/execution-lane.d.ts +9 -1
- package/dist/boot/execution-lane.js +6 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-boundary.d.ts +14 -51
- package/dist/boot/memory-boundary.js +10 -20
- package/dist/boot/memory-consolidation.d.ts +15 -11
- package/dist/boot/memory-consolidation.js +25 -14
- package/dist/boot/resolve-spec.d.ts +4 -6
- package/dist/boot/resolve-spec.js +10 -13
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +6 -4
- package/dist/boot/runner-deps.js +3 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-03-store-backend.d.ts +2 -6
- package/dist/boot/stage-03-store-backend.js +2 -3
- package/dist/boot/stage-04-stores.d.ts +2 -6
- package/dist/boot/stage-04-stores.js +5 -5
- package/dist/boot/stage-05-execution-env.d.ts +2 -6
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.d.ts +2 -6
- package/dist/boot/stage-06-runners.js +2 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -6
- package/dist/boot/stage-08-reapers.d.ts +2 -6
- package/dist/boot/stage-09-leader.d.ts +2 -6
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.d.ts +2 -6
- package/dist/boot/stage-10-http-server.js +7 -6
- package/dist/boot/stores.d.ts +3 -6
- package/dist/boot/stores.js +27 -40
- package/dist/boot/task-list-lane.d.ts +1 -1
- package/dist/boot/task-list-lane.js +1 -1
- package/dist/boot/web-admin.js +37 -11
- package/dist/capabilities/execution-lane.d.ts +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +9 -8
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +33 -37
- package/dist/config-provider.js +51 -30
- package/dist/config-types.d.ts +21 -7
- package/dist/config.d.ts +33 -0
- package/dist/config.js +46 -16
- package/dist/delegated-session-owner.d.ts +4 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/deployment-shape.d.ts +10 -9
- package/dist/deployment-shape.js +4 -4
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +115 -105
- package/dist/execution-lane-caps.js +76 -82
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +3 -1
- package/dist/http/memory-compliance-failure-reply.js +5 -0
- package/dist/http/memory-layer-reply.d.ts +30 -0
- package/dist/http/memory-layer-reply.js +24 -0
- package/dist/http/resume-legs.js +63 -36
- package/dist/http/retired-service-deps.d.ts +1 -1
- package/dist/http/retired-service-deps.js +1 -1
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/adoption.js +1 -1
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +8 -1
- package/dist/http/routes/memory-consolidation.js +12 -1
- package/dist/http/routes/memory-origin.js +14 -2
- package/dist/http/routes/memory-policy.js +5 -0
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +143 -22
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +41 -33
- package/dist/http/server.d.ts +40 -44
- package/dist/http/server.js +13 -2
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/identity-source.d.ts +1 -1
- package/dist/identity-source.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/lane-home-probe.d.ts +68 -42
- package/dist/lane-home-probe.js +53 -30
- package/dist/leader/wire.js +5 -4
- package/dist/memory-bundle-engine.d.ts +16 -9
- package/dist/memory-bundle-engine.js +29 -6
- package/dist/memory-operator-faces.d.ts +85 -30
- package/dist/memory-operator-faces.js +136 -30
- package/dist/memory-project-layer.d.ts +3 -21
- package/dist/memory-project-layer.js +11 -8
- package/dist/memory-scope-stores.d.ts +73 -0
- package/dist/memory-scope-stores.js +86 -0
- package/dist/memory-scope.d.ts +29 -5
- package/dist/memory-scope.js +16 -4
- package/dist/memory-sync-client.d.ts +70 -18
- package/dist/memory-sync-client.js +215 -117
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/adoption-log-sql.js +4 -4
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +44 -26
- package/dist/plugins/approval-nonce-store.js +4 -4
- package/dist/plugins/blob-backend.d.ts +4 -4
- package/dist/plugins/blob-backend.js +7 -7
- package/dist/plugins/breaker-state-sql.js +2 -2
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-index-columns.js +2 -2
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +21 -23
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +68 -122
- package/dist/plugins/ensure-index.d.ts +1 -2
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +9 -8
- package/dist/plugins/file-history-store-sql.js +9 -9
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/image-bake-store-sql.js +9 -9
- package/dist/plugins/image-index-sql.js +11 -11
- package/dist/plugins/leader-run-store-sql.js +4 -4
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/mailbox-store-sql.d.ts +2 -1
- package/dist/plugins/mailbox-store-sql.js +4 -4
- package/dist/plugins/memory-engine-tidb.js +5 -5
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +1 -1
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-sync-store-tidb.js +3 -3
- package/dist/plugins/one-time-migrations.d.ts +117 -8
- package/dist/plugins/one-time-migrations.js +408 -38
- package/dist/plugins/org-delivery-quota-store.js +2 -2
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/outcome-ledger-sql.js +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +17 -19
- package/dist/plugins/permission-rule-store-sql.js +29 -32
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +18 -19
- package/dist/plugins/remote-env-adb.js +91 -66
- package/dist/plugins/remote-env-device.d.ts +23 -31
- package/dist/plugins/remote-env-device.js +19 -17
- package/dist/plugins/remote-env-e2b.d.ts +27 -21
- package/dist/plugins/remote-env-e2b.js +91 -45
- package/dist/plugins/remote-env-file-error.d.ts +3 -1
- package/dist/plugins/remote-env-file-error.js +9 -3
- package/dist/plugins/remote-env-host.d.ts +31 -34
- package/dist/plugins/remote-env-host.js +91 -72
- package/dist/plugins/remote-env-k8s.d.ts +13 -29
- package/dist/plugins/remote-env-k8s.js +63 -55
- package/dist/plugins/remote-env-local-docker.d.ts +24 -12
- package/dist/plugins/remote-env-local-docker.js +49 -61
- package/dist/plugins/remote-env-ssh.d.ts +11 -19
- package/dist/plugins/remote-env-ssh.js +74 -57
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/resume-anchor-store-sql.js +2 -2
- package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
- package/dist/plugins/retention-lane-store-sql.js +2 -2
- package/dist/plugins/retention-store-sql.d.ts +10 -7
- package/dist/plugins/retention-store-sql.js +13 -7
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +20 -15
- package/dist/plugins/schema-consistency.d.ts +21 -11
- package/dist/plugins/schema-consistency.js +15 -9
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/schema-ensure-legs.d.ts +2 -3
- package/dist/plugins/schema-ensure-legs.js +2 -2
- package/dist/plugins/schema-live-columns.d.ts +1 -1
- package/dist/plugins/session-capture-record-store-sql.js +1 -1
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-rules-column.d.ts +1 -1
- package/dist/plugins/session-rules-column.js +1 -1
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.d.ts +1 -1
- package/dist/plugins/session-workspace-cell.js +1 -1
- package/dist/plugins/shared-memory-store-sql.js +2 -2
- package/dist/plugins/sql-driver.d.ts +10 -5
- package/dist/plugins/sql-driver.js +6 -6
- package/dist/plugins/sql-errors.js +1 -1
- package/dist/plugins/store-backend.d.ts +5 -7
- package/dist/plugins/store-backend.js +23 -11
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/task-list-store-sql.js +11 -11
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +13 -7
- package/dist/plugins/tidb-session-storage.js +4 -4
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +127 -59
- package/dist/plugins/tool-result-store-sql.d.ts +5 -4
- package/dist/plugins/tool-result-store-sql.js +21 -13
- package/dist/plugins/user-input-ledger-sql.js +2 -2
- package/dist/plugins/workflow-journal-store-sql.js +5 -5
- package/dist/plugins/workflow-run-store-sql.js +19 -19
- package/dist/plugins/write-behind-counter.d.ts +2 -3
- package/dist/plugins/write-behind-counter.js +5 -5
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/resource-suspend.d.ts +14 -12
- package/dist/resource-suspend.js +1 -1
- package/dist/resource-window.d.ts +4 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +34 -35
- package/dist/runs.d.ts +10 -5
- package/dist/runs.js +52 -66
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +20 -10
- package/dist/session-incarnation.js +5 -3
- package/dist/session-policy-wire.d.ts +1 -1
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/sql-engine-posture.d.ts +1 -1
- package/dist/sql-engine-posture.js +1 -1
- package/dist/{plugins → stores-sql}/background-agent-store-sql.d.ts +2 -2
- package/dist/{plugins → stores-sql}/background-agent-store-sql.js +8 -8
- package/dist/{plugins → stores-sql}/session-policy-store-sql.d.ts +7 -5
- package/dist/{plugins → stores-sql}/session-policy-store-sql.js +12 -12
- package/dist/{plugins → stores-sql}/usage-window-store-sql.d.ts +1 -1
- package/dist/{plugins → stores-sql}/usage-window-store-sql.js +3 -3
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-a2a.js +5 -6
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +22 -9
- package/dist/task-mcp.js +17 -9
- package/dist/terminal.d.ts +27 -0
- package/dist/terminal.js +6 -0
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/redact.d.ts +3 -1
- package/dist/trace/redact.js +80 -28
- package/dist/trace/self-minted-wire.d.ts +169 -2
- package/dist/trace/self-minted-wire.js +54 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/candidate-gate.d.ts +11 -7
- package/dist/web-admin-client/candidate-gate.js +1 -1
- package/dist/web-admin-client/http-client.d.ts +5 -4
- package/dist/web-admin-client/http-client.js +2 -2
- package/dist/web-admin-client/read-warnings.d.ts +7 -17
- package/dist/web-admin-client/read-warnings.js +5 -9
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/wire-changes/server@7.140.0.json +184 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
2
2
|
import { isDupKeyError, isCheckViolationError } from "./sql-errors.js";
|
|
3
|
-
import { DEVICE_AUDIT_EVENTS, assertAcquireShape, assertDeviceIdShape, assertDeviceOwnerShape, assertEnrollTokenHashShape, assertIssueTokenShape, assertNewDeviceShape, assertRebindShape, assertRevokeShape,
|
|
3
|
+
import { DEVICE_AUDIT_EVENTS, assertAcquireShape, assertDeviceIdShape, assertDeviceOwnerShape, assertEnrollTokenHashShape, assertIssueTokenShape, assertNewDeviceShape, assertPlaceDeviceShape, assertRebindShape, assertRevokeShape, assertWholeSeconds, parseDeviceAuditEvent, parseDevicePlatformOs, parseDeviceStatus, } from "../device-store.js";
|
|
4
|
+
import { WRITABLE, boundDeviceOf, settledDeviceBindOutcome, settledDeviceRebindOutcome } from "../execution-placement.js";
|
|
5
|
+
import { bindDeviceStatement, lockRootAndReadClaim, placeWriterStatement, readPlacementOn, rebindDeviceStatement, rowsOnSqlExec } from "./execution-placement-sql.js";
|
|
4
6
|
import { ensureIndexes } from "./ensure-index.js";
|
|
5
7
|
import { decodeJsonColumn, foldJsonColumn, jsonColumnText, readJsonObjectColumn } from "./sql-json-column.js";
|
|
6
8
|
export const DEVICES_TABLE = "device";
|
|
7
9
|
export const DEVICE_ENROLL_TOKENS_TABLE = "device_enroll_token";
|
|
8
|
-
export const DEVICE_SESSIONS_TABLE = "device_session";
|
|
9
10
|
export const DEVICE_AUDIT_TABLE = "device_audit";
|
|
10
11
|
const UQ_DEVICES_PUBKEY = "uq_device_pubkey";
|
|
11
12
|
const AUDIT_EVENT_CHECK = DEVICE_AUDIT_EVENTS.map((e) => `'${e}'`).join(", ");
|
|
@@ -56,17 +57,6 @@ export const TIDB_DEVICE_STATEMENTS = [
|
|
|
56
57
|
consumed_pubkey VARBINARY(128) NULL,
|
|
57
58
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
58
59
|
PRIMARY KEY (token_hash)
|
|
59
|
-
) COLLATE utf8mb4_bin`,
|
|
60
|
-
`CREATE TABLE IF NOT EXISTS ${DEVICE_SESSIONS_TABLE} (
|
|
61
|
-
-- 键 = 根会话(task-R1-3:后代继承不另绑);一根会话恒一设备(改绑 = 显式动作)。
|
|
62
|
-
root_session_id VARBINARY(128) NOT NULL,
|
|
63
|
-
device_id VARBINARY(64) NOT NULL,
|
|
64
|
-
owner_tenant VARBINARY(190) NOT NULL,
|
|
65
|
-
owner_subject VARBINARY(190) NOT NULL,
|
|
66
|
-
bound_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
67
|
-
rev BIGINT NOT NULL DEFAULT 0,
|
|
68
|
-
PRIMARY KEY (root_session_id),
|
|
69
|
-
KEY idx_device_session_device (device_id)
|
|
70
60
|
) COLLATE utf8mb4_bin`,
|
|
71
61
|
`CREATE TABLE IF NOT EXISTS ${DEVICE_AUDIT_TABLE} (
|
|
72
62
|
id BIGINT AUTO_INCREMENT,
|
|
@@ -90,7 +80,6 @@ export async function ensureTiDBDeviceSchema(pool) {
|
|
|
90
80
|
}
|
|
91
81
|
export const DEVICE_INDEXES = [
|
|
92
82
|
{ table: `${DEVICES_TABLE}`, name: "idx_device_owner", columns: ["owner_tenant", "owner_subject"] },
|
|
93
|
-
{ table: `${DEVICE_SESSIONS_TABLE}`, name: "idx_device_session_device", columns: ["device_id"] },
|
|
94
83
|
{ table: `${DEVICE_AUDIT_TABLE}`, name: "idx_device_audit_device", columns: ["device_id", "at"] },
|
|
95
84
|
{ table: `${DEVICE_AUDIT_TABLE}`, name: "idx_device_audit_event", columns: ["event", "at"] },
|
|
96
85
|
];
|
|
@@ -133,15 +122,6 @@ export async function ensurePgDeviceSchema(q) {
|
|
|
133
122
|
consumed_pubkey VARCHAR(128) COLLATE "C",
|
|
134
123
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
135
124
|
PRIMARY KEY (token_hash)
|
|
136
|
-
)`);
|
|
137
|
-
await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_SESSIONS_TABLE} (
|
|
138
|
-
root_session_id VARCHAR(128) COLLATE "C" NOT NULL,
|
|
139
|
-
device_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
140
|
-
owner_tenant VARCHAR(190) COLLATE "C" NOT NULL,
|
|
141
|
-
owner_subject VARCHAR(190) COLLATE "C" NOT NULL,
|
|
142
|
-
bound_at TIMESTAMPTZ(3) NOT NULL DEFAULT now(),
|
|
143
|
-
rev BIGINT NOT NULL DEFAULT 0,
|
|
144
|
-
PRIMARY KEY (root_session_id)
|
|
145
125
|
)`);
|
|
146
126
|
await q(`CREATE TABLE IF NOT EXISTS ${DEVICE_AUDIT_TABLE} (
|
|
147
127
|
id BIGSERIAL PRIMARY KEY,
|
|
@@ -160,7 +140,7 @@ export async function assertDeviceAuditRebindEventSchema(db) {
|
|
|
160
140
|
try {
|
|
161
141
|
await conn.begin();
|
|
162
142
|
try {
|
|
163
|
-
await conn.query(db.dialect === "
|
|
143
|
+
await conn.query(db.dialect === "mysql"
|
|
164
144
|
? `INSERT INTO ${DEVICE_AUDIT_TABLE} (at, event, device_id, owner_tenant, owner_subject, detail) VALUES (NOW(3), ?, NULL, NULL, NULL, NULL)`
|
|
165
145
|
: `INSERT INTO ${DEVICE_AUDIT_TABLE} (at, event, device_id, owner_tenant, owner_subject, detail) VALUES (now(), $1, NULL, NULL, NULL, NULL)`, ["session_rebound"]);
|
|
166
146
|
}
|
|
@@ -172,8 +152,8 @@ export async function assertDeviceAuditRebindEventSchema(db) {
|
|
|
172
152
|
: `ALTER TABLE ${DEVICE_AUDIT_TABLE} DROP CHECK ck_device_audit_event; ALTER TABLE ${DEVICE_AUDIT_TABLE} ADD CONSTRAINT ck_device_audit_event CHECK (event IN (${AUDIT_EVENT_CHECK}));`;
|
|
173
153
|
throw new Error(`${DEVICE_AUDIT_TABLE} still carries a pre-O4 event CHECK that rejects 'session_rebound' — refusing to start. ` +
|
|
174
154
|
"The rebind verb (POST /v1/devices/sessions/:rootSessionId/rebind) writes this audit event in the same transaction " +
|
|
175
|
-
"as its CAS, so on this table every rebind would fail at runtime after a healthy-looking boot.
|
|
176
|
-
"
|
|
155
|
+
"as its CAS, so on this table every rebind would fail at runtime after a healthy-looking boot. Nothing in this build " +
|
|
156
|
+
"rewrites this table's constraint on its own, and dropping an AUDIT table would destroy security-axis history — run the manual " +
|
|
177
157
|
`constraint rebuild instead (history preserved, defense-in-depth restored to the current closed set): \`${remedy}\` ` +
|
|
178
158
|
"The application layer remains the enforcing authority for this closed set (parseDeviceAuditEvent). " +
|
|
179
159
|
`Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -188,9 +168,9 @@ export async function assertDeviceAuditRebindEventSchema(db) {
|
|
|
188
168
|
conn.release();
|
|
189
169
|
}
|
|
190
170
|
}
|
|
191
|
-
const ms = (dialect, col, alias) => dialect === "
|
|
192
|
-
const NOW = (dialect) => (dialect === "
|
|
193
|
-
const nowPlusSeconds = (dialect, placeholder) => dialect === "
|
|
171
|
+
const ms = (dialect, col, alias) => dialect === "mysql" ? `ROUND(UNIX_TIMESTAMP(${col}) * 1000) AS ${alias}` : `(EXTRACT(EPOCH FROM ${col}) * 1000)::bigint AS ${alias}`;
|
|
172
|
+
const NOW = (dialect) => (dialect === "mysql" ? "NOW(3)" : "now()");
|
|
173
|
+
const nowPlusSeconds = (dialect, placeholder) => dialect === "mysql" ? `NOW(3) + INTERVAL ${placeholder} SECOND` : `now() + (${placeholder}::double precision * INTERVAL '1 second')`;
|
|
194
174
|
function deviceCols(dialect) {
|
|
195
175
|
return [
|
|
196
176
|
"device_id",
|
|
@@ -234,9 +214,6 @@ function tokenCols(dialect) {
|
|
|
234
214
|
"rev",
|
|
235
215
|
].join(", ");
|
|
236
216
|
}
|
|
237
|
-
function sessionCols(dialect) {
|
|
238
|
-
return ["root_session_id", "device_id", "owner_tenant", "owner_subject", ms(dialect, "bound_at", "bound_at_ms"), "rev"].join(", ");
|
|
239
|
-
}
|
|
240
217
|
function auditCols(dialect) {
|
|
241
218
|
return ["id", ms(dialect, "at", "at_ms"), "event", "device_id", "owner_tenant", "owner_subject", jsonColumnText(dialect, "detail")].join(", ");
|
|
242
219
|
}
|
|
@@ -287,15 +264,6 @@ function mapTokenRow(r) {
|
|
|
287
264
|
rev: Number(r.rev),
|
|
288
265
|
};
|
|
289
266
|
}
|
|
290
|
-
function mapSessionRow(r) {
|
|
291
|
-
return {
|
|
292
|
-
rootSessionId: bufToStr(r.root_session_id),
|
|
293
|
-
deviceId: bufToStr(r.device_id),
|
|
294
|
-
owner: { tenant: bufToStr(r.owner_tenant), subject: bufToStr(r.owner_subject) },
|
|
295
|
-
boundAtMs: Number(r.bound_at_ms),
|
|
296
|
-
rev: Number(r.rev),
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
267
|
function mapAuditRow(r) {
|
|
300
268
|
const tenant = strOrNull(r.owner_tenant);
|
|
301
269
|
const subject = strOrNull(r.owner_subject);
|
|
@@ -318,10 +286,10 @@ function readAuditDetail(raw, id) {
|
|
|
318
286
|
}
|
|
319
287
|
}
|
|
320
288
|
function ph(dialect, n) {
|
|
321
|
-
return dialect === "
|
|
289
|
+
return dialect === "mysql" ? "?" : `$${n}`;
|
|
322
290
|
}
|
|
323
291
|
function dupKeyName(dialect, err) {
|
|
324
|
-
if (dialect === "
|
|
292
|
+
if (dialect === "mysql") {
|
|
325
293
|
const msg = String(err?.sqlMessage ?? err?.message ?? "");
|
|
326
294
|
if (new RegExp(UQ_DEVICES_PUBKEY, "i").test(msg))
|
|
327
295
|
return "pubkey";
|
|
@@ -341,8 +309,8 @@ export class SqlDeviceStore {
|
|
|
341
309
|
constructor(db) {
|
|
342
310
|
this.db = db;
|
|
343
311
|
}
|
|
344
|
-
q(
|
|
345
|
-
return this.db.dialect === "
|
|
312
|
+
q(mysql, pg) {
|
|
313
|
+
return this.db.dialect === "mysql" ? mysql : pg;
|
|
346
314
|
}
|
|
347
315
|
json(value) {
|
|
348
316
|
return dialectJsonEncoder(this.db.dialect)(value);
|
|
@@ -351,7 +319,7 @@ export class SqlDeviceStore {
|
|
|
351
319
|
assertIssueTokenShape(input);
|
|
352
320
|
try {
|
|
353
321
|
await this.db.query(this.q(`INSERT INTO ${DEVICE_ENROLL_TOKENS_TABLE} (token_hash, target_tenant, target_subject, created_by, created_at, expires_at, consumed_at, consumed_device_id, consumed_pubkey, rev)
|
|
354
|
-
VALUES (?, ?, ?, ?, NOW(3), ${nowPlusSeconds("
|
|
322
|
+
VALUES (?, ?, ?, ?, NOW(3), ${nowPlusSeconds("mysql", "?")}, NULL, NULL, NULL, 0)`, `INSERT INTO ${DEVICE_ENROLL_TOKENS_TABLE} (token_hash, target_tenant, target_subject, created_by, created_at, expires_at, consumed_at, consumed_device_id, consumed_pubkey, rev)
|
|
355
323
|
VALUES ($1, $2, $3, $4, now(), ${nowPlusSeconds("pg", "$5")}, NULL, NULL, NULL, 0)`), [input.tokenHash, input.target.tenant, input.target.subject, input.createdBy, input.ttlSeconds]);
|
|
356
324
|
}
|
|
357
325
|
catch (err) {
|
|
@@ -366,7 +334,7 @@ export class SqlDeviceStore {
|
|
|
366
334
|
}
|
|
367
335
|
async getEnrollToken(tokenHash) {
|
|
368
336
|
assertEnrollTokenHashShape(tokenHash);
|
|
369
|
-
const { rows } = await this.db.query(this.q(`SELECT ${tokenCols("
|
|
337
|
+
const { rows } = await this.db.query(this.q(`SELECT ${tokenCols("mysql")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = ?`, `SELECT ${tokenCols("pg")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = $1`), [tokenHash]);
|
|
370
338
|
return rows[0] ? mapTokenRow(rows[0]) : null;
|
|
371
339
|
}
|
|
372
340
|
async consumeEnrollToken(input) {
|
|
@@ -414,7 +382,7 @@ export class SqlDeviceStore {
|
|
|
414
382
|
return { outcome: "enrolled", device };
|
|
415
383
|
}
|
|
416
384
|
await conn.rollback();
|
|
417
|
-
const { rows } = await conn.query(this.q(`SELECT ${tokenCols("
|
|
385
|
+
const { rows } = await conn.query(this.q(`SELECT ${tokenCols("mysql")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = ?`, `SELECT ${tokenCols("pg")} FROM ${DEVICE_ENROLL_TOKENS_TABLE} WHERE token_hash = $1`), [input.tokenHash]);
|
|
418
386
|
const token = rows[0] ? mapTokenRow(rows[0]) : null;
|
|
419
387
|
if (!token)
|
|
420
388
|
return { outcome: "unknown_token" };
|
|
@@ -437,28 +405,28 @@ export class SqlDeviceStore {
|
|
|
437
405
|
}
|
|
438
406
|
}
|
|
439
407
|
async readDeviceOn(conn, deviceId) {
|
|
440
|
-
const { rows } = await conn.query(this.q(`SELECT ${deviceCols("
|
|
408
|
+
const { rows } = await conn.query(this.q(`SELECT ${deviceCols("mysql")} FROM ${DEVICES_TABLE} WHERE device_id = ?`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1`), [deviceId]);
|
|
441
409
|
return rows[0] ? mapDeviceRow(rows[0]) : null;
|
|
442
410
|
}
|
|
443
411
|
async readDeviceForAdmission(deviceId, owner) {
|
|
444
412
|
assertDeviceIdShape(deviceId);
|
|
445
413
|
assertDeviceOwnerShape(owner);
|
|
446
|
-
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("
|
|
414
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("mysql")}, ${onlineCol("mysql")} FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ?`, `SELECT ${deviceCols("pg")}, ${onlineCol("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3`), [deviceId, owner.tenant, owner.subject]);
|
|
447
415
|
const r = rows[0];
|
|
448
416
|
return r ? { row: mapDeviceRow(r), online: Number(r.online) === 1 } : null;
|
|
449
417
|
}
|
|
450
418
|
async getDeviceUnscoped(deviceId) {
|
|
451
419
|
assertDeviceIdShape(deviceId);
|
|
452
|
-
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("
|
|
420
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("mysql")} FROM ${DEVICES_TABLE} WHERE device_id = ?`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1`), [deviceId]);
|
|
453
421
|
return rows[0] ? mapDeviceRow(rows[0]) : null;
|
|
454
422
|
}
|
|
455
423
|
async listDevicesByOwner(owner) {
|
|
456
424
|
assertDeviceOwnerShape(owner);
|
|
457
|
-
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("
|
|
425
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("mysql")} FROM ${DEVICES_TABLE} WHERE owner_tenant = ? AND owner_subject = ? ORDER BY device_id ASC`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE owner_tenant = $1 AND owner_subject = $2 ORDER BY device_id ASC`), [owner.tenant, owner.subject]);
|
|
458
426
|
return rows.map(mapDeviceRow);
|
|
459
427
|
}
|
|
460
428
|
async listDevicesUnscoped() {
|
|
461
|
-
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("
|
|
429
|
+
const { rows } = await this.db.query(this.q(`SELECT ${deviceCols("mysql")} FROM ${DEVICES_TABLE} ORDER BY device_id ASC`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} ORDER BY device_id ASC`));
|
|
462
430
|
return rows.map(mapDeviceRow);
|
|
463
431
|
}
|
|
464
432
|
async revokeDevice(input) {
|
|
@@ -478,7 +446,7 @@ export class SqlDeviceStore {
|
|
|
478
446
|
return { outcome: "revoked", row };
|
|
479
447
|
}
|
|
480
448
|
await conn.rollback();
|
|
481
|
-
const { rows } = await conn.query(this.q(`SELECT ${deviceCols("
|
|
449
|
+
const { rows } = await conn.query(this.q(`SELECT ${deviceCols("mysql")} FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ?`, `SELECT ${deviceCols("pg")} FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3`), [input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
482
450
|
if (!rows[0])
|
|
483
451
|
return { outcome: "not_found" };
|
|
484
452
|
return { outcome: "already_revoked", row: mapDeviceRow(rows[0]) };
|
|
@@ -496,7 +464,7 @@ export class SqlDeviceStore {
|
|
|
496
464
|
const conn = await this.db.connect();
|
|
497
465
|
try {
|
|
498
466
|
await conn.begin();
|
|
499
|
-
const cas = await conn.query(this.q(`UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = ?, replica_id = ?, lease_expires_at = ${nowPlusSeconds("
|
|
467
|
+
const cas = await conn.query(this.q(`UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = ?, replica_id = ?, lease_expires_at = ${nowPlusSeconds("mysql", "?")}, connected_at = NOW(3), last_seen_at = NOW(3), rev = rev + 1
|
|
500
468
|
WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? AND status = 'active' AND (lease_expires_at IS NULL OR lease_expires_at <= NOW(3))`, `UPDATE ${DEVICES_TABLE} SET conn_generation = conn_generation + 1, conn_epoch = $1, replica_id = $2, lease_expires_at = ${nowPlusSeconds("pg", "$3")}, connected_at = now(), last_seen_at = now(), rev = rev + 1
|
|
501
469
|
WHERE device_id = $4 AND owner_tenant = $5 AND owner_subject = $6 AND status = 'active' AND (lease_expires_at IS NULL OR lease_expires_at <= now())`), [input.connEpoch, input.replicaId, input.leaseSeconds, input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
502
470
|
const { rows } = await conn.query(this.q(`SELECT conn_generation, status FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? FOR UPDATE`, `SELECT conn_generation, status FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3 FOR UPDATE`), [input.deviceId, input.owner.tenant, input.owner.subject]);
|
|
@@ -520,7 +488,7 @@ export class SqlDeviceStore {
|
|
|
520
488
|
async heartbeatConnection(input) {
|
|
521
489
|
assertDeviceIdShape(input.deviceId);
|
|
522
490
|
assertWholeSeconds("leaseSeconds", input.leaseSeconds);
|
|
523
|
-
const res = await this.db.query(this.q(`UPDATE ${DEVICES_TABLE} SET last_seen_at = NOW(3), lease_expires_at = ${nowPlusSeconds("
|
|
491
|
+
const res = await this.db.query(this.q(`UPDATE ${DEVICES_TABLE} SET last_seen_at = NOW(3), lease_expires_at = ${nowPlusSeconds("mysql", "?")}, rev = rev + 1 WHERE device_id = ? AND conn_generation = ? AND status = 'active'`, `UPDATE ${DEVICES_TABLE} SET last_seen_at = now(), lease_expires_at = ${nowPlusSeconds("pg", "$1")}, rev = rev + 1 WHERE device_id = $2 AND conn_generation = $3 AND status = 'active'`), [input.leaseSeconds, input.deviceId, input.generation]);
|
|
524
492
|
return res.affected === 1;
|
|
525
493
|
}
|
|
526
494
|
async releaseConnection(deviceId, generation) {
|
|
@@ -528,51 +496,38 @@ export class SqlDeviceStore {
|
|
|
528
496
|
const res = await this.db.query(this.q(`UPDATE ${DEVICES_TABLE} SET lease_expires_at = NULL, rev = rev + 1 WHERE device_id = ? AND conn_generation = ?`, `UPDATE ${DEVICES_TABLE} SET lease_expires_at = NULL, rev = rev + 1 WHERE device_id = $1 AND conn_generation = $2`), [deviceId, generation]);
|
|
529
497
|
return res.affected === 1;
|
|
530
498
|
}
|
|
531
|
-
async
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
499
|
+
async lockDeviceStatus(conn, deviceId, owner) {
|
|
500
|
+
const dev = await conn.query(this.q(`SELECT status FROM ${DEVICES_TABLE} WHERE device_id = ? AND owner_tenant = ? AND owner_subject = ? FOR UPDATE`, `SELECT status FROM ${DEVICES_TABLE} WHERE device_id = $1 AND owner_tenant = $2 AND owner_subject = $3 FOR UPDATE`), [deviceId, owner.tenant, owner.subject]);
|
|
501
|
+
return dev.rows[0] ? parseDeviceStatus(bufToStr(dev.rows[0].status)) : undefined;
|
|
502
|
+
}
|
|
503
|
+
async placeDeviceExecution(input) {
|
|
504
|
+
assertPlaceDeviceShape(input);
|
|
535
505
|
const conn = await this.db.connect();
|
|
536
506
|
try {
|
|
537
507
|
await conn.begin();
|
|
538
|
-
const
|
|
539
|
-
if (
|
|
508
|
+
const status = await this.lockDeviceStatus(conn, input.deviceId, input.owner);
|
|
509
|
+
if (status !== "active") {
|
|
540
510
|
await conn.rollback();
|
|
541
|
-
return { outcome: "device_not_found" };
|
|
511
|
+
return { outcome: status === undefined ? "device_not_found" : "device_revoked" };
|
|
542
512
|
}
|
|
543
|
-
|
|
513
|
+
const rowsOn = rowsOnSqlExec(conn);
|
|
514
|
+
const reading = await readPlacementOn(rowsOn, this.db.dialect, input.root);
|
|
515
|
+
const settled = settledDeviceBindOutcome(input.root, reading, input.occupant, input.deviceId);
|
|
516
|
+
if (settled !== WRITABLE) {
|
|
544
517
|
await conn.rollback();
|
|
545
|
-
return
|
|
546
|
-
}
|
|
547
|
-
const clock = await conn.query(this.q(`SELECT ${ms("tidb", NOW("tidb"), "bound_at_ms")}`, `SELECT ${ms("pg", NOW("pg"), "bound_at_ms")}`));
|
|
548
|
-
const boundAtMs = Number(clock.rows[0].bound_at_ms);
|
|
549
|
-
const ins = await conn.query(this.q(`INSERT IGNORE INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES (?, ?, ?, ?, FROM_UNIXTIME(? / 1000), 0)`, `INSERT INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES ($1, $2, $3, $4, to_timestamp($5::double precision / 1000.0), 0) ON CONFLICT (root_session_id) DO NOTHING`), [input.rootSessionId, input.deviceId, input.owner.tenant, input.owner.subject, boundAtMs]);
|
|
550
|
-
if (ins.affected === 1) {
|
|
551
|
-
await conn.commit();
|
|
552
|
-
return { outcome: "bound", row: { rootSessionId: input.rootSessionId, deviceId: input.deviceId, owner: input.owner, boundAtMs, rev: 0 } };
|
|
518
|
+
return settled;
|
|
553
519
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
return await this.observeLostBind(input);
|
|
564
|
-
}
|
|
565
|
-
async observeLostBind(input) {
|
|
566
|
-
const conn = await this.db.connect();
|
|
567
|
-
try {
|
|
568
|
-
await conn.begin();
|
|
569
|
-
const { rows } = await conn.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ? FOR UPDATE`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1 FOR UPDATE`), [input.rootSessionId]);
|
|
520
|
+
const now = new Date();
|
|
521
|
+
const device = { id: input.deviceId, owner: input.owner };
|
|
522
|
+
const stmt = reading.cell.role === "absent"
|
|
523
|
+
? placeWriterStatement(this.db.dialect, input.root, input.occupant, { lane: "device", device }, now)
|
|
524
|
+
: bindDeviceStatement(this.db.dialect, input.root, input.occupant, device, now);
|
|
525
|
+
const res = await conn.query(stmt.text, [...stmt.values]);
|
|
526
|
+
if (res.affected !== 1)
|
|
527
|
+
throw new Error(`device-store: first bind of session ${input.root} affected ${res.affected} rows under a held row lock — engine anomaly, refusing to guess`);
|
|
570
528
|
await conn.commit();
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
throw new Error("device-store: the winning session binding vanished before the losing binder could observe it");
|
|
574
|
-
const same = row.deviceId === input.deviceId && row.owner.tenant === input.owner.tenant && row.owner.subject === input.owner.subject;
|
|
575
|
-
return { outcome: same ? "idempotent" : "conflict", row };
|
|
529
|
+
const placed = { id: input.deviceId, owner: { tenant: input.owner.tenant, subject: input.owner.subject }, rev: 0, placedAt: now.toISOString() };
|
|
530
|
+
return { outcome: "bound", device: placed };
|
|
576
531
|
}
|
|
577
532
|
catch (err) {
|
|
578
533
|
await conn.rollback().catch(() => undefined);
|
|
@@ -582,40 +537,41 @@ export class SqlDeviceStore {
|
|
|
582
537
|
conn.release();
|
|
583
538
|
}
|
|
584
539
|
}
|
|
585
|
-
async
|
|
540
|
+
async rebindDeviceExecution(input) {
|
|
586
541
|
assertRebindShape(input);
|
|
587
542
|
const conn = await this.db.connect();
|
|
588
543
|
try {
|
|
589
544
|
await conn.begin();
|
|
590
|
-
|
|
591
|
-
if (!dev.rows[0] || parseDeviceStatus(bufToStr(dev.rows[0].status)) !== "active") {
|
|
545
|
+
if ((await this.lockDeviceStatus(conn, input.targetDeviceId, input.owner)) !== "active") {
|
|
592
546
|
await conn.rollback();
|
|
593
547
|
return { outcome: "device_not_found" };
|
|
594
548
|
}
|
|
595
|
-
const
|
|
596
|
-
|
|
549
|
+
const rowsOn = rowsOnSqlExec(conn);
|
|
550
|
+
const claim = await lockRootAndReadClaim(rowsOn, this.db.dialect, input.root);
|
|
551
|
+
if (!claim.rowPresent) {
|
|
597
552
|
await conn.rollback();
|
|
598
|
-
return { outcome: "
|
|
553
|
+
return { outcome: "gone" };
|
|
599
554
|
}
|
|
600
|
-
|
|
601
|
-
if (row.deviceId === input.toDeviceId && row.rev === input.expectedRev) {
|
|
555
|
+
if (claim.activeTaskId !== null) {
|
|
602
556
|
await conn.rollback();
|
|
603
|
-
return { outcome: "
|
|
557
|
+
return { outcome: "active_run", activeTaskId: claim.activeTaskId };
|
|
604
558
|
}
|
|
605
|
-
|
|
559
|
+
const reading = await readPlacementOn(rowsOn, this.db.dialect, input.root);
|
|
560
|
+
const settled = settledDeviceRebindOutcome(reading, input.occupant, input.owner, input.expectedRev, input.targetDeviceId);
|
|
561
|
+
if (settled !== WRITABLE) {
|
|
606
562
|
await conn.rollback();
|
|
607
|
-
return
|
|
563
|
+
return settled;
|
|
608
564
|
}
|
|
609
|
-
const
|
|
610
|
-
|
|
565
|
+
const from = boundDeviceOf(reading.cell).id;
|
|
566
|
+
const now = new Date();
|
|
567
|
+
const stmt = rebindDeviceStatement(this.db.dialect, input.root, input.occupant, input.owner, input.expectedRev, input.targetDeviceId, now);
|
|
568
|
+
const cas = await conn.query(stmt.text, [...stmt.values]);
|
|
569
|
+
if (cas.affected !== 1)
|
|
611
570
|
throw new Error(`device-store: rebind CAS affected ${cas.affected} rows under a held row lock — engine anomaly, refusing to guess`);
|
|
612
|
-
|
|
613
|
-
await this.insertAuditOn(conn, { event: "session_rebound", deviceId: input.
|
|
614
|
-
const { rows } = await conn.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ?`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1`), [input.rootSessionId]);
|
|
571
|
+
const rev = input.expectedRev + 1;
|
|
572
|
+
await this.insertAuditOn(conn, { event: "session_rebound", deviceId: input.targetDeviceId, owner: input.owner, detail: { from, to: input.targetDeviceId, rev } });
|
|
615
573
|
await conn.commit();
|
|
616
|
-
|
|
617
|
-
throw new Error("device-store: rebound session binding vanished inside its own transaction");
|
|
618
|
-
return { outcome: "rebound", row: mapSessionRow(rows[0]) };
|
|
574
|
+
return { outcome: "rebound", from, device: { id: input.targetDeviceId, owner: { tenant: input.owner.tenant, subject: input.owner.subject }, rev, placedAt: now.toISOString() } };
|
|
619
575
|
}
|
|
620
576
|
catch (err) {
|
|
621
577
|
await conn.rollback().catch(() => undefined);
|
|
@@ -625,16 +581,6 @@ export class SqlDeviceStore {
|
|
|
625
581
|
conn.release();
|
|
626
582
|
}
|
|
627
583
|
}
|
|
628
|
-
async getSessionBinding(rootSessionId) {
|
|
629
|
-
assertRootSessionIdShape(rootSessionId);
|
|
630
|
-
const { rows } = await this.db.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ?`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1`), [rootSessionId]);
|
|
631
|
-
return rows[0] ? mapSessionRow(rows[0]) : null;
|
|
632
|
-
}
|
|
633
|
-
async deleteByRootSession(rootSessionId) {
|
|
634
|
-
assertRootSessionIdShape(rootSessionId);
|
|
635
|
-
const res = await this.db.query(this.q(`DELETE FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ?`, `DELETE FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1`), [rootSessionId]);
|
|
636
|
-
return res.affected;
|
|
637
|
-
}
|
|
638
584
|
async appendAudit(input) {
|
|
639
585
|
await this.insertAuditOn(this.db, input);
|
|
640
586
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export type SqlDialect = "mysql" | "pg";
|
|
1
|
+
import type { SqlDialect } from "./sql-driver.js";
|
|
3
2
|
/**
|
|
4
3
|
* 一条索引的**声明**(纯数据)。两方言共用同一份声明 —— 这就是"一处属主"的兑现物:
|
|
5
4
|
* per-store 文件各导出自己那份 `readonly IndexSpec[]`,PG 侧由该 store 的 `ensurePg*Schema` 应用,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **执行放置的 SQL 唯一模块** —— `session_meta` 上 `execution_*` 七列 + `fork_key` 一列的列定义、住客谓词、行 → 格解码、每一种转换的语句,
|
|
3
|
+
* 与「写事务内先锁根行再读认领」那一读。规则在 `src/execution-placement.ts` 头注;本模块只说 SQL。
|
|
4
|
+
*
|
|
5
|
+
* ## 属主
|
|
6
|
+
* · **列定义唯一属主**(两方言):中央 DDL(`tidb-pool.ts` 的 `CREATE TABLE session_meta`、`pg-session-storage.ts` 的 `PG_SESSION_SCHEMA`)内插
|
|
7
|
+
* {@link sessionMetaPlacementColumnsDdl}(先例:`SESSION_META_INCARNATION_COLUMN`);起服列册对账(`schema-consistency.ts`)从同一份建表文本解析期望列集。
|
|
8
|
+
* 存量库的加列只由一次性迁移做,`ensureSchema` 不 ALTER(SCHEMA POLICY)。
|
|
9
|
+
* · **语句唯一属主**:会话店(执行席、导入写口)与设备店(首绑 / 换绑)、一次性迁移(搬绑定 / 回填)的事务都借这里的语句,别处零手写。
|
|
10
|
+
* 语句是方言无关的 `{ text, values }`(调用方用自己的连接跑:SqlDriver 的 `SqlTxConn`、node-pg 的客户端、mysql2 的连接都行)。
|
|
11
|
+
*
|
|
12
|
+
* ## 住客谓词(写与 judged 复核的唯一定址)
|
|
13
|
+
* `session_id = :root AND incarnation = :inc AND owner <=> :owner(PG:IS NOT DISTINCT FROM)AND placement_kind IS NULL` —— core 行期望判官 `sessionRowMatches`
|
|
14
|
+
* (属主 + 化身;会话行格写原语在锁住的行上判的就是它,`session-row.ts`)的 SQL 拼法(同一化身的属主会变:local M10 认领、identical 重戳、SQL 导入提交点改属主)。
|
|
15
|
+
* 0 行一律按住客复读分类(判官在 `execution-placement.ts`),永不对现住客重试。
|
|
16
|
+
*
|
|
17
|
+
* ## 读写序律(写事务内的认领读)
|
|
18
|
+
* 改根住客 / 放置的每个写者(换绑 CAS · 导入三写口 · identical 补 mirror · 删除终刀 · 留存删除)在写事务内、用该事务自己的连接先 `FOR UPDATE` 锁根
|
|
19
|
+
* `session_meta` 行,再以新语句锁定读 `task_active(root)`({@link lockRootAndReadClaim}):有认领 ⇒ 不写。全局锁序:设备行 → 根 meta → `task_*`
|
|
20
|
+
* (`run-store-sql.ts` 的 `createRun` 同序先锁根行)。认领持有者自己的初始放置写(place / bind)是唯一例外,不读认领、仍按住客谓词。
|
|
21
|
+
*/
|
|
22
|
+
import type { SqlDialect, SqlDriver, SqlExec } from "./sql-driver.js";
|
|
23
|
+
import type { DeviceOwner } from "../device-store.js";
|
|
24
|
+
import { type ExecutionLane } from "../execution-lane-caps.js";
|
|
25
|
+
import { type SessionFace } from "../session-incarnation.js";
|
|
26
|
+
import { type ExecutionPlacementCell, type ExecutionPlacementReading, type ExecutionPlacementSeat } from "../execution-placement.js";
|
|
27
|
+
/** 一条语句:文本 + 按序的参数(`?` / `$n` 已按方言铸好)。 */
|
|
28
|
+
export interface PlacementStatement {
|
|
29
|
+
readonly text: string;
|
|
30
|
+
readonly values: readonly unknown[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `session_meta` 的八列(执行放置七列 + `fork_key`)—— 两方言各一句,**唯一属主**。宽度:车道词 ≤ 32、设备 id 与 `device` 表同宽 64、
|
|
34
|
+
* 属主两列与设备店同宽 190、fork 键 255(服务端拼的不透明串)。PG 文本列 `COLLATE "C"`(与同表同形)。全部可空:NULL = absent / 非键控 fork。
|
|
35
|
+
*/
|
|
36
|
+
export declare const SESSION_META_PLACEMENT_COLUMNS: readonly [{
|
|
37
|
+
readonly name: "execution_role";
|
|
38
|
+
readonly mysql: "execution_role VARCHAR(16) NULL";
|
|
39
|
+
readonly pg: "execution_role VARCHAR(16) COLLATE \"C\"";
|
|
40
|
+
}, {
|
|
41
|
+
readonly name: "execution_lane";
|
|
42
|
+
readonly mysql: "execution_lane VARCHAR(32) NULL";
|
|
43
|
+
readonly pg: "execution_lane VARCHAR(32) COLLATE \"C\"";
|
|
44
|
+
}, {
|
|
45
|
+
readonly name: "execution_device_id";
|
|
46
|
+
readonly mysql: "execution_device_id VARCHAR(64) NULL";
|
|
47
|
+
readonly pg: "execution_device_id VARCHAR(64) COLLATE \"C\"";
|
|
48
|
+
}, {
|
|
49
|
+
readonly name: "execution_device_owner_tenant";
|
|
50
|
+
readonly mysql: "execution_device_owner_tenant VARCHAR(190) NULL";
|
|
51
|
+
readonly pg: "execution_device_owner_tenant VARCHAR(190) COLLATE \"C\"";
|
|
52
|
+
}, {
|
|
53
|
+
readonly name: "execution_device_owner_subject";
|
|
54
|
+
readonly mysql: "execution_device_owner_subject VARCHAR(190) NULL";
|
|
55
|
+
readonly pg: "execution_device_owner_subject VARCHAR(190) COLLATE \"C\"";
|
|
56
|
+
}, {
|
|
57
|
+
readonly name: "execution_placed_at";
|
|
58
|
+
readonly mysql: "execution_placed_at DATETIME(3) NULL";
|
|
59
|
+
readonly pg: "execution_placed_at TIMESTAMPTZ(3)";
|
|
60
|
+
}, {
|
|
61
|
+
readonly name: "execution_rev";
|
|
62
|
+
readonly mysql: "execution_rev BIGINT NULL";
|
|
63
|
+
readonly pg: "execution_rev BIGINT";
|
|
64
|
+
}, {
|
|
65
|
+
readonly name: "fork_key";
|
|
66
|
+
readonly mysql: "fork_key VARCHAR(255) NULL";
|
|
67
|
+
readonly pg: "fork_key VARCHAR(255) COLLATE \"C\"";
|
|
68
|
+
}];
|
|
69
|
+
/** 中央 DDL 内插的那一段(列与列之间逗号分隔,不带首尾逗号)。 */
|
|
70
|
+
export declare function sessionMetaPlacementColumnsDdl(dialect: SqlDialect): string;
|
|
71
|
+
/** 导入提交点同句落 mirror 的 SET 片段与守卫谓词(会话店的提交点 UPDATE 内插这两段:`SET …, ${MIRROR_ON_COMMIT_SET} WHERE session_id = ? AND ${MIRROR_ON_COMMIT_GUARD}`)。 */
|
|
72
|
+
export declare const MIRROR_ON_COMMIT_SET = "execution_role = 'mirror'";
|
|
73
|
+
export declare const MIRROR_ON_COMMIT_GUARD = "(execution_role IS NULL OR execution_role <> 'writer')";
|
|
74
|
+
/** 导入新建 / 换装重插的那一行在执行与 fork 两格上的值:`fork_key` = 沿用的键(或 NULL)、`execution_role` = mirror(列名与值同出一处)。 */
|
|
75
|
+
export declare function importedRowCells(forkKey: string | null): {
|
|
76
|
+
readonly columns: string;
|
|
77
|
+
readonly values: readonly unknown[];
|
|
78
|
+
};
|
|
79
|
+
/** 读一行放置要的列(住客两列 + 委派判别位 + 执行七列;行 → 读数的唯一解码 {@link placementReadingFromRow} 吃的就是这一组)。导入写口的事务内锁读内插它。 */
|
|
80
|
+
export declare const SESSION_META_PLACEMENT_READ_COLUMNS = "owner, incarnation, placement_kind, execution_role, execution_lane, execution_device_id, execution_device_owner_tenant, execution_device_owner_subject, execution_placed_at, execution_rev";
|
|
81
|
+
/** 按根读一行(`lock` ⇒ `FOR UPDATE`:写事务内的当前读;非事务读不加锁)。 */
|
|
82
|
+
export declare function readPlacementRowStatement(dialect: SqlDialect, root: string, opts: {
|
|
83
|
+
readonly lock: boolean;
|
|
84
|
+
}): PlacementStatement;
|
|
85
|
+
/**
|
|
86
|
+
* place:absent → writer(L[, D]),`execution_rev = 0`。非 device 车道由会话店的执行席用(`device` 缺);device 车道的首绑由设备店在锁住设备行的
|
|
87
|
+
* 同一事务里用(`device` 在场)。时刻用调用方的钟(与 `updated_at` 同一只)。
|
|
88
|
+
*/
|
|
89
|
+
export declare function placeWriterStatement(dialect: SqlDialect, root: string, occupant: SessionFace, cell: {
|
|
90
|
+
readonly lane: ExecutionLane;
|
|
91
|
+
readonly device?: {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly owner: DeviceOwner;
|
|
94
|
+
};
|
|
95
|
+
}, now: Date): PlacementStatement;
|
|
96
|
+
/** bind:writer(device, ∅) → writer(device, D)(fresh 首绑撞上迁移回填的格),`execution_rev = 0`。 */
|
|
97
|
+
export declare function bindDeviceStatement(dialect: SqlDialect, root: string, occupant: SessionFace, device: {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly owner: DeviceOwner;
|
|
100
|
+
}, now: Date): PlacementStatement;
|
|
101
|
+
/** rebind:writer(device, D) → writer(device, D'),按住客 ∧ 两属主列 ∧ rev 定址,`execution_rev + 1`(恒变列)。属主两列不动(换绑只在同一属主的设备之间)。 */
|
|
102
|
+
export declare function rebindDeviceStatement(dialect: SqlDialect, root: string, occupant: SessionFace, owner: DeviceOwner, expectedRev: number, targetDeviceId: string, now: Date): PlacementStatement;
|
|
103
|
+
/** identical 臂补 mirror:absent / mirror → mirror,WHERE 住客谓词 ∧ 不是 writer。 */
|
|
104
|
+
export declare function markMirrorStatement(dialect: SqlDialect, root: string, occupant: SessionFace): PlacementStatement;
|
|
105
|
+
/** 回填(一次性迁移):无执行格、非委派分区、有 run 行的会话行 ⇒ writer(表唯一项)。幂等(第二次跑零行)。 */
|
|
106
|
+
export declare function backfillWriterStatement(dialect: SqlDialect, lane: ExecutionLane, now: Date): PlacementStatement;
|
|
107
|
+
/**
|
|
108
|
+
* 搬绑定(一次性迁移):按 `sourceTable`(退役改名后的绑定表,列 = `root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev`)整行覆盖
|
|
109
|
+
* 根会话行的 device 位(`rev` 原值、`bound_at → execution_placed_at`)。只碰非委派分区行。MySQL 协议那张表的列是 VARBINARY ⇒ 转成字符串再比 / 再写。
|
|
110
|
+
* `sourceTable` 是调用方的常量表名(不是用户输入)。
|
|
111
|
+
*/
|
|
112
|
+
export declare function migrateDeviceBindingsStatement(dialect: SqlDialect, sourceTable: string): PlacementStatement;
|
|
113
|
+
/** 搬绑定的孤儿计数(绑定行指向的根会话行不在,或是委派分区行 ⇒ 丢弃):迁移收据里点名的那个数。 */
|
|
114
|
+
export declare function countOrphanBindingsStatement(dialect: SqlDialect, sourceTable: string): PlacementStatement;
|
|
115
|
+
/** 一行放置读不通(角色 / 车道不在闭集,或列的组合不是四态之一)—— 响亮拒,不读作 absent(那等于把一行坏数据当成「还没放」放行给首交)。 */
|
|
116
|
+
export declare class ExecutionPlacementRowError extends Error {
|
|
117
|
+
readonly sessionId: string;
|
|
118
|
+
readonly code = "execution_placement.row_unreadable";
|
|
119
|
+
constructor(sessionId: string, detail: string);
|
|
120
|
+
}
|
|
121
|
+
/** 行 → 格(三态;`forking` 只 local)。列的组合必须是四态之一,否则 {@link ExecutionPlacementRowError}。 */
|
|
122
|
+
export declare function placementCellFromRow(root: string, row: Readonly<Record<string, unknown>>): Exclude<ExecutionPlacementCell, {
|
|
123
|
+
role: "forking";
|
|
124
|
+
}>;
|
|
125
|
+
/** 一行 → 一次读数(住客 + 格)。行不在 / 委派分区行 ⇒ `undefined`;空化身 ⇒ 响亮拒(活住客的投影 `sessionFaceFromRow`)。 */
|
|
126
|
+
export declare function placementReadingFromRow(root: string, row: Readonly<Record<string, unknown>> | undefined): ExecutionPlacementReading | undefined;
|
|
127
|
+
/** 一条语句在调用方事务连接上的执行口(只要行):`SqlTxConn` / node-pg 客户端 / mysql2 连接各包一行即得。 */
|
|
128
|
+
export type PlacementRowsOn = (text: string, values: readonly unknown[]) => Promise<ReadonlyArray<Readonly<Record<string, unknown>>>>;
|
|
129
|
+
/** SqlDriver 连接的执行口。 */
|
|
130
|
+
export declare function rowsOnSqlExec(exec: SqlExec): PlacementRowsOn;
|
|
131
|
+
/** 锁根行之后读认领的读数:`rowPresent: false` ⇒ 根行不在(认领读不做,`activeTaskId` 恒 null)。 */
|
|
132
|
+
export interface RootClaimReading {
|
|
133
|
+
readonly rowPresent: boolean;
|
|
134
|
+
readonly activeTaskId: string | null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 读写序律那一读(头注):**同一连接**上先 `SELECT … FROM session_meta WHERE session_id = ? FOR UPDATE` 锁根行,再以新语句
|
|
138
|
+
* `SELECT task_id FROM task_active WHERE session_id = ? FOR UPDATE` 锁定读认领(PG 读已提交下取新快照、TiDB 悲观事务是当前读)。
|
|
139
|
+
* 只在写事务内调;池级 `getActiveTaskId` 不是它(另一只连接,不在锁序里)。根行不在 ⇒ 不读认领。
|
|
140
|
+
*/
|
|
141
|
+
export declare function lockRootAndReadClaim(conn: PlacementRowsOn, dialect: SqlDialect, root: string): Promise<RootClaimReading>;
|
|
142
|
+
/** 写事务内按根的当前读(`FOR UPDATE`)→ 读数。 */
|
|
143
|
+
export declare function readPlacementOn(conn: PlacementRowsOn, dialect: SqlDialect, root: string): Promise<ExecutionPlacementReading | undefined>;
|
|
144
|
+
/**
|
|
145
|
+
* SQL 会话店的执行席(两孪生同一只实现:语句与判官都在这里与 `execution-placement.ts`,方言只在语句文本里)。`db` = 店自己的池包成的 SqlDriver。
|
|
146
|
+
* · `read` —— 一次池读;
|
|
147
|
+
* · `place` —— 一句自动提交的 UPDATE(认领持有者自己的初始放置写:不读认领、按住客谓词);0 行 ⇒ 按根复读、交判官分类;
|
|
148
|
+
* · `markMirror` —— 一只事务:锁根行读认领 → 锁定读这一行 → 判官 → 有认领 ⇒ `active` → UPDATE(持锁下必中一行)。
|
|
149
|
+
*/
|
|
150
|
+
export declare function createSqlExecutionPlacementSeat(db: Pick<SqlDriver, "dialect" | "query" | "connect">): ExecutionPlacementSeat;
|
|
151
|
+
//# sourceMappingURL=execution-placement-sql.d.ts.map
|