@nylorun/runtime 0.23.0-beta → 0.24.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +79 -28
  3. package/dist/adapters/execution/restate.js +1 -1
  4. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  5. package/dist/adapters/replication/pgoutput.js +6 -3
  6. package/dist/adapters/sandbox/local.d.ts +9 -1
  7. package/dist/adapters/sandbox/local.js +16 -4
  8. package/dist/adapters/sandbox/virtual.js +4 -1
  9. package/dist/api/a2a/endpoint.js +5 -1
  10. package/dist/api/a2a/routes.d.ts +2 -0
  11. package/dist/api/a2a/routes.js +2 -2
  12. package/dist/api/a2a/tasks.js +2 -1
  13. package/dist/api/ag-ui/endpoint.js +4 -2
  14. package/dist/api/ag-ui/history.js +4 -1
  15. package/dist/api/ag-ui/routes.d.ts +4 -1
  16. package/dist/api/ag-ui/routes.js +11 -10
  17. package/dist/api/ag-ui/translate.js +3 -2
  18. package/dist/api/components.d.ts +4953 -1204
  19. package/dist/api/components.js +25 -16
  20. package/dist/api/gate/keys.js +7 -1
  21. package/dist/api/gate/routes.d.ts +6 -0
  22. package/dist/api/gate/routes.js +39 -3
  23. package/dist/api/http/app.js +4 -4
  24. package/dist/api/http/define.d.ts +6 -0
  25. package/dist/api/http/define.js +13 -1
  26. package/dist/api/http/respond.js +1 -1
  27. package/dist/api/http/routes/artifacts.js +35 -8
  28. package/dist/api/http/routes/coverage.d.ts +9 -0
  29. package/dist/api/http/routes/coverage.js +34 -0
  30. package/dist/api/http/routes/environments.d.ts +11 -0
  31. package/dist/api/http/routes/environments.js +283 -0
  32. package/dist/api/http/routes/mcp.d.ts +1 -1
  33. package/dist/api/http/routes/mcp.js +2 -2
  34. package/dist/api/http/routes/me.js +1 -1
  35. package/dist/api/http/routes/reads.d.ts +45 -9
  36. package/dist/api/http/routes/reads.js +54 -5
  37. package/dist/api/http/routes/sessions.js +11 -6
  38. package/dist/api/http/routes/tenant.js +32 -10
  39. package/dist/api/openapi.js +31 -26
  40. package/dist/artifacts/disk.d.ts +59 -0
  41. package/dist/artifacts/disk.js +563 -0
  42. package/dist/artifacts/export.d.ts +1 -1
  43. package/dist/artifacts/export.js +2 -3
  44. package/dist/artifacts/folders.d.ts +6 -3
  45. package/dist/artifacts/folders.js +17 -12
  46. package/dist/artifacts/service.d.ts +2 -1
  47. package/dist/artifacts/service.js +6 -1
  48. package/dist/artifacts/workspace.d.ts +1 -1
  49. package/dist/artifacts/workspace.js +1 -1
  50. package/dist/core/flow-host.d.ts +2 -3
  51. package/dist/core/flow-host.js +3 -0
  52. package/dist/core/provider.d.ts +3 -2
  53. package/dist/core/provider.js +5 -37
  54. package/dist/core/sandbox-routes.d.ts +2 -21
  55. package/dist/core/sandbox-routes.js +10 -43
  56. package/dist/execution/types.d.ts +1 -1
  57. package/dist/gates/contract.d.ts +3 -24
  58. package/dist/gates/contract.js +1 -36
  59. package/dist/gates/egress-credentials.d.ts +9 -0
  60. package/dist/gates/egress-credentials.js +11 -4
  61. package/dist/gates/egress.d.ts +28 -1
  62. package/dist/gates/egress.js +115 -10
  63. package/dist/gates/fixture-model.d.ts +28 -0
  64. package/dist/gates/fixture-model.js +92 -0
  65. package/dist/gates/handler.js +4 -4
  66. package/dist/gates/http-tool.d.ts +6 -2
  67. package/dist/gates/http-tool.js +3 -5
  68. package/dist/gates/in-process.d.ts +5 -0
  69. package/dist/gates/in-process.js +12 -1
  70. package/dist/gates/journal.d.ts +30 -0
  71. package/dist/gates/journal.js +52 -0
  72. package/dist/gates/mcp-handler.d.ts +4 -0
  73. package/dist/gates/mcp-handler.js +5 -1
  74. package/dist/gates/meter.d.ts +10 -2
  75. package/dist/gates/meter.js +20 -5
  76. package/dist/gates/policy.d.ts +49 -0
  77. package/dist/gates/policy.js +134 -0
  78. package/dist/gates/tenant-vaults.d.ts +7 -7
  79. package/dist/gates/tenant-vaults.js +5 -5
  80. package/dist/gates/tool-calls.d.ts +10 -5
  81. package/dist/gates/tool-calls.js +28 -10
  82. package/dist/gates/tool-client.d.ts +8 -0
  83. package/dist/gates/tool-client.js +27 -11
  84. package/dist/gates/tool-contract.d.ts +10 -68
  85. package/dist/gates/tool-contract.js +1 -85
  86. package/dist/gates/tool-gate.d.ts +1 -1
  87. package/dist/harness/calls.d.ts +12 -5
  88. package/dist/harness/calls.js +21 -12
  89. package/dist/harness/executors.js +12 -9
  90. package/dist/harness/main.js +18 -5
  91. package/dist/harness/pod.d.ts +34 -0
  92. package/dist/harness/pod.js +41 -0
  93. package/dist/harness/service.d.ts +0 -1
  94. package/dist/harness/service.js +13 -5
  95. package/dist/harness/setup.d.ts +27 -0
  96. package/dist/harness/setup.js +150 -0
  97. package/dist/harness/ws-client.js +3 -2
  98. package/dist/harness/ws-port.d.ts +1 -5
  99. package/dist/harness/ws-port.js +1 -5
  100. package/dist/harness-api/in-process.js +3 -3
  101. package/dist/harness-api/record.d.ts +15 -1
  102. package/dist/harness-api/record.js +174 -44
  103. package/dist/harness-api/redact.d.ts +3 -0
  104. package/dist/harness-api/redact.js +45 -0
  105. package/dist/harness-api/server.d.ts +8 -0
  106. package/dist/harness-api/server.js +93 -13
  107. package/dist/harness-api/start.d.ts +10 -3
  108. package/dist/harness-api/start.js +40 -10
  109. package/dist/harness-api/tool-journal.d.ts +96 -0
  110. package/dist/harness-api/tool-journal.js +102 -0
  111. package/dist/harness-api/tool-status.d.ts +13 -0
  112. package/dist/harness-api/tool-status.js +26 -0
  113. package/dist/harness-api/workspace.d.ts +6 -3
  114. package/dist/harness-api/workspace.js +27 -11
  115. package/dist/harness-api/ws-server.d.ts +11 -3
  116. package/dist/harness-api/ws-server.js +94 -15
  117. package/dist/host/gates.d.ts +2 -1
  118. package/dist/host/gates.js +10 -2
  119. package/dist/host/main.js +54 -5
  120. package/dist/host/operate.js +67 -5
  121. package/dist/host/stack-config.d.ts +64 -0
  122. package/dist/host/stack-config.js +61 -1
  123. package/dist/keys/client.js +4 -1
  124. package/dist/keys/keys.d.ts +5 -5
  125. package/dist/keys/keys.js +5 -7
  126. package/dist/management-openapi.json +999 -2211
  127. package/dist/mcp/connect.d.ts +25 -20
  128. package/dist/mcp/connect.js +29 -30
  129. package/dist/mcp/pool.d.ts +2 -2
  130. package/dist/mcp/pool.js +1 -1
  131. package/dist/mcp/preview.d.ts +3 -3
  132. package/dist/mcp/preview.js +10 -20
  133. package/dist/mcp/snapshot.d.ts +5 -11
  134. package/dist/mcp/snapshot.js +6 -7
  135. package/dist/model/pi-model.js +8 -0
  136. package/dist/openapi.json +6955 -2526
  137. package/dist/reads/types.d.ts +16 -3
  138. package/dist/record/append.d.ts +3 -1
  139. package/dist/record/append.js +1 -0
  140. package/dist/record/envelope.d.ts +5 -1
  141. package/dist/record/envelope.js +7 -3
  142. package/dist/record/environment.d.ts +25 -0
  143. package/dist/record/environment.js +44 -0
  144. package/dist/record/index.d.ts +1 -1
  145. package/dist/record/index.js +1 -1
  146. package/dist/sandbox/disk-fs.d.ts +95 -0
  147. package/dist/sandbox/disk-fs.js +206 -0
  148. package/dist/sandbox/join.d.ts +17 -18
  149. package/dist/sandbox/join.js +42 -15
  150. package/dist/sandbox/manager.d.ts +16 -8
  151. package/dist/sandbox/manager.js +33 -17
  152. package/dist/sandbox/placement.d.ts +13 -12
  153. package/dist/sandbox/placement.js +29 -12
  154. package/dist/sandbox/pods/client.d.ts +6 -1
  155. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  156. package/dist/sandbox/pods/lifecycle.js +14 -26
  157. package/dist/sandbox/pods/reconcile.js +18 -12
  158. package/dist/sandbox/pods/spec.d.ts +15 -13
  159. package/dist/sandbox/pods/spec.js +16 -20
  160. package/dist/sandbox/records.d.ts +2 -2
  161. package/dist/sandbox/records.js +3 -3
  162. package/dist/sandbox/resolve.d.ts +35 -27
  163. package/dist/sandbox/resolve.js +66 -41
  164. package/dist/sandbox/session-sandbox.js +1 -1
  165. package/dist/sandbox/setup.d.ts +61 -0
  166. package/dist/sandbox/setup.js +123 -0
  167. package/dist/sandbox/share.d.ts +11 -21
  168. package/dist/sandbox/share.js +12 -41
  169. package/dist/sandbox/tenant-config.d.ts +12 -14
  170. package/dist/sandbox/tenant-config.js +12 -17
  171. package/dist/sandbox/types.d.ts +6 -0
  172. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  173. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  174. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  175. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  176. package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
  177. package/dist/store/postgres/reads.js +117 -17
  178. package/dist/store/postgres/record-writer.d.ts +4 -4
  179. package/dist/store/postgres/record-writer.js +9 -9
  180. package/dist/store/postgres/record.d.ts +7 -0
  181. package/dist/store/postgres/record.js +23 -2
  182. package/dist/store/postgres/schema.d.ts +934 -761
  183. package/dist/store/postgres/schema.js +102 -73
  184. package/dist/store/postgres/store.js +119 -66
  185. package/dist/store/types.d.ts +109 -48
  186. package/dist/tenant/advance.d.ts +7 -6
  187. package/dist/tenant/advance.js +115 -42
  188. package/dist/tenant/artifact-tool.js +3 -4
  189. package/dist/tenant/commands.d.ts +15 -2
  190. package/dist/tenant/commands.js +93 -12
  191. package/dist/tenant/context.d.ts +48 -22
  192. package/dist/tenant/context.js +4 -15
  193. package/dist/tenant/effects.d.ts +1 -1
  194. package/dist/tenant/effects.js +37 -20
  195. package/dist/tenant/environments.d.ts +167 -0
  196. package/dist/tenant/environments.js +823 -0
  197. package/dist/tenant/harness-tokens.d.ts +21 -0
  198. package/dist/tenant/harness-tokens.js +61 -0
  199. package/dist/tenant/host-token.d.ts +1 -1
  200. package/dist/tenant/host-token.js +2 -4
  201. package/dist/tenant/identity-file.d.ts +4 -4
  202. package/dist/tenant/identity-file.js +17 -8
  203. package/dist/tenant/issuers.js +3 -3
  204. package/dist/tenant/principals.js +1 -1
  205. package/dist/tenant/reset.js +3 -3
  206. package/dist/tenant/runtime.d.ts +6 -1
  207. package/dist/tenant/runtime.js +7 -1
  208. package/dist/tenant/session.d.ts +3 -3
  209. package/dist/tenant/session.js +0 -14
  210. package/dist/tenant/sessions.d.ts +13 -13
  211. package/dist/tenant/sessions.js +61 -138
  212. package/dist/tenant/signing-keys.d.ts +5 -1
  213. package/dist/tenant/status.js +3 -3
  214. package/dist/tenant/sweep.js +12 -6
  215. package/dist/tenant/transcript.d.ts +210 -20
  216. package/dist/tenant/transcript.js +62 -11
  217. package/dist/tenant/turn-timing.d.ts +43 -0
  218. package/dist/tenant/turn-timing.js +39 -0
  219. package/dist/tenant/types.d.ts +2 -0
  220. package/dist/traces/ids.d.ts +42 -0
  221. package/dist/traces/ids.js +85 -0
  222. package/dist/traces/key.d.ts +6 -0
  223. package/dist/traces/key.js +37 -0
  224. package/dist/traces/map.d.ts +113 -0
  225. package/dist/traces/map.js +317 -0
  226. package/dist/traces/options.d.ts +7 -0
  227. package/dist/traces/options.js +1 -0
  228. package/dist/traces/otlp.d.ts +42 -0
  229. package/dist/traces/otlp.js +84 -0
  230. package/dist/traces/projector.d.ts +58 -0
  231. package/dist/traces/projector.js +200 -0
  232. package/dist/traces/read.d.ts +23 -0
  233. package/dist/traces/read.js +16 -0
  234. package/dist/vault/coverage.d.ts +26 -0
  235. package/dist/vault/coverage.js +64 -0
  236. package/dist/vault/error.d.ts +11 -1
  237. package/dist/vault/error.js +5 -1
  238. package/dist/vault/service.d.ts +47 -48
  239. package/dist/vault/service.js +321 -382
  240. package/dist/vault/sources.d.ts +5 -16
  241. package/dist/vault/sources.js +6 -8
  242. package/dist/version.d.ts +1 -1
  243. package/dist/version.js +2 -2
  244. package/package.json +13 -13
  245. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  246. package/dist/api/http/routes/sandboxes.js +0 -153
  247. package/dist/api/http/routes/vaults.d.ts +0 -10
  248. package/dist/api/http/routes/vaults.js +0 -139
  249. package/dist/record/sandbox.d.ts +0 -25
  250. package/dist/record/sandbox.js +0 -43
  251. package/dist/tenant/sandboxes.d.ts +0 -123
  252. package/dist/tenant/sandboxes.js +0 -566
