@nylorun/runtime 0.26.0-beta → 0.28.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 +171 -0
- package/README.md +18 -14
- package/dist/adapters/execution/restate.d.ts +15 -18
- package/dist/adapters/execution/restate.js +31 -27
- package/dist/adapters/replication/pgoutput.d.ts +1 -1
- package/dist/adapters/replication/pgoutput.js +3 -2
- package/dist/adapters/sandbox/local.js +8 -5
- package/dist/adapters/sandbox/virtual.js +2 -2
- package/dist/adapters/streams/s2.d.ts +1 -1
- package/dist/adapters/streams/s2.js +18 -14
- package/dist/api/a2a/card.d.ts +4 -3
- package/dist/api/a2a/card.js +4 -4
- package/dist/api/a2a/endpoint.js +3 -4
- package/dist/api/a2a/protocol.js +4 -0
- package/dist/api/a2a/routes.d.ts +2 -6
- package/dist/api/a2a/routes.js +30 -25
- package/dist/api/ag-ui/endpoint.js +2 -3
- package/dist/api/ag-ui/routes.js +8 -11
- package/dist/api/components.d.ts +6789 -3405
- package/dist/api/components.js +43 -30
- package/dist/api/gate/keys.js +7 -4
- package/dist/api/gate/routes.d.ts +6 -2
- package/dist/api/gate/routes.js +15 -11
- package/dist/api/http/app.js +9 -4
- package/dist/api/http/body.d.ts +17 -3
- package/dist/api/http/body.js +35 -7
- package/dist/api/http/respond.js +6 -2
- package/dist/api/http/routes/access.js +5 -4
- package/dist/api/http/routes/agent-manifests.d.ts +10 -0
- package/dist/api/http/routes/agent-manifests.js +135 -0
- package/dist/api/http/routes/agents.d.ts +11 -0
- package/dist/api/http/routes/agents.js +418 -0
- package/dist/api/http/routes/artifacts.js +21 -9
- package/dist/api/http/routes/coverage.d.ts +4 -4
- package/dist/api/http/routes/coverage.js +13 -7
- package/dist/api/http/routes/files.d.ts +2 -1
- package/dist/api/http/routes/files.js +31 -5
- package/dist/api/http/routes/mcp.js +3 -3
- package/dist/api/http/routes/me.d.ts +1 -1
- package/dist/api/http/routes/me.js +1 -2
- package/dist/api/http/routes/memories.d.ts +9 -0
- package/dist/api/http/routes/memories.js +152 -0
- package/dist/api/http/routes/reads.d.ts +10 -111
- package/dist/api/http/routes/reads.js +51 -29
- package/dist/api/http/routes/sessions.js +26 -54
- package/dist/api/http/routes/tenant.d.ts +2 -2
- package/dist/api/http/routes/tenant.js +77 -24
- package/dist/api/http/routes/triggers.d.ts +10 -0
- package/dist/api/http/routes/triggers.js +180 -0
- package/dist/api/inbound-files.d.ts +3 -9
- package/dist/api/inbound-files.js +18 -24
- package/dist/api/openapi.js +73 -47
- package/dist/artifacts/disk.d.ts +10 -14
- package/dist/artifacts/disk.js +99 -95
- package/dist/artifacts/files.d.ts +13 -4
- package/dist/artifacts/files.js +27 -9
- package/dist/artifacts/folder-upload.d.ts +75 -0
- package/dist/artifacts/folder-upload.js +263 -0
- package/dist/artifacts/folders.d.ts +14 -8
- package/dist/artifacts/folders.js +19 -11
- package/dist/artifacts/links.js +7 -6
- package/dist/artifacts/parts.d.ts +9 -14
- package/dist/artifacts/parts.js +11 -9
- package/dist/artifacts/pipe.d.ts +15 -0
- package/dist/artifacts/pipe.js +71 -0
- package/dist/artifacts/quota.d.ts +36 -0
- package/dist/artifacts/quota.js +44 -0
- package/dist/artifacts/service.d.ts +25 -1
- package/dist/artifacts/service.js +72 -27
- package/dist/blob/fs.js +2 -1
- package/dist/blob/s3.d.ts +6 -2
- package/dist/blob/s3.js +9 -7
- package/dist/blob/types.d.ts +1 -1
- package/dist/blob/types.js +1 -1
- package/dist/core/flow-host.d.ts +0 -1
- package/dist/core/limits.d.ts +11 -35
- package/dist/core/limits.js +5 -73
- package/dist/core/sandbox-routes.js +0 -1
- package/dist/core/turn-manifest.d.ts +1 -2
- package/dist/core/turn-manifest.js +4 -28
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +8 -6
- package/dist/execution/types.d.ts +1 -6
- package/dist/execution/types.js +0 -5
- package/dist/gates/contract.d.ts +3 -2
- package/dist/gates/contract.js +1 -1
- package/dist/gates/egress-credentials.js +5 -4
- package/dist/gates/egress.d.ts +13 -13
- package/dist/gates/egress.js +19 -21
- package/dist/gates/fixture-model.js +13 -2
- package/dist/gates/handler.d.ts +1 -3
- package/dist/gates/handler.js +4 -3
- package/dist/gates/http-client.d.ts +4 -3
- package/dist/gates/http-client.js +5 -7
- package/dist/gates/http-tool.d.ts +24 -4
- package/dist/gates/http-tool.js +41 -13
- package/dist/gates/in-process.d.ts +2 -1
- package/dist/gates/in-process.js +17 -9
- package/dist/gates/inflight.d.ts +2 -2
- package/dist/gates/inflight.js +3 -2
- package/dist/gates/journal.js +5 -7
- package/dist/gates/mcp-handler.d.ts +4 -3
- package/dist/gates/mcp-handler.js +15 -5
- package/dist/gates/meter.js +6 -5
- package/dist/gates/model-gate.d.ts +11 -2
- package/dist/gates/policy.d.ts +34 -6
- package/dist/gates/policy.js +66 -18
- package/dist/gates/provider-gate.d.ts +22 -0
- package/dist/gates/provider-gate.js +30 -0
- package/dist/gates/tool-calls.d.ts +1 -3
- package/dist/gates/tool-calls.js +2 -3
- package/dist/gates/tool-client.d.ts +1 -1
- package/dist/gates/tool-client.js +5 -6
- package/dist/gates/tool-contract.d.ts +8 -5
- package/dist/gates/tool-contract.js +1 -1
- package/dist/gates/tool-gate.d.ts +18 -7
- package/dist/gates/tool-gate.js +49 -7
- package/dist/harness/calls.d.ts +27 -25
- package/dist/harness/calls.js +40 -46
- package/dist/harness/executors.d.ts +10 -3
- package/dist/harness/executors.js +53 -23
- package/dist/harness/main.js +2 -2
- package/dist/harness/pod.d.ts +2 -2
- package/dist/harness/pod.js +7 -6
- package/dist/harness/run-tokens.d.ts +4 -1
- package/dist/harness/run-tokens.js +6 -2
- package/dist/harness/service.d.ts +17 -14
- package/dist/harness/service.js +21 -35
- package/dist/harness/setup.d.ts +0 -2
- package/dist/harness/setup.js +21 -15
- package/dist/harness/ws-client.d.ts +2 -2
- package/dist/harness/ws-client.js +7 -5
- package/dist/harness/ws-port.d.ts +3 -4
- package/dist/harness/ws-port.js +3 -4
- package/dist/harness-api/artifacts.d.ts +20 -0
- package/dist/harness-api/artifacts.js +478 -0
- package/dist/harness-api/in-process.d.ts +2 -2
- package/dist/harness-api/in-process.js +18 -16
- package/dist/harness-api/record.d.ts +10 -9
- package/dist/harness-api/record.js +113 -157
- package/dist/harness-api/renew.d.ts +1 -1
- package/dist/harness-api/renew.js +2 -2
- package/dist/harness-api/server.d.ts +1 -16
- package/dist/harness-api/server.js +86 -73
- package/dist/harness-api/start.d.ts +27 -5
- package/dist/harness-api/start.js +102 -19
- package/dist/harness-api/tool-journal.d.ts +15 -10
- package/dist/harness-api/tool-journal.js +26 -24
- package/dist/harness-api/workspace.d.ts +0 -4
- package/dist/harness-api/workspace.js +4 -47
- package/dist/harness-api/ws-server.js +10 -10
- package/dist/host/create-host.js +2 -3
- package/dist/host/execution.js +2 -3
- package/dist/host/gates.d.ts +2 -9
- package/dist/host/gates.js +15 -12
- package/dist/host/http.d.ts +2 -4
- package/dist/host/http.js +5 -29
- package/dist/host/main.js +10 -3
- package/dist/host/operate.js +12 -3
- package/dist/host/stack-config.d.ts +11 -0
- package/dist/host/stack-config.js +21 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/index.js +2 -1
- package/dist/infra/readiness.d.ts +0 -2
- package/dist/infra/readiness.js +4 -3
- package/dist/keys/client.d.ts +1 -0
- package/dist/keys/client.js +2 -3
- package/dist/keys/contract.d.ts +0 -2
- package/dist/keys/contract.js +0 -2
- package/dist/keys/egress-ca.d.ts +0 -2
- package/dist/keys/egress-ca.js +2 -3
- package/dist/keys/keys.d.ts +15 -4
- package/dist/keys/keys.js +22 -1
- package/dist/keys/webhooks.d.ts +25 -0
- package/dist/keys/webhooks.js +62 -0
- package/dist/keys/x509.d.ts +8 -4
- package/dist/keys/x509.js +14 -5
- package/dist/management-openapi.json +3043 -1954
- package/dist/mcp/connect.d.ts +35 -26
- package/dist/mcp/connect.js +112 -141
- package/dist/mcp/pool.d.ts +27 -22
- package/dist/mcp/pool.js +34 -48
- package/dist/mcp/preview.d.ts +12 -5
- package/dist/mcp/preview.js +43 -26
- package/dist/mcp/snapshot.d.ts +35 -86
- package/dist/mcp/snapshot.js +93 -135
- package/dist/memory/service.d.ts +97 -0
- package/dist/memory/service.js +335 -0
- package/dist/memory/tool.d.ts +36 -0
- package/dist/memory/tool.js +84 -0
- package/dist/model/auth-store.js +5 -3
- package/dist/model/capabilities.d.ts +1 -1
- package/dist/model/catalog.d.ts +9 -36
- package/dist/model/catalog.js +6 -33
- package/dist/model/descriptor.d.ts +51 -0
- package/dist/model/descriptor.js +83 -0
- package/dist/model/models.d.ts +0 -3
- package/dist/model/models.js +3 -5
- package/dist/model/native-blocks.js +1 -1
- package/dist/model/pi-model.d.ts +16 -5
- package/dist/model/pi-model.js +86 -105
- package/dist/openapi.json +13653 -9986
- package/dist/reads/cursor.js +3 -1
- package/dist/reads/types.d.ts +3 -3
- package/dist/record/agent.d.ts +27 -0
- package/dist/record/agent.js +46 -0
- package/dist/record/append.d.ts +34 -2
- package/dist/record/append.js +50 -1
- package/dist/record/index.d.ts +2 -2
- package/dist/record/index.js +2 -2
- package/dist/redact.d.ts +1 -1
- package/dist/redact.js +3 -2
- package/dist/sandbox/egress-token.d.ts +3 -3
- package/dist/sandbox/egress-token.js +3 -3
- package/dist/sandbox/join.js +8 -7
- package/dist/sandbox/manager.d.ts +41 -41
- package/dist/sandbox/manager.js +112 -44
- package/dist/sandbox/placement.d.ts +2 -2
- package/dist/sandbox/placement.js +7 -10
- package/dist/sandbox/pods/client.d.ts +1 -10
- package/dist/sandbox/pods/client.js +8 -4
- package/dist/sandbox/pods/lifecycle.d.ts +25 -17
- package/dist/sandbox/pods/lifecycle.js +51 -29
- package/dist/sandbox/pods/network-gate.d.ts +4 -5
- package/dist/sandbox/pods/network-gate.js +7 -7
- package/dist/sandbox/pods/reconcile.js +14 -3
- package/dist/sandbox/pods/spec.js +8 -5
- package/dist/sandbox/policy.d.ts +7 -1
- package/dist/sandbox/policy.js +10 -3
- package/dist/sandbox/resolve.d.ts +11 -6
- package/dist/sandbox/resolve.js +19 -9
- package/dist/sandbox/setup.d.ts +6 -2
- package/dist/sandbox/setup.js +47 -14
- package/dist/sandbox/share.d.ts +5 -14
- package/dist/sandbox/share.js +5 -34
- package/dist/sandbox/skills.d.ts +7 -13
- package/dist/sandbox/skills.js +16 -31
- package/dist/sandbox/tenant-config.d.ts +2 -2
- package/dist/sandbox/tenant-config.js +4 -3
- package/dist/sandbox/tools.d.ts +7 -1
- package/dist/sandbox/tools.js +26 -26
- package/dist/sandbox/types.d.ts +10 -3
- package/dist/sandbox/types.js +9 -0
- package/dist/store/postgres/connect.d.ts +6 -5
- package/dist/store/postgres/connect.js +14 -11
- package/dist/store/postgres/control.js +13 -13
- package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
- package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
- package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
- package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -0
- package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
- package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
- package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
- package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +63 -0
- package/dist/store/postgres/reads.js +17 -82
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/schema.d.ts +1382 -151
- package/dist/store/postgres/schema.js +191 -37
- package/dist/store/postgres/store.js +384 -105
- package/dist/store/postgres/tenant.js +1 -2
- package/dist/store/types.d.ts +172 -57
- package/dist/store/types.js +0 -1
- package/dist/streams/relay/core.d.ts +3 -3
- package/dist/streams/relay/core.js +9 -10
- package/dist/tenant/advance.js +41 -39
- package/dist/tenant/agent-manifests.d.ts +72 -0
- package/dist/tenant/agent-manifests.js +359 -0
- package/dist/tenant/agent-versions.d.ts +126 -0
- package/dist/tenant/agent-versions.js +389 -0
- package/dist/tenant/agents.d.ts +230 -0
- package/dist/tenant/{environments.js → agents.js} +493 -328
- package/dist/tenant/auth.d.ts +1 -1
- package/dist/tenant/auth.js +3 -3
- package/dist/tenant/commands.d.ts +63 -2
- package/dist/tenant/commands.js +240 -90
- package/dist/tenant/context.d.ts +57 -33
- package/dist/tenant/context.js +6 -2
- package/dist/tenant/definition-files.d.ts +25 -4
- package/dist/tenant/definition-files.js +85 -47
- package/dist/tenant/effects.js +15 -51
- package/dist/tenant/ephemeral.js +2 -1
- package/dist/tenant/history.d.ts +1 -1
- package/dist/tenant/history.js +1 -1
- package/dist/tenant/host-token.d.ts +3 -3
- package/dist/tenant/host-token.js +11 -10
- package/dist/tenant/http.d.ts +5 -8
- package/dist/tenant/http.js +9 -0
- package/dist/tenant/identity-file.d.ts +29 -6
- package/dist/tenant/identity-file.js +45 -15
- package/dist/tenant/issuers.d.ts +5 -5
- package/dist/tenant/issuers.js +34 -26
- package/dist/tenant/jwt.d.ts +8 -2
- package/dist/tenant/jwt.js +10 -2
- package/dist/tenant/limits.d.ts +87 -0
- package/dist/tenant/limits.js +172 -0
- package/dist/tenant/memories.d.ts +38 -0
- package/dist/tenant/memories.js +119 -0
- package/dist/tenant/module.d.ts +4 -1
- package/dist/tenant/module.js +9 -8
- package/dist/tenant/outbound.d.ts +11 -14
- package/dist/tenant/outbound.js +10 -15
- package/dist/tenant/reset.js +2 -2
- package/dist/tenant/run-grants.d.ts +6 -0
- package/dist/tenant/run-grants.js +7 -2
- package/dist/tenant/run-token.d.ts +0 -4
- package/dist/tenant/run-token.js +6 -11
- package/dist/tenant/runtime-limits.d.ts +24 -0
- package/dist/tenant/runtime-limits.js +27 -0
- package/dist/tenant/runtime.d.ts +1 -1
- package/dist/tenant/runtime.js +38 -43
- package/dist/tenant/session-streams.js +7 -6
- package/dist/tenant/session.d.ts +1 -1
- package/dist/tenant/session.js +1 -1
- package/dist/tenant/sessions.d.ts +28 -45
- package/dist/tenant/sessions.js +89 -209
- package/dist/tenant/signing-keys.d.ts +1 -1
- package/dist/tenant/signing-keys.js +5 -6
- package/dist/tenant/status.js +10 -10
- package/dist/tenant/streams.js +18 -20
- package/dist/tenant/sweep.d.ts +1 -21
- package/dist/tenant/sweep.js +51 -51
- package/dist/tenant/transcript.d.ts +70 -23
- package/dist/tenant/transcript.js +28 -22
- package/dist/tenant/triggers.d.ts +143 -0
- package/dist/tenant/triggers.js +715 -0
- package/dist/tenant/turn-timing.d.ts +11 -1
- package/dist/tenant/turn-timing.js +3 -1
- package/dist/tenant/types.d.ts +9 -14
- package/dist/tenant/worker.d.ts +4 -10
- package/dist/tenant/worker.js +10 -14
- package/dist/traces/ids.js +5 -1
- package/dist/traces/map.d.ts +10 -7
- package/dist/traces/map.js +16 -12
- package/dist/traces/otlp.d.ts +3 -3
- package/dist/traces/otlp.js +5 -3
- package/dist/traces/projector.d.ts +9 -4
- package/dist/traces/projector.js +13 -7
- package/dist/traces/read.js +6 -1
- package/dist/tuning.d.ts +390 -0
- package/dist/tuning.js +677 -0
- package/dist/vault/headers.js +4 -2
- package/dist/vault/service.d.ts +13 -8
- package/dist/vault/service.js +61 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -12
- package/dist/api/http/routes/environment-manifests.d.ts +0 -9
- package/dist/api/http/routes/environment-manifests.js +0 -118
- package/dist/api/http/routes/environments.d.ts +0 -11
- package/dist/api/http/routes/environments.js +0 -332
- package/dist/artifacts/export.d.ts +0 -8
- package/dist/artifacts/export.js +0 -204
- package/dist/artifacts/media-types.d.ts +0 -30
- package/dist/artifacts/media-types.js +0 -137
- package/dist/artifacts/workspace.d.ts +0 -60
- package/dist/artifacts/workspace.js +0 -45
- package/dist/media.d.ts +0 -10
- package/dist/media.js +0 -58
- package/dist/record/environment.d.ts +0 -25
- package/dist/record/environment.js +0 -44
- package/dist/sandbox/runtime-tools.d.ts +0 -9
- package/dist/sandbox/runtime-tools.js +0 -16
- package/dist/sandbox/session-sandbox.d.ts +0 -16
- package/dist/sandbox/session-sandbox.js +0 -77
- package/dist/tenant/artifact-tool.d.ts +0 -13
- package/dist/tenant/artifact-tool.js +0 -243
- package/dist/tenant/environment-manifests.d.ts +0 -57
- package/dist/tenant/environment-manifests.js +0 -200
- package/dist/tenant/environments.d.ts +0 -174
- package/dist/tenant/skill-tool.d.ts +0 -20
- package/dist/tenant/skill-tool.js +0 -67
- package/dist/tenant/tool-results.d.ts +0 -57
- package/dist/tenant/tool-results.js +0 -298
- package/dist/tenant/tool-search.d.ts +0 -39
- package/dist/tenant/tool-search.js +0 -103
|
@@ -1,49 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Agents (protocol 16; Environments from protocol 11, D55, D56): `PUT`, `GET` and `DELETE
|
|
3
|
+
* /v1/agents/{id}`, the list, stop, reset, fork and the lifecycle stream; sessions on one, and the
|
|
4
|
+
* checks every turn start makes on it.
|
|
5
5
|
*
|
|
6
|
-
* - **
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
6
|
+
* - **The instance of a manifest.** An Agent is made from one version of a saved agent manifest
|
|
7
|
+
* (`manifest: "name"`, tracking, or `"name@n"`, pinned; `agent-manifests.ts`) and runs one
|
|
8
|
+
* version of it at a time (`resolvedVersion`). A `PUT` on an existing id may change its labels
|
|
9
|
+
* and owner guard, and moves it to the version its `manifest` names (track T3,
|
|
10
|
+
* `agent-versions.ts`; `?dryRun=true` answers the plan); a save moves every Agent that tracks
|
|
11
|
+
* the manifest. Its manifest's name, kind, harness and storage are fixed for its life: a fork
|
|
12
|
+
* makes a new Agent to change them. Its kind is its manifest's `runtime.kind` (`none` without a
|
|
13
|
+
* runtime section).
|
|
14
|
+
* - **No scope, no main session** (D70, D71). The Runtime decides nothing about who uses an
|
|
15
|
+
* Agent or which sessions it has: the app opens sessions on it with ids it picks, and deleting
|
|
16
|
+
* a session (a sessions reset) only detaches it.
|
|
17
|
+
* - **Its environment: one sandbox, one vault.** Kind `virtual` (a just-bash workspace in the
|
|
18
|
+
* harness container, keyed by the Agent id and created on the first tool call), `pod` (an
|
|
19
|
+
* agent-sandbox pod on the Tenant's cluster, Host feature `sandbox-pods`, created at once) or
|
|
20
|
+
* `none` (no compute). Each has one vault (`vault/service.ts`), created and deleted with it in
|
|
21
|
+
* one transaction. The store calls this part the Agent's environment (`Environment`, keyed by
|
|
22
|
+
* the Agent's id).
|
|
23
|
+
* - **State on read.** `state` is derived when it is read, the first rule that holds: `paused`
|
|
24
|
+
* (desired `off`, track T6), `failing` (its compute failed or was lost, or a session's last
|
|
25
|
+
* turn failed), `input_required` (a session waits on a person), `working` (a turn runs),
|
|
26
|
+
* `sleeping`; `attention` names the sessions behind each that holds (`stateOf`).
|
|
27
|
+
* - **Secret slots** (D64): when its manifest declares `secrets`, a secret write sends only the
|
|
22
28
|
* value, the binding comes from the slot, and a name it does not declare is
|
|
23
29
|
* `409 secret_not_in_manifest` (`secretRequestFor`).
|
|
24
|
-
* - **Access.** An application key reaches every
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* - **The owner guard.** When `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* - **Parallel turns (D66).** Sessions on one
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* (`409 environment_busy`), and a turn waits while a seed copies a disk.
|
|
30
|
+
* - **Access.** An application key reaches every Agent, and so does an application key acting
|
|
31
|
+
* for a subject (`Nylorun-Subject`), which needs `agents:write` to change one. A token caller
|
|
32
|
+
* reaches only the ids its Agent grants match, and changes them only with `agents:write`; any
|
|
33
|
+
* other id is the 404 of a missing one.
|
|
34
|
+
* - **The owner guard.** When `owner` is set, only that owner's sessions may run on the Agent,
|
|
35
|
+
* checked when a session opens and at every turn start (`403 agent_owner_mismatch`; the opaque
|
|
36
|
+
* 404 to a caller acting for a person).
|
|
37
|
+
* - **Parallel turns (D66).** Sessions on one Agent run their turns at the same time and share
|
|
38
|
+
* its `/workspace`; one turn per session stays. Disk work (snapshot, restore, stop, reset)
|
|
39
|
+
* waits until no turn runs (`409 agent_busy`), and a turn waits while a seed copies a disk.
|
|
35
40
|
* - **Lifecycle.** `POST .../stop` suspends a pod (the volume is kept; the next turn starts it),
|
|
36
|
-
* `POST .../reset` gives it a new pod on a new volume. A lost pod refuses turns
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* - **Lifecycle events** go to the
|
|
41
|
-
* `record/
|
|
41
|
+
* `POST .../reset` gives it a new pod on a new volume. A lost pod refuses turns (`agent_lost`)
|
|
42
|
+
* until a reset. `lifecycle.deleteAfter` deletes an Agent unused for that long (the Tenant
|
|
43
|
+
* sweep, `sweepUnusedAgents`). Deleting a pod Agent keeps its row (`desired: deleted`, invisible
|
|
44
|
+
* here) until its Sandbox is gone; its vault goes at once.
|
|
45
|
+
* - **Lifecycle events** go to the Agent's own stream in the record (`Tx.agentEvent`,
|
|
46
|
+
* `record/agent.ts`); the session's log records `agent.attached` too.
|
|
42
47
|
*/
|
|
43
48
|
import { randomUUID } from "node:crypto";
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import { isWorkflowManifest, parseSandboxDuration } from "@nylorun/core/define";
|
|
49
|
+
import { agentGranted, parseAgentManifestRef, PutAgentSecretRequestSchema, PutAgentSecretValueRequestSchema, } from "@nylorun/core/contracts";
|
|
50
|
+
import { isWorkflowManifest, parseSandboxDuration, runtimeOf } from "@nylorun/core/define";
|
|
47
51
|
import { canonical } from "../store/canonical.js";
|
|
48
52
|
import { resolveRecipe } from "../sandbox/resolve.js";
|
|
49
53
|
import { sandboxWorkspaceKey } from "../sandbox/records.js";
|
|
@@ -53,12 +57,18 @@ import { podLifecycleConfig } from "../sandbox/pods/spec.js";
|
|
|
53
57
|
import { SETUP_FAILED } from "../sandbox/pods/lifecycle.js";
|
|
54
58
|
import { requirePods } from "../sandbox/placement.js";
|
|
55
59
|
import { deleteDiskArtifact, forkSeedOf, runPendingSeed, snapshotEnvironment } from "../artifacts/disk.js";
|
|
56
|
-
import { manifestChanges, manifestRecipeRequest } from "./
|
|
60
|
+
import { manifestChanges, manifestRecipeRequest } from "./agent-manifests.js";
|
|
61
|
+
import { idleMsOf, moveAgent, planMove, resumeMove, switchTo, waitingFor } from "./agent-versions.js";
|
|
62
|
+
import { slotHolds } from "../vault/service.js";
|
|
63
|
+
import { labelIssues, limitIssues, limitsInForce, manifestShapeIssues, readTenantLimits, secretRequestIssues, } from "./limits.js";
|
|
64
|
+
import { readRuntimeLimits } from "./runtime-limits.js";
|
|
65
|
+
import { armAgentTriggers, createAgentTriggers } from "./triggers.js";
|
|
57
66
|
import { harnessOf, sessionOf } from "./context.js";
|
|
58
|
-
import { fail } from "./http.js";
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
import { fail, failRetryLater } from "./http.js";
|
|
68
|
+
import { hostTuning } from "../tuning.js";
|
|
69
|
+
/** The Agent grants that limit `scope`, or undefined when it reaches every Agent. */
|
|
70
|
+
export function agentGrantsOf(scope) {
|
|
71
|
+
return scope.kind === "token" && scope.agents !== "*" ? [...scope.agents] : undefined;
|
|
62
72
|
}
|
|
63
73
|
/** The subject a request acts for, when it acts for one. */
|
|
64
74
|
function subjectOf(scope) {
|
|
@@ -68,21 +78,21 @@ function subjectOf(scope) {
|
|
|
68
78
|
export function actorOf(scope) {
|
|
69
79
|
return subjectOf(scope) ?? "application";
|
|
70
80
|
}
|
|
71
|
-
const notFound = () => fail(404, "
|
|
72
|
-
/** The
|
|
81
|
+
const notFound = () => fail(404, "Agent not found");
|
|
82
|
+
/** The Agent, when `grants` reach it; otherwise the 404 of a missing one. */
|
|
73
83
|
function reachable(grants, id) {
|
|
74
|
-
if (grants !== undefined && !
|
|
84
|
+
if (grants !== undefined && !agentGranted(grants, id))
|
|
75
85
|
notFound();
|
|
76
86
|
}
|
|
77
|
-
/** Refuses (the 404 of a missing one)
|
|
87
|
+
/** Refuses (the 404 of a missing one) Agent `id` when the caller's grants miss it. */
|
|
78
88
|
export function assertReaches(scope, id) {
|
|
79
|
-
reachable(
|
|
89
|
+
reachable(agentGrantsOf(scope), id);
|
|
80
90
|
}
|
|
81
|
-
/** A pod
|
|
91
|
+
/** A pod Agent being deleted is no longer an Agent, here. */
|
|
82
92
|
function live(row) {
|
|
83
93
|
return row?.pod?.desired === "deleted" ? undefined : row;
|
|
84
94
|
}
|
|
85
|
-
/** An
|
|
95
|
+
/** An Agent's row, unless it is gone or being deleted. */
|
|
86
96
|
export async function liveEnvironment(t, id, options = {}) {
|
|
87
97
|
return live(await t.environment(id, options));
|
|
88
98
|
}
|
|
@@ -94,110 +104,147 @@ function podState(pod) {
|
|
|
94
104
|
return "creating";
|
|
95
105
|
return "stopped";
|
|
96
106
|
}
|
|
107
|
+
/** The order the states rank in: the first that holds is the Agent's. */
|
|
108
|
+
const STATE_RANK = ["paused", "failing", "input_required", "working", "sleeping"];
|
|
109
|
+
/**
|
|
110
|
+
* An Agent's state (protocol 16), from its desired state, its compute and its sessions, and
|
|
111
|
+
* every state that holds with the session behind it (`attention`), highest first. A session waits
|
|
112
|
+
* on a person while it is paused (an approval or an interaction) or uncertain, and the Agent while
|
|
113
|
+
* a move waits for its required secrets (track T3); a session's last turn
|
|
114
|
+
* failed while it is `failed`; a turn runs while it is `running`, `runnable` or `waiting`.
|
|
115
|
+
*/
|
|
116
|
+
export function stateOf(agent, sessions) {
|
|
117
|
+
const attention = [];
|
|
118
|
+
if (agent.desiredState === "off")
|
|
119
|
+
attention.push({ state: "paused", reason: "desired off" });
|
|
120
|
+
// A move that waits for a person to set its required secrets (track T3).
|
|
121
|
+
if (agent.targetVersion !== undefined && agent.appliedVersion === agent.version)
|
|
122
|
+
attention.push({ state: "input_required", reason: `secret_required: version ${agent.targetVersion} waits for its required secrets` });
|
|
123
|
+
const pod = agent.pod;
|
|
124
|
+
if (pod && (pod.observed === "failed" || pod.observed === "lost") && pod.desired !== "deleted")
|
|
125
|
+
attention.push({ state: "failing", reason: `compute ${pod.observed}${pod.reason ? `: ${pod.reason}` : ""}` });
|
|
126
|
+
// A start on a new version failed and went back (track T3): until a start on a new one succeeds.
|
|
127
|
+
if (agent.failedVersion !== undefined)
|
|
128
|
+
attention.push({ state: "failing", reason: `start_failed: version ${agent.failedVersion} did not start` });
|
|
129
|
+
for (const session of sessions) {
|
|
130
|
+
const status = String(session.status);
|
|
131
|
+
const state = status === "failed"
|
|
132
|
+
? "failing"
|
|
133
|
+
: status === "paused" || status === "uncertain"
|
|
134
|
+
? "input_required"
|
|
135
|
+
: status === "running" || status === "runnable" || status === "waiting"
|
|
136
|
+
? "working"
|
|
137
|
+
: undefined;
|
|
138
|
+
if (state)
|
|
139
|
+
attention.push({ state, sessionId: session.id, reason: status });
|
|
140
|
+
}
|
|
141
|
+
attention.sort((a, b) => STATE_RANK.indexOf(a.state) - STATE_RANK.indexOf(b.state));
|
|
142
|
+
return { state: attention[0]?.state ?? "sleeping", attention };
|
|
143
|
+
}
|
|
144
|
+
/** How an Agent names its manifest: `name` when it tracks it, `name@n` when pinned. */
|
|
145
|
+
export function manifestRefOf(agent) {
|
|
146
|
+
return agent.manifestRef === "tracking" ? agent.manifestName : `${agent.manifestName}@${agent.manifestVersion}`;
|
|
147
|
+
}
|
|
97
148
|
async function viewOf(ctx, t, environment, owner) {
|
|
98
149
|
const sessions = await t.sessionsOnEnvironment(environment.id);
|
|
99
150
|
const record = environment.kind === "virtual"
|
|
100
151
|
? await t.get("sandboxes", sandboxWorkspaceKey(ctx.config.tenantId, environment.id))
|
|
101
152
|
: undefined;
|
|
102
153
|
const pod = environment.pod;
|
|
103
|
-
const
|
|
154
|
+
const visible = sessions.filter((session) => owner === undefined || session.ownerUserId === owner);
|
|
155
|
+
const { state, attention } = stateOf(environment, visible);
|
|
104
156
|
return {
|
|
105
157
|
id: environment.id,
|
|
158
|
+
manifest: manifestRefOf(environment),
|
|
159
|
+
resolvedVersion: environment.manifestVersion,
|
|
160
|
+
manifestHash: environment.manifestHash,
|
|
161
|
+
tracking: environment.manifestRef,
|
|
162
|
+
targetVersion: environment.targetVersion ?? null,
|
|
163
|
+
waitingFor: waitingFor(environment),
|
|
164
|
+
failedVersion: environment.failedVersion ?? null,
|
|
165
|
+
startCheck: environment.startCheck
|
|
166
|
+
? { from: environment.startCheck.from.manifestVersion, startedAt: environment.startCheck.startedAt }
|
|
167
|
+
: null,
|
|
106
168
|
kind: environment.kind,
|
|
169
|
+
harness: environment.applied.harness,
|
|
170
|
+
owner: environment.ownerUserId ?? null,
|
|
107
171
|
labels: environment.labels,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
172
|
+
state,
|
|
173
|
+
attention,
|
|
174
|
+
desired: environment.desiredState,
|
|
175
|
+
nextWakeAt: await t.nextWakeAt(environment.id),
|
|
176
|
+
compute: {
|
|
177
|
+
state: pod ? podState(pod) : environment.kind === "none" ? "ready" : (record?.state ?? "ready"),
|
|
178
|
+
...(pod
|
|
179
|
+
? {
|
|
180
|
+
pod: {
|
|
181
|
+
desired: pod.desired,
|
|
182
|
+
observed: pod.observed,
|
|
183
|
+
volumeGeneration: pod.volumeGen,
|
|
184
|
+
hostEpoch: pod.hostEpoch,
|
|
185
|
+
...(pod.reason ? { reason: pod.reason } : {}),
|
|
186
|
+
...(pod.imageDigest ? { imageDigest: pod.imageDigest } : {}),
|
|
187
|
+
},
|
|
188
|
+
}
|
|
189
|
+
: {}),
|
|
190
|
+
},
|
|
191
|
+
runtime: environment.applied,
|
|
192
|
+
limits: limitsInForce(await readTenantLimits(t), environment.applied.limits),
|
|
193
|
+
sessions: visible.map((session) => ({
|
|
194
|
+
id: session.id,
|
|
195
|
+
ownerUserId: session.ownerUserId,
|
|
196
|
+
status: String(session.status),
|
|
197
|
+
activeTurnId: session.activeTurnId,
|
|
198
|
+
})),
|
|
199
|
+
slots: (await slotsOf(t, environment)) ?? [],
|
|
131
200
|
...(environment.kind === "none"
|
|
132
201
|
? {}
|
|
133
202
|
: {
|
|
134
203
|
disk: {
|
|
135
204
|
latestVersion: (await t.diskArtifact(environment.id))?.latestVersion || null,
|
|
136
|
-
|
|
205
|
+
operation: diskHeld(environment)
|
|
137
206
|
? {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
startedAt: environment.diskOp.startedAt,
|
|
142
|
-
},
|
|
207
|
+
kind: environment.diskOp.kind,
|
|
208
|
+
...(environment.diskOp.version === undefined ? {} : { version: environment.diskOp.version }),
|
|
209
|
+
startedAt: environment.diskOp.startedAt,
|
|
143
210
|
}
|
|
144
|
-
:
|
|
211
|
+
: null,
|
|
145
212
|
},
|
|
146
213
|
}),
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
.map((session) => ({ id: session.id, activeTurnId: session.activeTurnId })),
|
|
214
|
+
forkedFrom: environment.forkedFrom ?? null,
|
|
215
|
+
lastUsedAt: environment.lastUsedAt,
|
|
150
216
|
createdAt: environment.createdAt,
|
|
151
217
|
updatedAt: environment.updatedAt,
|
|
152
218
|
};
|
|
153
219
|
}
|
|
154
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* The secret slots a secret write fills (D64): the version `environment` moves to while it is
|
|
222
|
+
* outdated (track T3), so its new slots can be set before it applies, else the one it runs.
|
|
223
|
+
*/
|
|
224
|
+
async function slotsAhead(t, environment) {
|
|
225
|
+
if (environment.targetVersion === undefined)
|
|
226
|
+
return environment.applied.secrets;
|
|
227
|
+
// Waiting for its pod's start, `recipe` is the target's runtime.
|
|
228
|
+
if (environment.appliedVersion !== environment.version)
|
|
229
|
+
return environment.recipe.secrets;
|
|
230
|
+
const target = await t.agentManifest(environment.manifestName, environment.targetVersion);
|
|
231
|
+
return target ? runtimeOf(target.body).secrets : environment.applied.secrets;
|
|
232
|
+
}
|
|
233
|
+
/** Each secret slot `environment` declares (`slotsAhead`), and whether its vault holds a value that fills it. */
|
|
155
234
|
async function slotsOf(t, environment) {
|
|
156
|
-
const declared = environment
|
|
235
|
+
const declared = await slotsAhead(t, environment);
|
|
157
236
|
if (declared === undefined)
|
|
158
237
|
return undefined;
|
|
159
238
|
const vault = await t.vaultOfEnvironment(environment.id);
|
|
160
|
-
const
|
|
239
|
+
const rows = vault ? await t.credentialsForVault(vault.id) : [];
|
|
161
240
|
return Object.entries(declared)
|
|
162
241
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
163
|
-
.map(([name, slot]) => ({
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"resources",
|
|
170
|
-
"storage",
|
|
171
|
-
"network",
|
|
172
|
-
"harness",
|
|
173
|
-
"lifecycle",
|
|
174
|
-
"setup",
|
|
175
|
-
"files",
|
|
176
|
-
"secrets",
|
|
177
|
-
];
|
|
178
|
-
/** The hash of what an Environment runs, recorded when it is made. */
|
|
179
|
-
function recipeHash(recipe) {
|
|
180
|
-
return hashManifest(recipe);
|
|
181
|
-
}
|
|
182
|
-
/** What a `PUT` asks of the recipe: its fields over `existing`'s (fixed `storage` kept). */
|
|
183
|
-
function recipeRequest(body, existing) {
|
|
184
|
-
const recipe = existing?.recipe;
|
|
185
|
-
const pick = (key, sent) => sent !== undefined ? { [key]: sent } : recipe?.[key] !== undefined ? { [key]: recipe[key] } : {};
|
|
186
|
-
return {
|
|
187
|
-
...pick("image", body.image),
|
|
188
|
-
...pick("resources", body.resources),
|
|
189
|
-
...(recipe?.storage !== undefined
|
|
190
|
-
? { storage: recipe.storage }
|
|
191
|
-
: body.storage !== undefined
|
|
192
|
-
? { storage: body.storage }
|
|
193
|
-
: {}),
|
|
194
|
-
...pick("network", body.network),
|
|
195
|
-
...pick("harness", body.harness),
|
|
196
|
-
...pick("lifecycle", body.lifecycle),
|
|
197
|
-
...pick("setup", body.setup),
|
|
198
|
-
...pick("files", body.files),
|
|
199
|
-
...pick("secrets", body.secrets),
|
|
200
|
-
};
|
|
242
|
+
.map(([name, slot]) => ({
|
|
243
|
+
name,
|
|
244
|
+
type: slot.type,
|
|
245
|
+
required: slot.required ?? false,
|
|
246
|
+
set: rows.some((row) => row.name === name && slotHolds(slot, row)),
|
|
247
|
+
}));
|
|
201
248
|
}
|
|
202
249
|
/**
|
|
203
250
|
* Why input files cannot be used: each that is not a version of a Tenant-wide artifact (a
|
|
@@ -235,7 +282,7 @@ function checkGuard(scope, asked, current) {
|
|
|
235
282
|
if (subject === undefined || asked === undefined)
|
|
236
283
|
return;
|
|
237
284
|
if (asked === null ? (current !== undefined && current !== subject) : asked !== subject)
|
|
238
|
-
fail(403, "A request acting for a person may set
|
|
285
|
+
fail(403, "A request acting for a person may set owner only to that person.");
|
|
239
286
|
}
|
|
240
287
|
/** Asks for a pod's reconcile once the transaction commits (the sandbox signal outbox). */
|
|
241
288
|
function reconcileAfter(t, id) {
|
|
@@ -262,7 +309,8 @@ async function wantRunning(t, environment, now) {
|
|
|
262
309
|
await t.updateEnvironmentPod(environment.id, { observed: "creating", startedAt: now, lastActiveAt: now, reason: null }, now);
|
|
263
310
|
return;
|
|
264
311
|
}
|
|
265
|
-
|
|
312
|
+
// A move whose runtime waits for this start switches now (track T3).
|
|
313
|
+
if (!(await switchTo(t, environment, now)) && environment.appliedVersion !== environment.version)
|
|
266
314
|
await t.updateEnvironment(environment.id, { applied: environment.recipe, appliedVersion: environment.version }, now);
|
|
267
315
|
await t.updateEnvironmentPod(environment.id, {
|
|
268
316
|
desired: "running",
|
|
@@ -274,78 +322,48 @@ async function wantRunning(t, environment, now) {
|
|
|
274
322
|
}, now);
|
|
275
323
|
}
|
|
276
324
|
/**
|
|
277
|
-
* `PUT /v1/
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
325
|
+
* `PUT /v1/agents/{id}`: creates the Agent and its vault (within the Tenant's limit on their
|
|
326
|
+
* number) from a saved agent manifest's version, or finds the one with this id, so "get or
|
|
327
|
+
* create" is one call. On an existing one, its labels and owner guard may change, and `manifest`
|
|
328
|
+
* moves it to the version it names (track T3, `moveAgent`): `name` tracks the latest (unless that
|
|
329
|
+
* failed to start), `name@n` pins `n`. Another manifest name is `409 agent_fixed_field`. Every
|
|
330
|
+
* `PUT` counts as the Agent's use.
|
|
282
331
|
*/
|
|
283
|
-
export async function
|
|
332
|
+
export async function putAgent(ctx, id, body, scope, options = {}) {
|
|
284
333
|
const fork = options.fork;
|
|
285
|
-
reachable(
|
|
286
|
-
const ref =
|
|
287
|
-
|
|
288
|
-
if (ref === undefined)
|
|
289
|
-
fail(400, "manifest is an environment manifest's name, or name@version such as analyst-box@3.");
|
|
290
|
-
const inline = INLINE_FIELDS.filter((field) => body[field] !== undefined);
|
|
291
|
-
if (inline.length > 0)
|
|
292
|
-
fail(400, `An Environment made from manifest ${body.manifest} runs what the manifest says: send no ${inline.join(", ")} with it.`);
|
|
293
|
-
}
|
|
294
|
-
if (body.kind === "pod")
|
|
295
|
-
await requirePods(ctx.pods, "A pod Environment");
|
|
296
|
-
const immutable = (fields, existing) => fail(409, `Environment ${id} exists and never changes what it runs (${fields.join(", ")} differ${fields.length === 1 ? "s" : ""}): fork it into a new Environment with POST /v1/environments/${encodeURIComponent(id)}/fork, or use another id.`, {
|
|
297
|
-
code: "environment_immutable",
|
|
298
|
-
details: {
|
|
299
|
-
fields: [...fields],
|
|
300
|
-
...(existing.manifestName === undefined
|
|
301
|
-
? {}
|
|
302
|
-
: { manifest: `${existing.manifestName}@${existing.manifestVersion}` }),
|
|
303
|
-
},
|
|
304
|
-
});
|
|
334
|
+
reachable(agentGrantsOf(scope), id);
|
|
335
|
+
const ref = parseAgentManifestRef(body.manifest) ??
|
|
336
|
+
fail(400, "manifest is an agent manifest's name, or name@version such as triage@3.");
|
|
305
337
|
const view = await ctx.store.tx(async (t) => {
|
|
306
338
|
const now = new Date().toISOString();
|
|
307
339
|
const config = effectiveSandboxConfig(await readSandboxConfig(t));
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
340
|
+
const tenantLimits = await readTenantLimits(t);
|
|
341
|
+
const runtimeLimits = await readRuntimeLimits(t);
|
|
342
|
+
// Labels are checked whenever they are set; what it runs only when it is made, so a
|
|
343
|
+
// get-or-create on one made before the Tenant lowered a limit still answers.
|
|
344
|
+
const labels = labelIssues(body.labels, runtimeLimits);
|
|
345
|
+
if (labels.length > 0)
|
|
346
|
+
fail(400, labels.join(" "));
|
|
312
347
|
const deleting = (row) => {
|
|
313
348
|
if (row && !live(row))
|
|
314
|
-
|
|
315
|
-
code: "environment_busy",
|
|
316
|
-
details: { retryAfterSeconds: 5 },
|
|
317
|
-
}, { "retry-after": "5" });
|
|
349
|
+
failRetryLater(`Agent ${id} is being deleted. Try again once it is gone.`, "agent_busy");
|
|
318
350
|
};
|
|
319
351
|
const found = async (existing) => {
|
|
320
352
|
if (fork !== undefined)
|
|
321
|
-
fail(409, `
|
|
353
|
+
fail(409, `Agent ${id} exists: a fork makes a new Agent, so name another id.`);
|
|
322
354
|
if (body.from !== undefined)
|
|
323
|
-
fail(409, `
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
else if (INLINE_FIELDS.some((field) => body[field] !== undefined)) {
|
|
332
|
-
if (body.kind !== undefined && body.kind !== existing.kind)
|
|
333
|
-
immutable(["kind"], existing);
|
|
334
|
-
if (body.storage !== undefined && body.storage !== existing.recipe.storage)
|
|
335
|
-
immutable(["storage"], existing);
|
|
336
|
-
const recipe = resolve(existing.kind, recipeRequest(body, existing));
|
|
337
|
-
const differing = Object.keys({ ...recipe, ...existing.recipe })
|
|
338
|
-
.filter((field) => canonical(recipe[field]) !== canonical(existing.recipe[field]))
|
|
339
|
-
.sort();
|
|
340
|
-
if (differing.length > 0)
|
|
341
|
-
immutable(differing, existing);
|
|
342
|
-
}
|
|
343
|
-
checkGuard(scope, body.ownerUserId, existing.ownerUserId);
|
|
355
|
+
fail(409, `Agent ${id} exists: from applies only when a PUT creates one.`);
|
|
356
|
+
checkGuard(scope, body.owner, existing.ownerUserId);
|
|
357
|
+
const move = await moveOf(t, existing, ref);
|
|
358
|
+
// A tracking Agent named as it is made (get-or-create) stays where it can move to.
|
|
359
|
+
const stays = move.plan.applies === "refused" && ref.version === undefined && existing.manifestRef === "tracking";
|
|
360
|
+
if (!stays)
|
|
361
|
+
await moveAgent(t, existing, move, now);
|
|
344
362
|
const patch = { lastUsedAt: now };
|
|
345
363
|
if (body.labels !== undefined && canonical(body.labels) !== canonical(existing.labels))
|
|
346
364
|
patch.labels = body.labels;
|
|
347
|
-
if (body.
|
|
348
|
-
patch.ownerUserId = body.
|
|
365
|
+
if (body.owner !== undefined && (body.owner ?? undefined) !== existing.ownerUserId)
|
|
366
|
+
patch.ownerUserId = body.owner;
|
|
349
367
|
await t.updateEnvironment(id, patch, now);
|
|
350
368
|
return viewOf(ctx, t, (await t.environment(id)) ?? existing, subjectOf(scope));
|
|
351
369
|
};
|
|
@@ -353,21 +371,30 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
353
371
|
deleting(existing);
|
|
354
372
|
if (existing)
|
|
355
373
|
return found(existing);
|
|
356
|
-
checkGuard(scope, body.
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const kind =
|
|
364
|
-
if (
|
|
365
|
-
await requirePods(ctx.pods, `
|
|
366
|
-
const
|
|
374
|
+
checkGuard(scope, body.owner);
|
|
375
|
+
const version = fork?.version ?? ref.version;
|
|
376
|
+
const manifest = (await t.agentManifest(ref.name, version)) ??
|
|
377
|
+
fail(404, version === undefined
|
|
378
|
+
? `No agent manifest is named ${ref.name}: save it first (PUT /v1/agent-manifests/${ref.name}).`
|
|
379
|
+
: `Agent manifest ${ref.name} has no version ${version}.`, { code: "manifest_not_found" });
|
|
380
|
+
const runtime = runtimeOf(manifest.body);
|
|
381
|
+
const kind = runtime.kind;
|
|
382
|
+
if (kind === "pod")
|
|
383
|
+
await requirePods(ctx.pods, `Agent manifest ${manifest.name} is of kind pod, and a pod Agent`);
|
|
384
|
+
const resolved = resolveRecipe(kind, manifestRecipeRequest(manifest.body), config);
|
|
385
|
+
const errors = [
|
|
386
|
+
...(resolved.kind === "error" ? resolved.errors : []),
|
|
387
|
+
...limitIssues(runtime.limits, tenantLimits),
|
|
388
|
+
...manifestShapeIssues(runtime, runtimeLimits),
|
|
389
|
+
];
|
|
390
|
+
if (resolved.kind === "error" || errors.length > 0)
|
|
391
|
+
return fail(400, errors.join(" "));
|
|
392
|
+
const recipe = resolved.recipe;
|
|
367
393
|
await checkFiles(t, recipe.files);
|
|
368
394
|
const from = body.from === undefined ? undefined : await forkSource(t, scope, kind, body.from);
|
|
369
395
|
// A fork onto other setup runs it on the written disk (Q17).
|
|
370
396
|
const seed = from && fork?.setup ? { ...from, setup: true } : from;
|
|
397
|
+
const tracking = ref.version === undefined ? "tracking" : "pinned";
|
|
371
398
|
const row = {
|
|
372
399
|
id,
|
|
373
400
|
kind,
|
|
@@ -376,13 +403,16 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
376
403
|
applied: recipe,
|
|
377
404
|
version: 1,
|
|
378
405
|
appliedVersion: 1,
|
|
379
|
-
...(body.
|
|
406
|
+
...(body.owner ? { ownerUserId: body.owner } : {}),
|
|
380
407
|
lastUsedAt: now,
|
|
381
408
|
createdAt: now,
|
|
382
409
|
updatedAt: now,
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
410
|
+
manifestName: manifest.name,
|
|
411
|
+
manifestVersion: manifest.version,
|
|
412
|
+
manifestRef: tracking,
|
|
413
|
+
manifestHash: manifest.hash,
|
|
414
|
+
desiredState: "on",
|
|
415
|
+
...(fork ? { forkedFrom: { agent: fork.source, disk: seed?.version ?? null } } : {}),
|
|
386
416
|
...(seed
|
|
387
417
|
? {
|
|
388
418
|
diskSeed: seed,
|
|
@@ -390,7 +420,7 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
390
420
|
kind: "seed",
|
|
391
421
|
version: seed.version,
|
|
392
422
|
startedAt: now,
|
|
393
|
-
until: new Date(Date.now() +
|
|
423
|
+
until: new Date(Date.now() + hostTuning().tenant.seedLeaseMs).toISOString(),
|
|
394
424
|
},
|
|
395
425
|
}
|
|
396
426
|
: {}),
|
|
@@ -411,7 +441,7 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
411
441
|
};
|
|
412
442
|
const created = await t.createEnvironment(row, config.limits.environments);
|
|
413
443
|
if (created === "limit")
|
|
414
|
-
fail(409, `This Tenant allows at most ${config.limits.environments}
|
|
444
|
+
fail(409, `This Tenant allows at most ${config.limits.environments} Agents.`, {
|
|
415
445
|
code: "limit_exceeded",
|
|
416
446
|
details: { limit: "environments", max: config.limits.environments },
|
|
417
447
|
});
|
|
@@ -421,22 +451,26 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
421
451
|
deleting(again);
|
|
422
452
|
return found(again);
|
|
423
453
|
}
|
|
454
|
+
// Its triggers start active (D74); armed after the commit.
|
|
455
|
+
await createAgentTriggers(t, row);
|
|
456
|
+
// A fork starts with the source's memories as they are now, with no history (T5).
|
|
457
|
+
if (fork)
|
|
458
|
+
await t.copyMemories(fork.source, id, now);
|
|
424
459
|
const vaultId = await ctx.vault.createEnvironmentVault(t, id, actorOf(scope), fork?.sealed?.vaultId);
|
|
425
460
|
if (fork?.sealed)
|
|
426
461
|
fork.copied = await ctx.vault.insertEnvironmentSecretCopies(t, vaultId, fork.source, actorOf(scope), fork.sealed.copies);
|
|
427
|
-
|
|
428
|
-
await t.environmentEvent(id, "environment.created", {
|
|
462
|
+
await t.agentEvent(id, "agent.created", {
|
|
429
463
|
kind,
|
|
430
464
|
labels: row.labels,
|
|
431
|
-
|
|
432
|
-
...(seed ? { from: {
|
|
465
|
+
manifest: { name: manifest.name, version: manifest.version, tracking },
|
|
466
|
+
...(seed ? { from: { agent: body.from.agent, version: seed.version } } : {}),
|
|
433
467
|
...(row.forkedFrom ? { forkedFrom: row.forkedFrom } : {}),
|
|
434
468
|
});
|
|
435
469
|
if (fork)
|
|
436
|
-
await t.
|
|
470
|
+
await t.agentEvent(fork.source, "agent.forked", {
|
|
437
471
|
to: id,
|
|
438
472
|
disk: seed?.version ?? null,
|
|
439
|
-
|
|
473
|
+
manifest: { name: manifest.name, version: manifest.version },
|
|
440
474
|
secrets: fork.copied?.copied.length ?? 0,
|
|
441
475
|
});
|
|
442
476
|
if (row.pod) {
|
|
@@ -447,13 +481,60 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
447
481
|
}
|
|
448
482
|
return viewOf(ctx, t, row, subjectOf(scope));
|
|
449
483
|
});
|
|
484
|
+
await armAgentTriggers(ctx, id);
|
|
450
485
|
// A virtual fork's workspace is written at once (a pod's once its engine connects).
|
|
451
486
|
if (body.from !== undefined && view.kind === "virtual") {
|
|
452
487
|
await runPendingSeed(ctx, id);
|
|
453
|
-
return
|
|
488
|
+
return getAgent(ctx, id, scope);
|
|
454
489
|
}
|
|
455
490
|
return view;
|
|
456
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* The move a `PUT` naming `ref` asks of existing Agent `agent` (track T3): to version `n` for
|
|
494
|
+
* `name@n` (pinned), else to the latest version (tracking), unless that one failed to start, when
|
|
495
|
+
* it stays on what it runs. Another manifest name is `409 agent_fixed_field`.
|
|
496
|
+
*/
|
|
497
|
+
async function moveOf(t, agent, ref) {
|
|
498
|
+
if (ref.name !== agent.manifestName)
|
|
499
|
+
fail(409, `Agent ${agent.id} runs agent manifest ${agent.manifestName}, which is fixed for its life: fork it into a new Agent made from ${ref.name} with POST /v1/agents/${encodeURIComponent(agent.id)}/fork.`, { code: "agent_fixed_field", details: { fields: [{ field: "manifest", from: agent.manifestName, to: ref.name }] } });
|
|
500
|
+
const missing = () => fail(404, `Agent manifest ${ref.name} has no version ${ref.version}.`, { code: "manifest_not_found" });
|
|
501
|
+
let target = (await t.agentManifest(ref.name, ref.version)) ?? missing();
|
|
502
|
+
if (ref.version === undefined && target.version === agent.failedVersion)
|
|
503
|
+
target = (await t.agentManifest(ref.name, agent.manifestVersion)) ?? target;
|
|
504
|
+
return planMove(t, agent, target, ref.version === undefined ? "tracking" : "pinned");
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* `PUT /v1/agents/{id}?dryRun=true` (track T3): what moving existing Agent `id` to the version
|
|
508
|
+
* `body.manifest` names would do (`AgentMovePlan`), with nothing changed.
|
|
509
|
+
*/
|
|
510
|
+
export async function planAgentPut(ctx, id, body, scope) {
|
|
511
|
+
reachable(agentGrantsOf(scope), id);
|
|
512
|
+
const ref = parseAgentManifestRef(body.manifest) ??
|
|
513
|
+
fail(400, "manifest is an agent manifest's name, or name@version such as triage@3.");
|
|
514
|
+
return ctx.store.tx(async (t) => {
|
|
515
|
+
const agent = (await liveEnvironment(t, id)) ?? fail(404, "Agent not found: a dry run plans the move of an existing Agent.");
|
|
516
|
+
return { dryRun: true, plan: (await moveOf(t, agent, ref)).plan };
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* After a secret write on Agent `id`: a move that waited for its required secrets goes on once
|
|
521
|
+
* they are set (track T3). Best effort: the write stands either way.
|
|
522
|
+
*/
|
|
523
|
+
export async function secretWritten(ctx, id) {
|
|
524
|
+
try {
|
|
525
|
+
await ctx.store.tx(async (t) => {
|
|
526
|
+
const agent = await liveEnvironment(t, id, { lock: true });
|
|
527
|
+
if (agent)
|
|
528
|
+
await resumeMove(t, agent, new Date().toISOString());
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
ctx.config.logger.warn("agent move not resumed after a secret write", {
|
|
533
|
+
environmentId: id,
|
|
534
|
+
message: error instanceof Error ? error.message : String(error),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
457
538
|
/**
|
|
458
539
|
* The secret write `raw` asks of Environment `id` (D64). When what it runs declares secret
|
|
459
540
|
* slots, the name must be one (`409 secret_not_in_manifest`), and the body is `{ value }` (or
|
|
@@ -461,32 +542,43 @@ export async function putEnvironment(ctx, id, body, scope, options = {}) {
|
|
|
461
542
|
* accepted only when its binding is the slot's. Without slots, today's typed body.
|
|
462
543
|
*/
|
|
463
544
|
export async function secretRequestFor(ctx, id, name, raw) {
|
|
464
|
-
const
|
|
465
|
-
|
|
545
|
+
const request = await parseSecretRequest(ctx, id, name, raw);
|
|
546
|
+
// Within the Tenant's Runtime limits `secretValueChars` and `secretHosts`, read for the write.
|
|
547
|
+
const issues = secretRequestIssues(request, await ctx.store.tx((t) => readRuntimeLimits(t)));
|
|
548
|
+
if (issues.length > 0)
|
|
549
|
+
fail(400, issues.join(" "));
|
|
550
|
+
return request;
|
|
551
|
+
}
|
|
552
|
+
async function parseSecretRequest(ctx, id, name, raw) {
|
|
553
|
+
const { environment, slots } = await ctx.store.tx(async (t) => {
|
|
554
|
+
const environment = await liveEnvironment(t, id);
|
|
555
|
+
return { environment, slots: environment && (await slotsAhead(t, environment)) };
|
|
556
|
+
});
|
|
466
557
|
if (!environment || slots === undefined)
|
|
467
|
-
return
|
|
558
|
+
return PutAgentSecretRequestSchema.parse(raw);
|
|
559
|
+
const named = `${environment.manifestName}@${environment.targetVersion ?? environment.manifestVersion}`;
|
|
468
560
|
const slot = slots[name] ??
|
|
469
|
-
fail(409, `
|
|
561
|
+
fail(409, `Agent ${id}'s manifest (${named}) declares no secret ${name}: add the slot to the manifest's runtime section, then move the Agent to the new version.`, { code: "secret_not_in_manifest", details: { declared: Object.keys(slots).sort() } });
|
|
470
562
|
const full = slotRequest(slot);
|
|
471
563
|
if (raw !== null && typeof raw === "object" && "type" in raw) {
|
|
472
|
-
const body =
|
|
564
|
+
const body = PutAgentSecretRequestSchema.parse(raw);
|
|
473
565
|
const { requestId: _, value: _v, headers: _h, ...binding } = body;
|
|
474
566
|
if (canonical(binding) !== canonical(full))
|
|
475
|
-
fail(409, `Secret ${name}'s type and binding come from
|
|
567
|
+
fail(409, `Secret ${name}'s type and binding come from Agent ${id}'s manifest: send only its value ({ "value": "…" }).`, {
|
|
476
568
|
code: "secret_not_in_manifest",
|
|
477
569
|
});
|
|
478
570
|
return body;
|
|
479
571
|
}
|
|
480
|
-
const { requestId, value, headers } =
|
|
572
|
+
const { requestId, value, headers } = PutAgentSecretValueRequestSchema.parse(raw);
|
|
481
573
|
const withId = requestId === undefined ? {} : { requestId };
|
|
482
574
|
if (slot.type === "headers") {
|
|
483
575
|
if (headers === undefined)
|
|
484
576
|
fail(400, `Secret ${name} is a headers credential: send { "headers": { … } }.`);
|
|
485
|
-
return
|
|
577
|
+
return PutAgentSecretRequestSchema.parse({ ...withId, ...full, headers });
|
|
486
578
|
}
|
|
487
579
|
if (value === undefined)
|
|
488
580
|
fail(400, `Secret ${name} takes a value: send { "value": "…" }.`);
|
|
489
|
-
return
|
|
581
|
+
return PutAgentSecretRequestSchema.parse({ ...withId, ...full, value });
|
|
490
582
|
}
|
|
491
583
|
/** A slot's type and binding, as a secret write's fields (without the value). */
|
|
492
584
|
function slotRequest(slot) {
|
|
@@ -499,47 +591,53 @@ function slotRequest(slot) {
|
|
|
499
591
|
*/
|
|
500
592
|
async function forkSource(t, scope, kind, from) {
|
|
501
593
|
if (kind === "none")
|
|
502
|
-
fail(400, "from is for an
|
|
503
|
-
reachable(
|
|
504
|
-
const source = (await liveEnvironment(t, from.
|
|
594
|
+
fail(400, "from is for an Agent with a disk: kind none has none.");
|
|
595
|
+
reachable(agentGrantsOf(scope), from.agent);
|
|
596
|
+
const source = (await liveEnvironment(t, from.agent)) ?? notFound();
|
|
505
597
|
const subject = subjectOf(scope);
|
|
506
598
|
if (subject !== undefined && source.ownerUserId !== undefined && source.ownerUserId !== subject)
|
|
507
599
|
notFound();
|
|
508
|
-
return forkSeedOf(t, from);
|
|
600
|
+
return forkSeedOf(t, { environment: from.agent, version: from.version });
|
|
509
601
|
}
|
|
510
602
|
/**
|
|
511
|
-
* `POST /v1/
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
* its disk and its sessions. With
|
|
603
|
+
* `POST /v1/agents/{id}/fork` (Q15): copies an Agent into a new one, which is how an Agent changes
|
|
604
|
+
* what is fixed for its life (its manifest, kind, harness or storage). A new Agent `body.id` is made from the manifest
|
|
605
|
+
* (default the source's manifest name, tracking, at its latest version), its disk started from a
|
|
606
|
+
* version of the source's (`latest` takes a snapshot first, refused during a turn), and its
|
|
607
|
+
* secrets copied inside the Runtime. A fork onto other setup runs that setup on the copied disk
|
|
608
|
+
* before its first turn (Q17). The source keeps what it runs, its disk and its sessions. With
|
|
609
|
+
* `dryRun`, the plan only.
|
|
517
610
|
*/
|
|
518
|
-
export async function
|
|
519
|
-
const grants =
|
|
611
|
+
export async function forkAgent(ctx, sourceId, body, scope) {
|
|
612
|
+
const grants = agentGrantsOf(scope);
|
|
520
613
|
reachable(grants, sourceId);
|
|
521
614
|
reachable(grants, body.id);
|
|
522
615
|
if (body.id === sourceId)
|
|
523
|
-
fail(400, "A fork is a new
|
|
616
|
+
fail(400, "A fork is a new Agent: give it another id.");
|
|
524
617
|
const { plan: planned, slots } = await ctx.store.tx(async (t) => {
|
|
525
618
|
const source = (await liveEnvironment(t, sourceId)) ?? notFound();
|
|
526
619
|
const subject = subjectOf(scope);
|
|
527
620
|
if (subject !== undefined && source.ownerUserId !== undefined && source.ownerUserId !== subject)
|
|
528
621
|
notFound();
|
|
529
|
-
const ref = body.manifest === undefined
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
fail(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const kind = manifest.body.kind ?? "virtual";
|
|
622
|
+
const ref = body.manifest === undefined
|
|
623
|
+
? { name: source.manifestName }
|
|
624
|
+
: (parseAgentManifestRef(body.manifest) ??
|
|
625
|
+
fail(400, "manifest is an agent manifest's name, or name@version such as triage@3."));
|
|
626
|
+
const manifest = (await t.agentManifest(ref.name, ref.version)) ??
|
|
627
|
+
fail(404, ref.version === undefined
|
|
628
|
+
? `No agent manifest is named ${ref.name}.`
|
|
629
|
+
: `Agent manifest ${ref.name} has no version ${ref.version}.`, { code: "manifest_not_found" });
|
|
630
|
+
const runtime = runtimeOf(manifest.body);
|
|
631
|
+
const kind = runtime.kind;
|
|
540
632
|
const resolved = resolveRecipe(kind, manifestRecipeRequest(manifest.body), effectiveSandboxConfig(await readSandboxConfig(t)));
|
|
541
|
-
|
|
542
|
-
|
|
633
|
+
const errors = [
|
|
634
|
+
...(resolved.kind === "error" ? resolved.errors : []),
|
|
635
|
+
...limitIssues(runtime.limits, await readTenantLimits(t)),
|
|
636
|
+
...manifestShapeIssues(runtime, await readRuntimeLimits(t)),
|
|
637
|
+
...labelIssues(body.labels, await readRuntimeLimits(t)),
|
|
638
|
+
];
|
|
639
|
+
if (resolved.kind === "error" || errors.length > 0)
|
|
640
|
+
return fail(400, errors.join(" "));
|
|
543
641
|
const recipe = resolved.recipe;
|
|
544
642
|
const asked = body.disk ?? "latest";
|
|
545
643
|
const disk = kind === "none" || source.kind === "none" || asked === "empty"
|
|
@@ -560,7 +658,7 @@ export async function forkEnvironment(ctx, sourceId, body, scope) {
|
|
|
560
658
|
disk,
|
|
561
659
|
setup: kind === "pod" &&
|
|
562
660
|
disk !== null &&
|
|
563
|
-
|
|
661
|
+
canonical(recipe) !== canonical(source.applied) &&
|
|
564
662
|
Boolean(recipe.setup?.length || recipe.files?.length),
|
|
565
663
|
secrets: {
|
|
566
664
|
copied,
|
|
@@ -570,8 +668,9 @@ export async function forkEnvironment(ctx, sourceId, body, scope) {
|
|
|
570
668
|
.map(([item]) => item)
|
|
571
669
|
.sort(),
|
|
572
670
|
},
|
|
573
|
-
changes: manifestChanges(source.
|
|
671
|
+
changes: manifestChanges(source.applied, recipe),
|
|
574
672
|
sessions: (await t.sessionsOnEnvironment(sourceId)).length,
|
|
673
|
+
memories: await t.countMemories(sourceId),
|
|
575
674
|
};
|
|
576
675
|
const slots = recipe.secrets === undefined
|
|
577
676
|
? null
|
|
@@ -588,25 +687,31 @@ export async function forkEnvironment(ctx, sourceId, body, scope) {
|
|
|
588
687
|
const sealed = (body.secrets ?? "copy") === "copy"
|
|
589
688
|
? { vaultId, copies: await ctx.keys.sealEnvironmentSecretCopies(sourceId, vaultId, slots) }
|
|
590
689
|
: undefined;
|
|
591
|
-
const fork = {
|
|
592
|
-
|
|
690
|
+
const fork = {
|
|
691
|
+
source: sourceId,
|
|
692
|
+
version: planned.manifest.version,
|
|
693
|
+
setup: planned.setup,
|
|
694
|
+
...(sealed ? { sealed } : {}),
|
|
695
|
+
};
|
|
696
|
+
const agent = await putAgent(ctx, body.id, {
|
|
593
697
|
...(body.requestId === undefined ? {} : { requestId: body.requestId }),
|
|
594
|
-
|
|
698
|
+
// Tracking unless the request pinned a version; either way the plan's version is made.
|
|
699
|
+
manifest: body.manifest ?? planned.manifest.name,
|
|
595
700
|
...(body.labels === undefined ? {} : { labels: body.labels }),
|
|
596
|
-
...(body.
|
|
597
|
-
...(disk === null ? {} : { from: {
|
|
701
|
+
...(body.owner === undefined ? {} : { owner: body.owner }),
|
|
702
|
+
...(disk === null ? {} : { from: { agent: sourceId, version: disk } }),
|
|
598
703
|
}, scope, { fork });
|
|
599
704
|
const copied = fork.copied ?? { copied: [], dropped: planned.secrets.copied };
|
|
600
705
|
const dropped = [...new Set([...planned.secrets.dropped, ...copied.dropped])].sort();
|
|
601
706
|
return {
|
|
602
|
-
|
|
707
|
+
agent,
|
|
603
708
|
plan: {
|
|
604
709
|
...planned,
|
|
605
710
|
disk,
|
|
606
711
|
secrets: {
|
|
607
712
|
copied: copied.copied,
|
|
608
713
|
dropped,
|
|
609
|
-
missing: Object.entries(
|
|
714
|
+
missing: Object.entries(agent.runtime.secrets ?? {})
|
|
610
715
|
.filter(([item, slot]) => slot.required === true && !copied.copied.includes(item))
|
|
611
716
|
.map(([item]) => item)
|
|
612
717
|
.sort(),
|
|
@@ -614,24 +719,44 @@ export async function forkEnvironment(ctx, sourceId, body, scope) {
|
|
|
614
719
|
},
|
|
615
720
|
};
|
|
616
721
|
}
|
|
617
|
-
/** `GET /v1/
|
|
618
|
-
export async function
|
|
619
|
-
reachable(
|
|
722
|
+
/** `GET /v1/agents/{id}`. */
|
|
723
|
+
export async function getAgent(ctx, id, scope) {
|
|
724
|
+
reachable(agentGrantsOf(scope), id);
|
|
620
725
|
return ctx.store.tx(async (t) => {
|
|
621
726
|
const environment = (await liveEnvironment(t, id)) ?? notFound();
|
|
622
727
|
return viewOf(ctx, t, environment, subjectOf(scope));
|
|
623
728
|
});
|
|
624
729
|
}
|
|
625
|
-
/**
|
|
626
|
-
|
|
627
|
-
|
|
730
|
+
/**
|
|
731
|
+
* `GET /v1/agents?label=k=v&state=s&manifest=m`: every Agent with all the labels, in the state
|
|
732
|
+
* and made from the manifest, that the caller reaches, by id. With `page`, at most `limit` after
|
|
733
|
+
* the Agent id `after` (the cursor), and the id to continue from.
|
|
734
|
+
*/
|
|
735
|
+
export async function listAgents(ctx, filters, scope, page) {
|
|
736
|
+
const grants = agentGrantsOf(scope);
|
|
628
737
|
return ctx.store.tx(async (t) => {
|
|
629
|
-
const found = await t.listEnvironments({ labels });
|
|
738
|
+
const found = await t.listEnvironments({ labels: filters.labels });
|
|
630
739
|
const views = [];
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
740
|
+
let next = null;
|
|
741
|
+
for (const environment of found) {
|
|
742
|
+
if (!live(environment) || (grants !== undefined && !agentGranted(grants, environment.id)))
|
|
743
|
+
continue;
|
|
744
|
+
if (filters.manifest !== undefined && environment.manifestName !== filters.manifest)
|
|
745
|
+
continue;
|
|
746
|
+
if (filters.outdated !== undefined && (environment.targetVersion !== undefined) !== filters.outdated)
|
|
747
|
+
continue;
|
|
748
|
+
if (page?.after !== undefined && environment.id <= page.after)
|
|
749
|
+
continue;
|
|
750
|
+
const view = await viewOf(ctx, t, environment, subjectOf(scope));
|
|
751
|
+
if (filters.state !== undefined && view.state !== filters.state)
|
|
752
|
+
continue;
|
|
753
|
+
if (page !== undefined && views.length === page.limit) {
|
|
754
|
+
next = views.at(-1).id;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
views.push(view);
|
|
758
|
+
}
|
|
759
|
+
return { agents: views, next };
|
|
635
760
|
});
|
|
636
761
|
}
|
|
637
762
|
/**
|
|
@@ -640,15 +765,15 @@ export async function listEnvironments(ctx, labels, scope) {
|
|
|
640
765
|
*/
|
|
641
766
|
export async function wakePod(ctx, id) {
|
|
642
767
|
if (!ctx.pods)
|
|
643
|
-
fail(409, `
|
|
768
|
+
fail(409, `Agent ${id} runs on a pod, and this Runtime has no sandbox pods.`, { code: "agent_unavailable" });
|
|
644
769
|
await ctx.store.tx(async (t) => {
|
|
645
770
|
const environment = await liveEnvironment(t, id, { lock: true });
|
|
646
771
|
const pod = environment?.pod;
|
|
647
772
|
if (!environment || !pod)
|
|
648
773
|
return;
|
|
649
774
|
if (pod.observed === "lost")
|
|
650
|
-
fail(409, `
|
|
651
|
-
code: "
|
|
775
|
+
fail(409, `Agent ${id}'s pod was lost (${pod.reason ?? "its volume is gone"}). Reset or restore it.`, {
|
|
776
|
+
code: "agent_lost",
|
|
652
777
|
});
|
|
653
778
|
const now = new Date().toISOString();
|
|
654
779
|
if (pod.desired !== "running" || pod.observed === "failed") {
|
|
@@ -666,19 +791,13 @@ export function diskHeld(environment, now = Date.now()) {
|
|
|
666
791
|
/** Refused while a snapshot or restore copies the disk (a pending seed does not count). */
|
|
667
792
|
export function refuseDiskCopy(environment) {
|
|
668
793
|
if (diskHeld(environment) && environment.diskOp.kind !== "seed")
|
|
669
|
-
|
|
670
|
-
code: "environment_busy",
|
|
671
|
-
details: { retryAfterSeconds: 5 },
|
|
672
|
-
}, { "retry-after": "5" });
|
|
794
|
+
failRetryLater(`Agent ${environment.id}'s disk is being copied (${environment.diskOp.kind}). Try again shortly.`, "agent_busy");
|
|
673
795
|
}
|
|
674
796
|
/** Refused while a session that names Environment `id` has a turn running. */
|
|
675
797
|
async function refuseBusy(t, id) {
|
|
676
798
|
const busy = (await t.sessionsOnEnvironment(id)).some((session) => session.activeTurnId !== null);
|
|
677
799
|
if (busy)
|
|
678
|
-
|
|
679
|
-
code: "environment_busy",
|
|
680
|
-
details: { retryAfterSeconds: 5 },
|
|
681
|
-
}, { "retry-after": "5" });
|
|
800
|
+
failRetryLater(`Agent ${id} has a turn running. Cancel it, or wait for it to end.`, "agent_busy");
|
|
682
801
|
}
|
|
683
802
|
/**
|
|
684
803
|
* Deletes `environment` (locked, live) in `t`: its vault and credentials at once, its workspace
|
|
@@ -686,7 +805,11 @@ async function refuseBusy(t, id) {
|
|
|
686
805
|
*/
|
|
687
806
|
async function deleteIn(ctx, t, environment, actor, reason) {
|
|
688
807
|
const id = environment.id;
|
|
689
|
-
await t.
|
|
808
|
+
await t.agentEvent(id, "agent.deleted", { reason });
|
|
809
|
+
// Its triggers and their runs go with it; a timer still armed finds no row.
|
|
810
|
+
await t.deleteAgentTriggers(id);
|
|
811
|
+
// So do its memories and their versions (T5).
|
|
812
|
+
await t.deleteAgentMemories(id);
|
|
690
813
|
await ctx.vault.deleteEnvironmentVault(t, id, actor);
|
|
691
814
|
// Its disk versions go with it; their manifests' bytes are the caller's, after the commit.
|
|
692
815
|
const keys = await deleteDiskArtifact(ctx, t, id);
|
|
@@ -706,12 +829,12 @@ async function deleteKeys(ctx, keys) {
|
|
|
706
829
|
await ctx.blobs.delete(key).catch(() => undefined);
|
|
707
830
|
}
|
|
708
831
|
/**
|
|
709
|
-
* `DELETE /v1/
|
|
832
|
+
* `DELETE /v1/agents/{id}`: deletes the Environment, its vault and its workspace. Refused
|
|
710
833
|
* while a session that names it has an active turn. Sessions keep its id: their next turn is
|
|
711
|
-
* refused (`
|
|
834
|
+
* refused (`agent_unavailable`) unless an Environment with this id exists again.
|
|
712
835
|
*/
|
|
713
|
-
export async function
|
|
714
|
-
reachable(
|
|
836
|
+
export async function deleteAgent(ctx, id, scope) {
|
|
837
|
+
reachable(agentGrantsOf(scope), id);
|
|
715
838
|
const deleted = await ctx.store.tx(async (t) => {
|
|
716
839
|
const environment = await liveEnvironment(t, id, { lock: true });
|
|
717
840
|
if (!environment)
|
|
@@ -732,7 +855,7 @@ export async function deleteEnvironment(ctx, id, scope) {
|
|
|
732
855
|
* change, secret write, stop or reset since. One with a turn running waits for the next sweep.
|
|
733
856
|
* Run by the Tenant sweep.
|
|
734
857
|
*/
|
|
735
|
-
export async function
|
|
858
|
+
export async function sweepUnusedAgents(ctx, now = Date.now()) {
|
|
736
859
|
const due = (await ctx.store.tx((t) => t.listEnvironments())).filter((environment) => {
|
|
737
860
|
const after = environment.recipe.lifecycle.deleteAfter;
|
|
738
861
|
const ms = after === undefined ? undefined : parseSandboxDuration(after);
|
|
@@ -771,15 +894,15 @@ export async function sweepUnusedEnvironments(ctx, now = Date.now()) {
|
|
|
771
894
|
async function podEnvironment(t, id) {
|
|
772
895
|
const environment = (await liveEnvironment(t, id, { lock: true })) ?? notFound();
|
|
773
896
|
if (!environment.pod)
|
|
774
|
-
return fail(400, `
|
|
897
|
+
return fail(400, `Agent ${id} is of kind ${environment.kind}: only a pod Agent stops and resets`);
|
|
775
898
|
return environment;
|
|
776
899
|
}
|
|
777
900
|
/**
|
|
778
|
-
* `POST /v1/
|
|
901
|
+
* `POST /v1/agents/{id}/stop`: suspends a pod Environment. Its volume is kept, and the next
|
|
779
902
|
* turn of a session that names it starts it again. Refused during a turn.
|
|
780
903
|
*/
|
|
781
|
-
export async function
|
|
782
|
-
reachable(
|
|
904
|
+
export async function stopAgent(ctx, id, scope) {
|
|
905
|
+
reachable(agentGrantsOf(scope), id);
|
|
783
906
|
return ctx.store.tx(async (t) => {
|
|
784
907
|
const environment = await podEnvironment(t, id);
|
|
785
908
|
await refuseBusy(t, id);
|
|
@@ -788,7 +911,7 @@ export async function stopEnvironment(ctx, id, scope) {
|
|
|
788
911
|
if (pod.desired === "running" && pod.observed !== "lost") {
|
|
789
912
|
const epoch = pod.hostEpoch + 1;
|
|
790
913
|
await t.updateEnvironmentPod(id, { desired: "suspended", rev: pod.rev + 1, hostEpoch: epoch }, now);
|
|
791
|
-
await t.
|
|
914
|
+
await t.agentEvent(id, "agent.suspended", { reason: "stop" });
|
|
792
915
|
await reconcileAfter(t, id);
|
|
793
916
|
await revokeHost(t, id, epoch);
|
|
794
917
|
}
|
|
@@ -797,26 +920,65 @@ export async function stopEnvironment(ctx, id, scope) {
|
|
|
797
920
|
});
|
|
798
921
|
}
|
|
799
922
|
/**
|
|
800
|
-
* `POST /v1/
|
|
923
|
+
* `POST /v1/agents/{id}/restart` (track T3): applies a pending version now, refused during a
|
|
924
|
+
* turn. With `retry`, it first moves the Agent to the version that failed to start again. A
|
|
925
|
+
* running pod stops, so the next turn starts it on the version; a stopped or failed one is asked
|
|
926
|
+
* to run at once. Kinds `virtual` and `none` apply a version at once anyway.
|
|
927
|
+
*/
|
|
928
|
+
export async function restartAgent(ctx, id, options, scope) {
|
|
929
|
+
reachable(agentGrantsOf(scope), id);
|
|
930
|
+
return ctx.store.tx(async (t) => {
|
|
931
|
+
let agent = (await liveEnvironment(t, id, { lock: true })) ?? notFound();
|
|
932
|
+
await refuseBusy(t, id);
|
|
933
|
+
refuseDiskCopy(agent);
|
|
934
|
+
const now = new Date().toISOString();
|
|
935
|
+
if (options.retry && agent.failedVersion !== undefined) {
|
|
936
|
+
const failed = (await t.agentManifest(agent.manifestName, agent.failedVersion)) ??
|
|
937
|
+
fail(404, `Agent manifest ${agent.manifestName} has no version ${agent.failedVersion}.`, { code: "manifest_not_found" });
|
|
938
|
+
await moveAgent(t, agent, await planMove(t, agent, failed, agent.manifestRef), now);
|
|
939
|
+
agent = (await t.environment(id, { lock: true }));
|
|
940
|
+
}
|
|
941
|
+
const pod = agent.pod;
|
|
942
|
+
if (pod && waitingFor(agent) === "start") {
|
|
943
|
+
if (!ctx.pods)
|
|
944
|
+
fail(409, `Agent ${id} runs on a pod, and this Runtime has no sandbox pods.`, { code: "agent_unavailable" });
|
|
945
|
+
if (pod.observed === "lost")
|
|
946
|
+
fail(409, `Agent ${id}'s pod was lost (${pod.reason ?? "its volume is gone"}). Reset it to continue.`, { code: "agent_lost" });
|
|
947
|
+
if (pod.desired === "running" && pod.observed !== "failed") {
|
|
948
|
+
// Stopped now; the next turn starts it on the version.
|
|
949
|
+
const epoch = pod.hostEpoch + 1;
|
|
950
|
+
await t.updateEnvironmentPod(id, { desired: "suspended", rev: pod.rev + 1, hostEpoch: epoch }, now);
|
|
951
|
+
await t.agentEvent(id, "agent.suspended", { reason: "restart" });
|
|
952
|
+
await reconcileAfter(t, id);
|
|
953
|
+
await revokeHost(t, id, epoch);
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
await wantRunning(t, agent, now);
|
|
957
|
+
await reconcileAfter(t, id);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
await t.updateEnvironment(id, { lastUsedAt: now }, now);
|
|
961
|
+
return viewOf(ctx, t, (await t.environment(id)) ?? agent, subjectOf(scope));
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* `POST /v1/agents/{id}/reset`: a new pod on a new, empty volume (the next volume
|
|
801
966
|
* generation), with a new join token and the recipe applied; the old Sandbox and volume are
|
|
802
967
|
* deleted. Leaves `lost` and `failed`. Refused during a turn, and while an earlier reset's
|
|
803
968
|
* Sandbox is still being deleted.
|
|
804
969
|
*/
|
|
805
|
-
export async function
|
|
806
|
-
reachable(
|
|
970
|
+
export async function resetAgent(ctx, id, scope) {
|
|
971
|
+
reachable(agentGrantsOf(scope), id);
|
|
807
972
|
// A missing Environment is a 404, and one of another kind a 400, with or without sandbox pods.
|
|
808
973
|
await ctx.store.tx((t) => podEnvironment(t, id));
|
|
809
|
-
await requirePods(ctx.pods, "Resetting a pod
|
|
974
|
+
await requirePods(ctx.pods, "Resetting a pod Agent");
|
|
810
975
|
return ctx.store.tx(async (t) => {
|
|
811
976
|
const environment = await podEnvironment(t, id);
|
|
812
977
|
await refuseBusy(t, id);
|
|
813
978
|
refuseDiskCopy(environment);
|
|
814
979
|
const pod = environment.pod;
|
|
815
980
|
if (pod.retiring !== undefined)
|
|
816
|
-
|
|
817
|
-
code: "environment_busy",
|
|
818
|
-
details: { retryAfterSeconds: 5 },
|
|
819
|
-
}, { "retry-after": "5" });
|
|
981
|
+
failRetryLater(`Agent ${id} is still deleting its previous volume. Try again shortly.`, "agent_busy");
|
|
820
982
|
await newVolume(ctx, t, environment);
|
|
821
983
|
return viewOf(ctx, t, (await t.environment(id)) ?? environment, subjectOf(scope));
|
|
822
984
|
});
|
|
@@ -833,6 +995,8 @@ export async function newVolume(ctx, t, environment, options = {}) {
|
|
|
833
995
|
const now = new Date().toISOString();
|
|
834
996
|
const volumeGen = pod.volumeGen + 1;
|
|
835
997
|
const epoch = pod.hostEpoch + 1;
|
|
998
|
+
// A move whose runtime waits for a start switches now (track T3).
|
|
999
|
+
await switchTo(t, environment, now);
|
|
836
1000
|
await t.updateEnvironment(id, {
|
|
837
1001
|
applied: environment.recipe,
|
|
838
1002
|
appliedVersion: environment.version,
|
|
@@ -845,7 +1009,7 @@ export async function newVolume(ctx, t, environment, options = {}) {
|
|
|
845
1009
|
kind: "seed",
|
|
846
1010
|
version: options.seed.version,
|
|
847
1011
|
startedAt: now,
|
|
848
|
-
until: new Date(Date.now() +
|
|
1012
|
+
until: new Date(Date.now() + hostTuning().tenant.seedLeaseMs).toISOString(),
|
|
849
1013
|
},
|
|
850
1014
|
}),
|
|
851
1015
|
}, now);
|
|
@@ -864,7 +1028,7 @@ export async function newVolume(ctx, t, environment, options = {}) {
|
|
|
864
1028
|
lastActiveAt: now,
|
|
865
1029
|
reason: null,
|
|
866
1030
|
}, now);
|
|
867
|
-
await t.
|
|
1031
|
+
await t.agentEvent(id, "agent.reset", {
|
|
868
1032
|
volumeGeneration: volumeGen,
|
|
869
1033
|
...(options.reason ? { reason: options.reason } : {}),
|
|
870
1034
|
...(options.seed ? { from: { version: options.seed.version } } : {}),
|
|
@@ -873,12 +1037,13 @@ export async function newVolume(ctx, t, environment, options = {}) {
|
|
|
873
1037
|
await revokeHost(t, id, epoch);
|
|
874
1038
|
return volumeGen;
|
|
875
1039
|
}
|
|
876
|
-
/**
|
|
877
|
-
export
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1040
|
+
/** `GET /v1/agents/{id}/events`: the Environment's lifecycle stream. */
|
|
1041
|
+
export async function agentEventsOf(ctx, id, scope, query = {}) {
|
|
1042
|
+
reachable(agentGrantsOf(scope), id);
|
|
1043
|
+
// One read returns at most the Tenant's Runtime limit `environmentEventItems`; `from` reads on.
|
|
1044
|
+
return ctx.store.tx(async (t) => ({
|
|
1045
|
+
events: await t.agentEvents(id, { ...query, limit: (await readRuntimeLimits(t)).environmentEventItems }),
|
|
1046
|
+
}));
|
|
882
1047
|
}
|
|
883
1048
|
/**
|
|
884
1049
|
* The owner guard (Q7): refuses a session of `ownerUserId` on an Environment guarded for
|
|
@@ -889,23 +1054,23 @@ function checkOwner(environment, ownerUserId, opaque) {
|
|
|
889
1054
|
return;
|
|
890
1055
|
if (opaque)
|
|
891
1056
|
notFound();
|
|
892
|
-
fail(403, `
|
|
1057
|
+
fail(403, `Agent ${environment.id} is for another owner's sessions (its owner guard).`, { code: "agent_owner_mismatch" });
|
|
893
1058
|
}
|
|
894
1059
|
/**
|
|
895
1060
|
* Opens a new session on Environment `id` (`PutSessionRequest.environment`): it must exist, be
|
|
896
1061
|
* reachable with `grants` and pass its owner guard. Runs in the session's transaction, after
|
|
897
1062
|
* its session row lock; holds the Environment's row lock.
|
|
898
1063
|
*/
|
|
899
|
-
export async function
|
|
1064
|
+
export async function openAgent(t, id, options) {
|
|
900
1065
|
reachable(options.grants, id);
|
|
901
1066
|
const environment = (await liveEnvironment(t, id, { lock: true })) ?? notFound();
|
|
902
1067
|
checkOwner(environment, options.ownerUserId, options.opaque);
|
|
903
1068
|
return environment;
|
|
904
1069
|
}
|
|
905
1070
|
/** Records the attachment on the session's log and on the Environment's stream. */
|
|
906
|
-
export async function recordAttachment(t, sessionId,
|
|
907
|
-
await t.event(sessionId, null, "
|
|
908
|
-
await t.
|
|
1071
|
+
export async function recordAttachment(t, sessionId, agentId) {
|
|
1072
|
+
await t.event(sessionId, null, "agent.attached", { agentId });
|
|
1073
|
+
await t.agentEvent(agentId, "agent.attached", { sessionId });
|
|
909
1074
|
}
|
|
910
1075
|
/**
|
|
911
1076
|
* Checks a turn start on a session that names an Environment, in the command's transaction
|
|
@@ -914,25 +1079,25 @@ export async function recordAttachment(t, sessionId, environmentId) {
|
|
|
914
1079
|
* do not stop it (D66). Holds the Environment row's lock until the transaction ends, so a turn
|
|
915
1080
|
* start and disk work on one Environment are decided one after the other.
|
|
916
1081
|
*
|
|
917
|
-
* A pod Environment must also be usable: not lost (`
|
|
918
|
-
* sandbox pods (`
|
|
1082
|
+
* A pod Environment must also be usable: not lost (`agent_lost`), on a Runtime with
|
|
1083
|
+
* sandbox pods (`agent_unavailable`). A stopped or failed pod is asked to run again, with
|
|
919
1084
|
* its recipe applied, after the commit.
|
|
920
1085
|
*/
|
|
921
|
-
export async function
|
|
1086
|
+
export async function checkAgentTurn(t, session, scope, ctx) {
|
|
922
1087
|
const id = session.environmentId;
|
|
923
1088
|
if (id === undefined)
|
|
924
1089
|
return;
|
|
925
|
-
const grants =
|
|
926
|
-
if (grants !== undefined && !
|
|
927
|
-
fail(403, `This token does not reach
|
|
1090
|
+
const grants = agentGrantsOf(scope);
|
|
1091
|
+
if (grants !== undefined && !agentGranted(grants, id))
|
|
1092
|
+
fail(403, `This token does not reach Agent ${id}.`, { code: "agent_not_granted" });
|
|
928
1093
|
const environment = await liveEnvironment(t, id, { lock: true });
|
|
929
1094
|
if (!environment)
|
|
930
|
-
return fail(409, `
|
|
931
|
-
code: "
|
|
1095
|
+
return fail(409, `Agent ${id} was deleted. Create it again to continue this session.`, {
|
|
1096
|
+
code: "agent_unavailable",
|
|
932
1097
|
});
|
|
933
1098
|
if (environment.ownerUserId !== undefined && environment.ownerUserId !== session.ownerUserId)
|
|
934
|
-
fail(403, `
|
|
935
|
-
code: "
|
|
1099
|
+
fail(403, `Agent ${id} is for another owner's sessions (its owner guard).`, {
|
|
1100
|
+
code: "agent_owner_mismatch",
|
|
936
1101
|
});
|
|
937
1102
|
// The harness the turn would run on (R4.1, Q9): a pod about to wake runs its recipe's. A
|
|
938
1103
|
// session never moves to another harness: its state is that harness's. An Environment's
|
|
@@ -942,9 +1107,9 @@ export async function checkEnvironmentTurn(t, session, scope, ctx) {
|
|
|
942
1107
|
const waking = environment.pod !== undefined && (environment.pod.desired !== "running" || environment.pod.observed === "failed");
|
|
943
1108
|
const runs = waking ? environment.recipe.harness : environment.applied.harness;
|
|
944
1109
|
if (!isWorkflowManifest(session.manifest) && runs !== harnessOf(session))
|
|
945
|
-
fail(409, `
|
|
1110
|
+
fail(409, `Agent ${id} runs the ${runs} harness, and this session runs ${harnessOf(session)}: open a new session to run on it.`, {
|
|
946
1111
|
code: "harness_mismatch",
|
|
947
|
-
details: { harness: harnessOf(session),
|
|
1112
|
+
details: { harness: harnessOf(session), agentHarness: runs },
|
|
948
1113
|
});
|
|
949
1114
|
if (environment.kind === "none")
|
|
950
1115
|
return;
|
|
@@ -954,12 +1119,12 @@ export async function checkEnvironmentTurn(t, session, scope, ctx) {
|
|
|
954
1119
|
if (!pod)
|
|
955
1120
|
return;
|
|
956
1121
|
if (!ctx?.pods)
|
|
957
|
-
fail(409, `
|
|
958
|
-
code: "
|
|
1122
|
+
fail(409, `Agent ${id} runs on a pod, and this Runtime has no sandbox pods: run \`nylorun local sandbox enable --context <name>\`.`, {
|
|
1123
|
+
code: "agent_unavailable",
|
|
959
1124
|
});
|
|
960
1125
|
if (pod.observed === "lost")
|
|
961
|
-
fail(409, `
|
|
962
|
-
code: "
|
|
1126
|
+
fail(409, `Agent ${id}'s pod was lost (${pod.reason ?? "its volume is gone"}). Reset it to continue.`, {
|
|
1127
|
+
code: "agent_lost",
|
|
963
1128
|
});
|
|
964
1129
|
const now = new Date().toISOString();
|
|
965
1130
|
if (pod.desired !== "running" || pod.observed === "failed") {
|
|
@@ -977,7 +1142,7 @@ export async function detachAllSessions(t) {
|
|
|
977
1142
|
continue;
|
|
978
1143
|
await t.environment(environment.id, { lock: true });
|
|
979
1144
|
for (const session of sessions)
|
|
980
|
-
await t.
|
|
1145
|
+
await t.agentEvent(environment.id, "agent.detached", {
|
|
981
1146
|
sessionId: session.id,
|
|
982
1147
|
reason: "reset",
|
|
983
1148
|
});
|
|
@@ -988,7 +1153,7 @@ export async function detachAllSessions(t) {
|
|
|
988
1153
|
* from here), and for a running pod idle counts from now and the idle timer is armed (D34).
|
|
989
1154
|
* Best effort.
|
|
990
1155
|
*/
|
|
991
|
-
export async function
|
|
1156
|
+
export async function touchAgent(ctx, id) {
|
|
992
1157
|
try {
|
|
993
1158
|
await ctx.store.tx(async (t) => {
|
|
994
1159
|
const environment = await liveEnvironment(t, id, { lock: true });
|
|
@@ -1000,7 +1165,7 @@ export async function touchEnvironment(ctx, id) {
|
|
|
1000
1165
|
if (!environment.pod || environment.pod.desired !== "running")
|
|
1001
1166
|
return;
|
|
1002
1167
|
await t.updateEnvironmentPod(id, { lastActiveAt: iso }, iso);
|
|
1003
|
-
const idleMs =
|
|
1168
|
+
const idleMs = idleMsOf(await readSandboxConfig(t), environment);
|
|
1004
1169
|
await t.signalSandbox(id, { kind: "arm", timer: "idle", at: now.getTime() + idleMs });
|
|
1005
1170
|
});
|
|
1006
1171
|
}
|
|
@@ -1029,7 +1194,7 @@ export async function rearmPodTimers(ctx) {
|
|
|
1029
1194
|
await ctx.sandboxSignal(row.id, {
|
|
1030
1195
|
kind: "arm",
|
|
1031
1196
|
timer: "idle",
|
|
1032
|
-
at: Date.parse(row.pod.lastActiveAt ?? row.createdAt) +
|
|
1197
|
+
at: Date.parse(row.pod.lastActiveAt ?? row.createdAt) + idleMsOf(config, row),
|
|
1033
1198
|
});
|
|
1034
1199
|
}
|
|
1035
1200
|
catch (error) {
|