@stackfactor/agent-utils 1.2.18 → 1.2.20

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.
@@ -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,09DA6DjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
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"}
@@ -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 {
@@ -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
  /**
@@ -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"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;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,MA4KP,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 check-module path within the app root, rejecting any
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 resolveCheckPath = (rel) => {
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 modulePath = resolveCheckPath(rel);
110
- if (!modulePath) {
111
- return [
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: ${error?.message ?? error}`,
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, () => main(abort.signal));
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) },
@@ -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,09DA6DjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
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"}
@@ -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 {
@@ -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
  /**
@@ -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"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA8LtC;;;;;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,MA4KP,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 check-module path within the app root, rejecting any
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 resolveCheckPath = (rel) => {
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 modulePath = resolveCheckPath(rel);
71
- if (!modulePath) {
72
- return [
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: ${error?.message ?? error}`,
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, () => main(abort.signal));
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) },
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.18",
6
+ "version": "1.2.20",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",
@@ -54,7 +54,7 @@
54
54
  "@langchain/google-genai": "^2.1.26",
55
55
  "@langchain/openai": "^1.3.0",
56
56
  "@opentelemetry/api": "^1.9.0",
57
- "@stackfactor/client-api": "^1.1.242",
57
+ "@stackfactor/client-api": "^1.2.1",
58
58
  "langchain": "^1.2.35",
59
59
  "mongoose": "^9.3.1",
60
60
  "openai": "^6.32.0",