@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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Sql } from "postgres";
|
|
2
|
+
import { type TenantEnvelope } from "@nylorun/core/contracts";
|
|
3
|
+
import type { SessionStore } from "../types.js";
|
|
4
|
+
import { type Queryable } from "./db.js";
|
|
5
|
+
import { type Migration } from "./migrate.js";
|
|
6
|
+
/** An application principal the Tenant is created with (or given later). */
|
|
7
|
+
export interface InitialPrincipal {
|
|
8
|
+
id: string;
|
|
9
|
+
/** SHA-256 hex of the principal's key. */
|
|
10
|
+
credentialHash: string;
|
|
11
|
+
}
|
|
12
|
+
/** Who the Tenant is when the database holds none yet. */
|
|
13
|
+
export interface TenantCreation {
|
|
14
|
+
/** Default: a new id (`newTenantId()`). Ignored when the Tenant exists. */
|
|
15
|
+
tenantId?: string;
|
|
16
|
+
/** Ignored when the Tenant exists. */
|
|
17
|
+
name: string;
|
|
18
|
+
/**
|
|
19
|
+
* The principals the Tenant must have, given its id (derived keys depend on it). Created
|
|
20
|
+
* with the Tenant; on later opens, the missing ones are added.
|
|
21
|
+
*/
|
|
22
|
+
principals?(tenantId: string): readonly InitialPrincipal[];
|
|
23
|
+
}
|
|
24
|
+
export interface OpenTenantDatabaseOptions {
|
|
25
|
+
/** The pool on the Tenant's database. The store never ends it. */
|
|
26
|
+
sql: Sql;
|
|
27
|
+
create: TenantCreation;
|
|
28
|
+
/** Clock for the envelope and the principals' `createdAt`, and the store's. */
|
|
29
|
+
now?: () => Date;
|
|
30
|
+
/** Passed to the store (post-commit failures). */
|
|
31
|
+
onError?: (error: unknown) => void;
|
|
32
|
+
/** Tests only: the migrations this Runtime knows. Default: the shipped ones. */
|
|
33
|
+
migrations?: readonly Migration[];
|
|
34
|
+
}
|
|
35
|
+
export interface OpenedTenantDatabase {
|
|
36
|
+
store: SessionStore;
|
|
37
|
+
envelope: TenantEnvelope;
|
|
38
|
+
/** This call created the Tenant. */
|
|
39
|
+
created: boolean;
|
|
40
|
+
/** The schema versions (applied migrations) before and after. */
|
|
41
|
+
migrated: {
|
|
42
|
+
from: number;
|
|
43
|
+
to: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Migrates the database, creates its Tenant when it has none, and opens its Session Store.
|
|
48
|
+
* Throws a `TenantOpenError` with the cause when the Tenant cannot be opened (see above).
|
|
49
|
+
*/
|
|
50
|
+
export declare function openTenantDatabase(options: OpenTenantDatabaseOptions): Promise<OpenedTenantDatabase>;
|
|
51
|
+
/**
|
|
52
|
+
* The Tenant's envelope, or undefined when the database holds no Tenant yet (no `nylorun`
|
|
53
|
+
* schema, or an empty `tenant` table). Throws `envelope-invalid` when the row does not parse.
|
|
54
|
+
* Takes the pool or a Drizzle database or transaction on it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readTenantEnvelope(q: Sql | Queryable): Promise<TenantEnvelope | undefined>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant a database holds (tenancy.md §1, §4; session-store.md §2). One database is one
|
|
3
|
+
* Tenant: its state in the schema `nylorun`, its record in `nylorun_streams`, and its
|
|
4
|
+
* envelope in the one row of `nylorun.tenant`. There is no catalog.
|
|
5
|
+
*
|
|
6
|
+
* `openTenantDatabase` runs in one transaction, under the database's migration lock
|
|
7
|
+
* (`migrate.ts`), so several processes starting at once migrate and create once:
|
|
8
|
+
*
|
|
9
|
+
* 1. Refuse an old layout (`database-layout-old`): `tenant_<id>` schemas or a record keyed by
|
|
10
|
+
* Tenant (a Runtime from before one Tenant per installation), or the `schema_version`
|
|
11
|
+
* tables of a pre-release build of it. It is never touched: this release starts fresh.
|
|
12
|
+
* 2. Apply the missing migrations (`migrate.ts`). A database with migrations this Runtime
|
|
13
|
+
* does not know is `schema-too-new`; a migration Postgres rejects is `migration-failed`.
|
|
14
|
+
* 3. When `nylorun.tenant` is empty, create the Tenant: its row (id, name, created time) and
|
|
15
|
+
* its first principals. A database whose row is gone but that holds a Tenant's data
|
|
16
|
+
* (principals, sessions, agents, vaults, keys) is `envelope-invalid`, never given a new
|
|
17
|
+
* Tenant. Principals given later that the Tenant does not have yet are added (a derived
|
|
18
|
+
* principal configured after the first start); existing ones are kept.
|
|
19
|
+
* 4. Read the envelope back (`envelope-invalid` when it does not parse), and open the store.
|
|
20
|
+
*
|
|
21
|
+
* A lost connection or an unavailable server says nothing about the Tenant: it is thrown as
|
|
22
|
+
* it is, never as a cause.
|
|
23
|
+
*/
|
|
24
|
+
import { sql } from "drizzle-orm";
|
|
25
|
+
import { isTenantId, newTenantId } from "@nylorun/core/compatibility";
|
|
26
|
+
import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
27
|
+
import { openError, TenantOpenError } from "../../tenant/cause.js";
|
|
28
|
+
import { database, driverError } from "./db.js";
|
|
29
|
+
import { applyMigrations, assertCurrentLayout, isStatementError, lockMigrations, shippedMigrations, } from "./migrate.js";
|
|
30
|
+
import { definitions, principals, sessions, signingKeys, TENANT_SCHEMA, tenant, vaults, } from "./schema.js";
|
|
31
|
+
import { createPostgresSessionStore } from "./store.js";
|
|
32
|
+
/**
|
|
33
|
+
* Migrates the database, creates its Tenant when it has none, and opens its Session Store.
|
|
34
|
+
* Throws a `TenantOpenError` with the cause when the Tenant cannot be opened (see above).
|
|
35
|
+
*/
|
|
36
|
+
export async function openTenantDatabase(options) {
|
|
37
|
+
const { sql: pool } = options;
|
|
38
|
+
const db = database(pool);
|
|
39
|
+
const now = options.now ?? (() => new Date());
|
|
40
|
+
const migrations = options.migrations ?? shippedMigrations();
|
|
41
|
+
let outcome;
|
|
42
|
+
try {
|
|
43
|
+
outcome = await db.transaction(async (tx) => {
|
|
44
|
+
await lockMigrations(tx);
|
|
45
|
+
await assertCurrentLayout(tx);
|
|
46
|
+
const migrated = await applyMigrations(tx, migrations);
|
|
47
|
+
const created = await ensureTenant(tx, options.create, migrated, now);
|
|
48
|
+
return { created, migrated };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (thrown) {
|
|
52
|
+
const error = driverError(thrown);
|
|
53
|
+
if (!(error instanceof TenantOpenError) && !isStatementError(error))
|
|
54
|
+
throw error;
|
|
55
|
+
// A statement of the bootstrap itself (the layout check, the Tenant row) was rejected.
|
|
56
|
+
const failure = error instanceof TenantOpenError
|
|
57
|
+
? error
|
|
58
|
+
: openError("open-failed", `Opening the Tenant database failed: ${error.message}`);
|
|
59
|
+
// Name the Tenant when its row reads (a database newer than this Runtime, say).
|
|
60
|
+
if (failure.code !== "database-layout-old")
|
|
61
|
+
failure.envelope ??= await readTenantEnvelope(db).catch(() => undefined);
|
|
62
|
+
throw failure;
|
|
63
|
+
}
|
|
64
|
+
const envelope = await readTenantEnvelope(db);
|
|
65
|
+
if (!envelope)
|
|
66
|
+
throw openError("envelope-invalid", "The Tenant row is missing");
|
|
67
|
+
const store = createPostgresSessionStore({
|
|
68
|
+
sql: pool,
|
|
69
|
+
tenantId: envelope.id,
|
|
70
|
+
schemaVersion: migrations.length,
|
|
71
|
+
...(options.now ? { now: options.now } : {}),
|
|
72
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
73
|
+
});
|
|
74
|
+
return { store, envelope, ...outcome };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The Tenant's envelope, or undefined when the database holds no Tenant yet (no `nylorun`
|
|
78
|
+
* schema, or an empty `tenant` table). Throws `envelope-invalid` when the row does not parse.
|
|
79
|
+
* Takes the pool or a Drizzle database or transaction on it.
|
|
80
|
+
*/
|
|
81
|
+
export async function readTenantEnvelope(q) {
|
|
82
|
+
const db = typeof q === "function" ? database(q) : q;
|
|
83
|
+
try {
|
|
84
|
+
const [table] = await db.execute(sql `
|
|
85
|
+
SELECT to_regclass(${`${TENANT_SCHEMA}.tenant`}) IS NOT NULL AS exists`);
|
|
86
|
+
if (!table?.exists)
|
|
87
|
+
return undefined;
|
|
88
|
+
const [row] = await db
|
|
89
|
+
.select({
|
|
90
|
+
id: tenant.id,
|
|
91
|
+
name: tenant.name,
|
|
92
|
+
createdAt: tenant.createdAt,
|
|
93
|
+
updatedAt: tenant.updatedAt,
|
|
94
|
+
schemaVersion: tenant.schemaVersion,
|
|
95
|
+
})
|
|
96
|
+
.from(tenant);
|
|
97
|
+
if (!row)
|
|
98
|
+
return undefined;
|
|
99
|
+
const parsed = TenantEnvelopeSchema.safeParse(row);
|
|
100
|
+
if (!parsed.success || !isTenantId(parsed.data.id))
|
|
101
|
+
throw openError("envelope-invalid", "The Tenant row is not valid");
|
|
102
|
+
return parsed.data;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw driverError(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Creates the Tenant when there is none, and adds the principals it is missing. Records the
|
|
110
|
+
* schema version on the Tenant row when this transaction migrated.
|
|
111
|
+
*/
|
|
112
|
+
async function ensureTenant(tx, create, migrated, now) {
|
|
113
|
+
const [existing] = await tx.select({ id: tenant.id }).from(tenant);
|
|
114
|
+
const createdAt = now().toISOString();
|
|
115
|
+
let tenantId = existing?.id;
|
|
116
|
+
if (tenantId === undefined) {
|
|
117
|
+
const [data] = await tx.execute(sql `
|
|
118
|
+
SELECT EXISTS (SELECT 1 FROM ${principals})
|
|
119
|
+
OR EXISTS (SELECT 1 FROM ${sessions})
|
|
120
|
+
OR EXISTS (SELECT 1 FROM ${definitions})
|
|
121
|
+
OR EXISTS (SELECT 1 FROM ${vaults})
|
|
122
|
+
OR EXISTS (SELECT 1 FROM ${signingKeys}) AS held`);
|
|
123
|
+
if (data?.held)
|
|
124
|
+
throw openError("envelope-invalid", "The Tenant row is missing from a database that holds a Tenant's data: restore it from a backup");
|
|
125
|
+
tenantId = create.tenantId ?? newTenantId();
|
|
126
|
+
if (!isTenantId(tenantId))
|
|
127
|
+
throw new Error(`Invalid Tenant id: ${tenantId}`);
|
|
128
|
+
if (!create.name)
|
|
129
|
+
throw new Error("A Tenant needs a name");
|
|
130
|
+
await tx.insert(tenant).values({
|
|
131
|
+
id: tenantId,
|
|
132
|
+
name: create.name,
|
|
133
|
+
createdAt,
|
|
134
|
+
updatedAt: createdAt,
|
|
135
|
+
schemaVersion: migrated.to,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else if (migrated.from !== migrated.to) {
|
|
139
|
+
await tx.update(tenant).set({ schemaVersion: migrated.to });
|
|
140
|
+
}
|
|
141
|
+
for (const principal of create.principals?.(tenantId) ?? [])
|
|
142
|
+
// A principal with this id or this key already exists: it is kept as it is.
|
|
143
|
+
await tx
|
|
144
|
+
.insert(principals)
|
|
145
|
+
.values({
|
|
146
|
+
id: principal.id,
|
|
147
|
+
role: "application",
|
|
148
|
+
tokenHash: principal.credentialHash,
|
|
149
|
+
idempotencyKey: null,
|
|
150
|
+
createdAt,
|
|
151
|
+
})
|
|
152
|
+
.onConflictDoNothing();
|
|
153
|
+
return existing === undefined;
|
|
154
|
+
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* The Session Store seam (architecture §12.2).
|
|
3
3
|
*
|
|
4
4
|
* The Session Store records what happened. It is async, transactional and
|
|
5
|
-
* tenant-scoped: one `SessionStore` per Tenant. Postgres is the
|
|
6
|
-
* implementation
|
|
5
|
+
* tenant-scoped: one `SessionStore` per Tenant. Postgres is the
|
|
6
|
+
* implementation (`store/postgres/store.ts`); tests run on it too.
|
|
7
7
|
*
|
|
8
8
|
* ## Invariants every implementation keeps
|
|
9
9
|
*
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* committed transactions. The sequence is also the S2 sequence number of the
|
|
24
24
|
* event in the session's stream (`sessions/<id>` in the Tenant's basin
|
|
25
25
|
* generation, `streams/basin.ts`), and the cursor is
|
|
26
|
-
* `base64url("<sessionId>:<seq>")` (see `
|
|
26
|
+
* `base64url("<sessionId>:<seq>")` (see `record/cursor.ts`).
|
|
27
27
|
* 4. **No external I/O inside `fn`.** No model, tool, MCP, sandbox, Restate or
|
|
28
28
|
* S2 call, and no `fetch`, runs inside a transaction. Wakes and deliveries go
|
|
29
29
|
* through `afterCommit`, and events are delivered to commit listeners after
|
|
@@ -57,8 +57,9 @@
|
|
|
57
57
|
import type { RecordReader } from "../streams/relay/types.js";
|
|
58
58
|
import type { Action, EventPayload, EventType, LiveEvent, SessionEventOf } from "@nylorun/core/contracts";
|
|
59
59
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
60
|
+
import type { ModelBudgetRow, ModelUsageRow, PrincipalRow, ToolCrossingRow, PublishableKeyRow, SigningKeyRow, SubjectUsageRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow } from "./postgres/schema.js";
|
|
60
61
|
/** Tables holding JSON documents keyed by `id`. */
|
|
61
|
-
export type DocTable = "definitions" | "sessions" | "commands" | "
|
|
62
|
+
export type DocTable = "definitions" | "sessions" | "commands" | "effects" | "actions" | "sandboxes" | "links";
|
|
62
63
|
export declare const DOC_TABLES: readonly DocTable[];
|
|
63
64
|
/** Session lifecycle (architecture §10.2). */
|
|
64
65
|
export type SessionStatus = "idle" | "runnable" | "running" | "waiting" | "paused" | "uncertain" | "completed" | "failed" | "cancelled";
|
|
@@ -186,87 +187,29 @@ export type EndpointHealthUpdate =
|
|
|
186
187
|
implementationVersion: string;
|
|
187
188
|
manifestHash?: string;
|
|
188
189
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
export
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
/** Envelope-encrypted secret material. Always in columns, never inside a JSON body. */
|
|
206
|
-
export interface SealedSecret {
|
|
207
|
-
kekId: string;
|
|
208
|
-
nonce: Uint8Array;
|
|
209
|
-
ciphertext: Uint8Array;
|
|
210
|
-
wrappedDek: Uint8Array;
|
|
211
|
-
}
|
|
212
|
-
/** A Tenant signing key (subject tokens). The private key is sealed; the public JWK is not. */
|
|
213
|
-
export interface SigningKeyRow extends SealedSecret {
|
|
214
|
-
id: string;
|
|
215
|
-
state: "standby" | "current" | "previous" | "revoked";
|
|
216
|
-
alg: "ES256";
|
|
217
|
-
/** JSON text of the public JWK. */
|
|
218
|
-
publicJwk: string;
|
|
219
|
-
createdAt: string;
|
|
220
|
-
activatedAt: string | null;
|
|
221
|
-
retiredAt: string | null;
|
|
222
|
-
revokedAt: string | null;
|
|
223
|
-
}
|
|
224
|
-
/** A publishable key (Host feature `browser-access`). Public by design. */
|
|
225
|
-
export interface PublishableKeyRow {
|
|
226
|
-
id: string;
|
|
227
|
-
key: string;
|
|
228
|
-
name: string;
|
|
229
|
-
/** JSON array of allowed origins. */
|
|
230
|
-
originsJson: string;
|
|
231
|
-
createdAt: string;
|
|
232
|
-
revokedAt: string | null;
|
|
233
|
-
}
|
|
234
|
-
/** A subject's turn bucket (subject limits): tokens left and when they were last refilled. */
|
|
235
|
-
export interface SubjectUsageRow {
|
|
236
|
-
subject: string;
|
|
237
|
-
turnTokens: number;
|
|
238
|
-
refilledAt: string;
|
|
190
|
+
/**
|
|
191
|
+
* The rows of the typed tables, inferred from the tables Drizzle defines
|
|
192
|
+
* (`store/postgres/schema.ts`): a principal, a vault and its credentials (secrets sealed in
|
|
193
|
+
* `bytea` columns, never inside a JSON body), the vault's audit and idempotency records, the
|
|
194
|
+
* Tenant's signing keys, a subject's turn bucket, a publishable key, a model call in the usage
|
|
195
|
+
* ledger and a model budget.
|
|
196
|
+
*/
|
|
197
|
+
export type { ModelBudgetRow, ModelUsageRow, ToolCrossingRow, PrincipalRow, PublishableKeyRow, SigningKeyRow, SubjectUsageRow, VaultAuditRow, VaultCredentialRow, VaultIdempotencyRow, VaultRow, };
|
|
198
|
+
/** Which rows of the usage ledger a total covers. */
|
|
199
|
+
export interface ModelUsageQuery {
|
|
200
|
+
scope: "tenant" | "agent" | "turn";
|
|
201
|
+
/** The agent or turn id; ignored for `tenant`. */
|
|
202
|
+
id?: string;
|
|
203
|
+
/** Only rows created at or after this ISO time. */
|
|
204
|
+
since?: string;
|
|
239
205
|
}
|
|
240
|
-
export interface
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
type: "bearer" | "oauth" | "model";
|
|
245
|
-
/** JSON text of the non-secret binding (url, or provider and model). */
|
|
246
|
-
bindingJson: string;
|
|
247
|
-
expiresAt: string | null;
|
|
248
|
-
createdAt: string;
|
|
249
|
-
rotatedAt: string | null;
|
|
206
|
+
export interface ModelUsageTotals {
|
|
207
|
+
calls: number;
|
|
208
|
+
tokens: number;
|
|
209
|
+
costUsd: number;
|
|
250
210
|
}
|
|
251
211
|
/** Fields `updateCredential` may change. */
|
|
252
212
|
export type VaultCredentialPatch = Partial<Omit<VaultCredentialRow, "id" | "vaultId" | "createdAt">>;
|
|
253
|
-
export interface VaultAuditRow {
|
|
254
|
-
id: string;
|
|
255
|
-
at: string;
|
|
256
|
-
actor: string;
|
|
257
|
-
action: string;
|
|
258
|
-
vaultId: string | null;
|
|
259
|
-
credentialId: string | null;
|
|
260
|
-
sessionId: string | null;
|
|
261
|
-
target: string | null;
|
|
262
|
-
outcome: string;
|
|
263
|
-
}
|
|
264
|
-
export interface VaultIdempotencyRow {
|
|
265
|
-
id: string;
|
|
266
|
-
bodyHash: string;
|
|
267
|
-
/** JSON text of the original response. */
|
|
268
|
-
response: string;
|
|
269
|
-
}
|
|
270
213
|
export interface SessionEffectFilter {
|
|
271
214
|
/** Only this turn. Absent means every turn. */
|
|
272
215
|
turnId?: string;
|
|
@@ -356,7 +299,7 @@ export interface Tx {
|
|
|
356
299
|
* Locking twice in one transaction is a no-op.
|
|
357
300
|
*
|
|
358
301
|
* Lock order, so concurrent transactions cannot deadlock (Postgres takes row
|
|
359
|
-
* locks in statement order
|
|
302
|
+
* locks in statement order):
|
|
360
303
|
* - a linked agent (child) session is locked before its workflow (parent)
|
|
361
304
|
* session, never after it. `t.event` on a session takes its lock, so an
|
|
362
305
|
* event on a child after the parent is locked breaks the rule too. Work
|
|
@@ -519,19 +462,32 @@ export interface Tx {
|
|
|
519
462
|
publishableKeys(): Promise<PublishableKeyRow[]>;
|
|
520
463
|
/** Returns false when the key does not exist. */
|
|
521
464
|
updatePublishableKey(id: string, patch: Partial<Pick<PublishableKeyRow, "originsJson" | "revokedAt">>): Promise<boolean>;
|
|
465
|
+
/** Appends a row, setting `duplicate` when one with the same `effectKey` exists; returns it. */
|
|
466
|
+
recordModelUsage(row: Omit<ModelUsageRow, "duplicate">): Promise<ModelUsageRow>;
|
|
467
|
+
modelUsageTotals(query: ModelUsageQuery): Promise<ModelUsageTotals>;
|
|
468
|
+
/** Every budget, ordered by scope, then scope id. */
|
|
469
|
+
listModelBudgets(): Promise<ModelBudgetRow[]>;
|
|
470
|
+
/** Replaces every budget with `rows`. */
|
|
471
|
+
putModelBudgets(rows: readonly ModelBudgetRow[]): Promise<void>;
|
|
472
|
+
toolCrossing(key: string): Promise<ToolCrossingRow | undefined>;
|
|
473
|
+
/** Inserts the running call's row; false when a row with `key` already exists. */
|
|
474
|
+
startToolCrossing(row: Pick<ToolCrossingRow, "key" | "hash" | "startedAt">): Promise<boolean>;
|
|
475
|
+
/** Records the call's answer. */
|
|
476
|
+
settleToolCrossing(key: string, answer: unknown, settledAt: string): Promise<void>;
|
|
477
|
+
/** Deletes rows that settled before `before`; returns how many. */
|
|
478
|
+
pruneToolCrossings(before: string): Promise<number>;
|
|
522
479
|
getSetting(key: string): Promise<string | undefined>;
|
|
523
480
|
putSetting(key: string, value: string): Promise<void>;
|
|
524
481
|
/**
|
|
525
482
|
* Deletes Tenant state by scope, in this transaction:
|
|
526
|
-
* - `sessions`: sessions, commands,
|
|
483
|
+
* - `sessions`: sessions, commands, effects, actions, links, subject turn
|
|
527
484
|
* buckets and the Tenant's record rows and log heads. The Tenant moves to the next basin
|
|
528
485
|
* generation and the current one is retired, so session ids it frees start again in an
|
|
529
486
|
* empty basin;
|
|
530
487
|
* - `sandboxes`: sandbox records;
|
|
531
|
-
* - `all`: both, plus definitions, Action endpoints
|
|
532
|
-
*
|
|
533
|
-
* keys, settings,
|
|
534
|
-
* audit and vault idempotency rows stay.
|
|
488
|
+
* - `all`: both, plus definitions, Action endpoints, user vaults with their credentials,
|
|
489
|
+
* the model usage ledger and the model budgets. The host vault, principals, signing keys, subject epochs,
|
|
490
|
+
* publishable keys, settings, audit and vault idempotency rows stay.
|
|
535
491
|
*/
|
|
536
492
|
reset(scope: ResetScope): Promise<void>;
|
|
537
493
|
}
|
package/dist/store/types.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type DurableStreams } from "./types.js";
|
|
7
7
|
/** Appends a `session.cancel` signal: the process running the session's advance aborts it. */
|
|
8
|
-
export declare function signalCancel(streams: DurableStreams, basin: string, sessionId: string): Promise<void>;
|
|
8
|
+
export declare function signalCancel(streams: DurableStreams, basin: string, sessionId: string, turnId?: string): Promise<void>;
|
|
9
9
|
/**
|
|
10
10
|
* Appends a `sessions.reset` signal to the old generation's basin: every process with the
|
|
11
11
|
* Tenant open moves its readers to `generation` and ends the streams of deleted sessions.
|
package/dist/streams/control.js
CHANGED
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { CONTROL_STREAM } from "./types.js";
|
|
7
7
|
/** Appends a `session.cancel` signal: the process running the session's advance aborts it. */
|
|
8
|
-
export async function signalCancel(streams, basin, sessionId) {
|
|
8
|
+
export async function signalCancel(streams, basin, sessionId, turnId) {
|
|
9
9
|
if (!sessionId)
|
|
10
10
|
throw new Error("sessionId is required");
|
|
11
|
-
const signal = {
|
|
11
|
+
const signal = {
|
|
12
|
+
type: "session.cancel",
|
|
13
|
+
sessionId,
|
|
14
|
+
...(turnId ? { turnId } : {}),
|
|
15
|
+
};
|
|
12
16
|
await streams.append(basin, CONTROL_STREAM, [signal]);
|
|
13
17
|
}
|
|
14
18
|
/**
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Contracts of the stream relay (Durable Streams §7): what it reads committed events from,
|
|
3
3
|
* and how it reads the record back to refill and reconcile. Postgres implements both with
|
|
4
4
|
* logical replication (`adapters/replication/pgoutput.ts`) and the record tables
|
|
5
|
-
* (`store/postgres/record.ts`);
|
|
5
|
+
* (`store/postgres/record.ts`); the relay core's unit tests use an in-memory pair
|
|
6
|
+
* (`test/support/relay-record.ts`).
|
|
6
7
|
*/
|
|
7
8
|
/** One committed row of the record: a session event and the basin generation it goes to. */
|
|
8
9
|
export interface RecordRow {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Contracts of the stream relay (Durable Streams §7): what it reads committed events from,
|
|
3
3
|
* and how it reads the record back to refill and reconcile. Postgres implements both with
|
|
4
4
|
* logical replication (`adapters/replication/pgoutput.ts`) and the record tables
|
|
5
|
-
* (`store/postgres/record.ts`);
|
|
5
|
+
* (`store/postgres/record.ts`); the relay core's unit tests use an in-memory pair
|
|
6
|
+
* (`test/support/relay-record.ts`).
|
|
6
7
|
*/
|
|
7
8
|
export {};
|
package/dist/streams/types.d.ts
CHANGED
|
@@ -123,6 +123,11 @@ export declare function sessionStream(sessionId: string): string;
|
|
|
123
123
|
export interface SessionCancelSignal {
|
|
124
124
|
type: "session.cancel";
|
|
125
125
|
sessionId: string;
|
|
126
|
+
/**
|
|
127
|
+
* The cancelled turn. Only an advance of this turn stops, so a signal delivered late (an
|
|
128
|
+
* append retried after S2 returns) never stops a later turn. Absent: any advance stops.
|
|
129
|
+
*/
|
|
130
|
+
turnId?: string;
|
|
126
131
|
}
|
|
127
132
|
/**
|
|
128
133
|
* The Tenant was reset and moved to basin generation `generation`. Appended to the old
|
package/dist/tenant/advance.d.ts
CHANGED
|
@@ -8,8 +8,13 @@ import type { Session, TenantContext } from "./context.js";
|
|
|
8
8
|
export declare function advance(ctx: TenantContext, id: string, signal: AbortSignal): Promise<AdvanceResult>;
|
|
9
9
|
/**
|
|
10
10
|
* Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
|
|
11
|
-
* owner's `invoking` effects become `uncertain` and are never invoked again
|
|
11
|
+
* owner's `invoking` effects become `uncertain` and are never invoked again, except model
|
|
12
|
+
* calls when the gate recovers them (P1.2) and remote MCP calls when the Tool Gate does (F4.1
|
|
13
|
+
* G3): those stay `invoking`, and the replay re-sends them. When one belongs
|
|
12
14
|
* to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
|
|
13
15
|
* Mutates and writes `s`. Returns the effect ids it marked.
|
|
14
16
|
*/
|
|
15
|
-
export declare function takeOver(t: Tx, s: Session
|
|
17
|
+
export declare function takeOver(t: Tx, s: Session, options?: {
|
|
18
|
+
recoversModelCalls?: boolean;
|
|
19
|
+
recoversMcpCalls?: boolean;
|
|
20
|
+
}): Promise<string[]>;
|