@nylorun/runtime 0.27.0-beta → 0.29.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 (160) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +1 -1
  3. package/dist/api/a2a/card.d.ts +4 -3
  4. package/dist/api/a2a/card.js +3 -3
  5. package/dist/api/a2a/routes.js +18 -7
  6. package/dist/api/ag-ui/routes.js +4 -7
  7. package/dist/api/components.d.ts +5854 -2718
  8. package/dist/api/components.js +39 -30
  9. package/dist/api/http/app.js +9 -4
  10. package/dist/api/http/body.d.ts +2 -0
  11. package/dist/api/http/body.js +20 -0
  12. package/dist/api/http/define.d.ts +4 -0
  13. package/dist/api/http/define.js +20 -9
  14. package/dist/api/http/routes/agent-manifests.d.ts +10 -0
  15. package/dist/api/http/routes/agent-manifests.js +135 -0
  16. package/dist/api/http/routes/agents.d.ts +11 -0
  17. package/dist/api/http/routes/agents.js +418 -0
  18. package/dist/api/http/routes/artifacts.js +2 -2
  19. package/dist/api/http/routes/coverage.d.ts +4 -4
  20. package/dist/api/http/routes/coverage.js +12 -6
  21. package/dist/api/http/routes/files.js +1 -1
  22. package/dist/api/http/routes/mcp.js +2 -2
  23. package/dist/api/http/routes/me.d.ts +1 -1
  24. package/dist/api/http/routes/me.js +1 -2
  25. package/dist/api/http/routes/memories.d.ts +9 -0
  26. package/dist/api/http/routes/memories.js +152 -0
  27. package/dist/api/http/routes/reads.d.ts +1 -137
  28. package/dist/api/http/routes/reads.js +31 -12
  29. package/dist/api/http/routes/sessions.js +21 -50
  30. package/dist/api/http/routes/tenant.js +13 -10
  31. package/dist/api/http/routes/triggers.d.ts +10 -0
  32. package/dist/api/http/routes/triggers.js +183 -0
  33. package/dist/api/openapi.js +69 -47
  34. package/dist/artifacts/disk.d.ts +10 -10
  35. package/dist/artifacts/disk.js +27 -27
  36. package/dist/artifacts/quota.d.ts +1 -1
  37. package/dist/artifacts/quota.js +2 -2
  38. package/dist/artifacts/service.js +2 -2
  39. package/dist/core/flow-host.d.ts +0 -1
  40. package/dist/gates/egress-credentials.js +1 -1
  41. package/dist/gates/egress.d.ts +5 -5
  42. package/dist/gates/egress.js +6 -6
  43. package/dist/gates/http-tool.js +2 -2
  44. package/dist/gates/meter.js +22 -5
  45. package/dist/gates/policy.d.ts +8 -3
  46. package/dist/gates/policy.js +7 -2
  47. package/dist/gates/tenant-vaults.d.ts +1 -1
  48. package/dist/harness/pod.d.ts +2 -0
  49. package/dist/harness/setup.js +17 -6
  50. package/dist/harness-api/record.js +11 -0
  51. package/dist/harness-api/server.js +4 -0
  52. package/dist/harness-api/start.d.ts +11 -7
  53. package/dist/harness-api/start.js +49 -5
  54. package/dist/harness-api/workspace.js +2 -2
  55. package/dist/host/config-for.d.ts +2 -0
  56. package/dist/host/config-for.js +1 -0
  57. package/dist/host/http.d.ts +2 -2
  58. package/dist/host/http.js +5 -2
  59. package/dist/host/main.js +1 -0
  60. package/dist/keys/keys.d.ts +15 -4
  61. package/dist/keys/keys.js +16 -0
  62. package/dist/keys/webhooks.d.ts +25 -0
  63. package/dist/keys/webhooks.js +62 -0
  64. package/dist/management-openapi.json +313 -133
  65. package/dist/mcp/connect.d.ts +2 -2
  66. package/dist/mcp/connect.js +3 -3
  67. package/dist/mcp/preview.js +7 -7
  68. package/dist/memory/service.d.ts +97 -0
  69. package/dist/memory/service.js +335 -0
  70. package/dist/memory/tool.d.ts +36 -0
  71. package/dist/memory/tool.js +84 -0
  72. package/dist/model/pi-model.js +7 -0
  73. package/dist/openapi.json +13120 -9034
  74. package/dist/reads/types.d.ts +3 -3
  75. package/dist/record/agent.d.ts +27 -0
  76. package/dist/record/agent.js +46 -0
  77. package/dist/record/index.d.ts +1 -1
  78. package/dist/record/index.js +1 -1
  79. package/dist/sandbox/join.js +5 -5
  80. package/dist/sandbox/placement.d.ts +1 -1
  81. package/dist/sandbox/placement.js +3 -3
  82. package/dist/sandbox/pods/lifecycle.d.ts +6 -6
  83. package/dist/sandbox/pods/lifecycle.js +6 -6
  84. package/dist/sandbox/pods/reconcile.js +10 -2
  85. package/dist/sandbox/resolve.d.ts +3 -3
  86. package/dist/sandbox/resolve.js +7 -7
  87. package/dist/sandbox/setup.d.ts +3 -1
  88. package/dist/sandbox/setup.js +14 -4
  89. package/dist/store/postgres/drizzle/0032_agents.sql +54 -0
  90. package/dist/store/postgres/drizzle/0033_agent_versions.sql +7 -0
  91. package/dist/store/postgres/drizzle/0034_protocol_17_session_queue.sql +13 -0
  92. package/dist/store/postgres/drizzle/0035_t4_triggers.sql +41 -0
  93. package/dist/store/postgres/drizzle/0036_t5_memories.sql +30 -0
  94. package/dist/store/postgres/drizzle/0037_model_usage_cost_split.sql +4 -0
  95. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  96. package/dist/store/postgres/reads.js +44 -87
  97. package/dist/store/postgres/record-writer.d.ts +4 -4
  98. package/dist/store/postgres/record-writer.js +9 -9
  99. package/dist/store/postgres/schema.d.ts +1408 -123
  100. package/dist/store/postgres/schema.js +194 -38
  101. package/dist/store/postgres/store.js +329 -79
  102. package/dist/store/postgres/tenant.js +1 -2
  103. package/dist/store/types.d.ts +155 -50
  104. package/dist/store/types.js +0 -1
  105. package/dist/tenant/advance.js +20 -8
  106. package/dist/tenant/agent-manifests.d.ts +72 -0
  107. package/dist/tenant/agent-manifests.js +359 -0
  108. package/dist/tenant/agent-versions.d.ts +126 -0
  109. package/dist/tenant/agent-versions.js +389 -0
  110. package/dist/tenant/agents.d.ts +230 -0
  111. package/dist/tenant/{environments.js → agents.js} +464 -320
  112. package/dist/tenant/auth.d.ts +1 -1
  113. package/dist/tenant/auth.js +3 -3
  114. package/dist/tenant/commands.d.ts +63 -2
  115. package/dist/tenant/commands.js +240 -83
  116. package/dist/tenant/context.d.ts +46 -19
  117. package/dist/tenant/context.js +6 -2
  118. package/dist/tenant/effects.js +7 -16
  119. package/dist/tenant/identity-file.d.ts +6 -6
  120. package/dist/tenant/identity-file.js +12 -10
  121. package/dist/tenant/issuers.js +3 -3
  122. package/dist/tenant/limits.d.ts +0 -3
  123. package/dist/tenant/limits.js +0 -3
  124. package/dist/tenant/memories.d.ts +38 -0
  125. package/dist/tenant/memories.js +119 -0
  126. package/dist/tenant/reset.js +2 -2
  127. package/dist/tenant/run-grants.d.ts +6 -0
  128. package/dist/tenant/run-grants.js +4 -0
  129. package/dist/tenant/runtime.d.ts +1 -1
  130. package/dist/tenant/runtime.js +8 -0
  131. package/dist/tenant/sessions.d.ts +28 -45
  132. package/dist/tenant/sessions.js +85 -208
  133. package/dist/tenant/status.js +6 -7
  134. package/dist/tenant/sweep.js +12 -3
  135. package/dist/tenant/transcript.d.ts +16 -16
  136. package/dist/tenant/triggers.d.ts +143 -0
  137. package/dist/tenant/triggers.js +715 -0
  138. package/dist/tenant/turn-timing.d.ts +11 -1
  139. package/dist/tenant/turn-timing.js +3 -1
  140. package/dist/tenant/types.d.ts +5 -0
  141. package/dist/tenant/worker.d.ts +2 -0
  142. package/dist/tenant/worker.js +5 -0
  143. package/dist/traces/map.d.ts +1 -1
  144. package/dist/traces/map.js +7 -1
  145. package/dist/tuning.d.ts +7 -0
  146. package/dist/tuning.js +19 -0
  147. package/dist/vault/service.d.ts +13 -8
  148. package/dist/vault/service.js +61 -17
  149. package/dist/version.d.ts +1 -1
  150. package/dist/version.js +1 -1
  151. package/package.json +13 -12
  152. package/dist/api/http/routes/environment-manifests.d.ts +0 -9
  153. package/dist/api/http/routes/environment-manifests.js +0 -118
  154. package/dist/api/http/routes/environments.d.ts +0 -11
  155. package/dist/api/http/routes/environments.js +0 -332
  156. package/dist/record/environment.d.ts +0 -27
  157. package/dist/record/environment.js +0 -46
  158. package/dist/tenant/environment-manifests.d.ts +0 -57
  159. package/dist/tenant/environment-manifests.js +0 -208
  160. package/dist/tenant/environments.d.ts +0 -172
