@xtrape/capsule-agent-node 0.1.0-public-review.0 → 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 +141 -1
- package/dist/index.cjs +100 -14
- package/dist/index.d.cts +93 -20
- package/dist/index.d.ts +93 -20
- package/dist/index.js +96 -13
- package/package.json +10 -11
package/README.md
CHANGED
|
@@ -46,6 +46,15 @@ const agent = new CapsuleAgent({
|
|
|
46
46
|
backendUrl: process.env.OPSTAGE_BACKEND_URL ?? "http://localhost:8080",
|
|
47
47
|
registrationToken: process.env.OPSTAGE_REGISTRATION_TOKEN,
|
|
48
48
|
tokenStore: { file: "./data/agent-token.txt" },
|
|
49
|
+
// Optional. If omitted, the SDK derives agent identity from `service`.
|
|
50
|
+
// Provide it explicitly when one Agent owns multiple Capsule Services on
|
|
51
|
+
// the same host, or when you want a stable agent code that survives
|
|
52
|
+
// service renames.
|
|
53
|
+
agent: {
|
|
54
|
+
code: "hello-capsule-agent",
|
|
55
|
+
name: "Hello Capsule Agent",
|
|
56
|
+
runtime: "nodejs",
|
|
57
|
+
},
|
|
49
58
|
service: {
|
|
50
59
|
code: "hello-capsule",
|
|
51
60
|
name: "Hello Capsule",
|
|
@@ -276,7 +285,8 @@ Main exports:
|
|
|
276
285
|
- `CapsuleAgent`
|
|
277
286
|
- `FileTokenStore`
|
|
278
287
|
- `AgentApiClient`
|
|
279
|
-
- `AgentApiError`
|
|
288
|
+
- `AgentApiError`, `RegistrationError`, `AgentAuthError`, `NetworkError`
|
|
289
|
+
- `AgentLogLevel`, `AgentLogRecord`, `StructuredLogger`, `ConsoleLogger` (types)
|
|
280
290
|
- SDK option and provider types from `types.ts`
|
|
281
291
|
|
|
282
292
|
Core methods:
|
|
@@ -291,6 +301,90 @@ Core methods:
|
|
|
291
301
|
| `.stop()` | Stops background loops. |
|
|
292
302
|
| `.runHealth()` | Runs the configured health provider. |
|
|
293
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
|
+
|
|
294
388
|
## Version Compatibility
|
|
295
389
|
|
|
296
390
|
| Package | Compatible with |
|
|
@@ -300,6 +394,52 @@ Core methods:
|
|
|
300
394
|
Use matching minor versions across CE, Agent SDK, and Contracts during Public
|
|
301
395
|
Review and Public Preview. The wire protocol may still change before `v1.0`.
|
|
302
396
|
|
|
397
|
+
## Troubleshooting
|
|
398
|
+
|
|
399
|
+
### Registration fails with `401 / REGISTRATION_TOKEN_INVALID`
|
|
400
|
+
|
|
401
|
+
The token is single-use and short-lived.
|
|
402
|
+
|
|
403
|
+
- Check the token has not already been consumed by an earlier successful start
|
|
404
|
+
(registration tokens flip to `USED` on first use).
|
|
405
|
+
- Check the token has not expired. Operators set `expiresInSeconds` when
|
|
406
|
+
creating the token; the default in CE is short.
|
|
407
|
+
- Check the token has not been revoked from the Opstage console.
|
|
408
|
+
- Re-create a fresh token in the console and start the agent with it.
|
|
409
|
+
|
|
410
|
+
### Agent reports `ECONNREFUSED` / cannot reach backend
|
|
411
|
+
|
|
412
|
+
- Confirm `OPSTAGE_BACKEND_URL` resolves from inside the container or host
|
|
413
|
+
where the agent runs. The default of `http://localhost:8080` only works if
|
|
414
|
+
the agent runs on the same host as Opstage.
|
|
415
|
+
- If you sit behind a reverse proxy, point `backendUrl` at the proxy URL —
|
|
416
|
+
not the internal Opstage container address.
|
|
417
|
+
- Outbound HTTPS through corporate proxies: respect `HTTPS_PROXY` /
|
|
418
|
+
`NO_PROXY` env vars (`undici` honors them via `setGlobalDispatcher`).
|
|
419
|
+
|
|
420
|
+
### Agent token file is rejected on second start (`401 / UNAUTHORIZED`)
|
|
421
|
+
|
|
422
|
+
- File permissions: the agent process must be able to read
|
|
423
|
+
`tokenStore.file`. Run `chmod 600` (owner read/write) and ensure the
|
|
424
|
+
process owner matches.
|
|
425
|
+
- File contents: the SDK writes `<agentId>:<agentToken>` plaintext. If the
|
|
426
|
+
file exists but contains anything else, delete it and re-register with a
|
|
427
|
+
fresh registration token.
|
|
428
|
+
- Token revocation: an operator may have revoked the agent or the agent
|
|
429
|
+
token from the console. Inspect the agent's status in Opstage; if it shows
|
|
430
|
+
`REVOKED` or `DISABLED`, that's the cause.
|
|
431
|
+
|
|
432
|
+
### Heartbeats succeed but action results never arrive
|
|
433
|
+
|
|
434
|
+
- Confirm the action `name` registered with `agent.action({ name: "..." })`
|
|
435
|
+
matches the `actionName` the backend dispatches in the command. The SDK
|
|
436
|
+
reports `ACTION_HANDLER_NOT_FOUND` if they differ.
|
|
437
|
+
- Confirm `start()` is awaited and not called repeatedly — multiple
|
|
438
|
+
`CapsuleAgent` instances on the same host with the same agent code will
|
|
439
|
+
fight over heartbeats and command polls.
|
|
440
|
+
- Check `commandPollMs` / `commandPollIntervalSeconds` is not absurdly
|
|
441
|
+
large; default is 5s.
|
|
442
|
+
|
|
303
443
|
## Documentation
|
|
304
444
|
|
|
305
445
|
- Site: https://xtrape-com.github.io/xtrape-capsule-site/
|
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) {
|
|
@@ -78,13 +124,17 @@ var AgentApiClient = class {
|
|
|
78
124
|
var import_promises = require("fs/promises");
|
|
79
125
|
var import_node_path = require("path");
|
|
80
126
|
var FileTokenStore = class {
|
|
81
|
-
constructor(filePath = "./data/agent-token.
|
|
127
|
+
constructor(filePath = "./data/agent-token.txt") {
|
|
82
128
|
this.filePath = filePath;
|
|
83
129
|
}
|
|
84
130
|
filePath;
|
|
85
131
|
async load() {
|
|
86
132
|
try {
|
|
87
|
-
const
|
|
133
|
+
const raw = await (0, import_promises.readFile)(this.filePath, "utf8");
|
|
134
|
+
const trimmed = raw.trim();
|
|
135
|
+
if (!trimmed) return null;
|
|
136
|
+
if (!trimmed.startsWith("{")) return trimmed;
|
|
137
|
+
const parsed = JSON.parse(trimmed);
|
|
88
138
|
if (typeof parsed.agentId === "string" && typeof parsed.agentToken === "string") return `${parsed.agentId}:${parsed.agentToken}`;
|
|
89
139
|
return typeof parsed.agentToken === "string" ? parsed.agentToken : null;
|
|
90
140
|
} catch (e) {
|
|
@@ -93,9 +143,8 @@ var FileTokenStore = class {
|
|
|
93
143
|
}
|
|
94
144
|
}
|
|
95
145
|
async save(token) {
|
|
96
|
-
const [agentId, agentToken] = token.includes(":") ? token.split(":", 2) : [void 0, token];
|
|
97
146
|
await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.filePath), { recursive: true });
|
|
98
|
-
await (0, import_promises.writeFile)(this.filePath,
|
|
147
|
+
await (0, import_promises.writeFile)(this.filePath, token, { mode: 384 });
|
|
99
148
|
try {
|
|
100
149
|
await (0, import_promises.chmod)(this.filePath, 384);
|
|
101
150
|
} catch {
|
|
@@ -107,26 +156,47 @@ var FileTokenStore = class {
|
|
|
107
156
|
};
|
|
108
157
|
|
|
109
158
|
// src/security/redaction.ts
|
|
159
|
+
var SENSITIVE_KEY = /token|secret|password|cookie|authorization|api[-_]?key\b|generatedKey|rawKey|plainTextKey/i;
|
|
160
|
+
var TOKEN_PATTERN = /opstage_[a-z]+_[A-Za-z0-9_-]+/g;
|
|
110
161
|
function redact(value) {
|
|
111
|
-
if (typeof value === "string")
|
|
162
|
+
if (typeof value === "string") {
|
|
163
|
+
return value.replace(TOKEN_PATTERN, (match) => {
|
|
164
|
+
const prefixEnd = match.lastIndexOf("_");
|
|
165
|
+
return `${match.slice(0, prefixEnd + 1)}[REDACTED]`;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
112
168
|
if (!value || typeof value !== "object") return value;
|
|
113
169
|
if (Array.isArray(value)) return value.map(redact);
|
|
114
|
-
return Object.fromEntries(
|
|
170
|
+
return Object.fromEntries(
|
|
171
|
+
Object.entries(value).map(
|
|
172
|
+
([k, v]) => SENSITIVE_KEY.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]
|
|
173
|
+
)
|
|
174
|
+
);
|
|
115
175
|
}
|
|
116
176
|
|
|
117
177
|
// src/capsule-agent.ts
|
|
118
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
|
+
}
|
|
119
187
|
var CapsuleAgent = class {
|
|
120
188
|
constructor(options) {
|
|
121
189
|
this.options = options;
|
|
122
190
|
this.client = new AgentApiClient(options.backendUrl);
|
|
123
191
|
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
124
192
|
this.logger = options.logger ?? console;
|
|
193
|
+
this.structuredLogger = options.structuredLogger;
|
|
125
194
|
}
|
|
126
195
|
options;
|
|
127
196
|
client;
|
|
128
197
|
store;
|
|
129
198
|
logger;
|
|
199
|
+
structuredLogger;
|
|
130
200
|
agentId;
|
|
131
201
|
token;
|
|
132
202
|
stopped = true;
|
|
@@ -274,20 +344,36 @@ var CapsuleAgent = class {
|
|
|
274
344
|
return await fn();
|
|
275
345
|
} catch (e) {
|
|
276
346
|
last = e;
|
|
277
|
-
if (e instanceof
|
|
347
|
+
if (e instanceof NetworkError) {
|
|
348
|
+
} else if (e instanceof AgentApiError && e.status >= 500) {
|
|
349
|
+
} else {
|
|
350
|
+
throw e;
|
|
351
|
+
}
|
|
278
352
|
await sleep(Math.min(1e3 * 2 ** i, 15e3));
|
|
279
353
|
}
|
|
280
354
|
}
|
|
281
355
|
throw last;
|
|
282
356
|
}
|
|
283
357
|
log(level, msg, obj) {
|
|
284
|
-
|
|
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 };
|
|
285
368
|
}
|
|
286
369
|
};
|
|
287
370
|
// Annotate the CommonJS export names for ESM import in node:
|
|
288
371
|
0 && (module.exports = {
|
|
289
372
|
AgentApiClient,
|
|
290
373
|
AgentApiError,
|
|
374
|
+
AgentAuthError,
|
|
291
375
|
CapsuleAgent,
|
|
292
|
-
FileTokenStore
|
|
376
|
+
FileTokenStore,
|
|
377
|
+
NetworkError,
|
|
378
|
+
RegistrationError
|
|
293
379
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
|
|
2
2
|
|
|
3
|
+
type RuntimeKind = "nodejs" | "java" | "python" | "go" | "other";
|
|
4
|
+
type ActionPrepareResult = {
|
|
5
|
+
action?: Record<string, unknown>;
|
|
6
|
+
inputSchema?: Record<string, unknown>;
|
|
7
|
+
initialPayload?: Record<string, unknown>;
|
|
8
|
+
currentState?: Record<string, unknown>;
|
|
9
|
+
};
|
|
3
10
|
type AgentMode = "embedded";
|
|
4
11
|
type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
5
12
|
success?: boolean;
|
|
@@ -7,15 +14,37 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
|
7
14
|
data?: Record<string, unknown>;
|
|
8
15
|
error?: Record<string, unknown>;
|
|
9
16
|
} | Record<string, unknown>>;
|
|
10
|
-
type ActionPrepareHandler = () => Promise<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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">;
|
|
19
48
|
interface CapsuleAgentOptions {
|
|
20
49
|
backendUrl: string;
|
|
21
50
|
registrationToken?: string;
|
|
@@ -25,14 +54,14 @@ interface CapsuleAgentOptions {
|
|
|
25
54
|
agent?: {
|
|
26
55
|
code: string;
|
|
27
56
|
name?: string;
|
|
28
|
-
runtime?:
|
|
57
|
+
runtime?: RuntimeKind;
|
|
29
58
|
};
|
|
30
59
|
service: {
|
|
31
60
|
code: string;
|
|
32
61
|
name: string;
|
|
33
62
|
description?: string;
|
|
34
63
|
version: string;
|
|
35
|
-
runtime:
|
|
64
|
+
runtime: RuntimeKind;
|
|
36
65
|
manifest?: Record<string, unknown>;
|
|
37
66
|
};
|
|
38
67
|
intervals?: {
|
|
@@ -43,7 +72,14 @@ interface CapsuleAgentOptions {
|
|
|
43
72
|
heartbeatIntervalSeconds?: number;
|
|
44
73
|
commandPollIntervalSeconds?: number;
|
|
45
74
|
autoStartLoops?: boolean;
|
|
46
|
-
|
|
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;
|
|
47
83
|
failOnStartError?: boolean;
|
|
48
84
|
}
|
|
49
85
|
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
@@ -64,6 +100,7 @@ declare class CapsuleAgent {
|
|
|
64
100
|
private readonly client;
|
|
65
101
|
private readonly store;
|
|
66
102
|
private readonly logger;
|
|
103
|
+
private readonly structuredLogger?;
|
|
67
104
|
private agentId?;
|
|
68
105
|
private token?;
|
|
69
106
|
private stopped;
|
|
@@ -90,6 +127,7 @@ declare class CapsuleAgent {
|
|
|
90
127
|
private withTimeout;
|
|
91
128
|
private retry;
|
|
92
129
|
private log;
|
|
130
|
+
private normalizeFields;
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
declare class FileTokenStore implements TokenStore {
|
|
@@ -100,6 +138,47 @@ declare class FileTokenStore implements TokenStore {
|
|
|
100
138
|
clear(): Promise<void>;
|
|
101
139
|
}
|
|
102
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
|
+
|
|
103
182
|
declare class AgentApiClient {
|
|
104
183
|
private readonly backendUrl;
|
|
105
184
|
constructor(backendUrl: string);
|
|
@@ -114,7 +193,7 @@ declare class AgentApiClient {
|
|
|
114
193
|
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
115
194
|
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
116
195
|
pollCommands(agentId: string, token: string): Promise<{
|
|
117
|
-
type: "ACTION_PREPARE" | "ACTION_EXECUTE"
|
|
196
|
+
type: "ACTION_PREPARE" | "ACTION_EXECUTE";
|
|
118
197
|
id: string;
|
|
119
198
|
createdAt: string;
|
|
120
199
|
status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
|
|
@@ -130,10 +209,4 @@ declare class AgentApiClient {
|
|
|
130
209
|
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
131
210
|
}
|
|
132
211
|
|
|
133
|
-
|
|
134
|
-
readonly status: number;
|
|
135
|
-
readonly body?: unknown | undefined;
|
|
136
|
-
constructor(status: number, message: string, body?: unknown | undefined);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, 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
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
|
|
2
2
|
|
|
3
|
+
type RuntimeKind = "nodejs" | "java" | "python" | "go" | "other";
|
|
4
|
+
type ActionPrepareResult = {
|
|
5
|
+
action?: Record<string, unknown>;
|
|
6
|
+
inputSchema?: Record<string, unknown>;
|
|
7
|
+
initialPayload?: Record<string, unknown>;
|
|
8
|
+
currentState?: Record<string, unknown>;
|
|
9
|
+
};
|
|
3
10
|
type AgentMode = "embedded";
|
|
4
11
|
type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
5
12
|
success?: boolean;
|
|
@@ -7,15 +14,37 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
|
7
14
|
data?: Record<string, unknown>;
|
|
8
15
|
error?: Record<string, unknown>;
|
|
9
16
|
} | Record<string, unknown>>;
|
|
10
|
-
type ActionPrepareHandler = () => Promise<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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">;
|
|
19
48
|
interface CapsuleAgentOptions {
|
|
20
49
|
backendUrl: string;
|
|
21
50
|
registrationToken?: string;
|
|
@@ -25,14 +54,14 @@ interface CapsuleAgentOptions {
|
|
|
25
54
|
agent?: {
|
|
26
55
|
code: string;
|
|
27
56
|
name?: string;
|
|
28
|
-
runtime?:
|
|
57
|
+
runtime?: RuntimeKind;
|
|
29
58
|
};
|
|
30
59
|
service: {
|
|
31
60
|
code: string;
|
|
32
61
|
name: string;
|
|
33
62
|
description?: string;
|
|
34
63
|
version: string;
|
|
35
|
-
runtime:
|
|
64
|
+
runtime: RuntimeKind;
|
|
36
65
|
manifest?: Record<string, unknown>;
|
|
37
66
|
};
|
|
38
67
|
intervals?: {
|
|
@@ -43,7 +72,14 @@ interface CapsuleAgentOptions {
|
|
|
43
72
|
heartbeatIntervalSeconds?: number;
|
|
44
73
|
commandPollIntervalSeconds?: number;
|
|
45
74
|
autoStartLoops?: boolean;
|
|
46
|
-
|
|
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;
|
|
47
83
|
failOnStartError?: boolean;
|
|
48
84
|
}
|
|
49
85
|
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
@@ -64,6 +100,7 @@ declare class CapsuleAgent {
|
|
|
64
100
|
private readonly client;
|
|
65
101
|
private readonly store;
|
|
66
102
|
private readonly logger;
|
|
103
|
+
private readonly structuredLogger?;
|
|
67
104
|
private agentId?;
|
|
68
105
|
private token?;
|
|
69
106
|
private stopped;
|
|
@@ -90,6 +127,7 @@ declare class CapsuleAgent {
|
|
|
90
127
|
private withTimeout;
|
|
91
128
|
private retry;
|
|
92
129
|
private log;
|
|
130
|
+
private normalizeFields;
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
declare class FileTokenStore implements TokenStore {
|
|
@@ -100,6 +138,47 @@ declare class FileTokenStore implements TokenStore {
|
|
|
100
138
|
clear(): Promise<void>;
|
|
101
139
|
}
|
|
102
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
|
+
|
|
103
182
|
declare class AgentApiClient {
|
|
104
183
|
private readonly backendUrl;
|
|
105
184
|
constructor(backendUrl: string);
|
|
@@ -114,7 +193,7 @@ declare class AgentApiClient {
|
|
|
114
193
|
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
115
194
|
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
116
195
|
pollCommands(agentId: string, token: string): Promise<{
|
|
117
|
-
type: "ACTION_PREPARE" | "ACTION_EXECUTE"
|
|
196
|
+
type: "ACTION_PREPARE" | "ACTION_EXECUTE";
|
|
118
197
|
id: string;
|
|
119
198
|
createdAt: string;
|
|
120
199
|
status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
|
|
@@ -130,10 +209,4 @@ declare class AgentApiClient {
|
|
|
130
209
|
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
131
210
|
}
|
|
132
211
|
|
|
133
|
-
|
|
134
|
-
readonly status: number;
|
|
135
|
-
readonly body?: unknown | undefined;
|
|
136
|
-
constructor(status: number, message: string, body?: unknown | undefined);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, 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) {
|
|
@@ -49,13 +92,17 @@ var AgentApiClient = class {
|
|
|
49
92
|
import { mkdir, readFile, rm, writeFile, chmod } from "fs/promises";
|
|
50
93
|
import { dirname } from "path";
|
|
51
94
|
var FileTokenStore = class {
|
|
52
|
-
constructor(filePath = "./data/agent-token.
|
|
95
|
+
constructor(filePath = "./data/agent-token.txt") {
|
|
53
96
|
this.filePath = filePath;
|
|
54
97
|
}
|
|
55
98
|
filePath;
|
|
56
99
|
async load() {
|
|
57
100
|
try {
|
|
58
|
-
const
|
|
101
|
+
const raw = await readFile(this.filePath, "utf8");
|
|
102
|
+
const trimmed = raw.trim();
|
|
103
|
+
if (!trimmed) return null;
|
|
104
|
+
if (!trimmed.startsWith("{")) return trimmed;
|
|
105
|
+
const parsed = JSON.parse(trimmed);
|
|
59
106
|
if (typeof parsed.agentId === "string" && typeof parsed.agentToken === "string") return `${parsed.agentId}:${parsed.agentToken}`;
|
|
60
107
|
return typeof parsed.agentToken === "string" ? parsed.agentToken : null;
|
|
61
108
|
} catch (e) {
|
|
@@ -64,9 +111,8 @@ var FileTokenStore = class {
|
|
|
64
111
|
}
|
|
65
112
|
}
|
|
66
113
|
async save(token) {
|
|
67
|
-
const [agentId, agentToken] = token.includes(":") ? token.split(":", 2) : [void 0, token];
|
|
68
114
|
await mkdir(dirname(this.filePath), { recursive: true });
|
|
69
|
-
await writeFile(this.filePath,
|
|
115
|
+
await writeFile(this.filePath, token, { mode: 384 });
|
|
70
116
|
try {
|
|
71
117
|
await chmod(this.filePath, 384);
|
|
72
118
|
} catch {
|
|
@@ -78,26 +124,47 @@ var FileTokenStore = class {
|
|
|
78
124
|
};
|
|
79
125
|
|
|
80
126
|
// src/security/redaction.ts
|
|
127
|
+
var SENSITIVE_KEY = /token|secret|password|cookie|authorization|api[-_]?key\b|generatedKey|rawKey|plainTextKey/i;
|
|
128
|
+
var TOKEN_PATTERN = /opstage_[a-z]+_[A-Za-z0-9_-]+/g;
|
|
81
129
|
function redact(value) {
|
|
82
|
-
if (typeof value === "string")
|
|
130
|
+
if (typeof value === "string") {
|
|
131
|
+
return value.replace(TOKEN_PATTERN, (match) => {
|
|
132
|
+
const prefixEnd = match.lastIndexOf("_");
|
|
133
|
+
return `${match.slice(0, prefixEnd + 1)}[REDACTED]`;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
83
136
|
if (!value || typeof value !== "object") return value;
|
|
84
137
|
if (Array.isArray(value)) return value.map(redact);
|
|
85
|
-
return Object.fromEntries(
|
|
138
|
+
return Object.fromEntries(
|
|
139
|
+
Object.entries(value).map(
|
|
140
|
+
([k, v]) => SENSITIVE_KEY.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]
|
|
141
|
+
)
|
|
142
|
+
);
|
|
86
143
|
}
|
|
87
144
|
|
|
88
145
|
// src/capsule-agent.ts
|
|
89
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
|
+
}
|
|
90
155
|
var CapsuleAgent = class {
|
|
91
156
|
constructor(options) {
|
|
92
157
|
this.options = options;
|
|
93
158
|
this.client = new AgentApiClient(options.backendUrl);
|
|
94
159
|
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
95
160
|
this.logger = options.logger ?? console;
|
|
161
|
+
this.structuredLogger = options.structuredLogger;
|
|
96
162
|
}
|
|
97
163
|
options;
|
|
98
164
|
client;
|
|
99
165
|
store;
|
|
100
166
|
logger;
|
|
167
|
+
structuredLogger;
|
|
101
168
|
agentId;
|
|
102
169
|
token;
|
|
103
170
|
stopped = true;
|
|
@@ -245,19 +312,35 @@ var CapsuleAgent = class {
|
|
|
245
312
|
return await fn();
|
|
246
313
|
} catch (e) {
|
|
247
314
|
last = e;
|
|
248
|
-
if (e instanceof
|
|
315
|
+
if (e instanceof NetworkError) {
|
|
316
|
+
} else if (e instanceof AgentApiError && e.status >= 500) {
|
|
317
|
+
} else {
|
|
318
|
+
throw e;
|
|
319
|
+
}
|
|
249
320
|
await sleep(Math.min(1e3 * 2 ** i, 15e3));
|
|
250
321
|
}
|
|
251
322
|
}
|
|
252
323
|
throw last;
|
|
253
324
|
}
|
|
254
325
|
log(level, msg, obj) {
|
|
255
|
-
|
|
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 };
|
|
256
336
|
}
|
|
257
337
|
};
|
|
258
338
|
export {
|
|
259
339
|
AgentApiClient,
|
|
260
340
|
AgentApiError,
|
|
341
|
+
AgentAuthError,
|
|
261
342
|
CapsuleAgent,
|
|
262
|
-
FileTokenStore
|
|
343
|
+
FileTokenStore,
|
|
344
|
+
NetworkError,
|
|
345
|
+
RegistrationError
|
|
263
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
|
+
}
|