@mongodb-js/agent-engine-runner-shared 0.11.3

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 (220) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE.md +201 -0
  3. package/README.md +29 -0
  4. package/dist/agent_config.d.ts +167 -0
  5. package/dist/agent_config.d.ts.map +1 -0
  6. package/dist/agent_config.js +544 -0
  7. package/dist/call_interrupted.d.ts +12 -0
  8. package/dist/call_interrupted.d.ts.map +1 -0
  9. package/dist/call_interrupted.js +11 -0
  10. package/dist/checkpoint_workspace.d.ts +25 -0
  11. package/dist/checkpoint_workspace.d.ts.map +1 -0
  12. package/dist/checkpoint_workspace.js +44 -0
  13. package/dist/context.d.ts +235 -0
  14. package/dist/context.d.ts.map +1 -0
  15. package/dist/context.js +322 -0
  16. package/dist/db_config.d.ts +28 -0
  17. package/dist/db_config.d.ts.map +1 -0
  18. package/dist/db_config.js +66 -0
  19. package/dist/db_naming.d.ts +54 -0
  20. package/dist/db_naming.d.ts.map +1 -0
  21. package/dist/db_naming.js +94 -0
  22. package/dist/error_reporting.d.ts +67 -0
  23. package/dist/error_reporting.d.ts.map +1 -0
  24. package/dist/error_reporting.js +311 -0
  25. package/dist/generated/workflow/v1/activity_pb.d.ts +342 -0
  26. package/dist/generated/workflow/v1/activity_pb.d.ts.map +1 -0
  27. package/dist/generated/workflow/v1/activity_pb.js +115 -0
  28. package/dist/generated/workflow/v1/common_pb.d.ts +184 -0
  29. package/dist/generated/workflow/v1/common_pb.d.ts.map +1 -0
  30. package/dist/generated/workflow/v1/common_pb.js +86 -0
  31. package/dist/generated/workflow/v1/runtime_pb.d.ts +200 -0
  32. package/dist/generated/workflow/v1/runtime_pb.d.ts.map +1 -0
  33. package/dist/generated/workflow/v1/runtime_pb.js +40 -0
  34. package/dist/generated/workflow/v1/state_pb.d.ts +254 -0
  35. package/dist/generated/workflow/v1/state_pb.d.ts.map +1 -0
  36. package/dist/generated/workflow/v1/state_pb.js +68 -0
  37. package/dist/guardrails_evaluator/core.d.ts +23 -0
  38. package/dist/guardrails_evaluator/core.d.ts.map +1 -0
  39. package/dist/guardrails_evaluator/core.js +122 -0
  40. package/dist/guardrails_evaluator/index.d.ts +10 -0
  41. package/dist/guardrails_evaluator/index.d.ts.map +1 -0
  42. package/dist/guardrails_evaluator/index.js +11 -0
  43. package/dist/guardrails_evaluator/regex.d.ts +20 -0
  44. package/dist/guardrails_evaluator/regex.d.ts.map +1 -0
  45. package/dist/guardrails_evaluator/regex.js +233 -0
  46. package/dist/hooks.d.ts +109 -0
  47. package/dist/hooks.d.ts.map +1 -0
  48. package/dist/hooks.js +216 -0
  49. package/dist/http_path.d.ts +18 -0
  50. package/dist/http_path.d.ts.map +1 -0
  51. package/dist/http_path.js +53 -0
  52. package/dist/index.d.ts +35 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +41 -0
  55. package/dist/launcher.d.ts +130 -0
  56. package/dist/launcher.d.ts.map +1 -0
  57. package/dist/launcher.js +325 -0
  58. package/dist/logger.d.ts +96 -0
  59. package/dist/logger.d.ts.map +1 -0
  60. package/dist/logger.js +204 -0
  61. package/dist/mcp_oauth.d.ts +51 -0
  62. package/dist/mcp_oauth.d.ts.map +1 -0
  63. package/dist/mcp_oauth.js +389 -0
  64. package/dist/mcp_oauth_secret.d.ts +21 -0
  65. package/dist/mcp_oauth_secret.d.ts.map +1 -0
  66. package/dist/mcp_oauth_secret.js +122 -0
  67. package/dist/mcp_tools.d.ts +71 -0
  68. package/dist/mcp_tools.d.ts.map +1 -0
  69. package/dist/mcp_tools.js +301 -0
  70. package/dist/memory_appbound.d.ts +42 -0
  71. package/dist/memory_appbound.d.ts.map +1 -0
  72. package/dist/memory_appbound.js +159 -0
  73. package/dist/memory_writer.d.ts +49 -0
  74. package/dist/memory_writer.d.ts.map +1 -0
  75. package/dist/memory_writer.js +171 -0
  76. package/dist/metrics.d.ts +84 -0
  77. package/dist/metrics.d.ts.map +1 -0
  78. package/dist/metrics.js +205 -0
  79. package/dist/models.d.ts +1458 -0
  80. package/dist/models.d.ts.map +1 -0
  81. package/dist/models.js +1726 -0
  82. package/dist/node_logger.d.ts +43 -0
  83. package/dist/node_logger.d.ts.map +1 -0
  84. package/dist/node_logger.js +158 -0
  85. package/dist/owner_callback.d.ts +16 -0
  86. package/dist/owner_callback.d.ts.map +1 -0
  87. package/dist/owner_callback.js +40 -0
  88. package/dist/progress.d.ts +57 -0
  89. package/dist/progress.d.ts.map +1 -0
  90. package/dist/progress.js +140 -0
  91. package/dist/runtime.d.ts +131 -0
  92. package/dist/runtime.d.ts.map +1 -0
  93. package/dist/runtime.js +351 -0
  94. package/dist/secure_llm_proxy.d.ts +115 -0
  95. package/dist/secure_llm_proxy.d.ts.map +1 -0
  96. package/dist/secure_llm_proxy.js +922 -0
  97. package/dist/secure_wrapper.d.ts +332 -0
  98. package/dist/secure_wrapper.d.ts.map +1 -0
  99. package/dist/secure_wrapper.js +1249 -0
  100. package/dist/server/aer.d.ts +61 -0
  101. package/dist/server/aer.d.ts.map +1 -0
  102. package/dist/server/aer.js +1124 -0
  103. package/dist/server/auth.d.ts +56 -0
  104. package/dist/server/auth.d.ts.map +1 -0
  105. package/dist/server/auth.js +132 -0
  106. package/dist/server/base.d.ts +104 -0
  107. package/dist/server/base.d.ts.map +1 -0
  108. package/dist/server/base.js +150 -0
  109. package/dist/server/callInterrupt.d.ts +49 -0
  110. package/dist/server/callInterrupt.d.ts.map +1 -0
  111. package/dist/server/callInterrupt.js +68 -0
  112. package/dist/server/callback_delivery.d.ts +14 -0
  113. package/dist/server/callback_delivery.d.ts.map +1 -0
  114. package/dist/server/callback_delivery.js +141 -0
  115. package/dist/server/chunk_types.d.ts +50 -0
  116. package/dist/server/chunk_types.d.ts.map +1 -0
  117. package/dist/server/chunk_types.js +62 -0
  118. package/dist/server/cors.d.ts +52 -0
  119. package/dist/server/cors.d.ts.map +1 -0
  120. package/dist/server/cors.js +107 -0
  121. package/dist/server/drain.d.ts +169 -0
  122. package/dist/server/drain.d.ts.map +1 -0
  123. package/dist/server/drain.js +455 -0
  124. package/dist/server/function.d.ts +77 -0
  125. package/dist/server/function.d.ts.map +1 -0
  126. package/dist/server/function.js +337 -0
  127. package/dist/server/http_retry.d.ts +37 -0
  128. package/dist/server/http_retry.d.ts.map +1 -0
  129. package/dist/server/http_retry.js +157 -0
  130. package/dist/server/index.d.ts +7 -0
  131. package/dist/server/index.d.ts.map +1 -0
  132. package/dist/server/index.js +5 -0
  133. package/dist/server/metadata.d.ts +50 -0
  134. package/dist/server/metadata.d.ts.map +1 -0
  135. package/dist/server/metadata.js +193 -0
  136. package/dist/server/oe_url.d.ts +36 -0
  137. package/dist/server/oe_url.d.ts.map +1 -0
  138. package/dist/server/oe_url.js +50 -0
  139. package/dist/server/owner_url.d.ts +35 -0
  140. package/dist/server/owner_url.d.ts.map +1 -0
  141. package/dist/server/owner_url.js +146 -0
  142. package/dist/server/query.d.ts +42 -0
  143. package/dist/server/query.d.ts.map +1 -0
  144. package/dist/server/query.js +28 -0
  145. package/dist/server/tool.d.ts +138 -0
  146. package/dist/server/tool.d.ts.map +1 -0
  147. package/dist/server/tool.js +1017 -0
  148. package/dist/span_names.d.ts +21 -0
  149. package/dist/span_names.d.ts.map +1 -0
  150. package/dist/span_names.js +31 -0
  151. package/dist/structured_logging/constants.d.ts +17 -0
  152. package/dist/structured_logging/constants.d.ts.map +1 -0
  153. package/dist/structured_logging/constants.js +71 -0
  154. package/dist/structured_logging/env.d.ts +18 -0
  155. package/dist/structured_logging/env.d.ts.map +1 -0
  156. package/dist/structured_logging/env.js +39 -0
  157. package/dist/structured_logging/install.d.ts +56 -0
  158. package/dist/structured_logging/install.d.ts.map +1 -0
  159. package/dist/structured_logging/install.js +107 -0
  160. package/dist/structured_logging/layout.d.ts +9 -0
  161. package/dist/structured_logging/layout.d.ts.map +1 -0
  162. package/dist/structured_logging/layout.js +144 -0
  163. package/dist/structured_logging/serialize.d.ts +27 -0
  164. package/dist/structured_logging/serialize.d.ts.map +1 -0
  165. package/dist/structured_logging/serialize.js +61 -0
  166. package/dist/structured_logging/stdio_capture.d.ts +59 -0
  167. package/dist/structured_logging/stdio_capture.d.ts.map +1 -0
  168. package/dist/structured_logging/stdio_capture.js +164 -0
  169. package/dist/structured_logging/uncaught.d.ts +14 -0
  170. package/dist/structured_logging/uncaught.d.ts.map +1 -0
  171. package/dist/structured_logging/uncaught.js +58 -0
  172. package/dist/structured_logging.d.ts +48 -0
  173. package/dist/structured_logging.d.ts.map +1 -0
  174. package/dist/structured_logging.js +47 -0
  175. package/dist/tls_client.d.ts +61 -0
  176. package/dist/tls_client.d.ts.map +1 -0
  177. package/dist/tls_client.js +298 -0
  178. package/dist/tool_api_error.d.ts +62 -0
  179. package/dist/tool_api_error.d.ts.map +1 -0
  180. package/dist/tool_api_error.js +399 -0
  181. package/dist/tool_memory_ownership.d.ts +10 -0
  182. package/dist/tool_memory_ownership.d.ts.map +1 -0
  183. package/dist/tool_memory_ownership.js +36 -0
  184. package/dist/toolpod_handlers.d.ts +126 -0
  185. package/dist/toolpod_handlers.d.ts.map +1 -0
  186. package/dist/toolpod_handlers.js +1016 -0
  187. package/dist/tracing/exporters.d.ts +51 -0
  188. package/dist/tracing/exporters.d.ts.map +1 -0
  189. package/dist/tracing/exporters.js +327 -0
  190. package/dist/tracing/index.d.ts +3 -0
  191. package/dist/tracing/index.d.ts.map +1 -0
  192. package/dist/tracing/index.js +2 -0
  193. package/dist/tracing/setup.d.ts +76 -0
  194. package/dist/tracing/setup.d.ts.map +1 -0
  195. package/dist/tracing/setup.js +436 -0
  196. package/dist/utils.d.ts +204 -0
  197. package/dist/utils.d.ts.map +1 -0
  198. package/dist/utils.js +867 -0
  199. package/dist/workflow/activity.d.ts +71 -0
  200. package/dist/workflow/activity.d.ts.map +1 -0
  201. package/dist/workflow/activity.js +357 -0
  202. package/dist/workflow/attempt.d.ts +12 -0
  203. package/dist/workflow/attempt.d.ts.map +1 -0
  204. package/dist/workflow/attempt.js +96 -0
  205. package/dist/workflow/client.d.ts +46 -0
  206. package/dist/workflow/client.d.ts.map +1 -0
  207. package/dist/workflow/client.js +299 -0
  208. package/dist/workflow/context.d.ts +37 -0
  209. package/dist/workflow/context.d.ts.map +1 -0
  210. package/dist/workflow/context.js +350 -0
  211. package/dist/workflow/heartbeat.d.ts +15 -0
  212. package/dist/workflow/heartbeat.d.ts.map +1 -0
  213. package/dist/workflow/heartbeat.js +78 -0
  214. package/dist/workflow/index.d.ts +14 -0
  215. package/dist/workflow/index.d.ts.map +1 -0
  216. package/dist/workflow/index.js +10 -0
  217. package/dist/workflow/memory.d.ts +17 -0
  218. package/dist/workflow/memory.d.ts.map +1 -0
  219. package/dist/workflow/memory.js +184 -0
  220. package/package.json +73 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Read request-scoped secrets from the fctr metadata directory.
