@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,102 @@
1
+ // EasyNet Axon for AgentNet -- bundle resolution and parsing helpers for ability-dispatch workflow.
2
+ import { createHash } from "node:crypto";
3
+ import { existsSync, readdirSync, statSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
5
+ import { gunzipSync } from "node:zlib";
6
+ export function sha256Prefixed(payload) {
7
+ const hash = createHash("sha256").update(payload).digest("hex");
8
+ return `sha256:${hash}`;
9
+ }
10
+ export function parseTarSize(raw) {
11
+ const text = raw.toString("utf8").replace(/\0.*$/, "").trim();
12
+ if (!text) {
13
+ return 0;
14
+ }
15
+ const parsed = Number.parseInt(text, 8);
16
+ if (!Number.isFinite(parsed) || parsed < 0) {
17
+ throw new Error(`invalid tar entry size: '${text}'`);
18
+ }
19
+ return parsed;
20
+ }
21
+ export function isZeroBlock(block) {
22
+ for (let i = 0; i < block.length; i += 1) {
23
+ if (block[i] !== 0) {
24
+ return false;
25
+ }
26
+ }
27
+ return true;
28
+ }
29
+ export function readBundleVersion(bundleBytes) {
30
+ const tar = gunzipSync(bundleBytes);
31
+ let offset = 0;
32
+ while (offset + 512 <= tar.length) {
33
+ const header = tar.subarray(offset, offset + 512);
34
+ if (isZeroBlock(header)) {
35
+ break;
36
+ }
37
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
38
+ const prefix = header.subarray(345, 500).toString("utf8").replace(/\0.*$/, "");
39
+ const fullName = prefix ? `${prefix}/${name}` : name;
40
+ const typeFlag = String.fromCharCode(header[156] || 0);
41
+ const size = parseTarSize(header.subarray(124, 136));
42
+ const dataStart = offset + 512;
43
+ const dataEnd = dataStart + size;
44
+ if (dataEnd > tar.length) {
45
+ throw new Error(`tar entry exceeds archive bounds: ${fullName}`);
46
+ }
47
+ const isManifest = fullName === "MANIFEST.json" || fullName.endsWith("/MANIFEST.json");
48
+ const isRegularFile = typeFlag === "\0" || typeFlag === "0";
49
+ if (isManifest && isRegularFile && !fullName.includes("/PaxHeader/")) {
50
+ const manifestRaw = tar.subarray(dataStart, dataEnd).toString("utf8").replace(/\u0000/g, "").trim();
51
+ const manifest = JSON.parse(manifestRaw);
52
+ const version = String(manifest["version"] ?? "").trim();
53
+ if (!version) {
54
+ throw new Error("bundle MANIFEST.json missing non-empty version");
55
+ }
56
+ return version;
57
+ }
58
+ const blocks = Math.ceil(size / 512);
59
+ offset = dataStart + blocks * 512;
60
+ }
61
+ throw new Error("bundle MANIFEST.json not found");
62
+ }
63
+ function collectCaseSearchDirs() {
64
+ const cwd = process.cwd();
65
+ const ancestors = [];
66
+ let current = cwd;
67
+ for (let i = 0; i < 7; i += 1) {
68
+ ancestors.push(current);
69
+ const parent = dirname(current);
70
+ if (parent === current) {
71
+ break;
72
+ }
73
+ current = parent;
74
+ }
75
+ const candidates = new Set();
76
+ for (const root of ancestors) {
77
+ candidates.add(root);
78
+ candidates.add(resolve(root, "capability-package", "dist"));
79
+ candidates.add(resolve(root, "dist"));
80
+ }
81
+ return Array.from(candidates).filter((d) => existsSync(d));
82
+ }
83
+ export function resolveBundlePath(raw, cfg) {
84
+ if (raw.trim()) {
85
+ const abs = resolve(raw);
86
+ if (!existsSync(abs)) {
87
+ throw new Error(`bundle not found: ${abs}`);
88
+ }
89
+ return abs;
90
+ }
91
+ const searchDirs = collectCaseSearchDirs();
92
+ const matchesPrefix = (name) => cfg.bundleFilePrefixes.some((prefix) => name.startsWith(prefix));
93
+ const candidates = searchDirs.flatMap((dir) => readdirSync(dir)
94
+ .filter((name) => matchesPrefix(name) && name.endsWith(".tar.gz"))
95
+ .map((name) => resolve(dir, name)));
96
+ if (candidates.length === 0) {
97
+ const checked = searchDirs.join(", ");
98
+ throw new Error(`no pre-built bundle found under search directories: ${checked}`);
99
+ }
100
+ candidates.sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
101
+ return candidates[candidates.length - 1];
102
+ }
@@ -0,0 +1,6 @@
1
+ type JsonMap = Record<string, unknown>;
2
+ export declare function decodeMediaBase64(fieldName: string, raw: string): Buffer;
3
+ export declare function defaultOutputDir(): string;
4
+ export declare function safeFileToken(raw: string): string;
5
+ export declare function persistMediaFile(outputDir: string, mode: "photo" | "video", taskResult: JsonMap, payload: Buffer, extension: string): JsonMap;
6
+ export {};
@@ -0,0 +1,48 @@
1
+ // EasyNet Axon for AgentNet -- media decode/persist helpers for ability-dispatch workflow.
2
+ import { writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { sha256Prefixed } from "./bundle.js";
5
+ export function decodeMediaBase64(fieldName, raw) {
6
+ const compact = raw.replace(/\s+/g, "");
7
+ if (!compact) {
8
+ throw new Error(`${fieldName} is empty`);
9
+ }
10
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(compact) || compact.length % 4 !== 0) {
11
+ throw new Error(`${fieldName} is not valid base64`);
12
+ }
13
+ const decoded = Buffer.from(compact, "base64");
14
+ if (decoded.length === 0) {
15
+ throw new Error(`${fieldName} decoded to empty payload`);
16
+ }
17
+ const normalizedIn = compact.replace(/=+$/, "");
18
+ const normalizedOut = decoded.toString("base64").replace(/=+$/, "");
19
+ if (normalizedIn !== normalizedOut) {
20
+ throw new Error(`${fieldName} is not valid base64`);
21
+ }
22
+ return decoded;
23
+ }
24
+ export function defaultOutputDir() {
25
+ return resolve(process.cwd(), "output");
26
+ }
27
+ export function safeFileToken(raw) {
28
+ const cleaned = raw
29
+ .split("")
30
+ .map((ch) => (/^[A-Za-z0-9_-]$/.test(ch) ? ch : "-"))
31
+ .join("");
32
+ const collapsed = cleaned
33
+ .split("-")
34
+ .filter((seg) => seg.length > 0)
35
+ .join("-");
36
+ return collapsed.slice(0, 80) || "unknown";
37
+ }
38
+ export function persistMediaFile(outputDir, mode, taskResult, payload, extension) {
39
+ const taskId = safeFileToken(String(taskResult["task_id"] ?? "")) || `${mode}-task`;
40
+ const fileName = `${mode}-${taskId}.${extension}`;
41
+ const path = resolve(outputDir, fileName);
42
+ writeFileSync(path, payload);
43
+ return {
44
+ path,
45
+ size_bytes: payload.length,
46
+ sha256: sha256Prefixed(payload),
47
+ };
48
+ }
@@ -0,0 +1,21 @@
1
+ export type Json = Record<string, unknown>;
2
+ export type OrchestratorInstallRef = {
3
+ mode: string;
4
+ node_id: string;
5
+ install_id: string;
6
+ };
7
+ export declare class Orchestrator {
8
+ private endpoint;
9
+ private tenant;
10
+ private connectTimeoutMs;
11
+ private bridge;
12
+ constructor(endpoint: string, tenant: string, connectTimeoutMs?: number);
13
+ listNodes(ownerId?: string): Json[];
14
+ selectNode(nodes: Json[], explicitNodeId: string): Json;
15
+ publishInstallActivate(nodeId: string, packageId: string, capabilityName: string, version: string, digest: string, packageBytesBase64: string, metadata: Record<string, string>): Json;
16
+ sendA2ATask(nodeId: string, skillId: string, inputJson: Json): Json;
17
+ uninstallCapability(nodeId: string, installId: string): Json;
18
+ cleanupInstalls(installs: OrchestratorInstallRef[]): Json;
19
+ close(): void;
20
+ static deterministicSignatureTokenBase64(packageId: string, capabilityName: string, digest: string): string;
21
+ }
@@ -0,0 +1,117 @@
1
+ // Lightweight Orchestrator for case servers (Node)
2
+ import { DendriteBridge } from "../../dendrite_bridge.js";
3
+ import { createHash } from "node:crypto";
4
+ export class Orchestrator {
5
+ endpoint;
6
+ tenant;
7
+ connectTimeoutMs;
8
+ bridge;
9
+ constructor(endpoint, tenant, connectTimeoutMs = 5000) {
10
+ this.endpoint = endpoint;
11
+ this.tenant = tenant;
12
+ this.connectTimeoutMs = connectTimeoutMs;
13
+ this.bridge = new DendriteBridge({ endpoint, connectTimeoutMs });
14
+ }
15
+ listNodes(ownerId) {
16
+ return this.bridge.listNodes(this.tenant, { ownerId });
17
+ }
18
+ selectNode(nodes, explicitNodeId) {
19
+ if (explicitNodeId) {
20
+ const found = nodes.find((node) => String(node["node_id"] ?? "").trim() === explicitNodeId);
21
+ if (!found) {
22
+ throw new Error(`target node not found: ${explicitNodeId}`);
23
+ }
24
+ return found;
25
+ }
26
+ const online = nodes
27
+ .filter((node) => Boolean(node["online"]))
28
+ .sort((a, b) => String(a["node_id"] ?? "").localeCompare(String(b["node_id"] ?? "")));
29
+ if (online.length === 0) {
30
+ throw new Error("no online nodes available for dispatch");
31
+ }
32
+ return online[0];
33
+ }
34
+ publishInstallActivate(nodeId, packageId, capabilityName, version, digest, packageBytesBase64, metadata) {
35
+ const signatureBase64 = Orchestrator.deterministicSignatureTokenBase64(packageId, capabilityName, digest);
36
+ const publish = this.bridge.publishCapability(this.tenant, packageId, capabilityName, {
37
+ version,
38
+ digest,
39
+ signatureBase64,
40
+ metadata,
41
+ packageBytesBase64,
42
+ });
43
+ let publishDigest = digest;
44
+ const packageRef = publish["package_ref"];
45
+ if (packageRef && typeof packageRef === "object") {
46
+ const maybe = String(packageRef["digest"] ?? "").trim();
47
+ if (maybe)
48
+ publishDigest = maybe;
49
+ }
50
+ const install = this.bridge.installCapability(this.tenant, nodeId, packageId, {
51
+ version,
52
+ digest: publishDigest,
53
+ allowTransferredCode: true,
54
+ executionMode: "sandbox_first",
55
+ installTimeoutSeconds: 45,
56
+ });
57
+ const installId = String(install["install_id"] ?? "").trim();
58
+ if (!installId)
59
+ throw new Error(`install_id missing: ${JSON.stringify(install)}`);
60
+ const activate = this.bridge.activateCapability(this.tenant, nodeId, installId);
61
+ return { package_id: packageId, capability_name: capabilityName, package_version: version, publish, install, activate };
62
+ }
63
+ sendA2ATask(nodeId, skillId, inputJson) {
64
+ return this.bridge.sendA2aTask(this.tenant, nodeId, skillId, { inputJson });
65
+ }
66
+ uninstallCapability(nodeId, installId) {
67
+ return this.bridge.uninstallCapability(this.tenant, nodeId, installId, {
68
+ deactivateFirst: true,
69
+ deactivateReason: "sdk cleanup",
70
+ force: false,
71
+ });
72
+ }
73
+ cleanupInstalls(installs) {
74
+ const results = [];
75
+ let succeeded = 0;
76
+ let failed = 0;
77
+ for (let idx = installs.length - 1; idx >= 0; idx -= 1) {
78
+ const item = installs[idx];
79
+ const nodeId = String(item.node_id ?? "").trim();
80
+ const installId = String(item.install_id ?? "").trim();
81
+ const mode = String(item.mode ?? "").trim();
82
+ const row = { mode, node_id: nodeId, install_id: installId };
83
+ if (!nodeId || !installId) {
84
+ row["ok"] = false;
85
+ row["error"] = "missing node_id or install_id";
86
+ failed += 1;
87
+ results.push(row);
88
+ continue;
89
+ }
90
+ try {
91
+ row["ok"] = true;
92
+ row["response"] = this.uninstallCapability(nodeId, installId);
93
+ succeeded += 1;
94
+ }
95
+ catch (error) {
96
+ row["ok"] = false;
97
+ row["error"] = String(error);
98
+ failed += 1;
99
+ }
100
+ results.push(row);
101
+ }
102
+ return {
103
+ attempted: installs.length,
104
+ succeeded,
105
+ failed,
106
+ ok: failed === 0,
107
+ results,
108
+ };
109
+ }
110
+ close() {
111
+ this.bridge.close();
112
+ }
113
+ static deterministicSignatureTokenBase64(packageId, capabilityName, digest) {
114
+ const seed = `${packageId}:${capabilityName}:${digest}`;
115
+ return createHash("sha256").update(seed, "utf8").digest("base64");
116
+ }
117
+ }
@@ -0,0 +1,50 @@
1
+ import { Orchestrator, type OrchestratorInstallRef } from "./orchestrator.js";
2
+ export { parseArgs, parsePositiveInt, BOOL_FLAGS } from "./args.js";
3
+ export { readBundleVersion, parseTarSize, isZeroBlock, sha256Prefixed, resolveBundlePath } from "./bundle.js";
4
+ export { decodeMediaBase64, persistMediaFile, safeFileToken, defaultOutputDir } from "./media.js";
5
+ type JsonMap = Record<string, unknown>;
6
+ type InstallRef = OrchestratorInstallRef;
7
+ export interface ModeSpec {
8
+ capabilityName: string;
9
+ abilityPrefix: string;
10
+ mediaField: string;
11
+ mediaExt: string;
12
+ }
13
+ export declare const MEDIA_CAPTURE_MODE_SPECS: Record<string, ModeSpec>;
14
+ export interface AbilityDispatchConfig {
15
+ /** Mode specifications mapping mode names to their capability/skill/media details.
16
+ * Required at construction -- an error is thrown if not provided. */
17
+ modeSpecs: Record<string, ModeSpec>;
18
+ /** Bundle file-name prefixes to match when auto-discovering .tar.gz bundles.
19
+ * Default: ["ability-dispatch-media-capability-", "case01-media-capability-"] */
20
+ bundleFilePrefixes?: string[];
21
+ /** Custom result validator. Receives (mode, taskResult, requireReal) and should return the validated result_json or throw. */
22
+ resultValidator?: (mode: string, taskResult: Record<string, unknown>, requireReal: boolean) => Record<string, unknown>;
23
+ /** Custom metadata builder. Receives (mode, skillId, ts, config) and returns metadata record for publishInstallActivate. */
24
+ metadataBuilder?: (mode: string, skillId: string, ts: number, config: Record<string, unknown>) => Record<string, string>;
25
+ /** Custom task payload builder. Receives (mode, config, ts) and returns the task payload for sendA2ATask. */
26
+ taskPayloadBuilder?: (mode: string, config: Record<string, unknown>, ts: number) => Record<string, unknown>;
27
+ }
28
+ export declare function validateMediaCaptureResult(mode: string, taskResult: Record<string, unknown>, requireReal: boolean): Record<string, unknown>;
29
+ export declare function mediaCaptureMetadataBuilder(mode: string, skillId: string, ts: number, config: Record<string, unknown>): Record<string, string>;
30
+ export declare function mediaCaptureTaskPayload(mode: string, config: Record<string, unknown>, ts: number): Record<string, unknown>;
31
+ export interface RunModeContext {
32
+ mode: string;
33
+ spec: ModeSpec;
34
+ orch: Orchestrator;
35
+ targetNodeId: string;
36
+ timestamp: number;
37
+ bundle: {
38
+ version: string;
39
+ digest: string;
40
+ base64: string;
41
+ };
42
+ cfg: Required<AbilityDispatchConfig>;
43
+ runtimeConfig: JsonMap;
44
+ requireReal: boolean;
45
+ writeMediaFiles: boolean;
46
+ outputDir: string;
47
+ createdInstalls: InstallRef[];
48
+ }
49
+ export declare function runCaseFromArgs(argv: string[], userConfig?: Partial<AbilityDispatchConfig>): number;
50
+ export declare function runCase(argv: string[], config?: AbilityDispatchConfig): number;
@@ -0,0 +1,333 @@
1
+ // EasyNet Axon for AgentNet
2
+ // =========================
3
+ //
4
+ // File: sdk/node/src/presets/ability_dispatch/workflow.ts
5
+ // Description: Ability-dispatch Node server orchestration for package lifecycle and selected-client dispatch flow.
6
+ //
7
+ // Protocol Responsibility:
8
+ // - Defines contract/configuration behavior required by this Axon runtime/SDK/gallery workflow surface.
9
+ // - Preserves stable cross-language semantics and interoperability expectations.
10
+ //
11
+ // Implementation Approach:
12
+ // - Keeps declarations, defaults, and orchestration assumptions explicit and auditable.
13
+ // - Uses deterministic structure so behavior stays reproducible across environments.
14
+ //
15
+ // Usage Contract:
16
+ // - Changes here should remain consistent with related runtime/SDK/service definitions.
17
+ // - Validate modifications via protocol checks or scenario smoke tests before release.
18
+ //
19
+ // Architectural Position:
20
+ // - Case-demo Node server control-plane orchestration layer.
21
+ //
22
+ // Author: Silan.Hu
23
+ // Email: silan.hu@u.nus.edu
24
+ // Copyright (c) 2026-2027 easynet. All rights reserved.
25
+ // Node server flow: publish/install/activate media capability on selected client and invoke tasks.
26
+ import { Orchestrator } from "./orchestrator.js";
27
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
28
+ import { dirname, resolve } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+ import { parseArgs, parsePositiveInt } from "./args.js";
31
+ import { readBundleVersion, resolveBundlePath, sha256Prefixed } from "./bundle.js";
32
+ import { decodeMediaBase64, defaultOutputDir, persistMediaFile } from "./media.js";
33
+ export { parseArgs, parsePositiveInt, BOOL_FLAGS } from "./args.js";
34
+ export { readBundleVersion, parseTarSize, isZeroBlock, sha256Prefixed, resolveBundlePath } from "./bundle.js";
35
+ export { decodeMediaBase64, persistMediaFile, safeFileToken, defaultOutputDir } from "./media.js";
36
+ export const MEDIA_CAPTURE_MODE_SPECS = {
37
+ photo: { capabilityName: "desktop_camera_photo", abilityPrefix: "take_photo", mediaField: "image_base64", mediaExt: "jpg" },
38
+ video: { capabilityName: "desktop_camera_video", abilityPrefix: "record_video", mediaField: "video_base64", mediaExt: "mp4" },
39
+ };
40
+ const DEFAULT_BUNDLE_FILE_PREFIXES = [
41
+ "ability-dispatch-media-capability-",
42
+ "case01-media-capability-",
43
+ ];
44
+ function resolveConfig(cfg) {
45
+ if (!cfg?.modeSpecs) {
46
+ throw new Error("AbilityDispatchConfig.modeSpecs is required");
47
+ }
48
+ return {
49
+ modeSpecs: cfg.modeSpecs,
50
+ bundleFilePrefixes: cfg?.bundleFilePrefixes ?? [...DEFAULT_BUNDLE_FILE_PREFIXES],
51
+ resultValidator: cfg?.resultValidator ?? validateMediaCaptureResult,
52
+ metadataBuilder: cfg?.metadataBuilder ?? mediaCaptureMetadataBuilder,
53
+ taskPayloadBuilder: cfg?.taskPayloadBuilder ?? mediaCaptureTaskPayload,
54
+ };
55
+ }
56
+ function defaultBridgeLibName() {
57
+ if (process.platform === "darwin")
58
+ return "libaxon_dendrite_bridge.dylib";
59
+ if (process.platform === "win32")
60
+ return "axon_dendrite_bridge.dll";
61
+ return "libaxon_dendrite_bridge.so";
62
+ }
63
+ function findRepoRootForBridge(startDir) {
64
+ let current = resolve(startDir);
65
+ for (let steps = 0; steps < 10; steps += 1) {
66
+ const candidate = resolve(current, "core/runtime-rs/dendrite-bridge/target/release", defaultBridgeLibName());
67
+ if (existsSync(candidate)) {
68
+ return current;
69
+ }
70
+ const parent = dirname(current);
71
+ if (parent === current) {
72
+ break;
73
+ }
74
+ current = parent;
75
+ }
76
+ return "";
77
+ }
78
+ function ensureBridgeLibEnv() {
79
+ if (process.env.EASYNET_DENDRITE_BRIDGE_LIB)
80
+ return;
81
+ const here = dirname(fileURLToPath(import.meta.url));
82
+ const repoRoot = findRepoRootForBridge(here) || process.cwd();
83
+ const candidate = resolve(repoRoot, "core/runtime-rs/dendrite-bridge/target/release", defaultBridgeLibName());
84
+ if (existsSync(candidate)) {
85
+ process.env.EASYNET_DENDRITE_BRIDGE_LIB = candidate;
86
+ }
87
+ }
88
+ function extractInstallId(lifecycle) {
89
+ const install = lifecycle["install"];
90
+ return String(install?.["install_id"] ?? "").trim();
91
+ }
92
+ export function validateMediaCaptureResult(mode, taskResult, requireReal) {
93
+ const resultJson = taskResult["result_json"];
94
+ if (!resultJson || typeof resultJson !== "object") {
95
+ throw new Error(`task_result.result_json missing for ${mode}: ${JSON.stringify(taskResult)}`);
96
+ }
97
+ const captureMode = String(resultJson["capture_mode"] ?? "").trim().toLowerCase();
98
+ if (captureMode === "real_failed") {
99
+ let captureError = String(resultJson["capture_error"] ?? "").trim() || `${mode} real capture failed`;
100
+ const nextAction = String(resultJson["permission_next_action"] ?? "").trim();
101
+ if (Boolean(resultJson["permission_required"]) && nextAction) {
102
+ captureError = `${captureError}; next_action: ${nextAction}`;
103
+ }
104
+ throw new Error(`${mode} capture failed: ${captureError}`);
105
+ }
106
+ const spec = MEDIA_CAPTURE_MODE_SPECS[mode];
107
+ const mediaField = spec ? spec.mediaField : (mode === "photo" ? "image_base64" : "video_base64");
108
+ if (!String(resultJson[mediaField] ?? "").trim()) {
109
+ throw new Error(`${mode} result field '${mediaField}' is empty: ${JSON.stringify(resultJson)}`);
110
+ }
111
+ if (requireReal) {
112
+ if (captureMode !== "real") {
113
+ throw new Error(`${mode} capture_mode expected real but got ${captureMode}: ${JSON.stringify(resultJson)}`);
114
+ }
115
+ if (!Boolean(resultJson["real_capture_confirmed"])) {
116
+ throw new Error(`${mode} real_capture_confirmed is false: ${JSON.stringify(resultJson)}`);
117
+ }
118
+ }
119
+ else if (!new Set(["real", "mock", "mock_fallback"]).has(captureMode)) {
120
+ throw new Error(`${mode} capture_mode invalid: ${JSON.stringify(resultJson)}`);
121
+ }
122
+ return resultJson;
123
+ }
124
+ export function mediaCaptureMetadataBuilder(mode, skillId, ts, config) {
125
+ const abilityDisplayName = mode === "photo" ? "Take Photo" : "Record Video";
126
+ const execTimeout = mode === "photo" ? "120000" : "180000";
127
+ return {
128
+ "a2a.skill_id": skillId,
129
+ "a2a.ability_name": abilityDisplayName,
130
+ "axon.exec.command": `sh {ability_package_root}/scripts/run.sh ${mode} {node_id}`,
131
+ "axon.exec.timeout_ms": execTimeout,
132
+ "axon.package.install_bootstrap_command": "bash {ability_package_root}/scripts/bootstrap.sh",
133
+ "axon.package.install_bootstrap_timeout_ms": "300000",
134
+ };
135
+ }
136
+ export function mediaCaptureTaskPayload(mode, config, ts) {
137
+ if (mode === "photo") {
138
+ return {
139
+ camera_id: config.cameraId,
140
+ resolution: config.photoResolution,
141
+ require_real: config.requireReal,
142
+ prefer_real_camera: true,
143
+ auto_request_permission: config.autoRequestPermission,
144
+ };
145
+ }
146
+ return {
147
+ camera_id: config.cameraId,
148
+ resolution: config.videoResolution,
149
+ duration_seconds: config.durationSeconds,
150
+ require_real: config.requireReal,
151
+ prefer_real_camera: true,
152
+ auto_request_permission: config.autoRequestPermission,
153
+ };
154
+ }
155
+ function runMode(ctx) {
156
+ const { mode, spec, orch, targetNodeId, timestamp, bundle, cfg, runtimeConfig, requireReal, writeMediaFiles, outputDir, createdInstalls } = ctx;
157
+ const skillId = `${spec.abilityPrefix}_${timestamp}`;
158
+ const capability = spec.capabilityName;
159
+ const mediaField = spec.mediaField;
160
+ const mediaExt = spec.mediaExt;
161
+ const packageId = `pkg.${capability}.${timestamp}`;
162
+ const metadata = cfg.metadataBuilder(mode, skillId, timestamp, runtimeConfig);
163
+ const lifecycle = orch.publishInstallActivate(targetNodeId, packageId, capability, bundle.version, bundle.digest, bundle.base64, metadata);
164
+ const installId = extractInstallId(lifecycle);
165
+ if (installId) {
166
+ createdInstalls.push({
167
+ mode,
168
+ node_id: targetNodeId,
169
+ install_id: installId,
170
+ });
171
+ }
172
+ const taskPayload = cfg.taskPayloadBuilder(mode, runtimeConfig, timestamp);
173
+ const taskResult = orch.sendA2ATask(targetNodeId, skillId, taskPayload);
174
+ const resultJson = cfg.resultValidator(mode, taskResult, requireReal);
175
+ const mediaBytes = decodeMediaBase64(`${mode}.${mediaField}`, String(resultJson[mediaField] ?? ""));
176
+ const savedFile = writeMediaFiles
177
+ ? persistMediaFile(outputDir, mode, taskResult, mediaBytes, mediaExt)
178
+ : null;
179
+ return {
180
+ ...lifecycle,
181
+ skill_id: skillId,
182
+ task_result: taskResult,
183
+ result_json: resultJson,
184
+ media_size_bytes: mediaBytes.length,
185
+ saved_file: savedFile,
186
+ };
187
+ }
188
+ function executeCase(argv, userConfig) {
189
+ const cfg = resolveConfig(userConfig);
190
+ const args = parseArgs(argv);
191
+ const endpoint = String(args["endpoint"] ?? "http://127.0.0.1:50051");
192
+ const tenant = String(args["tenant"] ?? "tenant-test");
193
+ const nodeId = String(args["node-id"] ?? "");
194
+ const ownerId = String(args["owner-id"] ?? "");
195
+ const connectTimeoutMs = parsePositiveInt(typeof args["connect-timeout-ms"] === "string" ? args["connect-timeout-ms"] : undefined, 30000, "connect-timeout-ms");
196
+ const bundlePathRaw = String(args["bundle-path"] ?? "");
197
+ const modeRaw = String(args["mode"] ?? "both");
198
+ const cameraId = String(args["camera-id"] ?? "default");
199
+ const photoResolution = String(args["photo-resolution"] ?? "1280x720");
200
+ const videoResolution = String(args["video-resolution"] ?? "1280x720");
201
+ const durationSeconds = parsePositiveInt(typeof args["duration-seconds"] === "string" ? args["duration-seconds"] : undefined, 3, "duration-seconds");
202
+ const autoRequestPermission = args["auto-request-permission"] === true;
203
+ const outputDirRaw = String(args["output-dir"] ?? "");
204
+ const noWriteMediaFiles = args["no-write-media-files"] === true;
205
+ const keepInstalled = args["keep-installed"] === true;
206
+ const requireRealFlag = args["require-real"] === true;
207
+ const allowMockFallback = args["allow-mock-fallback"] === true;
208
+ const availableModes = Object.keys(cfg.modeSpecs);
209
+ if (modeRaw !== "both" && !cfg.modeSpecs[modeRaw]) {
210
+ throw new Error(`--mode must be one of: ${availableModes.join(", ")}, both`);
211
+ }
212
+ const modesToRun = modeRaw === "both" ? availableModes : [modeRaw];
213
+ const requireReal = requireRealFlag || !allowMockFallback;
214
+ const writeMediaFiles = !noWriteMediaFiles;
215
+ const outputDir = outputDirRaw.trim() ? resolve(outputDirRaw) : resolve(defaultOutputDir());
216
+ if (writeMediaFiles) {
217
+ mkdirSync(outputDir, { recursive: true });
218
+ }
219
+ ensureBridgeLibEnv();
220
+ const bundlePath = resolveBundlePath(bundlePathRaw, cfg);
221
+ const bundleBytes = readFileSync(bundlePath);
222
+ if (bundleBytes.length === 0) {
223
+ throw new Error(`bundle is empty: ${bundlePath}`);
224
+ }
225
+ const digest = sha256Prefixed(bundleBytes);
226
+ const bundleVersion = readBundleVersion(bundleBytes);
227
+ const bundleBase64 = bundleBytes.toString("base64");
228
+ const orch = new Orchestrator(endpoint, tenant, connectTimeoutMs);
229
+ const nodes = orch.listNodes(ownerId || undefined);
230
+ const targetNode = orch.selectNode(nodes, nodeId);
231
+ const targetNodeId = String(targetNode["node_id"] ?? "").trim();
232
+ if (!targetNodeId) {
233
+ throw new Error(`selected node_id is empty: ${JSON.stringify(targetNode)}`);
234
+ }
235
+ if (!Boolean(targetNode["online"])) {
236
+ throw new Error(`target node is offline: ${targetNodeId}`);
237
+ }
238
+ const timestamp = Date.now();
239
+ const runtimeConfig = {
240
+ cameraId,
241
+ photoResolution,
242
+ videoResolution,
243
+ durationSeconds,
244
+ requireReal,
245
+ autoRequestPermission,
246
+ };
247
+ const out = {
248
+ ok: true,
249
+ endpoint,
250
+ tenant,
251
+ selected_node_id: targetNodeId,
252
+ selected_node: targetNode,
253
+ nodes_count: nodes.length,
254
+ owner_id_filter: ownerId,
255
+ mode: modeRaw,
256
+ camera_id: cameraId,
257
+ photo_resolution: photoResolution,
258
+ video_resolution: videoResolution,
259
+ duration_seconds: durationSeconds,
260
+ require_real: requireReal,
261
+ auto_request_permission: autoRequestPermission,
262
+ write_media_files: writeMediaFiles,
263
+ output_dir: writeMediaFiles ? outputDir : "",
264
+ bundle_path: bundlePath,
265
+ bundle_digest: digest,
266
+ bundle_version: bundleVersion,
267
+ keep_installed: keepInstalled,
268
+ };
269
+ const createdInstalls = [];
270
+ let operationError = null;
271
+ try {
272
+ for (const currentMode of modesToRun) {
273
+ const spec = cfg.modeSpecs[currentMode];
274
+ out[currentMode] = runMode({
275
+ mode: currentMode,
276
+ spec,
277
+ orch,
278
+ targetNodeId,
279
+ timestamp,
280
+ bundle: { version: bundleVersion, digest, base64: bundleBase64 },
281
+ cfg,
282
+ runtimeConfig,
283
+ requireReal,
284
+ writeMediaFiles,
285
+ outputDir,
286
+ createdInstalls,
287
+ });
288
+ }
289
+ }
290
+ catch (err) {
291
+ operationError = err;
292
+ }
293
+ try {
294
+ if (keepInstalled) {
295
+ out["cleanup"] = {
296
+ skipped: true,
297
+ reason: "--keep-installed is set",
298
+ ok: true,
299
+ attempted: 0,
300
+ succeeded: 0,
301
+ failed: 0,
302
+ results: [],
303
+ };
304
+ }
305
+ else {
306
+ const cleanup = orch.cleanupInstalls(createdInstalls);
307
+ out["cleanup"] = cleanup;
308
+ if (Number(cleanup["failed"] ?? 0) > 0) {
309
+ process.stderr.write(`cleanup had ${String(cleanup["failed"])} uninstall failure(s)\n`);
310
+ }
311
+ }
312
+ if (operationError) {
313
+ throw operationError;
314
+ }
315
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
316
+ return 0;
317
+ }
318
+ finally {
319
+ orch.close();
320
+ }
321
+ }
322
+ export function runCaseFromArgs(argv, userConfig) {
323
+ return executeCase(argv, {
324
+ modeSpecs: MEDIA_CAPTURE_MODE_SPECS,
325
+ metadataBuilder: mediaCaptureMetadataBuilder,
326
+ taskPayloadBuilder: mediaCaptureTaskPayload,
327
+ resultValidator: validateMediaCaptureResult,
328
+ ...userConfig,
329
+ });
330
+ }
331
+ export function runCase(argv, config) {
332
+ return executeCase(argv, config);
333
+ }