@@ -56,7 +56,6 @@ export type FlowHostSession = {
56
56
  ownerUserId: string;
57
57
  manifest: any;
58
58
  manifestHash: string;
59
- implementationVersion: string;
60
59
  status: string;
61
60
  activeTurnId: string | null;
62
61
  info?: any;
@@ -12,7 +12,7 @@
12
12
  * Nothing here knows a tool: the header name and its format are the credential's. A release the
13
13
  * vault refuses (two secrets for one host, an unreadable value) answers 502 and is logged, never
14
14
  * as an event; a value never is. Upgrades (WebSocket) and HTTP/2 are not served on this path.
15
- * Each connection keeps a tally for its `environment.egress` event (R7.3, D67): how many requests it
15
+ * Each connection keeps a tally for its `agent.egress` event (R7.3, D67): how many requests it
16
16
  * read and which credentials it set, by id.
17
17
  */
18
18
  import { createServer } from "node:http";
@@ -1,6 +1,6 @@
1
1
  import { type Server } from "node:http";
2
2
  import { type Socket } from "node:net";
3
- import type { EgressRunningTurn, EnvironmentEventPayload } from "@nylorun/core/contracts";
3
+ import type { EgressRunningTurn, AgentEventPayload } from "@nylorun/core/contracts";
4
4
  import type { EgressTokenVerdict } from "../sandbox/egress-token.js";
5
5
  import type { SessionStore } from "../store/types.js";
6
6
  import type { Logger } from "../tenant/types.js";
@@ -16,10 +16,10 @@ export interface EgressSandboxes {
16
16
  readonly allow: readonly string[];
17
17
  } | undefined>;
18
18
  }
