@nylorun/runtime 0.22.0-beta → 0.24.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 +161 -0
- package/README.md +95 -30
- package/dist/adapters/execution/restate.d.ts +15 -3
- package/dist/adapters/execution/restate.js +78 -84
- 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/adapters/streams/s2.js +1 -13
- package/dist/api/a2a/endpoint.js +5 -1
- package/dist/api/a2a/routes.d.ts +2 -0
- package/dist/api/a2a/routes.js +2 -2
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +4 -2
- package/dist/api/ag-ui/history.js +4 -1
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +11 -10
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +4953 -1204
- package/dist/api/components.js +25 -16
- package/dist/api/gate/keys.js +7 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +39 -3
- package/dist/api/http/app.js +4 -4
- 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/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +283 -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 +45 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +11 -6
- package/dist/api/http/routes/tenant.js +32 -10
- package/dist/api/openapi.js +31 -26
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +563 -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/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +1 -1
- package/dist/artifacts/workspace.js +1 -1
- package/dist/core/flow-host.d.ts +3 -22
- package/dist/core/flow-host.js +11 -37
- 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 +10 -43
- package/dist/execution/types.d.ts +11 -9
- package/dist/execution/types.js +5 -7
- 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 +28 -1
- package/dist/gates/egress.js +115 -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 +6 -2
- package/dist/gates/http-tool.js +3 -5
- 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 +52 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +5 -1
- 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 +134 -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 +10 -68
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +12 -5
- package/dist/harness/calls.js +21 -12
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +18 -5
- package/dist/harness/pod.d.ts +34 -0
- package/dist/harness/pod.js +41 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +13 -5
- package/dist/harness/setup.d.ts +27 -0
- package/dist/harness/setup.js +150 -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 +174 -44
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +8 -0
- package/dist/harness-api/server.js +93 -13
- 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 +96 -0
- package/dist/harness-api/tool-journal.js +102 -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 +6 -3
- package/dist/harness-api/workspace.js +27 -11
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/execution.d.ts +8 -6
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +64 -21
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +70 -0
- package/dist/host/stack-config.js +67 -1
- package/dist/infra/execution.d.ts +18 -1
- package/dist/infra/execution.js +28 -4
- package/dist/infra/index.d.ts +1 -1
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +5 -5
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +999 -2211
- package/dist/mcp/connect.d.ts +25 -20
- package/dist/mcp/connect.js +29 -30
- package/dist/mcp/pool.d.ts +2 -2
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -20
- package/dist/mcp/snapshot.d.ts +5 -11
- package/dist/mcp/snapshot.js +6 -7
- package/dist/model/pi-model.js +8 -0
- package/dist/openapi.json +6955 -2526
- 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/retry.d.ts +21 -0
- package/dist/retry.js +34 -0
- 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 -20
- package/dist/sandbox/manager.d.ts +16 -8
- package/dist/sandbox/manager.js +33 -17
- package/dist/sandbox/placement.d.ts +13 -12
- package/dist/sandbox/placement.js +29 -12
- package/dist/sandbox/pods/client.d.ts +6 -1
- package/dist/sandbox/pods/lifecycle.d.ts +28 -27
- package/dist/sandbox/pods/lifecycle.js +14 -26
- package/dist/sandbox/pods/reconcile.js +21 -15
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +16 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +35 -27
- package/dist/sandbox/resolve.js +66 -41
- package/dist/sandbox/session-sandbox.js +1 -1
- package/dist/sandbox/setup.d.ts +61 -0
- package/dist/sandbox/setup.js +123 -0
- package/dist/sandbox/share.d.ts +11 -21
- package/dist/sandbox/share.js +12 -41
- 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/control.js +8 -1
- package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
- package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -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/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +117 -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 +1213 -779
- package/dist/store/postgres/schema.js +136 -76
- package/dist/store/postgres/store.js +283 -117
- package/dist/store/types.d.ts +206 -88
- package/dist/streams/relay/core.js +1 -13
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +121 -49
- package/dist/tenant/artifact-tool.js +3 -4
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +94 -14
- package/dist/tenant/context.d.ts +67 -33
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/control.d.ts +8 -1
- package/dist/tenant/control.js +2 -0
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +37 -20
- package/dist/tenant/environments.d.ts +167 -0
- package/dist/tenant/environments.js +823 -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/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +30 -4
- package/dist/tenant/scheduler.d.ts +7 -0
- package/dist/tenant/scheduler.js +38 -7
- package/dist/tenant/session-streams.d.ts +0 -1
- package/dist/tenant/session-streams.js +1 -13
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +13 -13
- package/dist/tenant/sessions.js +61 -138
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/streams.js +14 -29
- package/dist/tenant/sweep.d.ts +39 -12
- package/dist/tenant/sweep.js +168 -63
- package/dist/tenant/transcript.d.ts +210 -20
- package/dist/tenant/transcript.js +62 -11
- 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 +42 -0
- package/dist/traces/ids.js +85 -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 +317 -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 +64 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/service.d.ts +47 -48
- package/dist/vault/service.js +321 -382
- 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 +14 -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/tenant/sandboxes.d.ts +0 -122
- package/dist/tenant/sandboxes.js +0 -562
|
@@ -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 `egress.connection` event (R7.3): 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 { EventPayload, EventWriter } 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,22 @@ export interface EgressSandboxes {
|
|
|
15
16
|
readonly allow: readonly string[];
|
|
16
17
|
} | undefined>;
|
|
17
18
|
}
|
|
19
|
+
/** The session and turn holding an Environment: turns are serial per Environment, so one at most. */
|
|
20
|
+
export interface EgressHolder {
|
|
21
|
+
readonly sessionId: string;
|
|
22
|
+
readonly turnId: string;
|
|
23
|
+
}
|
|
24
|
+
/** One connection's record (R7.3): `egress.connection`'s payload, `environment.egress`'s too. */
|
|
25
|
+
export type EgressConnection = EventPayload<"egress.connection">;
|
|
26
|
+
/** Where egress-gate records connections (R7.3). */
|
|
27
|
+
export interface EgressRecord {
|
|
28
|
+
/** The session whose turn holds the Environment now; `undefined` between turns. */
|
|
29
|
+
holder(environmentId: string): Promise<EgressHolder | undefined>;
|
|
30
|
+
/** Writes `connection` to `holder`'s session log, or to the Environment's stream without one. */
|
|
31
|
+
write(holder: EgressHolder | undefined, connection: EgressConnection): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** egress-gate as the writer of its events (R4.9): the gate saw the connection itself. */
|
|
34
|
+
export declare const EGRESS_WRITER: EventWriter;
|
|
18
35
|
/** Ports a tunnel may open. */
|
|
19
36
|
export declare const EGRESS_PORTS: readonly number[];
|
|
20
37
|
/** Tunnels one sandbox may hold open at once. */
|
|
@@ -49,6 +66,8 @@ export interface StartEgressGateOptions {
|
|
|
49
66
|
readonly dial?: (address: string, port: number) => Socket;
|
|
50
67
|
/** The vault and keys, for hosts an `environment_secret` is bound to (R2c). None: tunnels only. */
|
|
51
68
|
readonly credentials?: EgressCredentials;
|
|
69
|
+
/** Where connections are recorded (R7.3). None: they are only logged. */
|
|
70
|
+
readonly record?: EgressRecord;
|
|
52
71
|
}
|
|
53
72
|
export interface EgressGate {
|
|
54
73
|
/** Where the gate listens, e.g. `http://0.0.0.0:4200`. */
|
|
@@ -70,6 +89,14 @@ export declare function startEgressGate(options: StartEgressGateOptions): Promis
|
|
|
70
89
|
/**
|
|
71
90
|
* `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
|
|
72
91
|
* host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
|
|
73
|
-
*
|
|
92
|
+
* Environment, or one deleted or lost, as `staleHost` judges a host token.
|
|
74
93
|
*/
|
|
75
94
|
export declare function storeEgressSandboxes(store: SessionStore): EgressSandboxes;
|
|
95
|
+
/**
|
|
96
|
+
* `EgressRecord` over the Tenant's record (R7.3). The holder is the session on the Environment
|
|
97
|
+
* whose body has an `activeTurnId` (turns are serial per Environment). With a holder, the write
|
|
98
|
+
* is `egress.connection` on its session log, at that turn, under the session row's lock; without
|
|
99
|
+
* one, `environment.egress` on the Environment's stream, under its row's lock. A deleted
|
|
100
|
+
* Environment gets nothing.
|
|
101
|
+
*/
|
|
102
|
+
export declare function storeEgressRecord(store: SessionStore): EgressRecord;
|
package/dist/gates/egress.js
CHANGED
|
@@ -21,8 +21,17 @@
|
|
|
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, Q6, Q7), when it
|
|
28
|
+
* closes or at once when the gate refuses it: `egress.connection` on the session log of the turn
|
|
29
|
+
* that held the Environment when it opened (read as the name resolves), else `environment.egress`
|
|
30
|
+
* on the Environment's own stream. Host, port, outcome, bytes, duration and, for a credential
|
|
31
|
+
* host, the request count and credential ids; never a header value, a path or a body. A refusal
|
|
32
|
+
* before that (no token, a stale one, no `host:port`) stays a log line, and a client that hangs up
|
|
33
|
+
* before the gate answers leaves nothing. A write never holds the tunnel up, and a failed one is
|
|
34
|
+
* logged (`egress_record_failed`).
|
|
26
35
|
*/
|
|
27
36
|
import { lookup as dnsLookup } from "node:dns/promises";
|
|
28
37
|
import { createServer } from "node:http";
|
|
@@ -30,7 +39,9 @@ import { BlockList, connect, isIP } from "node:net";
|
|
|
30
39
|
import { isSandboxHostPattern } from "@nylorun/core/define";
|
|
31
40
|
import { isPrivateAddress } from "../tenant/outbound.js";
|
|
32
41
|
import { bindListener } from "../host/http.js";
|
|
33
|
-
import { credentialTerminator } from "./egress-credentials.js";
|
|
42
|
+
import { CONNECT_ESTABLISHED, credentialTerminator, } from "./egress-credentials.js";
|
|
43
|
+
/** egress-gate as the writer of its events (R4.9): the gate saw the connection itself. */
|
|
44
|
+
export const EGRESS_WRITER = { source: { kind: "egress-gate", id: "egress" }, evidence: "observed" };
|
|
34
45
|
/** Ports a tunnel may open. */
|
|
35
46
|
export const EGRESS_PORTS = [443, 80];
|
|
36
47
|
/** Tunnels one sandbox may hold open at once. */
|
|
@@ -134,6 +145,49 @@ function answer(socket, status, message, headers = []) {
|
|
|
134
145
|
body,
|
|
135
146
|
].join("\r\n"));
|
|
136
147
|
}
|
|
148
|
+
function connectionRecord(sink, logger, client, at) {
|
|
149
|
+
let holding;
|
|
150
|
+
let written = false;
|
|
151
|
+
const holder = (to) => (holding ??= to.holder(at.environmentId));
|
|
152
|
+
const write = (fields) => {
|
|
153
|
+
if (written || !sink)
|
|
154
|
+
return;
|
|
155
|
+
written = true;
|
|
156
|
+
const socket = client;
|
|
157
|
+
const allowed = fields.outcome === "allowed";
|
|
158
|
+
const connection = {
|
|
159
|
+
environmentId: at.environmentId,
|
|
160
|
+
host: at.host,
|
|
161
|
+
port: at.port,
|
|
162
|
+
...fields,
|
|
163
|
+
mode: record.credentialBound ? "credential" : "tunnel",
|
|
164
|
+
credentialBound: record.credentialBound,
|
|
165
|
+
// Up is what the sandbox sent after its CONNECT, down what it got after the gate's 200.
|
|
166
|
+
bytesUp: allowed ? Math.max(0, (socket.bytesRead ?? 0) - at.upFrom) : 0,
|
|
167
|
+
bytesDown: allowed ? Math.max(0, (socket.bytesWritten ?? 0) - CONNECT_ESTABLISHED.length) : 0,
|
|
168
|
+
durationMs: Math.max(0, Date.now() - at.startedAt.getTime()),
|
|
169
|
+
startedAt: at.startedAt.toISOString(),
|
|
170
|
+
};
|
|
171
|
+
holder(sink)
|
|
172
|
+
.then((held) => sink.write(held, connection))
|
|
173
|
+
.catch((error) => logger.error("egress_record_failed", {
|
|
174
|
+
sandboxId: at.environmentId,
|
|
175
|
+
host: at.host,
|
|
176
|
+
error: error instanceof Error ? error.message : String(error),
|
|
177
|
+
}));
|
|
178
|
+
};
|
|
179
|
+
const record = {
|
|
180
|
+
credentialBound: false,
|
|
181
|
+
// A failed read is logged when the write needs it.
|
|
182
|
+
holder: () => void (sink && holder(sink).catch(() => undefined)),
|
|
183
|
+
refused: (status, reason) => write({ outcome: "refused", reason, status }),
|
|
184
|
+
closed: (tally) => write({
|
|
185
|
+
outcome: "allowed",
|
|
186
|
+
...(tally ? { requests: tally.requests, credentialIds: [...tally.credentialIds] } : {}),
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
return record;
|
|
190
|
+
}
|
|
137
191
|
export async function startEgressGate(options) {
|
|
138
192
|
const { logger, sandboxes } = options;
|
|
139
193
|
const idleMs = options.idleMs ?? EGRESS_IDLE_MS;
|
|
@@ -160,11 +214,17 @@ export async function startEgressGate(options) {
|
|
|
160
214
|
: undefined;
|
|
161
215
|
const open = new Map();
|
|
162
216
|
const tunnels = new Set();
|
|
217
|
+
/** Each connection's record, once its Environment and target are known. */
|
|
218
|
+
const records = new WeakMap();
|
|
163
219
|
async function onConnect(req, client, head) {
|
|
220
|
+
const startedAt = new Date();
|
|
221
|
+
// What the sandbox sent before the tunnel: its CONNECT, less the bytes after it (`head`).
|
|
222
|
+
const upFrom = (client.bytesRead ?? 0) - head.length;
|
|
164
223
|
const target = parseTarget(req.url);
|
|
165
224
|
const refuse = (status, reason, fields = {}, message) => {
|
|
166
225
|
logger.warn("egress_refused", { status, reason, ...(target ? { host: target.host, port: target.port } : {}), ...fields });
|
|
167
226
|
answer(client, status, message ?? reason, status === 407 ? ['proxy-authenticate: Basic realm="nylorun-egress"'] : []);
|
|
227
|
+
records.get(client)?.refused(status, reason);
|
|
168
228
|
};
|
|
169
229
|
const raw = proxyToken(req.headers["proxy-authorization"]);
|
|
170
230
|
if (!raw)
|
|
@@ -183,12 +243,22 @@ export async function startEgressGate(options) {
|
|
|
183
243
|
if (!target)
|
|
184
244
|
return refuse(403, "target_invalid", { sandboxId }, "CONNECT needs host:port");
|
|
185
245
|
const { host, port } = target;
|
|
246
|
+
const record = connectionRecord(options.record, logger, client, {
|
|
247
|
+
environmentId: sandboxId,
|
|
248
|
+
host,
|
|
249
|
+
port,
|
|
250
|
+
startedAt,
|
|
251
|
+
upFrom,
|
|
252
|
+
});
|
|
253
|
+
records.set(client, record);
|
|
186
254
|
if (!EGRESS_PORTS.includes(port))
|
|
187
255
|
return refuse(403, "port_refused", { sandboxId }, `port ${port} is not allowed; tunnels open to 443 or 80 only`);
|
|
188
256
|
if (isIP(host.replace(/^\[|\]$/g, "")) !== 0 || !isSandboxHostPattern(host) || host.startsWith("*."))
|
|
189
257
|
return refuse(403, "not_a_host_name", { sandboxId }, `${host} is not a host name; IP literals are refused`);
|
|
190
258
|
if (!egressAllows(host, sandbox.allow))
|
|
191
259
|
return refuse(403, "host_not_allowed", { sandboxId }, `${host} is not in this sandbox's network.allow`);
|
|
260
|
+
// Who holds the Environment as the connection opens, read while the name resolves.
|
|
261
|
+
record.holder();
|
|
192
262
|
let mine = open.get(sandboxId);
|
|
193
263
|
if (!mine)
|
|
194
264
|
open.set(sandboxId, (mine = new Set()));
|
|
@@ -217,10 +287,13 @@ export async function startEgressGate(options) {
|
|
|
217
287
|
if (client.destroyed)
|
|
218
288
|
return;
|
|
219
289
|
if (terminator && port === 443 && (await options.credentials.bound(sandboxId, host))) {
|
|
290
|
+
record.credentialBound = true;
|
|
220
291
|
// A client that spoke before the 200 is not one this path serves.
|
|
221
292
|
if (head.length > 0)
|
|
222
293
|
return refuse(403, "early_data", { sandboxId }, "wait for the CONNECT answer before sending");
|
|
223
|
-
|
|
294
|
+
const tally = { requests: 0, credentialIds: new Set() };
|
|
295
|
+
client.once("close", () => record.closed(tally));
|
|
296
|
+
return terminator.terminate(client, { sandboxId, host, addresses: allowed, tally });
|
|
224
297
|
}
|
|
225
298
|
const upstream = await dialFirst(allowed.map((entry) => entry.address), port);
|
|
226
299
|
if (!upstream)
|
|
@@ -237,8 +310,11 @@ export async function startEgressGate(options) {
|
|
|
237
310
|
upstream.on("timeout", close);
|
|
238
311
|
upstream.on("error", close);
|
|
239
312
|
upstream.on("close", () => client.destroy());
|
|
240
|
-
client.on("close", () =>
|
|
241
|
-
|
|
313
|
+
client.on("close", () => {
|
|
314
|
+
upstream.destroy();
|
|
315
|
+
record.closed();
|
|
316
|
+
});
|
|
317
|
+
client.write(CONNECT_ESTABLISHED);
|
|
242
318
|
if (head.length > 0)
|
|
243
319
|
upstream.write(head);
|
|
244
320
|
upstream.pipe(client);
|
|
@@ -284,6 +360,7 @@ export async function startEgressGate(options) {
|
|
|
284
360
|
onConnect(req, client, head).catch((error) => {
|
|
285
361
|
logger.error("egress_failed", { error: error instanceof Error ? error.message : String(error) });
|
|
286
362
|
answer(client, 502, "the tunnel failed");
|
|
363
|
+
records.get(client)?.refused(502, "egress_failed");
|
|
287
364
|
});
|
|
288
365
|
});
|
|
289
366
|
server.on("clientError", (_error, socket) => {
|
|
@@ -311,22 +388,50 @@ export async function startEgressGate(options) {
|
|
|
311
388
|
/**
|
|
312
389
|
* `EgressSandboxes` over the Tenant's `sandbox_resources` (one indexed read): a pod sandbox's
|
|
313
390
|
* host epoch, the pod that joined at it and its spec's allowlist; nothing for a virtual
|
|
314
|
-
*
|
|
391
|
+
* Environment, or one deleted or lost, as `staleHost` judges a host token.
|
|
315
392
|
*/
|
|
316
393
|
export function storeEgressSandboxes(store) {
|
|
317
394
|
return {
|
|
318
395
|
async live(sandboxId) {
|
|
319
|
-
const sandbox = await store.tx((t) => t.
|
|
396
|
+
const sandbox = await store.tx((t) => t.environment(sandboxId));
|
|
320
397
|
const pod = sandbox?.pod;
|
|
321
398
|
if (!sandbox || sandbox.kind !== "pod" || !pod)
|
|
322
399
|
return undefined;
|
|
323
|
-
if (pod.desired === "deleted" || pod.observed === "lost"
|
|
400
|
+
if (pod.desired === "deleted" || pod.observed === "lost")
|
|
324
401
|
return undefined;
|
|
325
402
|
return {
|
|
326
403
|
epoch: pod.hostEpoch,
|
|
327
404
|
...(pod.podUid ? { podUid: pod.podUid } : {}),
|
|
328
|
-
|
|
405
|
+
// The applied allowlist: a recipe change writes it at once (Q8).
|
|
406
|
+
allow: sandbox.applied.network?.allowedHosts ?? [],
|
|
329
407
|
};
|
|
330
408
|
},
|
|
331
409
|
};
|
|
332
410
|
}
|
|
411
|
+
/**
|
|
412
|
+
* `EgressRecord` over the Tenant's record (R7.3). The holder is the session on the Environment
|
|
413
|
+
* whose body has an `activeTurnId` (turns are serial per Environment). With a holder, the write
|
|
414
|
+
* is `egress.connection` on its session log, at that turn, under the session row's lock; without
|
|
415
|
+
* one, `environment.egress` on the Environment's stream, under its row's lock. A deleted
|
|
416
|
+
* Environment gets nothing.
|
|
417
|
+
*/
|
|
418
|
+
export function storeEgressRecord(store) {
|
|
419
|
+
return {
|
|
420
|
+
async holder(environmentId) {
|
|
421
|
+
const sessions = await store.tx((t) => t.sessionsOnEnvironment(environmentId));
|
|
422
|
+
const holding = sessions.find((session) => typeof session.activeTurnId === "string");
|
|
423
|
+
return holding ? { sessionId: holding.id, turnId: holding.activeTurnId } : undefined;
|
|
424
|
+
},
|
|
425
|
+
async write(holder, connection) {
|
|
426
|
+
await store.tx(async (t) => {
|
|
427
|
+
if (holder) {
|
|
428
|
+
await t.event(holder.sessionId, holder.turnId, "egress.connection", connection, EGRESS_WRITER);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (!(await t.environment(connection.environmentId, { lock: true })))
|
|
432
|
+
return;
|
|
433
|
+
await t.environmentEvent(connection.environmentId, "environment.egress", connection);
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
}
|
|
@@ -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) {
|
|
@@ -17,9 +17,8 @@ export type HttpOutcome = {
|
|
|
17
17
|
readonly kind: "failed";
|
|
18
18
|
readonly code: string;
|
|
19
19
|
readonly message: string;
|
|
20
|
-
/** With `credential_rejected`: the tool's `credential
|
|
20
|
+
/** With `credential_rejected`: the tool's `credential`. */
|
|
21
21
|
readonly server?: string;
|
|
22
|
-
readonly vault?: "installation" | "user";
|
|
23
22
|
/** How many credential values the message had in it, replaced (R2b C8). */
|
|
24
23
|
readonly redacted?: number;
|
|
25
24
|
};
|
|
@@ -45,6 +44,11 @@ export interface HttpToolCall {
|
|
|
45
44
|
readonly effectId: string;
|
|
46
45
|
readonly turnId: string;
|
|
47
46
|
readonly input: unknown;
|
|
47
|
+
/**
|
|
48
|
+
* The call's span context (R7.4), sent as `traceparent`: in the gates service, from the call's
|
|
49
|
+
* journal row (`gates/journal.ts`); in process, from core's `execute` answer. Never a body field.
|
|
50
|
+
*/
|
|
51
|
+
readonly traceparent?: string;
|
|
48
52
|
}
|
|
49
53
|
/** What `runHttpTool` reads of the Tenant: the session's pinned manifest and its credentials. */
|
|
50
54
|
export interface HttpToolTenant {
|
package/dist/gates/http-tool.js
CHANGED
|
@@ -163,7 +163,6 @@ export async function runHttpTool(tenant, call, options) {
|
|
|
163
163
|
let credential = {};
|
|
164
164
|
let identity;
|
|
165
165
|
let target = http;
|
|
166
|
-
let vault;
|
|
167
166
|
if (http.credential !== undefined) {
|
|
168
167
|
const authorized = await tenant.authorize(tool.sessionId, {
|
|
169
168
|
url: http.url,
|
|
@@ -172,10 +171,9 @@ export async function runHttpTool(tenant, call, options) {
|
|
|
172
171
|
if (authorized.status === "refused")
|
|
173
172
|
return failed("http.credential", `The credential '${http.credential}' was refused: ${authorized.reason}`);
|
|
174
173
|
if (authorized.status === "unauthenticated")
|
|
175
|
-
return failed("http.credential", `The session's
|
|
174
|
+
return failed("http.credential", `The session's Environment holds no credential '${http.credential}' for ${authorized.url}`);
|
|
176
175
|
credential = authorized.headers;
|
|
177
176
|
identity = authorized.identity;
|
|
178
|
-
vault = authorized.vault;
|
|
179
177
|
// A credential with `via` sends the call there, a gateway (R2b C2, Q9).
|
|
180
178
|
if (authorized.via !== undefined)
|
|
181
179
|
target = { ...http, url: authorized.via };
|
|
@@ -187,6 +185,7 @@ export async function runHttpTool(tenant, call, options) {
|
|
|
187
185
|
[TURN_ID_HEADER]: call.turnId,
|
|
188
186
|
[AGENT_ID_HEADER]: agentId ?? session.manifest.id,
|
|
189
187
|
"idempotency-key": call.effectId,
|
|
188
|
+
...(call.traceparent ? { traceparent: call.traceparent } : {}),
|
|
190
189
|
},
|
|
191
190
|
policy: options.policy,
|
|
192
191
|
signal: options.signal,
|
|
@@ -199,9 +198,8 @@ export async function runHttpTool(tenant, call, options) {
|
|
|
199
198
|
return outcome;
|
|
200
199
|
return {
|
|
201
200
|
...outcome,
|
|
202
|
-
message: `The service answered HTTP 401: it rejected the credential '${http.credential}' from the
|
|
201
|
+
message: `The service answered HTTP 401: it rejected the credential '${http.credential}' from the session's Environment`,
|
|
203
202
|
server: http.credential,
|
|
204
|
-
...(vault === undefined ? {} : { vault }),
|
|
205
203
|
};
|
|
206
204
|
}
|
|
207
205
|
}
|
|
@@ -41,4 +41,9 @@ export declare function tenantModelGate(options: {
|
|
|
41
41
|
/** The Tenant's Object store, for the files a prompt names. */
|
|
42
42
|
readonly blobs?: BlobStore;
|
|
43
43
|
}): ModelGate;
|
|
44
|
+
/**
|
|
45
|
+
* Serves one call: from the fixture model while the Tenant's `model.fixture` setting is on (R4.3),
|
|
46
|
+
* else from `gate`. Read per call, so a seed applies to the next call.
|
|
47
|
+
*/
|
|
48
|
+
export declare function serve(store: SessionStore, gate: ModelGate, request: ModelGateRequest, signal: AbortSignal): ReturnType<ModelGate["call"]>;
|
|
44
49
|
export declare function inProcessModelGate(options: InProcessModelGateOptions): ModelGate;
|
package/dist/gates/in-process.js
CHANGED
|
@@ -2,6 +2,8 @@ import { piModel } from "../model/pi-model.js";
|
|
|
2
2
|
import { HostModelVault } from "../vault/host-model.js";
|
|
3
3
|
import { createMeter } from "./meter.js";
|
|
4
4
|
import { artifactFiles } from "../artifacts/files.js";
|
|
5
|
+
import { usesFixtureModel } from "../tenant/model-setting.js";
|
|
6
|
+
import { fixtureAnswer } from "./fixture-model.js";
|
|
5
7
|
/**
|
|
6
8
|
* The files a call names, from the Tenant's artifacts, limited to the call's session: in
|
|
7
9
|
* process, the loop's own request; over HTTP, the session of the run token's claims, which the
|
|
@@ -26,9 +28,18 @@ export function tenantModelGate(options) {
|
|
|
26
28
|
});
|
|
27
29
|
const meter = createMeter({ logger: options.logger });
|
|
28
30
|
return {
|
|
29
|
-
call: (request, signal) => meter.call(options.store, request, () => gate
|
|
31
|
+
call: (request, signal) => meter.call(options.store, request, () => serve(options.store, gate, request, signal)),
|
|
30
32
|
};
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Serves one call: from the fixture model while the Tenant's `model.fixture` setting is on (R4.3),
|
|
36
|
+
* else from `gate`. Read per call, so a seed applies to the next call.
|
|
37
|
+
*/
|
|
38
|
+
export async function serve(store, gate, request, signal) {
|
|
39
|
+
if (await store.tx((t) => usesFixtureModel(t)))
|
|
40
|
+
return fixtureAnswer(request.call, signal);
|
|
41
|
+
return gate.call(request, signal);
|
|
42
|
+
}
|
|
32
43
|
export function inProcessModelGate(options) {
|
|
33
44
|
return {
|
|
34
45
|
async call(request, signal) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { McpServerRef } from "../mcp/pool.js";
|
|
2
|
+
import type { RunClaims } from "../tenant/run-token.js";
|
|
3
|
+
import { type HttpToolRef } from "./http-tool.js";
|
|
4
|
+
import type { TenantVaults } from "./tenant-vaults.js";
|
|
5
|
+
/** The call a run asks the gate to send, as the gate's body names it. */
|
|
6
|
+
export type GateToolCall = {
|
|
7
|
+
readonly kind: "mcp";
|
|
8
|
+
readonly server: McpServerRef;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly arguments: Record<string, unknown>;
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: "http";
|
|
13
|
+
readonly tool: HttpToolRef;
|
|
14
|
+
readonly input: unknown;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* What the journal says of a call: why it does not hold it, or that it does, with the call's span
|
|
18
|
+
* context from the row (R7.4), which the gate sends on as `traceparent`.
|
|
19
|
+
*/
|
|
20
|
+
export type JournalAnswer = {
|
|
21
|
+
readonly refused: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly refused?: undefined;
|
|
24
|
+
readonly traceparent?: string;
|
|
25
|
+
};
|
|
26
|
+
export interface JournalCheck {
|
|
27
|
+
/** Whether the journal holds `call` under `key` for the run `claims` names. */
|
|
28
|
+
check(tenantId: string | undefined, claims: RunClaims, key: string, call: GateToolCall): Promise<JournalAnswer>;
|
|
29
|
+
}
|
|
30
|
+
export declare function journalCheck(vaults: TenantVaults): JournalCheck;
|