@easynet-run/node 0.27.14

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.
Files changed (61) hide show
  1. package/README.md +135 -0
  2. package/native/dendrite-bridge-manifest.json +38 -0
  3. package/native/dendrite-bridge.json +15 -0
  4. package/native/include/axon_dendrite_bridge.h +460 -0
  5. package/native/libaxon_dendrite_bridge.so +0 -0
  6. package/package.json +67 -0
  7. package/runtime/easynet-runtime-rs-0.27.14-x86_64-unknown-linux-gnu.tar.gz +0 -0
  8. package/runtime/runtime-bridge-manifest.json +20 -0
  9. package/runtime/runtime-bridge.json +9 -0
  10. package/src/ability_lifecycle.d.ts +140 -0
  11. package/src/ability_lifecycle.js +525 -0
  12. package/src/capability_request.d.ts +14 -0
  13. package/src/capability_request.js +247 -0
  14. package/src/dendrite_bridge/bridge.d.ts +98 -0
  15. package/src/dendrite_bridge/bridge.js +712 -0
  16. package/src/dendrite_bridge/ffi.d.ts +60 -0
  17. package/src/dendrite_bridge/ffi.js +139 -0
  18. package/src/dendrite_bridge/index.d.ts +3 -0
  19. package/src/dendrite_bridge/index.js +25 -0
  20. package/src/dendrite_bridge/types.d.ts +179 -0
  21. package/src/dendrite_bridge/types.js +23 -0
  22. package/src/dendrite_bridge.d.ts +1 -0
  23. package/src/dendrite_bridge.js +27 -0
  24. package/src/errors.d.ts +83 -0
  25. package/src/errors.js +146 -0
  26. package/src/index.d.ts +55 -0
  27. package/src/index.js +164 -0
  28. package/src/koffi.d.ts +34 -0
  29. package/src/mcp/server.d.ts +29 -0
  30. package/src/mcp/server.js +190 -0
  31. package/src/presets/ability_dispatch/args.d.ts +5 -0
  32. package/src/presets/ability_dispatch/args.js +36 -0
  33. package/src/presets/ability_dispatch/bundle.d.ts +7 -0
  34. package/src/presets/ability_dispatch/bundle.js +102 -0
  35. package/src/presets/ability_dispatch/media.d.ts +6 -0
  36. package/src/presets/ability_dispatch/media.js +48 -0
  37. package/src/presets/ability_dispatch/orchestrator.d.ts +21 -0
  38. package/src/presets/ability_dispatch/orchestrator.js +117 -0
  39. package/src/presets/ability_dispatch/workflow.d.ts +50 -0
  40. package/src/presets/ability_dispatch/workflow.js +333 -0
  41. package/src/presets/ability_dispatch.d.ts +1 -0
  42. package/src/presets/ability_dispatch.js +2 -0
  43. package/src/presets/remote_control/config.d.ts +16 -0
  44. package/src/presets/remote_control/config.js +63 -0
  45. package/src/presets/remote_control/descriptor.d.ts +34 -0
  46. package/src/presets/remote_control/descriptor.js +183 -0
  47. package/src/presets/remote_control/handlers.d.ts +12 -0
  48. package/src/presets/remote_control/handlers.js +279 -0
  49. package/src/presets/remote_control/kit.d.ts +22 -0
  50. package/src/presets/remote_control/kit.js +72 -0
  51. package/src/presets/remote_control/kit.test.js +87 -0
  52. package/src/presets/remote_control/orchestrator.d.ts +28 -0
  53. package/src/presets/remote_control/orchestrator.js +118 -0
  54. package/src/presets/remote_control/specs.d.ts +2 -0
  55. package/src/presets/remote_control/specs.js +152 -0
  56. package/src/presets/remote_control_case.d.ts +7 -0
  57. package/src/presets/remote_control_case.js +3 -0
  58. package/src/receipt.d.ts +46 -0
  59. package/src/receipt.js +98 -0
  60. package/src/tool_adapter.d.ts +90 -0
  61. package/src/tool_adapter.js +169 -0
