@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
package/dist/tenant/paths.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
|
-
import { isTenantId } from "@nylorun/core/compatibility";
|
|
4
|
-
function ensureDir(path) {
|
|
5
|
-
mkdirSync(path, { recursive: true });
|
|
6
|
-
return realpathSync(path);
|
|
7
|
-
}
|
|
8
3
|
function assertContained(parent, child) {
|
|
9
4
|
const root = parent.endsWith(sep) ? parent : parent + sep;
|
|
10
5
|
if (child !== parent && !child.startsWith(root)) {
|
|
11
|
-
throw new Error(`Path ${child} escapes
|
|
6
|
+
throw new Error(`Path ${child} escapes the Host root ${parent}`);
|
|
12
7
|
}
|
|
13
8
|
}
|
|
14
9
|
/** Host layout under NYLORUN_HOME / ~/.nylorun. */
|
|
@@ -20,72 +15,33 @@ export function hostPaths(hostRoot) {
|
|
|
20
15
|
credentials: join(root, "host-credentials.json"),
|
|
21
16
|
home: join(root, "home"),
|
|
22
17
|
tmp: join(root, "tmp"),
|
|
23
|
-
|
|
24
|
-
trash: join(root, "trash"),
|
|
18
|
+
tenant: join(root, "tenant"),
|
|
25
19
|
};
|
|
26
20
|
}
|
|
27
21
|
/**
|
|
28
|
-
* Absolute Tenant directory
|
|
29
|
-
*
|
|
22
|
+
* Absolute paths of the Tenant directory, `<hostRoot>/tenant/`: a Host serves one Tenant
|
|
23
|
+
* (tenancy.md §1), so the directory has no id segment. Asserts that it stays inside the Host
|
|
24
|
+
* root after realpath.
|
|
30
25
|
*
|
|
31
|
-
* The directory holds what stays on the Host (the Tenant's data is its
|
|
32
|
-
* `
|
|
26
|
+
* The directory holds what stays on the Host (the Tenant's data is its database): `home/`,
|
|
27
|
+
* `tmp/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`. The vault key is the one exception:
|
|
28
|
+
* it lives beside the Tenant directory, in `<hostRoot>/keys/vault-kek`, which the local stack
|
|
29
|
+
* mounts only into the gateway (F4.2), never into the runtime container.
|
|
33
30
|
*/
|
|
34
|
-
export function tenantPaths(hostRoot
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const root = join(tenantsRoot, tenantId);
|
|
41
|
-
// Containment before the Tenant directory exists: resolve under real tenants root.
|
|
42
|
-
const resolvedRoot = resolve(tenantsRoot, tenantId);
|
|
43
|
-
assertContained(tenantsRoot, resolvedRoot);
|
|
44
|
-
if (existsSync(root)) {
|
|
45
|
-
assertContained(tenantsRoot, realpathSync(root));
|
|
46
|
-
}
|
|
31
|
+
export function tenantPaths(hostRoot) {
|
|
32
|
+
mkdirSync(hostRoot, { recursive: true });
|
|
33
|
+
const host = realpathSync(hostRoot);
|
|
34
|
+
const root = join(host, "tenant");
|
|
35
|
+
if (existsSync(root))
|
|
36
|
+
assertContained(host, realpathSync(root));
|
|
47
37
|
return {
|
|
48
|
-
root
|
|
49
|
-
kek: join(
|
|
50
|
-
home: join(
|
|
51
|
-
tmp: join(
|
|
52
|
-
sandboxes: join(
|
|
53
|
-
pluginData: join(
|
|
54
|
-
logs: join(
|
|
55
|
-
log: join(
|
|
38
|
+
root,
|
|
39
|
+
kek: join(host, "keys", "vault-kek"),
|
|
40
|
+
home: join(root, "home"),
|
|
41
|
+
tmp: join(root, "tmp"),
|
|
42
|
+
sandboxes: join(root, "sandboxes"),
|
|
43
|
+
pluginData: join(root, "plugin-data"),
|
|
44
|
+
logs: join(root, "logs"),
|
|
45
|
+
log: join(root, "logs", "tenant.log"),
|
|
56
46
|
};
|
|
57
47
|
}
|
|
58
|
-
/** The database file of a Tenant from before the Postgres Session Store. */
|
|
59
|
-
const SQLITE_DATABASE = "tenant.sqlite";
|
|
60
|
-
/**
|
|
61
|
-
* Moves every Tenant directory that holds a `tenant.sqlite` to `trash/`, logging each.
|
|
62
|
-
*
|
|
63
|
-
* Those are beta Tenants from the SQLite Runtime. They are not migrated (recreate them); a
|
|
64
|
-
* Postgres Tenant's directory never holds that file. Moving them at Host start keeps a new
|
|
65
|
-
* Postgres Tenant with the same id from sharing the old directory. Returns the moved ids.
|
|
66
|
-
*/
|
|
67
|
-
export function trashSqliteTenants(hostRoot, logger) {
|
|
68
|
-
const host = hostPaths(hostRoot);
|
|
69
|
-
if (!existsSync(host.tenants))
|
|
70
|
-
return [];
|
|
71
|
-
const moved = [];
|
|
72
|
-
const stamp = new Date().toISOString().replaceAll(":", "-");
|
|
73
|
-
for (const entry of readdirSync(host.tenants, { withFileTypes: true })) {
|
|
74
|
-
if (!entry.isDirectory())
|
|
75
|
-
continue;
|
|
76
|
-
const directory = join(host.tenants, entry.name);
|
|
77
|
-
if (!existsSync(join(directory, SQLITE_DATABASE)))
|
|
78
|
-
continue;
|
|
79
|
-
mkdirSync(host.trash, { recursive: true });
|
|
80
|
-
const destination = join(host.trash, `${entry.name}-sqlite-${stamp}`);
|
|
81
|
-
renameSync(directory, destination);
|
|
82
|
-
moved.push(entry.name);
|
|
83
|
-
logger.warn("sqlite_tenant_moved_to_trash", {
|
|
84
|
-
tenant: entry.name,
|
|
85
|
-
from: directory,
|
|
86
|
-
to: destination,
|
|
87
|
-
reason: "SQLite Tenants are not migrated to the Postgres Session Store; recreate the Tenant",
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return moved;
|
|
91
|
-
}
|
package/dist/tenant/pool.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export declare class TimeoutError extends Error {
|
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Race a promise against a timeout. On timeout the late result is handed to
|
|
6
|
-
* `onLate` (Risk R6:
|
|
6
|
+
* `onLate` (Risk R6: the module closes a handle that opened too late).
|
|
7
7
|
*/
|
|
8
8
|
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onLate?: (late: Promise<T>) => void): Promise<T>;
|
package/dist/tenant/pool.js
CHANGED
|
@@ -6,7 +6,7 @@ export class TimeoutError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Race a promise against a timeout. On timeout the late result is handed to
|
|
9
|
-
* `onLate` (Risk R6:
|
|
9
|
+
* `onLate` (Risk R6: the module closes a handle that opened too late).
|
|
10
10
|
*/
|
|
11
11
|
export async function withTimeout(promise, timeoutMs, onLate) {
|
|
12
12
|
let timer;
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { BootstrapPrincipal } from "./types.js";
|
|
1
|
+
import type { InitialPrincipal } from "../store/postgres/tenant.js";
|
|
3
2
|
export type { PrincipalRow } from "../store/types.js";
|
|
4
3
|
/** Principal id of the Studio key derived from the admin key. */
|
|
5
4
|
export declare const STUDIO_PRINCIPAL_ID = "studio";
|
|
5
|
+
/** The derived principal Projects use (`PROJECT_PRINCIPAL_ID` in `@nylorun/admin`). */
|
|
6
|
+
export declare const PROJECT_PRINCIPAL_ID = "project";
|
|
7
|
+
/** The derived principals a Host registers when not told otherwise. */
|
|
8
|
+
export declare const DEFAULT_DERIVED_PRINCIPALS: readonly string[];
|
|
9
|
+
/** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
|
|
10
|
+
export declare function deriveStudioKey(adminKey: string, tenantId: string): string;
|
|
11
|
+
/** The key of derived principal `principalId` on `tenantId`: HMAC-SHA256 of the admin key. */
|
|
12
|
+
export declare function deriveTenantKey(adminKey: string, tenantId: string, principalId: string): string;
|
|
13
|
+
/** Whether `id` may name a derived principal: the client's name, never `studio`. */
|
|
14
|
+
export declare function isDerivedPrincipalId(id: string): boolean;
|
|
6
15
|
/**
|
|
7
|
-
*
|
|
8
|
-
* principal
|
|
9
|
-
* Exported for WS-B's `TenantStore.create` (A3).
|
|
16
|
+
* The principals a Host's Tenant is created with: `studio`, each derived principal, and an
|
|
17
|
+
* application principal when a key is given (the ephemeral Runtime). Keys are hashed here.
|
|
10
18
|
*/
|
|
11
|
-
export declare function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
export declare function hostPrincipals(options: {
|
|
20
|
+
adminKey: string;
|
|
21
|
+
derived?: readonly string[];
|
|
22
|
+
application?: {
|
|
23
|
+
principalId: string;
|
|
24
|
+
key: string;
|
|
25
|
+
};
|
|
26
|
+
/** Registers `studio` with this hash instead of the admin key's derivation. */
|
|
27
|
+
studioCredentialHash?: string;
|
|
28
|
+
}): (tenantId: string) => InitialPrincipal[];
|
|
@@ -1,56 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The principals the Host gives its Tenant when it creates it (tenancy.md §4, plan P9): the
|
|
3
|
+
* Studio principal and the derived principals, whose keys the admin key derives, so the
|
|
4
|
+
* clients holding the admin key (Studio, `nylorun start`, Babai) store none. The Tenant keeps
|
|
5
|
+
* only their hashes.
|
|
6
|
+
*
|
|
7
|
+
* The derivations are the ones `@nylorun/admin` computes on the client side
|
|
8
|
+
* (`admin/src/derived-credentials.ts`: `deriveStudioToken`, `deriveTenantKey`); both sides
|
|
9
|
+
* must stay byte for byte the same.
|
|
10
|
+
*/
|
|
11
|
+
import { createHmac } from "node:crypto";
|
|
12
|
+
import { DERIVED_PRINCIPAL_ID_PATTERN } from "@nylorun/core/compatibility";
|
|
13
|
+
import { hashToken } from "../core/bearer.js";
|
|
1
14
|
/** Principal id of the Studio key derived from the admin key. */
|
|
2
15
|
export const STUDIO_PRINCIPAL_ID = "studio";
|
|
16
|
+
/** The derived principal Projects use (`PROJECT_PRINCIPAL_ID` in `@nylorun/admin`). */
|
|
17
|
+
export const PROJECT_PRINCIPAL_ID = "project";
|
|
18
|
+
/** The derived principals a Host registers when not told otherwise. */
|
|
19
|
+
export const DEFAULT_DERIVED_PRINCIPALS = [PROJECT_PRINCIPAL_ID];
|
|
20
|
+
/** The Studio key of `tenantId`: HMAC-SHA256 of the admin key. */
|
|
21
|
+
export function deriveStudioKey(adminKey, tenantId) {
|
|
22
|
+
return hmac(adminKey, ["nylorun/studio/v1", tenantId]);
|
|
23
|
+
}
|
|
24
|
+
/** The key of derived principal `principalId` on `tenantId`: HMAC-SHA256 of the admin key. */
|
|
25
|
+
export function deriveTenantKey(adminKey, tenantId, principalId) {
|
|
26
|
+
return hmac(adminKey, ["nylorun/principal/v1", principalId, tenantId]);
|
|
27
|
+
}
|
|
28
|
+
/** Whether `id` may name a derived principal: the client's name, never `studio`. */
|
|
29
|
+
export function isDerivedPrincipalId(id) {
|
|
30
|
+
return DERIVED_PRINCIPAL_ID_PATTERN.test(id) && id !== STUDIO_PRINCIPAL_ID;
|
|
31
|
+
}
|
|
3
32
|
/**
|
|
4
|
-
*
|
|
5
|
-
* principal
|
|
6
|
-
* Exported for WS-B's `TenantStore.create` (A3).
|
|
33
|
+
* The principals a Host's Tenant is created with: `studio`, each derived principal, and an
|
|
34
|
+
* application principal when a key is given (the ephemeral Runtime). Keys are hashed here.
|
|
7
35
|
*/
|
|
8
|
-
export
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
id
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
export function hostPrincipals(options) {
|
|
37
|
+
const derived = options.derived ?? DEFAULT_DERIVED_PRINCIPALS;
|
|
38
|
+
for (const id of derived)
|
|
39
|
+
if (!isDerivedPrincipalId(id))
|
|
40
|
+
throw new Error(`Derived principal id '${id}' must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be '${STUDIO_PRINCIPAL_ID}'`);
|
|
41
|
+
return (tenantId) => [
|
|
42
|
+
...(options.application
|
|
43
|
+
? [
|
|
44
|
+
{
|
|
45
|
+
id: options.application.principalId,
|
|
46
|
+
credentialHash: hashToken(options.application.key),
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
: []),
|
|
50
|
+
{
|
|
19
51
|
id: STUDIO_PRINCIPAL_ID,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
role: "application",
|
|
29
|
-
tokenHash: principal.credentialHash,
|
|
30
|
-
idempotencyKey: null,
|
|
31
|
-
createdAt,
|
|
32
|
-
});
|
|
52
|
+
credentialHash: options.studioCredentialHash ??
|
|
53
|
+
hashToken(deriveStudioKey(options.adminKey, tenantId)),
|
|
54
|
+
},
|
|
55
|
+
...[...new Set(derived)].map((id) => ({
|
|
56
|
+
id,
|
|
57
|
+
credentialHash: hashToken(deriveTenantKey(options.adminKey, tenantId, id)),
|
|
58
|
+
})),
|
|
59
|
+
];
|
|
33
60
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
row.role !== "application" ||
|
|
42
|
-
row.tokenHash !== bootstrap.credentialHash ||
|
|
43
|
-
row.idempotencyKey !== bootstrap.idempotencyKey)
|
|
44
|
-
return false;
|
|
45
|
-
const studio = await t.principalById(STUDIO_PRINCIPAL_ID);
|
|
46
|
-
if ((studio?.role === "application" ? studio.tokenHash : undefined) !==
|
|
47
|
-
bootstrap.studioCredentialHash)
|
|
48
|
-
return false;
|
|
49
|
-
for (const principal of bootstrap.derivedPrincipals ?? []) {
|
|
50
|
-
const stored = await t.principalById(principal.id);
|
|
51
|
-
if (stored?.role !== "application" ||
|
|
52
|
-
stored.tokenHash !== principal.credentialHash)
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
61
|
+
/** HMAC-SHA256 (hex) of `parts` joined by NUL bytes, keyed with the admin key. */
|
|
62
|
+
function hmac(adminKey, parts) {
|
|
63
|
+
const message = Buffer.concat(parts.flatMap((part, index) => [
|
|
64
|
+
...(index > 0 ? [Buffer.from([0])] : []),
|
|
65
|
+
Buffer.from(part, "utf8"),
|
|
66
|
+
]));
|
|
67
|
+
return createHmac("sha256", Buffer.from(adminKey, "utf8")).update(message).digest("hex");
|
|
56
68
|
}
|
package/dist/tenant/runtime.d.ts
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import type { TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
2
|
import { type ModelProvider } from "../core/provider.js";
|
|
3
|
+
import type { ModelGate } from "../gates/model-gate.js";
|
|
3
4
|
import { type AuthorizeResult } from "../vault/service.js";
|
|
4
5
|
import type { DurableStreams } from "../streams/types.js";
|
|
5
6
|
import type { NodeBindings, OpenedTenant, TenantConfig, TenantHandle, TenantSummary } from "./types.js";
|
|
6
7
|
import { type StreamsStatus } from "./streams.js";
|
|
7
8
|
import { type TenantExecution, type TenantWorker } from "./worker.js";
|
|
9
|
+
import { type ToolGate } from "../gates/tool-gate.js";
|
|
10
|
+
import { type Keys } from "../keys/keys.js";
|
|
8
11
|
/** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
|
|
9
12
|
export type TenantOpenHooks = {
|
|
10
13
|
modelProvider?: ModelProvider;
|
|
14
|
+
/**
|
|
15
|
+
* Serves the Tenant's vault-backed model calls (the gates service's client). Without one,
|
|
16
|
+
* the Tenant calls the model in this process.
|
|
17
|
+
*/
|
|
18
|
+
modelGate?: ModelGate;
|
|
19
|
+
/**
|
|
20
|
+
* Serves the Tenant's remote MCP servers and Action deliveries (the gates service's client).
|
|
21
|
+
* Without one, the Tenant opens them and POSTs deliveries in this process.
|
|
22
|
+
*/
|
|
23
|
+
toolGate?: ToolGate;
|
|
24
|
+
/**
|
|
25
|
+
* Runs vault writes and token signing (the `keys` service's client, F4.2). With one, this
|
|
26
|
+
* process never reads, creates or holds the vault key. Without one, it does them here.
|
|
27
|
+
*/
|
|
28
|
+
keys?: Keys;
|
|
11
29
|
vaultKek?: Buffer | string | null;
|
|
12
30
|
/** When true, create the KEK file on first vault write (tests / new Tenants). */
|
|
13
31
|
createKekIfMissing?: boolean;
|
|
@@ -26,7 +44,7 @@ export type TenantOpenHooks = {
|
|
|
26
44
|
*/
|
|
27
45
|
streams?: DurableStreams;
|
|
28
46
|
/**
|
|
29
|
-
* The Host's stream relay feeds `streams` from the record
|
|
47
|
+
* The Host's stream relay feeds `streams` from the record. Without it the
|
|
30
48
|
* Tenant relays its own commits.
|
|
31
49
|
*/
|
|
32
50
|
hostRelay?: boolean;
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { resolveFlowLimits } from "../core/limits.js";
|
|
3
|
-
import { scriptedModel,
|
|
3
|
+
import { scriptedModel, toolFixtureModel, } from "../core/provider.js";
|
|
4
|
+
import { tenantModelGate } from "../gates/in-process.js";
|
|
4
5
|
import { createKekFile, readVaultKek } from "../vault/kek.js";
|
|
5
6
|
import { SigningKeys } from "./signing-keys.js";
|
|
6
7
|
import { VaultService } from "../vault/service.js";
|
|
@@ -8,7 +9,7 @@ import { McpPool } from "../mcp/pool.js";
|
|
|
8
9
|
import { SandboxManager } from "../sandbox/manager.js";
|
|
9
10
|
import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
10
11
|
import { MemoryExecution } from "../execution/memory.js";
|
|
11
|
-
import {
|
|
12
|
+
import { openError } from "./cause.js";
|
|
12
13
|
import { MemoryStreams } from "../streams/memory.js";
|
|
13
14
|
import { createSessionStreams, endAllStreams, } from "./session-streams.js";
|
|
14
15
|
import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
|
|
@@ -18,6 +19,8 @@ import { sweep } from "./sweep.js";
|
|
|
18
19
|
import { deliverAction } from "./delivery.js";
|
|
19
20
|
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
20
21
|
import { authorize } from "./effects.js";
|
|
22
|
+
import { inProcessToolGate } from "../gates/tool-gate.js";
|
|
23
|
+
import { inProcessKeys } from "../keys/keys.js";
|
|
21
24
|
import { tenantApi } from "../api/http/app.js";
|
|
22
25
|
/** Default ownership lease of an advance; the heartbeat renews it every third. */
|
|
23
26
|
const DEFAULT_OWNER_LEASE_MS = 30_000;
|
|
@@ -67,20 +70,28 @@ export class TenantRuntime {
|
|
|
67
70
|
let wired;
|
|
68
71
|
let detach;
|
|
69
72
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
// With the keys service (F4.2) the key lives in the gateway: this process never reads,
|
|
74
|
+
// creates or holds it, and the gateway reports a missing key on its readiness.
|
|
75
|
+
let kek = hooks.keys
|
|
76
|
+
? undefined
|
|
77
|
+
: readVaultKek({
|
|
78
|
+
vaultKek: hooks.vaultKek,
|
|
79
|
+
vaultKekPath: paths.kek,
|
|
80
|
+
});
|
|
81
|
+
if (!hooks.keys) {
|
|
82
|
+
const sealed = await store.tx(async (t) => (await t.countCredentials()) + (await t.countSigningKeys()));
|
|
83
|
+
if (sealed > 0 && !kek) {
|
|
84
|
+
throw openError("kek-missing", "Vault key-encryption key is missing for ciphertext (vault credentials or signing keys) in this Tenant");
|
|
85
|
+
}
|
|
77
86
|
}
|
|
78
87
|
const createKekIfMissing = hooks.createKekIfMissing !== false;
|
|
79
88
|
const ensureKek = () => {
|
|
89
|
+
if (hooks.keys)
|
|
90
|
+
throw new Error("The vault key lives in the gateway's keys service: this process never reads it");
|
|
80
91
|
if (kek)
|
|
81
92
|
return kek;
|
|
82
93
|
if (!createKekIfMissing)
|
|
83
|
-
throw
|
|
94
|
+
throw openError("kek-missing", "Vault key-encryption key is required");
|
|
84
95
|
kek = createKekFile(paths.kek);
|
|
85
96
|
return kek;
|
|
86
97
|
};
|
|
@@ -94,10 +105,14 @@ export class TenantRuntime {
|
|
|
94
105
|
});
|
|
95
106
|
// `ctx` is assigned below; these callbacks only run once the Tenant is open.
|
|
96
107
|
let ctx;
|
|
108
|
+
const toolGate = hooks.toolGate ?? inProcessToolGate(config.delivery ?? {});
|
|
109
|
+
const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
|
|
110
|
+
const keys = hooks.keys ?? inProcessKeys({ store: opened, vault, signingKeys, kek: ensureKek });
|
|
97
111
|
const mcp = new McpPool({
|
|
98
112
|
pluginData: paths.pluginData,
|
|
99
113
|
childEnv: config.childEnv,
|
|
100
114
|
authorize: (sessionId, request) => authorize(ctx, sessionId, request),
|
|
115
|
+
...(toolGate.openMcp ? { openRemote: (server) => toolGate.openMcp(server) } : {}),
|
|
101
116
|
});
|
|
102
117
|
const ephemeral = config.mode === "ephemeral";
|
|
103
118
|
const sandbox = new SandboxManager({
|
|
@@ -126,22 +141,18 @@ export class TenantRuntime {
|
|
|
126
141
|
modelProvider = scriptedModel(config.model.output);
|
|
127
142
|
else if (config.model.kind === "fixture")
|
|
128
143
|
modelProvider = toolFixtureModel();
|
|
129
|
-
else if (config.model.kind === "gateway") {
|
|
130
|
-
const gateway = config.model;
|
|
131
|
-
modelProvider = async (effect, signal) => {
|
|
132
|
-
const secret = await vault.readHostModel();
|
|
133
|
-
const token = typeof secret?.credential.key === "string"
|
|
134
|
-
? secret.credential.key
|
|
135
|
-
: "";
|
|
136
|
-
return gatewayModel({
|
|
137
|
-
url: gateway.url,
|
|
138
|
-
model: gateway.model,
|
|
139
|
-
token,
|
|
140
|
-
})(effect, signal);
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
144
|
else
|
|
144
145
|
modelProvider = scriptedModel();
|
|
146
|
+
// With the gates service the loop never reads a model credential: only a Runtime
|
|
147
|
+
// without one (embedding, the ephemeral Runtime, tests) reads it here.
|
|
148
|
+
const modelGate = hooks.modelGate ??
|
|
149
|
+
tenantModelGate({
|
|
150
|
+
store: opened,
|
|
151
|
+
kek: ensureKek,
|
|
152
|
+
root: paths.home,
|
|
153
|
+
logger: config.logger,
|
|
154
|
+
...(config.modelCall ? { settings: config.modelCall } : {}),
|
|
155
|
+
});
|
|
145
156
|
const sessionStreams = createSessionStreams();
|
|
146
157
|
const local = hooks.execution
|
|
147
158
|
? undefined
|
|
@@ -166,11 +177,14 @@ export class TenantRuntime {
|
|
|
166
177
|
flowLimits,
|
|
167
178
|
modelProvider,
|
|
168
179
|
useVaultModel,
|
|
180
|
+
modelGate,
|
|
181
|
+
toolGate,
|
|
169
182
|
closing: false,
|
|
170
183
|
closed: false,
|
|
171
184
|
work: createWorkState(),
|
|
172
185
|
sessionStreams,
|
|
173
|
-
signingKeys
|
|
186
|
+
signingKeys,
|
|
187
|
+
keys,
|
|
174
188
|
workerId: hooks.workerId ?? WORKER_ID,
|
|
175
189
|
ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
|
|
176
190
|
wake: async (sessionId, wake) => {
|
|
@@ -178,7 +192,7 @@ export class TenantRuntime {
|
|
|
178
192
|
return;
|
|
179
193
|
await execution.wake(config.tenantId, sessionId, wake);
|
|
180
194
|
},
|
|
181
|
-
abortLocal: (sessionId) => abortLocal(ctx, sessionId),
|
|
195
|
+
abortLocal: (sessionId, turnId) => abortLocal(ctx, sessionId, "cancel", turnId),
|
|
182
196
|
deliver: async (actionId) => {
|
|
183
197
|
if (ctx.closing || ctx.closed)
|
|
184
198
|
return;
|
|
@@ -3,6 +3,8 @@ import { type AdvanceAbortKind } from "./worker.js";
|
|
|
3
3
|
export interface WorkState {
|
|
4
4
|
/** Advances running on this process, by session id. */
|
|
5
5
|
readonly running: Map<string, AbortController>;
|
|
6
|
+
/** The turn each running advance belongs to, by session id (`null`: none). */
|
|
7
|
+
readonly runningTurns: Map<string, string | null>;
|
|
6
8
|
/** Deliveries to Action endpoints in flight on this process, by their Action's session id. */
|
|
7
9
|
readonly deliveries: Map<string, Set<AbortController>>;
|
|
8
10
|
/** When each Action last reported a failed delivery (`action.delivery_failed` throttling). */
|
|
@@ -12,9 +14,9 @@ export declare function createWorkState(): WorkState;
|
|
|
12
14
|
/**
|
|
13
15
|
* Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
|
|
14
16
|
* Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
|
|
15
|
-
* elsewhere.
|
|
17
|
+
* elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
|
|
16
18
|
*/
|
|
17
|
-
export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind): void;
|
|
19
|
+
export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind, turnId?: string): void;
|
|
18
20
|
/** Reset: abort every advance running on this process; their sessions are being cleared. */
|
|
19
21
|
export declare function clearWork(ctx: TenantContext): void;
|
|
20
22
|
/** Stop new advances; cancel active turns if asked; wait for running advances until the timeout. */
|
package/dist/tenant/scheduler.js
CHANGED
|
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto";
|
|
|
13
13
|
import { command } from "./commands.js";
|
|
14
14
|
import { AdvanceAbort } from "./worker.js";
|
|
15
15
|
export function createWorkState() {
|
|
16
|
-
return { running: new Map(), deliveries: new Map(), deliveryNotices: new Map() };
|
|
16
|
+
return { running: new Map(), runningTurns: new Map(), deliveries: new Map(), deliveryNotices: new Map() };
|
|
17
17
|
}
|
|
18
18
|
const MESSAGES = {
|
|
19
19
|
cancel: "Turn cancelled",
|
|
@@ -28,11 +28,11 @@ function abortWith(controller, kind) {
|
|
|
28
28
|
/**
|
|
29
29
|
* Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
|
|
30
30
|
* Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
|
|
31
|
-
* elsewhere.
|
|
31
|
+
* elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
|
|
32
32
|
*/
|
|
33
|
-
export function abortLocal(ctx, id, kind = "cancel") {
|
|
33
|
+
export function abortLocal(ctx, id, kind = "cancel", turnId) {
|
|
34
34
|
const controller = ctx.work.running.get(id);
|
|
35
|
-
if (controller)
|
|
35
|
+
if (controller && (turnId === undefined || ctx.work.runningTurns.get(id) === turnId))
|
|
36
36
|
abortWith(controller, kind);
|
|
37
37
|
// A cancel also closes the session's deliveries, which aborts the tools' `ctx.signal`.
|
|
38
38
|
for (const delivery of ctx.work.deliveries.get(id) ?? [])
|
|
@@ -105,6 +105,8 @@ export declare function observeSession(ctx: TenantContext, sessionId: string, cu
|
|
|
105
105
|
holder?: StreamHolder;
|
|
106
106
|
signal: AbortSignal;
|
|
107
107
|
}): AsyncGenerator<LiveEvent>;
|
|
108
|
+
/** Internal events (`transcript.updated`) are recorded and streamed but never served. */
|
|
109
|
+
export declare function served(event: LiveEvent): boolean;
|
|
108
110
|
/**
|
|
109
111
|
* Ends every feed in this process whose session is gone or whose Tenant moved to a new basin
|
|
110
112
|
* generation, and moves this process to the Tenant's current generation. Run on a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeCursor, encodeCursor } from "../
|
|
1
|
+
import { decodeCursor, encodeCursor } from "../record/index.js";
|
|
2
2
|
import { basinOf } from "../streams/basin.js";
|
|
3
3
|
import { sessionStream, } from "../streams/types.js";
|
|
4
4
|
import { fail } from "./http.js";
|
|
@@ -88,7 +88,7 @@ export async function readHistory(ctx, sessionId, cursor, agent) {
|
|
|
88
88
|
try {
|
|
89
89
|
for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
|
|
90
90
|
last = record.seq;
|
|
91
|
-
if (agent === undefined || belongsTo(record.body, agent))
|
|
91
|
+
if (served(record.body) && (agent === undefined || belongsTo(record.body, agent)))
|
|
92
92
|
items.push(record.body);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -255,7 +255,12 @@ function deliver(observer, record) {
|
|
|
255
255
|
if (record.seq < observer.next)
|
|
256
256
|
return;
|
|
257
257
|
observer.next = record.seq + 1;
|
|
258
|
-
|
|
258
|
+
if (served(record.body))
|
|
259
|
+
observer.sink.write(record.body);
|
|
260
|
+
}
|
|
261
|
+
/** Internal events (`transcript.updated`) are recorded and streamed but never served. */
|
|
262
|
+
export function served(event) {
|
|
263
|
+
return event.visibility !== "internal";
|
|
259
264
|
}
|
|
260
265
|
/**
|
|
261
266
|
* Follows the feed's stream from `feed.next` for the current `feed.read`, retrying failed
|
package/dist/tenant/status.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { TenantConfig } from "./types.js";
|
|
|
6
6
|
import type { TenantEnvelope } from "@nylorun/core/contracts";
|
|
7
7
|
import type { StuckInvocation } from "../execution/types.js";
|
|
8
8
|
import type { StreamsStatus } from "./streams.js";
|
|
9
|
+
import type { Keys } from "../keys/keys.js";
|
|
9
10
|
export interface TenantStatusContext {
|
|
10
11
|
envelope: TenantEnvelope;
|
|
11
12
|
config: TenantConfig;
|
|
@@ -31,4 +32,6 @@ export declare function buildTenantStatus(ctx: TenantStatusContext): Promise<Ten
|
|
|
31
32
|
export declare function seedTenantConfig(ctx: {
|
|
32
33
|
store: SessionStore;
|
|
33
34
|
vault: VaultService;
|
|
35
|
+
/** Seals the seeded model credential (F4.2). */
|
|
36
|
+
keys: Pick<Keys, "putHostModel">;
|
|
34
37
|
}, body: SeedTenantConfigRequest): Promise<SeedTenantConfigResponse>;
|
package/dist/tenant/status.js
CHANGED
|
@@ -125,7 +125,7 @@ export async function seedTenantConfig(ctx, body) {
|
|
|
125
125
|
kept.push("model");
|
|
126
126
|
}
|
|
127
127
|
else {
|
|
128
|
-
await ctx.
|
|
128
|
+
await ctx.keys.putHostModel({
|
|
129
129
|
requestId: body.requestId,
|
|
130
130
|
idempotencyKey: `seed-model:${body.requestId}`,
|
|
131
131
|
...body.model,
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { PostgresClient } from "../store/postgres/connect.js";
|
|
2
|
-
import type { Migration } from "../store/postgres/
|
|
3
|
-
import { type
|
|
4
|
-
|
|
2
|
+
import type { Migration } from "../store/postgres/migrate.js";
|
|
3
|
+
import { type TenantCreation } from "../store/postgres/tenant.js";
|
|
4
|
+
import { type Logger, type OpenTenantRuntime, type TenantConfig, type TenantOpener } from "./types.js";
|
|
5
|
+
export interface PostgresTenantOptions {
|
|
5
6
|
hostRoot: string;
|
|
6
|
-
/** The
|
|
7
|
+
/** The pool on the Tenant's database. The Tenant never ends it. */
|
|
7
8
|
sql: PostgresClient;
|
|
8
|
-
/**
|
|
9
|
+
/** Who the Tenant is when the database holds none yet. */
|
|
10
|
+
create: TenantCreation;
|
|
11
|
+
/** Opens the Tenant Runtime on the opened store; the Runtime then owns it. */
|
|
9
12
|
openRuntime: OpenTenantRuntime;
|
|
10
13
|
configFor: (tenantId: string) => TenantConfig;
|
|
11
14
|
logger?: Logger;
|
|
12
15
|
/** Tests only: the migrations this Runtime knows. */
|
|
13
16
|
migrations?: readonly Migration[];
|
|
14
17
|
}
|
|
15
|
-
export declare function
|
|
18
|
+
export declare function createPostgresTenantOpener(options: PostgresTenantOptions): TenantOpener;
|