@xtrape/capsule-agent-node 0.1.0-public-review.0 → 0.1.0-public-review.1

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 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",
@@ -300,6 +309,52 @@ Core methods:
300
309
  Use matching minor versions across CE, Agent SDK, and Contracts during Public
301
310
  Review and Public Preview. The wire protocol may still change before `v1.0`.
302
311
 
312
+ ## Troubleshooting
313
+
314
+ ### Registration fails with `401 / REGISTRATION_TOKEN_INVALID`
315
+
316
+ The token is single-use and short-lived.
317
+
318
+ - Check the token has not already been consumed by an earlier successful start
319
+ (registration tokens flip to `USED` on first use).
320
+ - Check the token has not expired. Operators set `expiresInSeconds` when
321
+ creating the token; the default in CE is short.
322
+ - Check the token has not been revoked from the Opstage console.
323
+ - Re-create a fresh token in the console and start the agent with it.
324
+
325
+ ### Agent reports `ECONNREFUSED` / cannot reach backend
326
+
327
+ - Confirm `OPSTAGE_BACKEND_URL` resolves from inside the container or host
328
+ where the agent runs. The default of `http://localhost:8080` only works if
329
+ the agent runs on the same host as Opstage.
330
+ - If you sit behind a reverse proxy, point `backendUrl` at the proxy URL —
331
+ not the internal Opstage container address.
332
+ - Outbound HTTPS through corporate proxies: respect `HTTPS_PROXY` /
333
+ `NO_PROXY` env vars (`undici` honors them via `setGlobalDispatcher`).
334
+
335
+ ### Agent token file is rejected on second start (`401 / UNAUTHORIZED`)
336
+
337
+ - File permissions: the agent process must be able to read
338
+ `tokenStore.file`. Run `chmod 600` (owner read/write) and ensure the
339
+ process owner matches.
340
+ - File contents: the SDK writes `<agentId>:<agentToken>` plaintext. If the
341
+ file exists but contains anything else, delete it and re-register with a
342
+ fresh registration token.
343
+ - Token revocation: an operator may have revoked the agent or the agent
344
+ token from the console. Inspect the agent's status in Opstage; if it shows
345
+ `REVOKED` or `DISABLED`, that's the cause.
346
+
347
+ ### Heartbeats succeed but action results never arrive
348
+
349
+ - Confirm the action `name` registered with `agent.action({ name: "..." })`
350
+ matches the `actionName` the backend dispatches in the command. The SDK
351
+ reports `ACTION_HANDLER_NOT_FOUND` if they differ.
352
+ - Confirm `start()` is awaited and not called repeatedly — multiple
353
+ `CapsuleAgent` instances on the same host with the same agent code will
354
+ fight over heartbeats and command polls.
355
+ - Check `commandPollMs` / `commandPollIntervalSeconds` is not absurdly
356
+ large; default is 5s.
357
+
303
358
  ## Documentation
304
359
 
305
360
  - Site: https://xtrape-com.github.io/xtrape-capsule-site/
