@stackfactor/agent-utils 1.1.8 → 1.2.0

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;AA+ItC;;;;;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,126 @@ 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
+ // Hard cap on a self-check; the agent receives this as its AbortSignal so its
52
+ // own fetch/SDK calls can cooperate, and we fail the check if it overruns.
53
+ const SELF_CHECK_TIMEOUT_MS = 20_000;
54
+ /**
55
+ * Resolves a repo-relative check-module path within the app root, rejecting any
56
+ * path that escapes it or is absolute (the path may originate from the
57
+ * integration config, so treat it as untrusted). Returns null when unsafe.
58
+ */
59
+ const resolveCheckPath = (checkCode) => {
60
+ const root = process.cwd();
61
+ const rel = (checkCode || "").trim() || DEFAULT_CHECK_PATH;
62
+ const dest = (0, node_path_1.resolve)(root, rel);
63
+ const back = (0, node_path_1.relative)(root, dest);
64
+ if (back === "" || back.startsWith("..") || (0, node_path_1.isAbsolute)(back)) {
65
+ return null;
66
+ }
67
+ return dest;
68
+ };
69
+ /** Coerces whatever the agent's check returned into well-formed CheckResult[]. */
70
+ const normalizeChecks = (raw) => {
71
+ if (!Array.isArray(raw)) {
72
+ return [];
73
+ }
74
+ return raw
75
+ .filter((entry) => entry && typeof entry.name === "string")
76
+ .map((entry) => ({
77
+ name: entry.name,
78
+ ok: !!entry.ok,
79
+ detail: typeof entry.detail === "string" ? entry.detail : "",
80
+ severity: entry.severity === "warn" ? "warn" : "error",
81
+ }));
82
+ };
83
+ /**
84
+ * Loads and runs the agent's self-check module (`check_code` or the default
85
+ * `src/check.js`). The check follows the same programming model as `main`: it
86
+ * receives the request {@link AbortSignal} and reads `config` / `request`
87
+ * (incl. `request.session`) from the per-request globals (bound here via
88
+ * {@link runWithContext}); the logger is imported. The agent owns only its own
89
+ * checks — reachability/version rows are synthesised backend-side. Never throws:
90
+ * a missing module yields no rows; a load error, bad export, thrown error, or
91
+ * timeout collapses to a single failed `self_check` row.
92
+ */
93
+ const runSelfCheck = async (checkCode, config, session) => {
94
+ const modulePath = resolveCheckPath(checkCode);
95
+ if (!modulePath || !(0, node_fs_1.existsSync)(modulePath)) {
96
+ return []; // no self-check module shipped — baseline checks only
97
+ }
98
+ let checkFn;
99
+ try {
100
+ const mod = await Promise.resolve(`${(0, node_url_1.pathToFileURL)(modulePath).href}`).then(s => __importStar(require(s)));
101
+ const exported = mod?.default ?? mod;
102
+ checkFn =
103
+ typeof exported === "function" ? exported : exported?.check ?? exported?.default;
104
+ }
105
+ catch (error) {
106
+ return [
107
+ {
108
+ name: "self_check",
109
+ ok: false,
110
+ detail: `failed to load check module: ${error?.message ?? error}`,
111
+ severity: "error",
112
+ },
113
+ ];
114
+ }
115
+ if (typeof checkFn !== "function") {
116
+ return [
117
+ {
118
+ name: "self_check",
119
+ ok: false,
120
+ detail: "check module does not export a function",
121
+ severity: "error",
122
+ },
123
+ ];
124
+ }
125
+ const abort = new AbortController();
126
+ const timer = setTimeout(() => abort.abort(), SELF_CHECK_TIMEOUT_MS);
127
+ // Bind the same per-request globals main() sees (config, request.session, …)
128
+ // so the check reads them identically; it receives the AbortSignal as its arg.
129
+ const requestContext = {
130
+ config,
131
+ params: {},
132
+ request: { session },
133
+ event: 0,
134
+ onProgress: null,
135
+ signal: abort.signal,
136
+ session,
137
+ };
138
+ try {
139
+ const raw = await Promise.race([
140
+ (0, runtimeContext_js_1.runWithContext)(requestContext, () => Promise.resolve(checkFn(abort.signal))),
141
+ new Promise((_, reject) => {
142
+ abort.signal.addEventListener("abort", () => reject(new Error(`self-check timed out after ${SELF_CHECK_TIMEOUT_MS}ms`)));
143
+ }),
144
+ ]);
145
+ return normalizeChecks(raw);
146
+ }
147
+ catch (error) {
148
+ return [
149
+ {
150
+ name: "self_check",
151
+ ok: false,
152
+ detail: error?.message ?? String(error),
153
+ severity: "error",
154
+ },
155
+ ];
156
+ }
157
+ finally {
158
+ clearTimeout(timer);
159
+ }
160
+ };
45
161
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
46
162
  class UnauthenticatedError extends Error {
47
163
  }