@@ -1,9 +1,5 @@
1
1
  import { WebSocket } from "ws";
2
- /** The URL path of the Harness API, and the header naming its version. */
3
- export const HARNESS_API_PATH = "/nylorun/harness/v1";
4
- export const HARNESS_API_HEADER = "nylorun-harness-api";
5
- /** Largest frame either end accepts. */
6
- export const HARNESS_MAX_PAYLOAD = 64 * 1024 * 1024;
2
+ export { HARNESS_API_HEADER, HARNESS_API_PATH, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD, } from "@nylorun/core/harness-api";
7
3
  export const DEFAULT_PING_MS = 30_000;
8
4
  export const DEFAULT_SILENCE_MS = 90_000;
9
5
  export function wsPort(socket, options = {}) {
@@ -9,7 +9,7 @@
9
9
  * SandboxManager and no store, as `--service harness` runs one.
10
10
  */
11
11
  import { randomBytes } from "node:crypto";
12
- import { memoryChannels } from "@nylorun/core/harness-api";
12
+ import { DEFAULT_HARNESS, memoryChannels } from "@nylorun/core/harness-api";
13
13
  import { createHarness } from "@nylorun/harness/api";
14
14
  import { harnessExecutors } from "../harness/executors.js";
15
15
  import { startHarnessService } from "../harness/service.js";
@@ -36,7 +36,8 @@ export async function startInProcessHarness(ctx, tools, options = {}) {
36
36
  sandbox: () => tools.sandbox,
37
37
  }),
