@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE.md +201 -0
- package/README.md +29 -0
- package/dist/agent_config.d.ts +167 -0
- package/dist/agent_config.d.ts.map +1 -0
- package/dist/agent_config.js +544 -0
- package/dist/call_interrupted.d.ts +12 -0
- package/dist/call_interrupted.d.ts.map +1 -0
- package/dist/call_interrupted.js +11 -0
- package/dist/checkpoint_workspace.d.ts +25 -0
- package/dist/checkpoint_workspace.d.ts.map +1 -0
- package/dist/checkpoint_workspace.js +44 -0
- package/dist/context.d.ts +235 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +322 -0
- package/dist/db_config.d.ts +28 -0
- package/dist/db_config.d.ts.map +1 -0
- package/dist/db_config.js +66 -0
- package/dist/db_naming.d.ts +54 -0
- package/dist/db_naming.d.ts.map +1 -0
- package/dist/db_naming.js +94 -0
- package/dist/error_reporting.d.ts +67 -0
- package/dist/error_reporting.d.ts.map +1 -0
- package/dist/error_reporting.js +311 -0
- package/dist/generated/workflow/v1/activity_pb.d.ts +342 -0
- package/dist/generated/workflow/v1/activity_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/activity_pb.js +115 -0
- package/dist/generated/workflow/v1/common_pb.d.ts +184 -0
- package/dist/generated/workflow/v1/common_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/common_pb.js +86 -0
- package/dist/generated/workflow/v1/runtime_pb.d.ts +200 -0
- package/dist/generated/workflow/v1/runtime_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/runtime_pb.js +40 -0
- package/dist/generated/workflow/v1/state_pb.d.ts +254 -0
- package/dist/generated/workflow/v1/state_pb.d.ts.map +1 -0
- package/dist/generated/workflow/v1/state_pb.js +68 -0
- package/dist/guardrails_evaluator/core.d.ts +23 -0
- package/dist/guardrails_evaluator/core.d.ts.map +1 -0
- package/dist/guardrails_evaluator/core.js +122 -0
- package/dist/guardrails_evaluator/index.d.ts +10 -0
- package/dist/guardrails_evaluator/index.d.ts.map +1 -0
- package/dist/guardrails_evaluator/index.js +11 -0
- package/dist/guardrails_evaluator/regex.d.ts +20 -0
- package/dist/guardrails_evaluator/regex.d.ts.map +1 -0
- package/dist/guardrails_evaluator/regex.js +233 -0
- package/dist/hooks.d.ts +109 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +216 -0
- package/dist/http_path.d.ts +18 -0
- package/dist/http_path.d.ts.map +1 -0
- package/dist/http_path.js +53 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/launcher.d.ts +130 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +325 -0
- package/dist/logger.d.ts +96 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +204 -0
- package/dist/mcp_oauth.d.ts +51 -0
- package/dist/mcp_oauth.d.ts.map +1 -0
- package/dist/mcp_oauth.js +389 -0
- package/dist/mcp_oauth_secret.d.ts +21 -0
- package/dist/mcp_oauth_secret.d.ts.map +1 -0
- package/dist/mcp_oauth_secret.js +122 -0
- package/dist/mcp_tools.d.ts +71 -0
- package/dist/mcp_tools.d.ts.map +1 -0
- package/dist/mcp_tools.js +301 -0
- package/dist/memory_appbound.d.ts +42 -0
- package/dist/memory_appbound.d.ts.map +1 -0
- package/dist/memory_appbound.js +159 -0
- package/dist/memory_writer.d.ts +49 -0
- package/dist/memory_writer.d.ts.map +1 -0
- package/dist/memory_writer.js +171 -0
- package/dist/metrics.d.ts +84 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +205 -0
- package/dist/models.d.ts +1458 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +1726 -0
- package/dist/node_logger.d.ts +43 -0
- package/dist/node_logger.d.ts.map +1 -0
- package/dist/node_logger.js +158 -0
- package/dist/owner_callback.d.ts +16 -0
- package/dist/owner_callback.d.ts.map +1 -0
- package/dist/owner_callback.js +40 -0
- package/dist/progress.d.ts +57 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +140 -0
- package/dist/runtime.d.ts +131 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +351 -0
- package/dist/secure_llm_proxy.d.ts +115 -0
- package/dist/secure_llm_proxy.d.ts.map +1 -0
- package/dist/secure_llm_proxy.js +922 -0
- package/dist/secure_wrapper.d.ts +332 -0
- package/dist/secure_wrapper.d.ts.map +1 -0
- package/dist/secure_wrapper.js +1249 -0
- package/dist/server/aer.d.ts +61 -0
- package/dist/server/aer.d.ts.map +1 -0
- package/dist/server/aer.js +1124 -0
- package/dist/server/auth.d.ts +56 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +132 -0
- package/dist/server/base.d.ts +104 -0
- package/dist/server/base.d.ts.map +1 -0
- package/dist/server/base.js +150 -0
- package/dist/server/callInterrupt.d.ts +49 -0
- package/dist/server/callInterrupt.d.ts.map +1 -0
- package/dist/server/callInterrupt.js +68 -0
- package/dist/server/callback_delivery.d.ts +14 -0
- package/dist/server/callback_delivery.d.ts.map +1 -0
- package/dist/server/callback_delivery.js +141 -0
- package/dist/server/chunk_types.d.ts +50 -0
- package/dist/server/chunk_types.d.ts.map +1 -0
- package/dist/server/chunk_types.js +62 -0
- package/dist/server/cors.d.ts +52 -0
- package/dist/server/cors.d.ts.map +1 -0
- package/dist/server/cors.js +107 -0
- package/dist/server/drain.d.ts +169 -0
- package/dist/server/drain.d.ts.map +1 -0
- package/dist/server/drain.js +455 -0
- package/dist/server/function.d.ts +77 -0
- package/dist/server/function.d.ts.map +1 -0
- package/dist/server/function.js +337 -0
- package/dist/server/http_retry.d.ts +37 -0
- package/dist/server/http_retry.d.ts.map +1 -0
- package/dist/server/http_retry.js +157 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/metadata.d.ts +50 -0
- package/dist/server/metadata.d.ts.map +1 -0
- package/dist/server/metadata.js +193 -0
- package/dist/server/oe_url.d.ts +36 -0
- package/dist/server/oe_url.d.ts.map +1 -0
- package/dist/server/oe_url.js +50 -0
- package/dist/server/owner_url.d.ts +35 -0
- package/dist/server/owner_url.d.ts.map +1 -0
- package/dist/server/owner_url.js +146 -0
- package/dist/server/query.d.ts +42 -0
- package/dist/server/query.d.ts.map +1 -0
- package/dist/server/query.js +28 -0
- package/dist/server/tool.d.ts +138 -0
- package/dist/server/tool.d.ts.map +1 -0
- package/dist/server/tool.js +1017 -0
- package/dist/span_names.d.ts +21 -0
- package/dist/span_names.d.ts.map +1 -0
- package/dist/span_names.js +31 -0
- package/dist/structured_logging/constants.d.ts +17 -0
- package/dist/structured_logging/constants.d.ts.map +1 -0
- package/dist/structured_logging/constants.js +71 -0
- package/dist/structured_logging/env.d.ts +18 -0
- package/dist/structured_logging/env.d.ts.map +1 -0
- package/dist/structured_logging/env.js +39 -0
- package/dist/structured_logging/install.d.ts +56 -0
- package/dist/structured_logging/install.d.ts.map +1 -0
- package/dist/structured_logging/install.js +107 -0
- package/dist/structured_logging/layout.d.ts +9 -0
- package/dist/structured_logging/layout.d.ts.map +1 -0
- package/dist/structured_logging/layout.js +144 -0
- package/dist/structured_logging/serialize.d.ts +27 -0
- package/dist/structured_logging/serialize.d.ts.map +1 -0
- package/dist/structured_logging/serialize.js +61 -0
- package/dist/structured_logging/stdio_capture.d.ts +59 -0
- package/dist/structured_logging/stdio_capture.d.ts.map +1 -0
- package/dist/structured_logging/stdio_capture.js +164 -0
- package/dist/structured_logging/uncaught.d.ts +14 -0
- package/dist/structured_logging/uncaught.d.ts.map +1 -0
- package/dist/structured_logging/uncaught.js +58 -0
- package/dist/structured_logging.d.ts +48 -0
- package/dist/structured_logging.d.ts.map +1 -0
- package/dist/structured_logging.js +47 -0
- package/dist/tls_client.d.ts +61 -0
- package/dist/tls_client.d.ts.map +1 -0
- package/dist/tls_client.js +298 -0
- package/dist/tool_api_error.d.ts +62 -0
- package/dist/tool_api_error.d.ts.map +1 -0
- package/dist/tool_api_error.js +399 -0
- package/dist/tool_memory_ownership.d.ts +10 -0
- package/dist/tool_memory_ownership.d.ts.map +1 -0
- package/dist/tool_memory_ownership.js +36 -0
- package/dist/toolpod_handlers.d.ts +126 -0
- package/dist/toolpod_handlers.d.ts.map +1 -0
- package/dist/toolpod_handlers.js +1016 -0
- package/dist/tracing/exporters.d.ts +51 -0
- package/dist/tracing/exporters.d.ts.map +1 -0
- package/dist/tracing/exporters.js +327 -0
- package/dist/tracing/index.d.ts +3 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +2 -0
- package/dist/tracing/setup.d.ts +76 -0
- package/dist/tracing/setup.d.ts.map +1 -0
- package/dist/tracing/setup.js +436 -0
- package/dist/utils.d.ts +204 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +867 -0
- package/dist/workflow/activity.d.ts +71 -0
- package/dist/workflow/activity.d.ts.map +1 -0
- package/dist/workflow/activity.js +357 -0
- package/dist/workflow/attempt.d.ts +12 -0
- package/dist/workflow/attempt.d.ts.map +1 -0
- package/dist/workflow/attempt.js +96 -0
- package/dist/workflow/client.d.ts +46 -0
- package/dist/workflow/client.d.ts.map +1 -0
- package/dist/workflow/client.js +299 -0
- package/dist/workflow/context.d.ts +37 -0
- package/dist/workflow/context.d.ts.map +1 -0
- package/dist/workflow/context.js +350 -0
- package/dist/workflow/heartbeat.d.ts +15 -0
- package/dist/workflow/heartbeat.d.ts.map +1 -0
- package/dist/workflow/heartbeat.js +78 -0
- package/dist/workflow/index.d.ts +14 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +10 -0
- package/dist/workflow/memory.d.ts +17 -0
- package/dist/workflow/memory.d.ts.map +1 -0
- package/dist/workflow/memory.js +184 -0
- package/package.json +73 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer-token authentication for Runner SDK servers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Python's `agent_engine_runner_shared.server.auth`. 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
|
+
* Runner servers registered every route — `/execute`, `/invoke_llm`, the AER
|
|
9
|
+
* session-message reads — with no authentication at all, relying on network
|
|
10
|
+
* reachability. The only legitimate caller is the Orchestration Engine, so
|
|
11
|
+
* this is service-to-service auth using the same bearer-token shape as the
|
|
12
|
+
* operator's gRPC control plane.
|
|
13
|
+
*
|
|
14
|
+
* Enforcement is conditional on `RUNNER_AUTH_TOKEN` being configured. The
|
|
15
|
+
* runner image also runs under `agentengine dev up` on a developer's laptop,
|
|
16
|
+
* where no deploy-time secret exists and hard-failing would break local
|
|
17
|
+
* development outright; an unconfigured server logs a warning at startup so
|
|
18
|
+
* the weaker posture is visible rather than silent.
|
|
19
|
+
*
|
|
20
|
+
* Deployments that provision the token can additionally set
|
|
21
|
+
* `RUNNER_AUTH_REQUIRED=true` to make a missing token fatal at startup: the
|
|
22
|
+
* server throws {@link MissingRunnerAuthTokenError} instead of serving
|
|
23
|
+
* unauthenticated, so a provisioning gap crash-loops loudly rather than
|
|
24
|
+
* silently weakening auth. The signal is opt-in so shipping this code
|
|
25
|
+
* changes no running behavior until token provisioning is in place.
|
|
26
|
+
*/
|
|
27
|
+
import type { FastifyInstance } from "fastify";
|
|
28
|
+
export declare function isUnauthenticatedPath(pathname: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Constant-time comparison, so a caller cannot recover the configured token
|
|
31
|
+
* byte by byte from response timing.
|
|
32
|
+
*
|
|
33
|
+
* Both values are hashed first so the comparison is always over two 32-byte
|
|
34
|
+
* buffers: `timingSafeEqual` throws on length mismatch, and short-circuiting
|
|
35
|
+
* on length would otherwise leak the configured token's length.
|
|
36
|
+
*/
|
|
37
|
+
export declare function tokensEqual(presented: string, expected: string): boolean;
|
|
38
|
+
/** Extracts the bearer token from an Authorization header value. */
|
|
39
|
+
export declare function bearerToken(headerValue: string | undefined): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Thrown at startup when `RUNNER_AUTH_REQUIRED=true` is set but no auth
|
|
42
|
+
* token is configured.
|
|
43
|
+
*/
|
|
44
|
+
export declare class MissingRunnerAuthTokenError extends Error {
|
|
45
|
+
constructor(modeName: string);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Registers the authentication hook on `app`, if a token is configured.
|
|
49
|
+
*
|
|
50
|
+
* Returns whether enforcement is active, so the caller can surface it.
|
|
51
|
+
* Throws {@link MissingRunnerAuthTokenError} when `RUNNER_AUTH_REQUIRED` is
|
|
52
|
+
* set to `true` (or `1`) but no token is configured, so a deployment that
|
|
53
|
+
* mandates auth fails loudly instead of serving unauthenticated.
|
|
54
|
+
*/
|
|
55
|
+
export declare function registerAuthHook(app: FastifyInstance, modeName: string, env?: NodeJS.ProcessEnv): boolean;
|
|
56
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/server/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AA0B7E,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG/D;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIxE;AAED,oEAAoE;AACpE,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAY1E;AAED;;;GAGG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,QAAQ,EAAE,MAAM;CAU7B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,eAAe,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAoCT"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bearer-token authentication for Runner SDK servers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Python's `agent_engine_runner_shared.server.auth`. 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
|
+
* Runner servers registered every route — `/execute`, `/invoke_llm`, the AER
|
|
9
|
+
* session-message reads — with no authentication at all, relying on network
|
|
10
|
+
* reachability. The only legitimate caller is the Orchestration Engine, so
|
|
11
|
+
* this is service-to-service auth using the same bearer-token shape as the
|
|
12
|
+
* operator's gRPC control plane.
|
|
13
|
+
*
|
|
14
|
+
* Enforcement is conditional on `RUNNER_AUTH_TOKEN` being configured. The
|
|
15
|
+
* runner image also runs under `agentengine dev up` on a developer's laptop,
|
|
16
|
+
* where no deploy-time secret exists and hard-failing would break local
|
|
17
|
+
* development outright; an unconfigured server logs a warning at startup so
|
|
18
|
+
* the weaker posture is visible rather than silent.
|
|
19
|
+
*
|
|
20
|
+
* Deployments that provision the token can additionally set
|
|
21
|
+
* `RUNNER_AUTH_REQUIRED=true` to make a missing token fatal at startup: the
|
|
22
|
+
* server throws {@link MissingRunnerAuthTokenError} instead of serving
|
|
23
|
+
* unauthenticated, so a provisioning gap crash-loops loudly rather than
|
|
24
|
+
* silently weakening auth. The signal is opt-in so shipping this code
|
|
25
|
+
* changes no running behavior until token provisioning is in place.
|
|
26
|
+
*/
|
|
27
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
28
|
+
import { getLogger } from "../logger.js";
|
|
29
|
+
const logger = getLogger("agent_engine_runner_shared.server.auth");
|
|
30
|
+
/**
|
|
31
|
+
* Routes reachable without a token.
|
|
32
|
+
*
|
|
33
|
+
* Liveness and readiness probes originate from the kubelet and cannot
|
|
34
|
+
* present one, and these responses carry no tenant data. Everything else —
|
|
35
|
+
* including every credential-bearing and data-returning route — is gated.
|
|
36
|
+
*
|
|
37
|
+
* `/metrics` is deliberately NOT here: it returns `Metrics.getAll()`, whose
|
|
38
|
+
* counter names carry tenant-derived labels (`tool_name`, `model_name`), so
|
|
39
|
+
* an unauthenticated reader could enumerate which tools and models an agent
|
|
40
|
+
* uses. It is not a Prometheus scrape target — nothing in the platform reads
|
|
41
|
+
* it, so gating it costs nothing.
|
|
42
|
+
*/
|
|
43
|
+
const UNAUTHENTICATED_PATHS = new Set([
|
|
44
|
+
"/",
|
|
45
|
+
"/health",
|
|
46
|
+
"/healthz",
|
|
47
|
+
"/ready",
|
|
48
|
+
"/readyz",
|
|
49
|
+
]);
|
|
50
|
+
export function isUnauthenticatedPath(pathname) {
|
|
51
|
+
const path = (pathname || "").split("?")[0];
|
|
52
|
+
return UNAUTHENTICATED_PATHS.has(path ?? "");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Constant-time comparison, so a caller cannot recover the configured token
|
|
56
|
+
* byte by byte from response timing.
|
|
57
|
+
*
|
|
58
|
+
* Both values are hashed first so the comparison is always over two 32-byte
|
|
59
|
+
* buffers: `timingSafeEqual` throws on length mismatch, and short-circuiting
|
|
60
|
+
* on length would otherwise leak the configured token's length.
|
|
61
|
+
*/
|
|
62
|
+
export function tokensEqual(presented, expected) {
|
|
63
|
+
const a = createHash("sha256").update(presented, "utf8").digest();
|
|
64
|
+
const b = createHash("sha256").update(expected, "utf8").digest();
|
|
65
|
+
return timingSafeEqual(a, b);
|
|
66
|
+
}
|
|
67
|
+
/** Extracts the bearer token from an Authorization header value. */
|
|
68
|
+
export function bearerToken(headerValue) {
|
|
69
|
+
if (!headerValue) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const prefix = "bearer ";
|
|
73
|
+
if (headerValue.length <= prefix.length) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (headerValue.slice(0, prefix.length).toLowerCase() !== prefix) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return headerValue.slice(prefix.length);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Thrown at startup when `RUNNER_AUTH_REQUIRED=true` is set but no auth
|
|
83
|
+
* token is configured.
|
|
84
|
+
*/
|
|
85
|
+
export class MissingRunnerAuthTokenError extends Error {
|
|
86
|
+
constructor(modeName) {
|
|
87
|
+
super(`${modeName} server refusing to start without request authentication: ` +
|
|
88
|
+
`RUNNER_AUTH_REQUIRED is set but RUNNER_AUTH_TOKEN is not, so any ` +
|
|
89
|
+
`caller that could reach this port would be able to execute tools ` +
|
|
90
|
+
`and read session data. Provision RUNNER_AUTH_TOKEN, or unset ` +
|
|
91
|
+
`RUNNER_AUTH_REQUIRED to accept running unauthenticated.`);
|
|
92
|
+
this.name = "MissingRunnerAuthTokenError";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Registers the authentication hook on `app`, if a token is configured.
|
|
97
|
+
*
|
|
98
|
+
* Returns whether enforcement is active, so the caller can surface it.
|
|
99
|
+
* Throws {@link MissingRunnerAuthTokenError} when `RUNNER_AUTH_REQUIRED` is
|
|
100
|
+
* set to `true` (or `1`) but no token is configured, so a deployment that
|
|
101
|
+
* mandates auth fails loudly instead of serving unauthenticated.
|
|
102
|
+
*/
|
|
103
|
+
export function registerAuthHook(app, modeName, env = process.env) {
|
|
104
|
+
const expected = (env["RUNNER_AUTH_TOKEN"] ?? "").trim();
|
|
105
|
+
if (!expected) {
|
|
106
|
+
const required = (env["RUNNER_AUTH_REQUIRED"] ?? "").trim().toLowerCase();
|
|
107
|
+
if (required === "1" || required === "true") {
|
|
108
|
+
throw new MissingRunnerAuthTokenError(modeName);
|
|
109
|
+
}
|
|
110
|
+
logger.warn(`${modeName} server is running without request authentication: ` +
|
|
111
|
+
`RUNNER_AUTH_TOKEN is not set, so any caller that can reach this ` +
|
|
112
|
+
`port can execute tools and read session data.`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
116
|
+
// Fastify's request.url is the raw request-line URL; it never consults
|
|
117
|
+
// the Host header, so it cannot be corrupted the way starlette's
|
|
118
|
+
// Host-derived request.url.path can (see the Python twin). No fix
|
|
119
|
+
// is needed here — this comment exists so the difference isn't mistaken
|
|
120
|
+
// for an oversight.
|
|
121
|
+
if (isUnauthenticatedPath(request.url)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const presented = bearerToken(request.headers.authorization);
|
|
125
|
+
if (presented === null || !tokensEqual(presented, expected)) {
|
|
126
|
+
// Returning the reply is the documented way to halt the lifecycle;
|
|
127
|
+
// relying on the `reply.sent` short-circuit is version-dependent.
|
|
128
|
+
return reply.code(401).send({ detail: "unauthorized" });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base server class for Runner SDK components.
|
|
3
|
+
*
|
|
4
|
+
* Provides common Fastify setup, health/root/metrics endpoints, and
|
|
5
|
+
* startup/shutdown lifecycle hooks shared by all runtime modes.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Python's `BaseServer` (FastAPI/uvicorn → Fastify).
|
|
8
|
+
*/
|
|
9
|
+
import { type FastifyInstance } from "fastify";
|
|
10
|
+
import type { BaseAgent, BaseExecutionCallback } from "@mongodb-js/agent-engine-sdk";
|
|
11
|
+
import type { RuntimeAgentConfig } from "../agent_config.js";
|
|
12
|
+
import type { TurnMemoryWriter } from "../memory_writer.js";
|
|
13
|
+
import { DrainRegistry } from "./drain.js";
|
|
14
|
+
export declare const DEFAULT_PORTS: Record<string, number>;
|
|
15
|
+
/** Tool function signature — receives the full arguments Record, may return a Promise. */
|
|
16
|
+
export type ServerToolFn = (args: Record<string, unknown>) => unknown;
|
|
17
|
+
/** Minimal graph builder interface. */
|
|
18
|
+
export interface GraphBuilderLike {
|
|
19
|
+
getAgent?(opts?: {
|
|
20
|
+
callbacks?: BaseExecutionCallback[];
|
|
21
|
+
}): BaseAgent;
|
|
22
|
+
/**
|
|
23
|
+
* Resolves once any async setup kicked off during construction (e.g. MCP
|
|
24
|
+
* tool discovery) has finished registering tools. `TenantRuntime.runAsync()`
|
|
25
|
+
* awaits this before binding the server, so no request can land before
|
|
26
|
+
* setup completes. Optional — builders with no async setup can omit it.
|
|
27
|
+
*/
|
|
28
|
+
ready?(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Build the agent graph before first use when explicitly requested. The
|
|
31
|
+
* TypeScript AER does not call this synchronous hook during standby warming
|
|
32
|
+
* because doing so would block the Node event loop and server health.
|
|
33
|
+
*/
|
|
34
|
+
warmUp?(): void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Minimal runtime interface that server classes depend on.
|
|
38
|
+
*/
|
|
39
|
+
export interface ITenantRuntime {
|
|
40
|
+
readonly appName: string;
|
|
41
|
+
readonly appVersion?: string;
|
|
42
|
+
readonly orgId: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Optional so pre-existing hand-written implementations (custom runtimes,
|
|
45
|
+
* test doubles) keep typechecking across the SDK bump that introduced it.
|
|
46
|
+
* The capability-advertise path treats absent and null alike by skipping.
|
|
47
|
+
*/
|
|
48
|
+
readonly projectId?: string | null;
|
|
49
|
+
/** Native-checkpoint turn writer, present only when Memory is enabled. */
|
|
50
|
+
readonly memoryWriter?: TurnMemoryWriter | null;
|
|
51
|
+
/** Graph builder instance — null if not set at startup. */
|
|
52
|
+
readonly graphBuilder: GraphBuilderLike | null;
|
|
53
|
+
/** Raw tool functions registered via `app.tool()`, keyed by name. */
|
|
54
|
+
readonly tools: Record<string, ServerToolFn>;
|
|
55
|
+
/** Tool metadata (description, is_local, etc.), keyed by name. */
|
|
56
|
+
readonly toolDefinitions: Record<string, Record<string, unknown>>;
|
|
57
|
+
/** Parsed `agent.yaml` configuration. */
|
|
58
|
+
getAgentConfig(): RuntimeAgentConfig;
|
|
59
|
+
/** Resolved MongoDB URI: constructor option > `MONGODB_URI` env var > null. */
|
|
60
|
+
getMongodbUri(): string | null;
|
|
61
|
+
/** Get a `BaseAgent` instance, optionally wired with execution callbacks. */
|
|
62
|
+
getAgent(opts?: {
|
|
63
|
+
callbacks?: BaseExecutionCallback[];
|
|
64
|
+
}): BaseAgent;
|
|
65
|
+
/** Explicit pre-build via the graph builder's `warmUp()`, if it has one. */
|
|
66
|
+
warmUpAgent?(): void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Abstract base server for all Runner SDK runtime modes.
|
|
70
|
+
*
|
|
71
|
+
* Subclasses implement:
|
|
72
|
+
* - `modeName` — 'aer' | 'tool' | 'memory-server'
|
|
73
|
+
* - `registerRoutes(app)` — mode-specific Fastify routes
|
|
74
|
+
* - `onStartup` / `onShutdown` — optional lifecycle hooks
|
|
75
|
+
* - `getHealthDetails` — optional extra fields for /health
|
|
76
|
+
*/
|
|
77
|
+
export declare abstract class BaseServer {
|
|
78
|
+
protected readonly runtime: ITenantRuntime;
|
|
79
|
+
protected app: FastifyInstance | null;
|
|
80
|
+
private _drainRegistry?;
|
|
81
|
+
constructor(runtime: ITenantRuntime);
|
|
82
|
+
/**
|
|
83
|
+
* Execution-scoped drain state for POST /drain; work routes register
|
|
84
|
+
* against it so a cancelled execution's in-flight work can be stopped.
|
|
85
|
+
* Lazily created on the prototype so tests that skip the constructor
|
|
86
|
+
* (`Object.create`) still get a working registry.
|
|
87
|
+
*/
|
|
88
|
+
get drainRegistry(): DrainRegistry;
|
|
89
|
+
abstract get modeName(): string;
|
|
90
|
+
get defaultPort(): number;
|
|
91
|
+
abstract registerRoutes(app: FastifyInstance): void;
|
|
92
|
+
onStartup(): Promise<void>;
|
|
93
|
+
onShutdown(): Promise<void>;
|
|
94
|
+
getHealthDetails(): Record<string, unknown>;
|
|
95
|
+
createApp(): FastifyInstance;
|
|
96
|
+
/**
|
|
97
|
+
* Gracefully close the underlying Fastify server, draining in-flight
|
|
98
|
+
* requests and running `onClose` hooks. No-op if the app was never created.
|
|
99
|
+
*/
|
|
100
|
+
close(): Promise<void>;
|
|
101
|
+
protected registerCommonRoutes(app: FastifyInstance): void;
|
|
102
|
+
run(host?: string, port?: number): Promise<void>;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/server/base.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base server class for Runner SDK components.
|
|
3
|
+
*
|
|
4
|
+
* Provides common Fastify setup, health/root/metrics endpoints, and
|
|
5
|
+
* startup/shutdown lifecycle hooks shared by all runtime modes.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors Python's `BaseServer` (FastAPI/uvicorn → Fastify).
|
|
8
|
+
*/
|
|
9
|
+
import fastify from "fastify";
|
|
10
|
+
import cors from "@fastify/cors";
|
|
11
|
+
import { ZodError } from "zod";
|
|
12
|
+
import { getLogger } from "../logger.js";
|
|
13
|
+
import { Metrics } from "../metrics.js";
|
|
14
|
+
import { HealthStatus } from "../models.js";
|
|
15
|
+
import { tracingStatus } from "../tracing/index.js";
|
|
16
|
+
import { resolveCorsPolicy } from "./cors.js";
|
|
17
|
+
import { registerAuthHook } from "./auth.js";
|
|
18
|
+
import { DrainRegistry, registerDrainRoute } from "./drain.js";
|
|
19
|
+
import { registerCallInterruptRoute } from "./callInterrupt.js";
|
|
20
|
+
const logger = getLogger("agent_engine_runner_shared.server.base");
|
|
21
|
+
export const DEFAULT_PORTS = {
|
|
22
|
+
aer: 8001,
|
|
23
|
+
tool: 8002,
|
|
24
|
+
"memory-server": 8081,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Abstract base server for all Runner SDK runtime modes.
|
|
28
|
+
*
|
|
29
|
+
* Subclasses implement:
|
|
30
|
+
* - `modeName` — 'aer' | 'tool' | 'memory-server'
|
|
31
|
+
* - `registerRoutes(app)` — mode-specific Fastify routes
|
|
32
|
+
* - `onStartup` / `onShutdown` — optional lifecycle hooks
|
|
33
|
+
* - `getHealthDetails` — optional extra fields for /health
|
|
34
|
+
*/
|
|
35
|
+
export class BaseServer {
|
|
36
|
+
runtime;
|
|
37
|
+
app = null;
|
|
38
|
+
_drainRegistry;
|
|
39
|
+
constructor(runtime) {
|
|
40
|
+
this.runtime = runtime;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Execution-scoped drain state for POST /drain; work routes register
|
|
44
|
+
* against it so a cancelled execution's in-flight work can be stopped.
|
|
45
|
+
* Lazily created on the prototype so tests that skip the constructor
|
|
46
|
+
* (`Object.create`) still get a working registry.
|
|
47
|
+
*/
|
|
48
|
+
get drainRegistry() {
|
|
49
|
+
return (this._drainRegistry ??= new DrainRegistry());
|
|
50
|
+
}
|
|
51
|
+
get defaultPort() {
|
|
52
|
+
return DEFAULT_PORTS[this.modeName] ?? 8000;
|
|
53
|
+
}
|
|
54
|
+
async onStartup() { }
|
|
55
|
+
async onShutdown() { }
|
|
56
|
+
getHealthDetails() {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
createApp() {
|
|
60
|
+
const corsPolicy = resolveCorsPolicy(process.env["CORS_ALLOWED_ORIGINS"]);
|
|
61
|
+
const app = fastify({ logger: false });
|
|
62
|
+
void app.register(cors, {
|
|
63
|
+
origin: corsPolicy.origin,
|
|
64
|
+
credentials: corsPolicy.credentials,
|
|
65
|
+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"],
|
|
66
|
+
});
|
|
67
|
+
registerAuthHook(app, this.modeName);
|
|
68
|
+
app.addHook("onReady", async () => {
|
|
69
|
+
logger.info(`Starting ${this.modeName} server...`);
|
|
70
|
+
await this.onStartup();
|
|
71
|
+
logger.info(`${this.modeName} server started`);
|
|
72
|
+
});
|
|
73
|
+
app.addHook("onClose", async () => {
|
|
74
|
+
logger.info(`Shutting down ${this.modeName} server...`);
|
|
75
|
+
await this.onShutdown();
|
|
76
|
+
logger.info(`${this.modeName} server stopped`);
|
|
77
|
+
});
|
|
78
|
+
this.registerCommonRoutes(app);
|
|
79
|
+
this.registerRoutes(app);
|
|
80
|
+
// Map request-validation failures to client errors. FastAPI/Pydantic
|
|
81
|
+
// returns 422 for a malformed body; without this, a ZodError thrown by a
|
|
82
|
+
// route's `Schema.parse(request.body)` becomes a generic 500, which OE and
|
|
83
|
+
// other callers may retry as a transient infra failure rather than reject
|
|
84
|
+
// as a bad payload. Non-Zod errors keep their explicit `statusCode`
|
|
85
|
+
// (e.g. the 400/504 set by the AER execute path) or default to 500.
|
|
86
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
87
|
+
if (error instanceof ZodError) {
|
|
88
|
+
return reply
|
|
89
|
+
.status(422)
|
|
90
|
+
.send({ error: "Validation failed", detail: error.issues });
|
|
91
|
+
}
|
|
92
|
+
const err = error;
|
|
93
|
+
const statusCode = err.statusCode ?? 500;
|
|
94
|
+
const message = err.message ?? "Internal Server Error";
|
|
95
|
+
if (statusCode >= 500) {
|
|
96
|
+
logger.error({ err: error }, `Unhandled error: ${message}`);
|
|
97
|
+
}
|
|
98
|
+
return reply.status(statusCode).send({ error: message });
|
|
99
|
+
});
|
|
100
|
+
this.app = app;
|
|
101
|
+
return app;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Gracefully close the underlying Fastify server, draining in-flight
|
|
105
|
+
* requests and running `onClose` hooks. No-op if the app was never created.
|
|
106
|
+
*/
|
|
107
|
+
async close() {
|
|
108
|
+
if (this.app) {
|
|
109
|
+
await this.app.close();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
registerCommonRoutes(app) {
|
|
113
|
+
registerDrainRoute(app, this.drainRegistry);
|
|
114
|
+
registerCallInterruptRoute(app, this.drainRegistry);
|
|
115
|
+
app.get("/health", async () => {
|
|
116
|
+
// Probes check the HTTP status code (2xx = pass), not the body, so a
|
|
117
|
+
// `degraded` status surfaces the state for observability without
|
|
118
|
+
// triggering a pod restart — e.g. the trace store being unreachable at
|
|
119
|
+
// startup while the pod keeps serving traffic.
|
|
120
|
+
const tracing = tracingStatus();
|
|
121
|
+
const status = tracing.database_exporter === "degraded"
|
|
122
|
+
? HealthStatus.DEGRADED
|
|
123
|
+
: HealthStatus.HEALTHY;
|
|
124
|
+
return {
|
|
125
|
+
status,
|
|
126
|
+
component: this.runtime.appName,
|
|
127
|
+
mode: this.modeName,
|
|
128
|
+
version: "1.0.0",
|
|
129
|
+
details: { ...this.getHealthDetails(), tracing },
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
app.get("/", async () => ({
|
|
133
|
+
service: this.runtime.appName,
|
|
134
|
+
mode: this.modeName,
|
|
135
|
+
status: "running",
|
|
136
|
+
}));
|
|
137
|
+
app.get("/metrics", async () => ({
|
|
138
|
+
status: "ok",
|
|
139
|
+
component: this.modeName,
|
|
140
|
+
metrics: Metrics.getAll(),
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
async run(host = "0.0.0.0", port) {
|
|
145
|
+
const listenPort = port ?? this.defaultPort;
|
|
146
|
+
const app = this.createApp();
|
|
147
|
+
logger.info(`Starting ${this.modeName} server on ${host}:${listenPort}`);
|
|
148
|
+
await app.listen({ host, port: listenPort });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-call interrupt receiver for Runner SDK servers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Python's `agent_engine_runner_shared.server.call_interrupt`. Both runtimes are
|
|
5
|
+
* parallel implementations of the same server architecture, so this file and
|
|
6
|
+
* its Python twin must be changed together.
|
|
7
|
+
*
|
|
8
|
+
* When the OE's per-call interrupt fires, it POSTs `/interrupt/call` with
|
|
9
|
+
* the execution and the step to abandon. The runtime aborts exactly that
|
|
10
|
+
* call's controller and nothing else — unlike `POST /drain`, which is
|
|
11
|
+
* terminal for the execution, an interrupt is surgical: no admission latch,
|
|
12
|
+
* no record, and later steps of the same execution proceed.
|
|
13
|
+
*
|
|
14
|
+
* Contract (pinned by the shared fixture in
|
|
15
|
+
* `client-libraries/test-fixtures/interrupt-call/contract.json`):
|
|
16
|
+
*
|
|
17
|
+
* - `POST /interrupt/call {execution_id, step_number, workspace_id}` —
|
|
18
|
+
* `workspace_id` is mandatory on workspace-scoped runtimes (`APP_ID` set)
|
|
19
|
+
* and must match it exactly.
|
|
20
|
+
* - Always 200 with an outcome: `interrupted` (the call's controller was
|
|
21
|
+
* aborted), `not_found` (no such in-flight call), `already_settled` (the
|
|
22
|
+
* call ended first), `not_cancellable` (tracked work with no signal
|
|
23
|
+
* channel — a plain tool function — reported honestly, never claimed
|
|
24
|
+
* abandoned).
|
|
25
|
+
* - Idempotent by (execution_id, step_number): a repeat re-reads the
|
|
26
|
+
* outcome.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import type { FastifyInstance } from "fastify";
|
|
30
|
+
import type { DrainRegistry } from "./drain.js";
|
|
31
|
+
export declare const CallInterruptRequestSchema: z.ZodObject<{
|
|
32
|
+
execution_id: z.ZodString;
|
|
33
|
+
step_number: z.ZodNumber;
|
|
34
|
+
workspace_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
export type CallInterruptRequest = z.infer<typeof CallInterruptRequestSchema>;
|
|
37
|
+
/**
|
|
38
|
+
* Register `POST /interrupt/call` on a runner server.
|
|
39
|
+
*
|
|
40
|
+
* The bearer-auth middleware already gates the path; this adds the
|
|
41
|
+
* execution/workspace-scoped validation on top, identical to the drain
|
|
42
|
+
* route's: when the platform scopes this process to a workspace (`APP_ID`
|
|
43
|
+
* set — every managed runtime), the request must name that workspace
|
|
44
|
+
* exactly; a missing `workspace_id` is a 400, a mismatch a 403. When
|
|
45
|
+
* `APP_ID` is unset (local development) no workspace check is possible and
|
|
46
|
+
* none is enforced.
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerCallInterruptRoute(app: FastifyInstance, registry: DrainRegistry, env?: NodeJS.ProcessEnv): void;
|
|
49
|
+
//# sourceMappingURL=callInterrupt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callInterrupt.d.ts","sourceRoot":"","sources":["../../src/server/callInterrupt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAG/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAIhD,eAAO,MAAM,0BAA0B;;;;iBAOrC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,eAAe,EACpB,QAAQ,EAAE,aAAa,EACvB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,IAAI,CAqBN"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-call interrupt receiver for Runner SDK servers.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Python's `agent_engine_runner_shared.server.call_interrupt`. Both runtimes are
|
|
5
|
+
* parallel implementations of the same server architecture, so this file and
|
|
6
|
+
* its Python twin must be changed together.
|
|
7
|
+
*
|
|
8
|
+
* When the OE's per-call interrupt fires, it POSTs `/interrupt/call` with
|
|
9
|
+
* the execution and the step to abandon. The runtime aborts exactly that
|
|
10
|
+
* call's controller and nothing else — unlike `POST /drain`, which is
|
|
11
|
+
* terminal for the execution, an interrupt is surgical: no admission latch,
|
|
12
|
+
* no record, and later steps of the same execution proceed.
|
|
13
|
+
*
|
|
14
|
+
* Contract (pinned by the shared fixture in
|
|
15
|
+
* `client-libraries/test-fixtures/interrupt-call/contract.json`):
|
|
16
|
+
*
|
|
17
|
+
* - `POST /interrupt/call {execution_id, step_number, workspace_id}` —
|
|
18
|
+
* `workspace_id` is mandatory on workspace-scoped runtimes (`APP_ID` set)
|
|
19
|
+
* and must match it exactly.
|
|
20
|
+
* - Always 200 with an outcome: `interrupted` (the call's controller was
|
|
21
|
+
* aborted), `not_found` (no such in-flight call), `already_settled` (the
|
|
22
|
+
* call ended first), `not_cancellable` (tracked work with no signal
|
|
23
|
+
* channel — a plain tool function — reported honestly, never claimed
|
|
24
|
+
* abandoned).
|
|
25
|
+
* - Idempotent by (execution_id, step_number): a repeat re-reads the
|
|
26
|
+
* outcome.
|
|
27
|
+
*/
|
|
28
|
+
import { z } from "zod";
|
|
29
|
+
import { getLogger } from "../logger.js";
|
|
30
|
+
const logger = getLogger("agent_engine_runner_shared.server.callInterrupt");
|
|
31
|
+
export const CallInterruptRequestSchema = z.object({
|
|
32
|
+
execution_id: z.string().min(1).max(256),
|
|
33
|
+
step_number: z.number().int().nonnegative(),
|
|
34
|
+
// Null is an accepted spelling of omission, matching the Python twin and
|
|
35
|
+
// the drain contract (string|null). The route's truthiness checks treat it
|
|
36
|
+
// as a missing scope claim either way.
|
|
37
|
+
workspace_id: z.string().max(256).nullish(),
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Register `POST /interrupt/call` on a runner server.
|
|
41
|
+
*
|
|
42
|
+
* The bearer-auth middleware already gates the path; this adds the
|
|
43
|
+
* execution/workspace-scoped validation on top, identical to the drain
|
|
44
|
+
* route's: when the platform scopes this process to a workspace (`APP_ID`
|
|
45
|
+
* set — every managed runtime), the request must name that workspace
|
|
46
|
+
* exactly; a missing `workspace_id` is a 400, a mismatch a 403. When
|
|
47
|
+
* `APP_ID` is unset (local development) no workspace check is possible and
|
|
48
|
+
* none is enforced.
|
|
49
|
+
*/
|
|
50
|
+
export function registerCallInterruptRoute(app, registry, env = process.env) {
|
|
51
|
+
const workspaceId = (env["APP_ID"] ?? "").trim();
|
|
52
|
+
app.post("/interrupt/call", async (request, reply) => {
|
|
53
|
+
const body = CallInterruptRequestSchema.parse(request.body);
|
|
54
|
+
if (workspaceId) {
|
|
55
|
+
if (!body.workspace_id) {
|
|
56
|
+
return reply.code(400).send({ detail: "workspace_id required" });
|
|
57
|
+
}
|
|
58
|
+
if (body.workspace_id !== workspaceId) {
|
|
59
|
+
return reply.code(403).send({ detail: "workspace mismatch" });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const outcome = registry.abortCall(body.execution_id, body.step_number);
|
|
63
|
+
// The runtime-side evidence of a per-call abort, mirroring the drain
|
|
64
|
+
// receiver's accepted/finalized markers.
|
|
65
|
+
logger.info(`Call interrupt ${outcome} for execution ${body.execution_id} (step ${body.step_number})`);
|
|
66
|
+
return reply.send({ outcome });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Process-local executor callback delivery and redelivery. */
|
|
2
|
+
import type { ExecutorCallbackRequest } from "../models.js";
|
|
3
|
+
export declare class CallbackDelivery {
|
|
4
|
+
private pending;
|
|
5
|
+
private tasks;
|
|
6
|
+
private retryController;
|
|
7
|
+
send(oeUrl: string, callback: ExecutorCallbackRequest, ownerUrl?: string | null, onOwnerFailure?: (() => void) | null): Promise<void>;
|
|
8
|
+
shutdown(): Promise<void>;
|
|
9
|
+
waitForTerminal(executionId: string): Promise<boolean>;
|
|
10
|
+
private reserve;
|
|
11
|
+
private startRedelivery;
|
|
12
|
+
private redeliver;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=callback_delivery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callback_delivery.d.ts","sourceRoot":"","sources":["../../src/server/callback_delivery.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAE/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAmD5D,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,eAAe,CAAyB;IAE1C,IAAI,CACR,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,uBAAuB,EACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,EACxB,cAAc,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,GACnC,OAAO,CAAC,IAAI,CAAC;IA2CV,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAOzB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO5D,OAAO,CAAC,OAAO;IAiCf,OAAO,CAAC,eAAe;YAaT,SAAS;CAwCxB"}
|