@stackfactor/agent-utils 1.1.7 → 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,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAElF,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,GAAI,MAAM,SAAS,EAAE,UAAS,YAAiB,KAAG,IAAI,CAAC,MAuIxE,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
  }
@@ -54,13 +170,13 @@ class UnauthenticatedError extends Error {
54
170
  const defaultAuthenticate = async (request) => {
55
171
  const token = request?.authToken ?? request?.authorization;
56
172
  if (!token) {
57
- throw new UnauthenticatedError("Missing auth token");
173
+ throw new UnauthenticatedError("Missing StackFactor auth token");
58
174
  }
59
175
  try {
60
176
  return await client_api_1.session.getSession(token);
61
177
  }
62
178
  catch (error) {
63
- throw new UnauthenticatedError(`Invalid auth token: ${error?.message ?? error}`);
179
+ throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
64
180
  }
65
181
  };
66
182
  const safeParse = (value, fallback) => {
@@ -151,7 +267,9 @@ const serve = (main, options = {}) => {
151
267
  try {
152
268
  const result = await (0, runtimeContext_js_1.runWithContext)(context, () => main(abort.signal));
153
269
  if (!abort.signal.aborted) {
154
- call.write({ result: { result_json: JSON.stringify(result ?? null) } });
270
+ call.write({
271
+ result: { result_json: JSON.stringify(result ?? null) },
272
+ });
155
273
  }
156
274
  }
157
275
  catch (error) {
@@ -173,19 +291,40 @@ const serve = (main, options = {}) => {
173
291
  // validates the StackFactor token against BACKEND_URL — surfacing a broken
174
292
  // backend callback (the `ECONNREFUSED 127.0.0.1:443` class) before it bites.
175
293
  const healthCheck = (call, callback) => {
176
- const respond = (ok, backendReachable, message) => callback(null, {
294
+ const respond = (ok, backendReachable, message, checks = []) => callback(null, {
177
295
  ok,
178
296
  backend_reachable: backendReachable,
179
297
  version: process.env.AGENT_VERSION ?? "",
180
298
  message,
299
+ checks,
181
300
  });
301
+ // Shallow: confirm the process is up + ingress auth (the caller reached us).
182
302
  if (!call.request?.deep) {
183
303
  respond(true, false, "ok");
184
304
  return;
185
305
  }
186
- void authenticate(safeParse(call.request?.request_json, {}))
187
- .then(() => respond(true, true, "ok"))
188
- .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
+ })();
189
328
  };
190
329
  const server = new grpc.Server();
191
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,CAAC,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC,CAAC;AAElF,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,GAAI,MAAM,SAAS,EAAE,UAAS,YAAiB,KAAG,IAAI,CAAC,MAuIxE,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
  }
@@ -15,13 +131,13 @@ class UnauthenticatedError extends Error {
15
131
  const defaultAuthenticate = async (request) => {
16
132
  const token = request?.authToken ?? request?.authorization;
17
133
  if (!token) {
18
- throw new UnauthenticatedError("Missing auth token");
134
+ throw new UnauthenticatedError("Missing StackFactor auth token");
19
135
  }
20
136
  try {
21
137
  return await clientSession.getSession(token);
22
138
  }
23
139
  catch (error) {
24
- throw new UnauthenticatedError(`Invalid auth token: ${error?.message ?? error}`);
140
+ throw new UnauthenticatedError(`Invalid StackFactor auth token: ${error?.message ?? error}`);
25
141
  }
26
142
  };
27
143
  const safeParse = (value, fallback) => {
@@ -112,7 +228,9 @@ export const serve = (main, options = {}) => {
112
228
  try {
113
229
  const result = await runWithContext(context, () => main(abort.signal));
114
230
  if (!abort.signal.aborted) {
115
- call.write({ result: { result_json: JSON.stringify(result ?? null) } });
231
+ call.write({
232
+ result: { result_json: JSON.stringify(result ?? null) },
233
+ });
116
234
  }
117
235
  }
118
236
  catch (error) {
@@ -134,19 +252,40 @@ export const serve = (main, options = {}) => {
134
252
  // validates the StackFactor token against BACKEND_URL — surfacing a broken
135
253
  // backend callback (the `ECONNREFUSED 127.0.0.1:443` class) before it bites.
136
254
  const healthCheck = (call, callback) => {
137
- const respond = (ok, backendReachable, message) => callback(null, {
255
+ const respond = (ok, backendReachable, message, checks = []) => callback(null, {
138
256
  ok,
139
257
  backend_reachable: backendReachable,
140
258
  version: process.env.AGENT_VERSION ?? "",
141
259
  message,
260
+ checks,
142
261
  });
262
+ // Shallow: confirm the process is up + ingress auth (the caller reached us).
143
263
  if (!call.request?.deep) {
144
264
  respond(true, false, "ok");
145
265
  return;
146
266
  }
147
- void authenticate(safeParse(call.request?.request_json, {}))
148
- .then(() => respond(true, true, "ok"))
149
- .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
+ })();
150
289
  };
151
290
  const server = new grpc.Server();
152
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.7",
6
+ "version": "1.2.0",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",