@nylorun/runtime 0.19.1-beta → 0.21.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 +90 -0
- package/README.md +22 -13
- package/dist/adapters/streams/s2.js +5 -11
- package/dist/api/components.d.ts +326 -205
- package/dist/api/components.js +3 -3
- package/dist/api/http/app.js +2 -0
- package/dist/api/http/define.d.ts +3 -2
- package/dist/api/http/define.js +10 -11
- package/dist/api/http/routes/mcp.d.ts +9 -0
- package/dist/api/http/routes/mcp.js +32 -0
- package/dist/api/http/routes/vaults.d.ts +1 -9
- package/dist/api/http/routes/vaults.js +3 -103
- package/dist/api/openapi.js +37 -6
- package/dist/artifacts/media-types.d.ts +2 -0
- package/dist/artifacts/media-types.js +5 -0
- package/dist/execution/types.d.ts +1 -1
- package/dist/gates/http-tool.d.ts +7 -0
- package/dist/gates/http-tool.js +34 -3
- package/dist/gates/mcp-handler.d.ts +4 -1
- package/dist/gates/mcp-handler.js +64 -8
- package/dist/gates/tenant-vaults.d.ts +9 -15
- package/dist/gates/tenant-vaults.js +9 -17
- package/dist/gates/tool-client.js +36 -17
- package/dist/gates/tool-contract.d.ts +19 -3
- package/dist/gates/tool-contract.js +9 -3
- package/dist/gates/tool-gate.d.ts +2 -2
- package/dist/harness/calls.d.ts +2 -0
- package/dist/harness/calls.js +13 -3
- package/dist/harness-api/record.d.ts +7 -6
- package/dist/harness-api/record.js +99 -17
- package/dist/harness-api/server.js +35 -27
- package/dist/harness-api/start.js +2 -1
- package/dist/host/app.d.ts +3 -0
- package/dist/host/app.js +33 -4
- package/dist/host/config-for.d.ts +0 -2
- package/dist/host/config-for.js +0 -1
- package/dist/host/create-host.d.ts +6 -0
- package/dist/host/create-host.js +1 -0
- package/dist/host/gates.js +0 -1
- package/dist/host/main.js +24 -7
- package/dist/host/stack-config.d.ts +5 -8
- package/dist/host/stack-config.js +9 -20
- package/dist/index.d.ts +0 -1
- package/dist/infra/index.d.ts +2 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/readiness.d.ts +6 -6
- package/dist/infra/readiness.js +2 -5
- package/dist/keys/client.js +1 -9
- package/dist/keys/keys.d.ts +11 -13
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +671 -378
- package/dist/mcp/connect.d.ts +159 -13
- package/dist/mcp/connect.js +538 -55
- package/dist/mcp/pool.d.ts +11 -2
- package/dist/mcp/pool.js +74 -27
- package/dist/mcp/preview.d.ts +35 -0
- package/dist/mcp/preview.js +215 -0
- package/dist/mcp/snapshot.d.ts +79 -5
- package/dist/mcp/snapshot.js +145 -27
- package/dist/model/catalog.d.ts +14 -0
- package/dist/model/catalog.js +14 -1
- package/dist/model/pi-model.js +36 -2
- package/dist/openapi.json +644 -266
- package/dist/redact.d.ts +19 -1
- package/dist/redact.js +42 -13
- package/dist/sandbox/session-sandbox.d.ts +7 -0
- package/dist/sandbox/session-sandbox.js +89 -28
- package/dist/store/postgres/connect.d.ts +10 -0
- package/dist/store/postgres/connect.js +30 -3
- package/dist/store/postgres/control.d.ts +14 -0
- package/dist/store/postgres/control.js +187 -0
- package/dist/store/postgres/drizzle/0015_control_signals.sql +11 -0
- package/dist/store/postgres/drizzle/0016_mcp_oauth_removed.sql +5 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +14 -0
- package/dist/store/postgres/schema.d.ts +122 -261
- package/dist/store/postgres/schema.js +22 -36
- package/dist/store/postgres/store.js +16 -18
- package/dist/store/postgres/tenant.d.ts +14 -0
- package/dist/store/postgres/tenant.js +22 -2
- package/dist/store/types.d.ts +42 -8
- package/dist/streams/types.d.ts +6 -29
- package/dist/streams/types.js +5 -4
- package/dist/tenant/artifact-tool.d.ts +9 -1
- package/dist/tenant/artifact-tool.js +113 -9
- package/dist/tenant/auth.d.ts +16 -6
- package/dist/tenant/auth.js +41 -17
- package/dist/tenant/commands.js +10 -6
- package/dist/tenant/context.d.ts +9 -6
- package/dist/tenant/control.d.ts +26 -0
- package/dist/tenant/control.js +35 -0
- package/dist/tenant/effects.d.ts +2 -2
- package/dist/tenant/effects.js +20 -5
- package/dist/tenant/ephemeral.d.ts +0 -6
- package/dist/tenant/ephemeral.js +1 -1
- package/dist/tenant/http.d.ts +4 -2
- package/dist/tenant/http.js +4 -2
- package/dist/tenant/identity-file.d.ts +16 -12
- package/dist/tenant/identity-file.js +41 -29
- package/dist/tenant/issuers.d.ts +4 -3
- package/dist/tenant/issuers.js +22 -16
- package/dist/tenant/outbound.d.ts +14 -5
- package/dist/tenant/outbound.js +21 -9
- package/dist/tenant/reset.d.ts +2 -2
- package/dist/tenant/reset.js +8 -2
- package/dist/tenant/resource-server.d.ts +56 -0
- package/dist/tenant/resource-server.js +62 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +16 -15
- package/dist/tenant/scheduler.d.ts +1 -1
- package/dist/tenant/scheduler.js +1 -1
- package/dist/tenant/sessions.js +20 -3
- package/dist/tenant/store-pg.js +9 -0
- package/dist/tenant/streams.d.ts +16 -22
- package/dist/tenant/streams.js +7 -87
- package/dist/tenant/sweep.js +9 -1
- package/dist/tenant/tool-results.d.ts +57 -0
- package/dist/tenant/tool-results.js +298 -0
- package/dist/tenant/tool-search.d.ts +43 -0
- package/dist/tenant/tool-search.js +100 -0
- package/dist/tenant/transcript.d.ts +108 -0
- package/dist/tenant/transcript.js +13 -0
- package/dist/tenant/types.d.ts +0 -14
- package/dist/vault/crypto.d.ts +0 -7
- package/dist/vault/crypto.js +0 -16
- package/dist/vault/headers.d.ts +17 -0
- package/dist/vault/headers.js +61 -0
- package/dist/vault/service.d.ts +11 -68
- package/dist/vault/service.js +83 -438
- package/dist/vault/sources.d.ts +20 -48
- package/dist/vault/sources.js +25 -189
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/streams/control.d.ts +0 -13
- package/dist/streams/control.js +0 -25
- package/dist/vault/oauth.d.ts +0 -55
- package/dist/vault/oauth.js +0 -186
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Runtime API as an OAuth 2.1 resource server (protocol 9, Host feature `resource-server`).
|
|
3
|
+
* Nylorun is never the authorization server: the operator's identity providers are, listed in
|
|
4
|
+
* the identity file, and this module only says so in the standard places.
|
|
5
|
+
*
|
|
6
|
+
* - `GET /.well-known/oauth-protected-resource` (RFC 9728): the resource (`NYLORUN_PUBLIC_URL`,
|
|
7
|
+
* else the origin the request reached), every trusted issuer in the file's order, and the
|
|
8
|
+
* scopes their tokens may carry. Without trusted issuers there is none: the Runtime takes no
|
|
9
|
+
* OAuth tokens.
|
|
10
|
+
* - `WWW-Authenticate: Bearer` challenges (OAuth 2.1 §5.3, RFC 6750): a missing credential
|
|
11
|
+
* carries no error code; a rejected one `error="invalid_token"`; a token missing a route's
|
|
12
|
+
* scope `error="insufficient_scope"` with the scopes that would do. On the Runtime API they
|
|
13
|
+
* point at the metadata (`resource_metadata`); the Management API takes management keys only
|
|
14
|
+
* and is no OAuth resource, so its challenge is a bare `Bearer`.
|
|
15
|
+
*/
|
|
16
|
+
import type { IncomingMessage } from "node:http";
|
|
17
|
+
import type { TrustedIssuers } from "./issuers.js";
|
|
18
|
+
/** Where the protected resource metadata is served (RFC 9728 §3). */
|
|
19
|
+
export declare const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
20
|
+
/** What the metadata and the challenges are made of: `TenantConfig`'s fields of the same names. */
|
|
21
|
+
export interface ResourceServerConfig {
|
|
22
|
+
/** The URL clients reach the Runtime API at (`NYLORUN_PUBLIC_URL`, no trailing slash). */
|
|
23
|
+
readonly publicUrl?: string;
|
|
24
|
+
/** The identity file's trusted issuers. */
|
|
25
|
+
readonly issuers?: TrustedIssuers;
|
|
26
|
+
}
|
|
27
|
+
/** RFC 9728 protected resource metadata. */
|
|
28
|
+
export interface ProtectedResourceMetadata {
|
|
29
|
+
readonly resource: string;
|
|
30
|
+
readonly authorization_servers: readonly string[];
|
|
31
|
+
readonly scopes_supported: readonly string[];
|
|
32
|
+
readonly bearer_methods_supported: readonly ["header"];
|
|
33
|
+
readonly resource_name: string;
|
|
34
|
+
}
|
|
35
|
+
/** The metadata, or undefined when the Runtime trusts no issuer. */
|
|
36
|
+
export declare function protectedResourceMetadata(config: ResourceServerConfig, incoming: IncomingMessage): ProtectedResourceMetadata | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The metadata's URL for a challenge's `resource_metadata`, or undefined when there is none.
|
|
39
|
+
* A resource with a path has its metadata at the origin, the path after the well-known part.
|
|
40
|
+
*/
|
|
41
|
+
export declare function protectedResourceMetadataUrl(config: ResourceServerConfig, incoming: IncomingMessage): string | undefined;
|
|
42
|
+
interface Challenge {
|
|
43
|
+
readonly error?: "invalid_token" | "insufficient_scope";
|
|
44
|
+
readonly description?: string;
|
|
45
|
+
readonly scope?: string;
|
|
46
|
+
readonly resourceMetadata?: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** A `WWW-Authenticate: Bearer` value. Every parameter value is plain ASCII without quotes. */
|
|
49
|
+
export declare function bearerChallenge(challenge?: Challenge): string;
|
|
50
|
+
/**
|
|
51
|
+
* The `401` for a request without a usable credential: `credential_required` when it sent
|
|
52
|
+
* none (no error code, OAuth 2.1 §5.3.2), `credential_invalid` when the Tenant refused the one
|
|
53
|
+
* it sent. The reason is the Runtime's log's, never the client's.
|
|
54
|
+
*/
|
|
55
|
+
export declare function failCredential(kind: "required" | "invalid", resourceMetadata: string | undefined): never;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { TOKEN_SCOPES } from "@nylorun/core/contracts";
|
|
2
|
+
import { HttpError } from "./http.js";
|
|
3
|
+
/** Where the protected resource metadata is served (RFC 9728 §3). */
|
|
4
|
+
export const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
5
|
+
/** The origin `incoming` reached, as `@hono/node-server` builds a request's URL. */
|
|
6
|
+
function requestOrigin(incoming) {
|
|
7
|
+
const scheme = incoming.socket?.encrypted ? "https" : "http";
|
|
8
|
+
return `${scheme}://${incoming.headers.host ?? "localhost"}`;
|
|
9
|
+
}
|
|
10
|
+
/** This Runtime API's resource identifier: the public URL, else the request's origin. */
|
|
11
|
+
function resourceOf(config, incoming) {
|
|
12
|
+
return config.publicUrl ?? requestOrigin(incoming);
|
|
13
|
+
}
|
|
14
|
+
/** The metadata, or undefined when the Runtime trusts no issuer. */
|
|
15
|
+
export function protectedResourceMetadata(config, incoming) {
|
|
16
|
+
const issuers = config.issuers?.issuers ?? [];
|
|
17
|
+
if (issuers.length === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
// `studio` signs a person in to Studio; no Runtime API route asks for it.
|
|
20
|
+
const granted = new Set(issuers.flatMap((issuer) => issuer.config.allowedScopes));
|
|
21
|
+
return {
|
|
22
|
+
resource: resourceOf(config, incoming),
|
|
23
|
+
authorization_servers: issuers.map((issuer) => issuer.config.issuer),
|
|
24
|
+
scopes_supported: TOKEN_SCOPES.filter((scope) => granted.has(scope)),
|
|
25
|
+
bearer_methods_supported: ["header"],
|
|
26
|
+
resource_name: "Nylorun Runtime API",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The metadata's URL for a challenge's `resource_metadata`, or undefined when there is none.
|
|
31
|
+
* A resource with a path has its metadata at the origin, the path after the well-known part.
|
|
32
|
+
*/
|
|
33
|
+
export function protectedResourceMetadataUrl(config, incoming) {
|
|
34
|
+
if ((config.issuers?.issuers.length ?? 0) === 0)
|
|
35
|
+
return undefined;
|
|
36
|
+
const url = new URL(resourceOf(config, incoming));
|
|
37
|
+
const path = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, "");
|
|
38
|
+
return `${url.origin}${PROTECTED_RESOURCE_PATH}${path}`;
|
|
39
|
+
}
|
|
40
|
+
/** A `WWW-Authenticate: Bearer` value. Every parameter value is plain ASCII without quotes. */
|
|
41
|
+
export function bearerChallenge(challenge = {}) {
|
|
42
|
+
const params = [];
|
|
43
|
+
if (challenge.error)
|
|
44
|
+
params.push(`error="${challenge.error}"`);
|
|
45
|
+
if (challenge.description)
|
|
46
|
+
params.push(`error_description="${challenge.description}"`);
|
|
47
|
+
if (challenge.scope)
|
|
48
|
+
params.push(`scope="${challenge.scope}"`);
|
|
49
|
+
if (challenge.resourceMetadata)
|
|
50
|
+
params.push(`resource_metadata="${challenge.resourceMetadata}"`);
|
|
51
|
+
return params.length === 0 ? "Bearer" : `Bearer ${params.join(", ")}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The `401` for a request without a usable credential: `credential_required` when it sent
|
|
55
|
+
* none (no error code, OAuth 2.1 §5.3.2), `credential_invalid` when the Tenant refused the one
|
|
56
|
+
* it sent. The reason is the Runtime's log's, never the client's.
|
|
57
|
+
*/
|
|
58
|
+
export function failCredential(kind, resourceMetadata) {
|
|
59
|
+
if (kind === "required")
|
|
60
|
+
throw new HttpError(401, "A bearer credential is required", { code: "credential_required" }, { "www-authenticate": bearerChallenge({ resourceMetadata }) });
|
|
61
|
+
throw new HttpError(401, "The credential is not valid here", { code: "credential_invalid" }, { "www-authenticate": bearerChallenge({ error: "invalid_token", resourceMetadata }) });
|
|
62
|
+
}
|
package/dist/tenant/runtime.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export declare class TenantRuntime implements TenantHandle {
|
|
|
108
108
|
drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
|
|
109
109
|
/**
|
|
110
110
|
* Aborts the advance of `sessionId` if it runs on this process. Cancel calls it after
|
|
111
|
-
* committing `cancelled`; the control
|
|
111
|
+
* committing `cancelled`; the control bus calls it for cancels made elsewhere.
|
|
112
112
|
*/
|
|
113
113
|
abortLocal(sessionId: string): void;
|
|
114
114
|
/**
|
package/dist/tenant/runtime.js
CHANGED
|
@@ -5,7 +5,6 @@ import { tenantModelGate } from "../gates/in-process.js";
|
|
|
5
5
|
import { createKekFile, readVaultKek } from "../vault/kek.js";
|
|
6
6
|
import { SigningKeys } from "./signing-keys.js";
|
|
7
7
|
import { VaultService } from "../vault/service.js";
|
|
8
|
-
import { CredentialSources } from "../vault/sources.js";
|
|
9
8
|
import { McpPool } from "../mcp/pool.js";
|
|
10
9
|
import { SandboxManager } from "../sandbox/manager.js";
|
|
11
10
|
import { storeSandboxRecords } from "../sandbox/records.js";
|
|
@@ -13,6 +12,7 @@ import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
|
13
12
|
import { MemoryExecution } from "../execution/memory.js";
|
|
14
13
|
import { openError } from "./cause.js";
|
|
15
14
|
import { MemoryStreams } from "../streams/memory.js";
|
|
15
|
+
import { closeControl, wireControl } from "./control.js";
|
|
16
16
|
import { createSessionStreams, endAllStreams, } from "./session-streams.js";
|
|
17
17
|
import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
|
|
18
18
|
import { abortAll, abortLocal, createWorkState, drain, waitForIdle, } from "./scheduler.js";
|
|
@@ -21,7 +21,6 @@ import { sweep } from "./sweep.js";
|
|
|
21
21
|
import { TenantWorkers, WORKER_ID, } from "./worker.js";
|
|
22
22
|
import { authorize } from "./effects.js";
|
|
23
23
|
import { inProcessToolGate } from "../gates/tool-gate.js";
|
|
24
|
-
import { guardedFetch } from "./outbound.js";
|
|
25
24
|
import { inProcessKeys } from "../keys/keys.js";
|
|
26
25
|
import { tenantApi } from "../api/http/app.js";
|
|
27
26
|
import { createFsBlobStore } from "../blob/index.js";
|
|
@@ -85,6 +84,7 @@ export class TenantRuntime {
|
|
|
85
84
|
mkdirSync(paths.logs, { recursive: true });
|
|
86
85
|
const store = hooks.store;
|
|
87
86
|
let wired;
|
|
87
|
+
let control;
|
|
88
88
|
let detach;
|
|
89
89
|
let harness;
|
|
90
90
|
try {
|
|
@@ -116,16 +116,7 @@ export class TenantRuntime {
|
|
|
116
116
|
const opened = store;
|
|
117
117
|
// A seeded Tenant setting wins over the Host default (A18).
|
|
118
118
|
const seededBackend = await opened.tx((t) => t.getSetting("sandbox.backend"));
|
|
119
|
-
const vault = new VaultService({
|
|
120
|
-
store: opened,
|
|
121
|
-
kek: ensureKek,
|
|
122
|
-
// OAuth refresh and connect follow the Host's address policy (F9 C2).
|
|
123
|
-
fetch: config.vaultFetch ?? guardedFetch(config.delivery ?? {}),
|
|
124
|
-
});
|
|
125
|
-
const credentials = new CredentialSources({
|
|
126
|
-
vault,
|
|
127
|
-
...(config.resolver ? { resolver: config.resolver } : {}),
|
|
128
|
-
});
|
|
119
|
+
const vault = new VaultService({ store: opened, kek: ensureKek });
|
|
129
120
|
// `ctx` is assigned below; these callbacks only run once the Tenant is open.
|
|
130
121
|
let ctx;
|
|
131
122
|
const toolGate = hooks.toolGate ??
|
|
@@ -135,7 +126,14 @@ export class TenantRuntime {
|
|
|
135
126
|
logger: config.logger,
|
|
136
127
|
});
|
|
137
128
|
const signingKeys = new SigningKeys({ tenantId: config.tenantId, kek: ensureKek });
|
|
138
|
-
const keys = hooks.keys ??
|
|
129
|
+
const keys = hooks.keys ??
|
|
130
|
+
inProcessKeys({
|
|
131
|
+
store: opened,
|
|
132
|
+
vault,
|
|
133
|
+
signingKeys,
|
|
134
|
+
kek: ensureKek,
|
|
135
|
+
policy: config.delivery ?? {},
|
|
136
|
+
});
|
|
139
137
|
const harnessMode = hooks.harness ?? "memory";
|
|
140
138
|
const inProcess = harnessMode === "memory" || harnessMode === "json";
|
|
141
139
|
const preference = seededBackend ?? config.sandbox.backend;
|
|
@@ -240,7 +238,6 @@ export class TenantRuntime {
|
|
|
240
238
|
reads: hooks.reads,
|
|
241
239
|
store: opened,
|
|
242
240
|
vault,
|
|
243
|
-
credentials,
|
|
244
241
|
...(mcp ? { mcp } : {}),
|
|
245
242
|
sandbox,
|
|
246
243
|
flowLimits,
|
|
@@ -303,6 +300,8 @@ export class TenantRuntime {
|
|
|
303
300
|
...(hooks.hostRelay ? { hostRelay: true } : {}),
|
|
304
301
|
...(hooks.retireGraceMs !== undefined ? { retireGraceMs: hooks.retireGraceMs } : {}),
|
|
305
302
|
});
|
|
303
|
+
await wireControl(ctx);
|
|
304
|
+
control = ctx.control;
|
|
306
305
|
// Register the handlers, then arm the sweep: its first pass runs at once and re-wakes
|
|
307
306
|
// sessions a previous process left runnable or running (takeover handles the rest).
|
|
308
307
|
const worker = {
|
|
@@ -324,6 +323,7 @@ export class TenantRuntime {
|
|
|
324
323
|
await detach?.().catch(() => undefined);
|
|
325
324
|
await harness?.stop().catch(() => undefined);
|
|
326
325
|
await wired?.close().catch(() => undefined);
|
|
326
|
+
await control?.close().catch(() => undefined);
|
|
327
327
|
await hooks.reads?.close().catch(() => undefined);
|
|
328
328
|
await store.close().catch(() => undefined);
|
|
329
329
|
throw error;
|
|
@@ -350,7 +350,7 @@ export class TenantRuntime {
|
|
|
350
350
|
}
|
|
351
351
|
/**
|
|
352
352
|
* Aborts the advance of `sessionId` if it runs on this process. Cancel calls it after
|
|
353
|
-
* committing `cancelled`; the control
|
|
353
|
+
* committing `cancelled`; the control bus calls it for cancels made elsewhere.
|
|
354
354
|
*/
|
|
355
355
|
abortLocal(sessionId) {
|
|
356
356
|
this.ctx.abortLocal(sessionId);
|
|
@@ -402,6 +402,7 @@ export class TenantRuntime {
|
|
|
402
402
|
return closeStreams(ctx);
|
|
403
403
|
},
|
|
404
404
|
],
|
|
405
|
+
["control", () => closeControl(ctx)],
|
|
405
406
|
["reads", () => ctx.reads?.close()],
|
|
406
407
|
["store", () => ctx.store.close()],
|
|
407
408
|
];
|
|
@@ -9,7 +9,7 @@ export interface WorkState {
|
|
|
9
9
|
export declare function createWorkState(): WorkState;
|
|
10
10
|
/**
|
|
11
11
|
* Abort the advance of `id` if it runs on this process.
|
|
12
|
-
* Cancel calls it after committing `cancelled`, and so does the control
|
|
12
|
+
* Cancel calls it after committing `cancelled`, and so does the control bus for cancels made
|
|
13
13
|
* elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
|
|
14
14
|
*/
|
|
15
15
|
export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind, turnId?: string): void;
|
package/dist/tenant/scheduler.js
CHANGED
|
@@ -27,7 +27,7 @@ function abortWith(controller, kind) {
|
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Abort the advance of `id` if it runs on this process.
|
|
30
|
-
* Cancel calls it after committing `cancelled`, and so does the control
|
|
30
|
+
* Cancel calls it after committing `cancelled`, and so does the control bus for cancels made
|
|
31
31
|
* elsewhere. With `turnId`, an advance of another turn keeps running: the signal came late.
|
|
32
32
|
*/
|
|
33
33
|
export function abortLocal(ctx, id, kind = "cancel", turnId) {
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -10,7 +10,7 @@ import { aggregateWaits, isWorkflowManifest } from "../core/flow-host.js";
|
|
|
10
10
|
import { canonical } from "../store/canonical.js";
|
|
11
11
|
import { validateSandboxAttach } from "../core/sandbox-routes.js";
|
|
12
12
|
import { resolveSandbox } from "../sandbox/resolve.js";
|
|
13
|
-
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
13
|
+
import { withPlatformTools, withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
14
14
|
import { declaredSandboxes, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
15
15
|
import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
16
16
|
import { checkPlacement, requirePods } from "../sandbox/placement.js";
|
|
@@ -156,12 +156,14 @@ export async function putSession(ctx, id, body, access, options = {}) {
|
|
|
156
156
|
checkPlacement(await readSandboxConfig(t), sandbox.kind);
|
|
157
157
|
if (sandbox.kind === "pod")
|
|
158
158
|
await requirePods(ctx.pods, `Sandbox ${sandbox.sandboxId}`);
|
|
159
|
+
const pinned = sandbox.manifest === undefined ? platformTools(definition) : sandbox;
|
|
159
160
|
const created = {
|
|
160
161
|
id,
|
|
161
162
|
agentId: body.agentId,
|
|
162
163
|
ownerUserId: body.ownerUserId,
|
|
163
|
-
manifest:
|
|
164
|
-
manifestHash:
|
|
164
|
+
manifest: pinned.manifest ?? definition.manifest,
|
|
165
|
+
manifestHash: pinned.manifestHash ?? definition.manifestHash,
|
|
166
|
+
definitionHash: definition.manifestHash,
|
|
165
167
|
implementationVersion: definition.implementationVersion,
|
|
166
168
|
info: body.info,
|
|
167
169
|
status: "idle",
|
|
@@ -232,6 +234,20 @@ async function sessionSandbox(t, body, definition, options) {
|
|
|
232
234
|
return fail(resolved.status, resolved.errors.join(" "));
|
|
233
235
|
return pin(definition, resolved.spec, resolved.source);
|
|
234
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The manifest a session without a sandbox pins: its agents' `read_artifact` (R2b C11), when
|
|
239
|
+
* one has a remote MCP server or an HTTP tool; nothing otherwise, and for a workflow.
|
|
240
|
+
*/
|
|
241
|
+
function platformTools(definition) {
|
|
242
|
+
if (isWorkflowManifest(definition.manifest))
|
|
243
|
+
return {};
|
|
244
|
+
const pinned = withPlatformTools(definition.manifest);
|
|
245
|
+
if (pinned === undefined)
|
|
246
|
+
return {};
|
|
247
|
+
if (!pinned.ok)
|
|
248
|
+
return fail(400, pinned.message);
|
|
249
|
+
return { manifest: pinned.manifest, manifestHash: pinned.manifestHash };
|
|
250
|
+
}
|
|
235
251
|
async function pin(definition, spec, source) {
|
|
236
252
|
// A workflow's manifest stays as registered: its agents inherit the sandbox when they start.
|
|
237
253
|
if (isWorkflowManifest(definition.manifest))
|
|
@@ -269,6 +285,7 @@ export async function sessionView(t, s) {
|
|
|
269
285
|
agentId: s.agentId,
|
|
270
286
|
ownerUserId: s.ownerUserId,
|
|
271
287
|
manifestHash: s.manifestHash,
|
|
288
|
+
...(s.definitionHash === undefined ? {} : { definitionHash: s.definitionHash }),
|
|
272
289
|
implementationVersion: s.implementationVersion,
|
|
273
290
|
status: s.status,
|
|
274
291
|
activeTurnId: s.activeTurnId,
|
package/dist/tenant/store-pg.js
CHANGED
|
@@ -55,6 +55,15 @@ export function createPostgresTenantOpener(options) {
|
|
|
55
55
|
from: opened.migrated.from,
|
|
56
56
|
to: opened.migrated.to,
|
|
57
57
|
});
|
|
58
|
+
// Protocol 10 removed the vault's `oauth` credential type: the migration deleted them.
|
|
59
|
+
for (const removed of opened.removedCredentials)
|
|
60
|
+
logger?.warn("oauth_credential_removed", {
|
|
61
|
+
tenantId,
|
|
62
|
+
vaultId: removed.vaultId,
|
|
63
|
+
credentialId: removed.credentialId,
|
|
64
|
+
url: removed.url,
|
|
65
|
+
message: "MCP OAuth credentials are gone (protocol 10): store a key for this server in a vault, or reach it through a gateway",
|
|
66
|
+
});
|
|
58
67
|
let reads;
|
|
59
68
|
try {
|
|
60
69
|
reads = createPostgresReadStore(options.sql, tenantId);
|
package/dist/tenant/streams.d.ts
CHANGED
|
@@ -8,23 +8,24 @@
|
|
|
8
8
|
* otherwise (tests, a Host without S2) this module runs a relay of the Tenant's own commits,
|
|
9
9
|
* which reconciles the record with the streams when it starts.
|
|
10
10
|
* - **Basin generations.** The Tenant's session streams live in its current basin generation
|
|
11
|
-
* (`streams/basin.ts`). A reset moves it to the next one
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* (`streams/basin.ts`). A reset moves it to the next one: its transaction puts a
|
|
12
|
+
* `sessions.reset` signal on the control bus (`control.ts`), every process moves its readers
|
|
13
|
+
* (`tenantReset` here, `checkSessionStreams` elsewhere), and the old basin is deleted after a
|
|
14
|
+
* grace period (again when the Tenant opens, until it is gone).
|
|
14
15
|
* - **Basin.** The current basin is created when the Tenant opens. A missing basin (S2 down
|
|
15
16
|
* then, or deleted since) is repaired on first use: a failed `ensureTenant` retries in the background with backoff,
|
|
16
17
|
* and `streamsStatus` reports the basin's state.
|
|
17
18
|
* - **Readers.** History and session SSE read the session's stream
|
|
18
|
-
* (`tenant/session-streams.ts`).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* (`tenant/session-streams.ts`). Those are the only readers: S2 serves API listeners, and
|
|
20
|
+
* nothing internal reads or writes it (D48). Signals between processes go over the control
|
|
21
|
+
* bus on Postgres (`control.ts`).
|
|
21
22
|
*
|
|
22
23
|
* The caller passes the streams: the Host's S2 streams, or `MemoryStreams` for tests and a
|
|
23
24
|
* local development Host (not durable).
|
|
24
25
|
*/
|
|
25
26
|
import type { SessionStore } from "../store/types.js";
|
|
26
27
|
import { type StreamRelay, type StreamRelayStatus } from "../streams/relay/core.js";
|
|
27
|
-
import {
|
|
28
|
+
import type { DurableStreams } from "../streams/types.js";
|
|
28
29
|
import type { TenantContext } from "./context.js";
|
|
29
30
|
export interface WireStreamsOptions {
|
|
30
31
|
tenantId: string;
|
|
@@ -56,7 +57,7 @@ export interface StreamsWiring {
|
|
|
56
57
|
readonly relay: StreamRelay | undefined;
|
|
57
58
|
/** The current basin's state (`streamsStatus`). */
|
|
58
59
|
basin(): BasinStatus;
|
|
59
|
-
/** Moves this process's readers
|
|
60
|
+
/** Moves this process's readers to basin generation `generation`. */
|
|
60
61
|
moveTo(generation: number): void;
|
|
61
62
|
/** Deletes the basins of retired generations after the grace period. */
|
|
62
63
|
retire(generations: readonly number[]): void;
|
|
@@ -65,23 +66,16 @@ export interface StreamsWiring {
|
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
67
68
|
* Wires the Tenant to Durable Streams: reads its basin generation, checks the basin, starts
|
|
68
|
-
* the Tenant's relay (unless the Host relays)
|
|
69
|
-
*
|
|
70
|
-
*
|
|
69
|
+
* the Tenant's relay (unless the Host relays) and the retired basins' deletion, and records
|
|
70
|
+
* the handles on `ctx.sessionStreams.wiring`. Call it once, after `ctx` is built and before
|
|
71
|
+
* the Tenant serves requests.
|
|
71
72
|
*/
|
|
72
73
|
export declare function wireStreams(ctx: TenantContext, options: WireStreamsOptions): Promise<StreamsWiring>;
|
|
73
74
|
/**
|
|
74
|
-
*
|
|
75
|
-
* process
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*/
|
|
79
|
-
export declare function signalSessionCancel(ctx: TenantContext, sessionId: string, turnId: string | null): void;
|
|
80
|
-
/**
|
|
81
|
-
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
|
|
82
|
-
* every process with the Tenant open (`sessions.reset` on the old basin's control stream),
|
|
83
|
-
* moves this process's readers, and deletes the old basin after the grace period. Failures
|
|
84
|
-
* are logged; the periodic check and the next open finish the job.
|
|
75
|
+
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: moves
|
|
76
|
+
* this process's readers at once and deletes the old basin after the grace period. The other
|
|
77
|
+
* processes move on the reset's `sessions.reset` signal (`control.ts`); the periodic check
|
|
78
|
+
* and the next open finish the job.
|
|
85
79
|
*/
|
|
86
80
|
export declare function tenantReset(ctx: TenantContext): Promise<void>;
|
|
87
81
|
/** Tenant status of the streams seam (for `GET /v1/tenant`). */
|
package/dist/tenant/streams.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { basinOf } from "../streams/basin.js";
|
|
2
|
-
import { signalCancel, signalSessionsReset } from "../streams/control.js";
|
|
3
2
|
import { createStreamRelay, } from "../streams/relay/core.js";
|
|
4
|
-
import { CONTROL_STREAM, } from "../streams/types.js";
|
|
5
3
|
import { checkSessionStreams, currentBasin, sleep, } from "./session-streams.js";
|
|
6
4
|
const RETRY_MIN_MS = 100;
|
|
7
5
|
const RETRY_MAX_MS = 2000;
|
|
@@ -16,9 +14,9 @@ const RETIRE_GRACE_MS = 60_000;
|
|
|
16
14
|
const messageOf = (error) => error instanceof Error ? error.message : String(error);
|
|
17
15
|
/**
|
|
18
16
|
* Wires the Tenant to Durable Streams: reads its basin generation, checks the basin, starts
|
|
19
|
-
* the Tenant's relay (unless the Host relays)
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* the Tenant's relay (unless the Host relays) and the retired basins' deletion, and records
|
|
18
|
+
* the handles on `ctx.sessionStreams.wiring`. Call it once, after `ctx` is built and before
|
|
19
|
+
* the Tenant serves requests.
|
|
22
20
|
*/
|
|
23
21
|
export async function wireStreams(ctx, options) {
|
|
24
22
|
if (ctx.sessionStreams.wiring)
|
|
@@ -53,17 +51,6 @@ export async function wireStreams(ctx, options) {
|
|
|
53
51
|
},
|
|
54
52
|
});
|
|
55
53
|
relay?.start();
|
|
56
|
-
// One control reader, on the current basin; moving to a new generation restarts it there.
|
|
57
|
-
let control = new AbortController();
|
|
58
|
-
const startControl = () => follow(streams, currentBasin(ctx), CONTROL_STREAM, AbortSignal.any([stop.signal, control.signal]), (body) => {
|
|
59
|
-
const signal = body;
|
|
60
|
-
if (signal?.type === "session.cancel" && typeof signal.sessionId === "string")
|
|
61
|
-
ctx.abortLocal(signal.sessionId, typeof signal.turnId === "string" ? signal.turnId : undefined);
|
|
62
|
-
else if (signal?.type === "sessions.reset")
|
|
63
|
-
void checkSessionStreams(ctx).catch(report("session stream check failed"));
|
|
64
|
-
}, report("control stream read failed; retrying"));
|
|
65
|
-
// Signals appended from here on reach this process.
|
|
66
|
-
await startControl();
|
|
67
54
|
// Streams whose session was reset end on the `sessions.reset` signal; this catches lost ones.
|
|
68
55
|
const feedCheck = setInterval(() => void checkSessionStreams(ctx).catch(report("session stream check failed")), FEED_CHECK_MS);
|
|
69
56
|
feedCheck.unref();
|
|
@@ -78,9 +65,6 @@ export async function wireStreams(ctx, options) {
|
|
|
78
65
|
return;
|
|
79
66
|
ctx.sessionStreams.generation = generation;
|
|
80
67
|
basin.repair();
|
|
81
|
-
control.abort();
|
|
82
|
-
control = new AbortController();
|
|
83
|
-
void startControl();
|
|
84
68
|
},
|
|
85
69
|
retire(retired) {
|
|
86
70
|
for (const generation of retired) {
|
|
@@ -153,33 +137,16 @@ function commitSource(store, tenantId) {
|
|
|
153
137
|
};
|
|
154
138
|
}
|
|
155
139
|
/**
|
|
156
|
-
*
|
|
157
|
-
* process
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*/
|
|
161
|
-
export function signalSessionCancel(ctx, sessionId, turnId) {
|
|
162
|
-
const streams = ctx.sessionStreams.wiring?.streams;
|
|
163
|
-
if (!streams)
|
|
164
|
-
return;
|
|
165
|
-
void signalCancel(streams, currentBasin(ctx), sessionId, turnId ?? undefined).catch((error) => ctx.config.logger.warn("cancel signal failed", {
|
|
166
|
-
sessionId,
|
|
167
|
-
message: messageOf(error),
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
|
|
172
|
-
* every process with the Tenant open (`sessions.reset` on the old basin's control stream),
|
|
173
|
-
* moves this process's readers, and deletes the old basin after the grace period. Failures
|
|
174
|
-
* are logged; the periodic check and the next open finish the job.
|
|
140
|
+
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: moves
|
|
141
|
+
* this process's readers at once and deletes the old basin after the grace period. The other
|
|
142
|
+
* processes move on the reset's `sessions.reset` signal (`control.ts`); the periodic check
|
|
143
|
+
* and the next open finish the job.
|
|
175
144
|
*/
|
|
176
145
|
export async function tenantReset(ctx) {
|
|
177
146
|
const wiring = ctx.sessionStreams.wiring;
|
|
178
147
|
if (!wiring)
|
|
179
148
|
return;
|
|
180
|
-
const previous = currentBasin(ctx);
|
|
181
149
|
const generations = await ctx.store.tx((t) => t.basinGenerations());
|
|
182
|
-
await signalSessionsReset(wiring.streams, previous, generations.current).catch((error) => ctx.config.logger.warn("sessions.reset signal failed", { message: messageOf(error) }));
|
|
183
150
|
wiring.moveTo(generations.current);
|
|
184
151
|
wiring.retire(generations.retired);
|
|
185
152
|
}
|
|
@@ -283,50 +250,3 @@ function basinKeeper(streams, basinKey, signal, onError) {
|
|
|
283
250
|
status: () => ({ ready, failures, lastError }),
|
|
284
251
|
};
|
|
285
252
|
}
|
|
286
|
-
/**
|
|
287
|
-
* Follows a signal stream from its tail at start, calling `onRecord` for each record, until
|
|
288
|
-
* `signal` aborts. Failed reads retry with backoff; a read that ends by itself (the basin was
|
|
289
|
-
* deleted) starts again from the new tail. Resolves once the first tail is known (or failed).
|
|
290
|
-
*/
|
|
291
|
-
function follow(streams, basin, stream, signal, onRecord, onError) {
|
|
292
|
-
let started;
|
|
293
|
-
const ready = new Promise((resolve) => (started = resolve));
|
|
294
|
-
void (async () => {
|
|
295
|
-
let from;
|
|
296
|
-
let delay = RETRY_MIN_MS;
|
|
297
|
-
while (!signal.aborted) {
|
|
298
|
-
try {
|
|
299
|
-
try {
|
|
300
|
-
from ??= await streams.tail(basin, stream);
|
|
301
|
-
}
|
|
302
|
-
finally {
|
|
303
|
-
started();
|
|
304
|
-
}
|
|
305
|
-
for await (const record of streams.read(basin, stream, from, {
|
|
306
|
-
signal,
|
|
307
|
-
})) {
|
|
308
|
-
from = record.seq + 1;
|
|
309
|
-
delay = RETRY_MIN_MS;
|
|
310
|
-
try {
|
|
311
|
-
onRecord(record.body);
|
|
312
|
-
}
|
|
313
|
-
catch (error) {
|
|
314
|
-
onError(error);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
if (signal.aborted)
|
|
318
|
-
return;
|
|
319
|
-
from = undefined;
|
|
320
|
-
}
|
|
321
|
-
catch (error) {
|
|
322
|
-
if (signal.aborted)
|
|
323
|
-
return;
|
|
324
|
-
onError(error);
|
|
325
|
-
}
|
|
326
|
-
await sleep(delay, signal);
|
|
327
|
-
delay = Math.min(delay * 2, RETRY_MAX_MS);
|
|
328
|
-
}
|
|
329
|
-
started();
|
|
330
|
-
})();
|
|
331
|
-
return ready;
|
|
332
|
-
}
|
package/dist/tenant/sweep.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* stopped, and sandboxes whose session or sandbox resource is gone are removed, by the
|
|
14
14
|
* workspace capability (`harness-api/workspace.ts`: here, or in the harness that serves
|
|
15
15
|
* workspaces). Idle MCP connections of the in-process harness are closed.
|
|
16
|
-
* 4. **
|
|
16
|
+
* 4. **Control signals.** Signals on the control bus older than `SIGNAL_RETENTION_MS` are
|
|
17
|
+
* deleted: followers read back only the last two minutes (`store/postgres/control.ts`).
|
|
18
|
+
* 5. **Hooks.** Callbacks registered with `ctx.onSweep`.
|
|
17
19
|
*
|
|
18
20
|
* Every step runs one transaction per session it changes (a linked agent and its workflow
|
|
19
21
|
* share one, child first), so the sweep follows the lock order in `store/types.ts`. A failing
|
|
@@ -22,6 +24,8 @@
|
|
|
22
24
|
*/
|
|
23
25
|
import { pendingAgentEffects, reconcilePendingAgentEffect, } from "../core/flow-host.js";
|
|
24
26
|
const BATCH = 100;
|
|
27
|
+
/** How long a control signal is kept after it was written. */
|
|
28
|
+
const SIGNAL_RETENTION_MS = 60 * 60 * 1000;
|
|
25
29
|
export async function sweep(ctx) {
|
|
26
30
|
if (ctx.closing || ctx.closed)
|
|
27
31
|
return;
|
|
@@ -40,6 +44,10 @@ export async function sweep(ctx) {
|
|
|
40
44
|
["pods", () => reconcileStalePods(ctx, now)],
|
|
41
45
|
// A harness elsewhere closes its own idle MCP connections.
|
|
42
46
|
["mcp", async () => ctx.mcp?.sweep()],
|
|
47
|
+
[
|
|
48
|
+
"signals",
|
|
49
|
+
() => ctx.store.tx((t) => t.pruneSignals(new Date(now.getTime() - SIGNAL_RETENTION_MS))),
|
|
50
|
+
],
|
|
43
51
|
...[...ctx.sweepHooks].map((hook) => ["hook", hook]),
|
|
44
52
|
];
|
|
45
53
|
let failure;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HostEffect } from "@nylorun/harness/run";
|
|
2
|
+
import type { TenantContext } from "./context.js";
|
|
3
|
+
/** The most of one tool result that is recorded inline (Q22): 32 KiB, as JSON. */
|
|
4
|
+
export declare const TOOL_RESULT_INLINE_BYTES: number;
|
|
5
|
+
/** How much of a stored result's start, and of its end, the preview keeps. */
|
|
6
|
+
export declare const PREVIEW_HEAD_BYTES: number;
|
|
7
|
+
export declare const PREVIEW_TAIL_BYTES = 1024;
|
|
8
|
+
/**
|
|
9
|
+
* The most one step's results may hold together: a step's results are one transcript entry, so
|
|
10
|
+
* one event, which must stay well under S2's 1 MiB record.
|
|
11
|
+
*/
|
|
12
|
+
export declare const STEP_RESULTS_BUDGET_BYTES: number;
|
|
13
|
+
/** A result's cap once its step's budget is spent: a stub naming its artifact. */
|
|
14
|
+
export declare const TOOL_RESULT_STUB_BYTES = 512;
|
|
15
|
+
/** A result this small is never shaped, nor counted against its step's budget. */
|
|
16
|
+
export declare const TOOL_RESULT_SMALL_BYTES = 1024;
|
|
17
|
+
/** The call whose outcome is shaped, and the session its artifacts belong to. */
|
|
18
|
+
export interface ShapedCall {
|
|
19
|
+
readonly request: HostEffect;
|
|
20
|
+
readonly session: {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly ownerUserId: string;
|
|
23
|
+
};
|
|
24
|
+
/** A remote MCP tool's: its output may be the server's content parts. */
|
|
25
|
+
readonly mcp: boolean;
|
|
26
|
+
/** This result's cap (`resultLimit`). Default `TOOL_RESULT_INLINE_BYTES`. */
|
|
27
|
+
readonly limit?: number;
|
|
28
|
+
}
|
|
29
|
+
/** A result's cap when the other results of its step already took `used` bytes. */
|
|
30
|
+
export declare function resultLimit(used: number): number;
|
|
31
|
+
/** The JSON size of `value` in bytes: what it adds to an event or a prompt. */
|
|
32
|
+
export declare function jsonBytes(value: unknown): number;
|
|
33
|
+
/**
|
|
34
|
+
* What a tool outcome's result weighs in its step's transcript entry: a completed one's output,
|
|
35
|
+
* a failed one's message; 0 for anything else, such as a model call's outcome.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resultBytes(value: unknown): number;
|
|
38
|
+
/**
|
|
39
|
+
* False for an outcome `shapeToolOutcome` returns as it is, whatever made it: a model call's, a
|
|
40
|
+
* small tool result with no parts. Spares a look at the effect for most outcomes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mayNeedShaping(value: unknown): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The outcome `value` of `call`, shaped to fit its cap (see the module comment). An outcome that
|
|
45
|
+
* fits and has no binary part comes back as it is.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shapeToolOutcome(ctx: TenantContext, call: ShapedCall, value: unknown, signal: AbortSignal): Promise<unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* The start and end of `text` in whole characters, `budget` bytes split four to one: by default
|
|
50
|
+
* the first `PREVIEW_HEAD_BYTES` and the last `PREVIEW_TAIL_BYTES`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function previewOf(text: string, budget?: number): string;
|
|
53
|
+
/**
|
|
54
|
+
* The nearest character boundary of the UTF-8 `bytes` at or before (`down`) or at or after
|
|
55
|
+
* (`up`) byte `at`: never inside a character.
|
|
56
|
+
*/
|
|
57
|
+
export declare function utf8Boundary(bytes: Uint8Array, at: number, direction: "down" | "up"): number;
|