@nylorun/runtime 0.14.0-beta → 0.15.0-beta
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/CHANGELOG.md +104 -0
- package/README.md +75 -33
- package/dist/adapters/execution/restate.js +2 -2
- package/dist/adapters/replication/pgoutput.d.ts +5 -0
- package/dist/adapters/replication/pgoutput.js +4 -3
- package/dist/admin-openapi.json +184 -685
- package/dist/api/components.d.ts +308 -68
- package/dist/api/components.js +4 -5
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/define.js +0 -8
- package/dist/api/http/routes/access.js +25 -18
- package/dist/api/http/routes/tenant.d.ts +2 -1
- package/dist/api/http/routes/tenant.js +77 -5
- package/dist/api/http/routes/vaults.js +2 -2
- package/dist/api/openapi.js +4 -5
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +7 -6
- package/dist/host/admin-api.js +9 -113
- package/dist/host/app.d.ts +1 -1
- package/dist/host/app.js +20 -24
- package/dist/host/config-for.d.ts +1 -1
- package/dist/host/config-for.js +2 -2
- package/dist/host/create-host.d.ts +5 -5
- package/dist/host/create-host.js +5 -15
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +6 -1
- package/dist/host/http.js +17 -0
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +151 -71
- package/dist/host/stack-config.d.ts +71 -19
- package/dist/host/stack-config.js +192 -18
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +21 -3
- package/dist/mcp/pool.js +15 -2
- package/dist/openapi.json +887 -694
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/{store/event.js → record/envelope.js} +1 -1
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/store/postgres/connect.d.ts +15 -12
- package/dist/store/postgres/connect.js +24 -12
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +2 -8
- package/dist/store/postgres/record.js +50 -54
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +481 -567
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +43 -87
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +1 -1
- package/dist/streams/control.js +6 -2
- package/dist/streams/relay/types.d.ts +2 -1
- package/dist/streams/relay/types.js +2 -1
- package/dist/streams/types.d.ts +5 -0
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +95 -16
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.js +4 -2
- package/dist/tenant/context.d.ts +26 -3
- package/dist/tenant/delivery-token.js +17 -18
- package/dist/tenant/delivery.d.ts +1 -1
- package/dist/tenant/delivery.js +18 -13
- package/dist/tenant/effects.d.ts +15 -1
- package/dist/tenant/effects.js +93 -17
- package/dist/tenant/ephemeral.d.ts +16 -7
- package/dist/tenant/ephemeral.js +45 -111
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +14 -28
- package/dist/tenant/module.js +131 -271
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -1
- package/dist/tenant/pool.js +1 -1
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/runtime.d.ts +19 -1
- package/dist/tenant/runtime.js +40 -26
- package/dist/tenant/scheduler.d.ts +4 -2
- package/dist/tenant/scheduler.js +4 -4
- package/dist/tenant/session-streams.d.ts +2 -0
- package/dist/tenant/session-streams.js +8 -3
- package/dist/tenant/status.d.ts +3 -0
- package/dist/tenant/status.js +1 -1
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +9 -25
- package/dist/tenant/streams.js +7 -22
- package/dist/tenant/tokens.js +16 -14
- package/dist/tenant/types.d.ts +41 -92
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.js +4 -4
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +26 -15
- package/dist/vault/service.js +70 -109
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -5
- package/dist/store/memory.d.ts +0 -69
- package/dist/store/memory.js +0 -810
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
- package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
- package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
- package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
- package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
- package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
- package/dist/store/postgres/migrations/007_event_time.js +0 -14
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
- package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
- package/dist/store/postgres/migrations/index.d.ts +0 -53
- package/dist/store/postgres/migrations/index.js +0 -96
- package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
- package/dist/store/postgres/migrations/shared/index.js +0 -92
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -228
- package/dist/streams/relay/memory.d.ts +0 -69
- package/dist/streams/relay/memory.js +0 -176
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
- /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Postgres Session Store (architecture §12.2):
|
|
3
|
-
*
|
|
2
|
+
* The Postgres Session Store (architecture §12.2, session-store.md §2–§3): the `SessionStore`
|
|
3
|
+
* of the one Tenant a database holds, its state in the schema `nylorun` and its record in
|
|
4
|
+
* `nylorun_streams`, queried through Drizzle (`schema.ts`). See `store/types.ts` for the
|
|
5
|
+
* invariants it keeps.
|
|
6
|
+
*
|
|
7
|
+
* ## Queries
|
|
8
|
+
*
|
|
9
|
+
* Plain reads and writes use Drizzle's query builder: `.set(patch)` and `and(...)` skip
|
|
10
|
+
* `undefined`, rows come back in the camelCase of `schema.ts`. Statements whose logic is SQL
|
|
11
|
+
* stay `sql` fragments inside it: the endpoint upsert's `CASE` arms, the log-head upsert,
|
|
12
|
+
* `orphanedSessions`' ordering, `counts()`, advisory locks and array updates.
|
|
4
13
|
*
|
|
5
14
|
* ## Transactions
|
|
6
15
|
*
|
|
7
|
-
* Every `tx` is one READ COMMITTED transaction on a pooled connection
|
|
8
|
-
* are addressed by
|
|
9
|
-
* never through `search_path
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
16
|
+
* Every `tx` is one READ COMMITTED transaction on a pooled connection (Drizzle's
|
|
17
|
+
* `transaction` on postgres.js `begin`). Tables are addressed by schema-qualified names,
|
|
18
|
+
* never through `search_path`. Commit listeners and `afterCommit` callbacks run after `COMMIT`
|
|
19
|
+
* returns and never reject the committed `tx`. Nested `tx` calls are detected with
|
|
20
|
+
* `AsyncLocalStorage` and rejected, and a `Tx` rejects every call once its callback settles.
|
|
21
|
+
* A failed statement rejects `tx` with the driver's error (`driverError`), not Drizzle's
|
|
22
|
+
* wrapper, so its SQLSTATE is `code`.
|
|
14
23
|
*
|
|
15
24
|
* ## Lock ordering
|
|
16
25
|
*
|
|
@@ -19,8 +28,8 @@
|
|
|
19
28
|
* ends. To stay deadlock-free:
|
|
20
29
|
*
|
|
21
30
|
* 1. A session-scoped transaction locks its session row before writing any
|
|
22
|
-
* other row of that session (effects, actions,
|
|
23
|
-
*
|
|
31
|
+
* other row of that session (effects, actions, its record rows and log
|
|
32
|
+
* head).
|
|
24
33
|
* 2. A transaction that touches several sessions locks them in ascending id
|
|
25
34
|
* order, with `lockSessions` (`./locking.ts`), before writing any of them.
|
|
26
35
|
*
|
|
@@ -31,32 +40,44 @@
|
|
|
31
40
|
* ## Values
|
|
32
41
|
*
|
|
33
42
|
* Document and event bodies are `json`, stored as the text `JSON.stringify`
|
|
34
|
-
* wrote, so every string round-trips, including U+0000 and
|
|
35
|
-
* surrogates that `jsonb` rejects
|
|
36
|
-
*
|
|
37
|
-
*
|
|
43
|
+
* wrote (`jsonText` in `schema.ts`), so every string round-trips, including U+0000 and
|
|
44
|
+
* unpaired surrogates that `jsonb` rejects, and key order is kept. Ids and ISO timestamps
|
|
45
|
+
* compared as text are `COLLATE "C"`. Delivery deadlines in action bodies are compared as
|
|
46
|
+
* `timestamptz`.
|
|
38
47
|
*/
|
|
39
48
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import { encodeCursor } from "../cursor.js";
|
|
49
|
+
import { and, count, eq, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, ne, or, sql, } from "drizzle-orm";
|
|
50
|
+
import { appendEvent } from "../../record/index.js";
|
|
43
51
|
import { OwnershipLostError } from "../ownership.js";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import { STREAMS_SCHEMA } from "./migrations/shared/index.js";
|
|
52
|
+
import { database, driverError } from "./db.js";
|
|
53
|
+
import { expectedSchemaVersion, readSchemaVersion } from "./migrate.js";
|
|
47
54
|
import { createPostgresRecordReader } from "./record.js";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const LOG_HEADS = `${STREAMS_SCHEMA}.session_log_heads`;
|
|
55
|
+
import { postgresRecordWriter } from "./record-writer.js";
|
|
56
|
+
import { actions, commands, definitions, effects, endpoints, links, modelBudgets, modelUsage, principals, publishableKeys, sandboxes, sessionEvents, sessionLogHeads, sessions, toolCrossings, signingKeys, subjectEpochs, subjectUsage, tenant, tenantSettings, vaultAudit, vaultCredentials, vaultIdempotency, vaults, } from "./schema.js";
|
|
51
57
|
export function createPostgresSessionStore(options) {
|
|
52
58
|
return new PostgresSessionStore(options);
|
|
53
59
|
}
|
|
54
|
-
const OWNERSHIP_KEYS = new Set(["owner", "epoch", "ownerExpiresAt"]);
|
|
55
60
|
const OPEN_SESSION = ["running", "runnable"];
|
|
61
|
+
const DOCUMENTS = {
|
|
62
|
+
definitions,
|
|
63
|
+
sessions: sessions,
|
|
64
|
+
commands,
|
|
65
|
+
effects: effects,
|
|
66
|
+
actions: actions,
|
|
67
|
+
sandboxes,
|
|
68
|
+
links: links,
|
|
69
|
+
};
|
|
70
|
+
/** A session row as `StoredSession` reads it: the body and the ownership columns. */
|
|
71
|
+
const SESSION = {
|
|
72
|
+
body: sessions.body,
|
|
73
|
+
owner: sessions.owner,
|
|
74
|
+
epoch: sessions.epoch,
|
|
75
|
+
ownerExpiresAt: sessions.ownerExpiresAt,
|
|
76
|
+
};
|
|
56
77
|
class PostgresSessionStore {
|
|
57
78
|
tenantId;
|
|
58
79
|
sql;
|
|
59
|
-
|
|
80
|
+
db;
|
|
60
81
|
schemaVersion;
|
|
61
82
|
active = new AsyncLocalStorage();
|
|
62
83
|
listeners = new Set();
|
|
@@ -67,9 +88,8 @@ class PostgresSessionStore {
|
|
|
67
88
|
constructor(options) {
|
|
68
89
|
this.tenantId = options.tenantId;
|
|
69
90
|
this.sql = options.sql;
|
|
70
|
-
this.
|
|
71
|
-
|
|
72
|
-
this.schemaVersion = options.schemaVersion ?? POSTGRES_SCHEMA_VERSION;
|
|
91
|
+
this.db = database(options.sql);
|
|
92
|
+
this.schemaVersion = options.schemaVersion ?? expectedSchemaVersion();
|
|
73
93
|
this.now = options.now ?? (() => new Date());
|
|
74
94
|
this.onError =
|
|
75
95
|
options.onError ??
|
|
@@ -84,19 +104,22 @@ class PostgresSessionStore {
|
|
|
84
104
|
throw new Error("Nested SessionStore.tx is not allowed");
|
|
85
105
|
let t;
|
|
86
106
|
// Wrapped so `begin` does not treat an array result as queries to await.
|
|
87
|
-
const run = this.
|
|
88
|
-
t = new PostgresTx(
|
|
107
|
+
const run = this.db.transaction(async (db) => {
|
|
108
|
+
t = new PostgresTx(db, this.tenantId, this.now);
|
|
89
109
|
try {
|
|
90
110
|
return { value: await this.active.run(this, () => fn(t)) };
|
|
91
111
|
}
|
|
92
112
|
finally {
|
|
93
113
|
t.closed = true;
|
|
94
114
|
}
|
|
95
|
-
});
|
|
115
|
+
}, { isolationLevel: "read committed" });
|
|
96
116
|
this.inflight.add(run);
|
|
97
117
|
let result;
|
|
98
118
|
try {
|
|
99
|
-
result =
|
|
119
|
+
result = await run;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw driverError(error);
|
|
100
123
|
}
|
|
101
124
|
finally {
|
|
102
125
|
this.inflight.delete(run);
|
|
@@ -137,7 +160,7 @@ class PostgresSessionStore {
|
|
|
137
160
|
if (this.closed)
|
|
138
161
|
return { ok: false, schemaVersion: 0, expectedSchemaVersion };
|
|
139
162
|
try {
|
|
140
|
-
const schemaVersion = (await readSchemaVersion(this.
|
|
163
|
+
const schemaVersion = (await readSchemaVersion(this.db)) ?? 0;
|
|
141
164
|
return {
|
|
142
165
|
ok: schemaVersion === expectedSchemaVersion,
|
|
143
166
|
schemaVersion,
|
|
@@ -154,160 +177,43 @@ class PostgresSessionStore {
|
|
|
154
177
|
await Promise.allSettled([...this.inflight]);
|
|
155
178
|
}
|
|
156
179
|
}
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
180
|
function storedSession(row) {
|
|
159
181
|
return { ...row.body, ...ownership(row) };
|
|
160
182
|
}
|
|
161
183
|
function ownership(row) {
|
|
162
184
|
return {
|
|
163
|
-
owner: row.owner
|
|
164
|
-
epoch:
|
|
165
|
-
ownerExpiresAt: row.
|
|
166
|
-
? new Date(row.owner_expires_at).toISOString()
|
|
167
|
-
: null,
|
|
185
|
+
owner: row.owner,
|
|
186
|
+
epoch: row.epoch,
|
|
187
|
+
ownerExpiresAt: row.ownerExpiresAt?.toISOString() ?? null,
|
|
168
188
|
};
|
|
169
189
|
}
|
|
170
|
-
|
|
171
|
-
const json = JSON.stringify(value);
|
|
172
|
-
if (json === undefined)
|
|
173
|
-
throw new Error("Document body must be JSON");
|
|
174
|
-
return json;
|
|
175
|
-
}
|
|
176
|
-
const bytes = (value) => Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
177
|
-
const fromBytes = (value) => new Uint8Array(value);
|
|
190
|
+
/** An endpoint row with its unset (null) columns left out, as `EndpointRow` has them. */
|
|
178
191
|
function endpointRow(row) {
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
agentId: row.agent_id,
|
|
182
|
-
url: row.url,
|
|
183
|
-
implementationVersion: row.implementation_version,
|
|
184
|
-
...optional("manifestHash", row.manifest_hash),
|
|
185
|
-
timeoutMs: row.timeout_ms,
|
|
186
|
-
maxConcurrent: row.max_concurrent,
|
|
187
|
-
...optional("principalId", row.principal_id),
|
|
188
|
-
...optional("lastDeliveryAt", row.last_delivery_at),
|
|
189
|
-
...optional("lastSuccessAt", row.last_success_at),
|
|
190
|
-
...optional("lastErrorCode", row.last_error_code),
|
|
191
|
-
...optional("lastErrorMessage", row.last_error_message),
|
|
192
|
-
consecutiveFailures: row.consecutive_failures,
|
|
193
|
-
...optional("servedImplementationVersion", row.served_implementation_version),
|
|
194
|
-
...optional("servedManifestHash", row.served_manifest_hash),
|
|
195
|
-
createdAt: row.created_at,
|
|
196
|
-
updatedAt: row.updated_at,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
function principalRow(row) {
|
|
200
|
-
return {
|
|
201
|
-
id: row.id,
|
|
202
|
-
role: row.role,
|
|
203
|
-
tokenHash: row.token_hash,
|
|
204
|
-
idempotencyKey: row.idempotency_key,
|
|
205
|
-
createdAt: row.created_at,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
function signingKeyRow(row) {
|
|
209
|
-
return {
|
|
210
|
-
id: row.id,
|
|
211
|
-
state: row.state,
|
|
212
|
-
alg: row.alg,
|
|
213
|
-
publicJwk: row.public_jwk,
|
|
214
|
-
createdAt: row.created_at,
|
|
215
|
-
activatedAt: row.activated_at,
|
|
216
|
-
retiredAt: row.retired_at,
|
|
217
|
-
revokedAt: row.revoked_at,
|
|
218
|
-
kekId: row.kek_id,
|
|
219
|
-
nonce: fromBytes(row.nonce),
|
|
220
|
-
ciphertext: fromBytes(row.ciphertext),
|
|
221
|
-
wrappedDek: fromBytes(row.wrapped_dek),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
function publishableKeyRow(row) {
|
|
225
|
-
return {
|
|
226
|
-
id: row.id,
|
|
227
|
-
key: row.key,
|
|
228
|
-
name: row.name,
|
|
229
|
-
originsJson: row.origins_json,
|
|
230
|
-
createdAt: row.created_at,
|
|
231
|
-
revokedAt: row.revoked_at,
|
|
232
|
-
};
|
|
192
|
+
return Object.fromEntries(Object.entries(row).filter(([, value]) => value !== null));
|
|
233
193
|
}
|
|
234
194
|
/** The time column a signing key's new state stamps. */
|
|
235
195
|
const SIGNING_KEY_STAMP = {
|
|
236
|
-
current: "
|
|
237
|
-
previous: "
|
|
238
|
-
revoked: "
|
|
196
|
+
current: "activatedAt",
|
|
197
|
+
previous: "retiredAt",
|
|
198
|
+
revoked: "revokedAt",
|
|
239
199
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
scope: row.scope,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function credentialRow(row) {
|
|
251
|
-
return {
|
|
252
|
-
id: row.id,
|
|
253
|
-
vaultId: row.vault_id,
|
|
254
|
-
name: row.name,
|
|
255
|
-
type: row.type,
|
|
256
|
-
bindingJson: row.binding_json,
|
|
257
|
-
expiresAt: row.expires_at,
|
|
258
|
-
createdAt: row.created_at,
|
|
259
|
-
rotatedAt: row.rotated_at,
|
|
260
|
-
kekId: row.kek_id,
|
|
261
|
-
nonce: fromBytes(row.nonce),
|
|
262
|
-
ciphertext: fromBytes(row.ciphertext),
|
|
263
|
-
wrappedDek: fromBytes(row.wrapped_dek),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
function auditRow(row) {
|
|
267
|
-
return {
|
|
268
|
-
id: row.id,
|
|
269
|
-
at: row.at,
|
|
270
|
-
actor: row.actor,
|
|
271
|
-
action: row.action,
|
|
272
|
-
vaultId: row.vault_id,
|
|
273
|
-
credentialId: row.credential_id,
|
|
274
|
-
sessionId: row.session_id,
|
|
275
|
-
target: row.target,
|
|
276
|
-
outcome: row.outcome,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
/** `VaultCredentialPatch` field → column. */
|
|
280
|
-
const CREDENTIAL_COLUMNS = {
|
|
281
|
-
name: "name",
|
|
282
|
-
type: "type",
|
|
283
|
-
bindingJson: "binding_json",
|
|
284
|
-
expiresAt: "expires_at",
|
|
285
|
-
rotatedAt: "rotated_at",
|
|
286
|
-
kekId: "kek_id",
|
|
287
|
-
nonce: "nonce",
|
|
288
|
-
ciphertext: "ciphertext",
|
|
289
|
-
wrappedDek: "wrapped_dek",
|
|
290
|
-
};
|
|
291
|
-
const SESSION_TABLES = [
|
|
292
|
-
"sessions",
|
|
293
|
-
"commands",
|
|
294
|
-
"checkpoints",
|
|
295
|
-
"effects",
|
|
296
|
-
"actions",
|
|
297
|
-
"links",
|
|
298
|
-
];
|
|
200
|
+
/** The audit columns a row has (`ord` only orders them). */
|
|
201
|
+
const { ord: _ord, ...AUDIT } = getTableColumns(vaultAudit);
|
|
202
|
+
const SESSION_TABLES = [sessions, commands, effects, actions, links];
|
|
203
|
+
/** The inserted row's `column` (`ON CONFLICT … DO UPDATE`). */
|
|
204
|
+
const excluded = (column) => sql.raw(`excluded.${column}`);
|
|
205
|
+
/** Keeps an endpoint's health `column` when its URL stays; a new URL starts with `fresh`. */
|
|
206
|
+
const sameUrl = (column, fresh = sql `NULL`) => sql `CASE WHEN ${endpoints.url} = excluded.url THEN ${column} ELSE ${fresh} END`;
|
|
299
207
|
class PostgresTx {
|
|
300
|
-
|
|
301
|
-
schema;
|
|
208
|
+
db;
|
|
302
209
|
tenantId;
|
|
303
210
|
now;
|
|
304
211
|
closed = false;
|
|
305
212
|
events = [];
|
|
306
213
|
generations = [];
|
|
307
214
|
callbacks = [];
|
|
308
|
-
constructor(
|
|
309
|
-
this.
|
|
310
|
-
this.schema = schema;
|
|
215
|
+
constructor(db, tenantId, now) {
|
|
216
|
+
this.db = db;
|
|
311
217
|
this.tenantId = tenantId;
|
|
312
218
|
this.now = now;
|
|
313
219
|
}
|
|
@@ -315,87 +221,67 @@ class PostgresTx {
|
|
|
315
221
|
if (this.closed)
|
|
316
222
|
throw new Error("Tx used after its transaction ended");
|
|
317
223
|
}
|
|
318
|
-
/** The quoted, schema-qualified table. */
|
|
319
|
-
t(table) {
|
|
320
|
-
return this.sql(`${this.schema}.${table}`);
|
|
321
|
-
}
|
|
322
224
|
// --- documents -----------------------------------------------------------
|
|
323
225
|
async get(table, id) {
|
|
324
226
|
this.check();
|
|
325
|
-
const sql = this.sql;
|
|
326
227
|
if (table === "sessions") {
|
|
327
|
-
const [row] = await
|
|
328
|
-
SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
329
|
-
WHERE id = ${id}`;
|
|
228
|
+
const [row] = await this.sessions().where(eq(sessions.id, id));
|
|
330
229
|
return row && storedSession(row);
|
|
331
230
|
}
|
|
332
|
-
const
|
|
231
|
+
const t = DOCUMENTS[table];
|
|
232
|
+
const [row] = await this.db.select({ body: t.body }).from(t).where(eq(t.id, id));
|
|
333
233
|
return row?.body;
|
|
334
234
|
}
|
|
335
235
|
async put(table, id, body) {
|
|
336
236
|
this.check();
|
|
337
237
|
let value = body;
|
|
338
|
-
if (table === "sessions" && body && typeof body === "object")
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
238
|
+
if (table === "sessions" && body && typeof body === "object") {
|
|
239
|
+
// Ownership is store-managed: never in the body.
|
|
240
|
+
const { owner: _owner, epoch: _epoch, ownerExpiresAt: _expires, ...rest } = body;
|
|
241
|
+
value = rest;
|
|
242
|
+
}
|
|
243
|
+
const t = DOCUMENTS[table];
|
|
244
|
+
await this.db
|
|
245
|
+
.insert(t)
|
|
246
|
+
.values({ id, body: value })
|
|
247
|
+
.onConflictDoUpdate({ target: t.id, set: { body: excluded("body") } });
|
|
344
248
|
}
|
|
345
249
|
async delete(table, id) {
|
|
346
250
|
this.check();
|
|
347
251
|
// A session's record rows and log head stay: a session created again with this id
|
|
348
252
|
// continues its log (per-session record deletion is deferred, Durable Streams §15).
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
355
|
-
${where ? sql `WHERE ${where}` : sql ``}
|
|
356
|
-
${tail ?? sql `ORDER BY id`}`;
|
|
357
|
-
return rows.map((row) => storedSession(row));
|
|
253
|
+
const t = DOCUMENTS[table];
|
|
254
|
+
await this.db.delete(t).where(eq(t.id, id));
|
|
255
|
+
}
|
|
256
|
+
sessions() {
|
|
257
|
+
return this.db.select(SESSION).from(sessions).$dynamic();
|
|
358
258
|
}
|
|
359
259
|
// --- ordering ------------------------------------------------------------
|
|
360
260
|
async lockSession(id) {
|
|
361
261
|
this.check();
|
|
362
|
-
const [row] = await this.
|
|
363
|
-
SELECT body, owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
364
|
-
WHERE id = ${id} FOR UPDATE`;
|
|
262
|
+
const [row] = await this.sessions().where(eq(sessions.id, id)).for("update");
|
|
365
263
|
return row && storedSession(row);
|
|
366
264
|
}
|
|
367
265
|
async event(sessionId, turnId, type, payload) {
|
|
368
266
|
this.check();
|
|
369
|
-
const
|
|
267
|
+
const db = this.db;
|
|
370
268
|
// The session row lock orders the session's events; the log head allocates under it.
|
|
371
|
-
const [session] = await
|
|
372
|
-
|
|
269
|
+
const [session] = await db
|
|
270
|
+
.select({ epoch: sessions.epoch })
|
|
271
|
+
.from(sessions)
|
|
272
|
+
.where(eq(sessions.id, sessionId))
|
|
273
|
+
.for("update");
|
|
373
274
|
if (!session)
|
|
374
275
|
throw new Error(`Session ${sessionId} not found`);
|
|
375
|
-
const
|
|
376
|
-
INSERT INTO ${sql(LOG_HEADS)} (tenant_id, session_id, generation, head)
|
|
377
|
-
VALUES (
|
|
378
|
-
${this.tenantId}, ${sessionId},
|
|
379
|
-
coalesce((SELECT basin_generation FROM ${this.t("tenant")}), 0), 1)
|
|
380
|
-
ON CONFLICT (tenant_id, session_id)
|
|
381
|
-
DO UPDATE SET head = ${sql(LOG_HEADS)}.head + 1
|
|
382
|
-
RETURNING head - 1 AS seq, generation`;
|
|
383
|
-
const seq = Number(row.seq);
|
|
384
|
-
const generation = Number(row.generation);
|
|
385
|
-
const event = buildEvent({
|
|
276
|
+
const { event, generation } = await appendEvent(postgresRecordWriter(db), {
|
|
386
277
|
tenantId: this.tenantId,
|
|
387
278
|
sessionId,
|
|
388
279
|
turnId,
|
|
389
|
-
|
|
390
|
-
epoch: Number(session.epoch),
|
|
280
|
+
epoch: session.epoch,
|
|
391
281
|
time: this.now(),
|
|
392
282
|
type,
|
|
393
283
|
payload,
|
|
394
284
|
});
|
|
395
|
-
await sql `
|
|
396
|
-
INSERT INTO ${sql(SESSION_EVENTS)} (tenant_id, session_id, seq, generation, type, body)
|
|
397
|
-
VALUES (${this.tenantId}, ${sessionId}, ${seq}, ${generation}, ${type},
|
|
398
|
-
${JSON.stringify(event)}::text::json)`;
|
|
399
285
|
this.events.push(event);
|
|
400
286
|
this.generations.push(generation);
|
|
401
287
|
return structuredClone(event);
|
|
@@ -407,10 +293,7 @@ class PostgresTx {
|
|
|
407
293
|
// --- ownership -----------------------------------------------------------
|
|
408
294
|
async takeOwnership(sessionId, claim) {
|
|
409
295
|
this.check();
|
|
410
|
-
const
|
|
411
|
-
const [row] = await sql `
|
|
412
|
-
SELECT owner, epoch, owner_expires_at FROM ${this.t("sessions")}
|
|
413
|
-
WHERE id = ${sessionId} FOR UPDATE`;
|
|
296
|
+
const [row] = await this.sessions().where(eq(sessions.id, sessionId)).for("update");
|
|
414
297
|
if (!row)
|
|
415
298
|
return { status: "missing" };
|
|
416
299
|
const previous = ownership(row);
|
|
@@ -422,30 +305,36 @@ class PostgresTx {
|
|
|
422
305
|
owner: previous.owner,
|
|
423
306
|
ownerExpiresAt: previous.ownerExpiresAt,
|
|
424
307
|
};
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
308
|
+
const [updated] = await this.db
|
|
309
|
+
.update(sessions)
|
|
310
|
+
.set({
|
|
311
|
+
owner: claim.owner,
|
|
312
|
+
epoch: sql `${sessions.epoch} + 1`,
|
|
313
|
+
ownerExpiresAt: new Date(claim.now.getTime() + claim.leaseMs),
|
|
314
|
+
})
|
|
315
|
+
.where(eq(sessions.id, sessionId))
|
|
316
|
+
.returning({ epoch: sessions.epoch });
|
|
430
317
|
return {
|
|
431
318
|
status: "owned",
|
|
432
|
-
epoch:
|
|
319
|
+
epoch: updated.epoch,
|
|
433
320
|
takeover: previous.owner !== null,
|
|
434
321
|
previous,
|
|
435
322
|
};
|
|
436
323
|
}
|
|
437
324
|
async renewOwnership(sessionId, owner, epoch, until) {
|
|
438
325
|
this.check();
|
|
439
|
-
const result = await this.
|
|
440
|
-
|
|
441
|
-
|
|
326
|
+
const result = await this.db
|
|
327
|
+
.update(sessions)
|
|
328
|
+
.set({ ownerExpiresAt: until })
|
|
329
|
+
.where(and(eq(sessions.id, sessionId), eq(sessions.owner, owner), eq(sessions.epoch, epoch)));
|
|
442
330
|
return result.count > 0;
|
|
443
331
|
}
|
|
444
332
|
async releaseOwnership(sessionId, owner, epoch) {
|
|
445
333
|
this.check();
|
|
446
|
-
const result = await this.
|
|
447
|
-
|
|
448
|
-
|
|
334
|
+
const result = await this.db
|
|
335
|
+
.update(sessions)
|
|
336
|
+
.set({ owner: null, ownerExpiresAt: null })
|
|
337
|
+
.where(and(eq(sessions.id, sessionId), eq(sessions.owner, owner), eq(sessions.epoch, epoch)));
|
|
449
338
|
return result.count > 0;
|
|
450
339
|
}
|
|
451
340
|
async assertEpoch(sessionId, epoch) {
|
|
@@ -459,96 +348,97 @@ class PostgresTx {
|
|
|
459
348
|
// --- typed queries -------------------------------------------------------
|
|
460
349
|
async sessionsWithStatus(statuses) {
|
|
461
350
|
this.check();
|
|
462
|
-
|
|
351
|
+
const rows = await this.sessions()
|
|
352
|
+
.where(inArray(sessions.status, statuses))
|
|
353
|
+
.orderBy(sessions.id);
|
|
354
|
+
return rows.map((row) => storedSession(row));
|
|
463
355
|
}
|
|
464
356
|
async orphanedSessions(now, limit) {
|
|
465
357
|
this.check();
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
358
|
+
const rows = await this.sessions()
|
|
359
|
+
.where(and(inArray(sessions.status, OPEN_SESSION), or(isNull(sessions.owner), isNull(sessions.ownerExpiresAt), lte(sessions.ownerExpiresAt, now))))
|
|
360
|
+
// Never-owned sessions first, then the longest expired.
|
|
361
|
+
.orderBy(sql `CASE WHEN ${sessions.owner} IS NULL THEN NULL ELSE ${sessions.ownerExpiresAt} END ASC NULLS FIRST`, sessions.id)
|
|
362
|
+
.limit(limit);
|
|
363
|
+
return rows.map((row) => storedSession(row));
|
|
472
364
|
}
|
|
473
365
|
async listSessions(filter = {}) {
|
|
474
366
|
this.check();
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
...(filter.agentId === undefined ? [] : [sql `agent_id = ${filter.agentId}`]),
|
|
478
|
-
...(filter.ownerUserId === undefined
|
|
479
|
-
? []
|
|
480
|
-
: [sql `owner_user_id = ${filter.ownerUserId}`]),
|
|
481
|
-
];
|
|
482
|
-
return this.sessionRows(conditions.length === 0
|
|
367
|
+
const rows = await this.sessions()
|
|
368
|
+
.where(and(filter.agentId === undefined ? undefined : eq(sessions.agentId, filter.agentId), filter.ownerUserId === undefined
|
|
483
369
|
? undefined
|
|
484
|
-
:
|
|
370
|
+
: eq(sessions.ownerUserId, filter.ownerUserId)))
|
|
371
|
+
.orderBy(sessions.id);
|
|
372
|
+
return rows.map((row) => storedSession(row));
|
|
485
373
|
}
|
|
486
374
|
async bodies(table) {
|
|
487
|
-
const rows = await this.
|
|
375
|
+
const rows = await this.db.select({ body: table.body }).from(table).orderBy(table.id);
|
|
488
376
|
return rows.map((row) => row.body);
|
|
489
377
|
}
|
|
490
378
|
async listDefinitions() {
|
|
491
379
|
this.check();
|
|
492
|
-
return this.bodies(
|
|
380
|
+
return this.bodies(definitions);
|
|
493
381
|
}
|
|
494
382
|
async listSandboxes() {
|
|
495
383
|
this.check();
|
|
496
|
-
return this.bodies(
|
|
384
|
+
return this.bodies(sandboxes);
|
|
385
|
+
}
|
|
386
|
+
async actionBodies(where) {
|
|
387
|
+
const rows = await this.db
|
|
388
|
+
.select({ body: actions.body })
|
|
389
|
+
.from(actions)
|
|
390
|
+
.where(where)
|
|
391
|
+
.orderBy(actions.id);
|
|
392
|
+
return rows.map((row) => row.body);
|
|
497
393
|
}
|
|
498
394
|
async pendingActions(agentId) {
|
|
499
395
|
this.check();
|
|
500
|
-
|
|
501
|
-
SELECT body FROM ${this.t("actions")}
|
|
502
|
-
WHERE agent_id = ${agentId} AND status = 'pending'
|
|
503
|
-
ORDER BY id`;
|
|
504
|
-
return rows.map((row) => row.body);
|
|
396
|
+
return this.actionBodies(and(eq(actions.agentId, agentId), eq(actions.status, "pending")));
|
|
505
397
|
}
|
|
506
398
|
async deliveringCount(agentId) {
|
|
507
399
|
this.check();
|
|
508
|
-
const [row] = await this.
|
|
509
|
-
|
|
510
|
-
|
|
400
|
+
const [row] = await this.db
|
|
401
|
+
.select({ n: count() })
|
|
402
|
+
.from(actions)
|
|
403
|
+
.where(and(eq(actions.agentId, agentId), eq(actions.status, "delivering")));
|
|
511
404
|
return row.n;
|
|
512
405
|
}
|
|
513
406
|
async pendingActionsWithEndpoint(limit) {
|
|
514
407
|
this.check();
|
|
515
|
-
const rows = await this.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
408
|
+
const rows = await this.db
|
|
409
|
+
.select({ body: actions.body })
|
|
410
|
+
.from(actions)
|
|
411
|
+
.innerJoin(endpoints, eq(endpoints.agentId, actions.agentId))
|
|
412
|
+
.where(eq(actions.status, "pending"))
|
|
413
|
+
.orderBy(actions.id)
|
|
414
|
+
.limit(limit);
|
|
521
415
|
return rows.map((row) => row.body);
|
|
522
416
|
}
|
|
523
417
|
async expiredDeliveries(now, limit) {
|
|
524
418
|
this.check();
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
419
|
+
const deadline = sql `${actions.deadlineAt}::timestamptz`;
|
|
420
|
+
const rows = await this.db
|
|
421
|
+
.select({ body: actions.body })
|
|
422
|
+
.from(actions)
|
|
423
|
+
.where(and(eq(actions.status, "delivering"), isNotNull(actions.deadlineAt), sql `${deadline} <= ${now.toISOString()}::timestamptz`))
|
|
424
|
+
.orderBy(deadline, actions.id)
|
|
425
|
+
.limit(limit);
|
|
531
426
|
return rows.map((row) => row.body);
|
|
532
427
|
}
|
|
533
428
|
async actionsForSession(sessionId, filter = {}) {
|
|
534
429
|
this.check();
|
|
535
|
-
|
|
536
|
-
const rows = await sql `
|
|
537
|
-
SELECT body FROM ${this.t("actions")}
|
|
538
|
-
WHERE session_id = ${sessionId}
|
|
539
|
-
${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
|
|
540
|
-
${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
|
|
541
|
-
ORDER BY id`;
|
|
542
|
-
return rows.map((row) => row.body);
|
|
430
|
+
return this.actionBodies(and(eq(actions.sessionId, sessionId), filter.turnId === undefined ? undefined : eq(actions.turnId, filter.turnId), filter.statuses === undefined ? undefined : inArray(actions.status, filter.statuses)));
|
|
543
431
|
}
|
|
544
432
|
async actionsWithStatus(statuses, filter = {}) {
|
|
545
433
|
this.check();
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
434
|
+
return this.actionBodies(and(inArray(actions.status, statuses), filter.kinds === undefined ? undefined : inArray(actions.kind, filter.kinds)));
|
|
435
|
+
}
|
|
436
|
+
async effectBodies(where) {
|
|
437
|
+
const rows = await this.db
|
|
438
|
+
.select({ body: effects.body })
|
|
439
|
+
.from(effects)
|
|
440
|
+
.where(where)
|
|
441
|
+
.orderBy(effects.id);
|
|
552
442
|
return rows.map((row) => row.body);
|
|
553
443
|
}
|
|
554
444
|
async invokingEffects(sessionId) {
|
|
@@ -556,35 +446,23 @@ class PostgresTx {
|
|
|
556
446
|
}
|
|
557
447
|
async effectsForSession(sessionId, filter = {}) {
|
|
558
448
|
this.check();
|
|
559
|
-
|
|
560
|
-
const rows = await sql `
|
|
561
|
-
SELECT body FROM ${this.t("effects")}
|
|
562
|
-
WHERE session_id = ${sessionId}
|
|
563
|
-
${filter.turnId === undefined ? sql `` : sql `AND turn_id = ${filter.turnId}`}
|
|
564
|
-
${filter.statuses === undefined ? sql `` : sql `AND status = ANY(${[...filter.statuses]})`}
|
|
565
|
-
ORDER BY id`;
|
|
566
|
-
return rows.map((row) => row.body);
|
|
449
|
+
return this.effectBodies(and(eq(effects.sessionId, sessionId), filter.turnId === undefined ? undefined : eq(effects.turnId, filter.turnId), filter.statuses === undefined ? undefined : inArray(effects.status, filter.statuses)));
|
|
567
450
|
}
|
|
568
451
|
async effectsForTurn(sessionId, turnId, statuses) {
|
|
569
452
|
return this.effectsForSession(sessionId, { turnId, statuses });
|
|
570
453
|
}
|
|
571
454
|
async effectsWithStatus(statuses, filter = {}) {
|
|
572
455
|
this.check();
|
|
573
|
-
|
|
574
|
-
const rows = await sql `
|
|
575
|
-
SELECT body FROM ${this.t("effects")}
|
|
576
|
-
WHERE status = ANY(${[...statuses]})
|
|
577
|
-
${filter.kinds === undefined ? sql `` : sql `AND kind = ANY(${[...filter.kinds]})`}
|
|
578
|
-
ORDER BY id`;
|
|
579
|
-
return rows.map((row) => row.body);
|
|
456
|
+
return this.effectBodies(and(inArray(effects.status, statuses), filter.kinds === undefined ? undefined : inArray(effects.kind, filter.kinds)));
|
|
580
457
|
}
|
|
581
458
|
async linkedSessions(workflowSessionId) {
|
|
582
459
|
this.check();
|
|
583
|
-
const rows = await this.
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
460
|
+
const rows = await this.db
|
|
461
|
+
.select({ id: links.id, link: links.body, ...SESSION })
|
|
462
|
+
.from(links)
|
|
463
|
+
.innerJoin(sessions, eq(sessions.id, links.id))
|
|
464
|
+
.where(eq(links.workflowSessionId, workflowSessionId))
|
|
465
|
+
.orderBy(links.id);
|
|
588
466
|
return rows.map((row) => ({
|
|
589
467
|
agentSessionId: row.id,
|
|
590
468
|
link: row.link,
|
|
@@ -593,15 +471,16 @@ class PostgresTx {
|
|
|
593
471
|
}
|
|
594
472
|
async counts() {
|
|
595
473
|
this.check();
|
|
596
|
-
const
|
|
597
|
-
const [row] = await sql `
|
|
474
|
+
const [row] = await this.db.execute(sql `
|
|
598
475
|
SELECT
|
|
599
|
-
(SELECT count(*) FROM ${
|
|
600
|
-
(SELECT count(*) FROM ${
|
|
601
|
-
|
|
602
|
-
(SELECT count(*) FROM ${
|
|
603
|
-
|
|
604
|
-
(SELECT count(*) FROM ${
|
|
476
|
+
(SELECT count(*) FROM ${sessions})::int AS sessions,
|
|
477
|
+
(SELECT count(*) FROM ${sessions}
|
|
478
|
+
WHERE ${sessions.status} IN ('running', 'runnable'))::int AS running,
|
|
479
|
+
(SELECT count(*) FROM ${actions}
|
|
480
|
+
WHERE ${actions.status} IN ('pending', 'delivering'))::int AS actions,
|
|
481
|
+
(SELECT count(*) FROM ${effects} WHERE ${effects.status} = 'uncertain')::int AS uncertain,
|
|
482
|
+
(SELECT count(*) FROM ${sandboxes})::int AS sandboxes,
|
|
483
|
+
(SELECT count(*) FROM ${definitions})::int AS definitions`);
|
|
605
484
|
return {
|
|
606
485
|
sessions: row.sessions,
|
|
607
486
|
runningSessions: row.running,
|
|
@@ -614,426 +493,461 @@ class PostgresTx {
|
|
|
614
493
|
// --- basin generations --------------------------------------------------
|
|
615
494
|
async basinGenerations() {
|
|
616
495
|
this.check();
|
|
617
|
-
const [row] = await this.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
return {
|
|
623
|
-
current: Number(row.basin_generation),
|
|
624
|
-
retired: row.retired_generations.map(Number),
|
|
625
|
-
};
|
|
496
|
+
const [row] = await this.db
|
|
497
|
+
.select({ current: tenant.basinGeneration, retired: tenant.retiredGenerations })
|
|
498
|
+
.from(tenant);
|
|
499
|
+
// A database whose Tenant row is not written yet is at generation 0.
|
|
500
|
+
return row ?? { current: 0, retired: [] };
|
|
626
501
|
}
|
|
627
502
|
async forgetRetiredGeneration(generation) {
|
|
628
503
|
this.check();
|
|
629
|
-
await this.
|
|
630
|
-
|
|
631
|
-
|
|
504
|
+
await this.db.update(tenant).set({
|
|
505
|
+
retiredGenerations: sql `array_remove(${tenant.retiredGenerations}, ${generation}::int)`,
|
|
506
|
+
});
|
|
632
507
|
}
|
|
633
508
|
// --- Action endpoints -----------------------------------------------------
|
|
634
509
|
async listEndpoints() {
|
|
635
510
|
this.check();
|
|
636
|
-
const rows = await this.
|
|
511
|
+
const rows = await this.db.select().from(endpoints).orderBy(endpoints.agentId);
|
|
637
512
|
return rows.map(endpointRow);
|
|
638
513
|
}
|
|
639
514
|
async getEndpoint(agentId) {
|
|
640
515
|
this.check();
|
|
641
|
-
const [row] = await this.
|
|
642
|
-
SELECT * FROM ${this.t("endpoints")} WHERE agent_id = ${agentId}`;
|
|
516
|
+
const [row] = await this.db.select().from(endpoints).where(eq(endpoints.agentId, agentId));
|
|
643
517
|
return row && endpointRow(row);
|
|
644
518
|
}
|
|
645
519
|
async putEndpoint(row) {
|
|
646
520
|
this.check();
|
|
647
521
|
// Health belongs to a URL: a new URL starts with none.
|
|
648
|
-
await this.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
522
|
+
await this.db
|
|
523
|
+
.insert(endpoints)
|
|
524
|
+
.values({
|
|
525
|
+
agentId: row.agentId,
|
|
526
|
+
url: row.url,
|
|
527
|
+
implementationVersion: row.implementationVersion,
|
|
528
|
+
manifestHash: row.manifestHash ?? null,
|
|
529
|
+
timeoutMs: row.timeoutMs,
|
|
530
|
+
maxConcurrent: row.maxConcurrent,
|
|
531
|
+
principalId: row.principalId ?? null,
|
|
532
|
+
consecutiveFailures: 0,
|
|
533
|
+
createdAt: row.updatedAt,
|
|
534
|
+
updatedAt: row.updatedAt,
|
|
535
|
+
})
|
|
536
|
+
.onConflictDoUpdate({
|
|
537
|
+
target: endpoints.agentId,
|
|
538
|
+
set: {
|
|
539
|
+
url: excluded("url"),
|
|
540
|
+
implementationVersion: excluded("implementation_version"),
|
|
541
|
+
manifestHash: excluded("manifest_hash"),
|
|
542
|
+
timeoutMs: excluded("timeout_ms"),
|
|
543
|
+
maxConcurrent: excluded("max_concurrent"),
|
|
544
|
+
principalId: excluded("principal_id"),
|
|
545
|
+
updatedAt: excluded("updated_at"),
|
|
546
|
+
lastDeliveryAt: sameUrl(sql `${endpoints.lastDeliveryAt}`),
|
|
547
|
+
lastSuccessAt: sameUrl(sql `${endpoints.lastSuccessAt}`),
|
|
548
|
+
lastErrorCode: sameUrl(sql `${endpoints.lastErrorCode}`),
|
|
549
|
+
lastErrorMessage: sameUrl(sql `${endpoints.lastErrorMessage}`),
|
|
550
|
+
consecutiveFailures: sameUrl(sql `${endpoints.consecutiveFailures}`, sql `0`),
|
|
551
|
+
servedImplementationVersion: sameUrl(sql `${endpoints.servedImplementationVersion}`),
|
|
552
|
+
servedManifestHash: sameUrl(sql `${endpoints.servedManifestHash}`),
|
|
553
|
+
},
|
|
554
|
+
});
|
|
675
555
|
}
|
|
676
556
|
async deleteEndpoint(agentId) {
|
|
677
557
|
this.check();
|
|
678
|
-
await this.
|
|
558
|
+
await this.db.delete(endpoints).where(eq(endpoints.agentId, agentId));
|
|
679
559
|
}
|
|
680
560
|
async recordEndpointHealth(agentId, update) {
|
|
681
561
|
this.check();
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
562
|
+
const set = update.kind === "success"
|
|
563
|
+
? {
|
|
564
|
+
lastDeliveryAt: update.at,
|
|
565
|
+
lastSuccessAt: update.at,
|
|
566
|
+
consecutiveFailures: 0,
|
|
567
|
+
lastErrorCode: null,
|
|
568
|
+
lastErrorMessage: null,
|
|
569
|
+
}
|
|
570
|
+
: update.kind === "failure"
|
|
571
|
+
? {
|
|
572
|
+
lastDeliveryAt: update.at,
|
|
573
|
+
lastErrorCode: update.code,
|
|
574
|
+
lastErrorMessage: update.message,
|
|
575
|
+
consecutiveFailures: sql `${endpoints.consecutiveFailures} + 1`,
|
|
576
|
+
}
|
|
577
|
+
: {
|
|
578
|
+
servedImplementationVersion: update.implementationVersion,
|
|
579
|
+
servedManifestHash: update.manifestHash ?? null,
|
|
580
|
+
};
|
|
581
|
+
await this.db.update(endpoints).set(set).where(eq(endpoints.agentId, agentId));
|
|
704
582
|
}
|
|
705
583
|
// --- principals ----------------------------------------------------------
|
|
706
584
|
async insertPrincipal(row) {
|
|
707
585
|
this.check();
|
|
708
|
-
await this.
|
|
709
|
-
INSERT INTO ${this.t("principals")} (id, role, token_hash, idempotency_key, created_at)
|
|
710
|
-
VALUES (${row.id}, ${row.role}, ${row.tokenHash}, ${row.idempotencyKey}, ${row.createdAt})`;
|
|
586
|
+
await this.db.insert(principals).values(row);
|
|
711
587
|
}
|
|
712
588
|
async principalByTokenHash(tokenHash) {
|
|
713
589
|
this.check();
|
|
714
|
-
const [row] = await this.
|
|
715
|
-
|
|
716
|
-
|
|
590
|
+
const [row] = await this.db
|
|
591
|
+
.select()
|
|
592
|
+
.from(principals)
|
|
593
|
+
.where(eq(principals.tokenHash, tokenHash));
|
|
594
|
+
return row;
|
|
717
595
|
}
|
|
718
596
|
async principalById(id) {
|
|
719
597
|
this.check();
|
|
720
|
-
const [row] = await this.
|
|
721
|
-
|
|
722
|
-
return row && principalRow(row);
|
|
598
|
+
const [row] = await this.db.select().from(principals).where(eq(principals.id, id));
|
|
599
|
+
return row;
|
|
723
600
|
}
|
|
724
601
|
async applicationTokenHashes() {
|
|
725
602
|
this.check();
|
|
726
|
-
const rows = await this.
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
603
|
+
const rows = await this.db
|
|
604
|
+
.select({ tokenHash: principals.tokenHash })
|
|
605
|
+
.from(principals)
|
|
606
|
+
.where(eq(principals.role, "application"))
|
|
607
|
+
.orderBy(principals.id);
|
|
608
|
+
return rows.map((row) => row.tokenHash);
|
|
730
609
|
}
|
|
731
610
|
// --- vault ---------------------------------------------------------------
|
|
732
611
|
async insertVault(row) {
|
|
733
612
|
this.check();
|
|
734
|
-
await this.
|
|
735
|
-
INSERT INTO ${this.t("vaults")} (id, name, owner_user_id, metadata_json, created_at, scope)
|
|
736
|
-
VALUES (${row.id}, ${row.name}, ${row.ownerUserId}, ${row.metadataJson},
|
|
737
|
-
${row.createdAt}, ${row.scope})`;
|
|
613
|
+
await this.db.insert(vaults).values(row);
|
|
738
614
|
}
|
|
739
615
|
async getVault(id) {
|
|
740
616
|
this.check();
|
|
741
|
-
const [row] = await this.
|
|
742
|
-
return row
|
|
617
|
+
const [row] = await this.db.select().from(vaults).where(eq(vaults.id, id));
|
|
618
|
+
return row;
|
|
743
619
|
}
|
|
744
620
|
async vaultsByOwner(ownerUserId) {
|
|
745
621
|
this.check();
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
622
|
+
return this.db
|
|
623
|
+
.select()
|
|
624
|
+
.from(vaults)
|
|
625
|
+
.where(and(eq(vaults.ownerUserId, ownerUserId), eq(vaults.scope, "user")))
|
|
626
|
+
.orderBy(vaults.createdAt, vaults.id);
|
|
751
627
|
}
|
|
752
628
|
async updateVaultMetadata(id, metadataJson) {
|
|
753
629
|
this.check();
|
|
754
|
-
await this.
|
|
755
|
-
UPDATE ${this.t("vaults")} SET metadata_json = ${metadataJson} WHERE id = ${id}`;
|
|
630
|
+
await this.db.update(vaults).set({ metadataJson }).where(eq(vaults.id, id));
|
|
756
631
|
}
|
|
757
632
|
async deleteVault(id) {
|
|
758
633
|
this.check();
|
|
759
634
|
// Credentials go with it (ON DELETE CASCADE).
|
|
760
|
-
await this.
|
|
635
|
+
await this.db.delete(vaults).where(eq(vaults.id, id));
|
|
761
636
|
}
|
|
762
637
|
async insertCredential(row) {
|
|
763
638
|
this.check();
|
|
764
|
-
await this.
|
|
765
|
-
INSERT INTO ${this.t("vault_credentials")} (
|
|
766
|
-
id, vault_id, name, type, binding_json, expires_at, created_at,
|
|
767
|
-
rotated_at, kek_id, nonce, ciphertext, wrapped_dek
|
|
768
|
-
) VALUES (
|
|
769
|
-
${row.id}, ${row.vaultId}, ${row.name}, ${row.type}, ${row.bindingJson},
|
|
770
|
-
${row.expiresAt}, ${row.createdAt}, ${row.rotatedAt}, ${row.kekId},
|
|
771
|
-
${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)}
|
|
772
|
-
)`;
|
|
639
|
+
await this.db.insert(vaultCredentials).values(row);
|
|
773
640
|
}
|
|
774
641
|
async getCredential(id) {
|
|
775
642
|
this.check();
|
|
776
|
-
const [row] = await this.
|
|
777
|
-
|
|
778
|
-
|
|
643
|
+
const [row] = await this.db
|
|
644
|
+
.select()
|
|
645
|
+
.from(vaultCredentials)
|
|
646
|
+
.where(eq(vaultCredentials.id, id));
|
|
647
|
+
return row;
|
|
779
648
|
}
|
|
780
649
|
async credentialsForVault(vaultId, filter = {}) {
|
|
781
650
|
this.check();
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
ORDER BY created_at, id`;
|
|
788
|
-
return rows.map(credentialRow);
|
|
651
|
+
return this.db
|
|
652
|
+
.select()
|
|
653
|
+
.from(vaultCredentials)
|
|
654
|
+
.where(and(eq(vaultCredentials.vaultId, vaultId), filter.type === undefined ? undefined : eq(vaultCredentials.type, filter.type)))
|
|
655
|
+
.orderBy(vaultCredentials.createdAt, vaultCredentials.id);
|
|
789
656
|
}
|
|
790
657
|
async updateCredential(vaultId, id, patch) {
|
|
791
658
|
this.check();
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
columns[column] = value instanceof Uint8Array ? bytes(value) : value;
|
|
799
|
-
}
|
|
800
|
-
if (Object.keys(columns).length === 0) {
|
|
801
|
-
const rows = await sql `
|
|
802
|
-
SELECT 1 FROM ${this.t("vault_credentials")}
|
|
803
|
-
WHERE vault_id = ${vaultId} AND id = ${id}`;
|
|
659
|
+
const where = and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.id, id));
|
|
660
|
+
if (Object.values(patch).every((value) => value === undefined)) {
|
|
661
|
+
const rows = await this.db
|
|
662
|
+
.select({ id: vaultCredentials.id })
|
|
663
|
+
.from(vaultCredentials)
|
|
664
|
+
.where(where);
|
|
804
665
|
return rows.length > 0;
|
|
805
666
|
}
|
|
806
|
-
const result = await
|
|
807
|
-
UPDATE ${this.t("vault_credentials")} SET ${sql(columns)}
|
|
808
|
-
WHERE vault_id = ${vaultId} AND id = ${id}`;
|
|
667
|
+
const result = await this.db.update(vaultCredentials).set(patch).where(where);
|
|
809
668
|
return result.count > 0;
|
|
810
669
|
}
|
|
811
670
|
async deleteCredential(vaultId, id) {
|
|
812
671
|
this.check();
|
|
813
|
-
const result = await this.
|
|
814
|
-
|
|
815
|
-
|
|
672
|
+
const result = await this.db
|
|
673
|
+
.delete(vaultCredentials)
|
|
674
|
+
.where(and(eq(vaultCredentials.vaultId, vaultId), eq(vaultCredentials.id, id)));
|
|
816
675
|
return result.count > 0;
|
|
817
676
|
}
|
|
818
677
|
async countCredentials() {
|
|
819
678
|
this.check();
|
|
820
|
-
const [row] = await this.
|
|
821
|
-
SELECT count(*)::int AS n FROM ${this.t("vault_credentials")}`;
|
|
679
|
+
const [row] = await this.db.select({ n: count() }).from(vaultCredentials);
|
|
822
680
|
return row.n;
|
|
823
681
|
}
|
|
824
682
|
async insertVaultAudit(row) {
|
|
825
683
|
this.check();
|
|
826
|
-
await this.
|
|
827
|
-
INSERT INTO ${this.t("vault_audit")} (
|
|
828
|
-
id, at, actor, action, vault_id, credential_id, session_id, target, outcome
|
|
829
|
-
) VALUES (
|
|
830
|
-
${row.id}, ${row.at}, ${row.actor}, ${row.action}, ${row.vaultId},
|
|
831
|
-
${row.credentialId}, ${row.sessionId}, ${row.target}, ${row.outcome}
|
|
832
|
-
)`;
|
|
684
|
+
await this.db.insert(vaultAudit).values(row);
|
|
833
685
|
}
|
|
834
686
|
async vaultAudit(filter = {}) {
|
|
835
687
|
this.check();
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return
|
|
688
|
+
const query = this.db
|
|
689
|
+
.select(AUDIT)
|
|
690
|
+
.from(vaultAudit)
|
|
691
|
+
.where(filter.vaultId === undefined ? undefined : eq(vaultAudit.vaultId, filter.vaultId))
|
|
692
|
+
.orderBy(vaultAudit.ord)
|
|
693
|
+
.$dynamic();
|
|
694
|
+
return filter.limit === undefined ? query : query.limit(filter.limit);
|
|
843
695
|
}
|
|
844
696
|
async getVaultIdempotency(id) {
|
|
845
697
|
this.check();
|
|
846
|
-
const [row] = await this.
|
|
847
|
-
|
|
848
|
-
|
|
698
|
+
const [row] = await this.db
|
|
699
|
+
.select()
|
|
700
|
+
.from(vaultIdempotency)
|
|
701
|
+
.where(eq(vaultIdempotency.id, id));
|
|
702
|
+
return row;
|
|
849
703
|
}
|
|
850
704
|
async insertVaultIdempotency(row) {
|
|
851
705
|
this.check();
|
|
852
|
-
await this.
|
|
853
|
-
INSERT INTO ${this.t("vault_idempotency")} (id, body_hash, response)
|
|
854
|
-
VALUES (${row.id}, ${row.bodyHash}, ${row.response})`;
|
|
706
|
+
await this.db.insert(vaultIdempotency).values(row);
|
|
855
707
|
}
|
|
856
708
|
// --- subject tokens ------------------------------------------------------
|
|
857
709
|
async lockSigningKeys() {
|
|
858
710
|
this.check();
|
|
859
|
-
|
|
860
|
-
await this.sql `SELECT pg_advisory_xact_lock(hashtext(${key}))`;
|
|
711
|
+
await this.db.execute(sql `SELECT pg_advisory_xact_lock(hashtext('nylorun.signing_keys'))`);
|
|
861
712
|
}
|
|
862
713
|
async insertSigningKey(row) {
|
|
863
714
|
this.check();
|
|
864
|
-
await this.
|
|
865
|
-
INSERT INTO ${this.t("signing_keys")} (
|
|
866
|
-
id, state, alg, public_jwk, kek_id, nonce, ciphertext, wrapped_dek,
|
|
867
|
-
created_at, activated_at, retired_at, revoked_at
|
|
868
|
-
) VALUES (
|
|
869
|
-
${row.id}, ${row.state}, ${row.alg}, ${row.publicJwk}, ${row.kekId},
|
|
870
|
-
${bytes(row.nonce)}, ${bytes(row.ciphertext)}, ${bytes(row.wrappedDek)},
|
|
871
|
-
${row.createdAt}, ${row.activatedAt}, ${row.retiredAt}, ${row.revokedAt}
|
|
872
|
-
)`;
|
|
715
|
+
await this.db.insert(signingKeys).values(row);
|
|
873
716
|
}
|
|
874
717
|
async signingKey(id) {
|
|
875
718
|
this.check();
|
|
876
|
-
const [row] = await this.
|
|
877
|
-
|
|
878
|
-
return row && signingKeyRow(row);
|
|
719
|
+
const [row] = await this.db.select().from(signingKeys).where(eq(signingKeys.id, id));
|
|
720
|
+
return row;
|
|
879
721
|
}
|
|
880
722
|
async signingKeys(states) {
|
|
881
723
|
this.check();
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return rows.map(signingKeyRow);
|
|
724
|
+
return this.db
|
|
725
|
+
.select()
|
|
726
|
+
.from(signingKeys)
|
|
727
|
+
.where(states === undefined ? undefined : inArray(signingKeys.state, states))
|
|
728
|
+
.orderBy(signingKeys.createdAt, signingKeys.id);
|
|
888
729
|
}
|
|
889
730
|
async setSigningKeyState(id, from, to, at) {
|
|
890
731
|
this.check();
|
|
891
|
-
const
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
RETURNING id`;
|
|
732
|
+
const stamp = SIGNING_KEY_STAMP[to];
|
|
733
|
+
const rows = await this.db
|
|
734
|
+
.update(signingKeys)
|
|
735
|
+
.set({ state: to, ...(stamp ? { [stamp]: at } : {}) })
|
|
736
|
+
.where(and(eq(signingKeys.id, id), eq(signingKeys.state, from)))
|
|
737
|
+
.returning({ id: signingKeys.id });
|
|
898
738
|
return rows.length === 1;
|
|
899
739
|
}
|
|
900
740
|
async countSigningKeys() {
|
|
901
741
|
this.check();
|
|
902
|
-
const [row] = await this.
|
|
903
|
-
SELECT count(*)::int AS n FROM ${this.t("signing_keys")}`;
|
|
742
|
+
const [row] = await this.db.select({ n: count() }).from(signingKeys);
|
|
904
743
|
return row.n;
|
|
905
744
|
}
|
|
906
745
|
async subjectEpoch(subject) {
|
|
907
746
|
this.check();
|
|
908
|
-
const [row] = await this.
|
|
909
|
-
|
|
910
|
-
|
|
747
|
+
const [row] = await this.db
|
|
748
|
+
.select({ epoch: subjectEpochs.epoch })
|
|
749
|
+
.from(subjectEpochs)
|
|
750
|
+
.where(eq(subjectEpochs.subject, subject));
|
|
751
|
+
return row?.epoch ?? 0;
|
|
911
752
|
}
|
|
912
753
|
async subjectEpochs(subjects) {
|
|
913
754
|
this.check();
|
|
914
755
|
if (subjects.length === 0)
|
|
915
756
|
return new Map();
|
|
916
|
-
const rows = await this.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
757
|
+
const rows = await this.db
|
|
758
|
+
.select({ subject: subjectEpochs.subject, epoch: subjectEpochs.epoch })
|
|
759
|
+
.from(subjectEpochs)
|
|
760
|
+
.where(inArray(subjectEpochs.subject, subjects));
|
|
761
|
+
return new Map(rows.map((row) => [row.subject, row.epoch]));
|
|
920
762
|
}
|
|
921
763
|
async bumpSubjectEpoch(subject, at) {
|
|
922
764
|
this.check();
|
|
923
|
-
const [row] = await this.
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
765
|
+
const [row] = await this.db
|
|
766
|
+
.insert(subjectEpochs)
|
|
767
|
+
.values({ subject, epoch: 1, revokedAt: at })
|
|
768
|
+
.onConflictDoUpdate({
|
|
769
|
+
target: subjectEpochs.subject,
|
|
770
|
+
set: { epoch: sql `${subjectEpochs.epoch} + 1`, revokedAt: excluded("revoked_at") },
|
|
771
|
+
})
|
|
772
|
+
.returning({ epoch: subjectEpochs.epoch });
|
|
773
|
+
return row.epoch;
|
|
930
774
|
}
|
|
931
775
|
async lockSubjectUsage(initial) {
|
|
932
776
|
this.check();
|
|
933
|
-
await this.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
return {
|
|
941
|
-
subject: row.subject,
|
|
942
|
-
turnTokens: Number(row.turn_tokens),
|
|
943
|
-
refilledAt: row.refilled_at,
|
|
944
|
-
};
|
|
777
|
+
await this.db.insert(subjectUsage).values(initial).onConflictDoNothing();
|
|
778
|
+
const [row] = await this.db
|
|
779
|
+
.select()
|
|
780
|
+
.from(subjectUsage)
|
|
781
|
+
.where(eq(subjectUsage.subject, initial.subject))
|
|
782
|
+
.for("update");
|
|
783
|
+
return row;
|
|
945
784
|
}
|
|
946
785
|
async putSubjectUsage(row) {
|
|
947
786
|
this.check();
|
|
948
|
-
await this.
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
787
|
+
await this.db
|
|
788
|
+
.insert(subjectUsage)
|
|
789
|
+
.values(row)
|
|
790
|
+
.onConflictDoUpdate({
|
|
791
|
+
target: subjectUsage.subject,
|
|
792
|
+
set: { turnTokens: excluded("turn_tokens"), refilledAt: excluded("refilled_at") },
|
|
793
|
+
});
|
|
953
794
|
}
|
|
954
795
|
async countOwnerSessions(ownerUserId, statuses) {
|
|
955
796
|
this.check();
|
|
956
|
-
const [row] = await this.
|
|
957
|
-
|
|
958
|
-
|
|
797
|
+
const [row] = await this.db
|
|
798
|
+
.select({ n: count() })
|
|
799
|
+
.from(sessions)
|
|
800
|
+
.where(and(eq(sessions.ownerUserId, ownerUserId), inArray(sessions.status, statuses)));
|
|
959
801
|
return row.n;
|
|
960
802
|
}
|
|
961
803
|
// --- publishable keys ----------------------------------------------------
|
|
962
804
|
async insertPublishableKey(row) {
|
|
963
805
|
this.check();
|
|
964
|
-
await this.
|
|
965
|
-
INSERT INTO ${this.t("publishable_keys")} (id, key, name, origins_json, created_at, revoked_at)
|
|
966
|
-
VALUES (${row.id}, ${row.key}, ${row.name}, ${row.originsJson}, ${row.createdAt}, ${row.revokedAt})`;
|
|
806
|
+
await this.db.insert(publishableKeys).values(row);
|
|
967
807
|
}
|
|
968
808
|
async publishableKeyByKey(key) {
|
|
969
809
|
this.check();
|
|
970
|
-
const [row] = await this.
|
|
971
|
-
|
|
972
|
-
|
|
810
|
+
const [row] = await this.db
|
|
811
|
+
.select()
|
|
812
|
+
.from(publishableKeys)
|
|
813
|
+
.where(eq(publishableKeys.key, key));
|
|
814
|
+
return row;
|
|
973
815
|
}
|
|
974
816
|
async publishableKey(id) {
|
|
975
817
|
this.check();
|
|
976
|
-
const [row] = await this.
|
|
977
|
-
|
|
978
|
-
|
|
818
|
+
const [row] = await this.db
|
|
819
|
+
.select()
|
|
820
|
+
.from(publishableKeys)
|
|
821
|
+
.where(eq(publishableKeys.id, id));
|
|
822
|
+
return row;
|
|
979
823
|
}
|
|
980
824
|
async publishableKeys() {
|
|
981
825
|
this.check();
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
826
|
+
return this.db
|
|
827
|
+
.select()
|
|
828
|
+
.from(publishableKeys)
|
|
829
|
+
.orderBy(publishableKeys.createdAt, publishableKeys.id);
|
|
985
830
|
}
|
|
986
831
|
async updatePublishableKey(id, patch) {
|
|
987
832
|
this.check();
|
|
988
|
-
|
|
989
|
-
const sets = [
|
|
990
|
-
...(patch.originsJson === undefined ? [] : [sql `origins_json = ${patch.originsJson}`]),
|
|
991
|
-
...(patch.revokedAt === undefined ? [] : [sql `revoked_at = ${patch.revokedAt}`]),
|
|
992
|
-
];
|
|
993
|
-
if (sets.length === 0)
|
|
833
|
+
if (patch.originsJson === undefined && patch.revokedAt === undefined)
|
|
994
834
|
return (await this.publishableKey(id)) !== undefined;
|
|
995
|
-
const rows = await
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
835
|
+
const rows = await this.db
|
|
836
|
+
.update(publishableKeys)
|
|
837
|
+
.set(patch)
|
|
838
|
+
.where(eq(publishableKeys.id, id))
|
|
839
|
+
.returning({ id: publishableKeys.id });
|
|
999
840
|
return rows.length === 1;
|
|
1000
841
|
}
|
|
842
|
+
// --- model usage ---------------------------------------------------------
|
|
843
|
+
async recordModelUsage(row) {
|
|
844
|
+
this.check();
|
|
845
|
+
const [inserted] = await this.db
|
|
846
|
+
.insert(modelUsage)
|
|
847
|
+
.values({
|
|
848
|
+
...row,
|
|
849
|
+
duplicate: sql `EXISTS (SELECT 1 FROM ${modelUsage} WHERE ${modelUsage.effectKey} = ${row.effectKey})`,
|
|
850
|
+
})
|
|
851
|
+
.returning({ duplicate: modelUsage.duplicate });
|
|
852
|
+
return { ...row, duplicate: inserted.duplicate };
|
|
853
|
+
}
|
|
854
|
+
async modelUsageTotals(query) {
|
|
855
|
+
this.check();
|
|
856
|
+
const [row] = await this.db
|
|
857
|
+
.select({
|
|
858
|
+
calls: count(),
|
|
859
|
+
tokens: sql `coalesce(sum(${modelUsage.totalTokens}), 0)::bigint`.mapWith(Number),
|
|
860
|
+
costUsd: sql `coalesce(sum(${modelUsage.costUsd}), 0)::double precision`.mapWith(Number),
|
|
861
|
+
})
|
|
862
|
+
.from(modelUsage)
|
|
863
|
+
.where(and(query.scope === "agent" ? eq(modelUsage.agentId, query.id ?? "") : undefined, query.scope === "turn" ? eq(modelUsage.turnId, query.id ?? "") : undefined, query.since !== undefined ? gte(modelUsage.createdAt, query.since) : undefined));
|
|
864
|
+
return row;
|
|
865
|
+
}
|
|
866
|
+
async listModelBudgets() {
|
|
867
|
+
this.check();
|
|
868
|
+
return this.db
|
|
869
|
+
.select()
|
|
870
|
+
.from(modelBudgets)
|
|
871
|
+
.orderBy(sql `${modelBudgets.scope} COLLATE "C"`, modelBudgets.scopeId);
|
|
872
|
+
}
|
|
873
|
+
async putModelBudgets(rows) {
|
|
874
|
+
this.check();
|
|
875
|
+
await this.db.delete(modelBudgets);
|
|
876
|
+
if (rows.length > 0)
|
|
877
|
+
await this.db.insert(modelBudgets).values([...rows]);
|
|
878
|
+
}
|
|
879
|
+
// --- tool crossings --------------------------------------------------------
|
|
880
|
+
async toolCrossing(key) {
|
|
881
|
+
this.check();
|
|
882
|
+
const [row] = await this.db.select().from(toolCrossings).where(eq(toolCrossings.key, key));
|
|
883
|
+
return row;
|
|
884
|
+
}
|
|
885
|
+
async startToolCrossing(row) {
|
|
886
|
+
this.check();
|
|
887
|
+
const inserted = await this.db
|
|
888
|
+
.insert(toolCrossings)
|
|
889
|
+
.values({ ...row, settledAt: null, answer: null })
|
|
890
|
+
.onConflictDoNothing()
|
|
891
|
+
.returning({ key: toolCrossings.key });
|
|
892
|
+
return inserted.length === 1;
|
|
893
|
+
}
|
|
894
|
+
async settleToolCrossing(key, answer, settledAt) {
|
|
895
|
+
this.check();
|
|
896
|
+
await this.db
|
|
897
|
+
.update(toolCrossings)
|
|
898
|
+
.set({ answer, settledAt })
|
|
899
|
+
.where(eq(toolCrossings.key, key));
|
|
900
|
+
}
|
|
901
|
+
async pruneToolCrossings(before) {
|
|
902
|
+
this.check();
|
|
903
|
+
const deleted = await this.db
|
|
904
|
+
.delete(toolCrossings)
|
|
905
|
+
.where(lt(toolCrossings.settledAt, before))
|
|
906
|
+
.returning({ key: toolCrossings.key });
|
|
907
|
+
return deleted.length;
|
|
908
|
+
}
|
|
1001
909
|
// --- settings ------------------------------------------------------------
|
|
1002
910
|
async getSetting(key) {
|
|
1003
911
|
this.check();
|
|
1004
|
-
const [row] = await this.
|
|
1005
|
-
|
|
912
|
+
const [row] = await this.db
|
|
913
|
+
.select({ value: tenantSettings.value })
|
|
914
|
+
.from(tenantSettings)
|
|
915
|
+
.where(eq(tenantSettings.key, key));
|
|
1006
916
|
return row?.value;
|
|
1007
917
|
}
|
|
1008
918
|
async putSetting(key, value) {
|
|
1009
919
|
this.check();
|
|
1010
|
-
await this.
|
|
1011
|
-
|
|
1012
|
-
|
|
920
|
+
await this.db
|
|
921
|
+
.insert(tenantSettings)
|
|
922
|
+
.values({ key, value })
|
|
923
|
+
.onConflictDoUpdate({ target: tenantSettings.key, set: { value: excluded("value") } });
|
|
1013
924
|
}
|
|
1014
925
|
// --- reset ---------------------------------------------------------------
|
|
1015
926
|
async reset(scope) {
|
|
1016
927
|
this.check();
|
|
1017
|
-
const
|
|
928
|
+
const db = this.db;
|
|
1018
929
|
if (scope === "sessions" || scope === "all") {
|
|
1019
930
|
for (const table of SESSION_TABLES)
|
|
1020
|
-
await
|
|
1021
|
-
await
|
|
931
|
+
await db.delete(table);
|
|
932
|
+
await db.delete(subjectUsage);
|
|
933
|
+
await db.delete(toolCrossings);
|
|
1022
934
|
// The record goes with the sessions, and the Tenant moves to a new basin: the ids it
|
|
1023
935
|
// frees start again in an empty one (Durable Streams §8.1).
|
|
1024
|
-
await
|
|
1025
|
-
await
|
|
1026
|
-
await
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
936
|
+
await db.delete(sessionEvents);
|
|
937
|
+
await db.delete(sessionLogHeads);
|
|
938
|
+
await db.update(tenant).set({
|
|
939
|
+
retiredGenerations: sql `array_append(${tenant.retiredGenerations}, ${tenant.basinGeneration})`,
|
|
940
|
+
basinGeneration: sql `${tenant.basinGeneration} + 1`,
|
|
941
|
+
});
|
|
1030
942
|
}
|
|
1031
943
|
if (scope === "sandboxes" || scope === "all")
|
|
1032
|
-
await
|
|
944
|
+
await db.delete(sandboxes);
|
|
1033
945
|
if (scope === "all") {
|
|
1034
|
-
await
|
|
1035
|
-
await
|
|
1036
|
-
await
|
|
946
|
+
await db.delete(definitions);
|
|
947
|
+
await db.delete(endpoints);
|
|
948
|
+
await db.delete(modelUsage);
|
|
949
|
+
await db.delete(modelBudgets);
|
|
950
|
+
await db.delete(vaults).where(ne(vaults.scope, "host"));
|
|
1037
951
|
}
|
|
1038
952
|
}
|
|
1039
953
|
}
|