@nylorun/runtime 0.23.0-beta → 0.25.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 +252 -0
- package/README.md +87 -28
- package/dist/adapters/execution/restate.js +1 -1
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +9 -3
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +6 -1
- package/dist/api/a2a/routes.js +67 -9
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +8 -3
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +39 -3
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +63 -20
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +5327 -1091
- package/dist/api/components.js +40 -16
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +14 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +52 -3
- package/dist/api/http/app.js +17 -6
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- package/dist/api/http/routes/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +332 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +94 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +24 -9
- package/dist/api/http/routes/tenant.js +65 -15
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +48 -28
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +569 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- package/dist/artifacts/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +5 -5
- package/dist/artifacts/workspace.js +7 -19
- package/dist/core/flow-host.d.ts +3 -4
- package/dist/core/flow-host.js +5 -2
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +11 -44
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/execution/types.d.ts +1 -1
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +22 -1
- package/dist/gates/egress.js +116 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +9 -6
- package/dist/gates/http-tool.js +11 -17
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +51 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +7 -4
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +132 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +11 -69
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +14 -7
- package/dist/harness/calls.js +30 -24
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +24 -5
- package/dist/harness/pod.d.ts +39 -0
- package/dist/harness/pod.js +46 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +17 -9
- package/dist/harness/setup.d.ts +29 -0
- package/dist/harness/setup.js +174 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +180 -50
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +35 -4
- package/dist/harness-api/server.js +130 -17
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +105 -0
- package/dist/harness-api/tool-journal.js +132 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +12 -7
- package/dist/harness-api/workspace.js +47 -25
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +54 -5
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +64 -0
- package/dist/host/stack-config.js +62 -2
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +13 -6
- package/dist/keys/keys.js +9 -8
- package/dist/management-openapi.json +1264 -2171
- package/dist/mcp/connect.d.ts +26 -22
- package/dist/mcp/connect.js +30 -34
- package/dist/mcp/pool.d.ts +3 -6
- package/dist/mcp/pool.js +14 -28
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -21
- package/dist/mcp/snapshot.d.ts +16 -24
- package/dist/mcp/snapshot.js +33 -45
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +114 -94
- package/dist/openapi.json +12822 -3943
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -15
- package/dist/sandbox/manager.d.ts +47 -21
- package/dist/sandbox/manager.js +162 -107
- package/dist/sandbox/placement.d.ts +14 -13
- package/dist/sandbox/placement.js +31 -14
- package/dist/sandbox/pods/client.d.ts +9 -2
- package/dist/sandbox/pods/lifecycle.d.ts +36 -27
- package/dist/sandbox/pods/lifecycle.js +29 -26
- package/dist/sandbox/pods/reconcile.js +18 -12
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +17 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +37 -27
- package/dist/sandbox/resolve.js +73 -41
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +64 -0
- package/dist/sandbox/setup.js +160 -0
- package/dist/sandbox/share.d.ts +21 -26
- package/dist/sandbox/share.js +36 -63
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +56 -0
- package/dist/store/postgres/reads.js +123 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1126 -736
- package/dist/store/postgres/schema.js +136 -73
- package/dist/store/postgres/store.js +195 -67
- package/dist/store/types.d.ts +165 -50
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +118 -42
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +46 -26
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +115 -14
- package/dist/tenant/context.d.ts +56 -24
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +44 -29
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +174 -0
- package/dist/tenant/environments.js +1044 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +7 -1
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +18 -14
- package/dist/tenant/sessions.js +99 -143
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/sweep.js +13 -7
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +245 -36
- package/dist/tenant/transcript.js +64 -12
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +41 -0
- package/dist/traces/ids.js +81 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +289 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +62 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +92 -48
- package/dist/vault/service.js +462 -376
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +13 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
- package/dist/tenant/sandboxes.d.ts +0 -123
- package/dist/tenant/sandboxes.js +0 -566
|
@@ -16,8 +16,9 @@ export type TurnManifestErr = {
|
|
|
16
16
|
readonly message: string;
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* Parse `message.manifest`, require it to be a variant of the session pin,
|
|
20
|
-
*
|
|
19
|
+
* Parse `message.manifest`, require it to be a variant of the session pin, and store the
|
|
20
|
+
* manifest the turn runs with: the variant with the pin's `runtime` block (a variant never sets
|
|
21
|
+
* one), by its hash. Applies to that turn only (caller must not latch).
|
|
21
22
|
*/
|
|
22
23
|
export declare function pinTurnManifest(args: {
|
|
23
24
|
readonly candidate: unknown;
|
|
@@ -2,8 +2,29 @@ import { AgentManifestSchema, } from "@nylorun/core/contracts";
|
|
|
2
2
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
3
3
|
import { isVariantOf } from "@nylorun/core/define";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The candidate's agents used as tools with the `runtime` block the pin gave each, when the
|
|
6
|
+
* candidate left it out: a turn variant is written from the definition, and only the Runtime
|
|
7
|
+
* writes `runtime`. A candidate that sets one keeps it, and must then match the pin's.
|
|
8
|
+
*/
|
|
9
|
+
function withPinnedDelegates(candidate, pinned) {
|
|
10
|
+
if (candidate.tools === undefined)
|
|
11
|
+
return candidate;
|
|
12
|
+
const pinnedAgents = new Map((pinned.tools ?? []).flatMap((tool) => tool.agent !== undefined && !("kind" in tool.agent) ? [[tool.name, tool.agent]] : []));
|
|
13
|
+
let changed = false;
|
|
14
|
+
const tools = candidate.tools.map((tool) => {
|
|
15
|
+
const agent = tool.agent;
|
|
16
|
+
const prior = pinnedAgents.get(tool.name);
|
|
17
|
+
if (agent === undefined || "kind" in agent || agent.runtime !== undefined || prior?.runtime === undefined)
|
|
18
|
+
return tool;
|
|
19
|
+
changed = true;
|
|
20
|
+
return { ...tool, agent: { ...agent, runtime: prior.runtime } };
|
|
21
|
+
});
|
|
22
|
+
return changed ? { ...candidate, tools } : candidate;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse `message.manifest`, require it to be a variant of the session pin, and store the
|
|
26
|
+
* manifest the turn runs with: the variant with the pin's `runtime` block (a variant never sets
|
|
27
|
+
* one), by its hash. Applies to that turn only (caller must not latch).
|
|
7
28
|
*/
|
|
8
29
|
export function pinTurnManifest(args) {
|
|
9
30
|
const parsed = AgentManifestSchema.safeParse(args.candidate);
|
|
@@ -14,14 +35,17 @@ export function pinTurnManifest(args) {
|
|
|
14
35
|
message: "Turn manifest failed AgentManifestSchema validation",
|
|
15
36
|
};
|
|
16
37
|
}
|
|
17
|
-
const
|
|
18
|
-
if (!isVariantOf(
|
|
38
|
+
const candidate = withPinnedDelegates(parsed.data, args.pinned);
|
|
39
|
+
if (!isVariantOf(candidate, args.pinned)) {
|
|
19
40
|
return {
|
|
20
41
|
ok: false,
|
|
21
42
|
code: "loop.invalid-agent",
|
|
22
|
-
message:
|
|
43
|
+
message: candidate.runtime === undefined
|
|
44
|
+
? "Turn manifest is not a variant of the session's pinned manifest"
|
|
45
|
+
: "Turn manifest sets runtime, which only the Runtime writes: leave it out, and the turn runs with the session's",
|
|
23
46
|
};
|
|
24
47
|
}
|
|
48
|
+
const manifest = args.pinned.runtime === undefined ? candidate : { ...candidate, runtime: args.pinned.runtime };
|
|
25
49
|
const hash = hashManifest(manifest);
|
|
26
50
|
args.store.put(hash, manifest);
|
|
27
51
|
return { ok: true, manifest, hash };
|
|
@@ -72,7 +72,7 @@ export type AdvanceResult = {
|
|
|
72
72
|
retryAfterMs: number;
|
|
73
73
|
};
|
|
74
74
|
/** A pod sandbox's timers. */
|
|
75
|
-
export type SandboxTimer = "idle"
|
|
75
|
+
export type SandboxTimer = "idle";
|
|
76
76
|
/**
|
|
77
77
|
* What `DurableExecution.sandbox` asks for: a reconcile, or a timer. A reconcile's `key` merges
|
|
78
78
|
* repeated sends of one request (the outbox's `signal:<row id>`); without one, every send runs.
|
package/dist/gates/contract.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The gates service's wire format (blueprint §15, P1.1).
|
|
3
|
-
* it
|
|
2
|
+
* The gates service's wire format (blueprint §15, P1.1). Not in the published OpenAPI documents:
|
|
3
|
+
* harnesses call it, and its bodies are published in `@nylorun/core/harness-api` (R4.5).
|
|
4
4
|
*
|
|
5
5
|
* `POST /nylorun/v1/model-calls`
|
|
6
6
|
* - Headers: `Authorization: Bearer <run token>` (F5, `tenant/run-token.ts`), `Idempotency-Key:
|
|
@@ -22,31 +22,10 @@
|
|
|
22
22
|
* whether or not it exists, `403 gate_forbidden` when it is another session's call. The token
|
|
23
23
|
* need not be live: a user cancel makes it stale, and still stops its call.
|
|
24
24
|
*/
|
|
25
|
-
import { z } from "zod";
|
|
26
25
|
import type { ModelGateOutcome } from "./model-gate.js";
|
|
27
|
-
export
|
|
26
|
+
export { MAX_MODEL_CALL_BYTES, MODEL_CALLS_PATH, ModelCallBodySchema, type GateErrorBody, type ModelCallBody, } from "@nylorun/core/harness-api";
|
|
28
27
|
/** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
|
|
29
28
|
export declare const TENANT_HEADER = "nylorun-tenant";
|
|
30
|
-
/** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
|
|
31
|
-
export declare const MAX_MODEL_CALL_BYTES: number;
|
|
32
|
-
export declare const ModelCallBodySchema: z.ZodObject<{
|
|
33
|
-
effectId: z.ZodString;
|
|
34
|
-
invocationId: z.ZodString;
|
|
35
|
-
call: z.ZodObject<{
|
|
36
|
-
prompt: z.ZodArray<z.ZodUnknown>;
|
|
37
|
-
tools: z.ZodArray<z.ZodObject<{
|
|
38
|
-
name: z.ZodString;
|
|
39
|
-
}, z.core.$loose>>;
|
|
40
|
-
}, z.core.$loose>;
|
|
41
|
-
}, z.core.$strict>;
|
|
42
|
-
export type ModelCallBody = z.infer<typeof ModelCallBodySchema>;
|
|
43
29
|
export interface ModelCallResponse {
|
|
44
30
|
readonly outcome: ModelGateOutcome;
|
|
45
31
|
}
|
|
46
|
-
/** An error answer: `{error: {code, message}}`. */
|
|
47
|
-
export interface GateErrorBody {
|
|
48
|
-
readonly error: {
|
|
49
|
-
readonly code: "gate_unauthorized" | "gate_forbidden" | "run_stale" | "invalid_request" | "gate_conflict";
|
|
50
|
-
readonly message: string;
|
|
51
|
-
};
|
|
52
|
-
}
|
package/dist/gates/contract.js
CHANGED
|
@@ -1,38 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
* The gates service's wire format (blueprint §15, P1.1). Internal: only the loop calls it, so
|
|
3
|
-
* it is not in the published OpenAPI documents.
|
|
4
|
-
*
|
|
5
|
-
* `POST /nylorun/v1/model-calls`
|
|
6
|
-
* - Headers: `Authorization: Bearer <run token>` (F5, `tenant/run-token.ts`), `Idempotency-Key:
|
|
7
|
-
* <effect id>`, and optionally `Nylorun-Tenant: <tenant id>`, which must then be the token's
|
|
8
|
-
* Tenant. Core's credential (`NYLORUN_GATES_TOKEN`) is refused: only a run calls the model.
|
|
9
|
-
* - Body: `ModelCallBody`, at most `MAX_MODEL_CALL_BYTES`. It names no session, turn or agent:
|
|
10
|
-
* the gate takes them from the token, and refuses a body that names them.
|
|
11
|
-
* - `200 {outcome}` once the call has finished, whether the outcome is a candidate or a
|
|
12
|
-
* failure; nothing is sent while it runs. `401 gate_unauthorized` for a missing, malformed,
|
|
13
|
-
* expired or foreign token, `409 run_stale` when the token's lease is no longer the
|
|
14
|
-
* session's (a cancel, a new turn, a takeover), `400 invalid_request` for a malformed Tenant
|
|
15
|
-
* header or a bad body, `409 gate_conflict` when a different request already runs under the
|
|
16
|
-
* same key.
|
|
17
|
-
* - With an `Idempotency-Key` the call outlives its client (P1.2): a re-send with the same key
|
|
18
|
-
* and body joins it, or gets its outcome for 30 minutes. Without one, a client that goes
|
|
19
|
-
* away aborts the call.
|
|
20
|
-
*
|
|
21
|
-
* `POST /nylorun/v1/model-calls/{key}/cancel` (same headers): aborts the keyed call; `204`
|
|
22
|
-
* whether or not it exists, `403 gate_forbidden` when it is another session's call. The token
|
|
23
|
-
* need not be live: a user cancel makes it stale, and still stops its call.
|
|
24
|
-
*/
|
|
25
|
-
import { z } from "zod";
|
|
26
|
-
export const MODEL_CALLS_PATH = "/nylorun/v1/model-calls";
|
|
1
|
+
export { MAX_MODEL_CALL_BYTES, MODEL_CALLS_PATH, ModelCallBodySchema, } from "@nylorun/core/harness-api";
|
|
27
2
|
/** Header naming the Tenant a call is for: optional, and checked against the gate's Tenant. */
|
|
28
3
|
export const TENANT_HEADER = "nylorun-tenant";
|
|
29
|
-
/** Largest request body the gate reads: one `ModelCall` with its whole projected history. */
|
|
30
|
-
export const MAX_MODEL_CALL_BYTES = 32 * 1024 * 1024;
|
|
31
|
-
export const ModelCallBodySchema = z.strictObject({
|
|
32
|
-
effectId: z.string().min(1),
|
|
33
|
-
invocationId: z.string().min(1),
|
|
34
|
-
call: z.looseObject({
|
|
35
|
-
prompt: z.array(z.unknown()),
|
|
36
|
-
tools: z.array(z.looseObject({ name: z.string() })),
|
|
37
|
-
}),
|
|
38
|
-
});
|
|
@@ -27,6 +27,13 @@ export interface EgressCredentials {
|
|
|
27
27
|
/** Tests: the port requests are sent to, instead of 443. */
|
|
28
28
|
readonly upstreamPort?: number;
|
|
29
29
|
}
|
|
30
|
+
/** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
|
|
31
|
+
export declare const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
32
|
+
/** What one terminated connection did so far (R7.3): requests read, credentials set (ids). */
|
|
33
|
+
export interface EgressTally {
|
|
34
|
+
requests: number;
|
|
35
|
+
readonly credentialIds: Set<string>;
|
|
36
|
+
}
|
|
30
37
|
export interface CredentialTerminator {
|
|
31
38
|
/**
|
|
32
39
|
* Answers the CONNECT, terminates TLS for `host` and serves its requests, sending each to one
|
|
@@ -39,6 +46,8 @@ export interface CredentialTerminator {
|
|
|
39
46
|
address: string;
|
|
40
47
|
family: number;
|
|
41
48
|
}[];
|
|
49
|
+
/** Counted as requests are served, read when the connection closes. */
|
|
50
|
+
readonly tally: EgressTally;
|
|
42
51
|
}): Promise<void>;
|
|
43
52
|
}
|
|
44
53
|
export declare function credentialTerminator(options: {
|
|
@@ -12,10 +12,14 @@
|
|
|
12
12
|
* Nothing here knows a tool: the header name and its format are the credential's. A release the
|
|
13
13
|
* vault refuses (two secrets for one host, an unreadable value) answers 502 and is logged, never
|
|
14
14
|
* as an event; a value never is. Upgrades (WebSocket) and HTTP/2 are not served on this path.
|
|
15
|
+
* Each connection keeps a tally for its `environment.egress` event (R7.3, D67): how many requests it
|
|
16
|
+
* read and which credentials it set, by id.
|
|
15
17
|
*/
|
|
16
18
|
import { createServer } from "node:http";
|
|
17
19
|
import { Agent, request } from "node:https";
|
|
18
20
|
import { createSecureContext, TLSSocket } from "node:tls";
|
|
21
|
+
/** The answer to a CONNECT the gate serves; a tunnel's byte counts leave it out. */
|
|
22
|
+
export const CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
19
23
|
/** Headers that describe one hop and are never forwarded. */
|
|
20
24
|
const HOP_BY_HOP = new Set([
|
|
21
25
|
"connection",
|
|
@@ -74,6 +78,7 @@ export function credentialTerminator(options) {
|
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
76
80
|
const { sandboxId, host } = at;
|
|
81
|
+
at.tally.requests += 1;
|
|
77
82
|
const release = await credentials.release(sandboxId, host);
|
|
78
83
|
if (release.status === "refused") {
|
|
79
84
|
logger.warn("egress_credential_refused", { status: 502, reason: release.reason, sandboxId, host });
|
|
@@ -82,8 +87,10 @@ export function credentialTerminator(options) {
|
|
|
82
87
|
}
|
|
83
88
|
const header = release.status === "released" ? release.header.toLowerCase() : undefined;
|
|
84
89
|
const headers = forwardable(req.rawHeaders, header);
|
|
85
|
-
if (release.status === "released")
|
|
90
|
+
if (release.status === "released") {
|
|
86
91
|
headers[header] = [release.value];
|
|
92
|
+
at.tally.credentialIds.add(release.credentialId);
|
|
93
|
+
}
|
|
87
94
|
const upstream = request({
|
|
88
95
|
host,
|
|
89
96
|
port: credentials.upstreamPort ?? 443,
|
|
@@ -111,7 +118,7 @@ export function credentialTerminator(options) {
|
|
|
111
118
|
}
|
|
112
119
|
return {
|
|
113
120
|
async terminate(client, target) {
|
|
114
|
-
const { sandboxId, host, addresses } = target;
|
|
121
|
+
const { sandboxId, host, addresses, tally } = target;
|
|
115
122
|
const leaf = await credentials.leaf(host);
|
|
116
123
|
if (client.destroyed)
|
|
117
124
|
return;
|
|
@@ -128,13 +135,13 @@ export function credentialTerminator(options) {
|
|
|
128
135
|
lookup,
|
|
129
136
|
...(credentials.upstreamCa ? { ca: credentials.upstreamCa } : {}),
|
|
130
137
|
});
|
|
131
|
-
client.write(
|
|
138
|
+
client.write(CONNECT_ESTABLISHED);
|
|
132
139
|
const tls = new TLSSocket(client, {
|
|
133
140
|
isServer: true,
|
|
134
141
|
secureContext: createSecureContext({ key: leaf.key, cert: leaf.cert }),
|
|
135
142
|
ALPNProtocols: ["http/1.1"],
|
|
136
143
|
});
|
|
137
|
-
terminated.set(tls, { sandboxId, host, agent });
|
|
144
|
+
terminated.set(tls, { sandboxId, host, agent, tally });
|
|
138
145
|
tls.on("error", () => tls.destroy());
|
|
139
146
|
tls.on("close", () => {
|
|
140
147
|
agent.destroy();
|
package/dist/gates/egress.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
2
|
import { type Socket } from "node:net";
|
|
3
|
+
import type { EgressRunningTurn, EnvironmentEventPayload } from "@nylorun/core/contracts";
|
|
3
4
|
import type { EgressTokenVerdict } from "../sandbox/egress-token.js";
|
|
4
5
|
import type { SessionStore } from "../store/types.js";
|
|
5
6
|
import type { Logger } from "../tenant/types.js";
|
|
@@ -15,6 +16,17 @@ export interface EgressSandboxes {
|
|
|
15
16
|
readonly allow: readonly string[];
|
|
16
17
|
} | undefined>;
|
|
17
18
|
}
|
|
19
|
+
/** One connection's record (R7.3, D67): `environment.egress`'s payload. */
|
|
20
|
+
export type EgressConnection = EnvironmentEventPayload<"environment.egress">;
|
|
21
|
+
/** How many running turns one connection names, at most. */
|
|
22
|
+
export declare const EGRESS_RUNNING_TURNS_MAX = 16;
|
|
23
|
+
/** Where egress-gate records connections (R7.3, D67). */
|
|
24
|
+
export interface EgressRecord {
|
|
25
|
+
/** The turns active on the Environment now, at most `EGRESS_RUNNING_TURNS_MAX`. */
|
|
26
|
+
runningTurns(environmentId: string): Promise<readonly EgressRunningTurn[]>;
|
|
27
|
+
/** Writes `connection` to its Environment's stream; a deleted Environment gets nothing. */
|
|
28
|
+
write(connection: EgressConnection): Promise<void>;
|
|
29
|
+
}
|
|
18
30
|
/** Ports a tunnel may open. */
|
|
19
31
|
export declare const EGRESS_PORTS: readonly number[];
|
|
20
32
|
/** Tunnels one sandbox may hold open at once. */
|
|
@@ -49,6 +61,8 @@ export interface StartEgressGateOptions {
|
|
|
49
61
|
readonly dial?: (address: string, port: number) => Socket;
|
|
50
62
|
/** The vault and keys, for hosts an `environment_secret` is bound to (R2c). None: tunnels only. */
|
|
51
63
|
readonly credentials?: EgressCredentials;
|
|
64
|
+
/** Where connections are recorded (R7.3). None: they are only logged. */
|
|
65
|
+
readonly record?: EgressRecord;
|
|
52
66
|
}
|
|
53
67
|
export interface EgressGate {
|
|
54
68
|
/** Where the gate listens, e.g. `http://0.0.0.0:4200`. */
|
|
@@ -70,6 +84,13 @@ export declare function startEgressGate(options: StartEgressGateOptions): Promis
|
|
|
70
84
|
/**
|
|
71
85
|
* `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
|
|
72
86
|
* host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
|
|
73
|
-
*
|
|
87
|
+
* Environment, or one deleted or lost, as `staleHost` judges a host token.
|
|
74
88
|
*/
|
|
75
89
|
export declare function storeEgressSandboxes(store: SessionStore): EgressSandboxes;
|
|
90
|
+
/**
|
|
91
|
+
* `EgressRecord` over the Tenant's record (R7.3, D67). The running turns are the sessions on the
|
|
92
|
+
* Environment whose body has an `activeTurnId`, in id order, at most `EGRESS_RUNNING_TURNS_MAX`.
|
|
93
|
+
* The write is `environment.egress` on the Environment's stream, under its row's lock; a deleted
|
|
94
|
+
* Environment gets nothing.
|
|
95
|
+
*/
|
|
96
|
+
export declare function storeEgressRecord(store: SessionStore): EgressRecord;
|
package/dist/gates/egress.js
CHANGED
|
@@ -21,8 +21,18 @@
|
|
|
21
21
|
* `environment_secret` of a live session on the sandbox is bound to. There the gate terminates
|
|
22
22
|
* TLS and sets the credential's header on every request (`egress-credentials.ts`), still sending
|
|
23
23
|
* only to the checked address. Every other host gets the tunnel. Any other request
|
|
24
|
-
* (an absolute-form plain HTTP request included) is answered 405. Refusals are logged
|
|
25
|
-
*
|
|
24
|
+
* (an absolute-form plain HTTP request included) is answered 405. Refusals are logged; the token
|
|
25
|
+
* never is. Tunnels close after `idleMs` without traffic in either direction.
|
|
26
|
+
*
|
|
27
|
+
* Every connection to a named host on a live token is recorded once (R7.3, D67), when it closes
|
|
28
|
+
* or at once when the gate refuses it, as `environment.egress` on the Environment's own stream:
|
|
29
|
+
* the connection is the Environment's, since its token names the pod and turns run in parallel
|
|
30
|
+
* (D66). `runningTurns` lists the turns active on the Environment as it opened (read as the name
|
|
31
|
+
* resolves), as context and never as an owner. Host, port, outcome, bytes, duration and, for a
|
|
32
|
+
* credential host, the request count and credential ids; never a header value, a path or a body. A refusal
|
|
33
|
+
* before that (no token, a stale one, no `host:port`) stays a log line, and a client that hangs up
|
|
34
|
+
* before the gate answers leaves nothing. A write never holds the tunnel up, and a failed one is
|
|
35
|
+
* logged (`egress_record_failed`).
|
|
26
36
|
*/
|
|
27
37
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
28
38
|
import { createServer } from "node:http";
|
|
@@ -30,7 +40,9 @@ import { BlockList, connect, isIP } from "node:net";
|
|
|
30
40
|
import { isSandboxHostPattern } from "@nylorun/core/define";
|
|
31
41
|
import { isPrivateAddress } from "../tenant/outbound.js";
|
|
32
42
|
import { bindListener } from "../host/http.js";
|
|
33
|
-
import { credentialTerminator } from "./egress-credentials.js";
|
|
43
|
+
import { CONNECT_ESTABLISHED, credentialTerminator, } from "./egress-credentials.js";
|
|
44
|
+
/** How many running turns one connection names, at most. */
|
|
45
|
+
export const EGRESS_RUNNING_TURNS_MAX = 16;
|
|
34
46
|
/** Ports a tunnel may open. */
|
|
35
47
|
export const EGRESS_PORTS = [443, 80];
|
|
36
48
|
/** Tunnels one sandbox may hold open at once. */
|
|
@@ -134,6 +146,51 @@ function answer(socket, status, message, headers = []) {
|
|
|
134
146
|
body,
|
|
135
147
|
].join("\r\n"));
|
|
136
148
|
}
|
|
149
|
+
function connectionRecord(sink, logger, client, at) {
|
|
150
|
+
let running;
|
|
151
|
+
let written = false;
|
|
152
|
+
const runningTurns = (to) => (running ??= to.runningTurns(at.environmentId));
|
|
153
|
+
const write = (fields) => {
|
|
154
|
+
if (written || !sink)
|
|
155
|
+
return;
|
|
156
|
+
written = true;
|
|
157
|
+
const socket = client;
|
|
158
|
+
const allowed = fields.outcome === "allowed";
|
|
159
|
+
const durationMs = Math.max(0, Date.now() - at.startedAt.getTime());
|
|
160
|
+
const connection = (turns) => ({
|
|
161
|
+
environmentId: at.environmentId,
|
|
162
|
+
host: at.host,
|
|
163
|
+
port: at.port,
|
|
164
|
+
...fields,
|
|
165
|
+
mode: record.credentialBound ? "credential" : "tunnel",
|
|
166
|
+
credentialBound: record.credentialBound,
|
|
167
|
+
// Up is what the sandbox sent after its CONNECT, down what it got after the gate's 200.
|
|
168
|
+
bytesUp: allowed ? Math.max(0, (socket.bytesRead ?? 0) - at.upFrom) : 0,
|
|
169
|
+
bytesDown: allowed ? Math.max(0, (socket.bytesWritten ?? 0) - CONNECT_ESTABLISHED.length) : 0,
|
|
170
|
+
durationMs,
|
|
171
|
+
startedAt: at.startedAt.toISOString(),
|
|
172
|
+
runningTurns: [...turns],
|
|
173
|
+
});
|
|
174
|
+
runningTurns(sink)
|
|
175
|
+
.then((turns) => sink.write(connection(turns)))
|
|
176
|
+
.catch((error) => logger.error("egress_record_failed", {
|
|
177
|
+
sandboxId: at.environmentId,
|
|
178
|
+
host: at.host,
|
|
179
|
+
error: error instanceof Error ? error.message : String(error),
|
|
180
|
+
}));
|
|
181
|
+
};
|
|
182
|
+
const record = {
|
|
183
|
+
credentialBound: false,
|
|
184
|
+
// A failed read is logged when the write needs it.
|
|
185
|
+
opened: () => void (sink && runningTurns(sink).catch(() => undefined)),
|
|
186
|
+
refused: (status, reason) => write({ outcome: "refused", reason, status }),
|
|
187
|
+
closed: (tally) => write({
|
|
188
|
+
outcome: "allowed",
|
|
189
|
+
...(tally ? { requests: tally.requests, credentialIds: [...tally.credentialIds] } : {}),
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
return record;
|
|
193
|
+
}
|
|
137
194
|
export async function startEgressGate(options) {
|
|
138
195
|
const { logger, sandboxes } = options;
|
|
139
196
|
const idleMs = options.idleMs ?? EGRESS_IDLE_MS;
|
|
@@ -160,11 +217,17 @@ export async function startEgressGate(options) {
|
|
|
160
217
|
: undefined;
|
|
161
218
|
const open = new Map();
|
|
162
219
|
const tunnels = new Set();
|
|
220
|
+
/** Each connection's record, once its Environment and target are known. */
|
|
221
|
+
const records = new WeakMap();
|
|
163
222
|
async function onConnect(req, client, head) {
|
|
223
|
+
const startedAt = new Date();
|
|
224
|
+
// What the sandbox sent before the tunnel: its CONNECT, less the bytes after it (`head`).
|
|
225
|
+
const upFrom = (client.bytesRead ?? 0) - head.length;
|
|
164
226
|
const target = parseTarget(req.url);
|
|
165
227
|
const refuse = (status, reason, fields = {}, message) => {
|
|
166
228
|
logger.warn("egress_refused", { status, reason, ...(target ? { host: target.host, port: target.port } : {}), ...fields });
|
|
167
229
|
answer(client, status, message ?? reason, status === 407 ? ['proxy-authenticate: Basic realm="nylorun-egress"'] : []);
|
|
230
|
+
records.get(client)?.refused(status, reason);
|
|
168
231
|
};
|
|
169
232
|
const raw = proxyToken(req.headers["proxy-authorization"]);
|
|
170
233
|
if (!raw)
|
|
@@ -183,12 +246,22 @@ export async function startEgressGate(options) {
|
|
|
183
246
|
if (!target)
|
|
184
247
|
return refuse(403, "target_invalid", { sandboxId }, "CONNECT needs host:port");
|
|
185
248
|
const { host, port } = target;
|
|
249
|
+
const record = connectionRecord(options.record, logger, client, {
|
|
250
|
+
environmentId: sandboxId,
|
|
251
|
+
host,
|
|
252
|
+
port,
|
|
253
|
+
startedAt,
|
|
254
|
+
upFrom,
|
|
255
|
+
});
|
|
256
|
+
records.set(client, record);
|
|
186
257
|
if (!EGRESS_PORTS.includes(port))
|
|
187
258
|
return refuse(403, "port_refused", { sandboxId }, `port ${port} is not allowed; tunnels open to 443 or 80 only`);
|
|
188
259
|
if (isIP(host.replace(/^\[|\]$/g, "")) !== 0 || !isSandboxHostPattern(host) || host.startsWith("*."))
|
|
189
260
|
return refuse(403, "not_a_host_name", { sandboxId }, `${host} is not a host name; IP literals are refused`);
|
|
190
261
|
if (!egressAllows(host, sandbox.allow))
|
|
191
262
|
return refuse(403, "host_not_allowed", { sandboxId }, `${host} is not in this sandbox's network.allow`);
|
|
263
|
+
// The turns running on the Environment as the connection opens, read while the name resolves.
|
|
264
|
+
record.opened();
|
|
192
265
|
let mine = open.get(sandboxId);
|
|
193
266
|
if (!mine)
|
|
194
267
|
open.set(sandboxId, (mine = new Set()));
|
|
@@ -217,10 +290,13 @@ export async function startEgressGate(options) {
|
|
|
217
290
|
if (client.destroyed)
|
|
218
291
|
return;
|
|
219
292
|
if (terminator && port === 443 && (await options.credentials.bound(sandboxId, host))) {
|
|
293
|
+
record.credentialBound = true;
|
|
220
294
|
// A client that spoke before the 200 is not one this path serves.
|
|
221
295
|
if (head.length > 0)
|
|
222
296
|
return refuse(403, "early_data", { sandboxId }, "wait for the CONNECT answer before sending");
|
|
223
|
-
|
|
297
|
+
const tally = { requests: 0, credentialIds: new Set() };
|
|
298
|
+
client.once("close", () => record.closed(tally));
|
|
299
|
+
return terminator.terminate(client, { sandboxId, host, addresses: allowed, tally });
|
|
224
300
|
}
|
|
225
301
|
const upstream = await dialFirst(allowed.map((entry) => entry.address), port);
|
|
226
302
|
if (!upstream)
|
|
@@ -237,8 +313,11 @@ export async function startEgressGate(options) {
|
|
|
237
313
|
upstream.on("timeout", close);
|
|
238
314
|
upstream.on("error", close);
|
|
239
315
|
upstream.on("close", () => client.destroy());
|
|
240
|
-
client.on("close", () =>
|
|
241
|
-
|
|
316
|
+
client.on("close", () => {
|
|
317
|
+
upstream.destroy();
|
|
318
|
+
record.closed();
|
|
319
|
+
});
|
|
320
|
+
client.write(CONNECT_ESTABLISHED);
|
|
242
321
|
if (head.length > 0)
|
|
243
322
|
upstream.write(head);
|
|
244
323
|
upstream.pipe(client);
|
|
@@ -284,6 +363,7 @@ export async function startEgressGate(options) {
|
|
|
284
363
|
onConnect(req, client, head).catch((error) => {
|
|
285
364
|
logger.error("egress_failed", { error: error instanceof Error ? error.message : String(error) });
|
|
286
365
|
answer(client, 502, "the tunnel failed");
|
|
366
|
+
records.get(client)?.refused(502, "egress_failed");
|
|
287
367
|
});
|
|
288
368
|
});
|
|
289
369
|
server.on("clientError", (_error, socket) => {
|
|
@@ -311,22 +391,48 @@ export async function startEgressGate(options) {
|
|
|
311
391
|
/**
|
|
312
392
|
* `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
|
|
313
393
|
* host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
|
|
314
|
-
*
|
|
394
|
+
* Environment, or one deleted or lost, as `staleHost` judges a host token.
|
|
315
395
|
*/
|
|
316
396
|
export function storeEgressSandboxes(store) {
|
|
317
397
|
return {
|
|
318
398
|
async live(sandboxId) {
|
|
319
|
-
const sandbox = await store.tx((t) => t.
|
|
399
|
+
const sandbox = await store.tx((t) => t.environment(sandboxId));
|
|
320
400
|
const pod = sandbox?.pod;
|
|
321
401
|
if (!sandbox || sandbox.kind !== "pod" || !pod)
|
|
322
402
|
return undefined;
|
|
323
|
-
if (pod.desired === "deleted" || pod.observed === "lost"
|
|
403
|
+
if (pod.desired === "deleted" || pod.observed === "lost")
|
|
324
404
|
return undefined;
|
|
325
405
|
return {
|
|
326
406
|
epoch: pod.hostEpoch,
|
|
327
407
|
...(pod.podUid ? { podUid: pod.podUid } : {}),
|
|
328
|
-
|
|
408
|
+
// The applied allowlist: a recipe change writes it at once (Q8).
|
|
409
|
+
allow: sandbox.applied.network?.allowedHosts ?? [],
|
|
329
410
|
};
|
|
330
411
|
},
|
|
331
412
|
};
|
|
332
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* `EgressRecord` over the Tenant's record (R7.3, D67). The running turns are the sessions on the
|
|
416
|
+
* Environment whose body has an `activeTurnId`, in id order, at most `EGRESS_RUNNING_TURNS_MAX`.
|
|
417
|
+
* The write is `environment.egress` on the Environment's stream, under its row's lock; a deleted
|
|
418
|
+
* Environment gets nothing.
|
|
419
|
+
*/
|
|
420
|
+
export function storeEgressRecord(store) {
|
|
421
|
+
return {
|
|
422
|
+
async runningTurns(environmentId) {
|
|
423
|
+
const sessions = await store.tx((t) => t.sessionsOnEnvironment(environmentId));
|
|
424
|
+
return sessions
|
|
425
|
+
.filter((session) => typeof session.activeTurnId === "string")
|
|
426
|
+
.map((session) => ({ sessionId: session.id, turnId: session.activeTurnId }))
|
|
427
|
+
.sort((a, b) => (a.sessionId < b.sessionId ? -1 : a.sessionId > b.sessionId ? 1 : 0))
|
|
428
|
+
.slice(0, EGRESS_RUNNING_TURNS_MAX);
|
|
429
|
+
},
|
|
430
|
+
async write(connection) {
|
|
431
|
+
await store.tx(async (t) => {
|
|
432
|
+
if (!(await t.environment(connection.environmentId, { lock: true })))
|
|
433
|
+
return;
|
|
434
|
+
await t.environmentEvent(connection.environmentId, "environment.egress", connection);
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixture model (R4.3): a deterministic, scripted model the Model Gate answers with when the
|
|
3
|
+
* Tenant's setting `model.fixture` is on (`PUT /v1/tenant/config/seed {fixtureModel: true}`).
|
|
4
|
+
* The gate answers it itself, metered like any model (a `model_usage` row per call, provider
|
|
5
|
+
* `fixture`, tokens counted from the text, cost 0), so every harness that calls the gate is seen
|
|
6
|
+
* the same way: ours, and one written from the published contract.
|
|
7
|
+
*
|
|
8
|
+
* What it answers is read from the call: the last user message and the offered tools.
|
|
9
|
+
* - `fixture:text`: one text answer, `fixture ok`.
|
|
10
|
+
* - `fixture:tool <name> <json>`: one call of the offered tool `<name>` with `<json>` as its
|
|
11
|
+
* arguments; once the tool's result is in the prompt, a text answer that quotes it.
|
|
12
|
+
* - `fixture:unoffered <name> <json>`: the same call of a tool `<name>` the call does *not*
|
|
13
|
+
* offer (a model that names a tool it was never given), for the check that such a call runs
|
|
14
|
+
* nothing; once a result is in the prompt, a text answer that quotes it.
|
|
15
|
+
* - `fixture:budget`: a `budget_exhausted` failure.
|
|
16
|
+
* - `fixture:slow <seconds>`: a text answer after that long (at most 60 s), or nothing once
|
|
17
|
+
* the call is aborted, for cancel checks.
|
|
18
|
+
* - Anything else keeps the release demo: with a `lookup_order` tool it looks up order
|
|
19
|
+
* `demo-123` once, then answers with the result; on a later user message it says what the
|
|
20
|
+
* first one was; otherwise it says hello.
|
|
21
|
+
*/
|
|
22
|
+
import type { ModelCandidate, ModelFailureOutcome } from "@nylorun/core/harness-api";
|
|
23
|
+
import type { RuntimeModelCall } from "../contracts.js";
|
|
24
|
+
export declare const FIXTURE_PROVIDER = "fixture";
|
|
25
|
+
type Answer = ModelCandidate | ModelFailureOutcome;
|
|
26
|
+
/** The fixture model's answer to `call`. */
|
|
27
|
+
export declare function fixtureAnswer(call: RuntimeModelCall, signal?: AbortSignal): Promise<Answer>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const FIXTURE_PROVIDER = "fixture";
|
|
2
|
+
const MARKER = /^fixture:(\w+)(?:\s+([\s\S]*))?$/;
|
|
3
|
+
/** The fixture model's answer to `call`. */
|
|
4
|
+
export async function fixtureAnswer(call, signal) {
|
|
5
|
+
const last = call.prompt.at(-1);
|
|
6
|
+
const users = call.prompt.filter((item) => item.kind === "message" && item.role === "user");
|
|
7
|
+
const marker = MARKER.exec(textOf(users.at(-1)?.content ?? []).trim());
|
|
8
|
+
if (marker)
|
|
9
|
+
return scripted(call, marker[1], marker[2]?.trim() ?? "", last, signal);
|
|
10
|
+
if (last?.kind === "tool-result")
|
|
11
|
+
return candidate(call, `Order lookup complete: ${textOf(last.content, true)}`);
|
|
12
|
+
if (users.length > 1)
|
|
13
|
+
return candidate(call, `I remember: ${textOf(users[0].content)}`);
|
|
14
|
+
if (!call.tools.some((tool) => tool.name === "lookup_order"))
|
|
15
|
+
return candidate(call, "Hello from the fixture model.");
|
|
16
|
+
return candidate(call, [{ type: "tool-call", id: "fixture_lookup", name: "lookup_order", args: { orderId: "demo-123" } }]);
|
|
17
|
+
}
|
|
18
|
+
async function scripted(call, kind, rest, last, signal) {
|
|
19
|
+
switch (kind) {
|
|
20
|
+
case "text":
|
|
21
|
+
return candidate(call, "fixture ok");
|
|
22
|
+
case "tool":
|
|
23
|
+
case "unoffered": {
|
|
24
|
+
if (last?.kind === "tool-result")
|
|
25
|
+
return candidate(call, `fixture tool ${last.status}: ${textOf(last.content, true)}`);
|
|
26
|
+
const space = rest.indexOf(" ");
|
|
27
|
+
const name = space < 0 ? rest : rest.slice(0, space);
|
|
28
|
+
const offered = call.tools.some((tool) => tool.name === name);
|
|
29
|
+
if (kind === "tool" && !offered)
|
|
30
|
+
return candidate(call, `fixture: no tool ${name} is offered`);
|
|
31
|
+
if (kind === "unoffered" && offered)
|
|
32
|
+
return candidate(call, `fixture: tool ${name} is offered`);
|
|
33
|
+
let args = {};
|
|
34
|
+
try {
|
|
35
|
+
if (space >= 0)
|
|
36
|
+
args = JSON.parse(rest.slice(space + 1));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return candidate(call, "fixture: the tool arguments are not JSON");
|
|
40
|
+
}
|
|
41
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
42
|
+
return candidate(call, "fixture: the tool arguments are not an object");
|
|
43
|
+
return candidate(call, [{ type: "tool-call", id: "fixture_call", name, args: args }]);
|
|
44
|
+
}
|
|
45
|
+
case "budget":
|
|
46
|
+
return {
|
|
47
|
+
kind: "failed",
|
|
48
|
+
code: "budget_exhausted",
|
|
49
|
+
message: "The fixture model's budget is exhausted (fixture:budget)",
|
|
50
|
+
retryable: false,
|
|
51
|
+
};
|
|
52
|
+
case "slow": {
|
|
53
|
+
const seconds = Math.min(Math.max(Number(rest) || 0, 0), 60);
|
|
54
|
+
await sleep(seconds * 1000, signal);
|
|
55
|
+
return candidate(call, `fixture slept ${seconds} s`);
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
return candidate(call, `fixture: unknown marker ${kind}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function candidate(call, output) {
|
|
62
|
+
const blocks = typeof output === "string" ? [{ type: "text", text: output }] : output;
|
|
63
|
+
const inputTokens = tokens(JSON.stringify(call.prompt));
|
|
64
|
+
const outputTokens = tokens(JSON.stringify(blocks));
|
|
65
|
+
return {
|
|
66
|
+
output: blocks,
|
|
67
|
+
finishReason: blocks.some((block) => block.type === "tool-call") ? "tool-calls" : "stop",
|
|
68
|
+
usage: { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens, costUsd: 0 },
|
|
69
|
+
evidence: {
|
|
70
|
+
resolvedModel: FIXTURE_PROVIDER,
|
|
71
|
+
extras: { producer: { provider: FIXTURE_PROVIDER, model: FIXTURE_PROVIDER }, costKnown: true },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** About four characters a token, at least one. */
|
|
76
|
+
const tokens = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
77
|
+
function textOf(content, json = false) {
|
|
78
|
+
return content
|
|
79
|
+
.map((part) => (part.type === "text" ? (part.text ?? "") : json ? JSON.stringify(part) : ""))
|
|
80
|
+
.join(" ");
|
|
81
|
+
}
|
|
82
|
+
function sleep(ms, signal) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
if (signal?.aborted)
|
|
85
|
+
return reject(signal.reason);
|
|
86
|
+
const timer = setTimeout(resolve, ms);
|
|
87
|
+
signal?.addEventListener("abort", () => {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
reject(signal.reason);
|
|
90
|
+
}, { once: true });
|
|
91
|
+
});
|
|
92
|
+
}
|
package/dist/gates/handler.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Serves one model call in the gates service: opens the Tenant's vault (the database's one
|
|
3
|
-
* Tenant; a call naming another is refused) and calls the model
|
|
4
|
-
* through `inProcessModelGate`, the same code the loop ran before the gate, so outcomes,
|
|
3
|
+
* Tenant; a call naming another is refused) and calls the model (the fixture model while the
|
|
4
|
+
* Tenant's `model.fixture` is on, R4.3) through `inProcessModelGate`, the same code the loop ran before the gate, so outcomes,
|
|
5
5
|
* retries, classification and redaction are unchanged. The meter records the call's usage in
|
|
6
6
|
* the Tenant's ledger. Logs one line per call, never the prompt, the output or a credential.
|
|
7
7
|
*/
|
|
8
8
|
import { classifyThrown } from "../model/classify.js";
|
|
9
|
-
import { callFiles, inProcessModelGate } from "./in-process.js";
|
|
9
|
+
import { callFiles, inProcessModelGate, serve } from "./in-process.js";
|
|
10
10
|
import { createMeter } from "./meter.js";
|
|
11
11
|
import { GateRefusal } from "./tenant-vaults.js";
|
|
12
12
|
export function createModelCallHandler(options) {
|
|
@@ -28,7 +28,7 @@ export function createModelCallHandler(options) {
|
|
|
28
28
|
...(settings ? { settings } : {}),
|
|
29
29
|
...(blobs ? { files: callFiles(vault.store, blobs) } : {}),
|
|
30
30
|
});
|
|
31
|
-
outcome = await meter.call(vault.store, call, () => gate
|
|
31
|
+
outcome = await meter.call(vault.store, call, () => serve(vault.store, gate, call, signal));
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
34
|
if (signal.aborted) {
|