@@ -0,0 +1,72 @@
1
+ import { ensureRemoteControlNativeLibEnv, ensureNativeLibEnv, loadConfigFromEnv, loadRemoteControlConfigFromEnv, } from "./config.js";
2
+ import { resolveTenant } from "./descriptor.js";
3
+ import { handleCallRemoteTool, handleDeployAbility, handleDeployAbilityPackage, handleDiscoverNodes, handleDisconnectDevice, handleExecuteCommand, handleListRemoteTools, handlePackageAbility, handleUninstallAbility, } from "./handlers.js";
4
+ import { remoteControlToolSpecs } from "./specs.js";
5
+ import { buildOrchestrator } from "./orchestrator.js";
6
+ export class RemoteControlCaseKit {
7
+ config;
8
+ orchestratorFactory;
9
+ constructor(config, orchestratorFactory = buildOrchestrator) {
10
+ this.config = config;
11
+ this.orchestratorFactory = orchestratorFactory;
12
+ }
13
+ static loadConfigFromEnv() {
14
+ return loadConfigFromEnv();
15
+ }
16
+ static loadRemoteControlConfigFromEnv() {
17
+ return loadRemoteControlConfigFromEnv();
18
+ }
19
+ static ensureNativeLibEnv() {
20
+ ensureNativeLibEnv();
21
+ }
22
+ static ensureRemoteControlNativeLibEnv() {
23
+ ensureRemoteControlNativeLibEnv();
24
+ }
25
+ toolSpecs() {
26
+ return remoteControlToolSpecs();
27
+ }
28
+ handleToolCall(name, args) {
29
+ const tenant = resolveTenant(args.tenant_id, this.config.tenant);
30
+ return this.withOrchestrator(tenant, (orchestrator) => this.dispatch(name, args, orchestrator, tenant));
31
+ }
32
+ dispatch(name, args, orchestrator, tenant) {
33
+ switch (name) {
34
+ case "discover_nodes":
35
+ return this.toolResultWithPayload(handleDiscoverNodes(orchestrator, tenant));
36
+ case "list_remote_tools":
37
+ return this.toolResultWithPayload(handleListRemoteTools(orchestrator, tenant, args));
38
+ case "call_remote_tool":
39
+ return this.toolResultWithPayload(handleCallRemoteTool(orchestrator, tenant, args));
40
+ case "disconnect_device":
41
+ return this.toolResultWithPayload(handleDisconnectDevice(orchestrator, tenant, args));
42
+ case "uninstall_ability":
43
+ return this.toolResultWithPayload(handleUninstallAbility(orchestrator, tenant, args));
44
+ case "package_ability":
45
+ return this.toolResultWithPayload(handlePackageAbility(tenant, args, this.config.signatureBase64));
46
+ case "deploy_ability_package":
47
+ return this.toolResultWithPayload(handleDeployAbilityPackage(orchestrator, tenant, args, this.config.signatureBase64));
48
+ case "deploy_ability":
49
+ return this.toolResultWithPayload(handleDeployAbility(orchestrator, tenant, args, this.config.signatureBase64));
50
+ case "execute_command":
51
+ return this.toolResultWithPayload(handleExecuteCommand(orchestrator, tenant, args, this.config.signatureBase64));
52
+ default:
53
+ return this.toolResult(true, { ok: false, tenant_id: tenant, error: `unknown tool: ${name}` });
54
+ }
55
+ }
56
+ withOrchestrator(tenant, invoke) {
57
+ const orchestrator = this.orchestratorFactory(this.config, tenant);
58
+ try {
59
+ return invoke(orchestrator);
60
+ }
61
+ finally {
62
+ orchestrator.close();
63
+ }
64
+ }
65
+ toolResult(isError, payload) {
66
+ return { payload, isError };
67
+ }
68
+ toolResultWithPayload(payload) {
69
+ const ok = payload.ok !== false;
70
+ return this.toolResult(!ok, payload);
71
+ }
72
+ }
@@ -0,0 +1,87 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { RemoteControlCaseKit } from "./kit.js";
5
+ import { remoteControlToolSpecs } from "./specs.js";
6
+
7
+ function makeOrchestrator() {
8
+ return {
9
+ listNodes: () => [],
10
+ listMcpTools: () => [],
11
+ callMcpTool: () => ({ ok: true, state: 5, is_error: false }),
12
+ disconnectDevice: (nodeId, reason) => ({ node_id: nodeId, reason }),
13
+ uninstallAbility: (nodeId, installId, reason) => ({
14
+ node_id: nodeId,
15
+ install_id: installId,
16
+ reason,
17
+ }),
18
+ deployAbilityPackage: () => ({ install_id: "install-1", tool_name: "tool-1" }),
19
+ cleanupInstalls: () => ({ attempted: 0, succeeded: 0, failed: 0, results: [], ok: true }),
20
+ close: () => {},
21
+ };
22
+ }
23
+
24
+ describe("RemoteControlCaseKit", () => {
25
+ it("publishes lifecycle parity tools in the spec list", () => {
26
+ const names = remoteControlToolSpecs().map((spec) => String(spec.name));
27
+ assert.ok(names.includes("disconnect_device"));
28
+ assert.ok(names.includes("uninstall_ability"));
29
+ });
30
+
31
+ it("dispatches disconnect_device", () => {
32
+ const kit = new RemoteControlCaseKit(
33
+ {
34
+ endpoint: "http://127.0.0.1:50051",
35
+ tenant: "tenant-a",
36
+ connectTimeoutMs: 5000,
37
+ signatureBase64: "sig",
38
+ },
39
+ () => makeOrchestrator(),
40
+ );
41
+
42
+ const result = kit.handleToolCall("disconnect_device", { node_id: "node-a" });
43
+
44
+ assert.equal(result.isError, false);
45
+ assert.deepEqual(result.payload, {
46
+ ok: true,
47
+ tenant_id: "tenant-a",
48
+ node_id: "node-a",
49
+ status: "disconnected",
50
+ response: {
51
+ node_id: "node-a",
52
+ reason: "disconnect_device: requested by agent",
53
+ },
54
+ });
55
+ });
56
+
57
+ it("dispatches uninstall_ability", () => {
58
+ const kit = new RemoteControlCaseKit(
59
+ {
60
+ endpoint: "http://127.0.0.1:50051",
61
+ tenant: "tenant-a",
62
+ connectTimeoutMs: 5000,
63
+ signatureBase64: "sig",
64
+ },
65
+ () => makeOrchestrator(),
66
+ );
67
+
68
+ const result = kit.handleToolCall("uninstall_ability", {
69
+ node_id: "node-a",
70
+ install_id: "install-9",
71
+ });
72
+
73
+ assert.equal(result.isError, false);
74
+ assert.deepEqual(result.payload, {
75
+ ok: true,
76
+ tenant_id: "tenant-a",
77
+ node_id: "node-a",
78
+ install_id: "install-9",
79
+ status: "uninstalled",
80
+ response: {
81
+ node_id: "node-a",
82
+ install_id: "install-9",
83
+ reason: "uninstall_ability: requested by agent",
84
+ },
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,28 @@
1
+ import type { JsonRecord, AbilityPackageDescriptor } from "./descriptor.js";
2
+ export type { JsonRecord };
3
+ export type OrchestratorFactory = (config: {
4
+ endpoint: string;
5
+ tenant: string;
6
+ connectTimeoutMs: number;
7
+ signatureBase64: string;
8
+ }, tenant: string) => RemoteOrchestrator;
9
+ export interface RemoteOrchestrator {
10
+ listNodes(ownerId?: string): JsonRecord[];
11
+ listMcpTools(namePattern?: string, tags?: string[], nodeId?: string): JsonRecord[];
12
+ callMcpTool(toolName: string, targetNodeId: string, argumentsJson: JsonRecord): JsonRecord;
13
+ disconnectDevice(nodeId: string, reason: string): JsonRecord;
14
+ uninstallAbility(nodeId: string, installId: string, reason: string): JsonRecord;
15
+ deployAbilityPackage(descriptor: AbilityPackageDescriptor, nodeId: string, cleanupOnActivateFailure: boolean): JsonRecord;
16
+ cleanupInstalls(installs: Array<{
17
+ mode: string;
18
+ nodeId: string;
19
+ installId: string;
20
+ }>): JsonRecord;
21
+ close(): void;
22
+ }
23
+ export declare function buildOrchestrator(config: {
24
+ endpoint: string;
25
+ tenant: string;
26
+ connectTimeoutMs: number;
27
+ signatureBase64: string;
28
+ }, tenant: string): RemoteOrchestrator;
@@ -0,0 +1,118 @@
1
+ import { DendriteBridge } from "../../dendrite_bridge.js";
2
+ import { DEFAULT_EXECUTION_MODE, DEFAULT_INSTALL_TIMEOUT_SECONDS } from "./config.js";
3
+ export function buildOrchestrator(config, tenant) {
4
+ return new OrchestratorAdapter(config, tenant);
5
+ }
6
+ class OrchestratorAdapter {
7
+ config;
8
+ bridge;
9
+ tenant;
10
+ constructor(config, tenant) {
11
+ this.config = config;
12
+ this.tenant = tenant;
13
+ this.bridge = new DendriteBridge({
14
+ endpoint: config.endpoint,
15
+ connectTimeoutMs: config.connectTimeoutMs,
16
+ });
17
+ }
18
+ listNodes(ownerId = "") {
19
+ return this.bridge.listNodes(this.tenant, { ownerId });
20
+ }
21
+ listMcpTools(namePattern = "", tags = [], nodeId = "") {
22
+ const options = { namePattern, tags, nodeId };
23
+ return this.bridge.listMcpTools(this.tenant, options);
24
+ }
25
+ callMcpTool(toolName, targetNodeId, argumentsJson = {}) {
26
+ const options = { targetNodeId, argumentsJson };
27
+ return this.bridge.callMcpTool(this.tenant, toolName, options);
28
+ }
29
+ disconnectDevice(nodeId, reason) {
30
+ return this.bridge.deregisterNode(this.tenant, nodeId, reason);
31
+ }
32
+ uninstallAbility(nodeId, installId, reason) {
33
+ return this.bridge.uninstallCapability(this.tenant, nodeId, installId, {
34
+ deactivateFirst: true,
35
+ deactivateReason: reason,
36
+ });
37
+ }
38
+ deployAbilityPackage(descriptor, nodeId, cleanupOnActivateFailure) {
39
+ const publish = this.bridge.publishCapability(this.tenant, descriptor.packageId, descriptor.capabilityName, {
40
+ version: descriptor.version,
41
+ digest: descriptor.digest,
42
+ signatureBase64: descriptor.signatureBase64,
43
+ tags: descriptor.tags,
44
+ metadata: descriptor.metadata,
45
+ packageBytesBase64: descriptor.packageBytesBase64,
46
+ });
47
+ const packageRef = publish.package_ref ?? {};
48
+ const digest = String(packageRef.digest || descriptor.digest);
49
+ const install = this.bridge.installCapability(this.tenant, nodeId, descriptor.packageId, {
50
+ version: descriptor.version,
51
+ digest,
52
+ requireConsent: false,
53
+ allowTransferredCode: true,
54
+ executionMode: DEFAULT_EXECUTION_MODE,
55
+ installTimeoutSeconds: DEFAULT_INSTALL_TIMEOUT_SECONDS,
56
+ });
57
+ const installId = String(install.install_id || "").trim();
58
+ if (!installId) {
59
+ throw new Error("deploy_ability_package missing install_id after install");
60
+ }
61
+ try {
62
+ const activate = this.bridge.activateCapability(this.tenant, nodeId, installId);
63
+ return {
64
+ ok: true,
65
+ package_id: descriptor.packageId,
66
+ tool_name: descriptor.toolName,
67
+ capability_name: descriptor.capabilityName,
68
+ publish,
69
+ install,
70
+ activate,
71
+ install_id: installId,
72
+ };
73
+ }
74
+ catch (error) {
75
+ const cleanup = cleanupOnActivateFailure ? this.cleanupInstalls([{ mode: "publish_install_activate", nodeId, installId }]) : { attempted: false, ok: false, reason: "cleanup disabled" };
76
+ throw new Error(JSON.stringify({
77
+ message: "activate failed",
78
+ error: String(error),
79
+ install_id: installId,
80
+ cleanup,
81
+ }));
82
+ }
83
+ }
84
+ cleanupInstalls(installs) {
85
+ const results = [];
86
+ let attempted = 0;
87
+ let succeeded = 0;
88
+ let failed = 0;
89
+ const ordered = [...installs].reverse();
90
+ for (const install of ordered) {
91
+ attempted += 1;
92
+ if (!install.nodeId || !install.installId) {
93
+ failed += 1;
94
+ results.push({
95
+ mode: install.mode,
96
+ node_id: install.nodeId,
97
+ install_id: install.installId,
98
+ ok: false,
99
+ error: "missing node_id or install_id",
100
+ });
101
+ continue;
102
+ }
103
+ try {
104
+ const response = this.bridge.uninstallCapability(this.tenant, install.nodeId, install.installId, { deactivateFirst: true, deactivateReason: "sdk cleanup" });
105
+ succeeded += 1;
106
+ results.push({ mode: install.mode, node_id: install.nodeId, install_id: install.installId, ok: true, response });
107
+ }
108
+ catch (error) {
109
+ failed += 1;
110
+ results.push({ mode: install.mode, node_id: install.nodeId, install_id: install.installId, ok: false, error: String(error) });
111
+ }
112
+ }
113
+ return { attempted, succeeded, failed, results, ok: failed === 0 };
114
+ }
115
+ close() {
116
+ this.bridge.close();
117
+ }
118
+ }
@@ -0,0 +1,2 @@
1
+ import type { JsonRecord } from "./descriptor.js";
2
+ export declare function remoteControlToolSpecs(): JsonRecord[];
@@ -0,0 +1,152 @@
1
+ export function remoteControlToolSpecs() {
2
+ return [
3
+ // -----------------------------------------------------------------
4
+ // GENERIC TOOLS -- core remote-control primitives
5
+ // -----------------------------------------------------------------
6
+ {
7
+ name: "discover_nodes",
8
+ description: "Discover online nodes registered with Axon Runtime.",
9
+ inputSchema: {
10
+ type: "object",
11
+ properties: { tenant_id: { type: "string", description: "Tenant ID (default AXON_TENANT)" } },
12
+ },
13
+ },
14
+ {
15
+ name: "list_remote_tools",
16
+ description: "List MCP tools visible for a tenant.",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ tenant_id: { type: "string" },
21
+ node_id: { type: "string" },
22
+ name_pattern: { type: "string" },
23
+ },
24
+ },
25
+ },
26
+ {
27
+ name: "call_remote_tool",
28
+ description: "Call an MCP tool on a selected node.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ tenant_id: { type: "string" },
33
+ tool_name: { type: "string" },
34
+ node_id: { type: "string" },
35
+ arguments: { type: "object" },
36
+ },
37
+ required: ["tool_name", "node_id"],
38
+ },
39
+ },
40
+ {
41
+ name: "disconnect_device",
42
+ description: "Deregister a remote device from the Axon Runtime, closing its connection.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ tenant_id: { type: "string" },
47
+ node_id: { type: "string" },
48
+ reason: { type: "string" },
49
+ },
50
+ required: ["node_id"],
51
+ },
52
+ },
53
+ {
54
+ name: "uninstall_ability",
55
+ description: "Uninstall a deployed ability from a device by deactivating and removing it.",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ tenant_id: { type: "string" },
60
+ node_id: { type: "string" },
61
+ install_id: { type: "string" },
62
+ reason: { type: "string" },
63
+ },
64
+ required: ["node_id", "install_id"],
65
+ },
66
+ },
67
+ // -----------------------------------------------------------------
68
+ // AI-AGENT PRESET TOOLS -- convenience wrappers for AI agents
69
+ // -----------------------------------------------------------------
70
+ {
71
+ name: "deploy_ability",
72
+ description: "Deploy a command-backed MCP ability.",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ tenant_id: { type: "string" },
77
+ node_id: { type: "string" },
78
+ tool_name: { type: "string" },
79
+ description: { type: "string" },
80
+ command_template: { type: "string" },
81
+ },
82
+ required: ["node_id", "command_template"],
83
+ },
84
+ },
85
+ // -----------------------------------------------------------------
86
+ // GENERIC TOOLS (continued) -- ability packaging and deployment
87
+ // -----------------------------------------------------------------
88
+ {
89
+ name: "package_ability",
90
+ description: "Build a native ability package descriptor.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ tenant_id: { type: "string" },
95
+ ability_name: { type: "string" },
96
+ tool_name: { type: "string" },
97
+ description: { type: "string" },
98
+ command_template: { type: "string" },
99
+ input_schema: { type: "object" },
100
+ output_schema: { type: "object" },
101
+ version: { type: "string" },
102
+ tags: { type: "array", items: { type: "string" } },
103
+ package_id: { type: "string" },
104
+ capability_name: { type: "string" },
105
+ signature_base64: { type: "string" },
106
+ digest: { type: "string" },
107
+ },
108
+ required: ["ability_name", "command_template"],
109
+ },
110
+ },
111
+ {
112
+ name: "deploy_ability_package",
113
+ description: "Deploy a native ability package by publish/install/activate.",
114
+ inputSchema: {
115
+ type: "object",
116
+ properties: {
117
+ tenant_id: { type: "string" },
118
+ node_id: { type: "string" },
119
+ package: { type: "object" },
120
+ ability_name: { type: "string" },
121
+ tool_name: { type: "string" },
122
+ description: { type: "string" },
123
+ command_template: { type: "string" },
124
+ input_schema: { type: "object" },
125
+ output_schema: { type: "object" },
126
+ version: { type: "string" },
127
+ tags: { type: "array", items: { type: "string" } },
128
+ package_id: { type: "string" },
129
+ capability_name: { type: "string" },
130
+ signature_base64: { type: "string" },
131
+ cleanup_on_activate_failure: { type: "boolean" },
132
+ },
133
+ required: ["node_id"],
134
+ },
135
+ },
136
+ // AI-AGENT PRESET: one-shot command execution
137
+ {
138
+ name: "execute_command",
139
+ description: "One-shot command execution via temporary MCP ability.",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ tenant_id: { type: "string" },
144
+ node_id: { type: "string" },
145
+ command: { type: "string" },
146
+ cleanup: { type: "boolean" },
147
+ },
148
+ required: ["node_id", "command"],
149
+ },
150
+ },
151
+ ];
152
+ }
@@ -0,0 +1,7 @@
1
+ export { RemoteControlCaseKit, } from "./remote_control/kit.js";
2
+ export type { RemoteControlRuntimeConfig } from "./remote_control/config.js";
3
+ export type { RemoteOrchestratorFactory } from "./remote_control/kit.js";
4
+ export type { OrchestratorFactory } from "./remote_control/orchestrator.js";
5
+ export type { AbilityPackageDescriptor } from "./remote_control/descriptor.js";
6
+ export { DEFAULT_SIGNATURE, DEFAULT_VERSION, DEFAULT_INSTALL_TIMEOUT_SECONDS, DEFAULT_EXECUTION_MODE, } from "./remote_control/config.js";
7
+ export { loadRemoteControlConfigFromEnv, loadConfigFromEnv, ensureRemoteControlNativeLibEnv, ensureNativeLibEnv, } from "./remote_control/config.js";
@@ -0,0 +1,3 @@
1
+ export { RemoteControlCaseKit, } from "./remote_control/kit.js";
2
+ export { DEFAULT_SIGNATURE, DEFAULT_VERSION, DEFAULT_INSTALL_TIMEOUT_SECONDS, DEFAULT_EXECUTION_MODE, } from "./remote_control/config.js";
3
+ export { loadRemoteControlConfigFromEnv, loadConfigFromEnv, ensureRemoteControlNativeLibEnv, ensureNativeLibEnv, } from "./remote_control/config.js";
@@ -0,0 +1,46 @@
1
+ import type { AxonError } from "./errors.js";
2
+ /** A phase in the Axon ability lifecycle. */
3
+ export type Phase = "publish" | "install" | "activate" | "invoke" | "deactivate" | "uninstall" | "deploy";
4
+ /** Outcome of a lifecycle phase transition. */
5
+ export type PhaseStatus = "ok" | "error" | "skipped";
6
+ /** Structured record of a single lifecycle phase transition. */
7
+ export interface PhaseReceipt {
8
+ phase: Phase;
9
+ status: PhaseStatus;
10
+ started_ms: number;
11
+ ended_ms: number;
12
+ duration_ms: number;
13
+ tenant_id: string;
14
+ node_id: string;
15
+ ability_id: string;
16
+ install_id?: string;
17
+ error?: string;
18
+ error_code?: string;
19
+ metadata?: Record<string, unknown>;
20
+ }
21
+ /** In-progress receipt builder. */
22
+ export declare class PhaseReceiptBuilder {
23
+ private readonly phase;
24
+ private readonly startedMs;
25
+ private readonly tenantId;
26
+ private readonly nodeId;
27
+ private readonly abilityId;
28
+ constructor(phase: Phase, tenantId: string, nodeId: string, abilityId: string);
29
+ finishOk(installId?: string, metadata?: Record<string, unknown>): PhaseReceipt;
30
+ finishErr(error: AxonError | Error): PhaseReceipt;
31
+ finishSkipped(reason: string): PhaseReceipt;
32
+ }
33
+ /** Begin timing a lifecycle phase. */
34
+ export declare function beginPhase(phase: Phase, tenantId: string, nodeId: string, abilityId: string): PhaseReceiptBuilder;
35
+ /** Create a receipt for a skipped phase. */
36
+ export declare function skippedReceipt(phase: Phase, tenantId: string, nodeId: string, abilityId: string): PhaseReceipt;
37
+ /** Aggregated trace of a full deploy operation (publish → install → activate). */
38
+ export interface DeployTrace {
39
+ receipts: PhaseReceipt[];
40
+ total_ms: number;
41
+ ok: boolean;
42
+ }
43
+ /** Build a DeployTrace from a sequence of receipts. */
44
+ export declare function buildDeployTrace(receipts: PhaseReceipt[]): DeployTrace;
45
+ /** Get a receipt for a specific phase from a trace. */
46
+ export declare function phaseFromTrace(trace: DeployTrace, phase: Phase): PhaseReceipt | undefined;
package/src/receipt.js ADDED
@@ -0,0 +1,98 @@
1
+ // sdk/node/src/receipt.ts — Lifecycle phase receipts for observability and evaluation.
2
+ //
3
+ // Every phase transition in the Axon ability lifecycle emits a structured
4
+ // PhaseReceipt. Mirrors the Rust reference: sdk/rust/src/receipt.rs
5
+ //
6
+ // Copyright (c) 2026-2027 easynet. All rights reserved.
7
+ /** In-progress receipt builder. */
8
+ export class PhaseReceiptBuilder {
9
+ phase;
10
+ startedMs;
11
+ tenantId;
12
+ nodeId;
13
+ abilityId;
14
+ constructor(phase, tenantId, nodeId, abilityId) {
15
+ this.phase = phase;
16
+ this.startedMs = Date.now();
17
+ this.tenantId = tenantId;
18
+ this.nodeId = nodeId;
19
+ this.abilityId = abilityId;
20
+ }
21
+ finishOk(installId, metadata) {
22
+ const ended = Date.now();
23
+ return {
24
+ phase: this.phase,
25
+ status: "ok",
26
+ started_ms: this.startedMs,
27
+ ended_ms: ended,
28
+ duration_ms: ended - this.startedMs,
29
+ tenant_id: this.tenantId,
30
+ node_id: this.nodeId,
31
+ ability_id: this.abilityId,
32
+ install_id: installId,
33
+ metadata,
34
+ };
35
+ }
36
+ finishErr(error) {
37
+ const ended = Date.now();
38
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string"
39
+ ? error.code
40
+ : "UNKNOWN";
41
+ return {
42
+ phase: this.phase,
43
+ status: "error",
44
+ started_ms: this.startedMs,
45
+ ended_ms: ended,
46
+ duration_ms: ended - this.startedMs,
47
+ tenant_id: this.tenantId,
48
+ node_id: this.nodeId,
49
+ ability_id: this.abilityId,
50
+ error: error.message,
51
+ error_code: code,
52
+ };
53
+ }
54
+ finishSkipped(reason) {
55
+ const ended = Date.now();
56
+ return {
57
+ phase: this.phase,
58
+ status: "skipped",
59
+ started_ms: this.startedMs,
60
+ ended_ms: ended,
61
+ duration_ms: ended - this.startedMs,
62
+ tenant_id: this.tenantId,
63
+ node_id: this.nodeId,
64
+ ability_id: this.abilityId,
65
+ metadata: { skip_reason: reason },
66
+ };
67
+ }
68
+ }
69
+ /** Begin timing a lifecycle phase. */
70
+ export function beginPhase(phase, tenantId, nodeId, abilityId) {
71
+ return new PhaseReceiptBuilder(phase, tenantId, nodeId, abilityId);
72
+ }
73
+ /** Create a receipt for a skipped phase. */
74
+ export function skippedReceipt(phase, tenantId, nodeId, abilityId) {
75
+ const now = Date.now();
76
+ return {
77
+ phase,
78
+ status: "skipped",
79
+ started_ms: now,
80
+ ended_ms: now,
81
+ duration_ms: 0,
82
+ tenant_id: tenantId,
83
+ node_id: nodeId,
84
+ ability_id: abilityId,
85
+ };
86
+ }
87
+ /** Build a DeployTrace from a sequence of receipts. */
88
+ export function buildDeployTrace(receipts) {
89
+ const ok = !receipts.some((r) => r.status === "error");
90
+ const total = receipts.length > 0
91
+ ? receipts[receipts.length - 1].ended_ms - receipts[0].started_ms
92
+ : 0;
93
+ return { receipts, total_ms: total, ok };
94
+ }
95
+ /** Get a receipt for a specific phase from a trace. */
96
+ export function phaseFromTrace(trace, phase) {
97
+ return trace.receipts.find((r) => r.phase === phase);
98
+ }