@xtrape/capsule-agent-node 0.1.0-public-review.1 → 0.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/README.md +86 -1
- package/dist/index.cjs +80 -8
- package/dist/index.d.cts +83 -9
- package/dist/index.d.ts +83 -9
- package/dist/index.js +76 -7
- package/package.json +10 -11
package/README.md
CHANGED
|
@@ -285,7 +285,8 @@ Main exports:
|
|
|
285
285
|
- `CapsuleAgent`
|
|
286
286
|
- `FileTokenStore`
|
|
287
287
|
- `AgentApiClient`
|
|
288
|
-
- `AgentApiError`
|
|
288
|
+
- `AgentApiError`, `RegistrationError`, `AgentAuthError`, `NetworkError`
|
|
289
|
+
- `AgentLogLevel`, `AgentLogRecord`, `StructuredLogger`, `ConsoleLogger` (types)
|
|
289
290
|
- SDK option and provider types from `types.ts`
|
|
290
291
|
|
|
291
292
|
Core methods:
|
|
@@ -300,6 +301,90 @@ Core methods:
|
|
|
300
301
|
| `.stop()` | Stops background loops. |
|
|
301
302
|
| `.runHealth()` | Runs the configured health provider. |
|
|
302
303
|
|
|
304
|
+
### Typed errors
|
|
305
|
+
|
|
306
|
+
> Added in `0.2.0`. Pre-`0.2` code that catches `AgentApiError` still works
|
|
307
|
+
> because the new classes extend it.
|
|
308
|
+
|
|
309
|
+
The SDK surfaces four error classes so callers can branch on the failure
|
|
310
|
+
mode without parsing messages:
|
|
311
|
+
|
|
312
|
+
| Class | When it's thrown | Should the caller retry? |
|
|
313
|
+
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
|
|
314
|
+
| `RegistrationError` | `/api/agents/register` rejects the one-time token (expired, revoked, already used, malformed). Needs operator action — mint a fresh registration token. | No. |
|
|
315
|
+
| `AgentAuthError` | Any non-register call returns 401/403. The agent token has been revoked or the agent disabled. Re-register before resuming. | No. |
|
|
316
|
+
| `NetworkError` | `fetch()` threw before getting an HTTP response (DNS failure, ECONNREFUSED, abort, timeout). `status === 0`; carries an optional `cause`. | Yes — SDK already does. |
|
|
317
|
+
| `AgentApiError` | Catch-all parent. Direct instances carry HTTP 4xx/5xx that don't fit the more specific cases (e.g. 409 conflict, 422 validation, 5xx server error). | Only on 5xx. |
|
|
318
|
+
|
|
319
|
+
The SDK's internal `retry()` loop already implements this policy: only
|
|
320
|
+
`NetworkError` and 5xx `AgentApiError` are retried with exponential backoff;
|
|
321
|
+
4xx — including the two typed subclasses — surfaces immediately so the caller
|
|
322
|
+
isn't waiting on a backoff that can never succeed.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
import {
|
|
326
|
+
CapsuleAgent,
|
|
327
|
+
RegistrationError,
|
|
328
|
+
AgentAuthError,
|
|
329
|
+
NetworkError,
|
|
330
|
+
} from "@xtrape/capsule-agent-node";
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await agent.start();
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (err instanceof RegistrationError) {
|
|
336
|
+
console.error("registration token is no longer valid:", err.code);
|
|
337
|
+
process.exit(64);
|
|
338
|
+
}
|
|
339
|
+
if (err instanceof AgentAuthError) {
|
|
340
|
+
console.error("agent token rejected — was the agent revoked?");
|
|
341
|
+
process.exit(65);
|
|
342
|
+
}
|
|
343
|
+
if (err instanceof NetworkError) {
|
|
344
|
+
console.error("could not reach Opstage:", err.message);
|
|
345
|
+
process.exit(75);
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Structured logger
|
|
352
|
+
|
|
353
|
+
> Added in `0.2.0`. Existing zero-config callers (no `logger` / no
|
|
354
|
+
> `structuredLogger`) keep using the global `console` exactly as before.
|
|
355
|
+
|
|
356
|
+
By default the SDK calls `console.debug` / `.info` / `.warn` / `.error`. For
|
|
357
|
+
JSON-line logging, pino, OpenTelemetry, or any in-house collector, pass a
|
|
358
|
+
`structuredLogger`:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import { CapsuleAgent, type StructuredLogger } from "@xtrape/capsule-agent-node";
|
|
362
|
+
|
|
363
|
+
const collector: StructuredLogger = {
|
|
364
|
+
log({ level, message, fields }) {
|
|
365
|
+
process.stdout.write(JSON.stringify({ t: Date.now(), level, message, ...fields }) + "\n");
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const agent = new CapsuleAgent({
|
|
370
|
+
// ...
|
|
371
|
+
structuredLogger: collector,
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Each record arrives as `{ level: "debug" | "info" | "warn" | "error",
|
|
376
|
+
message: string, fields?: Record<string, unknown> }`. The SDK normalizes the
|
|
377
|
+
optional context before emitting:
|
|
378
|
+
|
|
379
|
+
- `Error` instances become `{ error: { name, message, stack } }` so they
|
|
380
|
+
survive `JSON.stringify`.
|
|
381
|
+
- Primitives become `{ value: <primitive> }`.
|
|
382
|
+
- Plain objects pass through unchanged.
|
|
383
|
+
|
|
384
|
+
In all cases the redactor runs over `fields` first, so tokens and
|
|
385
|
+
secret-bearing keys never reach the collector. If both `structuredLogger`
|
|
386
|
+
and `logger` are configured, `structuredLogger` wins.
|
|
387
|
+
|
|
303
388
|
## Version Compatibility
|
|
304
389
|
|
|
305
390
|
| Package | Compatible with |
|
package/dist/index.cjs
CHANGED
|
@@ -22,21 +22,53 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AgentApiClient: () => AgentApiClient,
|
|
24
24
|
AgentApiError: () => AgentApiError,
|
|
25
|
+
AgentAuthError: () => AgentAuthError,
|
|
25
26
|
CapsuleAgent: () => CapsuleAgent,
|
|
26
|
-
FileTokenStore: () => FileTokenStore
|
|
27
|
+
FileTokenStore: () => FileTokenStore,
|
|
28
|
+
NetworkError: () => NetworkError,
|
|
29
|
+
RegistrationError: () => RegistrationError
|
|
27
30
|
});
|
|
28
31
|
module.exports = __toCommonJS(index_exports);
|
|
29
32
|
|
|
30
33
|
// src/client/errors.ts
|
|
31
34
|
var AgentApiError = class extends Error {
|
|
32
|
-
constructor(status, message, body) {
|
|
35
|
+
constructor(status, message, body, code) {
|
|
33
36
|
super(message);
|
|
34
37
|
this.status = status;
|
|
35
38
|
this.body = body;
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.name = "AgentApiError";
|
|
36
41
|
}
|
|
37
42
|
status;
|
|
38
43
|
body;
|
|
44
|
+
code;
|
|
39
45
|
};
|
|
46
|
+
var RegistrationError = class extends AgentApiError {
|
|
47
|
+
constructor(message, status, body, code) {
|
|
48
|
+
super(status, message, body, code);
|
|
49
|
+
this.name = "RegistrationError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var AgentAuthError = class extends AgentApiError {
|
|
53
|
+
constructor(message, status, body, code) {
|
|
54
|
+
super(status, message, body, code);
|
|
55
|
+
this.name = "AgentAuthError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var NetworkError = class extends AgentApiError {
|
|
59
|
+
constructor(message, cause) {
|
|
60
|
+
super(0, message, void 0, "NETWORK_ERROR");
|
|
61
|
+
this.name = "NetworkError";
|
|
62
|
+
if (cause !== void 0) {
|
|
63
|
+
this.cause = cause;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function classifyAgentApiError(status, message, body, code, path) {
|
|
68
|
+
if (status === 401 && path.endsWith("/register")) return new RegistrationError(message, status, body, code);
|
|
69
|
+
if (status === 401 || status === 403) return new AgentAuthError(message, status, body, code);
|
|
70
|
+
return new AgentApiError(status, message, body, code);
|
|
71
|
+
}
|
|
40
72
|
|
|
41
73
|
// src/client/agent-api-client.ts
|
|
42
74
|
var AgentApiClient = class {
|
|
@@ -51,10 +83,24 @@ var AgentApiClient = class {
|
|
|
51
83
|
const headers = new Headers(init.headers);
|
|
52
84
|
headers.set("content-type", "application/json");
|
|
53
85
|
if (init.token) headers.set("authorization", `Bearer ${init.token}`);
|
|
54
|
-
|
|
86
|
+
let res;
|
|
87
|
+
try {
|
|
88
|
+
res = await fetch(this.url(path), { ...init, headers });
|
|
89
|
+
} catch (cause) {
|
|
90
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
91
|
+
throw new NetworkError(`Failed to reach ${path}: ${reason}`, cause);
|
|
92
|
+
}
|
|
55
93
|
const text = await res.text();
|
|
56
|
-
|
|
57
|
-
|
|
94
|
+
let body;
|
|
95
|
+
try {
|
|
96
|
+
body = text ? JSON.parse(text) : void 0;
|
|
97
|
+
} catch {
|
|
98
|
+
body = { error: { message: res.statusText } };
|
|
99
|
+
}
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const err = body?.error;
|
|
102
|
+
throw classifyAgentApiError(res.status, err?.message ?? res.statusText, body, err?.code, path);
|
|
103
|
+
}
|
|
58
104
|
return body?.data ?? body;
|
|
59
105
|
}
|
|
60
106
|
register(req) {
|
|
@@ -130,17 +176,27 @@ function redact(value) {
|
|
|
130
176
|
|
|
131
177
|
// src/capsule-agent.ts
|
|
132
178
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
179
|
+
function emitLog(consoleSink, structured, level, message, fields) {
|
|
180
|
+
if (structured) {
|
|
181
|
+
structured.log({ level, message, fields });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (fields === void 0) consoleSink[level](message);
|
|
185
|
+
else consoleSink[level](message, fields);
|
|
186
|
+
}
|
|
133
187
|
var CapsuleAgent = class {
|
|
134
188
|
constructor(options) {
|
|
135
189
|
this.options = options;
|
|
136
190
|
this.client = new AgentApiClient(options.backendUrl);
|
|
137
191
|
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
138
192
|
this.logger = options.logger ?? console;
|
|
193
|
+
this.structuredLogger = options.structuredLogger;
|
|
139
194
|
}
|
|
140
195
|
options;
|
|
141
196
|
client;
|
|
142
197
|
store;
|
|
143
198
|
logger;
|
|
199
|
+
structuredLogger;
|
|
144
200
|
agentId;
|
|
145
201
|
token;
|
|
146
202
|
stopped = true;
|
|
@@ -288,20 +344,36 @@ var CapsuleAgent = class {
|
|
|
288
344
|
return await fn();
|
|
289
345
|
} catch (e) {
|
|
290
346
|
last = e;
|
|
291
|
-
if (e instanceof
|
|
347
|
+
if (e instanceof NetworkError) {
|
|
348
|
+
} else if (e instanceof AgentApiError && e.status >= 500) {
|
|
349
|
+
} else {
|
|
350
|
+
throw e;
|
|
351
|
+
}
|
|
292
352
|
await sleep(Math.min(1e3 * 2 ** i, 15e3));
|
|
293
353
|
}
|
|
294
354
|
}
|
|
295
355
|
throw last;
|
|
296
356
|
}
|
|
297
357
|
log(level, msg, obj) {
|
|
298
|
-
|
|
358
|
+
const fields = obj === void 0 ? void 0 : this.normalizeFields(obj);
|
|
359
|
+
const redacted = fields ? redact(fields) : void 0;
|
|
360
|
+
emitLog(this.logger, this.structuredLogger, level, msg, redacted);
|
|
361
|
+
}
|
|
362
|
+
normalizeFields(obj) {
|
|
363
|
+
if (obj instanceof Error) {
|
|
364
|
+
return { error: { message: obj.message, name: obj.name, stack: obj.stack } };
|
|
365
|
+
}
|
|
366
|
+
if (typeof obj === "object" && obj !== null) return obj;
|
|
367
|
+
return { value: obj };
|
|
299
368
|
}
|
|
300
369
|
};
|
|
301
370
|
// Annotate the CommonJS export names for ESM import in node:
|
|
302
371
|
0 && (module.exports = {
|
|
303
372
|
AgentApiClient,
|
|
304
373
|
AgentApiError,
|
|
374
|
+
AgentAuthError,
|
|
305
375
|
CapsuleAgent,
|
|
306
|
-
FileTokenStore
|
|
376
|
+
FileTokenStore,
|
|
377
|
+
NetworkError,
|
|
378
|
+
RegistrationError
|
|
307
379
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -15,6 +15,36 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
|
15
15
|
error?: Record<string, unknown>;
|
|
16
16
|
} | Record<string, unknown>>;
|
|
17
17
|
type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Log level emitted by the SDK. Aligns with the four standard `Console`
|
|
20
|
+
* methods so a plain `console` works as a logger with no adapter.
|
|
21
|
+
*/
|
|
22
|
+
type AgentLogLevel = "debug" | "info" | "warn" | "error";
|
|
23
|
+
/**
|
|
24
|
+
* Structured log record passed to a `StructuredLogger`. The SDK populates
|
|
25
|
+
* `level` and `message`; `fields` carries an already-redacted Record of
|
|
26
|
+
* runtime context (agent id, error code, retry count, etc.).
|
|
27
|
+
*/
|
|
28
|
+
interface AgentLogRecord {
|
|
29
|
+
level: AgentLogLevel;
|
|
30
|
+
message: string;
|
|
31
|
+
fields?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A structured log sink. Useful when the host wants to forward SDK events
|
|
35
|
+
* into a JSON-line logger, pino, OpenTelemetry, or a custom collector
|
|
36
|
+
* without losing the per-field context to `console.warn`'s positional
|
|
37
|
+
* argument list.
|
|
38
|
+
*/
|
|
39
|
+
interface StructuredLogger {
|
|
40
|
+
log(record: AgentLogRecord): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Console-shaped logger (subset of Node's `Console`). The SDK defaults to
|
|
44
|
+
* the global `console` when neither option is set; if both are provided,
|
|
45
|
+
* `structuredLogger` wins.
|
|
46
|
+
*/
|
|
47
|
+
type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
|
|
18
48
|
interface CapsuleAgentOptions {
|
|
19
49
|
backendUrl: string;
|
|
20
50
|
registrationToken?: string;
|
|
@@ -42,7 +72,14 @@ interface CapsuleAgentOptions {
|
|
|
42
72
|
heartbeatIntervalSeconds?: number;
|
|
43
73
|
commandPollIntervalSeconds?: number;
|
|
44
74
|
autoStartLoops?: boolean;
|
|
45
|
-
|
|
75
|
+
/** Console-shaped sink; default is the global `console`. */
|
|
76
|
+
logger?: ConsoleLogger;
|
|
77
|
+
/**
|
|
78
|
+
* Structured sink. If provided, takes precedence over `logger`. The SDK
|
|
79
|
+
* still applies its own redaction before calling `log()`, so the host
|
|
80
|
+
* can route records as-is to a JSON stream.
|
|
81
|
+
*/
|
|
82
|
+
structuredLogger?: StructuredLogger;
|
|
46
83
|
failOnStartError?: boolean;
|
|
47
84
|
}
|
|
48
85
|
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
@@ -63,6 +100,7 @@ declare class CapsuleAgent {
|
|
|
63
100
|
private readonly client;
|
|
64
101
|
private readonly store;
|
|
65
102
|
private readonly logger;
|
|
103
|
+
private readonly structuredLogger?;
|
|
66
104
|
private agentId?;
|
|
67
105
|
private token?;
|
|
68
106
|
private stopped;
|
|
@@ -89,6 +127,7 @@ declare class CapsuleAgent {
|
|
|
89
127
|
private withTimeout;
|
|
90
128
|
private retry;
|
|
91
129
|
private log;
|
|
130
|
+
private normalizeFields;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
133
|
declare class FileTokenStore implements TokenStore {
|
|
@@ -99,6 +138,47 @@ declare class FileTokenStore implements TokenStore {
|
|
|
99
138
|
clear(): Promise<void>;
|
|
100
139
|
}
|
|
101
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Base class for any failure that crosses the Agent ↔ Backend wire. Use
|
|
143
|
+
* `instanceof AgentApiError` to catch the family, and the more specific
|
|
144
|
+
* subclasses below when the caller wants to branch on the failure mode.
|
|
145
|
+
*
|
|
146
|
+
* `status === 0` is reserved for transport failures that never reached the
|
|
147
|
+
* backend (DNS failure, ECONNREFUSED, network timeout, etc.); use
|
|
148
|
+
* `NetworkError` for those.
|
|
149
|
+
*/
|
|
150
|
+
declare class AgentApiError extends Error {
|
|
151
|
+
readonly status: number;
|
|
152
|
+
readonly body?: unknown | undefined;
|
|
153
|
+
readonly code?: string | undefined;
|
|
154
|
+
constructor(status: number, message: string, body?: unknown | undefined, code?: string | undefined);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The backend rejected the registration token (expired, revoked, already
|
|
158
|
+
* used, malformed). The agent cannot proceed without operator action: a
|
|
159
|
+
* fresh registration token must be created in Opstage.
|
|
160
|
+
*/
|
|
161
|
+
declare class RegistrationError extends AgentApiError {
|
|
162
|
+
constructor(message: string, status: number, body?: unknown, code?: string);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The backend rejected the persisted agent token (UNAUTHORIZED, agent
|
|
166
|
+
* REVOKED, agent DISABLED). The agent must be re-registered with a fresh
|
|
167
|
+
* registration token before it can resume.
|
|
168
|
+
*/
|
|
169
|
+
declare class AgentAuthError extends AgentApiError {
|
|
170
|
+
constructor(message: string, status: number, body?: unknown, code?: string);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Transport-level failure: the request did not reach the backend, or the
|
|
174
|
+
* backend's response was not a parseable HTTP envelope. The SDK retries
|
|
175
|
+
* these with exponential backoff; surface to the caller only after the
|
|
176
|
+
* retry budget is exhausted.
|
|
177
|
+
*/
|
|
178
|
+
declare class NetworkError extends AgentApiError {
|
|
179
|
+
constructor(message: string, cause?: unknown);
|
|
180
|
+
}
|
|
181
|
+
|
|
102
182
|
declare class AgentApiClient {
|
|
103
183
|
private readonly backendUrl;
|
|
104
184
|
constructor(backendUrl: string);
|
|
@@ -113,7 +193,7 @@ declare class AgentApiClient {
|
|
|
113
193
|
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
114
194
|
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
115
195
|
pollCommands(agentId: string, token: string): Promise<{
|
|
116
|
-
type: "ACTION_PREPARE" | "ACTION_EXECUTE"
|
|
196
|
+
type: "ACTION_PREPARE" | "ACTION_EXECUTE";
|
|
117
197
|
id: string;
|
|
118
198
|
createdAt: string;
|
|
119
199
|
status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
|
|
@@ -129,10 +209,4 @@ declare class AgentApiClient {
|
|
|
129
209
|
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
130
210
|
}
|
|
131
211
|
|
|
132
|
-
|
|
133
|
-
readonly status: number;
|
|
134
|
-
readonly body?: unknown | undefined;
|
|
135
|
-
constructor(status: number, message: string, body?: unknown | undefined);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type RuntimeKind, type ServiceSnapshot, type TokenStore };
|
|
212
|
+
export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type RegisteredAction, RegistrationError, type RuntimeKind, type ServiceSnapshot, type StructuredLogger, type TokenStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,36 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
|
15
15
|
error?: Record<string, unknown>;
|
|
16
16
|
} | Record<string, unknown>>;
|
|
17
17
|
type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Log level emitted by the SDK. Aligns with the four standard `Console`
|
|
20
|
+
* methods so a plain `console` works as a logger with no adapter.
|
|
21
|
+
*/
|
|
22
|
+
type AgentLogLevel = "debug" | "info" | "warn" | "error";
|
|
23
|
+
/**
|
|
24
|
+
* Structured log record passed to a `StructuredLogger`. The SDK populates
|
|
25
|
+
* `level` and `message`; `fields` carries an already-redacted Record of
|
|
26
|
+
* runtime context (agent id, error code, retry count, etc.).
|
|
27
|
+
*/
|
|
28
|
+
interface AgentLogRecord {
|
|
29
|
+
level: AgentLogLevel;
|
|
30
|
+
message: string;
|
|
31
|
+
fields?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A structured log sink. Useful when the host wants to forward SDK events
|
|
35
|
+
* into a JSON-line logger, pino, OpenTelemetry, or a custom collector
|
|
36
|
+
* without losing the per-field context to `console.warn`'s positional
|
|
37
|
+
* argument list.
|
|
38
|
+
*/
|
|
39
|
+
interface StructuredLogger {
|
|
40
|
+
log(record: AgentLogRecord): void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Console-shaped logger (subset of Node's `Console`). The SDK defaults to
|
|
44
|
+
* the global `console` when neither option is set; if both are provided,
|
|
45
|
+
* `structuredLogger` wins.
|
|
46
|
+
*/
|
|
47
|
+
type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
|
|
18
48
|
interface CapsuleAgentOptions {
|
|
19
49
|
backendUrl: string;
|
|
20
50
|
registrationToken?: string;
|
|
@@ -42,7 +72,14 @@ interface CapsuleAgentOptions {
|
|
|
42
72
|
heartbeatIntervalSeconds?: number;
|
|
43
73
|
commandPollIntervalSeconds?: number;
|
|
44
74
|
autoStartLoops?: boolean;
|
|
45
|
-
|
|
75
|
+
/** Console-shaped sink; default is the global `console`. */
|
|
76
|
+
logger?: ConsoleLogger;
|
|
77
|
+
/**
|
|
78
|
+
* Structured sink. If provided, takes precedence over `logger`. The SDK
|
|
79
|
+
* still applies its own redaction before calling `log()`, so the host
|
|
80
|
+
* can route records as-is to a JSON stream.
|
|
81
|
+
*/
|
|
82
|
+
structuredLogger?: StructuredLogger;
|
|
46
83
|
failOnStartError?: boolean;
|
|
47
84
|
}
|
|
48
85
|
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
@@ -63,6 +100,7 @@ declare class CapsuleAgent {
|
|
|
63
100
|
private readonly client;
|
|
64
101
|
private readonly store;
|
|
65
102
|
private readonly logger;
|
|
103
|
+
private readonly structuredLogger?;
|
|
66
104
|
private agentId?;
|
|
67
105
|
private token?;
|
|
68
106
|
private stopped;
|
|
@@ -89,6 +127,7 @@ declare class CapsuleAgent {
|
|
|
89
127
|
private withTimeout;
|
|
90
128
|
private retry;
|
|
91
129
|
private log;
|
|
130
|
+
private normalizeFields;
|
|
92
131
|
}
|
|
93
132
|
|
|
94
133
|
declare class FileTokenStore implements TokenStore {
|
|
@@ -99,6 +138,47 @@ declare class FileTokenStore implements TokenStore {
|
|
|
99
138
|
clear(): Promise<void>;
|
|
100
139
|
}
|
|
101
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Base class for any failure that crosses the Agent ↔ Backend wire. Use
|
|
143
|
+
* `instanceof AgentApiError` to catch the family, and the more specific
|
|
144
|
+
* subclasses below when the caller wants to branch on the failure mode.
|
|
145
|
+
*
|
|
146
|
+
* `status === 0` is reserved for transport failures that never reached the
|
|
147
|
+
* backend (DNS failure, ECONNREFUSED, network timeout, etc.); use
|
|
148
|
+
* `NetworkError` for those.
|
|
149
|
+
*/
|
|
150
|
+
declare class AgentApiError extends Error {
|
|
151
|
+
readonly status: number;
|
|
152
|
+
readonly body?: unknown | undefined;
|
|
153
|
+
readonly code?: string | undefined;
|
|
154
|
+
constructor(status: number, message: string, body?: unknown | undefined, code?: string | undefined);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The backend rejected the registration token (expired, revoked, already
|
|
158
|
+
* used, malformed). The agent cannot proceed without operator action: a
|
|
159
|
+
* fresh registration token must be created in Opstage.
|
|
160
|
+
*/
|
|
161
|
+
declare class RegistrationError extends AgentApiError {
|
|
162
|
+
constructor(message: string, status: number, body?: unknown, code?: string);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The backend rejected the persisted agent token (UNAUTHORIZED, agent
|
|
166
|
+
* REVOKED, agent DISABLED). The agent must be re-registered with a fresh
|
|
167
|
+
* registration token before it can resume.
|
|
168
|
+
*/
|
|
169
|
+
declare class AgentAuthError extends AgentApiError {
|
|
170
|
+
constructor(message: string, status: number, body?: unknown, code?: string);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Transport-level failure: the request did not reach the backend, or the
|
|
174
|
+
* backend's response was not a parseable HTTP envelope. The SDK retries
|
|
175
|
+
* these with exponential backoff; surface to the caller only after the
|
|
176
|
+
* retry budget is exhausted.
|
|
177
|
+
*/
|
|
178
|
+
declare class NetworkError extends AgentApiError {
|
|
179
|
+
constructor(message: string, cause?: unknown);
|
|
180
|
+
}
|
|
181
|
+
|
|
102
182
|
declare class AgentApiClient {
|
|
103
183
|
private readonly backendUrl;
|
|
104
184
|
constructor(backendUrl: string);
|
|
@@ -113,7 +193,7 @@ declare class AgentApiClient {
|
|
|
113
193
|
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
114
194
|
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
115
195
|
pollCommands(agentId: string, token: string): Promise<{
|
|
116
|
-
type: "ACTION_PREPARE" | "ACTION_EXECUTE"
|
|
196
|
+
type: "ACTION_PREPARE" | "ACTION_EXECUTE";
|
|
117
197
|
id: string;
|
|
118
198
|
createdAt: string;
|
|
119
199
|
status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
|
|
@@ -129,10 +209,4 @@ declare class AgentApiClient {
|
|
|
129
209
|
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
130
210
|
}
|
|
131
211
|
|
|
132
|
-
|
|
133
|
-
readonly status: number;
|
|
134
|
-
readonly body?: unknown | undefined;
|
|
135
|
-
constructor(status: number, message: string, body?: unknown | undefined);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type RuntimeKind, type ServiceSnapshot, type TokenStore };
|
|
212
|
+
export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type RegisteredAction, RegistrationError, type RuntimeKind, type ServiceSnapshot, type StructuredLogger, type TokenStore };
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
1
|
// src/client/errors.ts
|
|
2
2
|
var AgentApiError = class extends Error {
|
|
3
|
-
constructor(status, message, body) {
|
|
3
|
+
constructor(status, message, body, code) {
|
|
4
4
|
super(message);
|
|
5
5
|
this.status = status;
|
|
6
6
|
this.body = body;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = "AgentApiError";
|
|
7
9
|
}
|
|
8
10
|
status;
|
|
9
11
|
body;
|
|
12
|
+
code;
|
|
10
13
|
};
|
|
14
|
+
var RegistrationError = class extends AgentApiError {
|
|
15
|
+
constructor(message, status, body, code) {
|
|
16
|
+
super(status, message, body, code);
|
|
17
|
+
this.name = "RegistrationError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var AgentAuthError = class extends AgentApiError {
|
|
21
|
+
constructor(message, status, body, code) {
|
|
22
|
+
super(status, message, body, code);
|
|
23
|
+
this.name = "AgentAuthError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var NetworkError = class extends AgentApiError {
|
|
27
|
+
constructor(message, cause) {
|
|
28
|
+
super(0, message, void 0, "NETWORK_ERROR");
|
|
29
|
+
this.name = "NetworkError";
|
|
30
|
+
if (cause !== void 0) {
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function classifyAgentApiError(status, message, body, code, path) {
|
|
36
|
+
if (status === 401 && path.endsWith("/register")) return new RegistrationError(message, status, body, code);
|
|
37
|
+
if (status === 401 || status === 403) return new AgentAuthError(message, status, body, code);
|
|
38
|
+
return new AgentApiError(status, message, body, code);
|
|
39
|
+
}
|
|
11
40
|
|
|
12
41
|
// src/client/agent-api-client.ts
|
|
13
42
|
var AgentApiClient = class {
|
|
@@ -22,10 +51,24 @@ var AgentApiClient = class {
|
|
|
22
51
|
const headers = new Headers(init.headers);
|
|
23
52
|
headers.set("content-type", "application/json");
|
|
24
53
|
if (init.token) headers.set("authorization", `Bearer ${init.token}`);
|
|
25
|
-
|
|
54
|
+
let res;
|
|
55
|
+
try {
|
|
56
|
+
res = await fetch(this.url(path), { ...init, headers });
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
59
|
+
throw new NetworkError(`Failed to reach ${path}: ${reason}`, cause);
|
|
60
|
+
}
|
|
26
61
|
const text = await res.text();
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
let body;
|
|
63
|
+
try {
|
|
64
|
+
body = text ? JSON.parse(text) : void 0;
|
|
65
|
+
} catch {
|
|
66
|
+
body = { error: { message: res.statusText } };
|
|
67
|
+
}
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const err = body?.error;
|
|
70
|
+
throw classifyAgentApiError(res.status, err?.message ?? res.statusText, body, err?.code, path);
|
|
71
|
+
}
|
|
29
72
|
return body?.data ?? body;
|
|
30
73
|
}
|
|
31
74
|
register(req) {
|
|
@@ -101,17 +144,27 @@ function redact(value) {
|
|
|
101
144
|
|
|
102
145
|
// src/capsule-agent.ts
|
|
103
146
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
147
|
+
function emitLog(consoleSink, structured, level, message, fields) {
|
|
148
|
+
if (structured) {
|
|
149
|
+
structured.log({ level, message, fields });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (fields === void 0) consoleSink[level](message);
|
|
153
|
+
else consoleSink[level](message, fields);
|
|
154
|
+
}
|
|
104
155
|
var CapsuleAgent = class {
|
|
105
156
|
constructor(options) {
|
|
106
157
|
this.options = options;
|
|
107
158
|
this.client = new AgentApiClient(options.backendUrl);
|
|
108
159
|
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
109
160
|
this.logger = options.logger ?? console;
|
|
161
|
+
this.structuredLogger = options.structuredLogger;
|
|
110
162
|
}
|
|
111
163
|
options;
|
|
112
164
|
client;
|
|
113
165
|
store;
|
|
114
166
|
logger;
|
|
167
|
+
structuredLogger;
|
|
115
168
|
agentId;
|
|
116
169
|
token;
|
|
117
170
|
stopped = true;
|
|
@@ -259,19 +312,35 @@ var CapsuleAgent = class {
|
|
|
259
312
|
return await fn();
|
|
260
313
|
} catch (e) {
|
|
261
314
|
last = e;
|
|
262
|
-
if (e instanceof
|
|
315
|
+
if (e instanceof NetworkError) {
|
|
316
|
+
} else if (e instanceof AgentApiError && e.status >= 500) {
|
|
317
|
+
} else {
|
|
318
|
+
throw e;
|
|
319
|
+
}
|
|
263
320
|
await sleep(Math.min(1e3 * 2 ** i, 15e3));
|
|
264
321
|
}
|
|
265
322
|
}
|
|
266
323
|
throw last;
|
|
267
324
|
}
|
|
268
325
|
log(level, msg, obj) {
|
|
269
|
-
|
|
326
|
+
const fields = obj === void 0 ? void 0 : this.normalizeFields(obj);
|
|
327
|
+
const redacted = fields ? redact(fields) : void 0;
|
|
328
|
+
emitLog(this.logger, this.structuredLogger, level, msg, redacted);
|
|
329
|
+
}
|
|
330
|
+
normalizeFields(obj) {
|
|
331
|
+
if (obj instanceof Error) {
|
|
332
|
+
return { error: { message: obj.message, name: obj.name, stack: obj.stack } };
|
|
333
|
+
}
|
|
334
|
+
if (typeof obj === "object" && obj !== null) return obj;
|
|
335
|
+
return { value: obj };
|
|
270
336
|
}
|
|
271
337
|
};
|
|
272
338
|
export {
|
|
273
339
|
AgentApiClient,
|
|
274
340
|
AgentApiError,
|
|
341
|
+
AgentAuthError,
|
|
275
342
|
CapsuleAgent,
|
|
276
|
-
FileTokenStore
|
|
343
|
+
FileTokenStore,
|
|
344
|
+
NetworkError,
|
|
345
|
+
RegistrationError
|
|
277
346
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtrape/capsule-agent-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Node.js Agent SDK for embedding Capsule Services into the Opstage governance loop.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xtrape",
|
|
@@ -35,15 +35,8 @@
|
|
|
35
35
|
"LICENSE",
|
|
36
36
|
"NOTICE"
|
|
37
37
|
],
|
|
38
|
-
"scripts": {
|
|
39
|
-
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
40
|
-
"prepack": "pnpm build",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"typecheck": "tsc --noEmit",
|
|
43
|
-
"lint": "tsc --noEmit"
|
|
44
|
-
},
|
|
45
38
|
"dependencies": {
|
|
46
|
-
"@xtrape/capsule-contracts-node": "^0.
|
|
39
|
+
"@xtrape/capsule-contracts-node": "^0.2.0"
|
|
47
40
|
},
|
|
48
41
|
"devDependencies": {
|
|
49
42
|
"tsup": "^8.3.5",
|
|
@@ -58,5 +51,11 @@
|
|
|
58
51
|
"registry": "https://registry.npmjs.org/",
|
|
59
52
|
"access": "public"
|
|
60
53
|
},
|
|
61
|
-
"license": "Apache-2.0"
|
|
62
|
-
|
|
54
|
+
"license": "Apache-2.0",
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"lint": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|