38
38
  logger: ctx.config.logger,
39
- name: "in-process",
39
+ // Our harness: its hello names our harness id (R4.1).
40
+ name: DEFAULT_HARNESS,
40
41
  });
41
42
  await harness.start();
42
43
  return {
@@ -77,7 +78,6 @@ export async function startLoopbackHarness(ctx, options = {}) {
77
78
  ...(ctx.config.delivery ? { delivery: ctx.config.delivery } : {}),
78
79
  ...(options.sandboxBackends ? { sandboxBackends: options.sandboxBackends } : {}),
79
80
  logger: quietLogger(ctx),
80
- name: "loopback",
81
81
  ephemeral: ctx.config.mode === "ephemeral",
82
82
  backoff: { minMs: 50, maxMs: 500 },
83
83
  });
@@ -11,8 +11,15 @@
11
11
  *
12
12
  * Rows store the request's hash (`requestHash`) and, for a model call, the request without its
13
13
  * prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
14
+ *
15
+ * Every row a run journals keeps when it was journaled (`startedAt`, R7.1), and a rejoin keeps
16
+ * it: an outcome's `durationMs` is the journal's window, from the intent to the outcome. A model
17
+ * call's outcome event also carries what the meter recorded for it (`modelOutcomeFacts`). A tool
18
+ * row keeps its span's context (`traceparent`, R7.4), which the `execute` answer carries to the
19
+ * harness and the Tool Gate reads with the row.
14
20
  */
