@stackfactor/agent-utils 1.1.8 → 1.2.1

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}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\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}\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,+pCA2CjB,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,09DA6DjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
@@ -69,13 +69,31 @@ message HealthCheckRequest {
69
69
  // Optional JSON carrying { authToken } for the deep backend-callback check.
70
70
  string request_json = 1;
71
71
  bool deep = 2;
72
+ // Deep check only: repo-relative path to the agent's self-check module to run
73
+ // (e.g. "src/check.js"). Empty → fall back to the conventional "src/check.js".
74
+ string check_code = 3;
75
+ // Deep check only: JSON of the resolved agent config (constantsAndVars/
76
+ // secrets) made available to the self-check as ctx.config. May be empty.
77
+ string config_json = 4;
78
+ }
79
+
80
+ // One result row from the agent's self-check module. The agent owns only its
81
+ // own checks; reachability/ingress/version-drift rows are synthesised backend-
82
+ // side. severity is optional: "warn" marks a failing check as non-blocking;
83
+ // empty or "error" is blocking.
84
+ message CheckResult {
85
+ string name = 1;
86
+ bool ok = 2;
87
+ string detail = 3;
88
+ string severity = 4;
72
89
  }
73
90
 
74
91
  message HealthCheckResponse {
75
- bool ok = 1;
76
- bool backend_reachable = 2;
77
- string version = 3;
78
- string message = 4;
92
+ bool ok = 1;
93
+ bool backend_reachable = 2;
94
+ string version = 3;
95
+ string message = 4;
96
+ repeated CheckResult checks = 5;
79
97
  }
80
98
 
