@nylorun/runtime 0.9.0-beta → 0.10.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 +35 -0
- package/README.md +96 -25
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- package/dist/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +11 -2
- package/dist/core/executors.js +20 -4
- package/dist/core/flow-host.d.ts +264 -0
- package/dist/core/flow-host.js +783 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +35 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/store.d.ts +3 -1
- package/dist/core/store.js +14 -73
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- package/dist/host/config-for.d.ts +16 -0
- package/dist/host/config-for.js +45 -0
- package/dist/host/config.d.ts +24 -0
- package/dist/host/create-host.d.ts +21 -0
- package/dist/host/create-host.js +396 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/http.d.ts +35 -0
- package/dist/host/http.js +136 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.js +5 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +142 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- package/dist/launcher/commands.d.ts +24 -0
- package/dist/launcher/commands.js +260 -0
- package/dist/launcher/ensure-config.d.ts +16 -0
- package/dist/launcher/ensure-config.js +79 -0
- package/dist/launcher/errors.d.ts +12 -0
- package/dist/launcher/errors.js +22 -0
- package/dist/launcher/host-config.d.ts +17 -0
- package/dist/launcher/host-config.js +64 -0
- package/dist/launcher/host-state.d.ts +17 -0
- package/dist/launcher/host-state.js +61 -0
- package/dist/launcher/lifecycle.d.ts +66 -0
- package/dist/launcher/lifecycle.js +407 -0
- package/dist/launcher/locks.d.ts +26 -0
- package/dist/launcher/locks.js +117 -0
- package/dist/launcher/logs.d.ts +18 -0
- package/dist/launcher/logs.js +117 -0
- package/dist/launcher/main.d.ts +2 -0
- package/dist/launcher/main.js +66 -0
- package/dist/launcher/output.d.ts +11 -0
- package/dist/launcher/output.js +68 -0
- package/dist/launcher/paths.d.ts +20 -0
- package/dist/launcher/paths.js +36 -0
- package/dist/launcher/probe.d.ts +23 -0
- package/dist/launcher/probe.js +72 -0
- package/dist/launcher/protocol.d.ts +62 -0
- package/dist/launcher/protocol.js +1 -0
- package/dist/launcher/schema-guard.d.ts +6 -0
- package/dist/launcher/schema-guard.js +36 -0
- package/dist/launcher/spawn.d.ts +22 -0
- package/dist/launcher/spawn.js +85 -0
- package/dist/launcher/status.d.ts +6 -0
- package/dist/launcher/status.js +65 -0
- package/dist/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.js +1 -1
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/select.d.ts +4 -3
- package/dist/sandbox/select.js +15 -9
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- package/dist/tenant/envelope.d.ts +10 -0
- package/dist/tenant/envelope.js +54 -0
- package/dist/tenant/ephemeral.d.ts +31 -0
- package/dist/tenant/ephemeral.js +156 -0
- package/dist/tenant/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/migration.d.ts +16 -0
- package/dist/tenant/migration.js +116 -0
- package/dist/tenant/module.d.ts +17 -0
- package/dist/tenant/module.js +315 -0
- package/dist/tenant/paths.d.ts +24 -0
- package/dist/tenant/paths.js +65 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +17 -0
- package/dist/tenant/principals.js +44 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +15 -0
- package/dist/tenant/quarantine.d.ts +35 -0
- package/dist/tenant/quarantine.js +94 -0
- package/dist/tenant/reset.d.ts +21 -0
- package/dist/tenant/reset.js +125 -0
- package/dist/tenant/runtime.d.ts +80 -0
- package/dist/tenant/runtime.js +2256 -0
- package/dist/tenant/schema.d.ts +23 -0
- package/dist/tenant/schema.js +151 -0
- package/dist/tenant/status.d.ts +28 -0
- package/dist/tenant/status.js +95 -0
- package/dist/tenant/store-fs.d.ts +16 -0
- package/dist/tenant/store-fs.js +205 -0
- package/dist/tenant/store-memory.d.ts +19 -0
- package/dist/tenant/store-memory.js +100 -0
- package/dist/tenant/types.d.ts +125 -0
- package/dist/tenant/types.js +1 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -6
- package/dist/core/main.js +0 -35
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
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
|
+
function assertContained(parent, child) {
|
|
9
|
+
const root = parent.endsWith(sep) ? parent : parent + sep;
|
|
10
|
+
if (child !== parent && !child.startsWith(root)) {
|
|
11
|
+
throw new Error(`Path ${child} escapes host tenants root ${parent}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Host layout under NYLORUN_HOME / ~/.nylorun. */
|
|
15
|
+
export function hostPaths(hostRoot) {
|
|
16
|
+
const root = resolve(hostRoot);
|
|
17
|
+
return {
|
|
18
|
+
root,
|
|
19
|
+
config: join(root, "host.json"),
|
|
20
|
+
state: join(root, "host-state.json"),
|
|
21
|
+
credentials: join(root, "host-credentials.json"),
|
|
22
|
+
log: join(root, "runtime.log"),
|
|
23
|
+
home: join(root, "home"),
|
|
24
|
+
tmp: join(root, "tmp"),
|
|
25
|
+
runtime: join(root, "runtime"),
|
|
26
|
+
tenants: join(root, "tenants"),
|
|
27
|
+
trash: join(root, "trash"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Absolute Tenant paths under `<hostRoot>/tenants/<tenantId>/`.
|
|
32
|
+
* Validates the id and asserts containment after realpath.
|
|
33
|
+
*
|
|
34
|
+
* Basenames frozen here for Wave 0 (design Rev 3 absent):
|
|
35
|
+
* `tenant.json`, `tenant.sqlite`, `.runtime-lock`, `vault-kek`,
|
|
36
|
+
* `home/`, `tmp/`, `.migration/`, `sandboxes/`, `plugin-data/`, `logs/tenant.log`.
|
|
37
|
+
*/
|
|
38
|
+
export function tenantPaths(hostRoot, tenantId) {
|
|
39
|
+
if (!isTenantId(tenantId)) {
|
|
40
|
+
throw new Error(`Invalid tenant id: ${String(tenantId)}`);
|
|
41
|
+
}
|
|
42
|
+
const host = hostPaths(hostRoot);
|
|
43
|
+
const tenantsRoot = ensureDir(host.tenants);
|
|
44
|
+
const root = join(tenantsRoot, tenantId);
|
|
45
|
+
// Containment before the Tenant directory exists: resolve under real tenants root.
|
|
46
|
+
const resolvedRoot = resolve(tenantsRoot, tenantId);
|
|
47
|
+
assertContained(tenantsRoot, resolvedRoot);
|
|
48
|
+
if (existsSync(root)) {
|
|
49
|
+
assertContained(tenantsRoot, realpathSync(root));
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
root: resolvedRoot,
|
|
53
|
+
envelope: join(resolvedRoot, "tenant.json"),
|
|
54
|
+
database: join(resolvedRoot, "tenant.sqlite"),
|
|
55
|
+
lock: join(resolvedRoot, ".runtime-lock"),
|
|
56
|
+
kek: join(resolvedRoot, "vault-kek"),
|
|
57
|
+
home: join(resolvedRoot, "home"),
|
|
58
|
+
tmp: join(resolvedRoot, "tmp"),
|
|
59
|
+
migration: join(resolvedRoot, ".migration"),
|
|
60
|
+
sandboxes: join(resolvedRoot, "sandboxes"),
|
|
61
|
+
pluginData: join(resolvedRoot, "plugin-data"),
|
|
62
|
+
logs: join(resolvedRoot, "logs"),
|
|
63
|
+
log: join(resolvedRoot, "logs", "tenant.log"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Run async work over items with a fixed concurrency limit. */
|
|
2
|
+
export declare function mapPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
3
|
+
export declare class TimeoutError extends Error {
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Race a promise against a timeout. On timeout the late result is handed to
|
|
8
|
+
* `onLate` (Risk R6: mark quarantined and close a late handle).
|
|
9
|
+
*/
|
|
10
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, onLate?: (late: Promise<T>) => void): Promise<T>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Run async work over items with a fixed concurrency limit. */
|
|
2
|
+
export async function mapPool(items, concurrency, worker) {
|
|
3
|
+
if (items.length === 0)
|
|
4
|
+
return [];
|
|
5
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
6
|
+
const results = new Array(items.length);
|
|
7
|
+
let next = 0;
|
|
8
|
+
async function run() {
|
|
9
|
+
for (;;) {
|
|
10
|
+
const index = next++;
|
|
11
|
+
if (index >= items.length)
|
|
12
|
+
return;
|
|
13
|
+
results[index] = await worker(items[index], index);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
await Promise.all(Array.from({ length: limit }, () => run()));
|
|
17
|
+
return results;
|
|
18
|
+
}
|
|
19
|
+
export class TimeoutError extends Error {
|
|
20
|
+
constructor(message = "operation timed out") {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "TimeoutError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Race a promise against a timeout. On timeout the late result is handed to
|
|
27
|
+
* `onLate` (Risk R6: mark quarantined and close a late handle).
|
|
28
|
+
*/
|
|
29
|
+
export async function withTimeout(promise, timeoutMs, onLate) {
|
|
30
|
+
let timer;
|
|
31
|
+
let timedOut = false;
|
|
32
|
+
const timeout = new Promise((_, reject) => {
|
|
33
|
+
timer = setTimeout(() => {
|
|
34
|
+
timedOut = true;
|
|
35
|
+
reject(new TimeoutError(`timed out after ${timeoutMs}ms`));
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
const result = await Promise.race([promise, timeout]);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (timedOut && onLate) {
|
|
44
|
+
onLate(promise.then((value) => value, (lateError) => {
|
|
45
|
+
throw lateError;
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (timer !== undefined)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { BootstrapPrincipal } from "./types.js";
|
|
3
|
+
export interface PrincipalRow {
|
|
4
|
+
id: string;
|
|
5
|
+
role: "application";
|
|
6
|
+
tokenHash: string;
|
|
7
|
+
idempotencyKey: string | null;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Writes the application principal and idempotency key in one transaction.
|
|
12
|
+
* Exported for WS-B's `TenantStore.create` (A3).
|
|
13
|
+
*/
|
|
14
|
+
export declare function bootstrapPrincipal(db: DatabaseSync, bootstrap: BootstrapPrincipal): void;
|
|
15
|
+
export declare function findPrincipalByTokenHash(db: DatabaseSync, tokenHash: string): PrincipalRow | undefined;
|
|
16
|
+
export declare function applicationTokenHashes(db: DatabaseSync): readonly string[];
|
|
17
|
+
export declare function isApplicationTokenHash(db: DatabaseSync, tokenHash: string): boolean;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes the application principal and idempotency key in one transaction.
|
|
3
|
+
* Exported for WS-B's `TenantStore.create` (A3).
|
|
4
|
+
*/
|
|
5
|
+
export function bootstrapPrincipal(db, bootstrap) {
|
|
6
|
+
const now = new Date().toISOString();
|
|
7
|
+
db.exec("BEGIN IMMEDIATE");
|
|
8
|
+
try {
|
|
9
|
+
db.prepare(`INSERT INTO principals(id, role, token_hash, idempotency_key, created_at)
|
|
10
|
+
VALUES(?, 'application', ?, ?, ?)`).run(bootstrap.principalId, bootstrap.credentialHash, bootstrap.idempotencyKey, now);
|
|
11
|
+
db.exec("COMMIT");
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
db.exec("ROLLBACK");
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function findPrincipalByTokenHash(db, tokenHash) {
|
|
19
|
+
const row = db
|
|
20
|
+
.prepare(`SELECT id, role, token_hash, idempotency_key, created_at
|
|
21
|
+
FROM principals WHERE token_hash = ?`)
|
|
22
|
+
.get(tokenHash);
|
|
23
|
+
if (!row)
|
|
24
|
+
return undefined;
|
|
25
|
+
return {
|
|
26
|
+
id: String(row.id),
|
|
27
|
+
role: "application",
|
|
28
|
+
tokenHash: String(row.token_hash),
|
|
29
|
+
idempotencyKey: row.idempotency_key == null ? null : String(row.idempotency_key),
|
|
30
|
+
createdAt: String(row.created_at),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function applicationTokenHashes(db) {
|
|
34
|
+
return db
|
|
35
|
+
.prepare(`SELECT token_hash FROM principals WHERE role = 'application'`)
|
|
36
|
+
.all()
|
|
37
|
+
.map((row) => String(row.token_hash));
|
|
38
|
+
}
|
|
39
|
+
export function isApplicationTokenHash(db, tokenHash) {
|
|
40
|
+
const row = db
|
|
41
|
+
.prepare(`SELECT 1 AS ok FROM principals WHERE role = 'application' AND token_hash = ?`)
|
|
42
|
+
.get(tokenHash);
|
|
43
|
+
return !!row;
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Quarantine } from "./types.js";
|
|
2
|
+
/** Typed quarantine raised while opening a Tenant Runtime (A7). */
|
|
3
|
+
export declare class QuarantineError extends Error {
|
|
4
|
+
readonly quarantine: Quarantine;
|
|
5
|
+
constructor(code: Quarantine["code"], message: string, repair?: string, extra?: Pick<Quarantine, "lockPath" | "lockPid">);
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Typed quarantine raised while opening a Tenant Runtime (A7). */
|
|
2
|
+
export class QuarantineError extends Error {
|
|
3
|
+
quarantine;
|
|
4
|
+
constructor(code, message, repair = "nylorun tenant status", extra) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "QuarantineError";
|
|
7
|
+
this.quarantine = {
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
repair,
|
|
11
|
+
...(extra?.lockPath === undefined ? {} : { lockPath: extra.lockPath }),
|
|
12
|
+
...(extra?.lockPid === undefined ? {} : { lockPid: extra.lockPid }),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Quarantine } from "./types.js";
|
|
2
|
+
/** Typed quarantine failure thrown by stores and migration. */
|
|
3
|
+
export declare class QuarantineError extends Error implements Quarantine {
|
|
4
|
+
readonly code: Quarantine["code"];
|
|
5
|
+
readonly repair: string;
|
|
6
|
+
readonly lockPath?: string;
|
|
7
|
+
readonly lockPid?: number;
|
|
8
|
+
constructor(fields: Quarantine);
|
|
9
|
+
toQuarantine(): Quarantine;
|
|
10
|
+
}
|
|
11
|
+
export declare function isQuarantineError(error: unknown): error is QuarantineError;
|
|
12
|
+
export declare function asQuarantine(error: unknown): Quarantine | undefined;
|
|
13
|
+
/** CLI-facing repair strings for every quarantine code (B9). */
|
|
14
|
+
export declare function repairFor(code: Quarantine["code"], detail?: {
|
|
15
|
+
tenantId?: string;
|
|
16
|
+
lockPath?: string;
|
|
17
|
+
lockPid?: number;
|
|
18
|
+
migrationPath?: string;
|
|
19
|
+
}): string;
|
|
20
|
+
export declare function quarantine(code: Quarantine["code"], message: string, detail?: {
|
|
21
|
+
tenantId?: string;
|
|
22
|
+
lockPath?: string;
|
|
23
|
+
lockPid?: number;
|
|
24
|
+
migrationPath?: string;
|
|
25
|
+
}): QuarantineError;
|
|
26
|
+
/** Conflict when create bootstrap material does not match an existing Tenant (HTTP 409). */
|
|
27
|
+
export declare class TenantConflictError extends Error {
|
|
28
|
+
readonly statusCode: 409;
|
|
29
|
+
constructor(message?: string);
|
|
30
|
+
}
|
|
31
|
+
/** Delete refused because the Tenant still has live work (HTTP 409). */
|
|
32
|
+
export declare class TenantBusyError extends Error {
|
|
33
|
+
readonly statusCode: 409;
|
|
34
|
+
constructor(message?: string);
|
|
35
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Typed quarantine failure thrown by stores and migration. */
|
|
2
|
+
export class QuarantineError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
repair;
|
|
5
|
+
lockPath;
|
|
6
|
+
lockPid;
|
|
7
|
+
constructor(fields) {
|
|
8
|
+
super(fields.message);
|
|
9
|
+
this.name = "QuarantineError";
|
|
10
|
+
this.code = fields.code;
|
|
11
|
+
this.repair = fields.repair;
|
|
12
|
+
if (fields.lockPath !== undefined)
|
|
13
|
+
this.lockPath = fields.lockPath;
|
|
14
|
+
if (fields.lockPid !== undefined)
|
|
15
|
+
this.lockPid = fields.lockPid;
|
|
16
|
+
}
|
|
17
|
+
toQuarantine() {
|
|
18
|
+
return {
|
|
19
|
+
code: this.code,
|
|
20
|
+
message: this.message,
|
|
21
|
+
repair: this.repair,
|
|
22
|
+
...(this.lockPath !== undefined ? { lockPath: this.lockPath } : {}),
|
|
23
|
+
...(this.lockPid !== undefined ? { lockPid: this.lockPid } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function isQuarantineError(error) {
|
|
28
|
+
return error instanceof QuarantineError;
|
|
29
|
+
}
|
|
30
|
+
export function asQuarantine(error) {
|
|
31
|
+
if (isQuarantineError(error))
|
|
32
|
+
return error.toQuarantine();
|
|
33
|
+
if (error &&
|
|
34
|
+
typeof error === "object" &&
|
|
35
|
+
"code" in error &&
|
|
36
|
+
"message" in error &&
|
|
37
|
+
"repair" in error) {
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/** CLI-facing repair strings for every quarantine code (B9). */
|
|
43
|
+
export function repairFor(code, detail = {}) {
|
|
44
|
+
const id = detail.tenantId ? ` ${detail.tenantId}` : "";
|
|
45
|
+
switch (code) {
|
|
46
|
+
case "locked":
|
|
47
|
+
return (`nylorun tenant status${id}` +
|
|
48
|
+
(detail.lockPath !== undefined && detail.lockPid !== undefined
|
|
49
|
+
? ` — stop pid ${detail.lockPid} holding ${detail.lockPath}, or remove a stale lock`
|
|
50
|
+
: " — resolve the .runtime-lock conflict"));
|
|
51
|
+
case "kek-missing":
|
|
52
|
+
return `nylorun tenant status${id} — restore vault-kek for this Tenant (ciphertext cannot be opened without it)`;
|
|
53
|
+
case "corrupt":
|
|
54
|
+
return `nylorun tenant status${id} — repair or restore the Tenant directory from backup`;
|
|
55
|
+
case "schema-too-new":
|
|
56
|
+
return `nylorun runtime restart` +
|
|
57
|
+
`${id ? ` — Tenant${id} needs a newer Host` : " — Host schema is older than this Tenant"}`;
|
|
58
|
+
case "migration-failed":
|
|
59
|
+
return detail.migrationPath
|
|
60
|
+
? `restore ${detail.migrationPath} over the Tenant files, then nylorun tenant status${id}`
|
|
61
|
+
: `nylorun tenant status${id} — restore from .migration/ snapshot, then retry`;
|
|
62
|
+
case "envelope-invalid":
|
|
63
|
+
return `nylorun tenant status${id} — fix or replace tenant.json so its id matches the directory name`;
|
|
64
|
+
case "open-timeout":
|
|
65
|
+
return `nylorun tenant status${id} — open timed out; inspect locks and SQLite, then retry`;
|
|
66
|
+
case "open-failed":
|
|
67
|
+
return `nylorun tenant status${id} — inspect Tenant logs and repair before the Host retries open`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function quarantine(code, message, detail = {}) {
|
|
71
|
+
return new QuarantineError({
|
|
72
|
+
code,
|
|
73
|
+
message,
|
|
74
|
+
repair: repairFor(code, detail),
|
|
75
|
+
...(detail.lockPath !== undefined ? { lockPath: detail.lockPath } : {}),
|
|
76
|
+
...(detail.lockPid !== undefined ? { lockPid: detail.lockPid } : {}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Conflict when create bootstrap material does not match an existing Tenant (HTTP 409). */
|
|
80
|
+
export class TenantConflictError extends Error {
|
|
81
|
+
statusCode = 409;
|
|
82
|
+
constructor(message = "Tenant already exists with different bootstrap material") {
|
|
83
|
+
super(message);
|
|
84
|
+
this.name = "TenantConflictError";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Delete refused because the Tenant still has live work (HTTP 409). */
|
|
88
|
+
export class TenantBusyError extends Error {
|
|
89
|
+
statusCode = 409;
|
|
90
|
+
constructor(message = "Tenant has live work; pass activeWork=drain|cancel or retry later") {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "TenantBusyError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Store } from "../core/store.js";
|
|
2
|
+
import type { ExecutorRegistry } from "../core/executors.js";
|
|
3
|
+
import type { SandboxManager } from "../sandbox/manager.js";
|
|
4
|
+
import type { TenantPaths } from "./types.js";
|
|
5
|
+
export type ResetScope = "sessions" | "sandboxes" | "all";
|
|
6
|
+
export interface ResetTenantContext {
|
|
7
|
+
store: Store;
|
|
8
|
+
registry: ExecutorRegistry;
|
|
9
|
+
sandbox: SandboxManager;
|
|
10
|
+
paths: TenantPaths;
|
|
11
|
+
/** Clear in-memory session observers after DB wipe. */
|
|
12
|
+
clearSessionState: () => void;
|
|
13
|
+
/** Clear executor SSE streams when registrations are removed. */
|
|
14
|
+
clearExecutorStreams: () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reset Tenant durable state for `scope` (A17).
|
|
18
|
+
* Caller must have already drained (`activeWork`). Runs SQLite work in one
|
|
19
|
+
* transaction, then filesystem rename+delete for sandboxes / log as needed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resetTenant(ctx: ResetTenantContext, scope: ResetScope): Promise<void>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const SESSION_TABLES = [
|
|
4
|
+
"sessions",
|
|
5
|
+
"commands",
|
|
6
|
+
"checkpoints",
|
|
7
|
+
"effects",
|
|
8
|
+
"actions",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Rename `dir` to a sibling, delete the sibling, recreate an empty `dir`.
|
|
12
|
+
* Failure before rename leaves the original intact; after rename the empty
|
|
13
|
+
* target is restored so the Tenant layout stays valid (A17).
|
|
14
|
+
*/
|
|
15
|
+
function replaceDirectory(dir) {
|
|
16
|
+
mkdirSync(dir, { recursive: true });
|
|
17
|
+
const sibling = `${dir}.resetting`;
|
|
18
|
+
if (existsSync(sibling))
|
|
19
|
+
rmSync(sibling, { recursive: true, force: true });
|
|
20
|
+
renameSync(dir, sibling);
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
rmSync(sibling, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// Best-effort restore if recreation failed.
|
|
27
|
+
if (!existsSync(dir) && existsSync(sibling)) {
|
|
28
|
+
try {
|
|
29
|
+
renameSync(sibling, dir);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* leave sibling for operator inspection */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function clearSessionTables(store) {
|
|
39
|
+
for (const table of SESSION_TABLES) {
|
|
40
|
+
store.db.prepare(`DELETE FROM ${table}`).run();
|
|
41
|
+
}
|
|
42
|
+
store.db.prepare("DELETE FROM events").run();
|
|
43
|
+
}
|
|
44
|
+
function clearSandboxRows(store) {
|
|
45
|
+
store.db.prepare("DELETE FROM sandboxes").run();
|
|
46
|
+
}
|
|
47
|
+
function clearDefinitions(store) {
|
|
48
|
+
store.db.prepare("DELETE FROM definitions").run();
|
|
49
|
+
}
|
|
50
|
+
function clearExecutors(store, registry) {
|
|
51
|
+
for (const row of registry.list())
|
|
52
|
+
registry.remove(row.agentId);
|
|
53
|
+
store.db.prepare("DELETE FROM executors").run();
|
|
54
|
+
}
|
|
55
|
+
/** Delete user-scoped vaults; keep the host vault (model credentials). */
|
|
56
|
+
function clearUserVaults(store) {
|
|
57
|
+
const userVaults = store.db
|
|
58
|
+
.prepare(`SELECT id FROM vaults WHERE scope != 'host'`)
|
|
59
|
+
.all();
|
|
60
|
+
for (const vault of userVaults) {
|
|
61
|
+
store.db
|
|
62
|
+
.prepare(`DELETE FROM vault_credentials WHERE vault_id=?`)
|
|
63
|
+
.run(vault.id);
|
|
64
|
+
store.db.prepare(`DELETE FROM vaults WHERE id=?`).run(vault.id);
|
|
65
|
+
}
|
|
66
|
+
// Drop audit / idempotency rows that referenced user vaults only when all.
|
|
67
|
+
// Keep host-model idempotency so seed replay stays stable.
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Reset Tenant durable state for `scope` (A17).
|
|
71
|
+
* Caller must have already drained (`activeWork`). Runs SQLite work in one
|
|
72
|
+
* transaction, then filesystem rename+delete for sandboxes / log as needed.
|
|
73
|
+
*/
|
|
74
|
+
export async function resetTenant(ctx, scope) {
|
|
75
|
+
const clearSessions = scope === "sessions" || scope === "all";
|
|
76
|
+
const clearSandboxes = scope === "sandboxes" || scope === "all";
|
|
77
|
+
const clearAll = scope === "all";
|
|
78
|
+
if (clearSandboxes) {
|
|
79
|
+
await ctx.sandbox.reconcile(() => false);
|
|
80
|
+
}
|
|
81
|
+
ctx.store.tx(() => {
|
|
82
|
+
if (clearSessions)
|
|
83
|
+
clearSessionTables(ctx.store);
|
|
84
|
+
if (clearSandboxes)
|
|
85
|
+
clearSandboxRows(ctx.store);
|
|
86
|
+
if (clearAll) {
|
|
87
|
+
clearDefinitions(ctx.store);
|
|
88
|
+
clearExecutors(ctx.store, ctx.registry);
|
|
89
|
+
clearUserVaults(ctx.store);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (clearSessions)
|
|
93
|
+
ctx.clearSessionState();
|
|
94
|
+
if (clearAll)
|
|
95
|
+
ctx.clearExecutorStreams();
|
|
96
|
+
if (clearSandboxes)
|
|
97
|
+
replaceDirectory(ctx.paths.sandboxes);
|
|
98
|
+
if (clearAll) {
|
|
99
|
+
mkdirSync(ctx.paths.logs, { recursive: true });
|
|
100
|
+
const logSibling = join(ctx.paths.logs, "tenant.log.resetting");
|
|
101
|
+
if (existsSync(ctx.paths.log)) {
|
|
102
|
+
if (existsSync(logSibling))
|
|
103
|
+
rmSync(logSibling, { force: true });
|
|
104
|
+
renameSync(ctx.paths.log, logSibling);
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(ctx.paths.log, "");
|
|
107
|
+
rmSync(logSibling, { force: true });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!existsSync(ctx.paths.log) && existsSync(logSibling)) {
|
|
111
|
+
try {
|
|
112
|
+
renameSync(logSibling, ctx.paths.log);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* leave sibling */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
writeFileSync(ctx.paths.log, "");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ServerResponse, type IncomingMessage } from "node:http";
|
|
2
|
+
import { type ModelProvider } from "../core/provider.js";
|
|
3
|
+
import type { TenantConfig, TenantHandle, TenantSummary } from "./types.js";
|
|
4
|
+
import type { TenantEnvelope } from "@nylorun/core/contracts";
|
|
5
|
+
import { type AuthorizeResult } from "../vault/service.js";
|
|
6
|
+
/** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
|
|
7
|
+
export type TenantOpenHooks = {
|
|
8
|
+
modelProvider?: ModelProvider;
|
|
9
|
+
vaultKek?: Buffer | string | null;
|
|
10
|
+
/** When true, create the KEK file on first vault write (tests / new Tenants). */
|
|
11
|
+
createKekIfMissing?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare class TenantRuntime implements TenantHandle {
|
|
14
|
+
private readonly config;
|
|
15
|
+
private readonly store;
|
|
16
|
+
private readonly vault;
|
|
17
|
+
private kek;
|
|
18
|
+
private readonly kekPath;
|
|
19
|
+
private readonly running;
|
|
20
|
+
private readonly pending;
|
|
21
|
+
private readonly observers;
|
|
22
|
+
private readonly executorStreams;
|
|
23
|
+
private readonly registry;
|
|
24
|
+
private readonly mcp;
|
|
25
|
+
private readonly sandbox;
|
|
26
|
+
private closing;
|
|
27
|
+
private closed;
|
|
28
|
+
private readonly lockPath?;
|
|
29
|
+
private readonly timer;
|
|
30
|
+
private readonly modelProvider;
|
|
31
|
+
private readonly useVaultModel;
|
|
32
|
+
private readonly createKekIfMissing;
|
|
33
|
+
private readonly flowLimits;
|
|
34
|
+
readonly envelope: TenantEnvelope;
|
|
35
|
+
private constructor();
|
|
36
|
+
static open(config: TenantConfig, hooks?: TenantOpenHooks): Promise<TenantRuntime>;
|
|
37
|
+
private session;
|
|
38
|
+
private publish;
|
|
39
|
+
private send;
|
|
40
|
+
private notify;
|
|
41
|
+
/**
|
|
42
|
+
* Manifest v4 replaced beforeModelCall/afterModelCall with scoped hooks. Turns started under
|
|
43
|
+
* an older manifest cannot continue, and their hook actions no longer parse, so end them.
|
|
44
|
+
*/
|
|
45
|
+
private retireLegacyHooks;
|
|
46
|
+
private expireClaims;
|
|
47
|
+
private schedule;
|
|
48
|
+
private execute;
|
|
49
|
+
private invokeModel;
|
|
50
|
+
private resolveEffect;
|
|
51
|
+
/** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
|
|
52
|
+
private resolveNewFlowEffect;
|
|
53
|
+
private scope;
|
|
54
|
+
private scoped;
|
|
55
|
+
private requireApplication;
|
|
56
|
+
private sandboxRouteDeps;
|
|
57
|
+
private body;
|
|
58
|
+
private command;
|
|
59
|
+
private sse;
|
|
60
|
+
handle(request: IncomingMessage, response: ServerResponse, _url?: URL): Promise<void>;
|
|
61
|
+
private view;
|
|
62
|
+
private prepareMcp;
|
|
63
|
+
private callMcpTool;
|
|
64
|
+
private callSandboxTool;
|
|
65
|
+
authorize(sessionId: string, request: {
|
|
66
|
+
url: string;
|
|
67
|
+
serverName?: string;
|
|
68
|
+
}): Promise<AuthorizeResult>;
|
|
69
|
+
private ensureKek;
|
|
70
|
+
private dispatchTenant;
|
|
71
|
+
private dispatchVault;
|
|
72
|
+
summary(): TenantSummary;
|
|
73
|
+
drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
|
|
74
|
+
close(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Open a Tenant Runtime from an explicit `TenantConfig`. No listen() (A1).
|
|
78
|
+
* Implements `OpenTenantRuntime`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function openTenantRuntime(config: TenantConfig, hooks?: TenantOpenHooks): Promise<TenantHandle>;
|