3
+ *
4
+ * In RPC mode the OE delivers per-tool secrets via `SetMetadata` before each
5
+ * tool call and clears them after (when restriction is enabled). fctr
6
+ * materializes the keyset as files at `/run/meta/<KEY>`. This module reads
7
+ * those files so the tool server can inject them into `process.env` for the
8
+ * duration of the call — or permanently when restriction is disabled.
9
+ *
10
+ * Mirrors Python's `agent_engine_runner_shared.server.metadata`.
11
+ */
12
+ import { readdirSync, readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { getLogger } from "../logger.js";
15
+ const logger = getLogger("agent_engine_runner_shared.server.metadata");
16
+ const METADATA_DIR = "/run/meta";
17
+ // Overlap accounting for applied-env regions. process.env is process-global:
18
+ // if a region overlaps one that has secrets applied, its snapshot captures
19
+ // those secrets as baseline and its restore re-installs them permanently
20
+ // while deleting the other call's live ones. The ToolServer's
21
+ // per-instance executeGate serializes its own routes, but that
22
+ // invariant lives in the caller — these counters make the module itself fail
23
+ // closed (throw before any env mutation) if any future call site overlaps a
24
+ // secret-bearing region. Secret-less overlap stays allowed: the snapshots
25
+ // are then identical to the live env and restore is idempotent, which is
26
+ // what the cross-instance ToolServer overlap contract relies on.
27
+ let activeRegions = 0;
28
+ let activeSecretRegions = 0;
29
+ /**
30
+ * Read all key-value pairs from the metadata directory.
31
+ *
32
+ * Returns a record mapping filename (the secret name) to file content (the
33
+ * secret value). Skips files that cannot be read and logs a warning. Returns
34
+ * an empty record when the directory does not exist.
35
+ */
36
+ export function readMetadataSecrets(metadataDir = METADATA_DIR) {
37
+ let entries;
38
+ try {
39
+ entries = readdirSync(metadataDir, { withFileTypes: true });
40
+ }
41
+ catch {
42
+ return {};
43
+ }
44
+ const secrets = {};
45
+ for (const entry of entries) {
46
+ if (!entry.isFile())
47
+ continue;
48
+ try {
49
+ secrets[entry.name] = readFileSync(join(metadataDir, entry.name), "utf-8");
50
+ }
51
+ catch (e) {
52
+ logger.warn(`failed to read metadata file ${entry.name}`, e);
53
+ }
54
+ }
55
+ return secrets;
56
+ }
57
+ /**
58
+ * Overwrite `process.env` from `/run/meta`; no restore or prune.
59
+ */
60
+ export function mergeMetadataEnv(metadataDir = METADATA_DIR) {
61
+ const secrets = readMetadataSecrets(metadataDir);
62
+ const count = Object.keys(secrets).length;
63
+ if (count === 0)
64
+ return;
65
+ Object.assign(process.env, secrets);
66
+ logger.debug(`merged ${count} metadata secret(s) into process.env (no restore)`);
67
+ }
68
+ /**
69
+ * Snapshot `process.env`, merge in the metadata secrets, and return a
70
+ * `restore()` that reverts `process.env`.
71
+ *
72
+ * What `restore()` does depends on how the region was entered:
73
+ *
74
+ * - *clean* (no other region was active — the serialized production flow):
75
+ * full snapshot restore. Keys the region added are removed, overwritten
76
+ * keys are reverted, and keys the callback deleted are reinstated.
77
+ * - *tainted* (entered while another, necessarily secret-less, region was
78
+ * active): delete-only restore. Keys added since the snapshot are removed,
79
+ * but no values are written back, since the snapshot holds the other
80
+ * region's request-scoped state and rewriting it could outlive that
81
+ * region's own restore. A tainted callback's overwrite of a pre-existing
82
+ * key therefore survives the region.
83
+ *
84
+ * @throws if the region overlaps a concurrent secret-bearing region — either
85
+ * another region already has secrets applied, or this region carries secrets
86
+ * while any region is active. Thrown before `process.env` is touched (fail
87
+ * closed), so callers see a per-request error instead of a silent
88
+ * cross-request credential swap. Production call sites (`/execute`,
89
+ * `/invoke_llm`, `/invoke_llm/stream`) are serialized by the ToolServer's
90
+ * per-instance execute gate and do not hit this.
91
+ */
92
+ function applyMetadataEnv(metadataDir) {
93
+ const secrets = readMetadataSecrets(metadataDir);
94
+ const count = Object.keys(secrets).length;
95
+ if (activeSecretRegions > 0 || (count > 0 && activeRegions > 0)) {
96
+ // Fail closed before mutating anything: proceeding would snapshot or
97
+ // clobber another request's live secrets.
98
+ throw new Error("metadata env region overlaps a concurrent secret-bearing region; " +
99
+ "refusing to apply request secrets to the shared process.env");
100
+ }
101
+ const snapshot = { ...process.env };
102
+ // A region that entered on a quiet environment owns the true baseline:
103
+ // its restore is a full snapshot restore — deleting added keys, reverting
104
+ // overwritten ones, and reinstating keys the callback deleted — the
105
+ // documented contract for the serialized (production) flow. A region that
106
+ // entered while another was active holds a snapshot tainted with that
107
+ // region's request-scoped state: writing any value from it could
108
+ // permanently reassert the other request's data after its restore, so a
109
+ // tainted restore only deletes the keys added since its snapshot and
110
+ // never writes values. (Tainted regions are secret-less by the guard
111
+ // above, so at worst a tainted callback's own overwrite of a pre-existing
112
+ // key outlives it.)
113
+ const enteredClean = activeRegions === 0;
114
+ const restoreEnv = () => {
115
+ for (const key of Object.keys(process.env)) {
116
+ if (!(key in snapshot))
117
+ delete process.env[key];
118
+ }
119
+ if (enteredClean) {
120
+ Object.assign(process.env, snapshot);
121
+ }
122
+ };
123
+ try {
124
+ Object.assign(process.env, secrets);
125
+ }
126
+ catch (e) {
127
+ // Node rejects some values (e.g. containing NUL) and Object.assign may
128
+ // have applied a prefix of the keys before throwing: roll the partial
129
+ // application back, and only count the region once the apply succeeded —
130
+ // otherwise the counters stay unbalanced and every later region rejects.
131
+ restoreEnv();
132
+ throw e;
133
+ }
134
+ activeRegions += 1;
135
+ if (count > 0) {
136
+ activeSecretRegions += 1;
137
+ logger.debug(`applied ${count} metadata secret(s) to process.env`);
138
+ }
139
+ let restored = false;
140
+ return {
141
+ restore() {
142
+ if (restored)
143
+ return;
144
+ restored = true;
145
+ activeRegions -= 1;
146
+ if (count > 0)
147
+ activeSecretRegions -= 1;
148
+ restoreEnv();
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * Run `fn` with metadata secrets merged into `process.env`, restoring the
154
+ * environment afterwards (even on throw).
155
+ *
156
+ * Mirrors Python's `applied_metadata_env` context manager for non-streaming
157
+ * call paths.
158
+ *
159
+ * @throws if this region overlaps a concurrent secret-bearing region; see
160
+ * {@link applyMetadataEnv} for the fail-closed rule and the clean-vs-tainted
161
+ * restore semantics.
162
+ */
163
+ export async function withMetadataEnv(fn, metadataDir = METADATA_DIR) {
164
+ const applied = applyMetadataEnv(metadataDir);
165
+ try {
166
+ return await fn();
167
+ }
168
+ finally {
169
+ applied.restore();
170
+ }
171
+ }
172
+ /**
173
+ * Wrap an async generator so metadata secrets are present in `process.env`
174
+ * for the lifetime of the iteration, restoring the environment when iteration
175
+ * completes, throws, or is closed early.
176
+ *
177
+ * Mirrors Python's `applied_metadata_env` context manager for streaming call
178
+ * paths.
179
+ *
180
+ * @throws if this region overlaps a concurrent secret-bearing region — note a
181
+ * stream holds its region open for the whole iteration; see
182
+ * {@link applyMetadataEnv} for the fail-closed rule and the clean-vs-tainted
183
+ * restore semantics.
184
+ */
185
+ export async function* withMetadataEnvGen(genFactory, metadataDir = METADATA_DIR) {
186
+ const applied = applyMetadataEnv(metadataDir);
187
+ try {
188
+ yield* genFactory();
189
+ }
190
+ finally {
191
+ applied.restore();
192
+ }
193
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Trusted resolution of the Orchestration Engine callback URL.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.oe_url`. Both runtimes are parallel
5
+ * implementations of the same server architecture, so this file and its
6
+ * Python twin must be changed together.
7
+ *
8
+ * `/execute` accepts a `platform_api_url` field, and that value used to
9
+ * become the base for every outbound call the runner made during an
10
+ * execution: tool and LLM approval requests, stream chunks, terminal
11
+ * results, node execution records. The runner then treats those responses
12
+ * as authoritative — as both the real tool/LLM result and the policy
13
+ * approval decision — so a caller-chosen URL could exfiltrate prompts and
14
+ * checkpointed history while injecting fabricated tool output and forged
15
+ * "approved" decisions back into the agent's reasoning, which are then
16
+ * persisted into session state.
17
+ *
18
+ * The runner already knows where its OE is: ECP stamps `OE_URL` on every
19
+ * runner component at deploy time. Preferring that over the request field
20
+ * removes the callback-hijack and SSRF surface rather than trying to
21
+ * validate an attacker-supplied string.
22
+ */
23
+ /**
24
+ * Returns the OE base URL to use for callbacks during an execution.
25
+ *
26
+ * The runner's own `OE_URL` wins whenever it is configured. A differing
27
+ * request value is discarded and logged rather than honoured — quietly
28
+ * ignoring it would hide an attempted hijack.
29
+ *
30
+ * When `OE_URL` is unset the requested value is used. That is the local
31
+ * `agentengine dev` and unit-test path, where no deploy-time environment exists
32
+ * and the request originates from the developer's own stack; a warning is
33
+ * emitted so the weaker configuration is visible.
34
+ */
35
+ export declare function resolveOeUrl(requested: string, env?: NodeJS.ProcessEnv): string;
36
+ //# sourceMappingURL=oe_url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oe_url.d.ts","sourceRoot":"","sources":["../../src/server/oe_url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,MAAM,CAiBR"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Trusted resolution of the Orchestration Engine callback URL.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.oe_url`. Both runtimes are parallel
5
+ * implementations of the same server architecture, so this file and its
6
+ * Python twin must be changed together.
7
+ *
8
+ * `/execute` accepts a `platform_api_url` field, and that value used to
9
+ * become the base for every outbound call the runner made during an
10
+ * execution: tool and LLM approval requests, stream chunks, terminal
11
+ * results, node execution records. The runner then treats those responses
12
+ * as authoritative — as both the real tool/LLM result and the policy
13
+ * approval decision — so a caller-chosen URL could exfiltrate prompts and
14
+ * checkpointed history while injecting fabricated tool output and forged
15
+ * "approved" decisions back into the agent's reasoning, which are then
16
+ * persisted into session state.
17
+ *
18
+ * The runner already knows where its OE is: ECP stamps `OE_URL` on every
19
+ * runner component at deploy time. Preferring that over the request field
20
+ * removes the callback-hijack and SSRF surface rather than trying to
21
+ * validate an attacker-supplied string.
22
+ */
23
+ import { getLogger } from "../logger.js";
24
+ const logger = getLogger("agent_engine_runner_shared.server.oe_url");
25
+ /**
26
+ * Returns the OE base URL to use for callbacks during an execution.
27
+ *
28
+ * The runner's own `OE_URL` wins whenever it is configured. A differing
29
+ * request value is discarded and logged rather than honoured — quietly
30
+ * ignoring it would hide an attempted hijack.
31
+ *
32
+ * When `OE_URL` is unset the requested value is used. That is the local
33
+ * `agentengine dev` and unit-test path, where no deploy-time environment exists
34
+ * and the request originates from the developer's own stack; a warning is
35
+ * emitted so the weaker configuration is visible.
36
+ */
37
+ export function resolveOeUrl(requested, env = process.env) {
38
+ const configured = (env["OE_URL"] ?? "").trim();
39
+ if (!configured) {
40
+ logger.warn("OE_URL is not configured; falling back to the caller-supplied " +
41
+ "platform_api_url. Set OE_URL so the callback target cannot be " +
42
+ "chosen by the request.");
43
+ return requested;
44
+ }
45
+ if (requested && requested !== configured) {
46
+ logger.warn(`Discarding caller-supplied platform_api_url ${JSON.stringify(requested)}; ` +
47
+ `using the configured OE_URL ${JSON.stringify(configured)}.`);
48
+ }
49
+ return configured;
50
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Validation of a request-supplied replica-specific OE owner callback URL.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.owner_url`. Both runtimes are parallel
5
+ * implementations of the same server architecture, so this file and its Python
6
+ * twin must be changed together.
7
+ *
8
+ * Dark-ship owner-callback fallback: the OE may stamp a `*_owner_url`
9
+ * onto the execute / tool request naming the specific OE replica that owns the
10
+ * execution. The runner delivers stream chunks, terminal callbacks and tool
11
+ * results straight to that replica and falls back to the trusted service URL
12
+ * when that owner-specific callback attempt is unusable.
13
+ *
14
+ * That owner URL is request-supplied, which reopens the callback-hijack / SSRF
15
+ * surface that `resolveOeUrl` (see `oe_url.ts`) exists to close. Unlike the OE
16
+ * base URL — where the runner's own deploy-time `OE_URL` is simply preferred —
17
+ * the owner URL has no trusted counterpart to fall back to, so it is accepted
18
+ * only when it is provably the headless-service address of one replica *behind
19
+ * the already-trusted service URL*: identical scheme, identical port (explicit
20
+ * or scheme default), and a host of exactly
21
+ * `<one-dns-label>.<service-label>-headless.<rest>` when the service host is
22
+ * `<service-label>.<rest>`. Anything else is logged and discarded — never an
23
+ * error — and the caller keeps using the trusted service URL.
24
+ */
25
+ /**
26
+ * Return the canonical bare origin of `ownerUrl` when it is a valid replica of
27
+ * `serviceUrl`, else `null`.
28
+ *
29
+ * `null` means "no usable owner URL"; the caller sends to the trusted
30
+ * `serviceUrl` instead. A malformed or forged owner URL is logged at WARNING
31
+ * and discarded rather than thrown — a rejected value must degrade to the safe
32
+ * default, never fail the callback.
33
+ */
34
+ export declare function resolveOwnerUrl(ownerUrl: string | null | undefined, serviceUrl: string): string | null;
35
+ //# sourceMappingURL=owner_url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"owner_url.d.ts","sourceRoot":"","sources":["../../src/server/owner_url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAqDH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAqFf"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Validation of a request-supplied replica-specific OE owner callback URL.
3
+ *
4
+ * Mirrors Python's `agent_engine_runner_shared.server.owner_url`. Both runtimes are parallel
5
+ * implementations of the same server architecture, so this file and its Python
6
+ * twin must be changed together.
7
+ *
8
+ * Dark-ship owner-callback fallback: the OE may stamp a `*_owner_url`
9
+ * onto the execute / tool request naming the specific OE replica that owns the
10
+ * execution. The runner delivers stream chunks, terminal callbacks and tool
11
+ * results straight to that replica and falls back to the trusted service URL
12
+ * when that owner-specific callback attempt is unusable.
13
+ *
14
+ * That owner URL is request-supplied, which reopens the callback-hijack / SSRF
15
+ * surface that `resolveOeUrl` (see `oe_url.ts`) exists to close. Unlike the OE
16
+ * base URL — where the runner's own deploy-time `OE_URL` is simply preferred —
17
+ * the owner URL has no trusted counterpart to fall back to, so it is accepted
18
+ * only when it is provably the headless-service address of one replica *behind
19
+ * the already-trusted service URL*: identical scheme, identical port (explicit
20
+ * or scheme default), and a host of exactly
21
+ * `<one-dns-label>.<service-label>-headless.<rest>` when the service host is
22
+ * `<service-label>.<rest>`. Anything else is logged and discarded — never an
23
+ * error — and the caller keeps using the trusted service URL.
24
+ */
25
+ import { getLogger } from "../logger.js";
26
+ const logger = getLogger("agent_engine_runner_shared.server.owner_url");
27
+ // WHATWG URL protocols carry a trailing colon.
28
+ const DEFAULT_PORTS = { "http:": 80, "https:": 443 };
29
+ // A single DNS label: alphanumerics plus interior hyphens. A dashed pod IP such
30
+ // as `10-1-2-3` — the label the OE actually stamps — qualifies. WHATWG URL
31
+ // lowercases the host, so a lowercase-only pattern is sufficient.
32
+ const DNS_LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
33
+ /** Stable, non-secret URL summary safe for warning logs. */
34
+ function describeUrlForLog(url) {
35
+ return JSON.stringify(url.port === ""
36
+ ? `${url.protocol}//${url.hostname}`
37
+ : `${url.protocol}//${url.hostname}:${url.port}`);
38
+ }
39
+ /** Effective port for `url` — explicit if present, else the scheme default. */
40
+ function effectivePort(url) {
41
+ if (url.port !== "")
42
+ return Number(url.port);
43
+ return DEFAULT_PORTS[url.protocol] ?? null;
44
+ }
45
+ /** Canonical bare origin for `url`, omitting scheme-default ports. */
46
+ function canonicalOrigin(url) {
47
+ const port = effectivePort(url);
48
+ const defaultPort = DEFAULT_PORTS[url.protocol] ?? null;
49
+ return port === null || port === defaultPort
50
+ ? `${url.protocol}//${url.hostname}`
51
+ : `${url.protocol}//${url.hostname}:${port}`;
52
+ }
53
+ function hasAsciiControl(value) {
54
+ for (let i = 0; i < value.length; i += 1) {
55
+ const code = value.charCodeAt(i);
56
+ if (code <= 0x1f || code === 0x7f)
57
+ return true;
58
+ }
59
+ return false;
60
+ }
61
+ /** Split a host on its first dot into `[label, rest]`, or null if there is none. */
62
+ function partitionHost(host) {
63
+ const idx = host.indexOf(".");
64
+ if (idx === -1)
65
+ return null;
66
+ return [host.slice(0, idx), host.slice(idx + 1)];
67
+ }
68
+ /**
69
+ * Return the canonical bare origin of `ownerUrl` when it is a valid replica of
70
+ * `serviceUrl`, else `null`.
71
+ *
72
+ * `null` means "no usable owner URL"; the caller sends to the trusted
73
+ * `serviceUrl` instead. A malformed or forged owner URL is logged at WARNING
74
+ * and discarded rather than thrown — a rejected value must degrade to the safe
75
+ * default, never fail the callback.
76
+ */
77
+ export function resolveOwnerUrl(ownerUrl, serviceUrl) {
78
+ if (!ownerUrl)
79
+ return null;
80
+ if (hasAsciiControl(ownerUrl)) {
81
+ logger.warn("Discarding owner URL (contains control characters); " +
82
+ "using configured service URL");
83
+ return null;
84
+ }
85
+ let owner;
86
+ let service;
87
+ try {
88
+ owner = new URL(ownerUrl);
89
+ service = new URL(serviceUrl);
90
+ }
91
+ catch {
92
+ logger.warn("Discarding unparseable owner URL; " + "using configured service URL");
93
+ return null;
94
+ }
95
+ const serviceSummary = describeUrlForLog(service);
96
+ const reject = (reason) => {
97
+ logger.warn(`Discarding owner URL (${reason}); ` +
98
+ `using service URL ${serviceSummary}`);
99
+ };
100
+ // The OE stamps a bare origin. Credentials, a path, a query, or a fragment
101
+ // are never expected and would ride along when a callback path is appended,
102
+ // so treat their presence as a forged value. WHATWG URL yields a "/" pathname
103
+ // for a bare origin, so "" and "/" are the only acceptable paths.
104
+ if (owner.username ||
105
+ owner.password ||
106
+ (owner.pathname !== "" && owner.pathname !== "/") ||
107
+ owner.search ||
108
+ owner.hash) {
109
+ reject("not a bare origin");
110
+ return null;
111
+ }
112
+ if (owner.protocol !== service.protocol) {
113
+ reject("scheme mismatch");
114
+ return null;
115
+ }
116
+ const ownerPort = effectivePort(owner);
117
+ const servicePort = effectivePort(service);
118
+ if (ownerPort === null || ownerPort !== servicePort) {
119
+ reject("port mismatch");
120
+ return null;
121
+ }
122
+ const ownerHost = owner.hostname;
123
+ const serviceHost = service.hostname;
124
+ if (!ownerHost || !serviceHost) {
125
+ reject("missing host");
126
+ return null;
127
+ }
128
+ const servicePart = partitionHost(serviceHost);
129
+ if (servicePart === null || !servicePart[0] || !servicePart[1]) {
130
+ reject("service host is not <label>.<rest>");
131
+ return null;
132
+ }
133
+ const [serviceLabel, serviceRest] = servicePart;
134
+ // partitionHost() splits on the first dot, so this guarantees exactly one
135
+ // leading label; the exact-match on the remainder enforces the full shape.
136
+ const ownerPart = partitionHost(ownerHost);
137
+ if (ownerPart === null || !DNS_LABEL.test(ownerPart[0])) {
138
+ reject("owner host is not <label>.<rest>");
139
+ return null;
140
+ }
141
+ if (ownerPart[1] !== `${serviceLabel}-headless.${serviceRest}`) {
142
+ reject("owner host is not a headless replica of the service host");
143
+ return null;
144
+ }
145
+ return canonicalOrigin(owner);
146
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Framework-agnostic query plugin protocol for the AER.
3
+ *
4
+ * The AER hosts two read endpoints that surface framework-specific persisted
5
+ * state — per-session summary and conversation messages. The implementations
6
+ * live in framework adapter packages (e.g. `agent-engine-sdk-langgraph-ts`); the
7
+ * AER itself only hosts the routes and delegates to whichever plugin the
8
+ * framework SDK registered via `registerQueryPlugin()`.
9
+ *
10
+ * Mirrors Python's `agent_engine_runner_shared/server/query.py`.
11
+ *
12
+ * Trust model
13
+ * -----------
14
+ *
15
+ * These endpoints take no workspace identifier on the wire. The AER trusts its
16
+ * caller (the platform's Orchestration Engine proxy) to pass only session_ids
17
+ * that belong to the caller's workspace; the OE establishes that scope by
18
+ * consulting the `executions` collection — which is tagged with
19
+ * `workspace_id` — before issuing the request. The framework plugin then
20
+ * narrows reads by its own session key. For the LangGraph plugin that key is
21
+ * the workspace-scoped composite `session_id:workspace_id`. The workspace is
22
+ * workspace is resolved the same way on read and write — `APP_ID` when set,
23
+ * otherwise the wire `workspace_id` from the most recent `/execute` — so a
24
+ * plugin keyed to one workspace cannot read another's checkpoints even if a
25
+ * stray session_id slipped past the proxy — defense in depth, not a substitute
26
+ * for the upstream ownership check.
27
+ */
28
+ import type { SessionMessagesResponse, SessionsSummaryResponse } from "@mongodb-js/agent-engine-sdk";
29
+ /**
30
+ * Read-side plugin that surfaces framework-specific session state.
31
+ *
32
+ * Implementations live alongside framework adapters and read from the
33
+ * adapter's persistence (LangGraph checkpoint collections, ADK state
34
+ * store, etc.).
35
+ */
36
+ export interface AERQueryPlugin {
37
+ /** Given a list of session_ids, return a SessionsSummaryResponse. */
38
+ getSummariesForSessions(sessionIds: string[]): Promise<SessionsSummaryResponse>;
39
+ /** Given a session_id, return a SessionMessagesResponse. */
40
+ getMessagesForSession(sessionId: string): Promise<SessionMessagesResponse>;
41
+ }
42
+ //# sourceMappingURL=query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/server/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EACV,uBAAuB,EACvB,uBAAuB,EACxB,MAAM,8BAA8B,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,uBAAuB,CACrB,UAAU,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAEpC,4DAA4D;IAC5D,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC5E"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Framework-agnostic query plugin protocol for the AER.
3
+ *
4
+ * The AER hosts two read endpoints that surface framework-specific persisted
5
+ * state — per-session summary and conversation messages. The implementations
6
+ * live in framework adapter packages (e.g. `agent-engine-sdk-langgraph-ts`); the
7
+ * AER itself only hosts the routes and delegates to whichever plugin the
8
+ * framework SDK registered via `registerQueryPlugin()`.
9
+ *
10
+ * Mirrors Python's `agent_engine_runner_shared/server/query.py`.
11
+ *
12
+ * Trust model
13
+ * -----------
14
+ *
15
+ * These endpoints take no workspace identifier on the wire. The AER trusts its
16
+ * caller (the platform's Orchestration Engine proxy) to pass only session_ids
17
+ * that belong to the caller's workspace; the OE establishes that scope by
18
+ * consulting the `executions` collection — which is tagged with
19
+ * `workspace_id` — before issuing the request. The framework plugin then
20
+ * narrows reads by its own session key. For the LangGraph plugin that key is
21
+ * the workspace-scoped composite `session_id:workspace_id`. The workspace is
22
+ * workspace is resolved the same way on read and write — `APP_ID` when set,
23
+ * otherwise the wire `workspace_id` from the most recent `/execute` — so a
24
+ * plugin keyed to one workspace cannot read another's checkpoints even if a
25
+ * stray session_id slipped past the proxy — defense in depth, not a substitute
26
+ * for the upstream ownership check.
27
+ */
28
+ export {};