19
- /** One connection's record (R7.3, D67): `environment.egress`'s payload. */
20
- export type EgressConnection = EnvironmentEventPayload<"environment.egress">;
19
+ /** One connection's record (R7.3, D67): `agent.egress`'s payload. */
20
+ export type EgressConnection = AgentEventPayload<"agent.egress">;
21
21
  /**
22
- * How many running turns one connection names, at most: fixed by the `environment.egress` event
22
+ * How many running turns one connection names, at most: fixed by the `agent.egress` event
23
23
  * contract (`runningTurns` holds at most 16 in `/core/contracts`), which every reader
24
24
  * validates against, so not a setting here.
25
25
  */
@@ -90,7 +90,7 @@ export declare function storeEgressSandboxes(store: SessionStore): EgressSandbox
90
90
  /**
91
91
  * `EgressRecord` over the Tenant's record (R7.3, D67). The running turns are the sessions on the
92
92
  * Environment whose body has an `activeTurnId`, in id order, at most `EGRESS_RUNNING_TURNS_MAX`.
93
- * The write is `environment.egress` on the Environment's stream, under its row's lock; a deleted
93
+ * The write is `agent.egress` on the Environment's stream, under its row's lock; a deleted
94
94
  * Environment gets nothing.
95
95
  */
96
96
  export declare function storeEgressRecord(store: SessionStore): EgressRecord;
@@ -25,7 +25,7 @@
25
25
  * never is. Tunnels close after `idleMs` without traffic in either direction.
26
26
  *
27
27
  * Every connection to a named host on a live token is recorded once (R7.3, D67), when it closes
28
- * or at once when the gate refuses it, as `environment.egress` on the Environment's own stream:
28
+ * or at once when the gate refuses it, as `agent.egress` on the Environment's own stream:
29
29
  * the connection is the Environment's, since its token names the pod and turns run in parallel
30
30
  * (D66). `runningTurns` lists the turns active on the Environment as it opened, as context and
31
31
  * never as an owner: one read, started as the name resolves and never waited for, so a turn that
@@ -44,7 +44,7 @@ import { bindListener } from "../host/http.js";
44
44
  import { hostTuning } from "../tuning.js";
45
45
  import { CONNECT_ESTABLISHED, credentialTerminator, } from "./egress-credentials.js";
46
46
  /**
47
- * How many running turns one connection names, at most: fixed by the `environment.egress` event
47
+ * How many running turns one connection names, at most: fixed by the `agent.egress` event
48
48
  * contract (`runningTurns` holds at most 16 in `/core/contracts`), which every reader
49
49
  * validates against, so not a setting here.
50
50
  */