81
99
  message Update {
@@ -48,11 +48,29 @@ export interface CheckAgentOptions {
48
48
  deep?: boolean;
49
49
  /** StackFactor session token used for the deep backend-callback check. */
50
50
  authToken?: string;
51
+ /**
52
+ * Deep check only: repo-relative path to the agent's self-check module
53
+ * (e.g. "src/check.js"). Empty → the agent falls back to "src/check.js".
54
+ */
55
+ checkCode?: string;
56
+ /**
57
+ * Deep check only: resolved agent config (constantsAndVars/secrets) exposed to
58
+ * the self-check as `ctx.config`. Serialised to JSON on the wire.
59
+ */
60
+ config?: any;
51
61
  /** Call deadline; defaults to 15s (a health probe should be quick). */
52
62
  deadlineMs?: number;
53
63
  }
64
+ /** One self-check row reported by the agent's check module. */
65
+ export interface AgentCheck {
66
+ name: string;
67
+ ok: boolean;
68
+ detail: string;
69
+ /** "warn" → non-blocking; empty/"error" → blocking. */
70
+ severity: string;
71
+ }
54
72
  export interface AgentHealth {
55
- /** The agent answered (and, for a deep check, auth succeeded). */
73
+ /** The agent answered (and, for a deep check, auth + checks succeeded). */
56
74
  ok: boolean;
57
75
  /** Deep check only: the agent reached the StackFactor backend (BACKEND_URL). */
58
76
  backendReachable: boolean;
@@ -60,6 +78,8 @@ export interface AgentHealth {
60
78
  version: string;
61
79
  /** Human-readable detail — carries the failure reason when `ok` is false. */
62
80
  message: string;
81
+ /** Deep check only: the agent's own self-check rows (empty if none shipped). */
82
+ checks: AgentCheck[];
63
83
  }
64
84
  /**
65
85
  * Probes a deployed agent's `Agent.HealthCheck` (unary). Resolves with the
@@ -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;AAqCD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CAyDV,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,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,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;CACjB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CAkClB,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;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;AAqCD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CAyDV,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,CA6ClB,CAAC"}
@@ -146,6 +146,8 @@ const checkAgent = (agentUrl, options = {}) => new Promise((resolve, reject) =>
146
146
  const request = {
147
147
  request_json: JSON.stringify({ authToken: options.authToken }),
148
148
  deep: !!options.deep,
149
+ check_code: options.checkCode ?? "",
150
+ config_json: options.config === undefined ? "" : JSON.stringify(options.config),
149
151
  };
150
152
  client.HealthCheck(request, { deadline: Date.now() + (options.deadlineMs ?? DEFAULT_HEALTH_DEADLINE_MS) }, (error, response) => {
151
153
  try {
@@ -163,6 +165,14 @@ const checkAgent = (agentUrl, options = {}) => new Promise((resolve, reject) =>
163
165
  backendReachable: !!response?.backend_reachable,
164
166
  version: response?.version ?? "",
165
167
  message: response?.message ?? "",
168
+ checks: Array.isArray(response?.checks)
169
+ ? response.checks.map((check) => ({
170
+ name: check?.name ?? "",
171
+ ok: !!check?.ok,
172
+ detail: check?.detail ?? "",
173
+ severity: check?.severity ?? "error",
174
+ }))
175
+ : [],
166
176
  });
167
177
  });
168
178
  });
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA6IP,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA6JtC;;;;;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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAuKP,CAAC"}
package/dist/cjs/serve.js CHANGED
@@ -38,10 +38,136 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.serve = void 0;
40
40
  const grpc = __importStar(require("@grpc/grpc-js"));
41
+ const node_fs_1 = require("node:fs");
42
+ const node_path_1 = require("node:path");
43
+ const node_url_1 = require("node:url");
41
44
  const client_api_1 = require("@stackfactor/client-api");
42
45
  const logger_js_1 = __importDefault(require("./logger.js"));
43
46
  const agentProto_js_1 = require("./agentProto.js");
44
47
  const runtimeContext_js_1 = require("./runtimeContext.js");
48
+ // The conventional self-check module run on a deep HealthCheck when the caller
49
+ // doesn't specify `check_code`. Resolved relative to the agent's app root.
50
+ const DEFAULT_CHECK_PATH = "src/check.js";
51
+ // Genuine dynamic `import()` that survives TypeScript's CJS downlevelling (which
52
+ // would otherwise turn `import()` into `require()` and break file:// URLs and
53
+ // ESM check modules). Built once; see the call site in runSelfCheck.
54
+ const dynamicImport = new Function("specifier", "return import(specifier)");
55
+ // Hard cap on a self-check; the agent receives this as its AbortSignal so its
56
+ // own fetch/SDK calls can cooperate, and we fail the check if it overruns.
57
+ const SELF_CHECK_TIMEOUT_MS = 20_000;
58
+ /**
59
+ * Resolves a repo-relative check-module path within the app root, rejecting any
60
+ * path that escapes it or is absolute (the path may originate from the
61
+ * integration config, so treat it as untrusted). Returns null when unsafe.
62
+ */
63
+ const resolveCheckPath = (checkCode) => {
64
+ const root = process.cwd();
65
+ const rel = (checkCode || "").trim() || DEFAULT_CHECK_PATH;
66
+ const dest = (0, node_path_1.resolve)(root, rel);
67
+ const back = (0, node_path_1.relative)(root, dest);
68
+ if (back === "" || back.startsWith("..") || (0, node_path_1.isAbsolute)(back)) {
69
+ return null;
70
+ }
71
+ return dest;
72
+ };
73
+ /** Coerces whatever the agent's check returned into well-formed CheckResult[]. */
74
+ const normalizeChecks = (raw) => {
75
+ if (!Array.isArray(raw)) {
76
+ return [];
77
+ }
78
+ return raw
79
+ .filter((entry) => entry && typeof entry.name === "string")
80
+ .map((entry) => ({
81
+ name: entry.name,
82
+ ok: !!entry.ok,
83
+ detail: typeof entry.detail === "string" ? entry.detail : "",
84
+ severity: entry.severity === "warn" ? "warn" : "error",
85
+ }));
86
+ };
87
+ /**
88
+ * Loads and runs the agent's self-check module (`check_code` or the default
89
+ * `src/check.js`). The check follows the same programming model as `main`: it
90
+ * receives the request {@link AbortSignal} and reads `config` / `request`
91
+ * (incl. `request.session`) from the per-request globals (bound here via
92
+ * {@link runWithContext}); the logger is imported. The agent owns only its own
93
+ * checks — reachability/version rows are synthesised backend-side. Never throws:
94
+ * a missing module yields no rows; a load error, bad export, thrown error, or
95
+ * timeout collapses to a single failed `self_check` row.
96
+ */
97
+ const runSelfCheck = async (checkCode, config, session) => {
98
+ const modulePath = resolveCheckPath(checkCode);
99
+ if (!modulePath || !(0, node_fs_1.existsSync)(modulePath)) {
100
+ return []; // no self-check module shipped — baseline checks only
101
+ }
102
+ let checkFn;
103
+ try {
104
+ // Real dynamic import via an indirection the TS compiler won't rewrite. A
105
+ // literal `import()` is downlevelled to `require()` in the CJS build, and
106
+ // `require()` cannot resolve a file:// URL ("Cannot find module
107
+ // 'file:///app/src/check.js'"). `new Function` keeps a genuine `import()` in
108
+ // both builds; Node resolves the file URL and loads CJS or ESM check
109
+ // modules alike (CJS exports surface as the namespace `default`).
110
+ const mod = await dynamicImport((0, node_url_1.pathToFileURL)(modulePath).href);
111
+ const exported = mod?.default ?? mod;
112
+ checkFn =
113
+ typeof exported === "function" ? exported : exported?.check ?? exported?.default;
114
+ }
115
+ catch (error) {
116
+ return [
117
+ {
118
+ name: "self_check",
119
+ ok: false,
120
+ detail: `failed to load check module: ${error?.message ?? error}`,
121
+ severity: "error",
122
+ },
123
+ ];
124
+ }
125
+ if (typeof checkFn !== "function") {
126
+ return [
127
+ {
128
+ name: "self_check",
129
+ ok: false,
130
+ detail: "check module does not export a function",
131
+ severity: "error",
132
+ },
133
+ ];
134
+ }
135
+ const abort = new AbortController();
136
+ const timer = setTimeout(() => abort.abort(), SELF_CHECK_TIMEOUT_MS);
137
+ // Bind the same per-request globals main() sees (config, request.session, …)
138
+ // so the check reads them identically; it receives the AbortSignal as its arg.
139
+ const requestContext = {
140
+ config,
141
+ params: {},
142
+ request: { session },
143
+ event: 0,
144
+ onProgress: null,
145
+ signal: abort.signal,
146
+ session,
147
+ };
148
+ try {
149
+ const raw = await Promise.race([
150
+ (0, runtimeContext_js_1.runWithContext)(requestContext, () => Promise.resolve(checkFn(abort.signal))),
151
+ new Promise((_, reject) => {
152
+ abort.signal.addEventListener("abort", () => reject(new Error(`self-check timed out after ${SELF_CHECK_TIMEOUT_MS}ms`)));
153
+ }),
154
+ ]);
155
+ return normalizeChecks(raw);
156
+ }
157
+ catch (error) {
158
+ return [
159
+ {
160
+ name: "self_check",
161
+ ok: false,
162
+ detail: error?.message ?? String(error),
163
+ severity: "error",
164
+ },
165
+ ];
166
+ }
167
+ finally {
168
+ clearTimeout(timer);
169
+ }
170
+ };
45
171
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
46
172
  class UnauthenticatedError extends Error {
47
173
  }
@@ -175,19 +301,40 @@ const serve = (main, options = {}) => {
175
301
  // validates the StackFactor token against BACKEND_URL — surfacing a broken
176
302
  // backend callback (the `ECONNREFUSED 127.0.0.1:443` class) before it bites.
177
303
  const healthCheck = (call, callback) => {
178
- const respond = (ok, backendReachable, message) => callback(null, {
304
+ const respond = (ok, backendReachable, message, checks = []) => callback(null, {
179
305
  ok,
180
306
  backend_reachable: backendReachable,
181
307
  version: process.env.AGENT_VERSION ?? "",
182
308
  message,
309
+ checks,
183
310
  });
311
+ // Shallow: confirm the process is up + ingress auth (the caller reached us).
184
312
  if (!call.request?.deep) {
185
313
  respond(true, false, "ok");
186
314
  return;
187
315
  }
188
- void authenticate(safeParse(call.request?.request_json, {}))
189
- .then(() => respond(true, true, "ok"))
190
- .catch((error) => respond(false, false, error?.message ?? "auth failed"));
316
+ // Deep: run the same auth path Execute uses (validates the StackFactor token
317
+ // against BACKEND_URL) AND the agent's own self-check module, then roll the
318
+ // two up into a single verdict. Warn-severity check failures don't block ok.
319
+ const request = safeParse(call.request?.request_json, {});
320
+ const checkCode = String(call.request?.check_code ?? "");
321
+ const checkConfig = safeParse(call.request?.config_json, {});
322
+ void (async () => {
323
+ let backendReachable = false;
324
+ let session = null;
325
+ let message = "ok";
326
+ try {
327
+ session = await authenticate(request);
328
+ backendReachable = true;
329
+ }
330
+ catch (error) {
331
+ message = error?.message ?? "auth failed";
332
+ }
333
+ const checks = await runSelfCheck(checkCode, checkConfig, session);
334
+ const ok = backendReachable &&
335
+ checks.every((check) => check.ok || check.severity === "warn");
336
+ respond(ok, backendReachable, message, checks);
337
+ })();
191
338
  };
192
339
  const server = new grpc.Server();
193
340
  server.addService((0, agentProto_js_1.loadAgentService)(), {
@@ -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}\n\nmessage HealthCheckResponse {\n bool ok = 1;\n bool backend_reachable = 2;\n string version = 3;\n string message = 4;\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}\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,+pCA2CjB,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,09DA6DjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAO,GAWnC,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,gBAAgB,QAAO,IAAI,CAAC,iBACc,CAAC"}
@@ -33,13 +33,31 @@ message HealthCheckRequest {
33
33
  // Optional JSON carrying { authToken } for the deep backend-callback check.
34
34
  string request_json = 1;
35
35
  bool deep = 2;
36
+ // Deep check only: repo-relative path to the agent's self-check module to run
37
+ // (e.g. "src/check.js"). Empty → fall back to the conventional "src/check.js".
38
+ string check_code = 3;
39
+ // Deep check only: JSON of the resolved agent config (constantsAndVars/
40
+ // secrets) made available to the self-check as ctx.config. May be empty.
41
+ string config_json = 4;
42
+ }
43
+
44
+ // One result row from the agent's self-check module. The agent owns only its
45
+ // own checks; reachability/ingress/version-drift rows are synthesised backend-
46
+ // side. severity is optional: "warn" marks a failing check as non-blocking;
47
+ // empty or "error" is blocking.
48
+ message CheckResult {
49
+ string name = 1;
50
+ bool ok = 2;
51
+ string detail = 3;
52
+ string severity = 4;
36
53
  }
37
54
 
38
55
  message HealthCheckResponse {
39
- bool ok = 1;
40
- bool backend_reachable = 2;
41
- string version = 3;
42
- string message = 4;
56
+ bool ok = 1;
57
+ bool backend_reachable = 2;
58
+ string version = 3;
59
+ string message = 4;
60
+ repeated CheckResult checks = 5;
43
61
  }
44
62
 
45
63
  message Update {
@@ -48,11 +48,29 @@ export interface CheckAgentOptions {
48
48
  deep?: boolean;
49
49
  /** StackFactor session token used for the deep backend-callback check. */
50
50
  authToken?: string;
51
+ /**
52
+ * Deep check only: repo-relative path to the agent's self-check module
53
+ * (e.g. "src/check.js"). Empty → the agent falls back to "src/check.js".
54
+ */
55
+ checkCode?: string;
56
+ /**
57
+ * Deep check only: resolved agent config (constantsAndVars/secrets) exposed to
58
+ * the self-check as `ctx.config`. Serialised to JSON on the wire.
59
+ */
60
+ config?: any;
51
61
  /** Call deadline; defaults to 15s (a health probe should be quick). */
52
62
  deadlineMs?: number;
53
63
  }
64
+ /** One self-check row reported by the agent's check module. */
65
+ export interface AgentCheck {
66
+ name: string;
67
+ ok: boolean;
68
+ detail: string;
69
+ /** "warn" → non-blocking; empty/"error" → blocking. */
70
+ severity: string;
71
+ }
54
72
  export interface AgentHealth {
55
- /** The agent answered (and, for a deep check, auth succeeded). */
73
+ /** The agent answered (and, for a deep check, auth + checks succeeded). */
56
74
  ok: boolean;
57
75
  /** Deep check only: the agent reached the StackFactor backend (BACKEND_URL). */
58
76
  backendReachable: boolean;
@@ -60,6 +78,8 @@ export interface AgentHealth {
60
78
  version: string;
61
79
  /** Human-readable detail — carries the failure reason when `ok` is false. */
62
80
  message: string;
81
+ /** Deep check only: the agent's own self-check rows (empty if none shipped). */
82
+ checks: AgentCheck[];
63
83
  }
64
84
  /**
65
85
  * Probes a deployed agent's `Agent.HealthCheck` (unary). Resolves with the
@@ -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;AAqCD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CAyDV,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,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,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;CACjB;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GACrB,UAAU,MAAM,EAChB,UAAS,iBAAsB,KAC9B,OAAO,CAAC,WAAW,CAkClB,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;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;AAqCD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,MAAM,EAChB,SAAS,mBAAmB,EAC5B,UAAS,gBAAqB,KAC7B,OAAO,CAAC,GAAG,CAyDV,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,CA6ClB,CAAC"}
@@ -109,6 +109,8 @@ export const checkAgent = (agentUrl, options = {}) => new Promise((resolve, reje
109
109
  const request = {
110
110
  request_json: JSON.stringify({ authToken: options.authToken }),
111
111
  deep: !!options.deep,
112
+ check_code: options.checkCode ?? "",
113
+ config_json: options.config === undefined ? "" : JSON.stringify(options.config),
112
114
  };
113
115
  client.HealthCheck(request, { deadline: Date.now() + (options.deadlineMs ?? DEFAULT_HEALTH_DEADLINE_MS) }, (error, response) => {
114
116
  try {
@@ -126,6 +128,14 @@ export const checkAgent = (agentUrl, options = {}) => new Promise((resolve, reje
126
128
  backendReachable: !!response?.backend_reachable,
127
129
  version: response?.version ?? "",
128
130
  message: response?.message ?? "",
131
+ checks: Array.isArray(response?.checks)
132
+ ? response.checks.map((check) => ({
133
+ name: check?.name ?? "",
134
+ ok: !!check?.ok,
135
+ detail: check?.detail ?? "",
136
+ severity: check?.severity ?? "error",
137
+ }))
138
+ : [],
129
139
  });
130
140
  });
131
141
  });
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAMtC;;;;;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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MA6IP,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA6JtC;;;;;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;AAkDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,SAAS,EACf,UAAS,YAAiB,KACzB,IAAI,CAAC,MAuKP,CAAC"}
package/dist/esm/serve.js CHANGED
@@ -1,8 +1,134 @@
1
1
  import * as grpc from "@grpc/grpc-js";
2
+ import { existsSync } from "node:fs";
3
+ import { resolve, relative, isAbsolute } from "node:path";
4
+ import { pathToFileURL } from "node:url";
2
5
  import { session as clientSession } from "@stackfactor/client-api";
3
6
  import logger from "./logger.js";
4
7
  import { loadAgentService } from "./agentProto.js";
5
8
  import { runWithContext, installGlobals } from "./runtimeContext.js";
9
+ // The conventional self-check module run on a deep HealthCheck when the caller
10
+ // doesn't specify `check_code`. Resolved relative to the agent's app root.
11
+ const DEFAULT_CHECK_PATH = "src/check.js";
12
+ // Genuine dynamic `import()` that survives TypeScript's CJS downlevelling (which
13
+ // would otherwise turn `import()` into `require()` and break file:// URLs and
14
+ // ESM check modules). Built once; see the call site in runSelfCheck.
15
+ const dynamicImport = new Function("specifier", "return import(specifier)");
16
+ // Hard cap on a self-check; the agent receives this as its AbortSignal so its
17
+ // own fetch/SDK calls can cooperate, and we fail the check if it overruns.
18
+ const SELF_CHECK_TIMEOUT_MS = 20_000;
19
+ /**
20
+ * Resolves a repo-relative check-module path within the app root, rejecting any
21
+ * path that escapes it or is absolute (the path may originate from the
22
+ * integration config, so treat it as untrusted). Returns null when unsafe.
23
+ */
24
+ const resolveCheckPath = (checkCode) => {
25
+ const root = process.cwd();
26
+ const rel = (checkCode || "").trim() || DEFAULT_CHECK_PATH;
27
+ const dest = resolve(root, rel);
28
+ const back = relative(root, dest);
29
+ if (back === "" || back.startsWith("..") || isAbsolute(back)) {
30
+ return null;
31
+ }
32
+ return dest;
33
+ };
34
+ /** Coerces whatever the agent's check returned into well-formed CheckResult[]. */
35
+ const normalizeChecks = (raw) => {
36
+ if (!Array.isArray(raw)) {
37
+ return [];
38
+ }
39
+ return raw
40
+ .filter((entry) => entry && typeof entry.name === "string")
41
+ .map((entry) => ({
42
+ name: entry.name,
43
+ ok: !!entry.ok,
44
+ detail: typeof entry.detail === "string" ? entry.detail : "",
45
+ severity: entry.severity === "warn" ? "warn" : "error",
46
+ }));
47
+ };
48
+ /**
49
+ * Loads and runs the agent's self-check module (`check_code` or the default
50
+ * `src/check.js`). The check follows the same programming model as `main`: it
51
+ * receives the request {@link AbortSignal} and reads `config` / `request`
52
+ * (incl. `request.session`) from the per-request globals (bound here via
53
+ * {@link runWithContext}); the logger is imported. The agent owns only its own
54
+ * checks — reachability/version rows are synthesised backend-side. Never throws:
55
+ * a missing module yields no rows; a load error, bad export, thrown error, or
56
+ * timeout collapses to a single failed `self_check` row.
57
+ */
58
+ const runSelfCheck = async (checkCode, config, session) => {
59
+ const modulePath = resolveCheckPath(checkCode);
60
+ if (!modulePath || !existsSync(modulePath)) {
61
+ return []; // no self-check module shipped — baseline checks only
62
+ }
63
+ let checkFn;
64
+ try {
65
+ // Real dynamic import via an indirection the TS compiler won't rewrite. A
66
+ // literal `import()` is downlevelled to `require()` in the CJS build, and
67
+ // `require()` cannot resolve a file:// URL ("Cannot find module
68
+ // 'file:///app/src/check.js'"). `new Function` keeps a genuine `import()` in
69
+ // both builds; Node resolves the file URL and loads CJS or ESM check
70
+ // modules alike (CJS exports surface as the namespace `default`).
71
+ const mod = await dynamicImport(pathToFileURL(modulePath).href);
72
+ const exported = mod?.default ?? mod;
73
+ checkFn =
74
+ typeof exported === "function" ? exported : exported?.check ?? exported?.default;
75
+ }
76
+ catch (error) {
77
+ return [
78
+ {
79
+ name: "self_check",
80
+ ok: false,
81
+ detail: `failed to load check module: ${error?.message ?? error}`,
82
+ severity: "error",
83
+ },
84
+ ];
85
+ }
86
+ if (typeof checkFn !== "function") {
87
+ return [
88
+ {
89
+ name: "self_check",
90
+ ok: false,
91
+ detail: "check module does not export a function",
92
+ severity: "error",
93
+ },
94
+ ];
95
+ }
96
+ const abort = new AbortController();
97
+ const timer = setTimeout(() => abort.abort(), SELF_CHECK_TIMEOUT_MS);
98
+ // Bind the same per-request globals main() sees (config, request.session, …)
99
+ // so the check reads them identically; it receives the AbortSignal as its arg.
100
+ const requestContext = {
101
+ config,
102
+ params: {},
103
+ request: { session },
104
+ event: 0,
105
+ onProgress: null,
106
+ signal: abort.signal,
107
+ session,
108
+ };
109
+ try {
110
+ const raw = await Promise.race([
111
+ runWithContext(requestContext, () => Promise.resolve(checkFn(abort.signal))),
112
+ new Promise((_, reject) => {
113
+ abort.signal.addEventListener("abort", () => reject(new Error(`self-check timed out after ${SELF_CHECK_TIMEOUT_MS}ms`)));
114
+ }),
115
+ ]);
116
+ return normalizeChecks(raw);
117
+ }
118
+ catch (error) {
119
+ return [
120
+ {
121
+ name: "self_check",
122
+ ok: false,
123
+ detail: error?.message ?? String(error),
124
+ severity: "error",
125
+ },
126
+ ];
127
+ }
128
+ finally {
129
+ clearTimeout(timer);
130
+ }
131
+ };
6
132
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
7
133
  class UnauthenticatedError extends Error {
8
134
  }
@@ -136,19 +262,40 @@ export const serve = (main, options = {}) => {
136
262
  // validates the StackFactor token against BACKEND_URL — surfacing a broken
137
263
  // backend callback (the `ECONNREFUSED 127.0.0.1:443` class) before it bites.
138
264
  const healthCheck = (call, callback) => {
139
- const respond = (ok, backendReachable, message) => callback(null, {
265
+ const respond = (ok, backendReachable, message, checks = []) => callback(null, {
140
266
  ok,
141
267
  backend_reachable: backendReachable,
142
268
  version: process.env.AGENT_VERSION ?? "",
143
269
  message,
270
+ checks,
144
271
  });
272
+ // Shallow: confirm the process is up + ingress auth (the caller reached us).
145
273
  if (!call.request?.deep) {
146
274
  respond(true, false, "ok");
147
275
  return;
148
276
  }
149
- void authenticate(safeParse(call.request?.request_json, {}))
150
- .then(() => respond(true, true, "ok"))
151
- .catch((error) => respond(false, false, error?.message ?? "auth failed"));
277
+ // Deep: run the same auth path Execute uses (validates the StackFactor token
278
+ // against BACKEND_URL) AND the agent's own self-check module, then roll the
279
+ // two up into a single verdict. Warn-severity check failures don't block ok.
280
+ const request = safeParse(call.request?.request_json, {});
281
+ const checkCode = String(call.request?.check_code ?? "");
282
+ const checkConfig = safeParse(call.request?.config_json, {});
283
+ void (async () => {
284
+ let backendReachable = false;
285
+ let session = null;
286
+ let message = "ok";
287
+ try {
288
+ session = await authenticate(request);
289
+ backendReachable = true;
290
+ }
291
+ catch (error) {
292
+ message = error?.message ?? "auth failed";
293
+ }
294
+ const checks = await runSelfCheck(checkCode, checkConfig, session);
295
+ const ok = backendReachable &&
296
+ checks.every((check) => check.ok || check.severity === "warn");
297
+ respond(ok, backendReachable, message, checks);
298
+ })();
152
299
  };
153
300
  const server = new grpc.Server();
154
301
  server.addService(loadAgentService(), {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.8",
6
+ "version": "1.2.1",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",