@nylorun/runtime 0.51.2-beta → 0.53.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/adapters/sandbox/computer.d.ts +28 -0
  3. package/dist/adapters/sandbox/computer.js +184 -0
  4. package/dist/api/components.d.ts +48 -0
  5. package/dist/api/components.js +3 -1
  6. package/dist/api/http/routes/triggers.js +38 -6
  7. package/dist/api/openapi.js +1 -0
  8. package/dist/artifacts/disk.js +10 -2
  9. package/dist/harness/computer.d.ts +86 -0
  10. package/dist/harness/computer.js +273 -0
  11. package/dist/harness/executors.d.ts +5 -0
  12. package/dist/harness/executors.js +13 -5
  13. package/dist/harness/pod.d.ts +2 -0
  14. package/dist/harness/pod.js +1 -1
  15. package/dist/harness/service.js +58 -22
  16. package/dist/harness-api/server.js +17 -0
  17. package/dist/harness-api/start.js +2 -0
  18. package/dist/harness-api/workspace.d.ts +5 -0
  19. package/dist/harness-api/workspace.js +9 -5
  20. package/dist/host/harness-entry.js +7 -2
  21. package/dist/host/main.js +13 -0
  22. package/dist/host/stack-config.d.ts +13 -1
  23. package/dist/host/stack-config.js +42 -2
  24. package/dist/management-openapi.json +2 -1
  25. package/dist/openapi.json +348 -44
  26. package/dist/sandbox/computer.d.ts +53 -0
  27. package/dist/sandbox/computer.js +271 -0
  28. package/dist/sandbox/placement.d.ts +5 -2
  29. package/dist/sandbox/placement.js +6 -3
  30. package/dist/sandbox/pods/reconcile.js +1 -0
  31. package/dist/sandbox/pods/spec.d.ts +5 -0
  32. package/dist/sandbox/pods/spec.js +1 -1
  33. package/dist/sandbox/records.d.ts +2 -0
  34. package/dist/sandbox/records.js +11 -0
  35. package/dist/sandbox/types.d.ts +5 -2
  36. package/dist/store/postgres/drizzle/0049_agent_triggers_api.sql +3 -0
  37. package/dist/store/postgres/drizzle/meta/_journal.json +7 -0
  38. package/dist/store/postgres/schema.d.ts +21 -4
  39. package/dist/store/postgres/schema.js +10 -7
  40. package/dist/tenant/advance.js +6 -3
  41. package/dist/tenant/agent-triggers.js +5 -2
  42. package/dist/tenant/api-triggers.d.ts +33 -0
  43. package/dist/tenant/api-triggers.js +140 -0
  44. package/dist/tenant/context.d.ts +3 -0
  45. package/dist/tenant/runtime.js +34 -0
  46. package/dist/tenant/sessions.js +2 -1
  47. package/dist/tenant/sweep.js +2 -1
  48. package/dist/tenant/transcript.d.ts +8 -8
  49. package/dist/tenant/triggers.d.ts +6 -4
  50. package/dist/tenant/triggers.js +22 -11
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.53.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 5b49361: A pod Agent on an Agent Substrate cluster can be a computer (ADR-149, ADR-150): with `NYLORUN_SANDBOX_GATEWAY_URL` and `NYLORUN_COMPUTER_TICKET_KEY_FILE` set (the Runtime's Helm chart sets both), its sessions' runs go to the harness container, and their sandbox tools, file reads and writes, skill folders and disk versions reach the Agent's actor through the sandbox gateway and `atenet-router`, with a ticket core signs (Ed25519). The engine's `--service computer` runs no loop: it serves commands (a start and attaches, each under 50 s) and files on port 8080, checks each ticket against the key its template carries, and answers the wake-up probe on `/ready`. A harness opens a computer with `computer.open`; core wakes it, waits until it is ready, writes a seed onto a new disk, runs the disk's setup through it and passes a version's start check. The tools, `runtime.json` and every other backend are unchanged.
8
+
9
+ - `@nylorun/core`: Harness API 5.6 (`harness-api/v5.6.json`): `routing.sandbox.computer` and the request `computer.open`.
10
+ - `@nylorun/runtime`: `--service computer`; the `computer` sandbox backend; `NYLORUN_SANDBOX_GATEWAY_URL` and `NYLORUN_COMPUTER_TICKET_KEY_FILE`.
11
+
12
+ - 0cb752d: The Runtime's Helm chart, `charts/nylorun-runtime` (ADR-147, the Runtime on Kubernetes plan): the Runtime on an Agent Substrate cluster, with computers. It installs core, the gateway, the harness, Studio, the sandboxes service in the cluster, the sandbox gateway (atenet-router's client), Restate on a disk and s2-lite with its streams in the object store; Postgres and the object store are an external URL and S3 endpoint, or bundled (Postgres, RustFS). It makes the Runtime's secrets once and keeps them, and optionally pre-pulls images on worker nodes and makes the Tenant's atespace. Its README says what Substrate needs (the router's allowlist, the atespace) and has a GCP example.
13
+ - f2ef233: The sandboxes service runs in the cluster it drives, and Agent Substrate sandboxes can be computers (ADR-149, the Runtime on Kubernetes plan). `cluster.json`'s `inCluster: true` uses the pod's ServiceAccount and reaches Substrate's API by its Service with a projected token; `substrate.computer` renders every sandbox as a computer: its engine runs `--service computer`, with no join token, edge or edge rule, a wake-up probe on `/ready`, and the ticket key's public half. `sandboxes gateway` is the sandbox gateway (ADR-148): it checks a computer ticket and proxies to the computer through an mTLS CONNECT tunnel to `atenet-router`.
14
+
15
+ ### Patch Changes
16
+
17
+ - becf6a2: A computer runs our engine whatever harness drives its Agent: an Agent with a custom harness runs it through the Harness API, and its image is its computer. The chart sets numeric users for `runAsNonRoot`, uses the sandboxes service's `/health` for readiness (its `/ready` waits for the atespace the post-install Job makes), and says that Substrate's API admits only namespaces labelled `ate.dev/api-access=true`. Docs: Harness API §9b (computers), Kubernetes in SELF_HOSTING.md, and a MIGRATION entry.
18
+ - Pin core to the tested release.
19
+ - Pin harness to the tested release.
20
+ - Updated dependencies [5b49361]
21
+ - Updated dependencies
22
+ - @nylorun/core@0.46.0-beta
23
+ - @nylorun/harness@0.36.6-beta
24
+
25
+ ## 0.52.0-beta
26
+
27
+ ### Minor Changes
28
+
29
+ - 17951fe: A client can give one Agent a trigger over the API (D88, ADR-145; Host feature `api-triggers`). `PUT /v1/agents/{agentId}/triggers/{triggerId}` creates or replaces the Agent's trigger of a fourth origin, `api`: the body is a `Trigger` exactly as a definition's `runtime.triggers` entry holds it (one of `schedule` with `timezone`, `at` or `webhook`; `session`; `prompt`) and an optional `title`. A new one is active with its next time; a replaced one gets its next time again and keeps its status; the same body again changes nothing and records no event. An id the definition declares, or one an agent's trigger or a booked wake holds, is `409 trigger_conflict`. The checks and limits are a definition's, and `runtime.limits.triggers.count` now counts the triggers an Agent's agents made and those clients put together (`422 trigger_limit`). Upgrades and moves never touch an `api` trigger, `DELETE` removes it, and pause, resume, run and a webhook's secret work as for any trigger.
30
+
31
+ - `@nylorun/core`: `TRIGGER_ORIGINS` adds `api`; `TriggerPutSchema`; `TriggerView.principal`; `agent.trigger_created` and `agent.trigger_updated` carry `origin`, `principal`, the timing and `changed` (their `sessionId` is now optional, set for `agent` only; `kind` adds `webhook`), and `agent.trigger_deleted` carries `origin`; Host feature `api-triggers`.
32
+ - `@nylorun/runtime`: the route; `agent_triggers.principal` and the `api` origin (store migration 0049, run at start).
33
+ - `@nylorun/agents`: `agents.putTrigger(agentId, triggerId, trigger)`; `deleteTrigger` deletes `api` triggers too.
34
+ - `nylorun`: `nylorun agents triggers put <id> <trigger>` with `--schedule`, `--timezone`, `--at`, `--webhook`, `--session`, `--prompt` and `--title`, or a JSON file; `rm` deletes `api` triggers.
35
+ - `@nylorun/studio`: the Triggers tab shows where each trigger came from (the definition's version, the agent that made it, or the principal that put it), its prompt and time zone for every origin, and deletes one an agent made or a client put; Activity words trigger changes and filters them.
36
+
37
+ ### Patch Changes
38
+
39
+ - Pin core to the tested release.
40
+ - Pin harness to the tested release.
41
+ - Updated dependencies [17951fe]
42
+ - Updated dependencies
43
+ - @nylorun/core@0.45.0-beta
44
+ - @nylorun/harness@0.36.5-beta
45
+
3
46
  ## 0.51.2-beta
4
47
 
5
48
  ### Patch Changes
@@ -0,0 +1,28 @@
1
+ import { type SandboxBackend } from "../../sandbox/types.js";
2
+ /** What `open` answers: where this computer is, and the ticket for it. */
3
+ export interface ComputerAccess {
4
+ /** The gateway's URL for this computer, `<gateway>/<sandbox>`. */
5
+ readonly url: string;
6
+ readonly ticket: string;
7
+ /** When the ticket expires (ms since the epoch). */
8
+ readonly expiresAt: number;
9
+ }
10
+ export interface ComputerBackendOptions {
11
+ readonly open: (signal?: AbortSignal) => Promise<ComputerAccess>;
12
+ readonly fetch?: typeof fetch;
13
+ readonly now?: () => number;
14
+ /** How long a dropped request is asked again for, past the command's own timeout. */
15
+ readonly retryMs?: number;
16
+ }
17
+ /** A computer refused or failed an operation. */
18
+ export declare class ComputerError extends Error {
19
+ readonly status: number;
20
+ readonly code: string;
21
+ constructor(status: number, code: string, message: string);
22
+ }
23
+ /** The disk roots of every computer (R9, ADR-150). */
24
+ export declare const COMPUTER_DISK_ROOTS: {
25
+ readonly workspace: "/workspace";
26
+ readonly harness: "/harness";
27
+ };
28
+ export declare function computerBackend(options: ComputerBackendOptions): SandboxBackend;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Computer backend (ADR-149, ADR-150): a pod Agent's sandbox on Agent Substrate, reached through
3
+ * the sandbox gateway and Substrate's router (ADR-148). Its handle sends each operation to the
4
+ * computer's engine (`harness/computer.ts`) with a ticket core signs: `open` (a harness asks core
5
+ * with `computer.open`; core in process) wakes the computer, runs its disk's setup, and answers
6
+ * the gateway URL for this computer, the ticket and when it expires. A ticket is asked for again
7
+ * a minute before it expires.
8
+ *
9
+ * A command is a start and attaches, each answered within 50 s, under the command's own timeout:
10
+ * a gateway or router that drops a request (a rollout, a drain) is asked again with the same id,
11
+ * and the computer joins the command it already runs. One the computer no longer knows (lost with
12
+ * a crash or a revert) fails the call, saying so.
13
+ *
14
+ * One computer holds one sandbox: every key opens the same handle. Removing it is the Agent's reset
15
+ * or delete (core's pod lifecycle), so `remove` and `list` do nothing here; `stop` does nothing
16
+ * either, as core suspends an idle computer.
17
+ */
18
+ import { randomUUID } from "node:crypto";
19
+ import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
20
+ import { SandboxFileTooLargeError, wholeBytes, } from "../../sandbox/types.js";
21
+ /** A computer refused or failed an operation. */
22
+ export class ComputerError extends Error {
23
+ status;
24
+ code;
25
+ constructor(status, code, message) {
26
+ super(message);
27
+ this.status = status;
28
+ this.code = code;
29
+ this.name = "ComputerError";
30
+ }
31
+ }
32
+ const WAIT_MS = 50_000;
33
+ const RENEW_MS = 60_000;
34
+ /** The disk roots of every computer (R9, ADR-150). */
35
+ export const COMPUTER_DISK_ROOTS = { workspace: SANDBOX_WORKSPACE, harness: "/harness" };
36
+ export function computerBackend(options) {
37
+ const http = options.fetch ?? fetch;
38
+ const now = options.now ?? Date.now;
39
+ const retryMs = options.retryMs ?? 3 * 60_000;
40
+ let access;
41
+ let opening;
42
+ const current = async (signal) => {
43
+ if (access && access.expiresAt - now() > RENEW_MS)
44
+ return access;
45
+ opening ??= options.open(signal).finally(() => (opening = undefined));
46
+ access = await opening;
47
+ return access;
48
+ };
49
+ const call = async (method, path, init = {}) => {
50
+ const at = await current(init.signal);
51
+ const headers = { "x-nylorun-ticket": at.ticket };
52
+ let body = init.body;
53
+ if (init.json !== undefined) {
54
+ headers["content-type"] = "application/json";
55
+ body = JSON.stringify(init.json);
56
+ }
57
+ const response = await http(`${at.url}${path}`, {
58
+ method,
59
+ headers,
60
+ ...(body === undefined ? {} : { body }),
61
+ ...(body !== undefined && typeof body !== "string" && !(body instanceof Uint8Array) ? { duplex: "half" } : {}),
62
+ ...(init.signal ? { signal: init.signal } : {}),
63
+ });
64
+ if (response.status === 401)
65
+ access = undefined; // a new ticket next time
66
+ return response;
67
+ };
68
+ const failed = async (response) => {
69
+ let code = "computer_error";
70
+ let message = `The computer answered ${response.status}`;
71
+ try {
72
+ const parsed = (await response.json());
73
+ code = parsed.error?.code ?? code;
74
+ message = parsed.error?.message ?? message;
75
+ }
76
+ catch {
77
+ // not JSON
78
+ }
79
+ return new ComputerError(response.status, code, message);
80
+ };
81
+ /** Whether a failure is the way's (gateway, router, a computer moving), worth asking again. */
82
+ const transient = (error) => error instanceof ComputerError ? error.status === 401 || error.status >= 500 : !(error instanceof DOMException && error.name === "AbortError");
83
+ const exec = async (request, signal) => {
84
+ const id = randomUUID();
85
+ const deadline = now() + request.timeoutMs + retryMs;
86
+ let started = false;
87
+ const cancel = () => void call("POST", `/v1/exec/${encodeURIComponent(id)}/cancel`).catch(() => undefined);
88
+ signal.addEventListener("abort", cancel, { once: true });
89
+ try {
90
+ for (let attempt = 0;; attempt++) {
91
+ if (signal.aborted)
92
+ throw signal.reason ?? new Error("Turn cancelled");
93
+ try {
94
+ const response = started
95
+ ? await call("POST", `/v1/exec/${encodeURIComponent(id)}/attach`, { json: { waitMs: WAIT_MS }, signal })
96
+ : await call("POST", "/v1/exec", {
97
+ json: { id, command: request.command, cwd: request.cwd, timeoutMs: request.timeoutMs, env: request.env ?? {}, waitMs: WAIT_MS },
98
+ signal,
99
+ });
100
+ if (response.status === 404 && started)
101
+ return {
102
+ exitCode: 1,
103
+ stdout: "",
104
+ stderr: "The computer lost this command (it was restarted from its last snapshot).",
105
+ killed: true,
106
+ timedOut: false,
107
+ };
108
+ if (!response.ok)
109
+ throw await failed(response);
110
+ started = true;
111
+ attempt = 0;
112
+ const answer = (await response.json());
113
+ if (answer.state === "done")
114
+ return answer.result;
115
+ if (answer.state === "failed")
116
+ throw new ComputerError(500, "command_failed", answer.message);
117
+ }
118
+ catch (error) {
119
+ if (!transient(error) || now() > deadline)
120
+ throw error;
121
+ await new Promise((resolve) => setTimeout(resolve, Math.min(5_000, 250 * 2 ** attempt)));
122
+ }
123
+ }
124
+ }
125
+ finally {
126
+ signal.removeEventListener("abort", cancel);
127
+ }
128
+ };
129
+ const readBytes = async (path, maxBytes) => {
130
+ const response = await call("GET", `/v1/files?path=${encodeURIComponent(path)}&maxBytes=${maxBytes}`);
131
+ if (response.status === 404)
132
+ return undefined;
133
+ if (response.status === 413) {
134
+ const error = await failed(response);
135
+ const size = Number(/is (\d+) bytes/.exec(error.message)?.[1] ?? maxBytes + 1);
136
+ throw new SandboxFileTooLargeError(path, size, maxBytes);
137
+ }
138
+ if (!response.ok)
139
+ throw await failed(response);
140
+ return new Uint8Array(await response.arrayBuffer());
141
+ };
142
+ const handle = {
143
+ workspace: SANDBOX_WORKSPACE,
144
+ diskRoots: COMPUTER_DISK_ROOTS,
145
+ exec,
146
+ async readFile(path) {
147
+ const bytes = await readBytes(path, Number.MAX_SAFE_INTEGER);
148
+ return bytes === undefined ? undefined : Buffer.from(bytes).toString("utf8");
149
+ },
150
+ readBytes,
151
+ async listFiles(dir, maxEntries) {
152
+ const response = await call("GET", `/v1/list?dir=${encodeURIComponent(dir)}&maxEntries=${maxEntries}`);
153
+ if (response.status === 404)
154
+ return undefined;
155
+ if (!response.ok)
156
+ throw await failed(response);
157
+ return (await response.json());
158
+ },
159
+ async writeFile(path, content) {
160
+ // A file's chunks are gathered: a skill's file is at most a few megabytes.
161
+ const body = (typeof content === "string" ? content : Buffer.from(await wholeBytes(content)));
162
+ const response = await call("PUT", `/v1/files?path=${encodeURIComponent(path)}`, { body });
163
+ if (!response.ok)
164
+ throw await failed(response);
165
+ },
166
+ async stop() { },
167
+ };
168
+ return {
169
+ name: "computer",
170
+ isolation: "container",
171
+ async probe() {
172
+ return { name: "computer", isolation: "container", available: true, reason: "an Agent Substrate computer" };
173
+ },
174
+ unmet: () => undefined,
175
+ open: async () => {
176
+ await current();
177
+ return handle;
178
+ },
179
+ async remove() { },
180
+ async list() {
181
+ return [];
182
+ },
183
+ };
184
+ }
@@ -2301,11 +2301,13 @@ export declare const TriggerList: z.ZodObject<{
2301
2301
  }>;
2302
2302
  origin: z.ZodEnum<{
2303
2303
  agent: "agent";
2304
+ api: "api";
2304
2305
  manifest: "manifest";
2305
2306
  wake: "wake";
2306
2307
  }>;
2307
2308
  title: z.ZodNullable<z.ZodString>;
2308
2309
  createdSessionId: z.ZodNullable<z.ZodString>;
2310
+ principal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2309
2311
  status: z.ZodEnum<{
2310
2312
  active: "active";
2311
2313
  completed: "completed";
@@ -2323,6 +2325,50 @@ export declare const TriggerList: z.ZodObject<{
2323
2325
  prompt: z.ZodOptional<z.ZodString>;
2324
2326
  }, z.core.$strict>>;
2325
2327
  }, z.core.$strict>;
2328
+ export declare const TriggerView: z.ZodObject<{
2329
+ id: z.ZodString;
2330
+ kind: z.ZodEnum<{
2331
+ at: "at";
2332
+ schedule: "schedule";
2333
+ wake: "wake";
2334
+ webhook: "webhook";
2335
+ }>;
2336
+ origin: z.ZodEnum<{
2337
+ agent: "agent";
2338
+ api: "api";
2339
+ manifest: "manifest";
2340
+ wake: "wake";
2341
+ }>;
2342
+ title: z.ZodNullable<z.ZodString>;
2343
+ createdSessionId: z.ZodNullable<z.ZodString>;
2344
+ principal: z.ZodOptional<z.ZodNullable<z.ZodString>>;
2345
+ status: z.ZodEnum<{
2346
+ active: "active";
2347
+ completed: "completed";
2348
+ paused: "paused";
2349
+ }>;
2350
+ pausedReason: z.ZodNullable<z.ZodString>;
2351
+ schedule: z.ZodOptional<z.ZodString>;
2352
+ timezone: z.ZodOptional<z.ZodString>;
2353
+ at: z.ZodOptional<z.ZodString>;
2354
+ session: z.ZodString;
2355
+ nextRunAt: z.ZodNullable<z.ZodString>;
2356
+ lastRunAt: z.ZodNullable<z.ZodString>;
2357
+ webhookUrl: z.ZodOptional<z.ZodString>;
2358
+ secretSet: z.ZodOptional<z.ZodBoolean>;
2359
+ prompt: z.ZodOptional<z.ZodString>;
2360
+ }, z.core.$strict>;
2361
+ export declare const PutTriggerRequest: z.ZodObject<{
2362
+ schedule: z.ZodOptional<z.ZodString>;
2363
+ timezone: z.ZodOptional<z.ZodString>;
2364
+ at: z.ZodOptional<z.ZodISODateTime>;
2365
+ webhook: z.ZodOptional<z.ZodEnum<{
2366
+ "standard-webhooks": "standard-webhooks";
2367
+ }>>;
2368
+ session: z.ZodString;
2369
+ prompt: z.ZodString;
2370
+ title: z.ZodOptional<z.ZodString>;
2371
+ }, z.core.$strict>;
2326
2372
  export declare const MemoryList: z.ZodObject<{
2327
2373
  memories: z.ZodArray<z.ZodObject<{
2328
2374
  path: z.ZodString;
@@ -2440,6 +2486,7 @@ export declare const TenantRun: z.ZodObject<{
2440
2486
  late: z.ZodBoolean;
2441
2487
  origin: z.ZodEnum<{
2442
2488
  agent: "agent";
2489
+ api: "api";
2443
2490
  manifest: "manifest";
2444
2491
  wake: "wake";
2445
2492
  }>;
@@ -2472,6 +2519,7 @@ export declare const TenantRunEvent: z.ZodObject<{
2472
2519
  late: z.ZodBoolean;
2473
2520
  origin: z.ZodEnum<{
2474
2521
  agent: "agent";
2522
+ api: "api";
2475
2523
  manifest: "manifest";
2476
2524
  wake: "wake";
2477
2525
  }>;
@@ -5,7 +5,7 @@
5
5
  * global registry.
6
6
  */
7
7
  import { z } from "zod";
8
- import { PutSessionModelRequestSchema, SessionModelViewSchema, QueuedResponseSchema, TriggerListSchema, MemoryListSchema, MemorySchema, PutMemoryRequestSchema, MemoryVersionPageSchema, MemoryVersionDetailSchema, TriggerRunPageSchema, RunTriggerResponseSchema, WebhookSecretResponseSchema, WebhookAcceptedResponseSchema, AgentManifestV8Schema, EnvironmentSecretSlotsSchema, DeleteAgentManifestResponseSchema, AgentManifestPlanSchema, AgentManifestVersionsSchema, AgentManifestViewSchema, ListAgentManifestsResponseSchema, PutAgentManifestRequestSchema, PutAgentFolderRequestSchema, AgentManifestSourceSchema, AgentManifestSourceDiffSchema, PutAgentSecretValueRequestSchema, SaveAgentManifestResponseSchema, SessionListItemSchema, ModelCallSchema, SessionPageSchema, SessionManifestViewSchema, HistoryPageSchema, SessionUsageTotalsSchema, ModelCallsPageSchema, ModelCallExportPageSchema, AgentPageSchema, ToolCallSchema, ToolCallsPageSchema, TurnTraceSchema, TurnChangesPageSchema, TurnSummarySchema, JwksSchema, SigningKeyListSchema, SigningKeyViewSchema, RotateSigningKeysRequestSchema, RevokeSigningKeyRequestSchema, MeResponseSchema, TenantStatusSchema, ResetTenantRequestSchema, ResetTenantResponseSchema, SeedTenantConfigRequestSchema, SeedTenantConfigResponseSchema, HostModelCatalogSchema, TenantSandboxViewSchema, ModelUsageTotalsSchema, ModelBudgetsSchema, PutModelBudgetsRequestSchema, ToolPolicySchema, PutToolPolicyRequestSchema, PutModelInputsRequestSchema, ModelInputsViewSchema, PutTenantEnvironmentsRequestSchema, ListProvidersResponseSchema, HostModelViewSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutAgentSecretRequestSchema, AgentSecretViewSchema, ListAgentSecretsResponseSchema, McpPreviewRequestSchema, McpPreviewSchema, CredentialCoverageRequestSchema, CredentialCoverageSchema, DeletedResponseSchema, TenantRunEventSchema, TenantRunSchema, ListSessionsResponseSchema, LiveEventSchema, PutAgentRequestSchema, PutSessionRequestSchema, SessionItemsResponseSchema, SessionViewSchema, StreamClosedFrameSchema, AcceptedResponseSchema, SandboxToolOutcomeSchema, AgentViewSchema, AgentSnapshotSchema, RestoreAgentRequestSchema, ForkAgentRequestSchema, AgentForkPlanSchema, ForkAgentResponseSchema, PutAgentDryRunResponseSchema, RestartAgentRequestSchema, AgentRestoreSchema, AgentDiskVersionsSchema, ListAgentsResponseSchema, DeleteAgentResponseSchema, ListAgentEventsResponseSchema, ArtifactViewSchema, ArtifactListItemSchema, ArtifactPageSchema, ArtifactVersionViewSchema, ListArtifactsResponseSchema, UploadArtifactResponseSchema, DefinitionFileViewSchema, DeleteArtifactResponseSchema, CreateArtifactLinkRequestSchema, ArtifactLinkSchema, PutTenantArtifactsRequestSchema, PutTenantSkillsRequestSchema, PutTenantLimitsRequestSchema, TenantLimitsViewSchema, TenantArtifactsViewSchema, TenantSkillsViewSchema, ArtifactTreeSchema, ArtifactDiffSchema, SessionCommandSchema, ListOperatorKeysResponseSchema, PutOperatorKeyResponseSchema, DeleteOperatorKeyResponseSchema, OperatorKeySchema, ProtocolRejectedResponseSchema, RejectedResponseSchema, EVENT_SCHEMAS, EVENT_TYPES, } from "@nylorun/core/contracts";
8
+ import { PutSessionModelRequestSchema, SessionModelViewSchema, QueuedResponseSchema, TriggerListSchema, TriggerPutSchema, TriggerViewSchema, MemoryListSchema, MemorySchema, PutMemoryRequestSchema, MemoryVersionPageSchema, MemoryVersionDetailSchema, TriggerRunPageSchema, RunTriggerResponseSchema, WebhookSecretResponseSchema, WebhookAcceptedResponseSchema, AgentManifestV8Schema, EnvironmentSecretSlotsSchema, DeleteAgentManifestResponseSchema, AgentManifestPlanSchema, AgentManifestVersionsSchema, AgentManifestViewSchema, ListAgentManifestsResponseSchema, PutAgentManifestRequestSchema, PutAgentFolderRequestSchema, AgentManifestSourceSchema, AgentManifestSourceDiffSchema, PutAgentSecretValueRequestSchema, SaveAgentManifestResponseSchema, SessionListItemSchema, ModelCallSchema, SessionPageSchema, SessionManifestViewSchema, HistoryPageSchema, SessionUsageTotalsSchema, ModelCallsPageSchema, ModelCallExportPageSchema, AgentPageSchema, ToolCallSchema, ToolCallsPageSchema, TurnTraceSchema, TurnChangesPageSchema, TurnSummarySchema, JwksSchema, SigningKeyListSchema, SigningKeyViewSchema, RotateSigningKeysRequestSchema, RevokeSigningKeyRequestSchema, MeResponseSchema, TenantStatusSchema, ResetTenantRequestSchema, ResetTenantResponseSchema, SeedTenantConfigRequestSchema, SeedTenantConfigResponseSchema, HostModelCatalogSchema, TenantSandboxViewSchema, ModelUsageTotalsSchema, ModelBudgetsSchema, PutModelBudgetsRequestSchema, ToolPolicySchema, PutToolPolicyRequestSchema, PutModelInputsRequestSchema, ModelInputsViewSchema, PutTenantEnvironmentsRequestSchema, ListProvidersResponseSchema, HostModelViewSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutAgentSecretRequestSchema, AgentSecretViewSchema, ListAgentSecretsResponseSchema, McpPreviewRequestSchema, McpPreviewSchema, CredentialCoverageRequestSchema, CredentialCoverageSchema, DeletedResponseSchema, TenantRunEventSchema, TenantRunSchema, ListSessionsResponseSchema, LiveEventSchema, PutAgentRequestSchema, PutSessionRequestSchema, SessionItemsResponseSchema, SessionViewSchema, StreamClosedFrameSchema, AcceptedResponseSchema, SandboxToolOutcomeSchema, AgentViewSchema, AgentSnapshotSchema, RestoreAgentRequestSchema, ForkAgentRequestSchema, AgentForkPlanSchema, ForkAgentResponseSchema, PutAgentDryRunResponseSchema, RestartAgentRequestSchema, AgentRestoreSchema, AgentDiskVersionsSchema, ListAgentsResponseSchema, DeleteAgentResponseSchema, ListAgentEventsResponseSchema, ArtifactViewSchema, ArtifactListItemSchema, ArtifactPageSchema, ArtifactVersionViewSchema, ListArtifactsResponseSchema, UploadArtifactResponseSchema, DefinitionFileViewSchema, DeleteArtifactResponseSchema, CreateArtifactLinkRequestSchema, ArtifactLinkSchema, PutTenantArtifactsRequestSchema, PutTenantSkillsRequestSchema, PutTenantLimitsRequestSchema, TenantLimitsViewSchema, TenantArtifactsViewSchema, TenantSkillsViewSchema, ArtifactTreeSchema, ArtifactDiffSchema, SessionCommandSchema, ListOperatorKeysResponseSchema, PutOperatorKeyResponseSchema, DeleteOperatorKeyResponseSchema, OperatorKeySchema, ProtocolRejectedResponseSchema, RejectedResponseSchema, EVENT_SCHEMAS, EVENT_TYPES, } from "@nylorun/core/contracts";
9
9
  function named(id, schema) {
10
10
  return schema.meta({ id });
11
11
  }
@@ -65,6 +65,8 @@ export const SessionCommand = named("SessionCommand", SessionCommandSchema);
65
65
  export const AcceptedResponse = named("AcceptedResponse", AcceptedResponseSchema);
66
66
  export const QueuedResponse = named("QueuedResponse", QueuedResponseSchema);
67
67
  export const TriggerList = named("TriggerList", TriggerListSchema);
68
+ export const TriggerView = named("TriggerView", TriggerViewSchema);
69
+ export const PutTriggerRequest = named("PutTriggerRequest", TriggerPutSchema);
68
70
  export const MemoryList = named("MemoryList", MemoryListSchema);
69
71
  export const MemoryView = named("Memory", MemorySchema);
70
72
  export const PutMemoryRequest = named("PutMemoryRequest", PutMemoryRequestSchema);
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
- import { isAgentId } from "@nylorun/core/contracts";
3
- import { DeletedResponse, RunTriggerResponse, TriggerList, TriggerRunPage, WebhookAcceptedResponse, WebhookSecretResponse, } from "../../components.js";
2
+ import { isAgentId, TriggerIdSchema, TriggerPutSchema } from "@nylorun/core/contracts";
3
+ import { DeletedResponse, PutTriggerRequest, RunTriggerResponse, TriggerList, TriggerRunPage, TriggerView, WebhookAcceptedResponse, WebhookSecretResponse, } from "../../components.js";
4
4
  import { assertReaches } from "../../../tenant/agents.js";
5
5
  import { fail } from "../../../tenant/http.js";
6
6
  import { deleteTrigger, listRuns, listTriggers, receiveWebhook, rotateWebhookSecret, runTriggerNow, setTriggerStatus, } from "../../../tenant/triggers.js";
7
7
  import { readRuntimeLimits } from "../../../tenant/runtime-limits.js";
8
- import { readBytes } from "../body.js";
8
+ import { readBytes, readJson } from "../body.js";
9
+ import { putTrigger } from "../../../tenant/api-triggers.js";
9
10
  import { tenantRoute } from "../define.js";
10
11
  import { pageQuery, pageSize, parseQuery } from "./reads.js";
11
12
  import { jsonResponse } from "../respond.js";
@@ -46,7 +47,7 @@ export function triggerRoutes(api) {
46
47
  path: "/v1/agents/{agentId}/triggers",
47
48
  tags: ["Triggers"],
48
49
  summary: "List an Agent's triggers",
49
- description: "Each trigger its manifest declares, and each its agents made with the `triggers` tool (`origin: agent`, with the title and the session that made it), with what the Agent holds of it: its status (`active`, `paused` with a reason, `completed` for an `at` that ran), its prompt, when it runs next and last ran. A session's booked wake is not listed.",
50
+ description: "Each trigger its manifest declares, each its agents made with the `triggers` tool (`origin: agent`, with the title and the session that made it), and each a client put over the API (`origin: api`, with the title and the `principal` that put it), with what the Agent holds of it: its status (`active`, `paused` with a reason, `completed` for an `at` that ran), its prompt, when it runs next and last ran. A session's booked wake is not listed.",
50
51
  request: { params: agentId },
51
52
  responses: { 200: json(TriggerList, "The triggers") },
52
53
  }, async (c) => {
@@ -73,12 +74,43 @@ export function triggerRoutes(api) {
73
74
  assertReaches(c.get("scope"), id);
74
75
  return jsonResponse(200, await setTriggerStatus(c.env.tenant, id, c.req.param("triggerId"), action, originOf(c)));
75
76
  });
77
+ tenantRoute(api, WRITE, {
78
+ method: "put",
79
+ path: "/v1/agents/{agentId}/triggers/{triggerId}",
80
+ tags: ["Triggers"],
81
+ summary: "Put a trigger on an Agent",
82
+ description: "Creates or replaces the Agent's trigger `triggerId` of origin `api`: a `Trigger` exactly as a definition's `runtime.triggers` entry holds it (one of `schedule` with `timezone`, `at` or `webhook`; `session`; `prompt`), and an optional `title`. A new one is active with its next time; a replaced one gets its next time again and keeps its status, so a paused one stays paused. The same body again changes nothing and records no event. The checks and limits are a definition's; `runtime.limits.triggers.count` covers the triggers the Agent's agents made and those put here. Upgrades never touch it; `DELETE` removes it. Records `agent.trigger_created` or `agent.trigger_updated` (with `changed`) on the Agent's events. Host feature `api-triggers`.",
83
+ request: {
84
+ params: triggerPath,
85
+ body: { required: true, content: { "application/json": { schema: PutTriggerRequest } } },
86
+ },
87
+ responses: {
88
+ 200: json(TriggerView, "The trigger, as the list shows it"),
89
+ 400: { description: "Not a trigger, or one the checks refuse: a bad cron or time zone, runs closer than the minimum interval, an `at` not in the future, a prompt too long" },
90
+ 404: { description: "No such Agent" },
91
+ 409: { description: "The definition declares this id, or an agent's trigger or a booked wake holds it (`trigger_conflict`)" },
92
+ 422: { description: "The Agent holds as many triggers its agents or clients made as `limits.triggers.count` allows (`trigger_limit`)" },
93
+ },
94
+ }, async (c) => {
95
+ const id = idOf(c.req.param("agentId"));
96
+ const scope = c.get("scope");
97
+ assertReaches(scope, id);
98
+ const triggerId = TriggerIdSchema.safeParse(c.req.param("triggerId"));
99
+ if (!triggerId.success)
100
+ fail(400, triggerId.error.issues[0]?.message ?? "Not a trigger id");
101
+ const body = TriggerPutSchema.safeParse(await readJson(c));
102
+ if (!body.success)
103
+ fail(400, body.error.issues.map((issue) => `${issue.path.join(".") || "body"}: ${issue.message}`).join("; "), {
104
+ code: "invalid_request",
105
+ });
106
+ return jsonResponse(200, await putTrigger(c.env.tenant, id, triggerId.data, body.data, principalOf(scope), originOf(c)));
107
+ });
76
108
  tenantRoute(api, WRITE, {
77
109
  method: "delete",
78
110
  path: "/v1/agents/{agentId}/triggers/{triggerId}",
79
111
  tags: ["Triggers"],
80
- summary: "Delete a trigger an agent made",
81
- description: "Deletes a trigger one of the Agent's agents made with the `triggers` tool (`origin: agent`), with `agent.trigger_deleted` on the Agent's events; its runs stay. A trigger its manifest declares is refused (`trigger_from_manifest`): save a version without it, or pause it.",
112
+ summary: "Delete a trigger an agent made or a client put",
113
+ description: "Deletes a trigger one of the Agent's agents made with the `triggers` tool (`origin: agent`) or a client put (`origin: api`), with `agent.trigger_deleted` on the Agent's events; its runs stay. A trigger its manifest declares is refused (`trigger_from_manifest`): save a version without it, or pause it.",
82
114
  request: { params: triggerPath },
83
115
  responses: {
84
116
  200: json(DeletedResponse, "Deleted"),
@@ -161,6 +161,7 @@ const RUNTIME_TAGS = [
161
161
  description: "What starts an Agent's turns with no client connected: the schedules, one-shot times and webhook endpoints its manifest's `runtime.triggers` declare, and the schedules and follow-ups its agents set up with the `triggers` tool (`engine.triggers`). Each trigger's status is the Agent's: pause and resume it, run it now, delete one an agent made, make a webhook trigger's signing secret, and read its runs (`started`, `queued`, `skipped` or `failed`, with a reason), an Agent's or the whole Tenant's as a stream. In `@nylorun/agents` and `nylorun agents triggers`.",
162
162
  operations: [
163
163
  "GET /v1/agents/{agentId}/triggers",
164
+ "PUT /v1/agents/{agentId}/triggers/{triggerId}",
164
165
  "POST /v1/agents/{agentId}/triggers/{triggerId}/pause",
165
166
  "POST /v1/agents/{agentId}/triggers/{triggerId}/resume",
166
167
  "POST /v1/agents/{agentId}/triggers/{triggerId}/run",
@@ -20,6 +20,7 @@
20
20
  * Nothing here runs inside a transaction but the lease and the commit. No event type is new:
21
21
  * a restore or recovery is `agent.reset` with `reason` and `from`.
22
22
  */
23
+ import { computerAccess } from "../sandbox/computer.js";
23
24
  import { createHash, randomUUID } from "node:crypto";
24
25
  import { newArtifactId } from "@nylorun/core/compatibility";
25
26
  import { FOLDER_CONTENT_TYPE, FOLDER_MANIFEST_FORMAT_V2, } from "@nylorun/core/contracts";
@@ -137,6 +138,11 @@ function heartbeat(ctx, id, limits, ms = limits.leaseMs) {
137
138
  }
138
139
  /** Waits until the engine of pod Environment `id` is connected, starting the pod when it is stopped. */
139
140
  async function awaitEngine(ctx, id, waitMs) {
141
+ // A computer (ADR-149) has no engine connection: it is ready once it answers through the gateway.
142
+ if (ctx.pods?.computer) {
143
+ await computerAccess(ctx, id);
144
+ return;
145
+ }
140
146
  await wakePod(ctx, id);
141
147
  const deadline = Date.now() + waitMs;
142
148
  while (!ctx.harness.hosting(id)) {
@@ -459,7 +465,9 @@ export async function runPendingSeed(ctx, id) {
459
465
  if (!environment || !seed || seed.written)
460
466
  return;
461
467
  const limits = await copyLimits(ctx, environment);
462
- if (environment.kind === "pod") {
468
+ if (environment.kind === "pod" && ctx.pods?.computer)
469
+ await computerAccess(ctx, id);
470
+ else if (environment.kind === "pod") {
463
471
  const deadline = Date.now() + limits.podWaitMs;
464
472
  // The engine on the new volume: the old one's connection may still be open.
465
473
  while (!ctx.harness.hosting(id, environment.pod?.hostEpoch)) {
@@ -494,7 +502,7 @@ export async function runPendingSeed(ctx, id) {
494
502
  export async function sweepPendingSeeds(ctx) {
495
503
  const pending = (await ctx.store.tx((t) => t.listEnvironments())).filter((environment) => environment.diskSeed !== undefined && !environment.diskSeed.written && environment.pod?.desired !== "deleted");
496
504
  for (const environment of pending)
497
- if (environment.kind !== "pod" || ctx.harness.hosting(environment.id, environment.pod?.hostEpoch))
505
+ if (environment.kind !== "pod" || ctx.pods?.computer || ctx.harness.hosting(environment.id, environment.pod?.hostEpoch))
498
506
  void runPendingSeed(ctx, environment.id);
499
507
  }
500
508
  /**
@@ -0,0 +1,86 @@
1
+ /**
2
+ * The computer (`--service computer`, ADR-149): the engine in an Agent Substrate actor when every
3
+ * harness runs outside the sandbox. It runs no loop, joins nothing and dials nothing to the
4
+ * Runtime. It serves the sandbox handle (`SandboxHandle`: commands, files, listings) on
5
+ * `NYLORUN_COMPUTER_PORT` (8080), which the sandbox gateway reaches through Substrate's router
6
+ * (ADR-148), and runs each command as the `local` backend does: the image's shell in `/workspace`,
7
+ * its own process group, the trust variables pointing at a bundle with Substrate's egress CA.
8
+ *
9
+ * Every route but `GET /ready` (the template's wake-up probe) needs `x-nylorun-ticket`: a compact
10
+ * JWS core signs with Ed25519, checked against `NYLORUN_COMPUTER_TICKET_KEY` (the public half the
11
+ * template carries), with `aud: "nylorun-computer"`, `exp` ahead, and `sub` this actor's name
12
+ * (`NYLORUN_COMPUTER_NAME_FILE`), so a router client other than the gateway can't command it.
13
+ *
14
+ * Commands are a start and attaches (ADR-148 decision 5): `POST /v1/exec` starts command `id`, or
15
+ * joins it while it runs, and answers within `waitMs` (at most 50 s) with `{state: "running"}` or
16
+ * `{state: "done", result}`; `POST /v1/exec/{id}/attach` waits again. A command's result is kept 10
17
+ * minutes after it ends. After a drain, the next attach reaches this process on the new worker, its
18
+ * commands moved with the memory snapshot. SIGTERM on Substrate means the worker is stopping: the
19
+ * engine keeps running, so the sandboxes service can suspend it.
20
+ */
21
+ import { type KeyObject } from "node:crypto";
22
+ import { type IncomingMessage, type ServerResponse } from "node:http";
23
+ import { type ExecResult, type SandboxHandle } from "../sandbox/types.js";
24
+ export interface ComputerServiceConfig {
25
+ readonly port: number;
26
+ /** The ticket key's public half, SPKI PEM. */
27
+ readonly ticketKey: string;
28
+ /** The file holding this actor's name (Substrate's actorMetadata). */
29
+ readonly nameFile: string;
30
+ /** The engine's own dir on the durable disk (`/harness`). */
31
+ readonly root: string;
32
+ /** Substrate's egress CA, which commands trust for hosts whose keys the egress adds. */
33
+ readonly egressCaFile?: string;
34
+ /** On Agent Substrate: SIGTERM means the worker is stopping, not that the engine should. */
35
+ readonly substrate: boolean;
36
+ }
37
+ /** The ticket audience. */
38
+ export declare const COMPUTER_AUDIENCE = "nylorun-computer";
39
+ /** The longest a start or an attach waits before it answers `running`. */
40
+ export declare const MAX_WAIT_MS = 50000;
41
+ /** Checks a ticket; the claims, or why it fails. */
42
+ export declare function checkTicket(token: string | undefined, key: KeyObject, subject: string, now?: number): {
43
+ ok: true;
44
+ } | {
45
+ ok: false;
46
+ reason: string;
47
+ };
48
+ export interface ComputerExecRequest {
49
+ readonly id: string;
50
+ readonly command: string;
51
+ readonly cwd: string;
52
+ readonly timeoutMs: number;
53
+ readonly env?: Readonly<Record<string, string>>;
54
+ readonly waitMs?: number;
55
+ }
56
+ export type ComputerExecAnswer = {
57
+ readonly state: "running";
58
+ } | {
59
+ readonly state: "done";
60
+ readonly result: ExecResult;
61
+ } | {
62
+ readonly state: "failed";
63
+ readonly message: string;
64
+ };
65
+ /** The commands a computer runs, by id: start, join, attach, cancel. */
66
+ export declare class ComputerCommands {
67
+ private readonly handle;
68
+ private readonly commands;
69
+ constructor(handle: Pick<SandboxHandle, "exec">);
70
+ start(request: ComputerExecRequest): Promise<ComputerExecAnswer>;
71
+ /** Undefined when the computer doesn't know the command. */
72
+ attach(id: string, waitMs?: number): Promise<ComputerExecAnswer> | undefined;
73
+ cancel(id: string): boolean;
74
+ private wait;
75
+ private prune;
76
+ }
77
+ /** The routes, on a handle (tests give their own). */
78
+ export declare function computerHandler(options: {
79
+ readonly handle: SandboxHandle;
80
+ readonly key: KeyObject;
81
+ readonly name: string;
82
+ readonly ready: () => boolean;
83
+ readonly now?: () => number;
84
+ }): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
85
+ /** Runs the computer until the process ends. */
86
+ export declare function runComputer(config: ComputerServiceConfig, baseline: Readonly<Record<string, string>>): Promise<void>;