@nylorun/runtime 0.22.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 (273) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +95 -30
  3. package/dist/adapters/execution/restate.d.ts +15 -3
  4. package/dist/adapters/execution/restate.js +78 -84
  5. package/dist/adapters/replication/pgoutput.d.ts +2 -0
  6. package/dist/adapters/replication/pgoutput.js +6 -3
  7. package/dist/adapters/sandbox/local.d.ts +9 -1
  8. package/dist/adapters/sandbox/local.js +16 -4
  9. package/dist/adapters/sandbox/virtual.js +4 -1
  10. package/dist/adapters/streams/s2.js +1 -13
  11. package/dist/api/a2a/endpoint.js +5 -1
  12. package/dist/api/a2a/routes.d.ts +2 -0
  13. package/dist/api/a2a/routes.js +2 -2
  14. package/dist/api/a2a/tasks.js +2 -1
  15. package/dist/api/ag-ui/endpoint.js +4 -2
  16. package/dist/api/ag-ui/history.js +4 -1
  17. package/dist/api/ag-ui/routes.d.ts +4 -1
  18. package/dist/api/ag-ui/routes.js +11 -10
  19. package/dist/api/ag-ui/translate.js +3 -2
  20. package/dist/api/components.d.ts +4953 -1204
  21. package/dist/api/components.js +25 -16
  22. package/dist/api/gate/keys.js +7 -1
  23. package/dist/api/gate/routes.d.ts +6 -0
  24. package/dist/api/gate/routes.js +39 -3
  25. package/dist/api/http/app.js +4 -4
  26. package/dist/api/http/define.d.ts +6 -0
  27. package/dist/api/http/define.js +13 -1
  28. package/dist/api/http/respond.js +1 -1
  29. package/dist/api/http/routes/artifacts.js +35 -8
  30. package/dist/api/http/routes/coverage.d.ts +9 -0
  31. package/dist/api/http/routes/coverage.js +34 -0
  32. package/dist/api/http/routes/environments.d.ts +11 -0
  33. package/dist/api/http/routes/environments.js +283 -0
  34. package/dist/api/http/routes/mcp.d.ts +1 -1
  35. package/dist/api/http/routes/mcp.js +2 -2
  36. package/dist/api/http/routes/me.js +1 -1
  37. package/dist/api/http/routes/reads.d.ts +45 -9
  38. package/dist/api/http/routes/reads.js +54 -5
  39. package/dist/api/http/routes/sessions.js +11 -6
  40. package/dist/api/http/routes/tenant.js +32 -10
  41. package/dist/api/openapi.js +31 -26
  42. package/dist/artifacts/disk.d.ts +59 -0
  43. package/dist/artifacts/disk.js +563 -0
  44. package/dist/artifacts/export.d.ts +1 -1
  45. package/dist/artifacts/export.js +2 -3
  46. package/dist/artifacts/folders.d.ts +6 -3
  47. package/dist/artifacts/folders.js +17 -12
  48. package/dist/artifacts/service.d.ts +2 -1
  49. package/dist/artifacts/service.js +6 -1
  50. package/dist/artifacts/workspace.d.ts +1 -1
  51. package/dist/artifacts/workspace.js +1 -1
  52. package/dist/core/flow-host.d.ts +3 -22
  53. package/dist/core/flow-host.js +11 -37
  54. package/dist/core/provider.d.ts +3 -2
  55. package/dist/core/provider.js +5 -37
  56. package/dist/core/sandbox-routes.d.ts +2 -21
  57. package/dist/core/sandbox-routes.js +10 -43
  58. package/dist/execution/types.d.ts +11 -9
  59. package/dist/execution/types.js +5 -7
  60. package/dist/gates/contract.d.ts +3 -24
  61. package/dist/gates/contract.js +1 -36
  62. package/dist/gates/egress-credentials.d.ts +9 -0
  63. package/dist/gates/egress-credentials.js +11 -4
  64. package/dist/gates/egress.d.ts +28 -1
  65. package/dist/gates/egress.js +115 -10
  66. package/dist/gates/fixture-model.d.ts +28 -0
  67. package/dist/gates/fixture-model.js +92 -0
  68. package/dist/gates/handler.js +4 -4
  69. package/dist/gates/http-tool.d.ts +6 -2
  70. package/dist/gates/http-tool.js +3 -5
  71. package/dist/gates/in-process.d.ts +5 -0
  72. package/dist/gates/in-process.js +12 -1
  73. package/dist/gates/journal.d.ts +30 -0
  74. package/dist/gates/journal.js +52 -0
  75. package/dist/gates/mcp-handler.d.ts +4 -0
  76. package/dist/gates/mcp-handler.js +5 -1
  77. package/dist/gates/meter.d.ts +10 -2
  78. package/dist/gates/meter.js +20 -5
  79. package/dist/gates/policy.d.ts +49 -0
  80. package/dist/gates/policy.js +134 -0
  81. package/dist/gates/tenant-vaults.d.ts +7 -7
  82. package/dist/gates/tenant-vaults.js +5 -5
  83. package/dist/gates/tool-calls.d.ts +10 -5
  84. package/dist/gates/tool-calls.js +28 -10
  85. package/dist/gates/tool-client.d.ts +8 -0
  86. package/dist/gates/tool-client.js +27 -11
  87. package/dist/gates/tool-contract.d.ts +10 -68
  88. package/dist/gates/tool-contract.js +1 -85
  89. package/dist/gates/tool-gate.d.ts +1 -1
  90. package/dist/harness/calls.d.ts +12 -5
  91. package/dist/harness/calls.js +21 -12
  92. package/dist/harness/executors.js +12 -9
  93. package/dist/harness/main.js +18 -5
  94. package/dist/harness/pod.d.ts +34 -0
  95. package/dist/harness/pod.js +41 -0
  96. package/dist/harness/service.d.ts +0 -1
  97. package/dist/harness/service.js +13 -5
  98. package/dist/harness/setup.d.ts +27 -0
  99. package/dist/harness/setup.js +150 -0
  100. package/dist/harness/ws-client.js +3 -2
  101. package/dist/harness/ws-port.d.ts +1 -5
  102. package/dist/harness/ws-port.js +1 -5
  103. package/dist/harness-api/in-process.js +3 -3
  104. package/dist/harness-api/record.d.ts +15 -1
  105. package/dist/harness-api/record.js +174 -44
  106. package/dist/harness-api/redact.d.ts +3 -0
  107. package/dist/harness-api/redact.js +45 -0
  108. package/dist/harness-api/server.d.ts +8 -0
  109. package/dist/harness-api/server.js +93 -13
  110. package/dist/harness-api/start.d.ts +10 -3
  111. package/dist/harness-api/start.js +40 -10
  112. package/dist/harness-api/tool-journal.d.ts +96 -0
  113. package/dist/harness-api/tool-journal.js +102 -0
  114. package/dist/harness-api/tool-status.d.ts +13 -0
  115. package/dist/harness-api/tool-status.js +26 -0
  116. package/dist/harness-api/workspace.d.ts +6 -3
  117. package/dist/harness-api/workspace.js +27 -11
  118. package/dist/harness-api/ws-server.d.ts +11 -3
  119. package/dist/harness-api/ws-server.js +94 -15
  120. package/dist/host/execution.d.ts +8 -6
  121. package/dist/host/gates.d.ts +2 -1
  122. package/dist/host/gates.js +10 -2
  123. package/dist/host/main.js +64 -21
  124. package/dist/host/operate.js +67 -5
  125. package/dist/host/stack-config.d.ts +70 -0
  126. package/dist/host/stack-config.js +67 -1
  127. package/dist/infra/execution.d.ts +18 -1
  128. package/dist/infra/execution.js +28 -4
  129. package/dist/infra/index.d.ts +1 -1
  130. package/dist/keys/client.js +4 -1
  131. package/dist/keys/keys.d.ts +5 -5
  132. package/dist/keys/keys.js +5 -7
  133. package/dist/management-openapi.json +999 -2211
  134. package/dist/mcp/connect.d.ts +25 -20
  135. package/dist/mcp/connect.js +29 -30
  136. package/dist/mcp/pool.d.ts +2 -2
  137. package/dist/mcp/pool.js +1 -1
  138. package/dist/mcp/preview.d.ts +3 -3
  139. package/dist/mcp/preview.js +10 -20
  140. package/dist/mcp/snapshot.d.ts +5 -11
  141. package/dist/mcp/snapshot.js +6 -7
  142. package/dist/model/pi-model.js +8 -0
  143. package/dist/openapi.json +6955 -2526
  144. package/dist/reads/types.d.ts +16 -3
  145. package/dist/record/append.d.ts +3 -1
  146. package/dist/record/append.js +1 -0
  147. package/dist/record/envelope.d.ts +5 -1
  148. package/dist/record/envelope.js +7 -3
  149. package/dist/record/environment.d.ts +25 -0
  150. package/dist/record/environment.js +44 -0
  151. package/dist/record/index.d.ts +1 -1
  152. package/dist/record/index.js +1 -1
  153. package/dist/retry.d.ts +21 -0
  154. package/dist/retry.js +34 -0
  155. package/dist/sandbox/disk-fs.d.ts +95 -0
  156. package/dist/sandbox/disk-fs.js +206 -0
  157. package/dist/sandbox/join.d.ts +17 -18
  158. package/dist/sandbox/join.js +46 -20
  159. package/dist/sandbox/manager.d.ts +16 -8
  160. package/dist/sandbox/manager.js +33 -17
  161. package/dist/sandbox/placement.d.ts +13 -12
  162. package/dist/sandbox/placement.js +29 -12
  163. package/dist/sandbox/pods/client.d.ts +6 -1
  164. package/dist/sandbox/pods/lifecycle.d.ts +28 -27
  165. package/dist/sandbox/pods/lifecycle.js +14 -26
  166. package/dist/sandbox/pods/reconcile.js +21 -15
  167. package/dist/sandbox/pods/spec.d.ts +15 -13
  168. package/dist/sandbox/pods/spec.js +16 -20
  169. package/dist/sandbox/records.d.ts +2 -2
  170. package/dist/sandbox/records.js +3 -3
  171. package/dist/sandbox/resolve.d.ts +35 -27
  172. package/dist/sandbox/resolve.js +66 -41
  173. package/dist/sandbox/session-sandbox.js +1 -1
  174. package/dist/sandbox/setup.d.ts +61 -0
  175. package/dist/sandbox/setup.js +123 -0
  176. package/dist/sandbox/share.d.ts +11 -21
  177. package/dist/sandbox/share.js +12 -41
  178. package/dist/sandbox/tenant-config.d.ts +12 -14
  179. package/dist/sandbox/tenant-config.js +12 -17
  180. package/dist/sandbox/types.d.ts +6 -0
  181. package/dist/store/postgres/control.js +8 -1
  182. package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
  183. package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
  184. package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
  185. package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
  186. package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
  187. package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
  188. package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
  189. package/dist/store/postgres/reads.js +117 -17
  190. package/dist/store/postgres/record-writer.d.ts +4 -4
  191. package/dist/store/postgres/record-writer.js +9 -9
  192. package/dist/store/postgres/record.d.ts +7 -0
  193. package/dist/store/postgres/record.js +23 -2
  194. package/dist/store/postgres/schema.d.ts +1213 -779
  195. package/dist/store/postgres/schema.js +136 -76
  196. package/dist/store/postgres/store.js +283 -117
  197. package/dist/store/types.d.ts +206 -88
  198. package/dist/streams/relay/core.js +1 -13
  199. package/dist/tenant/advance.d.ts +7 -6
  200. package/dist/tenant/advance.js +121 -49
  201. package/dist/tenant/artifact-tool.js +3 -4
  202. package/dist/tenant/commands.d.ts +15 -2
  203. package/dist/tenant/commands.js +94 -14
  204. package/dist/tenant/context.d.ts +67 -33
  205. package/dist/tenant/context.js +4 -15
  206. package/dist/tenant/control.d.ts +8 -1
  207. package/dist/tenant/control.js +2 -0
  208. package/dist/tenant/effects.d.ts +1 -1
  209. package/dist/tenant/effects.js +37 -20
  210. package/dist/tenant/environments.d.ts +167 -0
  211. package/dist/tenant/environments.js +823 -0
  212. package/dist/tenant/harness-tokens.d.ts +21 -0
  213. package/dist/tenant/harness-tokens.js +61 -0
  214. package/dist/tenant/host-token.d.ts +1 -1
  215. package/dist/tenant/host-token.js +2 -4
  216. package/dist/tenant/identity-file.d.ts +4 -4
  217. package/dist/tenant/identity-file.js +17 -8
  218. package/dist/tenant/issuers.js +3 -3
  219. package/dist/tenant/principals.js +1 -1
  220. package/dist/tenant/reset.js +3 -3
  221. package/dist/tenant/runtime.d.ts +6 -1
  222. package/dist/tenant/runtime.js +30 -4
  223. package/dist/tenant/scheduler.d.ts +7 -0
  224. package/dist/tenant/scheduler.js +38 -7
  225. package/dist/tenant/session-streams.d.ts +0 -1
  226. package/dist/tenant/session-streams.js +1 -13
  227. package/dist/tenant/session.d.ts +3 -3
  228. package/dist/tenant/session.js +0 -14
  229. package/dist/tenant/sessions.d.ts +13 -13
  230. package/dist/tenant/sessions.js +61 -138
  231. package/dist/tenant/signing-keys.d.ts +5 -1
  232. package/dist/tenant/status.js +3 -3
  233. package/dist/tenant/streams.js +14 -29
  234. package/dist/tenant/sweep.d.ts +39 -12
  235. package/dist/tenant/sweep.js +168 -63
  236. package/dist/tenant/transcript.d.ts +210 -20
  237. package/dist/tenant/transcript.js +62 -11
  238. package/dist/tenant/turn-timing.d.ts +43 -0
  239. package/dist/tenant/turn-timing.js +39 -0
  240. package/dist/tenant/types.d.ts +2 -0
  241. package/dist/traces/ids.d.ts +42 -0
  242. package/dist/traces/ids.js +85 -0
  243. package/dist/traces/key.d.ts +6 -0
  244. package/dist/traces/key.js +37 -0
  245. package/dist/traces/map.d.ts +113 -0
  246. package/dist/traces/map.js +317 -0
  247. package/dist/traces/options.d.ts +7 -0
  248. package/dist/traces/options.js +1 -0
  249. package/dist/traces/otlp.d.ts +42 -0
  250. package/dist/traces/otlp.js +84 -0
  251. package/dist/traces/projector.d.ts +58 -0
  252. package/dist/traces/projector.js +200 -0
  253. package/dist/traces/read.d.ts +23 -0
  254. package/dist/traces/read.js +16 -0
  255. package/dist/vault/coverage.d.ts +26 -0
  256. package/dist/vault/coverage.js +64 -0
  257. package/dist/vault/error.d.ts +11 -1
  258. package/dist/vault/error.js +5 -1
  259. package/dist/vault/service.d.ts +47 -48
  260. package/dist/vault/service.js +321 -382
  261. package/dist/vault/sources.d.ts +5 -16
  262. package/dist/vault/sources.js +6 -8
  263. package/dist/version.d.ts +1 -1
  264. package/dist/version.js +2 -2
  265. package/package.json +14 -13
  266. package/dist/api/http/routes/sandboxes.d.ts +0 -9
  267. package/dist/api/http/routes/sandboxes.js +0 -153
  268. package/dist/api/http/routes/vaults.d.ts +0 -10
  269. package/dist/api/http/routes/vaults.js +0 -139
  270. package/dist/record/sandbox.d.ts +0 -25
  271. package/dist/record/sandbox.js +0 -43
  272. package/dist/tenant/sandboxes.d.ts +0 -122
  273. package/dist/tenant/sandboxes.js +0 -562