package/dist/index.cjs CHANGED
@@ -78,13 +78,17 @@ var AgentApiClient = class {
78
78
  var import_promises = require("fs/promises");
79
79
  var import_node_path = require("path");
80
80
  var FileTokenStore = class {
81
- constructor(filePath = "./data/agent-token.json") {
81
+ constructor(filePath = "./data/agent-token.txt") {
82
82
  this.filePath = filePath;
83
83
  }
84
84
  filePath;
85
85
  async load() {
86
86
  try {
87
- const parsed = JSON.parse(await (0, import_promises.readFile)(this.filePath, "utf8"));
87
+ const raw = await (0, import_promises.readFile)(this.filePath, "utf8");
88
+ const trimmed = raw.trim();
89
+ if (!trimmed) return null;
90
+ if (!trimmed.startsWith("{")) return trimmed;
91
+ const parsed = JSON.parse(trimmed);
88
92
  if (typeof parsed.agentId === "string" && typeof parsed.agentToken === "string") return `${parsed.agentId}:${parsed.agentToken}`;
89
93
  return typeof parsed.agentToken === "string" ? parsed.agentToken : null;
90
94
  } catch (e) {
@@ -93,9 +97,8 @@ var FileTokenStore = class {
93
97
  }
94
98
  }
95
99
  async save(token) {
96
- const [agentId, agentToken] = token.includes(":") ? token.split(":", 2) : [void 0, token];
97
100
  await (0, import_promises.mkdir)((0, import_node_path.dirname)(this.filePath), { recursive: true });
98
- await (0, import_promises.writeFile)(this.filePath, JSON.stringify({ ...agentId ? { agentId } : {}, agentToken, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 });
101
+ await (0, import_promises.writeFile)(this.filePath, token, { mode: 384 });
99
102
  try {
100
103
  await (0, import_promises.chmod)(this.filePath, 384);
101
104
  } catch {
@@ -107,11 +110,22 @@ var FileTokenStore = class {
107
110
  };
108
111
 
109
112
  // src/security/redaction.ts
113
+ var SENSITIVE_KEY = /token|secret|password|cookie|authorization|api[-_]?key\b|generatedKey|rawKey|plainTextKey/i;
114
+ var TOKEN_PATTERN = /opstage_[a-z]+_[A-Za-z0-9_-]+/g;
110
115
  function redact(value) {
111
- if (typeof value === "string") return value.replace(/opstage_(reg|agent)_[A-Za-z0-9_-]+/g, "opstage_$1_[REDACTED]");
116
+ if (typeof value === "string") {
117
+ return value.replace(TOKEN_PATTERN, (match) => {
118
+ const prefixEnd = match.lastIndexOf("_");
119
+ return `${match.slice(0, prefixEnd + 1)}[REDACTED]`;
120
+ });
121
+ }
112
122
  if (!value || typeof value !== "object") return value;
113
123
  if (Array.isArray(value)) return value.map(redact);
114
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [/token|secret|password|authorization/i.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]]));
124
+ return Object.fromEntries(
125
+ Object.entries(value).map(
126
+ ([k, v]) => SENSITIVE_KEY.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]
127
+ )
128
+ );
115
129
  }
116
130
 
117
131
  // src/capsule-agent.ts
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,7 @@ 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
- action?: Record<string, unknown>;
12
- initialPayload?: Record<string, unknown>;
13
- currentState?: Record<string, unknown>;
14
- } | Record<string, unknown>> | {
15
- action?: Record<string, unknown>;
16
- initialPayload?: Record<string, unknown>;
17
- currentState?: Record<string, unknown>;
18
- } | Record<string, unknown>;
17
+ type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
19
18
  interface CapsuleAgentOptions {
20
19
  backendUrl: string;
21
20
  registrationToken?: string;
@@ -25,14 +24,14 @@ interface CapsuleAgentOptions {
25
24
  agent?: {
26
25
  code: string;
27
26
  name?: string;
28
- runtime?: string;
27
+ runtime?: RuntimeKind;
29
28
  };
30
29
  service: {
31
30
  code: string;
32
31
  name: string;
33
32
  description?: string;
34
33
  version: string;
35
- runtime: "nodejs" | "java" | "python" | "go" | "other";
34
+ runtime: RuntimeKind;
36
35
  manifest?: Record<string, unknown>;
37
36
  };
38
37
  intervals?: {
@@ -136,4 +135,4 @@ declare class AgentApiError extends Error {
136
135
  constructor(status: number, message: string, body?: unknown | undefined);
137
136
  }
138
137
 
139
- export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type ServiceSnapshot, type TokenStore };
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 };
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,7 @@ 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
- action?: Record<string, unknown>;
12
- initialPayload?: Record<string, unknown>;
13
- currentState?: Record<string, unknown>;
14
- } | Record<string, unknown>> | {
15
- action?: Record<string, unknown>;
16
- initialPayload?: Record<string, unknown>;
17
- currentState?: Record<string, unknown>;
18
- } | Record<string, unknown>;
17
+ type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
19
18
  interface CapsuleAgentOptions {
20
19
  backendUrl: string;
21
20
  registrationToken?: string;
@@ -25,14 +24,14 @@ interface CapsuleAgentOptions {
25
24
  agent?: {
26
25
  code: string;
27
26
  name?: string;
28
- runtime?: string;
27
+ runtime?: RuntimeKind;
29
28
  };
30
29
  service: {
31
30
  code: string;
32
31
  name: string;
33
32
  description?: string;
34
33
  version: string;
35
- runtime: "nodejs" | "java" | "python" | "go" | "other";
34
+ runtime: RuntimeKind;
36
35
  manifest?: Record<string, unknown>;
37
36
  };
38
37
  intervals?: {
@@ -136,4 +135,4 @@ declare class AgentApiError extends Error {
136
135
  constructor(status: number, message: string, body?: unknown | undefined);
137
136
  }
138
137
 
139
- export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type ServiceSnapshot, type TokenStore };
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 };
package/dist/index.js CHANGED
@@ -49,13 +49,17 @@ var AgentApiClient = class {
49
49
  import { mkdir, readFile, rm, writeFile, chmod } from "fs/promises";
50
50
  import { dirname } from "path";
51
51
  var FileTokenStore = class {
52
- constructor(filePath = "./data/agent-token.json") {
52
+ constructor(filePath = "./data/agent-token.txt") {
53
53
  this.filePath = filePath;
54
54
  }
55
55
  filePath;
56
56
  async load() {
57
57
  try {
58
- const parsed = JSON.parse(await readFile(this.filePath, "utf8"));
58
+ const raw = await readFile(this.filePath, "utf8");
59
+ const trimmed = raw.trim();
60
+ if (!trimmed) return null;
61
+ if (!trimmed.startsWith("{")) return trimmed;
62
+ const parsed = JSON.parse(trimmed);
59
63
  if (typeof parsed.agentId === "string" && typeof parsed.agentToken === "string") return `${parsed.agentId}:${parsed.agentToken}`;
60
64
  return typeof parsed.agentToken === "string" ? parsed.agentToken : null;
61
65
  } catch (e) {
@@ -64,9 +68,8 @@ var FileTokenStore = class {
64
68
  }
65
69
  }
66
70
  async save(token) {
67
- const [agentId, agentToken] = token.includes(":") ? token.split(":", 2) : [void 0, token];
68
71
  await mkdir(dirname(this.filePath), { recursive: true });
69
- await writeFile(this.filePath, JSON.stringify({ ...agentId ? { agentId } : {}, agentToken, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 });
72
+ await writeFile(this.filePath, token, { mode: 384 });
70
73
  try {
71
74
  await chmod(this.filePath, 384);
72
75
  } catch {
@@ -78,11 +81,22 @@ var FileTokenStore = class {
78
81
  };
79
82
 
80
83
  // src/security/redaction.ts
84
+ var SENSITIVE_KEY = /token|secret|password|cookie|authorization|api[-_]?key\b|generatedKey|rawKey|plainTextKey/i;
85
+ var TOKEN_PATTERN = /opstage_[a-z]+_[A-Za-z0-9_-]+/g;
81
86
  function redact(value) {
82
- if (typeof value === "string") return value.replace(/opstage_(reg|agent)_[A-Za-z0-9_-]+/g, "opstage_$1_[REDACTED]");
87
+ if (typeof value === "string") {
88
+ return value.replace(TOKEN_PATTERN, (match) => {
89
+ const prefixEnd = match.lastIndexOf("_");
90
+ return `${match.slice(0, prefixEnd + 1)}[REDACTED]`;
91
+ });
92
+ }
83
93
  if (!value || typeof value !== "object") return value;
84
94
  if (Array.isArray(value)) return value.map(redact);
85
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [/token|secret|password|authorization/i.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]]));
95
+ return Object.fromEntries(
96
+ Object.entries(value).map(
97
+ ([k, v]) => SENSITIVE_KEY.test(k) ? [k, "[REDACTED]"] : [k, redact(v)]
98
+ )
99
+ );
86
100
  }
87
101
 
88
102
  // src/capsule-agent.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtrape/capsule-agent-node",
3
- "version": "0.1.0-public-review.0",
3
+ "version": "0.1.0-public-review.1",
4
4
  "description": "Node.js Agent SDK for embedding Capsule Services into the Opstage governance loop.",
5
5
  "keywords": [
6
6
  "xtrape",