@@ -175,19 +291,40 @@ const serve = (main, options = {}) => {
175
291
  // validates the StackFactor token against BACKEND_URL — surfacing a broken
176
292
  // backend callback (the `ECONNREFUSED 127.0.0.1:443` class) before it bites.
177
293
  const healthCheck = (call, callback) => {
178
- const respond = (ok, backendReachable, message) => callback(null, {
294
+ const respond = (ok, backendReachable, message, checks = []) => callback(null, {
179
295
  ok,
180
296
  backend_reachable: backendReachable,
181
297
  version: process.env.AGENT_VERSION ?? "",
182
298
  message,
299
+ checks,
183
300
  });
301
+ // Shallow: confirm the process is up + ingress auth (the caller reached us).
184
302
  if (!call.request?.deep) {
185
303
  respond(true, false, "ok");
186
304
  return;
187
305
  }
188
- void authenticate(safeParse(call.request?.request_json, {}))
189
- .then(() => respond(true, true, "ok"))
190
- .catch((error) => respond(false, false, error?.message ?? "auth failed"));
306
+ // Deep: run the same auth path Execute uses (validates the StackFactor token
307
+ // against BACKEND_URL) AND the agent's own self-check module, then roll the
308
+ // two up into a single verdict. Warn-severity check failures don't block ok.
309
+ const request = safeParse(call.request?.request_json, {});
310
+ const checkCode = String(call.request?.check_code ?? "");
311
+ const checkConfig = safeParse(call.request?.config_json, {});
312
+ void (async () => {
313
+ let backendReachable = false;
314
+ let session = null;
315
+ let message = "ok";
316
+ try {
317
+ session = await authenticate(request);
318
+ backendReachable = true;
319
+ }
320
+ catch (error) {
321
+ message = error?.message ?? "auth failed";
322
+ }
323
+ const checks = await runSelfCheck(checkCode, checkConfig, session);
324
+ const ok = backendReachable &&
325
+ checks.every((check) => check.ok || check.severity === "warn");
326
+ respond(ok, backendReachable, message, checks);
327
+ })();
191
328
  };
192
329
  const server = new grpc.Server();
193
330
  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;AA+ItC;;;;;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,124 @@
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
+ // Hard cap on a self-check; the agent receives this as its AbortSignal so its
13
+ // own fetch/SDK calls can cooperate, and we fail the check if it overruns.
14
+ const SELF_CHECK_TIMEOUT_MS = 20_000;
15
+ /**
16
+ * Resolves a repo-relative check-module path within the app root, rejecting any
17
+ * path that escapes it or is absolute (the path may originate from the
18
+ * integration config, so treat it as untrusted). Returns null when unsafe.
19
+ */
20
+ const resolveCheckPath = (checkCode) => {
21
+ const root = process.cwd();
22
+ const rel = (checkCode || "").trim() || DEFAULT_CHECK_PATH;
23
+ const dest = resolve(root, rel);
24
+ const back = relative(root, dest);
25
+ if (back === "" || back.startsWith("..") || isAbsolute(back)) {
26
+ return null;
27
+ }
28
+ return dest;
29
+ };
30
+ /** Coerces whatever the agent's check returned into well-formed CheckResult[]. */
31
+ const normalizeChecks = (raw) => {
32
+ if (!Array.isArray(raw)) {
33
+ return [];
34
+ }
35
+ return raw
36
+ .filter((entry) => entry && typeof entry.name === "string")
37
+ .map((entry) => ({
38
+ name: entry.name,
39
+ ok: !!entry.ok,
40
+ detail: typeof entry.detail === "string" ? entry.detail : "",
41
+ severity: entry.severity === "warn" ? "warn" : "error",
42
+ }));
43
+ };
44
+ /**
45
+ * Loads and runs the agent's self-check module (`check_code` or the default
46
+ * `src/check.js`). The check follows the same programming model as `main`: it
47
+ * receives the request {@link AbortSignal} and reads `config` / `request`
48
+ * (incl. `request.session`) from the per-request globals (bound here via
49
+ * {@link runWithContext}); the logger is imported. The agent owns only its own
50
+ * checks — reachability/version rows are synthesised backend-side. Never throws:
51
+ * a missing module yields no rows; a load error, bad export, thrown error, or
52
+ * timeout collapses to a single failed `self_check` row.
53
+ */
54
+ const runSelfCheck = async (checkCode, config, session) => {
55
+ const modulePath = resolveCheckPath(checkCode);
56
+ if (!modulePath || !existsSync(modulePath)) {
57
+ return []; // no self-check module shipped — baseline checks only
58
+ }
59
+ let checkFn;
60
+ try {
61
+ const mod = await import(pathToFileURL(modulePath).href);
62
+ const exported = mod?.default ?? mod;
63
+ checkFn =
64
+ typeof exported === "function" ? exported : exported?.check ?? exported?.default;
65
+ }
66
+ catch (error) {
67
+ return [
68
+ {
69
+ name: "self_check",
70
+ ok: false,
71
+ detail: `failed to load check module: ${error?.message ?? error}`,
72
+ severity: "error",
73
+ },
74
+ ];
75
+ }
76
+ if (typeof checkFn !== "function") {
77
+ return [
78
+ {
79
+ name: "self_check",
80
+ ok: false,
81
+ detail: "check module does not export a function",
82
+ severity: "error",
83
+ },
84
+ ];
85
+ }
86
+ const abort = new AbortController();
87
+ const timer = setTimeout(() => abort.abort(), SELF_CHECK_TIMEOUT_MS);
88
+ // Bind the same per-request globals main() sees (config, request.session, …)
89
+ // so the check reads them identically; it receives the AbortSignal as its arg.
90
+ const requestContext = {
91
+ config,
92
+ params: {},
93
+ request: { session },
94
+ event: 0,
95
+ onProgress: null,
96
+ signal: abort.signal,
97
+ session,
98
+ };
99
+ try {
100
+ const raw = await Promise.race([
101
+ runWithContext(requestContext, () => Promise.resolve(checkFn(abort.signal))),
102
+ new Promise((_, reject) => {
103
+ abort.signal.addEventListener("abort", () => reject(new Error(`self-check timed out after ${SELF_CHECK_TIMEOUT_MS}ms`)));
104
+ }),
105
+ ]);
106
+ return normalizeChecks(raw);
107
+ }
108
+ catch (error) {
109
+ return [
110
+ {
111
+ name: "self_check",
112
+ ok: false,
113
+ detail: error?.message ?? String(error),
114
+ severity: "error",
115
+ },
116
+ ];
117
+ }
118
+ finally {
119
+ clearTimeout(timer);
120
+ }
121
+ };
6
122
  /** Marks an auth failure so the handler can map it to gRPC UNAUTHENTICATED. */
7
123
  class UnauthenticatedError extends Error {
8
124
  }
@@ -136,19 +252,40 @@ export const serve = (main, options = {}) => {
136
252
  // validates the StackFactor token against BACKEND_URL — surfacing a broken
137
253
  // backend callback (the `ECONNREFUSED 127.0.0.1:443` class) before it bites.
138
254
  const healthCheck = (call, callback) => {
139
- const respond = (ok, backendReachable, message) => callback(null, {
255
+ const respond = (ok, backendReachable, message, checks = []) => callback(null, {
140
256
  ok,
141
257
  backend_reachable: backendReachable,
142
258
  version: process.env.AGENT_VERSION ?? "",
143
259
  message,
260
+ checks,
144
261
  });
262
+ // Shallow: confirm the process is up + ingress auth (the caller reached us).
145
263
  if (!call.request?.deep) {
146
264
  respond(true, false, "ok");
147
265
  return;
148
266
  }
149
- void authenticate(safeParse(call.request?.request_json, {}))
150
- .then(() => respond(true, true, "ok"))
151
- .catch((error) => respond(false, false, error?.message ?? "auth failed"));
267
+ // Deep: run the same auth path Execute uses (validates the StackFactor token
268
+ // against BACKEND_URL) AND the agent's own self-check module, then roll the
269
+ // two up into a single verdict. Warn-severity check failures don't block ok.
270
+ const request = safeParse(call.request?.request_json, {});
271
+ const checkCode = String(call.request?.check_code ?? "");
272
+ const checkConfig = safeParse(call.request?.config_json, {});
273
+ void (async () => {
274
+ let backendReachable = false;
275
+ let session = null;
276
+ let message = "ok";
277
+ try {
278
+ session = await authenticate(request);
279
+ backendReachable = true;
280
+ }
281
+ catch (error) {
282
+ message = error?.message ?? "auth failed";
283
+ }
284
+ const checks = await runSelfCheck(checkCode, checkConfig, session);
285
+ const ok = backendReachable &&
286
+ checks.every((check) => check.ok || check.severity === "warn");
287
+ respond(ok, backendReachable, message, checks);
288
+ })();
152
289
  };
153
290
  const server = new grpc.Server();
154
291
  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.0",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",