@stackfactor/agent-utils 1.2.18 → 1.2.19
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/dist/cjs/agentProto.d.ts +1 -1
- package/dist/cjs/agentProto.d.ts.map +1 -1
- package/dist/cjs/agentProto.js +6 -0
- package/dist/cjs/client.d.ts +7 -0
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +104 -37
- package/dist/esm/agentProto.d.ts +1 -1
- package/dist/esm/agentProto.d.ts.map +1 -1
- package/dist/esm/agentProto.js +6 -0
- package/dist/esm/client.d.ts +7 -0
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +104 -37
- package/package.json +1 -1
package/dist/cjs/agentProto.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as grpc from "@grpc/grpc-js";
|
|
|
6
6
|
* zero or more `Progress` frames (driven by the agent's `onProgress`) followed
|
|
7
7
|
* by exactly one `Result` or `Error`.
|
|
8
8
|
*/
|
|
9
|
-
export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n // Deep check only: repo-relative path to the agent's self-check module to run\n // (e.g. \"src/check.js\"). Empty \u2192 fall back to the conventional \"src/check.js\".\n string check_code = 3;\n // Deep check only: JSON of the resolved agent config (constantsAndVars/\n // secrets) made available to the self-check as ctx.config. May be empty.\n string config_json = 4;\n}\n\n// One result row from the agent's self-check module. The agent owns only its\n// own checks; reachability/ingress/version-drift rows are synthesised backend-\n// side. severity is optional: \"warn\" marks a failing check as non-blocking;\n// empty or \"error\" is blocking.\nmessage CheckResult {\n string name = 1;\n bool ok = 2;\n string detail = 3;\n string severity = 4;\n}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\n repeated CheckResult checks = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
|
|
9
|
+
export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n // Optional repo-relative module to run INSTEAD of the agent's main (e.g.\n // \"src/webhooks/inbound-sms.js\"). Empty -> main, which is every pre-webhook\n // caller, so this stays wire-compatible with agents built before it existed.\n // Set by the webhook receiver from the integration's webHooks[].code. Follows\n // the same untrusted-path rules as HealthCheck's check_code.\n string code = 6;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n // Deep check only: repo-relative path to the agent's self-check module to run\n // (e.g. \"src/check.js\"). Empty \u2192 fall back to the conventional \"src/check.js\".\n string check_code = 3;\n // Deep check only: JSON of the resolved agent config (constantsAndVars/\n // secrets) made available to the self-check as ctx.config. May be empty.\n string config_json = 4;\n}\n\n// One result row from the agent's self-check module. The agent owns only its\n// own checks; reachability/ingress/version-drift rows are synthesised backend-\n// side. severity is optional: \"warn\" marks a failing check as non-blocking;\n// empty or \"error\" is blocking.\nmessage CheckResult {\n string name = 1;\n bool ok = 2;\n string detail = 3;\n string severity = 4;\n}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\n repeated CheckResult checks = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
|
|
10
10
|
/**
|
|
11
11
|
* Loads the Agent proto package. proto-loader reads from a file, so the embedded
|
|
12
12
|
* schema is written to a temp path — keeps the package self-contained across the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,
|
|
1
|
+
{"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,y3EAmEjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
|
package/dist/cjs/agentProto.js
CHANGED
|
@@ -63,6 +63,12 @@ message ExecuteRequest {
|
|
|
63
63
|
string config_json = 3;
|
|
64
64
|
string request_json = 4;
|
|
65
65
|
int32 event = 5;
|
|
66
|
+
// Optional repo-relative module to run INSTEAD of the agent's main (e.g.
|
|
67
|
+
// "src/webhooks/inbound-sms.js"). Empty -> main, which is every pre-webhook
|
|
68
|
+
// caller, so this stays wire-compatible with agents built before it existed.
|
|
69
|
+
// Set by the webhook receiver from the integration's webHooks[].code. Follows
|
|
70
|
+
// the same untrusted-path rules as HealthCheck's check_code.
|
|
71
|
+
string code = 6;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
message HealthCheckRequest {
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface AgentExecuteRequest {
|
|
|
10
10
|
config_json: string;
|
|
11
11
|
request_json: string;
|
|
12
12
|
event: number;
|
|
13
|
+
/**
|
|
14
|
+
* Optional repo-relative module to run instead of the agent's `main` — the
|
|
15
|
+
* webhook receiver sets it from the integration's `webHooks[].code`. Omit (or
|
|
16
|
+
* pass "") for every other caller; the agent then runs `main` exactly as
|
|
17
|
+
* before. Agents built against an older agent-utils ignore the field.
|
|
18
|
+
*/
|
|
19
|
+
code?: string;
|
|
13
20
|
}
|
|
14
21
|
export interface CallAgentOptions {
|
|
15
22
|
/**
|
package/dist/cjs/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8DD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CA0DV,CAAC;AAEL,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CA8ClB,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8DD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CA0DV,CAAC;AAEL,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CA8ClB,CAAC"}
|
package/dist/cjs/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA+PtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA8MP,CAAC"}
|
package/dist/cjs/serve.js
CHANGED
|
@@ -53,11 +53,14 @@ const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
|
53
53
|
// own fetch/SDK calls can cooperate, and we fail the check if it overruns.
|
|
54
54
|
const SELF_CHECK_TIMEOUT_MS = 20_000;
|
|
55
55
|
/**
|
|
56
|
-
* Resolves a repo-relative
|
|
56
|
+
* Resolves a repo-relative agent-module path within the app root, rejecting any
|
|
57
57
|
* path that escapes it or is absolute (the path may originate from the
|
|
58
58
|
* integration config, so treat it as untrusted). Returns null when unsafe.
|
|
59
|
+
*
|
|
60
|
+
* Guards both entry points that name a module by configuration: HealthCheck's
|
|
61
|
+
* `check_code` and Execute's `code` (a webhook's handler module).
|
|
59
62
|
*/
|
|
60
|
-
const
|
|
63
|
+
const resolveAgentModulePath = (rel) => {
|
|
61
64
|
const root = process.cwd();
|
|
62
65
|
const dest = (0, node_path_1.resolve)(root, rel);
|
|
63
66
|
const back = (0, node_path_1.relative)(root, dest);
|
|
@@ -66,6 +69,60 @@ const resolveCheckPath = (rel) => {
|
|
|
66
69
|
}
|
|
67
70
|
return dest;
|
|
68
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* Loads the function exported by a repo-relative agent module. Shared by the
|
|
74
|
+
* self-check (`check_code`) and the webhook entry point (`code`) so both get the
|
|
75
|
+
* same untrusted-path guard and the same CJS/ESM interop, and neither can drift
|
|
76
|
+
* into resolving a path the other would reject.
|
|
77
|
+
*
|
|
78
|
+
* Never throws: every failure comes back as a `reason` the caller maps to its
|
|
79
|
+
* own error shape (a CheckResult row, or a gRPC error frame).
|
|
80
|
+
*
|
|
81
|
+
* @param rel - Repo-relative module path, e.g. "src/webhooks/inbound-sms.js".
|
|
82
|
+
* @param namedExport - Preferred named export, tried after `default`/bare export.
|
|
83
|
+
*/
|
|
84
|
+
const loadAgentModule = async (rel, namedExport) => {
|
|
85
|
+
const modulePath = resolveAgentModulePath(rel);
|
|
86
|
+
if (!modulePath) {
|
|
87
|
+
return {
|
|
88
|
+
reason: "invalid-path",
|
|
89
|
+
detail: `invalid module path "${rel}"`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (!(0, node_fs_1.existsSync)(modulePath)) {
|
|
93
|
+
return {
|
|
94
|
+
reason: "not-found",
|
|
95
|
+
detail: `module "${rel}" was not found in the agent image`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
let exported;
|
|
99
|
+
try {
|
|
100
|
+
// Real dynamic import via an indirection the TS compiler won't rewrite. A
|
|
101
|
+
// literal `import()` is downlevelled to `require()` in the CJS build, and
|
|
102
|
+
// `require()` cannot resolve a file:// URL ("Cannot find module
|
|
103
|
+
// 'file:///app/src/check.js'"). `new Function` keeps a genuine `import()` in
|
|
104
|
+
// both builds; Node resolves the file URL and loads CJS or ESM modules
|
|
105
|
+
// alike (CJS exports surface as the namespace `default`).
|
|
106
|
+
const mod = await dynamicImport((0, node_url_1.pathToFileURL)(modulePath).href);
|
|
107
|
+
exported = mod?.default ?? mod;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
reason: "load-failed",
|
|
112
|
+
detail: String(error?.message ?? error),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const fn = typeof exported === "function"
|
|
116
|
+
? exported
|
|
117
|
+
: exported?.[namedExport] ?? exported?.default;
|
|
118
|
+
if (typeof fn !== "function") {
|
|
119
|
+
return {
|
|
120
|
+
reason: "not-a-function",
|
|
121
|
+
detail: `module "${rel}" does not export a function`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return { fn: fn };
|
|
125
|
+
};
|
|
69
126
|
/** Coerces whatever the agent's check returned into well-formed CheckResult[]. */
|
|
70
127
|
const normalizeChecks = (raw) => {
|
|
71
128
|
if (!Array.isArray(raw)) {
|
|
@@ -106,60 +163,41 @@ const runSelfCheck = async (checkCode, config, session) => {
|
|
|
106
163
|
},
|
|
107
164
|
];
|
|
108
165
|
}
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
const loaded = await loadAgentModule(rel, "check");
|
|
167
|
+
if (loaded.reason) {
|
|
168
|
+
// These rows are the ones this check reported before the loader was factored
|
|
169
|
+
// out, preserved exactly: a missing or unreachable module is a non-blocking
|
|
170
|
+
// `warn` (synthetic testing simply isn't available), while a module that IS
|
|
171
|
+
// there but broken is a blocking `error` — a real defect in a live agent.
|
|
172
|
+
const rows = {
|
|
173
|
+
"invalid-path": {
|
|
113
174
|
name: "synthetic_check",
|
|
114
175
|
ok: false,
|
|
115
176
|
detail: `invalid checkCode path "${rel}"`,
|
|
116
177
|
severity: "warn",
|
|
117
178
|
},
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (!(0, node_fs_1.existsSync)(modulePath)) {
|
|
121
|
-
return [
|
|
122
|
-
{
|
|
179
|
+
"not-found": {
|
|
123
180
|
name: "synthetic_check",
|
|
124
181
|
ok: false,
|
|
125
182
|
detail: `checkCode "${rel}" is configured but the module was not found in the agent image`,
|
|
126
183
|
severity: "warn",
|
|
127
184
|
},
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
let checkFn;
|
|
131
|
-
try {
|
|
132
|
-
// Real dynamic import via an indirection the TS compiler won't rewrite. A
|
|
133
|
-
// literal `import()` is downlevelled to `require()` in the CJS build, and
|
|
134
|
-
// `require()` cannot resolve a file:// URL ("Cannot find module
|
|
135
|
-
// 'file:///app/src/check.js'"). `new Function` keeps a genuine `import()` in
|
|
136
|
-
// both builds; Node resolves the file URL and loads CJS or ESM check
|
|
137
|
-
// modules alike (CJS exports surface as the namespace `default`).
|
|
138
|
-
const mod = await dynamicImport((0, node_url_1.pathToFileURL)(modulePath).href);
|
|
139
|
-
const exported = mod?.default ?? mod;
|
|
140
|
-
checkFn =
|
|
141
|
-
typeof exported === "function" ? exported : exported?.check ?? exported?.default;
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
return [
|
|
145
|
-
{
|
|
185
|
+
"load-failed": {
|
|
146
186
|
name: "self_check",
|
|
147
187
|
ok: false,
|
|
148
|
-
detail: `failed to load check module: ${
|
|
188
|
+
detail: `failed to load check module: ${loaded.detail}`,
|
|
149
189
|
severity: "error",
|
|
150
190
|
},
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (typeof checkFn !== "function") {
|
|
154
|
-
return [
|
|
155
|
-
{
|
|
191
|
+
"not-a-function": {
|
|
156
192
|
name: "self_check",
|
|
157
193
|
ok: false,
|
|
158
194
|
detail: "check module does not export a function",
|
|
159
195
|
severity: "error",
|
|
160
196
|
},
|
|
161
|
-
|
|
197
|
+
};
|
|
198
|
+
return [rows[loaded.reason]];
|
|
162
199
|
}
|
|
200
|
+
const checkFn = loaded.fn;
|
|
163
201
|
const abort = new AbortController();
|
|
164
202
|
const timer = setTimeout(() => abort.abort(), SELF_CHECK_TIMEOUT_MS);
|
|
165
203
|
// Bind the same per-request globals main() sees (config, request.session, …)
|
|
@@ -360,8 +398,37 @@ const serve = (main, options = {}) => {
|
|
|
360
398
|
signal: abort.signal,
|
|
361
399
|
session,
|
|
362
400
|
};
|
|
401
|
+
// Which function actually runs. Default is the agent's own `main` — the
|
|
402
|
+
// path every caller took before `code` existed, and still the path taken
|
|
403
|
+
// whenever `code` is empty. A webhook instead names its own handler module
|
|
404
|
+
// (integration webHooks[].code), so one agent can serve many endpoints
|
|
405
|
+
// without `main` growing a routing switch. Resolved per request rather
|
|
406
|
+
// than at boot because the value is per-invocation configuration.
|
|
407
|
+
let entry = main;
|
|
408
|
+
const entryPath = String(req.code ?? "").trim();
|
|
409
|
+
if (entryPath) {
|
|
410
|
+
const loaded = await loadAgentModule(entryPath, "handler");
|
|
411
|
+
if (loaded.reason) {
|
|
412
|
+
// A named module that will not load is a deployment/config fault, not
|
|
413
|
+
// a bad request: the caller cannot fix it and retrying will not help.
|
|
414
|
+
// Report it as a result-bearing error frame (not a gRPC status) so the
|
|
415
|
+
// receiver gets the reason verbatim and can log which module failed.
|
|
416
|
+
logger_js_1.default.log(null, logger_js_1.default.levels.error, `agent Execute could not load entry module "${entryPath}" (${loaded.reason}): ${loaded.detail}`);
|
|
417
|
+
if (!abort.signal.aborted) {
|
|
418
|
+
call.write({
|
|
419
|
+
error: {
|
|
420
|
+
code: 500,
|
|
421
|
+
message: `entry module "${entryPath}" could not be loaded: ${loaded.detail}`,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
call.end();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
entry = loaded.fn;
|
|
429
|
+
}
|
|
363
430
|
try {
|
|
364
|
-
const result = await (0, runtimeContext_js_1.runWithContext)(context, () =>
|
|
431
|
+
const result = await (0, runtimeContext_js_1.runWithContext)(context, () => entry(abort.signal));
|
|
365
432
|
if (!abort.signal.aborted) {
|
|
366
433
|
call.write({
|
|
367
434
|
result: { result_json: JSON.stringify(result ?? null) },
|
package/dist/esm/agentProto.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as grpc from "@grpc/grpc-js";
|
|
|
6
6
|
* zero or more `Progress` frames (driven by the agent's `onProgress`) followed
|
|
7
7
|
* by exactly one `Result` or `Error`.
|
|
8
8
|
*/
|
|
9
|
-
export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n // Deep check only: repo-relative path to the agent's self-check module to run\n // (e.g. \"src/check.js\"). Empty \u2192 fall back to the conventional \"src/check.js\".\n string check_code = 3;\n // Deep check only: JSON of the resolved agent config (constantsAndVars/\n // secrets) made available to the self-check as ctx.config. May be empty.\n string config_json = 4;\n}\n\n// One result row from the agent's self-check module. The agent owns only its\n// own checks; reachability/ingress/version-drift rows are synthesised backend-\n// side. severity is optional: \"warn\" marks a failing check as non-blocking;\n// empty or \"error\" is blocking.\nmessage CheckResult {\n string name = 1;\n bool ok = 2;\n string detail = 3;\n string severity = 4;\n}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\n repeated CheckResult checks = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
|
|
9
|
+
export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n // Optional repo-relative module to run INSTEAD of the agent's main (e.g.\n // \"src/webhooks/inbound-sms.js\"). Empty -> main, which is every pre-webhook\n // caller, so this stays wire-compatible with agents built before it existed.\n // Set by the webhook receiver from the integration's webHooks[].code. Follows\n // the same untrusted-path rules as HealthCheck's check_code.\n string code = 6;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n // Deep check only: repo-relative path to the agent's self-check module to run\n // (e.g. \"src/check.js\"). Empty \u2192 fall back to the conventional \"src/check.js\".\n string check_code = 3;\n // Deep check only: JSON of the resolved agent config (constantsAndVars/\n // secrets) made available to the self-check as ctx.config. May be empty.\n string config_json = 4;\n}\n\n// One result row from the agent's self-check module. The agent owns only its\n// own checks; reachability/ingress/version-drift rows are synthesised backend-\n// side. severity is optional: \"warn\" marks a failing check as non-blocking;\n// empty or \"error\" is blocking.\nmessage CheckResult {\n string name = 1;\n bool ok = 2;\n string detail = 3;\n string severity = 4;\n}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\n repeated CheckResult checks = 5;\n}\n\nmessage Update {\n oneof payload {\n Progress progress = 1;\n Result result = 2;\n ErrorInfo error = 3;\n }\n}\n\nmessage Progress { int32 progress = 1; string message = 2; }\nmessage Result { string result_json = 1; }\nmessage ErrorInfo { int32 code = 1; string message = 2; }\n";
|
|
10
10
|
/**
|
|
11
11
|
* Loads the Agent proto package. proto-loader reads from a file, so the embedded
|
|
12
12
|
* schema is written to a temp path — keeps the package self-contained across the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,
|
|
1
|
+
{"version":3,"file":"agentProto.d.ts","sourceRoot":"","sources":["../../src/agentProto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,y3EAmEjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
|
package/dist/esm/agentProto.js
CHANGED
|
@@ -27,6 +27,12 @@ message ExecuteRequest {
|
|
|
27
27
|
string config_json = 3;
|
|
28
28
|
string request_json = 4;
|
|
29
29
|
int32 event = 5;
|
|
30
|
+
// Optional repo-relative module to run INSTEAD of the agent's main (e.g.
|
|
31
|
+
// "src/webhooks/inbound-sms.js"). Empty -> main, which is every pre-webhook
|
|
32
|
+
// caller, so this stays wire-compatible with agents built before it existed.
|
|
33
|
+
// Set by the webhook receiver from the integration's webHooks[].code. Follows
|
|
34
|
+
// the same untrusted-path rules as HealthCheck's check_code.
|
|
35
|
+
string code = 6;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
message HealthCheckRequest {
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export interface AgentExecuteRequest {
|
|
|
10
10
|
config_json: string;
|
|
11
11
|
request_json: string;
|
|
12
12
|
event: number;
|
|
13
|
+
/**
|
|
14
|
+
* Optional repo-relative module to run instead of the agent's `main` — the
|
|
15
|
+
* webhook receiver sets it from the integration's `webHooks[].code`. Omit (or
|
|
16
|
+
* pass "") for every other caller; the agent then runs `main` exactly as
|
|
17
|
+
* before. Agents built against an older agent-utils ignore the field.
|
|
18
|
+
*/
|
|
19
|
+
code?: string;
|
|
13
20
|
}
|
|
14
21
|
export interface CallAgentOptions {
|
|
15
22
|
/**
|
package/dist/esm/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8DD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CA0DV,CAAC;AAEL,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CA8ClB,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA8DD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CA0DV,CAAC;AAEL,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAC;IACZ,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CA8ClB,CAAC"}
|
package/dist/esm/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA+PtC;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAE/D;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,OAAO,EAAE,GAAG,KACT,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAEzC,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,YAAY,CAAC,EAAE,aAAa,CAAC;CAC9B;AAyHD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA8MP,CAAC"}
|
package/dist/esm/serve.js
CHANGED
|
@@ -14,11 +14,14 @@ const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
|
14
14
|
// own fetch/SDK calls can cooperate, and we fail the check if it overruns.
|
|
15
15
|
const SELF_CHECK_TIMEOUT_MS = 20_000;
|
|
16
16
|
/**
|
|
17
|
-
* Resolves a repo-relative
|
|
17
|
+
* Resolves a repo-relative agent-module path within the app root, rejecting any
|
|
18
18
|
* path that escapes it or is absolute (the path may originate from the
|
|
19
19
|
* integration config, so treat it as untrusted). Returns null when unsafe.
|
|
20
|
+
*
|
|
21
|
+
* Guards both entry points that name a module by configuration: HealthCheck's
|
|
22
|
+
* `check_code` and Execute's `code` (a webhook's handler module).
|
|
20
23
|
*/
|
|
21
|
-
const
|
|
24
|
+
const resolveAgentModulePath = (rel) => {
|
|
22
25
|
const root = process.cwd();
|
|
23
26
|
const dest = resolve(root, rel);
|
|
24
27
|
const back = relative(root, dest);
|
|
@@ -27,6 +30,60 @@ const resolveCheckPath = (rel) => {
|
|
|
27
30
|
}
|
|
28
31
|
return dest;
|
|
29
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Loads the function exported by a repo-relative agent module. Shared by the
|
|
35
|
+
* self-check (`check_code`) and the webhook entry point (`code`) so both get the
|
|
36
|
+
* same untrusted-path guard and the same CJS/ESM interop, and neither can drift
|
|
37
|
+
* into resolving a path the other would reject.
|
|
38
|
+
*
|
|
39
|
+
* Never throws: every failure comes back as a `reason` the caller maps to its
|
|
40
|
+
* own error shape (a CheckResult row, or a gRPC error frame).
|
|
41
|
+
*
|
|
42
|
+
* @param rel - Repo-relative module path, e.g. "src/webhooks/inbound-sms.js".
|
|
43
|
+
* @param namedExport - Preferred named export, tried after `default`/bare export.
|
|
44
|
+
*/
|
|
45
|
+
const loadAgentModule = async (rel, namedExport) => {
|
|
46
|
+
const modulePath = resolveAgentModulePath(rel);
|
|
47
|
+
if (!modulePath) {
|
|
48
|
+
return {
|
|
49
|
+
reason: "invalid-path",
|
|
50
|
+
detail: `invalid module path "${rel}"`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!existsSync(modulePath)) {
|
|
54
|
+
return {
|
|
55
|
+
reason: "not-found",
|
|
56
|
+
detail: `module "${rel}" was not found in the agent image`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
let exported;
|
|
60
|
+
try {
|
|
61
|
+
// Real dynamic import via an indirection the TS compiler won't rewrite. A
|
|
62
|
+
// literal `import()` is downlevelled to `require()` in the CJS build, and
|
|
63
|
+
// `require()` cannot resolve a file:// URL ("Cannot find module
|
|
64
|
+
// 'file:///app/src/check.js'"). `new Function` keeps a genuine `import()` in
|
|
65
|
+
// both builds; Node resolves the file URL and loads CJS or ESM modules
|
|
66
|
+
// alike (CJS exports surface as the namespace `default`).
|
|
67
|
+
const mod = await dynamicImport(pathToFileURL(modulePath).href);
|
|
68
|
+
exported = mod?.default ?? mod;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
reason: "load-failed",
|
|
73
|
+
detail: String(error?.message ?? error),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const fn = typeof exported === "function"
|
|
77
|
+
? exported
|
|
78
|
+
: exported?.[namedExport] ?? exported?.default;
|
|
79
|
+
if (typeof fn !== "function") {
|
|
80
|
+
return {
|
|
81
|
+
reason: "not-a-function",
|
|
82
|
+
detail: `module "${rel}" does not export a function`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return { fn: fn };
|
|
86
|
+
};
|
|
30
87
|
/** Coerces whatever the agent's check returned into well-formed CheckResult[]. */
|
|
31
88
|
const normalizeChecks = (raw) => {
|
|
32
89
|
if (!Array.isArray(raw)) {
|
|
@@ -67,60 +124,41 @@ const runSelfCheck = async (checkCode, config, session) => {
|
|
|
67
124
|
},
|
|
68
125
|
];
|
|
69
126
|
}
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
127
|
+
const loaded = await loadAgentModule(rel, "check");
|
|
128
|
+
if (loaded.reason) {
|
|
129
|
+
// These rows are the ones this check reported before the loader was factored
|
|
130
|
+
// out, preserved exactly: a missing or unreachable module is a non-blocking
|
|
131
|
+
// `warn` (synthetic testing simply isn't available), while a module that IS
|
|
132
|
+
// there but broken is a blocking `error` — a real defect in a live agent.
|
|
133
|
+
const rows = {
|
|
134
|
+
"invalid-path": {
|
|
74
135
|
name: "synthetic_check",
|
|
75
136
|
ok: false,
|
|
76
137
|
detail: `invalid checkCode path "${rel}"`,
|
|
77
138
|
severity: "warn",
|
|
78
139
|
},
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
if (!existsSync(modulePath)) {
|
|
82
|
-
return [
|
|
83
|
-
{
|
|
140
|
+
"not-found": {
|
|
84
141
|
name: "synthetic_check",
|
|
85
142
|
ok: false,
|
|
86
143
|
detail: `checkCode "${rel}" is configured but the module was not found in the agent image`,
|
|
87
144
|
severity: "warn",
|
|
88
145
|
},
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
let checkFn;
|
|
92
|
-
try {
|
|
93
|
-
// Real dynamic import via an indirection the TS compiler won't rewrite. A
|
|
94
|
-
// literal `import()` is downlevelled to `require()` in the CJS build, and
|
|
95
|
-
// `require()` cannot resolve a file:// URL ("Cannot find module
|
|
96
|
-
// 'file:///app/src/check.js'"). `new Function` keeps a genuine `import()` in
|
|
97
|
-
// both builds; Node resolves the file URL and loads CJS or ESM check
|
|
98
|
-
// modules alike (CJS exports surface as the namespace `default`).
|
|
99
|
-
const mod = await dynamicImport(pathToFileURL(modulePath).href);
|
|
100
|
-
const exported = mod?.default ?? mod;
|
|
101
|
-
checkFn =
|
|
102
|
-
typeof exported === "function" ? exported : exported?.check ?? exported?.default;
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
return [
|
|
106
|
-
{
|
|
146
|
+
"load-failed": {
|
|
107
147
|
name: "self_check",
|
|
108
148
|
ok: false,
|
|
109
|
-
detail: `failed to load check module: ${
|
|
149
|
+
detail: `failed to load check module: ${loaded.detail}`,
|
|
110
150
|
severity: "error",
|
|
111
151
|
},
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (typeof checkFn !== "function") {
|
|
115
|
-
return [
|
|
116
|
-
{
|
|
152
|
+
"not-a-function": {
|
|
117
153
|
name: "self_check",
|
|
118
154
|
ok: false,
|
|
119
155
|
detail: "check module does not export a function",
|
|
120
156
|
severity: "error",
|
|
121
157
|
},
|
|
122
|
-
|
|
158
|
+
};
|
|
159
|
+
return [rows[loaded.reason]];
|
|
123
160
|
}
|
|
161
|
+
const checkFn = loaded.fn;
|
|
124
162
|
const abort = new AbortController();
|
|
125
163
|
const timer = setTimeout(() => abort.abort(), SELF_CHECK_TIMEOUT_MS);
|
|
126
164
|
// Bind the same per-request globals main() sees (config, request.session, …)
|
|
@@ -321,8 +359,37 @@ export const serve = (main, options = {}) => {
|
|
|
321
359
|
signal: abort.signal,
|
|
322
360
|
session,
|
|
323
361
|
};
|
|
362
|
+
// Which function actually runs. Default is the agent's own `main` — the
|
|
363
|
+
// path every caller took before `code` existed, and still the path taken
|
|
364
|
+
// whenever `code` is empty. A webhook instead names its own handler module
|
|
365
|
+
// (integration webHooks[].code), so one agent can serve many endpoints
|
|
366
|
+
// without `main` growing a routing switch. Resolved per request rather
|
|
367
|
+
// than at boot because the value is per-invocation configuration.
|
|
368
|
+
let entry = main;
|
|
369
|
+
const entryPath = String(req.code ?? "").trim();
|
|
370
|
+
if (entryPath) {
|
|
371
|
+
const loaded = await loadAgentModule(entryPath, "handler");
|
|
372
|
+
if (loaded.reason) {
|
|
373
|
+
// A named module that will not load is a deployment/config fault, not
|
|
374
|
+
// a bad request: the caller cannot fix it and retrying will not help.
|
|
375
|
+
// Report it as a result-bearing error frame (not a gRPC status) so the
|
|
376
|
+
// receiver gets the reason verbatim and can log which module failed.
|
|
377
|
+
logger.log(null, logger.levels.error, `agent Execute could not load entry module "${entryPath}" (${loaded.reason}): ${loaded.detail}`);
|
|
378
|
+
if (!abort.signal.aborted) {
|
|
379
|
+
call.write({
|
|
380
|
+
error: {
|
|
381
|
+
code: 500,
|
|
382
|
+
message: `entry module "${entryPath}" could not be loaded: ${loaded.detail}`,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
call.end();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
entry = loaded.fn;
|
|
390
|
+
}
|
|
324
391
|
try {
|
|
325
|
-
const result = await runWithContext(context, () =>
|
|
392
|
+
const result = await runWithContext(context, () => entry(abort.signal));
|
|
326
393
|
if (!abort.signal.aborted) {
|
|
327
394
|
call.write({
|
|
328
395
|
result: { result_json: JSON.stringify(result ?? null) },
|