@@ -156,7 +156,7 @@ function connectionRecord(sink, logger, client, at) {
156
156
  const allowed = fields.outcome === "allowed";
157
157
  const durationMs = Math.max(0, Date.now() - at.startedAt.getTime());
158
158
  const connection = (turns) => ({
159
- environmentId: at.environmentId,
159
+ agentId: at.environmentId,
160
160
  host: at.host,
161
161
  port: at.port,
162
162
  ...fields,
@@ -414,7 +414,7 @@ export function storeEgressSandboxes(store) {
414
414
  /**
415
415
  * `EgressRecord` over the Tenant's record (R7.3, D67). The running turns are the sessions on the
416
416
  * Environment whose body has an `activeTurnId`, in id order, at most `EGRESS_RUNNING_TURNS_MAX`.
417
- * The write is `environment.egress` on the Environment's stream, under its row's lock; a deleted
417
+ * The write is `agent.egress` on the Environment's stream, under its row's lock; a deleted
418
418
  * Environment gets nothing.
419
419
  */
420
420
  export function storeEgressRecord(store) {
@@ -429,9 +429,9 @@ export function storeEgressRecord(store) {
429
429
  },
430
430
  async write(connection) {
431
431
  await store.tx(async (t) => {
432
- if (!(await t.environment(connection.environmentId, { lock: true })))
432
+ if (!(await t.environment(connection.agentId, { lock: true })))
433
433
  return;
434
- await t.environmentEvent(connection.environmentId, "environment.egress", connection);
434
+ await t.agentEvent(connection.agentId, "agent.egress", connection);
435
435
  });
436
436
  },
437
437
  };
@@ -198,7 +198,7 @@ export async function runHttpTool(tenant, call, options) {
198
198
  if (authorized.status === "refused")
199
199
  return failed("http.credential", `The credential '${http.credential}' was refused: ${authorized.reason}`);
200
200
  if (authorized.status === "unauthenticated")
201
- return failed("http.credential", `The session's Environment holds no credential '${http.credential}' for ${authorized.url}`);
201
+ return failed("http.credential", `The session's Agent holds no credential '${http.credential}' for ${authorized.url}`);
202
202
  credential = authorized.headers;
203
203
  identity = authorized.identity;
204
204
  // A credential with `via` sends the call there, a gateway (R2b C2, Q9): to its one URL, so
@@ -229,7 +229,7 @@ export async function runHttpTool(tenant, call, options) {
229
229
  return outcome;
230
230
  return {
231
231
  ...outcome,
232
- message: `The service answered HTTP 401: it rejected the credential '${http.credential}' from the session's Environment`,
232
+ message: `The service answered HTTP 401: it rejected the credential '${http.credential}' from the session's Agent`,
233
233
  server: http.credential,
234
234
  };
235
235
  }
@@ -37,7 +37,7 @@ export function createMeter(options) {
37
37
  session: request.sessionId,
38
38
  effect: request.effectId,
39
39
  scope: budget.scope,
40
- ...(budget.scope === "agent" ? { agent: budget.scopeId } : {}),
40
+ ...(budget.scope === "manifest" ? { manifest: budget.scopeId } : {}),
41
41
  });
42
42
  return { keys, refusal: failure("budget_exhausted", reached, false) };
43
43
  }
@@ -124,7 +124,8 @@ export function usageRow(request, outcome, at, timing) {
124
124
  : null,
125
125
  sessionId: request.sessionId,
126
126
  turnId: request.turnId,
127
- agentId: request.agentId,
127
+ // The calling agent's loop manifest id: a manifest version's name (protocol 16).
128
+ manifestName: request.agentId,
128
129
  provider: producer.provider ?? null,
129
130
  model: producer.model ?? outcome.evidence?.resolvedModel ?? request.call.model?.id ?? null,
130
131
  inputTokens,
@@ -141,6 +142,22 @@ export function usageRow(request, outcome, at, timing) {
141
142
  ...(typeof firstTokenMs === "number" && Number.isFinite(firstTokenMs) && firstTokenMs >= 0
142
143
  ? { firstTokenMs: Math.round(firstTokenMs) }
143
144
  : {}),
145
+ ...costSplitColumns(outcome.evidence?.extras?.costSplit),
146
+ };
147
+ }
148
+ /** The ledger's cost-by-kind columns from the adapter's `costSplit`; none unless all four are costs. */
149
+ function costSplitColumns(split) {
150
+ if (!split || typeof split !== "object" || Array.isArray(split))
151
+ return {};
152
+ const { input, cacheRead, cacheWrite, output } = split;
153
+ const parts = [input, cacheRead, cacheWrite, output];
154
+ if (!parts.every((part) => typeof part === "number" && Number.isFinite(part) && part >= 0))
155
+ return {};
156
+ return {
157
+ costInputUsd: input,
158
+ costCacheReadUsd: cacheRead,
159
+ costCacheWriteUsd: cacheWrite,
160
+ costOutputUsd: output,
144
161
  };
145
162
  }
146
163
  /** The ledger rows a budget counts for `request`; undefined when it doesn't apply. */
@@ -148,8 +165,8 @@ function scopeOf(budget, request, at) {
148
165
  const since = budget.period ? { since: periodStart(budget.period, at) } : {};
149
166
  if (budget.scope === "turn")
150
167
  return { scope: "turn", id: request.turnId };
151
- if (budget.scope === "agent")
152
- return budget.scopeId === request.agentId ? { scope: "agent", id: request.agentId, ...since } : undefined;
168
+ if (budget.scope === "manifest")
169
+ return budget.scopeId === request.agentId ? { scope: "manifest", id: request.agentId, ...since } : undefined;
153
170
  return { scope: "tenant", ...since };
154
171
  }
155
172
  /**
@@ -160,7 +177,7 @@ export function capReached(budget, spent, inflight) {
160
177
  const projected = (total) => total + (spent.calls > 0 ? (inflight * total) / spent.calls : 0);
161
178
  const name = budget.scope === "turn"
162
179
  ? "The turn's cap"
163
- : `${budget.scope === "agent" ? `Agent ${budget.scopeId}'s` : "The Tenant's"} ${budget.period === "day" ? "daily" : "monthly"} cap`;
180
+ : `${budget.scope === "manifest" ? `Agent manifest ${budget.scopeId}'s` : "The Tenant's"} ${budget.period === "day" ? "daily" : "monthly"} cap`;
164
181
  const raise = "raise it with PUT /v1/tenant/budgets";
165
182
  if (budget.limitTokens !== null && projected(spent.tokens) >= budget.limitTokens)
166
183
  return `${name} of ${budget.limitTokens} tokens is reached (${spent.tokens} used); ${raise}`;
@@ -35,7 +35,7 @@ export interface PolicyCall {
35
35
  readonly tool: string;
36
36
  /** The MCP server's name. */
37
37
  readonly server?: string;
38
- /** The agent that calls it. */
38
+ /** The agent that calls it: its manifest's id (an agent manifest's name), never an Agent's id. */
39
39
  readonly agent: string;
40
40
  readonly input: unknown;
41
41
  /** The name the model called it by, for display only: no rule matches it. */
@@ -61,8 +61,13 @@ export declare function readToolPolicy(t: Pick<Tx, "getSetting">): Promise<ToolP
61
61
  * pinned manifest's `approval`. A policy that cannot be read refuses the call.
62
62
  */
63
63
  export declare function toolCallDecision(t: Pick<Tx, "getSetting">, s: Session, request: HostEffect, kind: "mcp" | "http"): Promise<PolicyDecision>;
64
- /** `request` as the policy reads it: by its target's identity (protocol 15). */
65
- export declare function policyCallOf(s: Pick<Session, "agentId">, request: HostEffect, kind: "mcp" | "http"): PolicyCall;
64
+ /**
65
+ * `request` as the policy reads it: by its target's identity (protocol 15), and by the agent that
66
+ * calls it: an agent used as a tool by its id, the session's root agent by its loop manifest's id
67
+ * (protocol 16: the agent manifest's name). Never the session's Agent id (`s.agentId`), which
68
+ * protocol 16 made the Agent the session runs on (`users/ada/desk` for the manifest `desk`).
69
+ */
70
+ export declare function policyCallOf(s: Pick<Session, "manifest">, request: HostEffect, kind: "mcp" | "http"): PolicyCall;
66
71
  /** Decides `call` by `rules`; `pinnedAsk` adds the ask of a manifest saved before R5 step 2. */
67
72
  export declare function decide(rules: readonly ToolRule[], call: PolicyCall, pinnedAsk?: boolean): PolicyDecision;
68
73
  /**
@@ -68,7 +68,12 @@ export async function toolCallDecision(t, s, request, kind) {
68
68
  }
69
69
  return decide(rules, policyCallOf(s, request, kind), pinnedApproval(turnManifestOf(s), request));
70
70
  }
71
- /** `request` as the policy reads it: by its target's identity (protocol 15). */
71
+ /**
72
+ * `request` as the policy reads it: by its target's identity (protocol 15), and by the agent that
73
+ * calls it: an agent used as a tool by its id, the session's root agent by its loop manifest's id
74
+ * (protocol 16: the agent manifest's name). Never the session's Agent id (`s.agentId`), which
75
+ * protocol 16 made the Agent the session runs on (`users/ada/desk` for the manifest `desk`).
76
+ */
72
77
  export function policyCallOf(s, request, kind) {
73
78
  const target = request.target;
74
79
  return {
@@ -77,7 +82,7 @@ export function policyCallOf(s, request, kind) {
77
82
  ? target.tool
78
83
  : (request.key ?? request.effectId),
79
84
  ...(target?.kind === "mcp" ? { server: target.server } : {}),
80
- agent: request.agent?.id ?? s.agentId,
85
+ agent: request.agent?.id ?? s.manifest.id,
81
86
  input: request.input,
82
87
  ...(request.toolName === undefined ? {} : { name: request.toolName }),
83
88
  };
@@ -29,7 +29,7 @@ export interface TenantVault {
29
29
  session(sessionId: string): Promise<Session | undefined>;
30
30
  /**
31
31
  * The authorization of one request to a session's remote MCP server or HTTP tool: its
32
- * credential from the vault of the session's Environment.
32
+ * credential from the vault of the session's Agent.
33
33
  */
34
34
  authorizeMcp(sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
35
35
  /** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
@@ -2,6 +2,8 @@ import type { PodHostConfig } from "../host/stack-config.js";
2
2
  /** What to run and copy on a new disk (`sandbox/setup.ts` in core), from the join answer. */
3
3
  export interface DiskSetup {
4
4
  readonly recipeVersion: number;
5
+ /** The sha256 of the commands and files (track T3): a marker with another runs the setup again. */
6
+ readonly setupHash?: string;
5
7
  readonly commands: readonly {
6
8
  readonly command: string;
7
9
  readonly cwd: string;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * The disk's setup in a pod Environment's engine (R2d step 2): after it joins and before it takes
3
3
  * any turn, the engine copies the recipe's input files and runs its setup commands, once per
4
- * volume. A marker on the volume (`/harness/setup.json`, `{ volumeId, recipeVersion, status }`)
5
- * says it was done: a resume finds it and skips, a reset (a new volume, a new volume id) runs it
6
- * again. Commands run as the sandbox tools' do (`localExec`): `/bin/sh -c` in `cwd`, with the
4
+ * volume. A marker on the volume (`/harness/setup.json`, `{ volumeId, recipeVersion, setupHash,
5
+ * status }`) says it was done: a resume finds it and skips, a reset (a new volume, a new volume
6
+ * id) runs it again, and so does a start on a new version whose setup differs (`setupHash`,
7
+ * track T3). A marker without a hash (written before T3) counts as this setup's. Commands run as the sandbox tools' do (`localExec`): `/bin/sh -c` in `cwd`, with the
7
8
  * Environment's variables and secrets (as the sentinel) and egress through egress-gate.
8
9
  *
9
10
  * A failure is reported to core, which fails the pod and the turn waiting for it; the engine then
@@ -17,6 +18,15 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
17
18
  import { randomUUID } from "node:crypto";
18
19
  import { dirname, posix } from "node:path";
19
20
  import { hostTuning } from "../tuning.js";
21
+ /** The marker of `setup` done on volume `volumeId`. */
22
+ function markerOf(volumeId, setup) {
23
+ return {
24
+ volumeId,
25
+ recipeVersion: setup?.recipeVersion ?? 0,
26
+ ...(setup?.setupHash === undefined ? {} : { setupHash: setup.setupHash }),
27
+ status: "ok",
28
+ };
29
+ }
20
30
  async function readText(path) {
21
31
  try {
22
32
  return (await readFile(path, "utf8")).trim();
@@ -111,7 +121,7 @@ export async function prepareDisk(options) {
111
121
  else if (options.host.seeded) {
112
122
  // A volume seeded from a disk version: the version holds what setup made, so mark it done.
113
123
  if (volumeId) {
114
- const done = { volumeId, recipeVersion: setup?.recipeVersion ?? 0, status: "ok" };
124
+ const done = markerOf(volumeId, setup);
115
125
  await writeAtomic(options.markerFile, new TextEncoder().encode(`${JSON.stringify(done)}\n`));
116
126
  }
117
127
  options.logger.info("environment_setup_skipped", { reason: "seeded" });
@@ -123,7 +133,8 @@ export async function prepareDisk(options) {
123
133
  if (marker && volumeId) {
124
134
  try {
125
135
  const done = JSON.parse(marker);
126
- if (done.volumeId === volumeId && done.status === "ok")
136
+ const same = done.setupHash === undefined || setup.setupHash === undefined || done.setupHash === setup.setupHash;
137
+ if (done.volumeId === volumeId && done.status === "ok" && same)
127
138
  return;
128
139
  }
129
140
  catch {
@@ -137,7 +148,7 @@ export async function prepareDisk(options) {
137
148
  const report = await runOnce(options, setup);
138
149
  if (report.status === "ok") {
139
150
  if (volumeId) {
140
- const done = { volumeId, recipeVersion: setup.recipeVersion, status: "ok" };
151
+ const done = markerOf(volumeId, setup);
141
152
  await writeAtomic(options.markerFile, new TextEncoder().encode(`${JSON.stringify(done)}\n`));
142
153
  }
143
154
  await options.host.reportSetup(report).catch((error) => options.logger.warn("environment_setup_report_failed", {
@@ -38,6 +38,8 @@ import { abortKind } from "../tenant/worker.js";
38
38
  import { redactTokens } from "./redact.js";
39
39
  import { toolCallDecision } from "../gates/policy.js";
40
40
  import { traceKey } from "../traces/key.js";
41
+ import { serveRuntimeTool } from "../tenant/triggers.js";
42
+ import { serveMemoryTool } from "../memory/tool.js";
41
43
  import { effectSpanOf, traceparentOf, turnTraceIdOf } from "../traces/ids.js";
42
44
  import { firstSend, interrupted, settleToolRow, toolKindOf, } from "./tool-journal.js";
43
45
  const TURN_CANCELLED = () => new HarnessApiError("turn_cancelled", "Turn cancelled");
@@ -138,6 +140,14 @@ export async function recordIntent(scope, effect, requestHash) {
138
140
  const agentManifest = manifestFor(s.manifest, request.agent);
139
141
  if (!agentManifest)
140
142
  throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
143
+ // An operation the Runtime serves itself (track T4): settled here, in the call's own
144
+ // transaction, keyed by its effect id, so a replay finds it completed.
145
+ if (request.kind === "tool" && request.target?.kind === "runtime") {
146
+ const row = { request, requestHash, status: "invoking", ...firstSend(toolKindOf(s, agentManifest, request)) };
147
+ const served = request.target.operation === "memory" ? await serveMemoryTool(t, s, request) : await serveRuntimeTool(t, s, request);
148
+ const outcome = await settleToolRow(t, s, row, served, { writer: coreIn(scope) });
149
+ return { status: "completed", outcome };
150
+ }
141
151
  const refused = request.kind === "tool" ? refusal(s, request) : undefined;
142
152
  if (refused) {
143
153
  // A call with no target (an author's code tool, refused at save, so a backstop), or one
@@ -188,6 +198,7 @@ export async function recordIntent(scope, effect, requestHash) {
188
198
  * - `http`: an HTTP tool its agent declares under that name (R2 M3), which crosses the Tool Gate;
189
199
  * - `sandbox`: one of the sandbox operations, in a session with a sandbox;
190
200
  * - `harness`: a tool the harness runs in its own process; its egress gate is its only control.
201
+ * - `runtime`: served by `recordIntent` itself, before this (`schedule_wake`, track T4).
191
202
  *
192
203
  * No target, and anything else, runs nothing (`tool.unavailable`).
193
204
  */
@@ -45,6 +45,7 @@ import { RecordEntryTooLargeError } from "../record/index.js";
45
45
  import { redactTokens } from "./redact.js";
46
46
  import { beat, renewEveryMs, tokenOf } from "./renew.js";
47
47
  import { hostTuning } from "../tuning.js";
48
+ import { passStartCheck } from "../tenant/agent-versions.js";
48
49
  /** No attached harness serves workspaces (or, with `pod`, that pod sandbox's). */
49
50
  export class NoWorkspaceHarness extends Error {
50
51
  pod;
@@ -211,6 +212,9 @@ export function createHarnessApiServer(ctx, options = {}) {
211
212
  const expected = environment?.applied.harness ?? DEFAULT_HARNESS;
212
213
  if (expected !== name)
213
214
  throw new HarnessApiError("invalid", `Environment ${connection.host.sandboxId} runs the ${expected} harness, and this pod says ${name}`);
215
+ // Its pod started on the version it switched to, setup done (track T3): the start check passed.
216
+ if (environment?.startCheck)
217
+ await passStartCheck(ctx, connection.host.sandboxId, connection.host.epoch);
214
218
  }
215
219
  return name;
216
220
  };
@@ -1,4 +1,4 @@
1
- import type { ModelDescriptor, RecordedOutcome, RuntimeLimits, TurnEnvironment, TurnStart } from "@nylorun/core/harness-api";
1
+ import type { ModelDescriptor, RecordedOutcome, RuntimeLimits, TurnAgent, TurnMemory, TurnStart } from "@nylorun/core/harness-api";
2
2
  import { type SandboxManifest } from "@nylorun/core/define";
3
3
  import type { SandboxPodState } from "../store/types.js";
4
4
  import { type Lease, type Session, type TenantContext } from "../tenant/context.js";
@@ -14,7 +14,7 @@ export interface SegmentStart {
14
14
  /** The record position of `transcript` (`foldTranscriptWithCursor`). */
15
15
  readonly cursor: number;
16
16
  readonly outcomes: RecordedOutcome[];
17
- /** The session's sandbox workspace: the session, and the Environment whose workspace it is. */
17
+ /** The session's sandbox workspace: the session, and the Agent whose workspace it is. */
18
18
  readonly sandbox: {
19
19
  readonly ownerId: string;
20
20
  readonly environmentId?: string;
@@ -24,18 +24,20 @@ export interface SegmentStart {
24
24
  readonly spec?: SandboxManifest;
25
25
  };
26
26
  /**
27
- * The Environment the session runs in, as `TurnStart.environment` states it (protocol 15): its
28
- * id and kind, and its sandbox's workspace when the session has a sandbox.
27
+ * The Agent the session runs on, as `TurnStart.agent` states it (Harness API 5): its id and
28
+ * kind, and its sandbox's workspace when the session has a sandbox.
29
29
  */
30
- readonly environment?: TurnEnvironment;
30
+ readonly agent?: TurnAgent;
31
31
  /**
32
- * The Environment is a pod (F7.2): its engine runs the segment. Its lifecycle state, and
33
- * whether its new volume is still being seeded from a disk version (R2d step 3).
32
+ * The Agent is a pod (F7.2): its engine runs the segment. Its lifecycle state, and whether its
33
+ * new volume is still being seeded from a disk version (R2d step 3).
34
34
  */
35
35
  readonly pod?: {
36
36
  readonly id: string;
37
37
  readonly pod: SandboxPodState;
38
38
  readonly seeding?: boolean;
39
+ /** It starts on a new version (track T3) and has not joined yet: a failed start goes back. */
40
+ readonly starting?: boolean;
39
41
  };
40
42
  /** The turn's span context (R7.4): what `TurnStart.trace` hands the harness. */
41
43
  readonly trace?: TraceContext;
@@ -44,6 +46,8 @@ export interface SegmentStart {
44
46
  * hands an agent's harness. Absent for a flow, and when the Tenant has no model.
45
47
  */
46
48
  readonly model?: ModelDescriptor;
49
+ /** The Agent's memory (track T5): its access, instructions and the index the harness pins. */
50
+ readonly memory?: TurnMemory;
47
51
  }
48
52
  /** The segment's starting transaction and fold. */
49
53
  export declare function startSegment(ctx: TenantContext, lease: Lease,
@@ -11,6 +11,7 @@ import { isWorkflowManifest } from "../core/flow-host.js";
11
11
  import { sandboxWorkspaceOf } from "../sandbox/share.js";
12
12
  import { ownedTx } from "../store/ownership.js";
13
13
  import { harnessOf } from "../tenant/context.js";
14
+ import { repinTurn } from "../tenant/agent-versions.js";
14
15
  import { HARNESS_ID } from "../sandbox/placement.js";
15
16
  import { checkParity, foldTranscriptWithCursor, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "../tenant/history.js";
16
17
  import { usesFixtureModel } from "../tenant/model-setting.js";
@@ -20,6 +21,8 @@ import { flowLimitsOf } from "../core/limits.js";
20
21
  import { turnManifestOf } from "../tenant/session.js";
21
22
  import { mcpFactsOf } from "../mcp/snapshot.js";
22
23
  import { bulkOutcomes } from "./record.js";
24
+ import { renderMemoryIndex } from "../memory/service.js";
25
+ import { memorySettingsOf } from "../memory/tool.js";
23
26
  import { traceKey } from "../traces/key.js";
24
27
  import { hostTuning } from "../tuning.js";
25
28
  import { traceparentOf, turnSpanOf, turnTraceIdOf, } from "../traces/ids.js";
@@ -31,6 +34,9 @@ options = {}) {
31
34
  // Read before the transaction: the first read of the Tenant may create the key.
32
35
  const key = options.harness ? await traceKey(ctx.store) : undefined;
33
36
  const opened = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
37
+ // A turn's first segment runs the version its Agent runs now (T3, D73).
38
+ if (options.harness && (await repinTurn(t, current)))
39
+ await t.put("sessions", id, current);
34
40
  // A transcript left on our engine's state by an older Runtime is recorded once; another
35
41
  // harness's state is its own (R4.2).
36
42
  const limits = await readRuntimeLimits(t);
@@ -52,16 +58,25 @@ options = {}) {
52
58
  ...(current.sandbox === undefined ? {} : { spec: current.sandbox }),
53
59
  };
54
60
  const turnId = current.activeTurnId;
61
+ const memory = isWorkflowManifest(current.manifest) ? undefined : await turnMemoryOf(t, current, limits);
55
62
  return {
56
63
  current,
57
64
  fixtureModel,
58
65
  limits,
66
+ ...(memory ? { memory } : {}),
59
67
  outcomes: current.checkpoint ? await bulkOutcomes(t, current, current.checkpoint) : [],
60
68
  sandbox,
61
- ...(resource ? { environment: environmentOf(resource, current.sandbox) } : {}),
69
+ ...(resource ? { agent: agentOf(resource, current.sandbox, await selfWakeOf(t, current)) } : {}),
62
70
  ...(key && turnId ? { trace: turnContextOf(key, ctx.store.tenantId, current, turnId) } : {}),
63
71
  ...(resource?.pod
64
- ? { pod: { id: resource.id, pod: resource.pod, ...(resource.diskSeed ? { seeding: true } : {}) } }
72
+ ? {
73
+ pod: {
74
+ id: resource.id,
75
+ pod: resource.pod,
76
+ ...(resource.diskSeed ? { seeding: true } : {}),
77
+ ...(resource.startCheck ? { starting: true } : {}),
78
+ },
79
+ }
65
80
  : {}),
66
81
  };
67
82
  });
@@ -116,20 +131,49 @@ export function buildTurnStart(ctx, segment) {
116
131
  ...(segment.trace ? { trace: segment.trace } : {}),
117
132
  limits: harnessLimitsOf(segment.limits),
118
133
  ...(segment.model && !flow ? { model: segment.model } : {}),
119
- ...(segment.environment && !flow ? { environment: segment.environment } : {}),
134
+ ...(segment.agent && !flow ? { agent: segment.agent } : {}),
135
+ // The agent manifest version the session pinned, and its harness's own settings (protocol 16).
136
+ ...(current.definition === undefined ? {} : { definition: current.definition }),
137
+ ...(current.loop === undefined || flow ? {} : { loop: current.loop }),
138
+ ...(segment.memory && !flow ? { memory: segment.memory } : {}),
120
139
  };
121
140
  }
122
141
  /**
123
- * The facts of the Environment a session runs in (protocol 15): its id and kind, and, with the
142
+ * The Agent's memory as the segment starts (track T5, D72), when the version the session runs sets
143
+ * `harness.memory`: its access (the same in every turn), the agent's instructions, and the index of
144
+ * its memories, newest first, within the Tenant's index limits.
145
+ */
146
+ async function turnMemoryOf(t, s, limits) {
147
+ const settings = await memorySettingsOf(t, s);
148
+ if (!settings)
149
+ return undefined;
150
+ const { index, truncated } = await renderMemoryIndex(t, s.agentId, limits);
151
+ return {
152
+ access: settings.access ?? "read_write",
153
+ ...(settings.instructions === undefined ? {} : { instructions: settings.instructions }),
154
+ index,
155
+ truncated,
156
+ };
157
+ }
158
+ /**
159
+ * The facts of the Agent a session runs on (Harness API 5): its id and kind, and, with the
124
160
  * session's sandbox, the workspace its commands start in. The harness words them.
125
161
  */
126
- function environmentOf(resource, spec) {
162
+ function agentOf(resource, spec, selfWake) {
127
163
  return {
128
164
  id: resource.id,
129
165
  kind: resource.kind,
130
166
  ...(spec === undefined ? {} : { sandbox: { workspace: SANDBOX_WORKSPACE } }),
167
+ ...(selfWake ? { selfWake: true } : {}),
131
168
  };
132
169
  }
170
+ /** Whether the manifest version the session runs sets `runtime.selfWake` (track T4, D77). */
171
+ async function selfWakeOf(t, s) {
172
+ if (!s.definition)
173
+ return false;
174
+ const version = await t.agentManifest(s.definition.name, s.definition.version);
175
+ return version?.body.runtime?.selfWake === true;
176
+ }
133
177
  /**
134
178
  * The context of turn `turnId`'s span (R7.4): its trace (the caller's when its message carried a
135
179
  * `traceparent`) and its span, as the trace projector derives them, with the caller's
@@ -15,7 +15,7 @@
15
15
  * - **Pod sandboxes** (F7.2, `withPodWorkspaces`): a session attached to a pod sandbox has its
16
16
  * workspace in the pod, served by the sandbox's engine (its host connection). Its tool calls
17
17
  * and disk operations go there whatever serves the other workspaces; with the pod stopped or
18
- * starting they are `409 environment_unavailable`.
18
+ * starting they are `409 agent_unavailable`.
19
19
  */
20
20
  import { HarnessApiError } from "@nylorun/core/harness-api";
21
21
  import { fromWireAnswer, toWireOp } from "../sandbox/disk-fs.js";
@@ -57,7 +57,7 @@ export function remoteWorkspace(options) {
57
57
  : { pod: options.pod, ...(options.minEpoch === undefined ? {} : { minEpoch: options.minEpoch }) };
58
58
  const unavailable = (error) => error.pod === undefined
59
59
  ? fail(503, error.message, { code: "request_rejected" })
60
- : fail(409, error.message, { code: "environment_unavailable" });
60
+ : fail(409, error.message, { code: "agent_unavailable" });
61
61
  /** The harness's answer, or `undefined` when none serves workspaces. */
62
62
  const ask = async (method, params, signal) => {
63
63
  try {
@@ -14,4 +14,6 @@ export declare function configForFactory(options: {
14
14
  delivery?: TenantConfig["delivery"];
15
15
  /** The trusted issuers of the identity file (`NYLORUN_IDENTITY_FILE`). */
16
16
  issuers?: TenantConfig["issuers"];
17
+ /** The URL clients reach the Runtime API at (`NYLORUN_PUBLIC_URL`). */
18
+ publicUrl?: string;
17
19
  }): (id: string) => TenantConfig;
@@ -16,6 +16,7 @@ export function configForFactory(options) {
16
16
  model: options.model ?? { kind: "vault" },
17
17
  ...(options.delivery ? { delivery: options.delivery } : {}),
18
18
  ...(options.issuers ? { issuers: options.issuers } : {}),
19
+ ...(options.publicUrl ? { publicUrl: options.publicUrl } : {}),
19
20
  logger: {
20
21
  info: (message, fields) => options.logger.info(message, { tenantId: id, ...fields }),
21
22
  warn: (message, fields) => options.logger.warn(message, { tenantId: id, ...fields }),
@@ -44,8 +44,8 @@ export declare function requestHasBody(request: IncomingMessage): boolean;
44
44
  /** `/health` and `/ready` are polled; the request log leaves them out. */
45
45
  export declare function pathnameIsLogged(rawUrl: string | undefined): boolean;
46
46
  /**
47
- * Redact path parameters after `/v1/sessions/` for Host logs, and a capability link's token
48
- * (a credential) after `/v1/artifact-links/`.
47
+ * Redact path parameters after `/v1/sessions/` for Host logs, a capability link's token (a
48
+ * credential) after `/v1/artifact-links/`, and a webhook endpoint's id after `/v1/webhooks/`.
49
49
  */
50
50
  export declare function redactRoutePath(pathname: string): string;
51
51
  /** Constant-time comparison of SHA-256 digests of a presented and a stored secret. */
package/dist/host/http.js CHANGED
@@ -136,8 +136,8 @@ export function pathnameIsLogged(rawUrl) {
136
136
  return pathname !== "/health" && pathname !== "/ready";
137
137
  }
138
138
  /**
139
- * Redact path parameters after `/v1/sessions/` for Host logs, and a capability link's token
140
- * (a credential) after `/v1/artifact-links/`.
139
+ * Redact path parameters after `/v1/sessions/` for Host logs, a capability link's token (a
140
+ * credential) after `/v1/artifact-links/`, and a webhook endpoint's id after `/v1/webhooks/`.
141
141
  */
142
142
  export function redactRoutePath(pathname) {
143
143
  const parts = pathname.split("/");
@@ -147,6 +147,9 @@ export function redactRoutePath(pathname) {
147
147
  }
148
148
  if (parts[1] === "v1" && parts[2] === "artifact-links" && parts.length > 3)
149
149
  return "/v1/artifact-links/:token";
150
+ // A webhook endpoint's id is its address: whoever has it may try to deliver (T4).
151
+ if (parts[1] === "v1" && parts[2] === "webhooks" && parts.length > 3)
152
+ return "/v1/webhooks/:endpointId";
150
153
  return pathname;
151
154
  }
152
155
  /** Constant-time comparison of SHA-256 digests of a presented and a stored secret. */
package/dist/host/main.js CHANGED
@@ -279,6 +279,7 @@ export async function main() {
279
279
  logger,
280
280
  ...(stack.delivery ? { delivery: stack.delivery } : {}),
281
281
  ...(issuers ? { issuers } : {}),
282
+ ...(stack.publicUrl ? { publicUrl: stack.publicUrl } : {}),
282
283
  });
283
284
  // The process's Durable Session Execution: Restate when its endpoints are set, else the
284
285
  // in-process memory execution. An invocation that arrives while the Tenant opens waits for it.