@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.
- package/dist/cjs/agentProto.d.ts +1 -1
- package/dist/cjs/agentProto.d.ts.map +1 -1
- package/dist/cjs/agentProto.js +22 -4
- package/dist/cjs/client.d.ts +21 -1
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +10 -0
- package/dist/cjs/serve.d.ts.map +1 -1
- package/dist/cjs/serve.js +141 -4
- package/dist/esm/agentProto.d.ts +1 -1
- package/dist/esm/agentProto.d.ts.map +1 -1
- package/dist/esm/agentProto.js +22 -4
- package/dist/esm/client.d.ts +21 -1
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +10 -0
- package/dist/esm/serve.d.ts.map +1 -1
- package/dist/esm/serve.js +141 -4
- package/package.json +1 -1
package/dist/cjs/agentProto.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as grpc from "@grpc/grpc-js";
|
|
|
6
6
|
* zero or more `Progress` frames (driven by the agent's `onProgress`) followed
|
|
7
7
|
* by exactly one `Result` or `Error`.
|
|
8
8
|
*/
|
|
9
|
-
export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n}\n\nmessage HealthCheckResponse {\n bool ok
|
|
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
|
|
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"}
|
package/dist/cjs/agentProto.js
CHANGED
|
@@ -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
|
|
76
|
-
bool backend_reachable
|
|
77
|
-
string version
|
|
78
|
-
string message
|
|
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 {
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -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
|
package/dist/cjs/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;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,
|
|
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"}
|
package/dist/cjs/client.js
CHANGED
|
@@ -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
|
});
|
package/dist/cjs/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA+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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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)(), {
|
package/dist/esm/agentProto.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as grpc from "@grpc/grpc-js";
|
|
|
6
6
|
* zero or more `Progress` frames (driven by the agent's `onProgress`) followed
|
|
7
7
|
* by exactly one `Result` or `Error`.
|
|
8
8
|
*/
|
|
9
|
-
export declare const PROTO = "\nsyntax = \"proto3\";\npackage stackfactor.agent.v1;\n\nservice Agent {\n rpc Execute(ExecuteRequest) returns (stream Update);\n // Liveness/health probe served by serve() itself \u2014 no agent code required.\n // deep=true also exercises the StackFactor auth callback (BACKEND_URL).\n rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage ExecuteRequest {\n string content_type = 1;\n string data_json = 2;\n string config_json = 3;\n string request_json = 4;\n int32 event = 5;\n}\n\nmessage HealthCheckRequest {\n // Optional JSON carrying { authToken } for the deep backend-callback check.\n string request_json = 1;\n bool deep = 2;\n}\n\nmessage HealthCheckResponse {\n bool ok
|
|
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
|
|
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"}
|
package/dist/esm/agentProto.js
CHANGED
|
@@ -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
|
|
40
|
-
bool backend_reachable
|
|
41
|
-
string version
|
|
42
|
-
string message
|
|
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 {
|
package/dist/esm/client.d.ts
CHANGED
|
@@ -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
|
package/dist/esm/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,gFAAgF;IAChF,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;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,
|
|
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"}
|
package/dist/esm/client.js
CHANGED
|
@@ -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
|
});
|
package/dist/esm/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA+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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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(), {
|