@nylorun/runtime 0.15.1-beta → 0.16.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 +82 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/replication/pgoutput.js +3 -1
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +15245 -8850
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
package/dist/tenant/sessions.js
CHANGED
|
@@ -12,6 +12,8 @@ import { resolveSandbox } from "../sandbox/resolve.js";
|
|
|
12
12
|
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
13
13
|
import { declaredSandboxes, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
14
14
|
import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
15
|
+
import { checkPlacement, requirePods } from "../sandbox/placement.js";
|
|
16
|
+
import { attachSandbox, recordAttachment } from "./sandboxes.js";
|
|
15
17
|
import { sandboxLookup, } from "./context.js";
|
|
16
18
|
import { fail } from "./http.js";
|
|
17
19
|
/** A session's creation identity: everything but the request id and vault attachments. */
|
|
@@ -133,7 +135,14 @@ export async function putSession(ctx, id, body, access, options = {}) {
|
|
|
133
135
|
}
|
|
134
136
|
const definition = (await t.get("definitions", body.agentId)) ??
|
|
135
137
|
fail(404, "Definition not found");
|
|
136
|
-
const sandbox = await sessionSandbox(t, body, definition, {
|
|
138
|
+
const sandbox = await sessionSandbox(t, body, definition, {
|
|
139
|
+
opaque,
|
|
140
|
+
...(options.sandboxGrants === undefined ? {} : { grants: options.sandboxGrants }),
|
|
141
|
+
});
|
|
142
|
+
// Placement (D38): where this session's harness may run, decided now and kept.
|
|
143
|
+
checkPlacement(await readSandboxConfig(t), sandbox.kind);
|
|
144
|
+
if (sandbox.kind === "pod")
|
|
145
|
+
await requirePods(ctx.pods, `Sandbox ${sandbox.sandboxId}`);
|
|
137
146
|
const created = {
|
|
138
147
|
id,
|
|
139
148
|
agentId: body.agentId,
|
|
@@ -151,23 +160,40 @@ export async function putSession(ctx, id, body, access, options = {}) {
|
|
|
151
160
|
...(sandbox.sandboxOwnerId !== undefined
|
|
152
161
|
? { sandboxOwnerId: sandbox.sandboxOwnerId }
|
|
153
162
|
: {}),
|
|
163
|
+
...(sandbox.sandboxId !== undefined ? { sandboxId: sandbox.sandboxId } : {}),
|
|
154
164
|
...(sandbox.spec !== undefined
|
|
155
165
|
? { sandbox: sandbox.spec, sandboxSource: sandbox.source }
|
|
156
166
|
: {}),
|
|
157
167
|
};
|
|
158
168
|
await t.put("sessions", id, created);
|
|
159
169
|
await ctx.vault.recordAttachment(t, id, vaultIds);
|
|
170
|
+
if (sandbox.sandboxId !== undefined)
|
|
171
|
+
await recordAttachment(t, id, sandbox.sandboxId);
|
|
160
172
|
return created;
|
|
161
173
|
});
|
|
162
174
|
}
|
|
163
175
|
function isShare(value) {
|
|
164
176
|
return typeof value === "object" && value !== null && "session" in value;
|
|
165
177
|
}
|
|
178
|
+
function isAttach(value) {
|
|
179
|
+
return typeof value === "object" && value !== null && "id" in value;
|
|
180
|
+
}
|
|
166
181
|
/** Decide a new session's sandbox and, for an agent, the manifest that carries its tools. */
|
|
167
182
|
async function sessionSandbox(t, body, definition, options) {
|
|
168
183
|
const request = body.sandbox;
|
|
169
184
|
const declared = sandboxSpecOf(definition.manifest) !== undefined;
|
|
185
|
+
if (isAttach(request)) {
|
|
186
|
+
if (declared)
|
|
187
|
+
fail(400, `'${body.agentId}' declares its own sandbox with .sandbox(). Remove it from the agent to attach a sandbox.`);
|
|
188
|
+
const sandbox = await attachSandbox(t, request.id, options.grants);
|
|
189
|
+
// A pod's storage and lifecycle are the resource's, not the agent's sandbox manifest.
|
|
190
|
+
const { storage: _storage, lifecycle: _lifecycle, ...spec } = sandbox.spec;
|
|
191
|
+
return { ...(await pin(definition, spec, "sandbox")), sandboxId: sandbox.id, kind: sandbox.kind };
|
|
192
|
+
}
|
|
170
193
|
if (isShare(request)) {
|
|
194
|
+
const owner = await t.get("sessions", request.session);
|
|
195
|
+
if (owner?.sandboxId !== undefined && (!options.opaque || owner.ownerUserId === body.ownerUserId))
|
|
196
|
+
fail(400, `Session ${request.session} is attached to sandbox ${owner.sandboxId}: open this session with sandbox: { id: "${owner.sandboxId}" }.`);
|
|
171
197
|
const lookup = await sandboxLookup(t, request.session);
|
|
172
198
|
const sandboxOwnerId = validateSandboxAttach(body, definition.manifest, lookup, {
|
|
173
199
|
opaque: options.opaque,
|
|
@@ -240,6 +266,7 @@ export async function sessionView(t, s) {
|
|
|
240
266
|
vaultIds: s.vaultIds ?? [],
|
|
241
267
|
credentialSelections: s.credentialSelections ?? [],
|
|
242
268
|
sandboxOwnerId: s.sandboxOwnerId ?? null,
|
|
269
|
+
...(s.sandboxId === undefined ? {} : { sandboxId: s.sandboxId }),
|
|
243
270
|
sandbox: s.sandbox ?? null,
|
|
244
271
|
...(s.sandboxSource ? { sandboxSource: s.sandboxSource } : {}),
|
|
245
272
|
mcpSnapshot: s.mcpSnapshot ?? null,
|
package/dist/tenant/status.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { SeedTenantConfigRequest, SeedTenantConfigResponse, TenantStatus } from "@nylorun/core/contracts";
|
|
1
|
+
import type { HarnessStatus, SeedTenantConfigRequest, SeedTenantConfigResponse, TenantStatus } from "@nylorun/core/contracts";
|
|
2
2
|
import type { SessionStore } from "../store/types.js";
|
|
3
3
|
import type { VaultService } from "../vault/service.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { WorkspacePort } from "../harness-api/workspace.js";
|
|
5
5
|
import type { TenantConfig } from "./types.js";
|
|
6
|
+
import type { TenantContext } from "./context.js";
|
|
6
7
|
import type { TenantEnvelope } from "@nylorun/core/contracts";
|
|
7
8
|
import type { StuckInvocation } from "../execution/types.js";
|
|
8
9
|
import type { StreamsStatus } from "./streams.js";
|
|
@@ -12,7 +13,7 @@ export interface TenantStatusContext {
|
|
|
12
13
|
config: TenantConfig;
|
|
13
14
|
store: SessionStore;
|
|
14
15
|
vault: VaultService;
|
|
15
|
-
sandbox:
|
|
16
|
+
sandbox: Pick<WorkspacePort, "report">;
|
|
16
17
|
closing: boolean;
|
|
17
18
|
modelConfigured: boolean;
|
|
18
19
|
/**
|
|
@@ -22,7 +23,11 @@ export interface TenantStatusContext {
|
|
|
22
23
|
stuckInvocations?: () => Promise<StuckInvocation[]>;
|
|
23
24
|
/** This Tenant's Durable Streams status (`streamsStatus` in `streams.ts`). */
|
|
24
25
|
streamsStatus?: () => Promise<StreamsStatus>;
|
|
26
|
+
/** The Tenant's harnesses (F6.2). */
|
|
27
|
+
harness?: HarnessStatus;
|
|
25
28
|
}
|
|
29
|
+
/** The Tenant's harnesses, for its status and the Host's. */
|
|
30
|
+
export declare function harnessStatusOf(ctx: Pick<TenantContext, "mcp" | "harness">): HarnessStatus;
|
|
26
31
|
/** Build `TenantStatusSchema` with secrets redacted (A14). */
|
|
27
32
|
export declare function buildTenantStatus(ctx: TenantStatusContext): Promise<TenantStatus>;
|
|
28
33
|
/**
|
package/dist/tenant/status.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { FIXTURE_MODEL_SETTING, seedFixtureModel } from "./model-setting.js";
|
|
2
2
|
import { SANDBOX_CONFIG_SETTING, writeSandboxConfig } from "../sandbox/tenant-config.js";
|
|
3
|
+
/** The Tenant's harnesses, for its status and the Host's. */
|
|
4
|
+
export function harnessStatusOf(ctx) {
|
|
5
|
+
return { mode: ctx.mcp ? "in-process" : "remote", ...ctx.harness.status() };
|
|
6
|
+
}
|
|
3
7
|
/** How long status waits for the execution to list stuck invocations. */
|
|
4
8
|
const STUCK_TIMEOUT_MS = 5000;
|
|
5
9
|
/** `TenantStatus.execution`: paused and backing-off invocations, or why they are unknown. */
|
|
@@ -84,6 +88,7 @@ export async function buildTenantStatus(ctx) {
|
|
|
84
88
|
backend: sandboxReport.backend,
|
|
85
89
|
retained: counts.sandboxes,
|
|
86
90
|
},
|
|
91
|
+
...(ctx.harness ? { harness: ctx.harness } : {}),
|
|
87
92
|
...(execution ? { execution } : {}),
|
|
88
93
|
...(streams ? { streams } : {}),
|
|
89
94
|
};
|
package/dist/tenant/streams.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* - **Readers.** History and session SSE read the session's stream
|
|
18
18
|
* (`tenant/session-streams.ts`). This module runs one `tenant/control` reader per Tenant per
|
|
19
19
|
* process, on the current basin, which calls `ctx.abortLocal` for each `session.cancel`,
|
|
20
|
+
* passes an Action's outcome to a run held here for each `action.resolved` (F6.2),
|
|
20
21
|
* checks the session streams for each `sessions.reset`, and ends a subject's streams for
|
|
21
22
|
* each `subject.revoked`.
|
|
22
23
|
*
|
|
@@ -78,6 +79,12 @@ export declare function wireStreams(ctx: TenantContext, options: WireStreamsOpti
|
|
|
78
79
|
* effect), so failures are logged, never thrown.
|
|
79
80
|
*/
|
|
80
81
|
export declare function signalSessionCancel(ctx: TenantContext, sessionId: string, turnId: string | null): void;
|
|
82
|
+
/**
|
|
83
|
+
* Appends `action.resolved` for an Action whose outcome just committed, so a run held for it
|
|
84
|
+
* by a harness of another process goes on (F6.2). Call it from `t.afterCommit`. A lost signal
|
|
85
|
+
* costs the hold's time only: the segment then ends as waiting and resumes by replay.
|
|
86
|
+
*/
|
|
87
|
+
export declare function signalActionOutcome(ctx: TenantContext, sessionId: string, actionId: string): void;
|
|
81
88
|
/**
|
|
82
89
|
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
|
|
83
90
|
* every process with the Tenant open (`sessions.reset` on the old basin's control stream),
|
package/dist/tenant/streams.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basinOf } from "../streams/basin.js";
|
|
2
|
-
import { signalCancel, signalSessionsReset } from "../streams/control.js";
|
|
2
|
+
import { signalActionResolved, signalCancel, signalSessionsReset } from "../streams/control.js";
|
|
3
3
|
import { createStreamRelay, } from "../streams/relay/core.js";
|
|
4
4
|
import { CONTROL_STREAM, } from "../streams/types.js";
|
|
5
5
|
import { checkSessionStreams, currentBasin, endSubjectStreams, sleep, } from "./session-streams.js";
|
|
@@ -59,6 +59,10 @@ export async function wireStreams(ctx, options) {
|
|
|
59
59
|
const signal = body;
|
|
60
60
|
if (signal?.type === "session.cancel" && typeof signal.sessionId === "string")
|
|
61
61
|
ctx.abortLocal(signal.sessionId, typeof signal.turnId === "string" ? signal.turnId : undefined);
|
|
62
|
+
else if (signal?.type === "action.resolved" &&
|
|
63
|
+
typeof signal.sessionId === "string" &&
|
|
64
|
+
typeof signal.actionId === "string")
|
|
65
|
+
void resolveHeld(ctx, signal.sessionId, signal.actionId).catch(report("held run resolution failed"));
|
|
62
66
|
else if (signal?.type === "sessions.reset")
|
|
63
67
|
void checkSessionStreams(ctx).catch(report("session stream check failed"));
|
|
64
68
|
else if (signal?.type === "subject.revoked" &&
|
|
@@ -171,6 +175,25 @@ export function signalSessionCancel(ctx, sessionId, turnId) {
|
|
|
171
175
|
message: messageOf(error),
|
|
172
176
|
}));
|
|
173
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Appends `action.resolved` for an Action whose outcome just committed, so a run held for it
|
|
180
|
+
* by a harness of another process goes on (F6.2). Call it from `t.afterCommit`. A lost signal
|
|
181
|
+
* costs the hold's time only: the segment then ends as waiting and resumes by replay.
|
|
182
|
+
*/
|
|
183
|
+
export function signalActionOutcome(ctx, sessionId, actionId) {
|
|
184
|
+
const streams = ctx.sessionStreams.wiring?.streams;
|
|
185
|
+
if (!streams)
|
|
186
|
+
return;
|
|
187
|
+
void signalActionResolved(streams, currentBasin(ctx), sessionId, actionId).catch((error) => ctx.config.logger.warn("action resolved signal failed", { sessionId, message: messageOf(error) }));
|
|
188
|
+
}
|
|
189
|
+
/** Passes an Action's recorded outcome to the run of `sessionId` held here, if any. */
|
|
190
|
+
async function resolveHeld(ctx, sessionId, actionId) {
|
|
191
|
+
if (!ctx.harness.holds(sessionId))
|
|
192
|
+
return;
|
|
193
|
+
const effect = await ctx.store.tx((t) => t.get("effects", actionId));
|
|
194
|
+
if (effect?.status === "completed" && effect.outcome)
|
|
195
|
+
ctx.harness.resolved(sessionId, actionId, effect.outcome);
|
|
196
|
+
}
|
|
174
197
|
/**
|
|
175
198
|
* After a reset deleted the Tenant's sessions and moved it to a new basin generation: tells
|
|
176
199
|
* every process with the Tenant open (`sessions.reset` on the old basin's control stream),
|
package/dist/tenant/sweep.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* 2. **Orphaned sessions.** `running` or `runnable` sessions with no owner, or an owner whose
|
|
10
10
|
* lease expired, are woken with reason `recover`: a wake lost between commit and send, or
|
|
11
11
|
* a Worker that died mid-advance (the advance then takes over, §11.4).
|
|
12
|
-
* 3. **Sandboxes.** Idle sandboxes are stopped, records of compute
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* 3. **Sandboxes.** Idle sandboxes are stopped, records of compute no longer held are marked
|
|
13
|
+
* stopped, and sandboxes whose session or sandbox resource is gone are removed, by the
|
|
14
|
+
* workspace capability (`harness-api/workspace.ts`: here, or in the harness that serves
|
|
15
|
+
* workspaces). Idle MCP connections of the in-process harness are closed.
|
|
15
16
|
* 4. **Hooks.** Callbacks registered with `ctx.onSweep`.
|
|
16
17
|
* 5. **Deliveries.** Deliveries to Action endpoints whose deadline passed without an answer are
|
|
17
18
|
* lost (`delivery.ts` `loseAction`), and pending Actions of agents with an endpoint are sent
|
|
@@ -38,9 +39,12 @@ export async function sweep(ctx) {
|
|
|
38
39
|
() => ctx.sandbox.sweep({
|
|
39
40
|
now: now.getTime(),
|
|
40
41
|
sessionExists: async (id) => !!(await ctx.store.tx((t) => t.get("sessions", id))),
|
|
42
|
+
sandboxExists: async (id) => (await ctx.store.tx((t) => t.sandboxResource(id))) !== undefined,
|
|
41
43
|
}),
|
|
42
44
|
],
|
|
43
|
-
["
|
|
45
|
+
["pods", () => reconcileStalePods(ctx, now)],
|
|
46
|
+
// A harness elsewhere closes its own idle MCP connections.
|
|
47
|
+
["mcp", async () => ctx.mcp?.sweep()],
|
|
44
48
|
...[...ctx.sweepHooks].map((hook) => ["hook", hook]),
|
|
45
49
|
];
|
|
46
50
|
let failure;
|
|
@@ -80,3 +84,32 @@ export async function wakeOrphanedSessions(ctx, now = new Date()) {
|
|
|
80
84
|
await ctx.wake(id, { reason: "recover" });
|
|
81
85
|
return ids;
|
|
82
86
|
}
|
|
87
|
+
/** A pod sandbox in a change this long without a reconcile gets one from the sweep. */
|
|
88
|
+
const STALE_POD_MS = 60_000;
|
|
89
|
+
/**
|
|
90
|
+
* Pod sandboxes (F7.2) whose change has not finished (creating, deleting, an old volume still
|
|
91
|
+
* to delete, an expiry not yet recorded), or that should run but whose engine is not connected,
|
|
92
|
+
* and whose row has not moved for a minute: a reconcile the `Sandbox` object lost (a send
|
|
93
|
+
* dropped while the Tenant closed) is sent again, and a pod or volume deleted outside the
|
|
94
|
+
* Runtime is found.
|
|
95
|
+
*/
|
|
96
|
+
async function reconcileStalePods(ctx, now) {
|
|
97
|
+
if (!ctx.pods)
|
|
98
|
+
return;
|
|
99
|
+
const stale = await ctx.store.tx(async (t) => (await t.listSandboxResources()).filter((row) => {
|
|
100
|
+
const pod = row.pod;
|
|
101
|
+
if (!pod || now.getTime() - Date.parse(row.updatedAt) < STALE_POD_MS)
|
|
102
|
+
return false;
|
|
103
|
+
const expiring = pod.expiresAt !== undefined && Date.parse(pod.expiresAt) <= now.getTime() && pod.observed !== "expired";
|
|
104
|
+
return (pod.desired === "deleted" ||
|
|
105
|
+
pod.retiring !== undefined ||
|
|
106
|
+
pod.observed === "creating" ||
|
|
107
|
+
pod.observed === "deleting" ||
|
|
108
|
+
expiring ||
|
|
109
|
+
(pod.desired === "suspended" && pod.observed !== "suspended") ||
|
|
110
|
+
// Running, but its engine is not connected: the pod may be gone, or its volume.
|
|
111
|
+
(pod.desired === "running" && pod.observed === "running" && !ctx.harness.hosting(row.id)));
|
|
112
|
+
}));
|
|
113
|
+
for (const row of stale)
|
|
114
|
+
await ctx.sandboxSignal(row.id, { kind: "reconcile" });
|
|
115
|
+
}
|
package/dist/tenant/tokens.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { errors, jwtVerify } from "jose";
|
|
12
|
-
import { isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
|
|
12
|
+
import { isSandboxGrant, isSubject, SUBJECT_TOKEN_AUDIENCE, SUBJECT_TOKEN_TYPE, subjectTokenIssuer, TOKEN_SANDBOX_GRANTS_MAX, TOKEN_SCOPES, TOKEN_TTL_MIN_SECONDS, } from "@nylorun/core/contracts";
|
|
13
13
|
import { agentAllowed, readPolicy, resolveRole } from "./access-policy.js";
|
|
14
14
|
import { fail, failOpaque, HttpError } from "./http.js";
|
|
15
15
|
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
|
|
@@ -71,6 +71,7 @@ export async function mintToken(ctx, body) {
|
|
|
71
71
|
role: body.role,
|
|
72
72
|
scp: scopes.join(" "),
|
|
73
73
|
...(body.agents ? { agt: [...body.agents] } : {}),
|
|
74
|
+
...(body.sandboxes?.length ? { sbx: [...new Set(body.sandboxes)] } : {}),
|
|
74
75
|
epc: epoch,
|
|
75
76
|
};
|
|
76
77
|
// Signed by the keys service (F4.2): this process never holds the private key.
|
|
@@ -99,6 +100,7 @@ export async function mintToken(ctx, body) {
|
|
|
99
100
|
role: body.role,
|
|
100
101
|
scopes,
|
|
101
102
|
agents: access.agents === "*" ? "*" : [...access.agents],
|
|
103
|
+
...(claims.sbx ? { sandboxes: [...claims.sbx] } : {}),
|
|
102
104
|
keyId: signer.id,
|
|
103
105
|
};
|
|
104
106
|
}
|
|
@@ -151,7 +153,11 @@ export async function verifySubjectToken(ctx, raw) {
|
|
|
151
153
|
scopeNames.some((name) => !TOKEN_SCOPES.includes(name)) ||
|
|
152
154
|
(claims.agt !== undefined &&
|
|
153
155
|
(!Array.isArray(claims.agt) ||
|
|
154
|
-
claims.agt.some((agent) => typeof agent !== "string")))
|
|
156
|
+
claims.agt.some((agent) => typeof agent !== "string"))) ||
|
|
157
|
+
(claims.sbx !== undefined &&
|
|
158
|
+
(!Array.isArray(claims.sbx) ||
|
|
159
|
+
claims.sbx.length > TOKEN_SANDBOX_GRANTS_MAX ||
|
|
160
|
+
claims.sbx.some((grant) => !isSandboxGrant(grant)))))
|
|
155
161
|
return rejected(ctx, "token_claims");
|
|
156
162
|
const epoch = await ctx.store.tx((t) => t.subjectEpoch(claims.sub));
|
|
157
163
|
if (claims.epc !== epoch)
|
|
@@ -168,6 +174,7 @@ export async function verifySubjectToken(ctx, raw) {
|
|
|
168
174
|
agents: access.agents,
|
|
169
175
|
role: access.role,
|
|
170
176
|
...(access.limits ? { limits: access.limits } : {}),
|
|
177
|
+
...(claims.sbx ? { sandboxes: claims.sbx } : {}),
|
|
171
178
|
epoch,
|
|
172
179
|
expiresAt: claims.exp * 1000,
|
|
173
180
|
tokenId: claims.jti,
|
package/dist/tenant/types.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import type { HostAggregate, HostTenant, TenantEnvelope } from "@nylorun/core/contracts";
|
|
2
|
+
import type { HarnessStatus, HostAggregate, HostTenant, TenantEnvelope } from "@nylorun/core/contracts";
|
|
3
|
+
import type { HarnessChannel } from "@nylorun/core/harness-api";
|
|
3
4
|
import type { FlowLimits } from "../core/limits.js";
|
|
5
|
+
import type { HarnessPeer } from "../harness-api/server.js";
|
|
6
|
+
import type { HostAuthority } from "../sandbox/join.js";
|
|
4
7
|
import type { SessionStore } from "../store/types.js";
|
|
5
8
|
import type { SandboxBackend } from "../sandbox/types.js";
|
|
6
9
|
import type { TenantCause } from "./cause.js";
|
|
@@ -19,6 +22,8 @@ export interface TenantPaths {
|
|
|
19
22
|
tmp: string;
|
|
20
23
|
sandboxes: string;
|
|
21
24
|
pluginData: string;
|
|
25
|
+
/** The `fs` BlobStore's directory, used when the Host has no Object store. */
|
|
26
|
+
blobs: string;
|
|
22
27
|
logs: string;
|
|
23
28
|
log: string;
|
|
24
29
|
}
|
|
@@ -48,6 +53,12 @@ export interface TenantConfig {
|
|
|
48
53
|
steps?: number;
|
|
49
54
|
ms?: number;
|
|
50
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* How long a run waits in its lease for a pending Action's outcome (F6.2): the turn goes on
|
|
58
|
+
* at once when the Action endpoint answers in time, and ends its segment as waiting (resumed
|
|
59
|
+
* by replay) otherwise. Default 5 minutes; 0 never waits.
|
|
60
|
+
*/
|
|
61
|
+
actionHoldMs?: number;
|
|
51
62
|
/** Retries and timeouts for model calls (Model Calls §5, §6). Defaults in `piModel`. */
|
|
52
63
|
modelCall?: import("../gates/model-gate.js").ModelCallSettings;
|
|
53
64
|
childEnv: Readonly<Record<string, string>>;
|
|
@@ -84,6 +95,8 @@ export interface TenantSummary {
|
|
|
84
95
|
inFlightDeliveries: number;
|
|
85
96
|
pendingActions: number;
|
|
86
97
|
uncertainEffects: number;
|
|
98
|
+
/** The Tenant's harnesses (F6.2). */
|
|
99
|
+
harness?: HarnessStatus;
|
|
87
100
|
}
|
|
88
101
|
/**
|
|
89
102
|
* The Node request and response behind a `Request`, for what still writes to Node directly
|
|
@@ -107,6 +120,10 @@ export interface TenantHandle {
|
|
|
107
120
|
/** Stop scheduling; wait for or cancel active turns. */
|
|
108
121
|
drain(activeWork: "drain" | "cancel", timeoutMs?: number): Promise<void>;
|
|
109
122
|
close(): Promise<void>;
|
|
123
|
+
/** Serves a harness's Harness API channel until it closes. Returns a function that detaches it. */
|
|
124
|
+
attachHarness?(channel: HarnessChannel, peer: HarnessPeer): () => void;
|
|
125
|
+
/** The Tenant's side of a sandbox pod's join (F7.2); absent without sandbox pods. */
|
|
126
|
+
hostAuthority?(): HostAuthority | undefined;
|
|
110
127
|
}
|
|
111
128
|
/**
|
|
112
129
|
* The Host's Tenant for a request: open, or unavailable. `cause` says why when opening it
|
package/dist/tenant/worker.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AbortReason } from "@nylorun/core/harness-api";
|
|
2
|
+
import { RunAbort } from "@nylorun/harness/api";
|
|
3
|
+
import type { AdvanceResult, DeliverResult, SandboxResult, SandboxTrigger, DurableExecution, StuckInvocation, WorkerHandlers } from "../execution/types.js";
|
|
2
4
|
import type { Logger } from "./types.js";
|
|
3
5
|
/** This process's Worker id: the `owner` it writes on the sessions it advances (§10.6). */
|
|
4
6
|
export declare const WORKER_ID: string;
|
|
@@ -11,10 +13,12 @@ export declare const DEFAULT_ADVANCE_DEADLINE_MS: number;
|
|
|
11
13
|
/** Default time an aborted advance has to settle before it is abandoned. */
|
|
12
14
|
export declare const DEFAULT_ADVANCE_GRACE_MS = 30000;
|
|
13
15
|
/** Why an advance's signal aborted (see "Abort reasons" above). */
|
|
14
|
-
export type AdvanceAbortKind =
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export type AdvanceAbortKind = AbortReason;
|
|
17
|
+
/**
|
|
18
|
+
* The abort reason of an advance. A harness run's reason (`RunAbort`) has the same kinds, so the
|
|
19
|
+
* executors read either one.
|
|
20
|
+
*/
|
|
21
|
+
export declare class AdvanceAbort extends RunAbort {
|
|
18
22
|
readonly name: string;
|
|
19
23
|
constructor(kind: AdvanceAbortKind, message: string, options?: {
|
|
20
24
|
cause?: unknown;
|
|
@@ -40,6 +44,8 @@ export interface TenantWorker {
|
|
|
40
44
|
sweep(): Promise<void>;
|
|
41
45
|
/** Delivers one Action to its endpoint. Only infrastructure errors throw. */
|
|
42
46
|
deliver?(actionId: string, signal: AbortSignal): Promise<DeliverResult>;
|
|
47
|
+
/** Reconciles one pod sandbox (F7.2). Only infrastructure errors throw. */
|
|
48
|
+
sandbox?(sandboxId: string, trigger: SandboxTrigger, signal: AbortSignal): Promise<SandboxResult>;
|
|
43
49
|
}
|
|
44
50
|
export interface TenantWorkersOptions {
|
|
45
51
|
/** Finds or opens a Tenant that is not registered on this process. */
|
package/dist/tenant/worker.js
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
*/
|
|
52
52
|
import { hostname } from "node:os";
|
|
53
53
|
import { randomBytes } from "node:crypto";
|
|
54
|
+
import { RunAbort } from "@nylorun/harness/api";
|
|
54
55
|
/** This process's Worker id: the `owner` it writes on the sessions it advances (§10.6). */
|
|
55
56
|
export const WORKER_ID = `worker-${hostname()}-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
56
57
|
/**
|
|
@@ -61,13 +62,14 @@ export const WORKER_ID = `worker-${hostname()}-${process.pid}-${randomBytes(4).t
|
|
|
61
62
|
export const DEFAULT_ADVANCE_DEADLINE_MS = 50 * 60_000;
|
|
62
63
|
/** Default time an aborted advance has to settle before it is abandoned. */
|
|
63
64
|
export const DEFAULT_ADVANCE_GRACE_MS = 30_000;
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The abort reason of an advance. A harness run's reason (`RunAbort`) has the same kinds, so the
|
|
67
|
+
* executors read either one.
|
|
68
|
+
*/
|
|
69
|
+
export class AdvanceAbort extends RunAbort {
|
|
67
70
|
name = "AdvanceAbort";
|
|
68
71
|
constructor(kind, message, options) {
|
|
69
|
-
super(message, options);
|
|
70
|
-
this.kind = kind;
|
|
72
|
+
super(kind, message, options);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
75
|
/** The abort reason of an advance that ran past its deadline. */
|
|
@@ -87,7 +89,7 @@ export class AdvanceDeadlineError extends AdvanceAbort {
|
|
|
87
89
|
export function abortKind(signal) {
|
|
88
90
|
if (!signal.aborted)
|
|
89
91
|
return undefined;
|
|
90
|
-
return signal.reason instanceof
|
|
92
|
+
return signal.reason instanceof RunAbort ? signal.reason.kind : "shutdown";
|
|
91
93
|
}
|
|
92
94
|
/** The execution's abort reason as a `shutdown`, keeping its message. */
|
|
93
95
|
function shutdownOf(reason) {
|
|
@@ -138,6 +140,10 @@ export class TenantWorkers {
|
|
|
138
140
|
const worker = await this.find(tenantId);
|
|
139
141
|
return worker?.deliver ? worker.deliver(actionId, signal) : DONE;
|
|
140
142
|
},
|
|
143
|
+
sandbox: async (tenantId, sandboxId, trigger, signal) => {
|
|
144
|
+
const worker = await this.find(tenantId);
|
|
145
|
+
return worker?.sandbox ? worker.sandbox(sandboxId, trigger, signal) : {};
|
|
146
|
+
},
|
|
141
147
|
};
|
|
142
148
|
/** Runs one advance under the deadline and the grace period (see the module comment). */
|
|
143
149
|
async bounded(tenantId, sessionId, worker, signal) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.16.0-beta";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -59,16 +59,17 @@
|
|
|
59
59
|
"db:studio": "drizzle-kit studio"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@ag-ui/core": "~1.0.
|
|
62
|
+
"@ag-ui/core": "~1.0.1",
|
|
63
63
|
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
64
64
|
"@earendil-works/pi-ai": "0.99.1",
|
|
65
|
-
"@hono/node-server": "^2.1.
|
|
65
|
+
"@hono/node-server": "^2.1.3",
|
|
66
66
|
"@hono/zod-openapi": "^1.6.3",
|
|
67
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
68
|
-
"@nylorun/core": "0.
|
|
69
|
-
"@nylorun/harness": "0.
|
|
70
|
-
"@restatedev/restate-sdk": "1.
|
|
71
|
-
"@s2-dev/streamstore": "0.
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
68
|
+
"@nylorun/core": "0.12.0-beta",
|
|
69
|
+
"@nylorun/harness": "0.22.0-beta",
|
|
70
|
+
"@restatedev/restate-sdk": "1.17.2",
|
|
71
|
+
"@s2-dev/streamstore": "0.28.0",
|
|
72
|
+
"aws4fetch": "1.0.20",
|
|
72
73
|
"drizzle-orm": "^0.45.3",
|
|
73
74
|
"hono": "^4.13.12",
|
|
74
75
|
"jose": "^6.2.12",
|
|
@@ -76,17 +77,19 @@
|
|
|
76
77
|
"pg": "8.23.1",
|
|
77
78
|
"pg-logical-replication": "2.5.0",
|
|
78
79
|
"postgres": "^3.4.9",
|
|
80
|
+
"ws": "^8.22.0",
|
|
79
81
|
"zod": "^4.6.5"
|
|
80
82
|
},
|
|
81
83
|
"devDependencies": {
|
|
82
84
|
"@a2a-js/sdk": "~1.3.0",
|
|
83
|
-
"@ag-ui/client": "1.0.
|
|
85
|
+
"@ag-ui/client": "1.0.1",
|
|
84
86
|
"@scalar/openapi-parser": "^0.29.9",
|
|
85
87
|
"@types/node": "^26.5.0",
|
|
86
88
|
"@types/pg": "^8.23.1",
|
|
89
|
+
"@types/ws": "^8.18.2",
|
|
87
90
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
88
91
|
"drizzle-kit": "^0.31.11",
|
|
89
92
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
90
|
-
"vitest": "^5.0.
|
|
93
|
+
"vitest": "^5.0.2"
|
|
91
94
|
}
|
|
92
95
|
}
|
package/dist/adapters/media.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type MediaAsset, type MediaReference } from "../media.js";
|
|
2
|
-
export * from "../media.js";
|
|
3
|
-
/** Explicit local asset store; agents receive only opaque references. */
|
|
4
|
-
export declare class MediaStore {
|
|
5
|
-
private readonly root;
|
|
6
|
-
constructor(root: string);
|
|
7
|
-
saveInput(agentId: string, sessionId: string, mediaType: string, base64: string): Promise<MediaAsset>;
|
|
8
|
-
saveGenerated(agentId: string, sessionId: string, mediaType: string, bytes: Uint8Array): Promise<MediaAsset>;
|
|
9
|
-
dataUrl(reference: MediaReference, sessionId: string): Promise<{
|
|
10
|
-
readonly asset: MediaAsset;
|
|
11
|
-
readonly url: string;
|
|
12
|
-
} | undefined>;
|
|
13
|
-
latestInput(agentId: string, sessionId: string): Promise<MediaAsset | undefined>;
|
|
14
|
-
read(agentId: string, sessionId: string, assetId: string): Promise<{
|
|
15
|
-
asset: MediaAsset;
|
|
16
|
-
bytes: Uint8Array;
|
|
17
|
-
} | undefined>;
|
|
18
|
-
private save;
|
|
19
|
-
private readMetadata;
|
|
20
|
-
private directory;
|
|
21
|
-
}
|
|
22
|
-
export declare function localMedia(options: {
|
|
23
|
-
root: string;
|
|
24
|
-
}): MediaStore;
|
package/dist/adapters/media.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes, } from "../media.js";
|
|
5
|
-
export * from "../media.js";
|
|
6
|
-
/** Explicit local asset store; agents receive only opaque references. */
|
|
7
|
-
export class MediaStore {
|
|
8
|
-
root;
|
|
9
|
-
constructor(root) {
|
|
10
|
-
this.root = root;
|
|
11
|
-
}
|
|
12
|
-
async saveInput(agentId, sessionId, mediaType, base64) {
|
|
13
|
-
const bytes = decodeImageBase64(mediaType, base64);
|
|
14
|
-
return this.save(agentId, sessionId, "input", mediaType, bytes);
|
|
15
|
-
}
|
|
16
|
-
async saveGenerated(agentId, sessionId, mediaType, bytes) {
|
|
17
|
-
validateImageBytes(mediaType, bytes);
|
|
18
|
-
return this.save(agentId, sessionId, "generated", mediaType, bytes);
|
|
19
|
-
}
|
|
20
|
-
async dataUrl(reference, sessionId) {
|
|
21
|
-
const stored = await this.read(reference.agentId, sessionId, reference.assetId);
|
|
22
|
-
if (!stored)
|
|
23
|
-
return undefined;
|
|
24
|
-
return Object.freeze({
|
|
25
|
-
asset: stored.asset,
|
|
26
|
-
url: `data:${stored.asset.mediaType};base64,${Buffer.from(stored.bytes).toString("base64")}`,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
async latestInput(agentId, sessionId) {
|
|
30
|
-
const directory = this.directory(agentId, sessionId);
|
|
31
|
-
try {
|
|
32
|
-
const metadata = await Promise.all((await readdir(directory))
|
|
33
|
-
.filter((name) => name.endsWith(".json"))
|
|
34
|
-
.map(async (name) => this.readMetadata(directory, name)));
|
|
35
|
-
return metadata
|
|
36
|
-
.filter((entry) => entry?.kind === "input")
|
|
37
|
-
.sort((left, right) => right.createdAt - left.createdAt)[0];
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async read(agentId, sessionId, assetId) {
|
|
44
|
-
if (!safe(agentId) || !safe(sessionId) || !safe(assetId))
|
|
45
|
-
return undefined;
|
|
46
|
-
const directory = this.directory(agentId, sessionId);
|
|
47
|
-
const metadata = await this.readMetadata(directory, `${assetId}.json`);
|
|
48
|
-
if (!metadata)
|
|
49
|
-
return undefined;
|
|
50
|
-
try {
|
|
51
|
-
return {
|
|
52
|
-
asset: metadata,
|
|
53
|
-
bytes: await readFile(join(directory, `${assetId}.bin`)),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return undefined;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async save(agentId, sessionId, kind, mediaType, bytes) {
|
|
61
|
-
if (!safe(agentId) || !safe(sessionId))
|
|
62
|
-
throw new Error("Invalid media asset scope.");
|
|
63
|
-
const directory = this.directory(agentId, sessionId);
|
|
64
|
-
const asset = {
|
|
65
|
-
id: randomUUID(),
|
|
66
|
-
mediaType,
|
|
67
|
-
bytes: bytes.byteLength,
|
|
68
|
-
kind,
|
|
69
|
-
createdAt: Date.now(),
|
|
70
|
-
};
|
|
71
|
-
await mkdir(directory, { recursive: true });
|
|
72
|
-
await writeFile(join(directory, `${asset.id}.bin`), bytes);
|
|
73
|
-
await writeFile(join(directory, `${asset.id}.json`), JSON.stringify(asset));
|
|
74
|
-
return omitCreatedAt(asset);
|
|
75
|
-
}
|
|
76
|
-
async readMetadata(directory, name) {
|
|
77
|
-
try {
|
|
78
|
-
const value = JSON.parse(await readFile(join(directory, name), "utf8"));
|
|
79
|
-
return validAsset(value) ? value : undefined;
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
directory(agentId, sessionId) {
|
|
86
|
-
return join(this.root, agentId, sessionId);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/** Decode one canonical base64 image before it is persisted or sent to a provider. */
|
|
90
|
-
function validAsset(value) {
|
|
91
|
-
if (!value || typeof value !== "object")
|
|
92
|
-
return false;
|
|
93
|
-
const asset = value;
|
|
94
|
-
return (typeof asset.id === "string" &&
|
|
95
|
-
safe(asset.id) &&
|
|
96
|
-
typeof asset.mediaType === "string" &&
|
|
97
|
-
IMAGE_MEDIA_TYPES.includes(asset.mediaType) &&
|
|
98
|
-
typeof asset.bytes === "number" &&
|
|
99
|
-
(asset.kind === "input" || asset.kind === "generated") &&
|
|
100
|
-
typeof asset.createdAt === "number");
|
|
101
|
-
}
|
|
102
|
-
function omitCreatedAt(asset) {
|
|
103
|
-
const { createdAt: _createdAt, ...result } = asset;
|
|
104
|
-
return Object.freeze(result);
|
|
105
|
-
}
|
|
106
|
-
function safe(value) {
|
|
107
|
-
return value !== "." && value !== ".." && /^[a-zA-Z0-9._-]+$/u.test(value);
|
|
108
|
-
}
|
|
109
|
-
export function localMedia(options) {
|
|
110
|
-
return new MediaStore(options.root);
|
|
111
|
-
}
|