@@ -0,0 +1,84 @@
1
+ /**
2
+ * OTLP/HTTP JSON (R7.2): the encoder of a batch of spans as `resourceSpans`, and the poster that
3
+ * delivers one to the collector's `/v1/traces`. Hand-built JSON, no OpenTelemetry SDK: the spans
4
+ * are mapped from records (`map.ts`), not timed from live code.
5
+ *
6
+ * Delivery never holds the log back: a `429`, a `5xx` or a lost connection is tried again three
7
+ * times with backoff, then the batch is dropped and logged (`trace_export_dropped`); any other
8
+ * answer drops it at once. Either way `post` resolves, and the projector acknowledges the rows,
9
+ * so a dead collector makes Postgres keep no WAL. The trace route (R7.5) re-reads any turn.
10
+ */
11
+ import { RUNTIME_VERSION } from "../version.js";
12
+ import { GENAI_SEMCONV } from "./map.js";
13
+ export const DEFAULT_SERVICE_NAME = "nylorun";
14
+ /** The instrumentation scope of every span. */
15
+ const SCOPE = { name: "@nylorun/runtime", version: RUNTIME_VERSION };
16
+ /** A batch of spans as an OTLP `ExportTraceServiceRequest` in JSON. */
17
+ export function resourceSpans(resource, spans) {
18
+ const attributes = [];
19
+ for (const [key, value] of Object.entries(resource.attributes ?? {}))
20
+ if (key !== "service.name")
21
+ attributes.push({ key, value: { stringValue: value } });
22
+ attributes.unshift({ key: "service.name", value: { stringValue: resource.serviceName } });
23
+ attributes.push({ key: "nylorun.semconv.genai", value: { stringValue: GENAI_SEMCONV } });
24
+ return {
25
+ resourceSpans: [{ resource: { attributes }, scopeSpans: [{ scope: SCOPE, spans: [...spans] }] }],
26
+ };
27
+ }
28
+ export function createOtlpPoster(options) {
29
+ const send = options.fetch ?? fetch;
30
+ const retries = options.retries ?? 3;
31
+ const backoffMs = options.backoffMs ?? 500;
32
+ const timeoutMs = options.timeoutMs ?? 10_000;
33
+ const log = options.log ?? (() => { });
34
+ return async (spans, signal) => {
35
+ if (spans.length === 0)
36
+ return "posted";
37
+ const body = JSON.stringify(resourceSpans(options.resource, spans));
38
+ let reason = "";
39
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
40
+ if (attempt > 0 && !(await pause(backoffMs * 2 ** (attempt - 1), signal)))
41
+ break;
42
+ let status;
43
+ try {
44
+ const response = await send(options.url, {
45
+ method: "POST",
46
+ headers: { ...options.headers, "content-type": "application/json" },
47
+ body,
48
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs),
49
+ });
50
+ status = response.status;
51
+ await response.body?.cancel().catch(() => undefined);
52
+ }
53
+ catch (error) {
54
+ reason = error instanceof Error ? error.message : String(error);
55
+ if (signal?.aborted)
56
+ break;
57
+ continue;
58
+ }
59
+ if (status >= 200 && status < 300)
60
+ return "posted";
61
+ reason = `HTTP ${status}`;
62
+ if (status !== 429 && status < 500)
63
+ break;
64
+ }
65
+ log("trace_export_dropped", { spans: spans.length, reason });
66
+ return "dropped";
67
+ };
68
+ }
69
+ /** Waits `ms`; false when `signal` aborted first. */
70
+ function pause(ms, signal) {
71
+ if (signal?.aborted)
72
+ return Promise.resolve(false);
73
+ return new Promise((resolve) => {
74
+ const timer = setTimeout(() => {
75
+ signal?.removeEventListener("abort", onAbort);
76
+ resolve(true);
77
+ }, ms);
78
+ const onAbort = () => {
79
+ clearTimeout(timer);
80
+ resolve(false);
81
+ };
82
+ signal?.addEventListener("abort", onAbort, { once: true });
83
+ });
84
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The trace projector (R7.2, Q1): a second reader of the session log, beside the stream relay. It
3
+ * reads committed `session_events` from its own replication slot (`nylorun_trace_export`, on the
4
+ * relay's publication), maps each row to at most one span (`map.ts`), and posts them as OTLP to the
5
+ * operator's collector. It holds no state that changes a span, so a replay of its slot posts the
6
+ * same spans with the same ids. It never reads S2 (D48), and no turn waits for it.
7
+ *
8
+ * - **One process at a time.** The slot is the leader election, as for the relay: every process
9
+ * running core runs a projector, and one reads.
10
+ * - **Batches** of up to 512 spans, or what came within 2 s, posted one at a time
11
+ * (`otlp.ts`: retried, then dropped and logged).
12
+ * - **Acknowledged in commit order** once a transaction's spans are posted or dropped, so a dead
13
+ * collector makes Postgres keep no WAL. A transaction with no span is acknowledged with the
14
+ * rest. While the collector is slow, more than `maxQueuedSpans` waiting are dropped.
15
+ * - **Turns.** A bounded cache keeps each turn's trace (`turnTraceOf`) from its `command.message`;
16
+ * a turn not in it (after a restart mid turn, or one evicted) reads its `command.message` once.
17
+ */
18
+ import type { LiveEvent } from "@nylorun/core/contracts";
19
+ import type { ChangeSource } from "../streams/relay/types.js";
20
+ import type { TraceKey } from "./ids.js";
21
+ import { type TraceContent } from "./map.js";
22
+ import type { OtlpPoster } from "./otlp.js";
23
+ /** The projector's replication slot. */
24
+ export declare const TRACE_SLOT = "nylorun_trace_export";
25
+ export interface TraceProjectorOptions {
26
+ readonly source: ChangeSource;
27
+ readonly post: OtlpPoster;
28
+ /** The Tenant's trace key (`traces/key.ts`). */
29
+ readonly key: () => Promise<TraceKey>;
30
+ /** The `command.message` that started turn `turnId`, or undefined when the log has none. */
31
+ readonly turnMessage: (sessionId: string, turnId: string) => Promise<LiveEvent | undefined>;
32
+ readonly content: TraceContent;
33
+ /** Spans per post. Default 512. */
34
+ readonly maxBatch?: number;
35
+ /** How long a span waits for its batch to fill. Default 2 s. */
36
+ readonly maxDelayMs?: number;
37
+ /** Spans waiting while a post is out, past which the waiting ones are dropped. Default 8192. */
38
+ readonly maxQueuedSpans?: number;
39
+ /** Turns whose trace is kept. Default 1024. */
40
+ readonly turnCacheSize?: number;
41
+ readonly log?: (message: string, fields?: Record<string, unknown>) => void;
42
+ }
43
+ export interface TraceProjector {
44
+ start(): void;
45
+ stop(): Promise<void>;
46
+ /** This process reads the slot, and what it has not acknowledged. */
47
+ status(): {
48
+ active: boolean;
49
+ pendingTxs: number;
50
+ pendingSpans: number;
51
+ confirmed: string | null;
52
+ };
53
+ /** Resolves once everything received is posted or dropped and acknowledged (tests, shutdown). */
54
+ idle(): Promise<void>;
55
+ /** How far the slot is behind, in bytes, when known. */
56
+ lag(): Promise<number | undefined>;
57
+ }
58
+ export declare function createTraceProjector(options: TraceProjectorOptions): TraceProjector;
@@ -0,0 +1,200 @@
1
+ import { spanOf, turnTraceOf } from "./map.js";
2
+ /** The projector's replication slot. */
3
+ export const TRACE_SLOT = "nylorun_trace_export";
4
+ export function createTraceProjector(options) {
5
+ const { source, post } = options;
6
+ const log = options.log ?? (() => { });
7
+ const maxBatch = options.maxBatch ?? 512;
8
+ const maxDelayMs = options.maxDelayMs ?? 2000;
9
+ const maxQueued = options.maxQueuedSpans ?? 8192;
10
+ const cacheSize = options.turnCacheSize ?? 1024;
11
+ const turns = new Map();
12
+ const stopping = new AbortController();
13
+ /** Mapped transactions not yet posted, in commit order. */
14
+ let waiting = [];
15
+ let waitingSpans = 0;
16
+ /** Received transactions still being mapped, in commit order. */
17
+ let mapping = Promise.resolve();
18
+ let mappingCount = 0;
19
+ let posting;
20
+ let timer;
21
+ let active = false;
22
+ let confirmed = null;
23
+ /** Moves on with every loss of the source: work of an earlier activation is discarded. */
24
+ let activation = 0;
25
+ let idleWaiters = [];
26
+ function remember(key, trace) {
27
+ turns.delete(key);
28
+ turns.set(key, trace);
29
+ if (turns.size > cacheSize)
30
+ turns.delete(turns.keys().next().value);
31
+ return trace;
32
+ }
33
+ async function turnOf(row, settings) {
34
+ const turnId = row.turnId;
35
+ const cacheKey = `${row.sessionId}\u0000${turnId}`;
36
+ const ids = { tenantId: row.tenantId, sessionId: row.sessionId, turnId };
37
+ if (row.type === "command.message")
38
+ return remember(cacheKey, turnTraceOf(settings, ids, row));
39
+ const known = turns.get(cacheKey);
40
+ if (known)
41
+ return remember(cacheKey, known);
42
+ return remember(cacheKey, turnTraceOf(settings, ids, await options.turnMessage(row.sessionId, turnId)));
43
+ }
44
+ async function spansOf(tx) {
45
+ const spans = [];
46
+ if (tx.rows.length === 0)
47
+ return spans;
48
+ const settings = { key: await options.key(), content: options.content };
49
+ for (const record of tx.rows) {
50
+ const row = record.body;
51
+ if (!row || typeof row !== "object" || row.turnId == null)
52
+ continue;
53
+ const span = spanOf(row, await turnOf(row, settings), settings);
54
+ if (span)
55
+ spans.push(span);
56
+ }
57
+ return spans;
58
+ }
59
+ function onTx(tx) {
60
+ const of = activation;
61
+ mappingCount += 1;
62
+ mapping = mapping.then(async () => {
63
+ try {
64
+ let spans;
65
+ for (;;) {
66
+ try {
67
+ spans = await spansOf(tx);
68
+ break;
69
+ }
70
+ catch (error) {
71
+ // The key or a turn's message could not be read (Postgres away): try again, in order.
72
+ if (of !== activation || stopping.signal.aborted)
73
+ return;
74
+ log("trace_projector_retry", { message: error instanceof Error ? error.message : String(error) });
75
+ await new Promise((resolve) => setTimeout(resolve, 1000));
76
+ }
77
+ }
78
+ if (of !== activation)
79
+ return;
80
+ waiting.push({ endLsn: tx.endLsn, spans });
81
+ waitingSpans += spans.length;
82
+ if (posting && waitingSpans > maxQueued) {
83
+ // The collector is slower than the log: drop what waits, keep its place to acknowledge.
84
+ log("trace_export_dropped", { spans: waitingSpans, reason: "queue full" });
85
+ for (const pending of waiting)
86
+ pending.spans = [];
87
+ waitingSpans = 0;
88
+ }
89
+ schedule();
90
+ }
91
+ finally {
92
+ mappingCount -= 1;
93
+ settle();
94
+ }
95
+ });
96
+ }
97
+ function schedule() {
98
+ if (posting || waiting.length === 0)
99
+ return;
100
+ // Nothing to post: acknowledge at once.
101
+ if (waitingSpans === 0 || waitingSpans >= maxBatch || stopping.signal.aborted)
102
+ return void flush();
103
+ timer ??= setTimeout(() => {
104
+ timer = undefined;
105
+ flush();
106
+ }, maxDelayMs);
107
+ }
108
+ function flush() {
109
+ if (posting || waiting.length === 0)
110
+ return;
111
+ if (timer)
112
+ clearTimeout(timer);
113
+ timer = undefined;
114
+ const of = activation;
115
+ const batch = waiting;
116
+ waiting = [];
117
+ waitingSpans = 0;
118
+ posting = (async () => {
119
+ const spans = batch.flatMap((tx) => tx.spans);
120
+ // A post a shutdown cut short is not the collector's failure: those rows go again.
121
+ let cut = false;
122
+ for (let at = 0; at < spans.length; at += maxBatch)
123
+ if ((await post(spans.slice(at, at + maxBatch), stopping.signal)) === "dropped" && stopping.signal.aborted)
124
+ cut = true;
125
+ // Posted or dropped: the rows are done. A source lost meanwhile replays them anyway.
126
+ if (of === activation && !cut) {
127
+ confirmed = batch.at(-1).endLsn;
128
+ source.acknowledge(confirmed);
129
+ }
130
+ })().finally(() => {
131
+ posting = undefined;
132
+ schedule();
133
+ settle();
134
+ });
135
+ }
136
+ function settle() {
137
+ if (posting || mappingCount > 0 || waiting.length > 0)
138
+ return;
139
+ const waiters = idleWaiters;
140
+ idleWaiters = [];
141
+ for (const wake of waiters)
142
+ wake();
143
+ }
144
+ return {
145
+ start() {
146
+ source.start({
147
+ onActive({ fresh }) {
148
+ active = true;
149
+ log("trace projector active", { fresh });
150
+ // A fresh slot has nothing to reconcile: it exports from now on.
151
+ if (fresh)
152
+ void source.reconciled().catch(() => undefined);
153
+ },
154
+ onTx,
155
+ onKeepalive(lsn) {
156
+ // Caught up: let the slot move past WAL that holds none of our rows.
157
+ if (posting || mappingCount > 0 || waiting.length > 0)
158
+ return undefined;
159
+ confirmed = lsn;
160
+ return lsn;
161
+ },
162
+ onInactive(error) {
163
+ active = false;
164
+ activation += 1;
165
+ // The slot replays everything not acknowledged; what waits is dropped.
166
+ waiting = [];
167
+ waitingSpans = 0;
168
+ if (timer)
169
+ clearTimeout(timer);
170
+ timer = undefined;
171
+ settle();
172
+ if (error)
173
+ log("trace projector lost its source; retrying", {
174
+ message: error instanceof Error ? error.message : String(error),
175
+ });
176
+ },
177
+ });
178
+ },
179
+ async stop() {
180
+ // Post what is mapped (each post gives up at once), then stop reading.
181
+ stopping.abort();
182
+ await mapping.catch(() => undefined);
183
+ flush();
184
+ await posting?.catch(() => undefined);
185
+ await source.stop();
186
+ },
187
+ status: () => ({
188
+ active,
189
+ pendingTxs: waiting.length + mappingCount,
190
+ pendingSpans: waitingSpans,
191
+ confirmed,
192
+ }),
193
+ idle() {
194
+ if (!posting && mappingCount === 0 && waiting.length === 0)
195
+ return Promise.resolve();
196
+ return new Promise((resolve) => idleWaiters.push(resolve));
197
+ },
198
+ lag: async () => (await source.lag?.()) ?? undefined,
199
+ };
200
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The trace route's spans (R7.5): a turn's log rows mapped by the same `spanOf` the projector
3
+ * posts with, under the installation's resource and content opt-ins, so the route and the
4
+ * exporter agree span for span.
5
+ */
6
+ import type { LiveEvent } from "@nylorun/core/contracts";
7
+ import type { TenantContext } from "../tenant/context.js";
8
+ import { type OtlpSpan } from "./map.js";
9
+ /** Turn `turnId`'s spans as an OTLP `ExportTraceServiceRequest`, from its rows in log order. */
10
+ export declare function turnTrace(ctx: TenantContext, sessionId: string, turnId: string, events: readonly LiveEvent[]): Promise<{
11
+ resourceSpans: {
12
+ resource: {
13
+ attributes: import("./map.js").OtlpKeyValue[];
14
+ };
15
+ scopeSpans: {
16
+ scope: {
17
+ name: string;
18
+ version: string;
19
+ };
20
+ spans: OtlpSpan[];
21
+ }[];
22
+ }[];
23
+ }>;
@@ -0,0 +1,16 @@
1
+ import { traceKey } from "./key.js";
2
+ import { spanOf, turnTraceOf } from "./map.js";
3
+ import { DEFAULT_SERVICE_NAME, resourceSpans } from "./otlp.js";
4
+ /** Turn `turnId`'s spans as an OTLP `ExportTraceServiceRequest`, from its rows in log order. */
5
+ export async function turnTrace(ctx, sessionId, turnId, events) {
6
+ const settings = { key: await traceKey(ctx.store), content: ctx.traces?.content ?? new Set() };
7
+ const message = events.find((event) => event.type === "command.message");
8
+ const turn = turnTraceOf(settings, { tenantId: ctx.store.tenantId, sessionId, turnId }, message);
9
+ const spans = [];
10
+ for (const event of events) {
11
+ const span = spanOf(event, turn, settings);
12
+ if (span)
13
+ spans.push(span);
14
+ }
15
+ return resourceSpans(ctx.traces?.resource ?? { serviceName: DEFAULT_SERVICE_NAME }, spans);
16
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What an agent asks of a session's vaults (`POST /v1/tenant/credential-coverage`): each remote
3
+ * MCP server and each HTTP tool with a `credential`, of the agent, the agents it uses as tools and
4
+ * the agents of a flow. `VaultService.coverage` decides what a session would send for each, as
5
+ * `authorize` decides it for a call.
6
+ */
7
+ import { type AgentManifest, type WorkflowManifest } from "@nylorun/core/define";
8
+ /** One URL a manifest names that a vault credential may be bound to. */
9
+ export interface CredentialNeed {
10
+ readonly kind: "mcp" | "http";
11
+ /** The declaring agent; absent for the manifest's own agent. */
12
+ readonly agentId?: string;
13
+ /** A flow's HTTP stage or HTTP verifier: its stage key. */
14
+ readonly stage?: string;
15
+ /** The MCP server's name, or the HTTP tool's (or stage's) name. */
16
+ readonly name: string;
17
+ /** What a credential selection names: the server's name, or the HTTP tool's `credential`. */
18
+ readonly serverName: string;
19
+ /** The URL as the manifest names it. */
20
+ readonly url: string;
21
+ }
22
+ /**
23
+ * The manifest's needs: its own agent's, then each agent it uses as a tool, then a flow's
24
+ * stages and agents (nested flow agents under their stage keys). Repeats are dropped.
25
+ */
26
+ export declare function credentialNeeds(manifest: AgentManifest | WorkflowManifest): CredentialNeed[];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * What an agent asks of a session's vaults (`POST /v1/tenant/credential-coverage`): each remote
3
+ * MCP server and each HTTP tool with a `credential`, of the agent, the agents it uses as tools and
4
+ * the agents of a flow. `VaultService.coverage` decides what a session would send for each, as
5
+ * `authorize` decides it for a call.
6
+ */
7
+ import { delegatesOf, flowDelegatesOf, forEachFlowNode, isWorkflowManifest, } from "@nylorun/core/define";
8
+ /**
9
+ * The manifest's needs: its own agent's, then each agent it uses as a tool, then a flow's
10
+ * stages and agents (nested flow agents under their stage keys). Repeats are dropped.
11
+ */
12
+ export function credentialNeeds(manifest) {
13
+ const needs = new Map();
14
+ const add = (need) => {
15
+ const key = JSON.stringify([need.kind, need.agentId, need.stage, need.name, need.serverName, need.url]);
16
+ if (!needs.has(key))
17
+ needs.set(key, need);
18
+ };
19
+ const owner = (agentId) => (agentId === undefined ? {} : { agentId });
20
+ const agent = (manifest, agentId) => {
21
+ for (const capability of manifest.capabilities) {
22
+ for (const server of Object.values(capability.mcpServers ?? {}))
23
+ add({ kind: "mcp", ...owner(agentId), name: server.name, serverName: server.name, url: server.url });
24
+ for (const tool of capability.tools ?? [])
25
+ if (tool.http?.credential !== undefined)
26
+ add({
27
+ kind: "http",
28
+ ...owner(agentId),
29
+ name: tool.name,
30
+ serverName: tool.http.credential,
31
+ url: tool.http.url,
32
+ });
33
+ }
34
+ for (const child of delegatesOf(manifest))
35
+ agent(child.manifest, child.manifest.id);
36
+ for (const child of flowDelegatesOf(manifest))
37
+ flow(child.manifest, child.manifest.id);
38
+ };
39
+ const flow = (manifest, prefix) => {
40
+ forEachFlowNode(manifest.root, ({ node, key }) => {
41
+ const http = "http" in node ? node.http : "tool" in node ? node.tool.http : undefined;
42
+ if (http?.credential === undefined)
43
+ return;
44
+ add({
45
+ kind: "http",
46
+ stage: key,
47
+ name: "tool" in node ? node.tool.name : key,
48
+ serverName: http.credential,
49
+ url: http.url,
50
+ });
51
+ }, { prefix });
52
+ for (const [id, embedded] of Object.entries(manifest.agents)) {
53
+ if (isWorkflowManifest(embedded))
54
+ flow(embedded, prefix ? `${prefix}/${id}` : id);
55
+ else
56
+ agent(embedded, id);
57
+ }
58
+ };
59
+ if (isWorkflowManifest(manifest))
60
+ flow(manifest, "");
61
+ else
62
+ agent(manifest);
63
+ return [...needs.values()];
64
+ }
@@ -1,4 +1,14 @@
1
1
  export declare class VaultError extends Error {
2
2
  readonly status: number;
3
- constructor(status: number, message: string);
3
+ /** The rejection's code and details, when the answer carries one (`credential_conflict`). */
4
+ readonly rejection: {
5
+ readonly code?: string;
6
+ readonly details?: Record<string, unknown>;
7
+ };
8
+ constructor(status: number, message: string,
9
+ /** The rejection's code and details, when the answer carries one (`credential_conflict`). */
10
+ rejection?: {
11
+ readonly code?: string;
12
+ readonly details?: Record<string, unknown>;
13
+ });
4
14
  }
@@ -1,7 +1,11 @@
1
1
  export class VaultError extends Error {
2
2
  status;
3
- constructor(status, message) {
3
+ rejection;
4
+ constructor(status, message,
5
+ /** The rejection's code and details, when the answer carries one (`credential_conflict`). */
6
+ rejection = {}) {
4
7
  super(message);
5
8
  this.status = status;
9
+ this.rejection = rejection;
6
10
  }
7
11
  }
@@ -1,4 +1,5 @@
1
- import type { CreateCredentialRequest, CreateVaultRequest, CredentialIdentity, CredentialInfo, CredentialSelection, HostModelProviderInfo, HostModelView, PutHostModelRequest, CustomModelSettings, RotateCredentialRequest, SelectHostModelRequest, VaultInfo } from "@nylorun/core/contracts";
1
+ import type { CredentialCoverage, CredentialCoverageRequest, CredentialIdentity, EnvironmentSecretView, HostModelProviderInfo, HostModelView, PutEnvironmentSecretRequest, PutHostModelRequest, CustomModelSettings, SelectHostModelRequest } from "@nylorun/core/contracts";
2
+ import type { AgentManifest, WorkflowManifest } from "@nylorun/core/define";
2
3
  import type { SessionStore, Tx, VaultCredentialRow } from "../store/types.js";
3
4
  export declare const HOST_VAULT_ID = "host";
4
5
  export type HostModelSecret = {
@@ -31,8 +32,6 @@ export type AuthorizeResult = {
31
32
  via?: string;
32
33
  /** The header that names the session owner; `sessionCredentials` adds it. */
33
34
  identity?: CredentialIdentity;
34
- /** The scope of the vault the credential is in. */
35
- vault: "installation" | "user";
36
35
  } | {
37
36
  status: "refused";
38
37
  url: string;
@@ -48,54 +47,43 @@ export declare class VaultService {
48
47
  private readonly store;
49
48
  private readonly kek;
50
49
  constructor(options: VaultServiceOptions);
51
- createVault(body: CreateVaultRequest): Promise<VaultInfo>;
50
+ /** Creates Environment `environmentId`'s vault (D56), in the transaction that creates it. */
51
+ createEnvironmentVault(t: Tx, environmentId: string, actor: string): Promise<void>;
52
52
  /**
53
- * One person's vaults, then (with `installation`) the installation vaults. No owner lists
54
- * the installation vaults only.
53
+ * Deletes Environment `environmentId`'s vault and its credentials, with their audit rows, in
54
+ * the transaction that deletes the Environment: no release or tool call finds them after it
55
+ * commits.
55
56
  */
56
- listVaults(ownerUserId: string | undefined, options?: {
57
- installation?: boolean;
58
- }): Promise<VaultInfo[]>;
59
- getVault(id: string): Promise<VaultInfo>;
60
- deleteVault(id: string): Promise<{
61
- id: string;
62
- }>;
63
- createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>;
64
- listCredentials(vaultId: string): Promise<CredentialInfo[]>;
65
- getCredential(vaultId: string, id: string): Promise<CredentialInfo>;
66
- rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): Promise<CredentialInfo>;
67
- deleteCredential(vaultId: string, id: string): Promise<{
68
- id: string;
69
- }>;
57
+ deleteEnvironmentVault(t: Tx, environmentId: string, actor: string): Promise<void>;
70
58
  /**
71
- * Checks a session's vaults and selections. Run in the session PUT transaction. `opaque`
72
- * (a request acting for a subject) reports another owner's vault as the 404 of a missing one.
59
+ * `PUT /v1/environments/{id}/secrets/{name}`: creates or replaces one credential. The same
60
+ * name and type keeps the credential's id and re-seals its value and binding; another type
61
+ * replaces it. Refused (`409 credential_conflict`) when another credential of the vault is
62
+ * bound to one of its hosts or to its URL. Runs under the Environment row's lock, which also
63
+ * orders it with session opens and deletes, and counts as the Environment's use.
73
64
  */
74
- assertAttachment(t: Tx, ownerUserId: string, vaultIds: readonly string[], selections: readonly CredentialSelection[], options?: {
75
- opaque?: boolean;
76
- }): Promise<void>;
77
- /** Writes the `attach` audit row. Run in the session PUT transaction. */
78
- recordAttachment(t: Tx, sessionId: string, vaultIds: readonly string[]): Promise<void>;
65
+ putEnvironmentSecret(environmentId: string, name: string, body: PutEnvironmentSecretRequest, actor: string): Promise<EnvironmentSecretView>;
66
+ /** The Environment's secrets: names, types and bindings, never values. */
67
+ listEnvironmentSecrets(environmentId: string): Promise<EnvironmentSecretView[]>;
68
+ getEnvironmentSecret(environmentId: string, name: string): Promise<EnvironmentSecretView>;
69
+ deleteEnvironmentSecret(environmentId: string, name: string, actor: string): Promise<{
70
+ id: string;
71
+ }>;
79
72
  /**
80
73
  * The variables a session's sandbox commands get (R2c, D50): each `environment_secret`'s name
81
- * set to the sentinel, each `environment_variable`'s value, and each host a secret is bound to
82
- * with that secret's credential id. `conflict` says why they are ambiguous: two credentials with one variable name, or two
83
- * secrets bound to one host. Reads rows only; no plaintext. Run in the caller's transaction.
74
+ * set to the sentinel, and each `environment_variable`'s value, from the Environment's vault.
75
+ * Reads rows only; no plaintext. Run in the caller's transaction.
84
76
  */
85
- sessionEnvironment(t: Tx, vaultIds: readonly string[]): Promise<{
86
- environment: Record<string, string>;
87
- secretHosts: Record<string, string>;
88
- conflict?: string;
89
- }>;
77
+ environmentVariables(t: Tx, environmentId: string | undefined): Promise<Record<string, string>>;
90
78
  /**
91
- * The header egress-gate sets on a request from `sandboxId` to `host` (R2c, D50): the
92
- * `environment_secret` bound to `host` among the vaults of the sessions using the sandbox, its
93
- * value unsealed and rendered into its format. `none` when no secret is bound to the host;
94
- * `refused` when two are, or the sealed value cannot be read. One query and one unseal per
95
- * call; nothing is cached (E3). Plaintext exists only in the returned header.
79
+ * The header egress-gate sets on a request from Environment `environmentId`'s sandbox to
80
+ * `host` (R2c, D50): the `environment_secret` of its vault bound to `host`, its value unsealed
81
+ * and rendered into its format. `none` when no secret is bound to the host; `refused` when the
82
+ * sealed value cannot be read. One query and one unseal per call; nothing is cached (E3).
83
+ * Plaintext exists only in the returned header.
96
84
  */
97
85
  releaseEnvironmentSecret(input: {
98
- sandboxId: string;
86
+ environmentId: string;
99
87
  host: string;
100
88
  }): Promise<{
101
89
  status: "none";
@@ -108,16 +96,24 @@ export declare class VaultService {
108
96
  status: "refused";
109
97
  reason: "ambiguous" | "unreadable";
110
98
  }>;
111
- /** Whether any `environment_secret` of the sessions using `sandboxId` is bound to `host`. */
112
- environmentSecretBound(sandboxId: string, host: string): Promise<boolean>;
99
+ /** Whether an `environment_secret` of Environment `environmentId` is bound to `host`. */
100
+ environmentSecretBound(environmentId: string, host: string): Promise<boolean>;
101
+ /**
102
+ * The headers a request to `url` carries: the credential of the Environment's vault bound to
103
+ * it, or none (`unauthenticated`) without an Environment or such a credential.
104
+ */
113
105
  authorize(input: {
114
106
  /** The session the request is made for; absent for a tool preview (R2b C12). */
115
107
  sessionId?: string;
116
- vaultIds: readonly string[];
117
- credentialSelections: readonly CredentialSelection[];
108
+ environmentId?: string;
118
109
  url: string;
119
- serverName?: string;
120
110
  }): Promise<AuthorizeResult>;
111
+ /**
112
+ * What a session that names `request.environmentId` would send for each URL `manifest` names
113
+ * (`credentialNeeds`), decided as `authorize` decides a call: the credential of the
114
+ * Environment's vault bound to the URL. Reads no secret, writes no audit row.
115
+ */
116
+ coverage(manifest: AgentManifest | WorkflowManifest, request: CredentialCoverageRequest): Promise<CredentialCoverage>;
121
117
  getHostModel(): Promise<HostModelView>;
122
118
  listHostProviders(): Promise<{
123
119
  providers: HostModelProviderInfo[];
@@ -127,14 +123,17 @@ export declare class VaultService {
127
123
  private replay;
128
124
  /** Re-seals a user credential's payload with its new binding and returns the updated row. */
129
125
  private writePayload;
130
- private vaultInfo;
126
+ /**
127
+ * The vault of a live Environment (not being deleted), or the 404 of a missing one. With
128
+ * `lock`, the Environment's row is locked until the transaction ends.
129
+ */
130
+ private environmentVault;
131
131
  private ensureHostVault;
132
132
  private validateHostModel;
133
133
  private insertHostCredential;
134
134
  private hostModelView;
135
135
  private setActiveProvider;
136
136
  private deleteHostProviderRows;
137
- private credentialRow;
138
137
  private audit;
139
138
  }
140
139
  /** The host vault's model credential rows that parse. */