@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
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
* (`--service harness`, `main.ts`) its own, whose sandbox events it claims from core.
|
|
9
9
|
*/
|
|
10
10
|
import { hashManifest } from "@nylorun/core/define";
|
|
11
|
-
import {
|
|
11
|
+
import { GateCallLost } from "../gates/tool-client.js";
|
|
12
12
|
import { serversOf } from "../mcp/pool.js";
|
|
13
13
|
import { mcpToolOf } from "../mcp/snapshot.js";
|
|
14
14
|
import { isHttpToolCall } from "../gates/http-tool.js";
|
|
15
15
|
import { abortOn, callHttpTool, callMcpTool, callSandboxTool, invokeModel, isRemoteMcpCall, isSaveArtifactCall, } from "./calls.js";
|
|
16
|
-
/** The Tenant's model of the fixture-model setting. Stateless. */
|
|
17
|
-
const fixture = toolFixtureModel();
|
|
18
16
|
export function harnessExecutors(options) {
|
|
19
17
|
const recoversTools = options.toolGate.recovers === true;
|
|
20
18
|
const atGate = (effect, run) => {
|
|
@@ -23,16 +21,19 @@ export function harnessExecutors(options) {
|
|
|
23
21
|
};
|
|
24
22
|
const recovered = (effect, run) => recoversTools && atGate(effect, run);
|
|
25
23
|
return {
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// With the Tenant's fixture model on, the Model Gate answers it (R4.3), metered like any call.
|
|
25
|
+
model: (effect, signal, run) => invokeModel(options.model, effect, signal, { atGate: run.start.options.fixtureModel }),
|
|
26
|
+
tool(effect, signal, run, call) {
|
|
28
27
|
const routing = routingOf(run);
|
|
28
|
+
const traceparent = call?.traceparent;
|
|
29
29
|
// A call at the Tool Gate outlives a shutdown there; a local one finishes and is recorded.
|
|
30
30
|
const kinds = recovered(effect, run) ? ["cancel", "shutdown"] : ["cancel"];
|
|
31
31
|
if (isHttpToolCall(routing.rootManifest, effect))
|
|
32
|
-
return callHttpTool(options.toolGate, effect, abortOn(signal, kinds));
|
|
32
|
+
return callHttpTool(options.toolGate, effect, abortOn(signal, kinds), traceparent);
|
|
33
|
+
// A sandbox command joins its call's span (R7.4): a CLI that reads TRACEPARENT traces under it.
|
|
33
34
|
if (!mcpToolOf(routing.mcpSnapshot, effect))
|
|
34
|
-
return callSandboxTool(options.sandbox(), routing, effect, signal);
|
|
35
|
-
return callMcpTool(options.mcp, routing, effect, abortOn(signal, kinds));
|
|
35
|
+
return callSandboxTool(options.sandbox(), traceparent ? { ...routing, environment: { ...routing.environment, TRACEPARENT: traceparent } } : routing, effect, signal);
|
|
36
|
+
return callMcpTool(options.mcp, routing, effect, abortOn(signal, kinds), traceparent);
|
|
36
37
|
},
|
|
37
38
|
recovers: {
|
|
38
39
|
get model() {
|
|
@@ -40,6 +41,8 @@ export function harnessExecutors(options) {
|
|
|
40
41
|
},
|
|
41
42
|
tool: recovered,
|
|
42
43
|
},
|
|
44
|
+
// A gate hop that lost the answer: the call is sent again with its key (R5.3).
|
|
45
|
+
resends: (effect, error, run) => error instanceof GateCallLost && atGate(effect, run),
|
|
43
46
|
async cancelAtGate(effect) {
|
|
44
47
|
await options.toolGate.cancel?.({
|
|
45
48
|
tenantId: options.model.tenantId,
|
|
@@ -94,7 +97,7 @@ export function routingOf(run) {
|
|
|
94
97
|
rootManifest: routing.rootManifest,
|
|
95
98
|
...(routing.mcpSnapshot ? { mcpSnapshot: routing.mcpSnapshot } : {}),
|
|
96
99
|
sandboxOwnerId: routing.sandbox?.ownerId ?? run.grant.sessionId,
|
|
97
|
-
...(routing.sandbox?.
|
|
100
|
+
...(routing.sandbox?.environmentId === undefined ? {} : { environmentId: routing.sandbox.environmentId }),
|
|
98
101
|
...(routing.sandbox?.environment === undefined ? {} : { environment: routing.sandbox.environment }),
|
|
99
102
|
activeTurnId: run.grant.turnId,
|
|
100
103
|
claim: run.runId,
|
package/dist/harness/main.js
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* In a pod sandbox (`NYLORUN_SANDBOX_KIND=pod`, F7.2) it is that sandbox's engine: it first waits
|
|
12
12
|
* until the pod's NetworkPolicy is in force (`awaitNetworkPolicy`), then joins with the
|
|
13
|
-
* pod's join token instead of a harness token (`pod.ts`),
|
|
14
|
-
* pod itself (the `local` backend), with egress
|
|
13
|
+
* pod's join token instead of a harness token (`pod.ts`), runs its disk's setup on a new volume
|
|
14
|
+
* (`setup.ts`), and runs its sandbox's tools in the pod itself (the `local` backend), with egress
|
|
15
|
+
* through egress-gate. SIGTERM (tini forwards
|
|
15
16
|
* it; the pod's grace is 10 s by default) gives its runs back and exits.
|
|
16
17
|
*/
|
|
17
18
|
import { mkdirSync } from "node:fs";
|
|
@@ -21,9 +22,10 @@ import { httpModelGate } from "../gates/http-client.js";
|
|
|
21
22
|
import { httpToolGate } from "../gates/tool-client.js";
|
|
22
23
|
import { createHostLogger } from "../host/logger.js";
|
|
23
24
|
import { RUNTIME_VERSION } from "../version.js";
|
|
24
|
-
import { localBackend } from "../adapters/sandbox/local.js";
|
|
25
|
+
import { localBackend, localExec } from "../adapters/sandbox/local.js";
|
|
25
26
|
import { awaitNetworkPolicy } from "../sandbox/pods/network-gate.js";
|
|
26
27
|
import { podHost } from "./pod.js";
|
|
28
|
+
import { prepareDisk } from "./setup.js";
|
|
27
29
|
import { harnessRunTokens } from "./run-tokens.js";
|
|
28
30
|
import { startHarnessService } from "./service.js";
|
|
29
31
|
/**
|
|
@@ -38,6 +40,7 @@ export async function runHarness(stack, baseline) {
|
|
|
38
40
|
logger.info("host_stack_config", { services: [...stack.services], harness: config.url, gates: config.gatesUrl });
|
|
39
41
|
// A pod's engine trusts the network only once its NetworkPolicy blocks what it must.
|
|
40
42
|
let pod;
|
|
43
|
+
let prepare;
|
|
41
44
|
if (config.pod) {
|
|
42
45
|
const { waitedMs } = await awaitNetworkPolicy({
|
|
43
46
|
addresses: config.pod.blocked,
|
|
@@ -48,6 +51,19 @@ export async function runHarness(stack, baseline) {
|
|
|
48
51
|
volumeFile: join(config.root, "volume-id"),
|
|
49
52
|
caBundleFile: join(config.root, "egress-ca-bundle.pem"),
|
|
50
53
|
});
|
|
54
|
+
// Join, then make the disk ready (R2d step 2) before taking any turn.
|
|
55
|
+
await pod.token();
|
|
56
|
+
const host = pod;
|
|
57
|
+
prepare = () => prepareDisk({
|
|
58
|
+
host,
|
|
59
|
+
exec: localExec({ env: baseline, proxyEnv: () => host.proxyEnv() }),
|
|
60
|
+
volumeFile: join(config.root, "volume-id"),
|
|
61
|
+
markerFile: join(config.root, "setup.json"),
|
|
62
|
+
logger,
|
|
63
|
+
});
|
|
64
|
+
// A fork onto other setup (Q17) runs it after connecting: core writes its disk through the connection.
|
|
65
|
+
if (!pod.afterSeed)
|
|
66
|
+
await prepare();
|
|
51
67
|
}
|
|
52
68
|
const token = pod ? () => pod.token() : config.token;
|
|
53
69
|
if (token === undefined)
|
|
@@ -56,16 +72,19 @@ export async function runHarness(stack, baseline) {
|
|
|
56
72
|
const service = startHarnessService({
|
|
57
73
|
url: config.url,
|
|
58
74
|
token,
|
|
59
|
-
...(pod
|
|
75
|
+
...(pod
|
|
76
|
+
? { sandboxBackends: [localBackend({ env: baseline, proxyEnv: () => pod.proxyEnv(), harnessRoot: config.root })] }
|
|
77
|
+
: {}),
|
|
60
78
|
paths,
|
|
61
79
|
modelGate: httpModelGate({ url: config.gatesUrl, runTokens }),
|
|
62
80
|
useVaultModel: true,
|
|
63
81
|
toolGate: httpToolGate({ url: config.gatesUrl, runTokens }),
|
|
64
82
|
logger,
|
|
65
|
-
name: pod ? `sandbox ${config.pod.sandboxId}` : "harness",
|
|
66
83
|
version: RUNTIME_VERSION,
|
|
67
84
|
onGrant: (grant) => runTokens.grant(grant),
|
|
68
85
|
});
|
|
86
|
+
if (pod?.afterSeed && prepare)
|
|
87
|
+
void prepare().catch((error) => logger.warn("environment_setup_failed", { message: error instanceof Error ? error.message : String(error) }));
|
|
69
88
|
const health = createServer((request, response) => {
|
|
70
89
|
if (request.url !== "/health") {
|
|
71
90
|
response.writeHead(404).end();
|
package/dist/harness/pod.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import type { PodHostConfig } from "../host/stack-config.js";
|
|
2
2
|
/** How often the host and egress tokens are renewed. */
|
|
3
3
|
export declare const RENEW_MS: number;
|
|
4
|
+
/** What to run and copy on a new disk (`sandbox/setup.ts` in core), from the join answer. */
|
|
5
|
+
export interface DiskSetup {
|
|
6
|
+
readonly recipeVersion: number;
|
|
7
|
+
readonly commands: readonly {
|
|
8
|
+
readonly command: string;
|
|
9
|
+
readonly cwd: string;
|
|
10
|
+
readonly timeoutMs: number;
|
|
11
|
+
}[];
|
|
12
|
+
readonly files: readonly {
|
|
13
|
+
readonly index: number;
|
|
14
|
+
readonly path: string;
|
|
15
|
+
}[];
|
|
16
|
+
readonly environment: Readonly<Record<string, string>>;
|
|
17
|
+
}
|
|
18
|
+
/** How a setup ended, as the engine reports it. */
|
|
19
|
+
export interface DiskSetupReport {
|
|
20
|
+
readonly status: "ok" | "failed";
|
|
21
|
+
readonly index?: number;
|
|
22
|
+
readonly exitCode?: number;
|
|
23
|
+
readonly output?: string;
|
|
24
|
+
}
|
|
4
25
|
export interface PodHost {
|
|
5
26
|
/** A current host token, for the next connection. Throws when neither renewal nor join works. */
|
|
6
27
|
token(): Promise<string>;
|
|
@@ -8,6 +29,24 @@ export interface PodHost {
|
|
|
8
29
|
proxyEnv(): Record<string, string>;
|
|
9
30
|
/** The host epoch of the tokens held. */
|
|
10
31
|
readonly epoch: number | undefined;
|
|
32
|
+
/** The disk's setup from the last join, if its recipe has one. */
|
|
33
|
+
readonly setup: DiskSetup | undefined;
|
|
34
|
+
/** The last join's volume starts from a disk version core writes: no setup runs on it… */
|
|
35
|
+
readonly seeded: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* …unless it is a fork onto other setup (Q17): the setup runs once core has written the
|
|
38
|
+
* version, which it does through this engine's connection, so the engine connects first.
|
|
39
|
+
*/
|
|
40
|
+
readonly afterSeed: boolean;
|
|
41
|
+
/** Whether to run the setup again after a failure, with the plan to run now. */
|
|
42
|
+
setupState(): Promise<{
|
|
43
|
+
retry: boolean;
|
|
44
|
+
setup?: DiskSetup;
|
|
45
|
+
}>;
|
|
46
|
+
/** Reports how the setup ended. */
|
|
47
|
+
reportSetup(report: DiskSetupReport): Promise<void>;
|
|
48
|
+
/** Input file `index` (a file's bytes or a folder's listing), or one file of a folder. */
|
|
49
|
+
setupFile(index: number, path?: string): Promise<Response>;
|
|
11
50
|
stop(): void;
|
|
12
51
|
}
|
|
13
52
|
export declare function podHost(config: PodHostConfig, logger: {
|
package/dist/harness/pod.js
CHANGED
|
@@ -31,6 +31,9 @@ class Refused extends Error {
|
|
|
31
31
|
export function podHost(config, logger, options = {}) {
|
|
32
32
|
const call = options.fetch ?? fetch;
|
|
33
33
|
let held;
|
|
34
|
+
let setup;
|
|
35
|
+
let seeded = false;
|
|
36
|
+
let afterSeed = false;
|
|
34
37
|
/** The CA in the bundle on disk, once one is written. */
|
|
35
38
|
let bundled;
|
|
36
39
|
const post = async (path, init) => {
|
|
@@ -51,6 +54,11 @@ export function podHost(config, logger, options = {}) {
|
|
|
51
54
|
const body = JSON.parse(text);
|
|
52
55
|
if (typeof body.caCertificate === "string")
|
|
53
56
|
await writeBundle(body.caCertificate);
|
|
57
|
+
if (path === "join") {
|
|
58
|
+
setup = body.setup;
|
|
59
|
+
seeded = body.seeded === true;
|
|
60
|
+
afterSeed = body.afterSeed === true;
|
|
61
|
+
}
|
|
54
62
|
return {
|
|
55
63
|
hostToken: body.hostToken,
|
|
56
64
|
egressToken: body.egressToken,
|
|
@@ -99,6 +107,15 @@ export function podHost(config, logger, options = {}) {
|
|
|
99
107
|
logger.info("sandbox_host_joined", { sandboxId: config.sandboxId, epoch: held.epoch });
|
|
100
108
|
return held;
|
|
101
109
|
};
|
|
110
|
+
/** A request with a current host token: the held one, renewed near expiry, or a new join. */
|
|
111
|
+
const authorized = async (path, init) => {
|
|
112
|
+
const current = held && held.expiresAt - Date.now() > 60_000 ? held : ((await renew()) ?? (await join()));
|
|
113
|
+
return call(`${config.httpUrl}/nylorun/harness/v1/host/${path}`, {
|
|
114
|
+
...init,
|
|
115
|
+
headers: { ...init.headers, authorization: `Bearer ${current.hostToken}` },
|
|
116
|
+
signal: AbortSignal.timeout(5 * 60_000),
|
|
117
|
+
});
|
|
118
|
+
};
|
|
102
119
|
const renew = async () => {
|
|
103
120
|
if (!held)
|
|
104
121
|
return undefined;
|
|
@@ -147,6 +164,35 @@ export function podHost(config, logger, options = {}) {
|
|
|
147
164
|
get epoch() {
|
|
148
165
|
return held?.epoch;
|
|
149
166
|
},
|
|
167
|
+
get setup() {
|
|
168
|
+
return setup;
|
|
169
|
+
},
|
|
170
|
+
get seeded() {
|
|
171
|
+
return seeded;
|
|
172
|
+
},
|
|
173
|
+
get afterSeed() {
|
|
174
|
+
return afterSeed;
|
|
175
|
+
},
|
|
176
|
+
async setupState() {
|
|
177
|
+
const response = await authorized("setup", { method: "GET" });
|
|
178
|
+
if (!response.ok)
|
|
179
|
+
throw new Error(`host/setup answered ${response.status}`);
|
|
180
|
+
return (await response.json());
|
|
181
|
+
},
|
|
182
|
+
async reportSetup(report) {
|
|
183
|
+
const response = await authorized("setup", {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { "content-type": "application/json" },
|
|
186
|
+
body: JSON.stringify(report),
|
|
187
|
+
});
|
|
188
|
+
await response.body?.cancel();
|
|
189
|
+
if (!response.ok)
|
|
190
|
+
throw new Error(`host/setup answered ${response.status}`);
|
|
191
|
+
},
|
|
192
|
+
setupFile(index, path) {
|
|
193
|
+
const suffix = path === undefined ? "" : `/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
194
|
+
return authorized(`files/${index}${suffix}`, { method: "GET" });
|
|
195
|
+
},
|
|
150
196
|
stop() {
|
|
151
197
|
clearInterval(timer);
|
|
152
198
|
},
|
|
@@ -31,7 +31,6 @@ export interface HarnessServiceOptions {
|
|
|
31
31
|
/** Delete the workspaces on stop (an ephemeral Runtime's loopback harness). */
|
|
32
32
|
readonly ephemeral?: boolean;
|
|
33
33
|
readonly logger: HarnessClientOptions["logger"];
|
|
34
|
-
readonly name?: string;
|
|
35
34
|
readonly version?: string;
|
|
36
35
|
readonly onGrant?: HarnessOptions["onGrant"];
|
|
37
36
|
readonly backoff?: HarnessClientOptions["backoff"];
|
package/dist/harness/service.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
* keeps its compute records in `<sandboxes>/records.json` and claims its events from core
|
|
10
10
|
* (`event`): for the run that made the call, or the workspace request core sent.
|
|
11
11
|
*/
|
|
12
|
+
import { fromWireOp, toWireAnswer } from "../sandbox/disk-fs.js";
|
|
12
13
|
import { join } from "node:path";
|
|
13
|
-
import { HarnessApiError, } from "@nylorun/core/harness-api";
|
|
14
|
+
import { DEFAULT_HARNESS, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
14
15
|
import { McpPool } from "../mcp/pool.js";
|
|
15
16
|
import { SandboxManager } from "../sandbox/manager.js";
|
|
16
17
|
import { fileSandboxRecords, workspacePrefix } from "../sandbox/records.js";
|
|
@@ -33,7 +34,7 @@ function refOf(session) {
|
|
|
33
34
|
return {
|
|
34
35
|
id: session.ownerId,
|
|
35
36
|
activeTurnId: session.activeTurnId,
|
|
36
|
-
...(session.
|
|
37
|
+
...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
const noModel = async () => {
|
|
@@ -78,13 +79,19 @@ export function startHarnessService(options) {
|
|
|
78
79
|
case "workspace.read":
|
|
79
80
|
case "workspace.write":
|
|
80
81
|
case "workspace.exec": {
|
|
82
|
+
if ("disk" in params) {
|
|
83
|
+
// A disk version's read or write (R2d step 3).
|
|
84
|
+
const call = params;
|
|
85
|
+
const answer = await sandbox().disk(refOf(call.session), call.spec, fromWireOp(call.disk), signal);
|
|
86
|
+
return "code" in answer ? answer : toWireAnswer(answer);
|
|
87
|
+
}
|
|
81
88
|
if (method === "workspace.read" && "list" in params) {
|
|
82
89
|
const call = params;
|
|
83
|
-
return sandbox().listFiles(refOf(call.session),
|
|
90
|
+
return sandbox().listFiles(refOf(call.session), call.spec, call.list.dir, call.list.maxEntries, signal);
|
|
84
91
|
}
|
|
85
92
|
if (method === "workspace.read" && "bytes" in params) {
|
|
86
93
|
const call = params;
|
|
87
|
-
const read = await sandbox().readBytes(refOf(call.session),
|
|
94
|
+
const read = await sandbox().readBytes(refOf(call.session), call.spec, call.bytes.path, call.bytes.maxBytes, signal);
|
|
88
95
|
return read.kind === "read"
|
|
89
96
|
? { kind: "read", path: read.path, base64: Buffer.from(read.bytes).toString("base64") }
|
|
90
97
|
: read;
|
|
@@ -92,7 +99,7 @@ export function startHarnessService(options) {
|
|
|
92
99
|
const call = params;
|
|
93
100
|
if (METHODS[call.tool] !== method)
|
|
94
101
|
throw new HarnessApiError("invalid", `${method} does not run ${call.tool}`);
|
|
95
|
-
return sandbox().run(refOf(call.session),
|
|
102
|
+
return sandbox().run(refOf(call.session), call.spec, call.tool, call.input, signal);
|
|
96
103
|
}
|
|
97
104
|
case "workspace.report":
|
|
98
105
|
return sandbox().report();
|
|
@@ -102,7 +109,7 @@ export function startHarnessService(options) {
|
|
|
102
109
|
return { workspaces: await sandbox().workspaces() };
|
|
103
110
|
}
|
|
104
111
|
case "workspace.remove": {
|
|
105
|
-
const { keys,
|
|
112
|
+
const { keys, environmentIds, all } = params;
|
|
106
113
|
const workspaces = sandbox();
|
|
107
114
|
if (all) {
|
|
108
115
|
await workspaces.reconcile(() => false, () => false);
|
|
@@ -114,7 +121,7 @@ export function startHarnessService(options) {
|
|
|
114
121
|
throw new HarnessApiError("invalid", `${key} is not a workspace of this Tenant`);
|
|
115
122
|
await workspaces.remove(key);
|
|
116
123
|
}
|
|
117
|
-
for (const id of
|
|
124
|
+
for (const id of environmentIds ?? [])
|
|
118
125
|
await workspaces.removeSandbox(id);
|
|
119
126
|
return {};
|
|
120
127
|
}
|
|
@@ -128,9 +135,10 @@ export function startHarnessService(options) {
|
|
|
128
135
|
logger,
|
|
129
136
|
executors,
|
|
130
137
|
serve,
|
|
131
|
-
|
|
138
|
+
// Our harness: its hello names our harness id (R4.1).
|
|
139
|
+
name: DEFAULT_HARNESS,
|
|
132
140
|
...(options.version ? { version: options.version } : {}),
|
|
133
|
-
|
|
141
|
+
features: { workspace: { backends: backends.map((backend) => backend.name) } },
|
|
134
142
|
onHello(answer) {
|
|
135
143
|
tenantId = answer.tenantId;
|
|
136
144
|
manager ??= new SandboxManager({
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExecRequest, ExecResult } from "../sandbox/types.js";
|
|
2
|
+
import type { PodHost } from "./pod.js";
|
|
3
|
+
/** How often a failed setup asks whether to run again. */
|
|
4
|
+
export declare const SETUP_RETRY_POLL_MS = 5000;
|
|
5
|
+
export interface DiskSetupOptions {
|
|
6
|
+
readonly host: Pick<PodHost, "setup" | "setupState" | "reportSetup" | "setupFile"> & {
|
|
7
|
+
/** The volume starts from a disk version (R2d step 3): it holds what setup made. */
|
|
8
|
+
readonly seeded?: boolean;
|
|
9
|
+
/** A fork onto other setup (Q17): run it once core has written the version. */
|
|
10
|
+
readonly afterSeed?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** Runs one command as the sandbox tools do (`localExec`). */
|
|
13
|
+
readonly exec: (request: ExecRequest, signal: AbortSignal) => Promise<ExecResult>;
|
|
14
|
+
/** The volume's id file (`/harness/volume-id`). */
|
|
15
|
+
readonly volumeFile: string;
|
|
16
|
+
/** The marker (`/harness/setup.json`). */
|
|
17
|
+
readonly markerFile: string;
|
|
18
|
+
readonly logger: {
|
|
19
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
20
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
21
|
+
};
|
|
22
|
+
readonly pollMs?: number;
|
|
23
|
+
readonly signal?: AbortSignal;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Runs the disk's setup when this volume has not had it, reports how it ended, and after a
|
|
27
|
+
* failure waits for core to ask for it again. Resolves once the setup is done (or there is none).
|
|
28
|
+
*/
|
|
29
|
+
export declare function prepareDisk(options: DiskSetupOptions): Promise<void>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The disk's setup in a pod Environment's engine (R2d step 2): after it joins and before it takes
|
|
3
|
+
* any turn, the engine copies the recipe's input files and runs its setup commands, once per
|
|
4
|
+
* volume. A marker on the volume (`/harness/setup.json`, `{ volumeId, recipeVersion, status }`)
|
|
5
|
+
* says it was done: a resume finds it and skips, a reset (a new volume, a new volume id) runs it
|
|
6
|
+
* again. Commands run as the sandbox tools' do (`localExec`): `/bin/sh -c` in `cwd`, with the
|
|
7
|
+
* Environment's variables and secrets (as the sentinel) and egress through egress-gate.
|
|
8
|
+
*
|
|
9
|
+
* A failure is reported to core, which fails the pod and the turn waiting for it; the engine then
|
|
10
|
+
* polls until a turn start asks the pod to run again, and runs the setup again from the start.
|
|
11
|
+
*
|
|
12
|
+
* A volume seeded from a disk version skips it (the version holds what setup made), except on a
|
|
13
|
+
* fork onto other setup (Q17): the engine connects first, waits until core has written the
|
|
14
|
+
* version, then runs the setup on it as above. Turns wait until it reports the setup done.
|
|
15
|
+
*/
|
|
16
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { dirname, posix } from "node:path";
|
|
19
|
+
/** How often a failed setup asks whether to run again. */
|
|
20
|
+
export const SETUP_RETRY_POLL_MS = 5_000;
|
|
21
|
+
/** How often a fork's setup asks whether its disk version is written (Q17). */
|
|
22
|
+
const AFTER_SEED_POLL_MS = 1_000;
|
|
23
|
+
/** The end of a failed command's output kept in its report. */
|
|
24
|
+
const REPORT_TAIL = 4 * 1024;
|
|
25
|
+
async function readText(path) {
|
|
26
|
+
try {
|
|
27
|
+
return (await readFile(path, "utf8")).trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Writes `bytes` at `path`, by way of a temporary name, creating its directory. */
|
|
34
|
+
async function writeAtomic(path, bytes) {
|
|
35
|
+
await mkdir(dirname(path), { recursive: true });
|
|
36
|
+
const next = `${path}.${randomUUID()}.tmp`;
|
|
37
|
+
await writeFile(next, bytes);
|
|
38
|
+
await rename(next, path);
|
|
39
|
+
}
|
|
40
|
+
/** A folder entry's path joined under `base`, refused when it would leave it. */
|
|
41
|
+
function under(base, relative) {
|
|
42
|
+
const joined = posix.normalize(posix.join(base, relative));
|
|
43
|
+
if (joined !== base && !joined.startsWith(`${base.replace(/\/$/, "")}/`))
|
|
44
|
+
throw new Error(`${relative} would leave ${base}`);
|
|
45
|
+
return joined;
|
|
46
|
+
}
|
|
47
|
+
/** Copies input file `file` into place: a file artifact at its path, a folder under it. */
|
|
48
|
+
async function copyFile(host, file) {
|
|
49
|
+
const first = await host.setupFile(file.index);
|
|
50
|
+
if (!first.ok)
|
|
51
|
+
throw new Error(`input file ${file.index + 1} answered ${first.status}`);
|
|
52
|
+
if (first.headers.get("x-nylorun-artifact-kind") !== "folder") {
|
|
53
|
+
await writeAtomic(file.path, new Uint8Array(await first.arrayBuffer()));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const { entries } = (await first.json());
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const answer = await host.setupFile(file.index, entry.path);
|
|
59
|
+
if (!answer.ok)
|
|
60
|
+
throw new Error(`input file ${file.index + 1}, ${entry.path}, answered ${answer.status}`);
|
|
61
|
+
await writeAtomic(under(file.path, entry.path), new Uint8Array(await answer.arrayBuffer()));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Runs `setup` once: files first, then commands in order; the report of how it ended. */
|
|
65
|
+
async function runOnce(options, setup) {
|
|
66
|
+
for (const file of setup.files) {
|
|
67
|
+
try {
|
|
68
|
+
await copyFile(options.host, file);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return { status: "failed", output: error instanceof Error ? error.message : String(error) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const [index, command] of setup.commands.entries()) {
|
|
75
|
+
options.logger.info("environment_setup_command", { index, cwd: command.cwd });
|
|
76
|
+
await mkdir(command.cwd, { recursive: true }).catch(() => undefined);
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await options.exec({ command: command.command, cwd: command.cwd, timeoutMs: command.timeoutMs, env: setup.environment }, options.signal ?? new AbortController().signal);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { status: "failed", index, output: error instanceof Error ? error.message : String(error) };
|
|
83
|
+
}
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
85
|
+
const output = `${result.stdout}${result.stderr ? `\n${result.stderr}` : ""}${result.timedOut ? "\n[timed out]" : ""}`;
|
|
86
|
+
return { status: "failed", index, exitCode: result.exitCode, output: output.slice(-REPORT_TAIL) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { status: "ok" };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Runs the disk's setup when this volume has not had it, reports how it ended, and after a
|
|
93
|
+
* failure waits for core to ask for it again. Resolves once the setup is done (or there is none).
|
|
94
|
+
*/
|
|
95
|
+
export async function prepareDisk(options) {
|
|
96
|
+
let setup = options.host.setup;
|
|
97
|
+
const volumeId = await readText(options.volumeFile);
|
|
98
|
+
if (options.host.afterSeed) {
|
|
99
|
+
// A fork onto other setup (Q17): wait until core has written the disk version, then run it.
|
|
100
|
+
for (;;) {
|
|
101
|
+
if (options.signal?.aborted)
|
|
102
|
+
return;
|
|
103
|
+
const state = await options.host.setupState().catch(() => undefined);
|
|
104
|
+
if (state?.retry) {
|
|
105
|
+
setup = state.setup;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? AFTER_SEED_POLL_MS));
|
|
109
|
+
}
|
|
110
|
+
if (!setup) {
|
|
111
|
+
// Nothing to run: say so, so the fork's turns may start.
|
|
112
|
+
await options.host.reportSetup({ status: "ok" }).catch(() => undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (options.host.seeded) {
|
|
117
|
+
// A volume seeded from a disk version: the version holds what setup made, so mark it done.
|
|
118
|
+
if (volumeId) {
|
|
119
|
+
const done = { volumeId, recipeVersion: setup?.recipeVersion ?? 0, status: "ok" };
|
|
120
|
+
await writeAtomic(options.markerFile, new TextEncoder().encode(`${JSON.stringify(done)}\n`));
|
|
121
|
+
}
|
|
122
|
+
options.logger.info("environment_setup_skipped", { reason: "seeded" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!setup)
|
|
126
|
+
return;
|
|
127
|
+
const marker = await readText(options.markerFile);
|
|
128
|
+
if (marker && volumeId) {
|
|
129
|
+
try {
|
|
130
|
+
const done = JSON.parse(marker);
|
|
131
|
+
if (done.volumeId === volumeId && done.status === "ok")
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
/* a broken marker: run the setup again */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (;;) {
|
|
139
|
+
if (options.signal?.aborted)
|
|
140
|
+
return;
|
|
141
|
+
options.logger.info("environment_setup_started", { recipeVersion: setup.recipeVersion });
|
|
142
|
+
const report = await runOnce(options, setup);
|
|
143
|
+
if (report.status === "ok") {
|
|
144
|
+
if (volumeId) {
|
|
145
|
+
const done = { volumeId, recipeVersion: setup.recipeVersion, status: "ok" };
|
|
146
|
+
await writeAtomic(options.markerFile, new TextEncoder().encode(`${JSON.stringify(done)}\n`));
|
|
147
|
+
}
|
|
148
|
+
await options.host.reportSetup(report).catch((error) => options.logger.warn("environment_setup_report_failed", {
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
}));
|
|
151
|
+
options.logger.info("environment_setup_done", { recipeVersion: setup.recipeVersion });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
options.logger.warn("environment_setup_failed", { index: report.index, exitCode: report.exitCode });
|
|
155
|
+
let reported = false;
|
|
156
|
+
// Report it, then wait until a turn start asks the pod to run again, and run it again.
|
|
157
|
+
for (;;) {
|
|
158
|
+
if (!reported)
|
|
159
|
+
reported = await options.host.reportSetup(report).then(() => true, () => false);
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? SETUP_RETRY_POLL_MS));
|
|
161
|
+
if (options.signal?.aborted)
|
|
162
|
+
return;
|
|
163
|
+
if (!reported)
|
|
164
|
+
continue;
|
|
165
|
+
const state = await options.host.setupState().catch(() => undefined);
|
|
166
|
+
if (!state?.retry)
|
|
167
|
+
continue;
|
|
168
|
+
if (!state.setup)
|
|
169
|
+
return;
|
|
170
|
+
setup = state.setup;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import { createChannel } from "@nylorun/core/harness-api";
|
|
10
10
|
import { createHarness } from "@nylorun/harness/api";
|
|
11
11
|
import { WebSocket } from "ws";
|
|
12
|
-
import { HARNESS_API_HEADER,
|
|
12
|
+
import { HARNESS_API_HEADER, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD } from "@nylorun/core/harness-api";
|
|
13
|
+
import { wsPort } from "./ws-port.js";
|
|
13
14
|
export function connectHarness(options) {
|
|
14
15
|
const { logger } = options;
|
|
15
16
|
const backoff = options.backoff ?? { minMs: 500, maxMs: 10_000 };
|
|
@@ -23,7 +24,7 @@ export function connectHarness(options) {
|
|
|
23
24
|
const token = typeof options.token === "string" ? options.token : await options.token();
|
|
24
25
|
return new Promise((resolve, reject) => {
|
|
25
26
|
const socket = new WebSocket(options.url, {
|
|
26
|
-
headers: { authorization: `Bearer ${token}`, [HARNESS_API_HEADER]:
|
|
27
|
+
headers: { authorization: `Bearer ${token}`, [HARNESS_API_HEADER]: HARNESS_API_TRANSPORT },
|
|
27
28
|
maxPayload: HARNESS_MAX_PAYLOAD,
|
|
28
29
|
handshakeTimeout: 10_000,
|
|
29
30
|
});
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Port } from "@nylorun/core/harness-api";
|
|
8
8
|
import { WebSocket } from "ws";
|
|
9
|
-
|
|
10
|
-
export declare const HARNESS_API_PATH = "/nylorun/harness/v1";
|
|
11
|
-
export declare const HARNESS_API_HEADER = "nylorun-harness-api";
|
|
12
|
-
/** Largest frame either end accepts. */
|
|
13
|
-
export declare const HARNESS_MAX_PAYLOAD: number;
|
|
9
|
+
export { HARNESS_API_HEADER, HARNESS_API_PATH, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD, } from "@nylorun/core/harness-api";
|
|
14
10
|
export declare const DEFAULT_PING_MS = 30000;
|
|
15
11
|
export declare const DEFAULT_SILENCE_MS = 90000;
|
|
16
12
|
export interface WsPortOptions {
|
package/dist/harness/ws-port.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { WebSocket } from "ws";
|
|
2
|
-
|
|
3
|
-
export const HARNESS_API_PATH = "/nylorun/harness/v1";
|
|
4
|
-
export const HARNESS_API_HEADER = "nylorun-harness-api";
|
|
5
|
-
/** Largest frame either end accepts. */
|
|
6
|
-
export const HARNESS_MAX_PAYLOAD = 64 * 1024 * 1024;
|
|
2
|
+
export { HARNESS_API_HEADER, HARNESS_API_PATH, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD, } from "@nylorun/core/harness-api";
|
|
7
3
|
export const DEFAULT_PING_MS = 30_000;
|
|
8
4
|
export const DEFAULT_SILENCE_MS = 90_000;
|
|
9
5
|
export function wsPort(socket, options = {}) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* SandboxManager and no store, as `--service harness` runs one.
|
|
10
10
|
*/
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
|
-
import { memoryChannels } from "@nylorun/core/harness-api";
|
|
12
|
+
import { DEFAULT_HARNESS, memoryChannels } from "@nylorun/core/harness-api";
|
|
13
13
|
import { createHarness } from "@nylorun/harness/api";
|
|
14
14
|
import { harnessExecutors } from "../harness/executors.js";
|
|
15
15
|
import { startHarnessService } from "../harness/service.js";
|
|
@@ -36,7 +36,8 @@ export async function startInProcessHarness(ctx, tools, options = {}) {
|
|
|
36
36
|
sandbox: () => tools.sandbox,
|
|
37
37
|
}),
|
|
38
38
|
logger: ctx.config.logger,
|
|
39
|
-
|
|
39
|
+
// Our harness: its hello names our harness id (R4.1).
|
|
40
|
+
name: DEFAULT_HARNESS,
|
|
40
41
|
});
|
|
41
42
|
await harness.start();
|
|
42
43
|
return {
|
|
@@ -77,7 +78,6 @@ export async function startLoopbackHarness(ctx, options = {}) {
|
|
|
77
78
|
...(ctx.config.delivery ? { delivery: ctx.config.delivery } : {}),
|
|
78
79
|
...(options.sandboxBackends ? { sandboxBackends: options.sandboxBackends } : {}),
|
|
79
80
|
logger: quietLogger(ctx),
|
|
80
|
-
name: "loopback",
|
|
81
81
|
ephemeral: ctx.config.mode === "ephemeral",
|
|
82
82
|
backoff: { minMs: 50, maxMs: 500 },
|
|
83
83
|
});
|