15
21
  import { type EffectIntent, type IntentAnswer, type OutcomeAnswer, type RecordedOutcome } from "@nylorun/core/harness-api";
22
+ import type { EventWriter } from "@nylorun/core/contracts";
16
23
  import { type Lease, type Session, type TenantContext } from "../tenant/context.js";
17
24
  import type { Tx } from "../store/types.js";
18
25
  /** What a run's journal writes are scoped by: the advance's lease and its signal. */
@@ -20,6 +27,13 @@ export interface RecordScope {
20
27
  readonly ctx: TenantContext;
21
28
  readonly lease: Lease;
22
29
  readonly signal: AbortSignal;
30
+ /** The tools the run's harness declared it runs itself (`hello` `capabilities.tools`, R4.2). */
31
+ readonly nativeTools?: ReadonlySet<string>;
32
+ /**
33
+ * Who reported what this scope records (R4.9): the run's harness, so the events derived from
34
+ * its intents and outcomes are its claim. Absent, core recorded what it saw itself.
35
+ */
36
+ readonly writer?: EventWriter;
23
37
  }
24
38
  /** The hash a journal row's request was recorded with. */
25
39
  export declare function hashOf(row: {
@@ -41,7 +55,7 @@ export declare function recordIntent(scope: RecordScope, effect: EffectIntent, r
41
55
  * `error` that left it uncertain. Only a cancel discards an outcome in hand (§10.7): after any
42
56
  * other abort (shutdown, deadline) it is recorded, so the next advance replays it.
43
57
  */
44
- export declare function recordOutcome(scope: RecordScope, effectId: string, result: {
58
+ export declare function recordOutcome(scope: RecordScope, effectId: string, reported: {
45
59
  value: unknown;
46
60
  } | {
47
61
  error: string;
@@ -11,8 +11,14 @@
11
11
  *
12
12
  * Rows store the request's hash (`requestHash`) and, for a model call, the request without its
13
13
  * prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
14
+ *
15
+ * Every row a run journals keeps when it was journaled (`startedAt`, R7.1), and a rejoin keeps
16
+ * it: an outcome's `durationMs` is the journal's window, from the intent to the outcome. A model
17
+ * call's outcome event also carries what the meter recorded for it (`modelOutcomeFacts`). A tool
18
+ * row keeps its span's context (`traceparent`, R7.4), which the `execute` answer carries to the
19
+ * harness and the Tool Gate reads with the row.
14
20
  */
15
- import { HarnessApiError, effectRequestHash, } from "@nylorun/core/harness-api";
21
+ import { HARNESS_TOOLS_CAPABILITY, HarnessApiError, effectRequestHash, isEffectIdOf, } from "@nylorun/core/harness-api";
16
22
  import { isFlowEffect, isFlowToolEffect, recordVerdict, settleAgentEffect, } from "../core/flow-host.js";
17
23
  import { sandboxCapabilityOf } from "../sandbox/capability.js";
18
24
  import { abortOn, isReadArtifactCall, isSaveArtifactCall } from "../harness/calls.js";
@@ -26,8 +32,13 @@ import { manifestFor, mcpToolOf } from "../mcp/snapshot.js";
26
32
  import { isHttpToolCall } from "../gates/http-tool.js";
27
33
  import { ownedSession } from "../tenant/context.js";
28
34
  import { isGateToolEffect, linkedOutcome, recoversModelCalls, recoversToolCalls, resolveNewFlowEffect, } from "../tenant/effects.js";
29
- import { assistantMessage, contextCompacted, modelFailed, toolCompleted, } from "../tenant/transcript.js";
35
+ import { assistantMessage, contextCompacted, modelFailed, modelOutcomeFacts, } from "../tenant/transcript.js";
30
36
  import { abortKind } from "../tenant/worker.js";
37
+ import { redactTokens } from "./redact.js";
38
+ import { toolCallDecision } from "../gates/policy.js";
39
+ import { traceKey } from "../traces/key.js";
40
+ import { effectSpanOf, traceparentOf, turnTraceIdOf } from "../traces/ids.js";
41
+ import { MAX_TOOL_ATTEMPTS, firstSend, interrupted, settleToolRow, toolKindOf, } from "./tool-journal.js";
31
42
  const TURN_CANCELLED = () => new HarnessApiError("turn_cancelled", "Turn cancelled");
32
43
  /** The hash a journal row's request was recorded with. */
33
44
  export function hashOf(row) {
@@ -35,10 +46,9 @@ export function hashOf(row) {
35
46
  }
36
47
  /** The completed outcomes of a segment, which a run replays without asking. */
37
48
  export async function bulkOutcomes(t, session, checkpoint) {
38
- const prefix = `${checkpoint.turnId}:${checkpoint.segment}:`;
39
49
  const rows = await t.effectsForTurn(session.id, checkpoint.turnId, ["completed"]);
40
50
  return rows
41
- .filter((row) => row.request.effectId.startsWith(prefix) && row.outcome)
51
+ .filter((row) => isEffectIdOf(row.request.effectId, checkpoint.turnId, checkpoint.segment) && row.outcome)
42
52
  .map((row) => ({ effectId: row.request.effectId, requestHash: hashOf(row), outcome: row.outcome }));
43
53
  }
44
54
  /**
@@ -48,10 +58,14 @@ export async function bulkOutcomes(t, session, checkpoint) {
48
58
  export async function recordIntent(scope, effect, requestHash) {
49
59
  const { ctx, lease, signal } = scope;
50
60
  const request = effect;
61
+ // Read before the transaction: the first read of the Tenant may create the key.
62
+ const key = await traceKey(ctx.store);
51
63
  const answer = await ctx.store.tx(async (t) => {
52
64
  const s = await ownedSession(t, lease, request.sessionId);
53
65
  if (s.status === "cancelled" || s.activeTurnId !== request.turnId)
54
66
  throw TURN_CANCELLED();
67
+ // A tool call's span context (R7.4), kept on its row.
68
+ const traced = () => ({ traceparent: toolTraceparent(key, ctx.store.tenantId, s, request) });
55
69
  // An aborted advance starts no effect; the advance decides what the abort means.
56
70
  signal.throwIfAborted();
57
71
  const existing = await t.get("effects", request.effectId);
@@ -61,12 +75,12 @@ export async function recordIntent(scope, effect, requestHash) {
61
75
  if (existing.status === "completed")
62
76
  return { status: "completed", outcome: existing.outcome };
63
77
  // A call its previous owner left running at the gate: re-send it, same key and request,
64
- // to join it or collect its outcome (P1.2 for model calls, F4.1 for remote MCP and HTTP
65
- // tool calls).
78
+ // to join it or collect its outcome (P1.2).
66
79
  if (existing.status === "invoking" && request.kind === "model" && recoversModelCalls(ctx))
67
80
  return { status: "execute", rejoin: true };
68
- if (existing.status === "invoking" && recoversToolCalls(ctx) && isGateToolEffect(s, existing.request))
69
- return { status: "execute", rejoin: true };
81
+ // A tool call a crash, a shutdown or a lost gate hop interrupted (R5.3, R5.4).
82
+ if (existing.status === "invoking" && existing.request.kind === "tool")
83
+ return resend(t, s, existing, recoversToolCalls(ctx));
70
84
  if (request.kind === "agent" && existing.status === "pending" && existing.agentSessionId) {
71
85
  const agent = await t.get("sessions", existing.agentSessionId);
72
86
  const outcome = await linkedOutcome(t, existing, agent);
@@ -86,26 +100,38 @@ export async function recordIntent(scope, effect, requestHash) {
86
100
  if (request.kind !== "model" && effectRequestHash(request) !== requestHash)
87
101
  throw new HarnessApiError("invalid", "The request hash does not match the request");
88
102
  if (flowHttp) {
89
- await t.put("effects", request.effectId, { request, requestHash, status: "invoking" });
103
+ const row = { request, requestHash, status: "invoking", ...firstSend({ toolKind: "http" }), ...traced() };
90
104
  await t.event(s.id, s.activeTurnId, "node.started", {
91
105
  path: request.path,
92
106
  kind: "http",
93
107
  key: request.key,
94
108
  ...(request.iterations !== undefined ? { iterations: request.iterations } : {}),
95
- });
96
- return { status: "execute" };
109
+ }, scope.writer);
110
+ const decided = await bypolicy(t, s, row, "http");
111
+ if (decided)
112
+ return decided;
113
+ await t.put("effects", request.effectId, row);
114
+ return { status: "execute", traceparent: row.traceparent };
97
115
  }
98
116
  if (request.kind === "delegation") {
99
117
  // Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
100
118
  const outcome = { value: null };
101
- await t.put("effects", request.effectId, { request, requestHash, status: "completed", outcome });
119
+ const at = new Date().toISOString();
120
+ await t.put("effects", request.effectId, {
121
+ request,
122
+ requestHash,
123
+ status: "completed",
124
+ outcome,
125
+ startedAt: at,
126
+ settledAt: at,
127
+ });
102
128
  const settled = request.effectId.endsWith(":settled");
103
129
  const callId = request.context?.callId;
104
130
  await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
105
131
  agent: request.agent,
106
132
  ...(typeof callId === "string" ? { callId } : {}),
107
133
  ...request.input,
108
- });
134
+ }, scope.writer);
109
135
  return { status: "completed", outcome };
110
136
  }
111
137
  const agentManifest = manifestFor(s.manifest, request.agent);
@@ -124,24 +150,41 @@ export async function recordIntent(scope, effect, requestHash) {
124
150
  isReadArtifactCall(agentManifest, request) ||
125
151
  isSkillToolCall(agentManifest, request) ||
126
152
  // `tool_search` (R2b C10) reads the session's pinned MCP snapshot: core serves it.
127
- isToolSearchCall(agentManifest, request)));
153
+ isToolSearchCall(agentManifest, request) ||
154
+ // A tool the run's harness declared and runs itself (R4.2, Q3).
155
+ (request.capabilityId === HARNESS_TOOLS_CAPABILITY &&
156
+ scope.nativeTools?.has(request.toolName ?? "") === true)));
128
157
  if (!executed) {
129
158
  // A tool that would run the developer's code: refused at save (a turn's manifest only
130
159
  // removes tools), so this is a backstop, as is an MCP tool the turn disabled, which the
131
160
  // model was never offered. The model sees the failure; nothing runs.
132
161
  const disabled = request.kind === "tool" && mcpToolOf(s.mcpSnapshot, request) !== undefined;
133
- const outcome = { value: disabled ? disabledTool(request) : unrunnableTool(request) };
134
- await t.put("effects", request.effectId, { request, requestHash, status: "completed", outcome });
135
- const transcript = toolCompleted(request, outcome.value);
136
- if (transcript)
137
- await t.event(s.id, request.turnId, "tool.completed", transcript);
162
+ const row = {
163
+ request,
164
+ requestHash,
165
+ status: "invoking",
166
+ ...toolKindOf(s, agentManifest, request),
167
+ attempts: 0,
168
+ };
169
+ const outcome = await settleToolRow(t, s, row, disabled ? disabledTool(request) : unrunnableTool(request), { writer: coreIn(scope) });
138
170
  return { status: "completed", outcome };
139
171
  }
140
- await t.put("effects", request.effectId, {
172
+ const row = {
173
+ // A model row keeps no prompt; a tool row its whole request.
141
174
  request: storedRequest(request),
142
175
  requestHash,
143
176
  status: "invoking",
144
- });
177
+ ...(request.kind === "tool"
178
+ ? { ...firstSend(toolKindOf(s, agentManifest, request)), ...traced() }
179
+ : { startedAt: new Date().toISOString() }),
180
+ };
181
+ // The Tenant's tool policy decides an MCP or HTTP tool call as it is journaled (R5 step 2).
182
+ if (row.toolKind === "mcp" || row.toolKind === "http") {
183
+ const decided = await bypolicy(t, s, row, row.toolKind);
184
+ if (decided)
185
+ return decided;
186
+ }
187
+ await t.put("effects", request.effectId, row);
145
188
  // `save_artifact` writes the Tenant's artifacts: core runs it, reading the file through the
146
189
  // workspace capability, wherever the sandbox is.
147
190
  if (request.kind === "tool" && isSaveArtifactCall(agentManifest, request))
@@ -154,7 +197,7 @@ export async function recordIntent(scope, effect, requestHash) {
154
197
  return { skill: agentManifest };
155
198
  if (isToolSearchCall(agentManifest, request))
156
199
  return "search";
157
- return { status: "execute" };
200
+ return { status: "execute", ...(row.traceparent ? { traceparent: row.traceparent } : {}) };
158
201
  });
159
202
  if (answer === "save")
160
203
  return runInCore(scope, request, () => callSaveArtifact(scope.ctx, request, scope.signal));
@@ -177,6 +220,9 @@ export async function recordIntent(scope, effect, requestHash) {
177
220
  * the outcome as the intent's answer.
178
221
  */
179
222
  async function runInCore(scope, request, call) {
223
+ // Core ran the call: its outcome is what core saw, not the harness's claim (R4.9).
224
+ const writer = coreIn(scope);
225
+ const core = { ctx: scope.ctx, lease: scope.lease, signal: scope.signal, ...(writer ? { writer } : {}) };
180
226
  let value;
181
227
  try {
182
228
  value = await call();
@@ -184,11 +230,70 @@ async function runInCore(scope, request, call) {
184
230
  catch (error) {
185
231
  if (isOwnershipLost(error))
186
232
  throw error;
187
- return recordOutcome(scope, request.effectId, {
233
+ return recordOutcome(core, request.effectId, {
188
234
  error: error instanceof Error ? error.message : String(error),
189
235
  });
190
236
  }
191
- return recordOutcome(scope, request.effectId, { value });
237
+ return recordOutcome(core, request.effectId, { value });
238
+ }
239
+ /** Core's own write during the scope's run (R4.9): core saw it, in that run. */
240
+ function coreIn(scope) {
241
+ const runId = scope.writer?.runId;
242
+ return runId === undefined ? undefined : { source: { kind: "core", id: "runtime" }, runId };
243
+ }
244
+ /**
245
+ * The answer to a tool row still `invoking` (R5.3, R5.4): a crash, a shutdown or a lost gate
246
+ * hop interrupted its send. An MCP or HTTP call (it crosses the Tool Gate, which hands the tool
247
+ * its key) is sent again, one more attempt, until `MAX_TOOL_ATTEMPTS`; then it fails to the
248
+ * model. Any other call (sandbox, core-served) has no key to dedupe on: it fails to the model
249
+ * now, and is not run again (Q2).
250
+ */
251
+ async function resend(t, s, row, rejoin) {
252
+ if (!isGateToolEffect(s, row.request)) {
253
+ const outcome = await settleToolRow(t, s, row, interrupted("A crash interrupted this call before its outcome was recorded. It may have run; it is not run again."));
254
+ return { status: "completed", outcome };
255
+ }
256
+ const attempts = (row.attempts ?? 1) + 1;
257
+ if (attempts > MAX_TOOL_ATTEMPTS) {
258
+ const outcome = await settleToolRow(t, s, row, interrupted(`The call was interrupted ${MAX_TOOL_ATTEMPTS} times and may have run.`));
259
+ return { status: "completed", outcome };
260
+ }
261
+ row.attempts = attempts;
262
+ await t.put("effects", row.request.effectId, row);
263
+ const traced = row.traceparent ? { traceparent: row.traceparent } : {};
264
+ return rejoin ? { status: "execute", rejoin: true, ...traced } : { status: "execute", ...traced };
265
+ }
266
+ /** The span context of tool call `request` (R7.4): its turn's trace, and its own span. */
267
+ function toolTraceparent(key, tenantId, s, request) {
268
+ const traceId = turnTraceIdOf(key, { tenantId, sessionId: s.id, turnId: request.turnId }, s.turnTrace);
269
+ return traceparentOf(traceId, effectSpanOf(key, request.effectId));
270
+ }
271
+ /**
272
+ * The Tenant's tool policy on a tool call being journaled (R5 step 2, `gates/policy.ts`), or
273
+ * undefined when it runs. `deny`: the call fails to the model as `tool.denied`, unsent. `ask`:
274
+ * the row waits as `held` and the effect is pending; the segment ends waiting, the turn pauses,
275
+ * and `approve` answers it (`tenant/commands.ts`). The Tool Gate refuses a row not `invoking`, so
276
+ * neither runs whichever harness sends it. Writes `row`.
277
+ */
278
+ async function bypolicy(t, s, row, kind) {
279
+ const decision = await toolCallDecision(t, s, row.request, kind);
280
+ if (decision.effect === "allow")
281
+ return undefined;
282
+ // Nothing was sent.
283
+ row.attempts = 0;
284
+ if (decision.effect === "deny") {
285
+ const outcome = await settleToolRow(t, s, row, {
286
+ kind: "failed",
287
+ code: "tool.denied",
288
+ message: decision.reason,
289
+ retryable: false,
290
+ });
291
+ return { status: "completed", outcome };
292
+ }
293
+ row.status = "held";
294
+ row.hold = { reason: decision.reason, at: new Date().toISOString() };
295
+ await t.put("effects", row.request.effectId, row);
296
+ return { status: "pending" };
192
297
  }
193
298
  /** The failed outcome of an MCP tool the turn's manifest disables (R2b C9). */
194
299
  function disabledTool(request) {
@@ -203,7 +308,7 @@ function unrunnableTool(request) {
203
308
  return {
204
309
  kind: "failed",
205
310
  code: "tool.unavailable",
206
- message: `Tool '${request.toolName ?? ""}' runs your code, and the Runtime runs no code of yours during a session. Make it an http() tool or serve it from a remote MCP server.`,
311
+ message: `Tool '${request.toolName ?? ""}' runs your code, but the Runtime runs agents from their manifests alone. Make it an http() tool or serve it from a remote MCP server.`,
207
312
  };
208
313
  }
209
314
  /**
@@ -211,24 +316,38 @@ function unrunnableTool(request) {
211
316
  * `error` that left it uncertain. Only a cancel discards an outcome in hand (§10.7): after any
212
317
  * other abort (shutdown, deadline) it is recorded, so the next advance replays it.
213
318
  */
214
- export async function recordOutcome(scope, effectId, result) {
319
+ export async function recordOutcome(scope, effectId, reported) {
215
320
  const { ctx, lease, signal } = scope;
321
+ const result = "error" in reported ? { error: redactTokens(reported.error) } : reported;
216
322
  if ("error" in result) {
217
- await ctx.store.tx(async (t) => {
323
+ return ctx.store.tx(async (t) => {
218
324
  const s = await t.assertEpoch(lease.sessionId, lease.epoch);
219
325
  const effect = await t.get("effects", effectId);
220
326
  if (!effect)
221
- return;
327
+ return { status: "uncertain" };
222
328
  const { request } = effect;
329
+ const active = s && s.status !== "cancelled" && s.activeTurnId === request.turnId;
330
+ // A tool call whose fate the harness could not learn fails to the model (R5.3): it may
331
+ // have run, and nothing sends it again. A cancel already settled it.
332
+ if (request.kind === "tool") {
333
+ if (effect.status === "completed")
334
+ return { status: "completed", outcome: effect.outcome };
335
+ if (effect.status !== "invoking" || !active)
336
+ return { status: "uncertain" };
337
+ effect.error = result.error;
338
+ const outcome = await settleToolRow(t, s, effect, interrupted(`${result.error} (the call may have run; it is not sent again)`), { writer: coreIn(scope) });
339
+ return { status: "completed", outcome };
340
+ }
223
341
  effect.status = "uncertain";
224
342
  effect.error = result.error;
225
343
  await t.put("effects", effectId, effect);
226
- if (s && s.status !== "cancelled" && s.activeTurnId === request.turnId)
227
- await t.event(s.id, request.turnId, "effect.uncertain", { effectId, message: result.error });
344
+ if (active)
345
+ await t.event(s.id, request.turnId, "effect.uncertain", { effectId, message: result.error }, scope.writer);
346
+ return { status: "uncertain" };
228
347
  });
229
- return { status: "uncertain" };
230
348
  }
231
- const value = await shaped(scope, effectId, result.value);
349
+ // What a harness reports never carries the Runtime's own tokens into the record (R4).
350
+ const value = await shaped(scope, effectId, redactTokens(result.value));
232
351
  return ctx.store.tx(async (t) => {
233
352
  const s = await t.assertEpoch(lease.sessionId, lease.epoch);
234
353
  const effect = await t.get("effects", effectId);
@@ -237,29 +356,40 @@ export async function recordOutcome(scope, effectId, result) {
237
356
  const { request } = effect;
238
357
  if (s.status === "cancelled" || s.activeTurnId !== request.turnId || abortKind(signal) === "cancel")
239
358
  throw TURN_CANCELLED();
359
+ if (request.kind === "tool") {
360
+ // Settled already: a re-send after a lost hop collected what the first send recorded.
361
+ if (effect.status === "completed")
362
+ return { status: "completed", outcome: effect.outcome };
363
+ // What the call returned is the harness's claim, or, for a tool core ran, core's (R4.9).
364
+ const outcome = await settleToolRow(t, s, effect, value, { writer: scope.writer });
365
+ if (request.context?.role === "verify-http")
366
+ await recordVerdict(t, request, value);
367
+ return { status: "completed", outcome };
368
+ }
240
369
  effect.status = "completed";
241
370
  effect.outcome = { value };
371
+ effect.settledAt = new Date().toISOString();
242
372
  await t.put("effects", effectId, effect);
243
373
  const model = request.kind === "model";
244
374
  const failed = model ? modelFailed(request, value) : undefined;
245
- // A summary call is never an assistant message; the last one publishes context.compacted.
375
+ // A summary call is never an assistant message; it publishes context.compacted, `partial`
376
+ // unless it is the last.
246
377
  const summarizing = model && Boolean(request.context.compaction);
247
378
  const compacted = summarizing && !failed ? contextCompacted(request, value) : undefined;
248
- const transcript = failed || summarizing
249
- ? undefined
250
- : model
251
- ? assistantMessage(request, value)
252
- : toolCompleted(request, value);
379
+ const transcript = failed || summarizing || !model ? undefined : assistantMessage(request, value);
253
380
  if (request.context?.role === "verify-http")
254
381
  await recordVerdict(t, request, value);
382
+ const writer = scope.writer;
383
+ // The call's span (R7.1): the journal's window, and the meter's row for it when a gate ran it.
384
+ const facts = failed || compacted || transcript
385
+ ? modelOutcomeFacts(effectId, effect, await t.modelUsageOf(effectId))
386
+ : undefined;
255
387
  if (failed)
256
- await t.event(s.id, request.turnId, "model.failed", failed);
388
+ await t.event(s.id, request.turnId, "model.failed", { ...failed, ...facts }, writer);
257
389
  if (compacted)
258
- await t.event(s.id, request.turnId, "context.compacted", compacted);
259
- if (transcript && model)
260
- await t.event(s.id, request.turnId, "message.assistant", transcript);
261
- else if (transcript)
262
- await t.event(s.id, request.turnId, "tool.completed", transcript);
390
+ await t.event(s.id, request.turnId, "context.compacted", { ...compacted, ...facts }, writer);
391
+ if (transcript)
392
+ await t.event(s.id, request.turnId, "message.assistant", { ...transcript, ...facts }, writer);
263
393
  return { status: "completed", outcome: effect.outcome };
264
394
  });
265
395
  }
@@ -0,0 +1,3 @@
1
+ export declare const REDACTED_TOKEN = "[nylorun token redacted]";
2
+ /** `value` with every Nylorun-signed token in its strings replaced; the same value when there is none. */
3
+ export declare function redactTokens<T>(value: T): T;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Keeps the Runtime's own tokens out of the record (track R4). A harness runs
3
+ * model-written commands with the egress token in their proxy variables, and holds its run and
4
+ * host tokens; a command that prints its environment, or a model that repeats it, would put
5
+ * them into a tool result, an assistant message or a transcript edit. Core replaces every
6
+ * Nylorun-signed token (a compact JWT whose `typ` is `nylorun-…`) in what a harness reports with
7
+ * `[nylorun token redacted]` before recording it, whichever harness reported it. Other values
8
+ * pass through unchanged, and the walk keeps the value's shape.
9
+ */
10
+ import { tokenType } from "../tenant/jwt.js";
11
+ export const REDACTED_TOKEN = "[nylorun token redacted]";
12
+ /** A compact JWT: three base64url parts, the first two JSON objects. */
13
+ const COMPACT_JWT = /eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}/g;
14
+ function redactString(text) {
15
+ if (!text.includes("eyJ"))
16
+ return text;
17
+ return text.replace(COMPACT_JWT, (token) => (tokenType(token)?.startsWith("nylorun-") ? REDACTED_TOKEN : token));
18
+ }
19
+ /** `value` with every Nylorun-signed token in its strings replaced; the same value when there is none. */
20
+ export function redactTokens(value) {
21
+ if (typeof value === "string")
22
+ return redactString(value);
23
+ if (Array.isArray(value)) {
24
+ let changed = false;
25
+ const next = value.map((item) => {
26
+ const redacted = redactTokens(item);
27
+ if (redacted !== item)
28
+ changed = true;
29
+ return redacted;
30
+ });
31
+ return (changed ? next : value);
32
+ }
33
+ if (value !== null && typeof value === "object") {
34
+ let changed = false;
35
+ const next = {};
36
+ for (const [key, item] of Object.entries(value)) {
37
+ const redacted = redactTokens(item);
38
+ if (redacted !== item)
39
+ changed = true;
40
+ next[key] = redacted;
41
+ }
42
+ return (changed ? next : value);
43
+ }
44
+ return value;
45
+ }
@@ -5,6 +5,12 @@ import type { RunOf } from "../tenant/run-grants.js";
5
5
  export interface HarnessPeer {
6
6
  /** For logs. */
7
7
  readonly name: string;
8
+ /**
9
+ * The harness id its credential is for (R4.1): `nylorun` for the harness token, a custom
10
+ * harness's id for its own token. Its `hello` must name it. Absent for an in-process
11
+ * harness, which may name any id; a pod's host is checked against its Environment's harness.
12
+ */
13
+ readonly harness?: string;
8
14
  /** A pod sandbox's engine (F7.2): the sandbox it hosts, at the host epoch of its token. */
9
15
  readonly host?: {
10
16
  readonly sandboxId: string;
@@ -28,6 +34,8 @@ export interface RunOffer {
28
34
  * only a harness that hosts no sandbox does.
29
35
  */
30
36
  readonly pod?: string;
37
+ /** The session's harness (R4.1): only a connection that said this id takes it. Default ours. */
38
+ readonly harness?: string;
31
39
  /** How long to wait for a harness to take it. Default the server's `offerWaitMs`. */
32
40
  readonly waitMs?: number;
33
41
  }