@easynet-run/node 0.36.9 → 0.39.29

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.
@@ -1,7 +1,33 @@
1
+ // EasyNet Axon for AgentNet
2
+ // =========================
3
+ //
4
+ // File: sdk/node/src/presets/remote_control/kit.test.js
5
+ // Description: Node remote-control preset regression tests for tool dispatch, streaming cleanup, and lifecycle behavior.
6
+ //
7
+ // Protocol Responsibility:
8
+ // - Exercises public runtime behavior for the corresponding service surface under success and failure scenarios.
9
+ // - Guards regressions in tenant isolation, terminal states, and typed error shaping.
10
+ //
11
+ // Implementation Approach:
12
+ // - Builds in-memory runtimes and drives tonic service methods directly for deterministic assertions.
13
+ // - Uses focused fixtures instead of full external environments so protocol invariants stay easy to localize.
14
+ //
15
+ // Usage Contract:
16
+ // - Add new assertions here before changing runtime behavior for the covered service area.
17
+ // - Prefer explicit value checks over timing-sensitive or order-fragile expectations.
18
+ //
19
+ // Architectural Position:
20
+ // - Runtime verification boundary protecting public contract stability.
21
+ //
22
+ // Author: Silan.Hu
23
+ // Email: silan.hu@u.nus.edu
24
+ // Copyright (c) 2026-2027 easynet. All rights reserved.
25
+
1
26
  import { describe, it } from "node:test";
2
27
  import assert from "node:assert/strict";
3
28
 
4
29
  import { RemoteControlCaseKit } from "./kit.js";
30
+ import { buildDescriptor, sanitizeId } from "./descriptor.js";
5
31
  import { remoteControlToolSpecs } from "./specs.js";
6
32
  import { consumeStream } from "../../mcp/server.js";
7
33
  import { DEFAULT_MCP_TOOL_STREAM_TIMEOUT_MS } from "./config.js";
@@ -18,6 +44,7 @@ function makeOrchestrator() {
18
44
  },
19
45
  }),
20
46
  disconnectDevice: (nodeId, reason) => ({ node_id: nodeId, reason }),
47
+ drainNode: (nodeId, reason) => ({ node_id: nodeId, status: "drained", reason }),
21
48
  uninstallAbility: (nodeId, installId, reason) => ({
22
49
  node_id: nodeId,
23
50
  install_id: installId,
@@ -35,6 +62,8 @@ describe("RemoteControlCaseKit", () => {
35
62
  assert.ok(names.includes("disconnect_device"));
36
63
  assert.ok(names.includes("uninstall_ability"));
37
64
  assert.ok(names.includes("call_remote_tool_stream"));
65
+ assert.ok(names.includes("build_ability_descriptor"));
66
+ assert.ok(names.includes("redeploy_ability"));
38
67
  });
39
68
 
40
69
  it("dispatches disconnect_device", () => {
@@ -94,6 +123,93 @@ describe("RemoteControlCaseKit", () => {
94
123
  });
95
124
  });
96
125
 
126
+ it("rejects forget_all without confirm when dry_run is false", () => {
127
+ const kit = new RemoteControlCaseKit(
128
+ {
129
+ endpoint: "http://127.0.0.1:50051",
130
+ tenant: "tenant-a",
131
+ connectTimeoutMs: 5000,
132
+ signatureBase64: "sig",
133
+ },
134
+ () => makeOrchestrator(),
135
+ );
136
+
137
+ const result = kit.handleToolCall("forget_all", { node_id: "node-a" });
138
+
139
+ assert.equal(result.isError, true);
140
+ assert.equal(result.payload.error, "forget_all requires confirm: true (destructive operation)");
141
+ });
142
+
143
+ it("rejects build_ability_descriptor when name is empty", () => {
144
+ const kit = new RemoteControlCaseKit(
145
+ {
146
+ endpoint: "http://127.0.0.1:50051",
147
+ tenant: "tenant-a",
148
+ connectTimeoutMs: 5000,
149
+ signatureBase64: "sig",
150
+ },
151
+ () => makeOrchestrator(),
152
+ );
153
+
154
+ const result = kit.handleToolCall("build_ability_descriptor", {
155
+ command_template: "echo hi",
156
+ });
157
+
158
+ assert.equal(result.isError, true);
159
+ assert.equal(result.payload.error, "name is required");
160
+ });
161
+
162
+ it("rejects redeploy_ability when tool_name is missing", () => {
163
+ const kit = new RemoteControlCaseKit(
164
+ {
165
+ endpoint: "http://127.0.0.1:50051",
166
+ tenant: "tenant-a",
167
+ connectTimeoutMs: 5000,
168
+ signatureBase64: "sig",
169
+ },
170
+ () => makeOrchestrator(),
171
+ );
172
+
173
+ const result = kit.handleToolCall("redeploy_ability", {
174
+ node_id: "node-a",
175
+ command_template: "echo hi",
176
+ });
177
+
178
+ assert.equal(result.isError, true);
179
+ assert.equal(result.payload.error, "tool_name is required");
180
+ });
181
+
182
+ it("filters list_abilities to entries with install_id", () => {
183
+ const kit = new RemoteControlCaseKit(
184
+ {
185
+ endpoint: "http://127.0.0.1:50051",
186
+ tenant: "tenant-a",
187
+ connectTimeoutMs: 5000,
188
+ signatureBase64: "sig",
189
+ },
190
+ () => ({
191
+ ...makeOrchestrator(),
192
+ listMcpTools: () => [
193
+ { tool_name: "keep-me", description: "ok", capability_name: "cap.keep", install_id: "install-1" },
194
+ { tool_name: "skip-me", description: "synthetic", capability_name: "cap.skip", install_id: "" },
195
+ ],
196
+ }),
197
+ );
198
+
199
+ const result = kit.handleToolCall("list_abilities", { node_id: "node-a" });
200
+
201
+ assert.equal(result.isError, false);
202
+ assert.equal(result.payload.count, 1);
203
+ assert.deepEqual(result.payload.abilities, [
204
+ {
205
+ tool_name: "keep-me",
206
+ description: "ok",
207
+ capability_name: "cap.keep",
208
+ install_id: "install-1",
209
+ },
210
+ ]);
211
+ });
212
+
97
213
  it("closes the orchestrator when a streaming handle is closed", () => {
98
214
  let streamClosed = 0;
99
215
  let orchestratorClosed = 0;
@@ -502,6 +618,435 @@ describe("RemoteControlCaseKit", () => {
502
618
  assert.deepEqual(payload.chunks, ["stream-chunk-1", "stream-chunk-2"]);
503
619
  });
504
620
 
621
+ // -----------------------------------------------------------------
622
+ // drain_device tests
623
+ // -----------------------------------------------------------------
624
+ it("dispatches drain_device happy path", () => {
625
+ const kit = new RemoteControlCaseKit(
626
+ {
627
+ endpoint: "http://127.0.0.1:50051",
628
+ tenant: "tenant-a",
629
+ connectTimeoutMs: 5000,
630
+ signatureBase64: "sig",
631
+ },
632
+ () => makeOrchestrator(),
633
+ );
634
+
635
+ const result = kit.handleToolCall("drain_device", {
636
+ node_id: "node-a",
637
+ reason: "maintenance window",
638
+ });
639
+
640
+ assert.equal(result.isError, false);
641
+ assert.equal(result.payload.ok, true);
642
+ assert.equal(result.payload.tenant_id, "tenant-a");
643
+ assert.equal(result.payload.node_id, "node-a");
644
+ assert.equal(result.payload.status, "draining");
645
+ assert.equal(result.payload.response.node_id, "node-a");
646
+ assert.equal(result.payload.response.reason, "maintenance window");
647
+ });
648
+
649
+ it("rejects drain_device with missing node_id", () => {
650
+ const kit = new RemoteControlCaseKit(
651
+ {
652
+ endpoint: "http://127.0.0.1:50051",
653
+ tenant: "tenant-a",
654
+ connectTimeoutMs: 5000,
655
+ signatureBase64: "sig",
656
+ },
657
+ () => makeOrchestrator(),
658
+ );
659
+
660
+ const result = kit.handleToolCall("drain_device", { node_id: "" });
661
+
662
+ assert.equal(result.isError, true);
663
+ assert.equal(result.payload.error, "node_id is required");
664
+ });
665
+
666
+ // -----------------------------------------------------------------
667
+ // build_ability_descriptor tests
668
+ // -----------------------------------------------------------------
669
+ it("dispatches build_ability_descriptor happy path", () => {
670
+ const kit = new RemoteControlCaseKit(
671
+ {
672
+ endpoint: "http://127.0.0.1:50051",
673
+ tenant: "tenant-a",
674
+ connectTimeoutMs: 5000,
675
+ signatureBase64: "sig",
676
+ },
677
+ () => makeOrchestrator(),
678
+ );
679
+
680
+ const result = kit.handleToolCall("build_ability_descriptor", {
681
+ name: "screenshot",
682
+ command_template: "screencapture -x /tmp/shot.png",
683
+ description: "Capture a screenshot",
684
+ });
685
+
686
+ assert.equal(result.isError, false);
687
+ assert.equal(result.payload.ok, true);
688
+ const descriptor = result.payload.descriptor;
689
+ assert.ok(descriptor);
690
+ assert.equal(descriptor.name, "screenshot");
691
+ assert.match(descriptor.toolName, /^ability_screenshot$/);
692
+ assert.equal(descriptor.description, "Capture a screenshot");
693
+ assert.equal(descriptor.commandTemplate, "screencapture -x /tmp/shot.png");
694
+ });
695
+
696
+ it("dispatches build_ability_descriptor with agent extensions", () => {
697
+ const kit = new RemoteControlCaseKit(
698
+ {
699
+ endpoint: "http://127.0.0.1:50051",
700
+ tenant: "tenant-a",
701
+ connectTimeoutMs: 5000,
702
+ signatureBase64: "sig",
703
+ },
704
+ () => makeOrchestrator(),
705
+ );
706
+
707
+ const result = kit.handleToolCall("build_ability_descriptor", {
708
+ name: "gpu-info",
709
+ command_template: "nvidia-smi --query-gpu=name --format=csv",
710
+ instructions: "Use this to query GPU status on the device.",
711
+ input_examples: [{ query: "temperature" }],
712
+ prerequisites: ["GPU driver installed"],
713
+ context_bindings: { "env.CUDA_HOME": "/usr/local/cuda" },
714
+ category: "system",
715
+ });
716
+
717
+ assert.equal(result.isError, false);
718
+ const d = result.payload.descriptor;
719
+ assert.equal(d.instructions, "Use this to query GPU status on the device.");
720
+ assert.deepEqual(d.inputExamples, [{ query: "temperature" }]);
721
+ assert.deepEqual(d.prerequisites, ["GPU driver installed"]);
722
+ assert.deepEqual(d.contextBindings, { "env.CUDA_HOME": "/usr/local/cuda" });
723
+ assert.equal(d.category, "system");
724
+ });
725
+
726
+ it("buildDescriptor stores prerequisites as a JSON array", () => {
727
+ const descriptor = buildDescriptor({
728
+ ability_name: "gpu-info",
729
+ command_template: "nvidia-smi --query-gpu=name --format=csv",
730
+ prerequisites: ["GPU, driver installed", "session_start"],
731
+ }, "sig");
732
+
733
+ assert.equal(
734
+ descriptor.metadata["mcp.prerequisites"],
735
+ JSON.stringify(["GPU, driver installed", "session_start"]),
736
+ );
737
+ });
738
+
739
+ it("sanitizeId rejects names that normalize to empty", () => {
740
+ assert.throws(() => sanitizeId("数据分析"), /identifier contains no valid characters/);
741
+ });
742
+
743
+ // -----------------------------------------------------------------
744
+ // export_ability_skill tests
745
+ // -----------------------------------------------------------------
746
+ it("dispatches export_ability_skill and generates SKILL.md", () => {
747
+ const kit = new RemoteControlCaseKit(
748
+ {
749
+ endpoint: "http://127.0.0.1:50051",
750
+ tenant: "tenant-a",
751
+ connectTimeoutMs: 5000,
752
+ signatureBase64: "sig",
753
+ },
754
+ () => makeOrchestrator(),
755
+ );
756
+
757
+ const result = kit.handleToolCall("export_ability_skill", {
758
+ name: "disk-usage",
759
+ command_template: "df -h",
760
+ target: "claude",
761
+ });
762
+
763
+ assert.equal(result.isError, false);
764
+ assert.equal(result.payload.ok, true);
765
+ assert.ok(typeof result.payload.ability_md === "string");
766
+ assert.ok(typeof result.payload.invoke_script === "string");
767
+ assert.ok(result.payload.ability_md.length > 0);
768
+ assert.ok(result.payload.invoke_script.length > 0);
769
+ // Claude target should include allowed-tools
770
+ assert.ok(result.payload.ability_md.includes("allowed-tools"));
771
+ // Invoke script should be a bash script
772
+ assert.ok(result.payload.invoke_script.includes("#!/usr/bin/env bash"));
773
+ });
774
+
775
+ it("rejects export_ability_skill with shell-unsafe endpoint", () => {
776
+ const kit = new RemoteControlCaseKit(
777
+ {
778
+ endpoint: "http://127.0.0.1:50051",
779
+ tenant: "tenant-a",
780
+ connectTimeoutMs: 5000,
781
+ signatureBase64: "sig",
782
+ },
783
+ () => makeOrchestrator(),
784
+ );
785
+
786
+ const result = kit.handleToolCall("export_ability_skill", {
787
+ name: "probe",
788
+ command_template: "echo hello",
789
+ axon_endpoint: "http://evil.com`rm -rf /`",
790
+ });
791
+
792
+ assert.equal(result.isError, true);
793
+ assert.ok(result.payload.error);
794
+ assert.match(result.payload.error, /disallowed shell characters/);
795
+ });
796
+
797
+ it("rejects export_ability_skill with carriage-return endpoint", () => {
798
+ const kit = new RemoteControlCaseKit(
799
+ {
800
+ endpoint: "http://127.0.0.1:50051",
801
+ tenant: "tenant-a",
802
+ connectTimeoutMs: 5000,
803
+ signatureBase64: "sig",
804
+ },
805
+ () => makeOrchestrator(),
806
+ );
807
+
808
+ const result = kit.handleToolCall("export_ability_skill", {
809
+ name: "probe",
810
+ command_template: "echo hello",
811
+ axon_endpoint: "http://evil.com/\rprobe",
812
+ });
813
+
814
+ assert.equal(result.isError, true);
815
+ assert.ok(result.payload.error);
816
+ assert.match(result.payload.error, /disallowed shell characters/);
817
+ });
818
+
819
+ // -----------------------------------------------------------------
820
+ // forget_all tests
821
+ // -----------------------------------------------------------------
822
+ it("dispatches forget_all dry_run and returns would-remove list", () => {
823
+ const kit = new RemoteControlCaseKit(
824
+ {
825
+ endpoint: "http://127.0.0.1:50051",
826
+ tenant: "tenant-a",
827
+ connectTimeoutMs: 5000,
828
+ signatureBase64: "sig",
829
+ },
830
+ () => ({
831
+ ...makeOrchestrator(),
832
+ listMcpTools: () => [
833
+ { tool_name: "tool-with-id", description: "ok", capability_name: "cap.a", install_id: "inst-1" },
834
+ { tool_name: "tool-without-id", description: "synthetic", capability_name: "cap.b", install_id: "" },
835
+ ],
836
+ }),
837
+ );
838
+
839
+ const result = kit.handleToolCall("forget_all", {
840
+ node_id: "node-a",
841
+ dry_run: true,
842
+ });
843
+
844
+ assert.equal(result.isError, false);
845
+ assert.equal(result.payload.ok, true);
846
+ assert.equal(result.payload.dry_run, true);
847
+ assert.deepEqual(result.payload.removed, ["tool-with-id"]);
848
+ assert.equal(result.payload.removed_count, 1);
849
+ assert.equal(result.payload.failed_count, 1);
850
+ assert.equal(result.payload.failed[0].tool_name, "tool-without-id");
851
+ });
852
+
853
+ it("dispatches forget_all with confirm and removes abilities", () => {
854
+ let uninstallCalls = [];
855
+ const kit = new RemoteControlCaseKit(
856
+ {
857
+ endpoint: "http://127.0.0.1:50051",
858
+ tenant: "tenant-a",
859
+ connectTimeoutMs: 5000,
860
+ signatureBase64: "sig",
861
+ },
862
+ () => ({
863
+ ...makeOrchestrator(),
864
+ listMcpTools: () => [
865
+ { tool_name: "my-tool", description: "test", capability_name: "cap.x", install_id: "inst-7" },
866
+ ],
867
+ uninstallAbility: (nodeId, installId, reason) => {
868
+ uninstallCalls.push({ nodeId, installId, reason });
869
+ return { node_id: nodeId, install_id: installId, reason };
870
+ },
871
+ }),
872
+ );
873
+
874
+ const result = kit.handleToolCall("forget_all", {
875
+ node_id: "node-a",
876
+ confirm: true,
877
+ });
878
+
879
+ assert.equal(result.isError, false);
880
+ assert.equal(result.payload.ok, true);
881
+ assert.equal(result.payload.dry_run, false);
882
+ assert.deepEqual(result.payload.removed, ["my-tool"]);
883
+ assert.equal(result.payload.removed_count, 1);
884
+ assert.equal(result.payload.failed_count, 0);
885
+ assert.equal(uninstallCalls.length, 1);
886
+ assert.equal(uninstallCalls[0].installId, "inst-7");
887
+ });
888
+
889
+ // -----------------------------------------------------------------
890
+ // redeploy_ability tests
891
+ // -----------------------------------------------------------------
892
+ it("dispatches redeploy_ability happy path", () => {
893
+ const kit = new RemoteControlCaseKit(
894
+ {
895
+ endpoint: "http://127.0.0.1:50051",
896
+ tenant: "tenant-a",
897
+ connectTimeoutMs: 5000,
898
+ signatureBase64: "sig",
899
+ },
900
+ () => ({
901
+ ...makeOrchestrator(),
902
+ deployAbilityPackage: () => ({ install_id: "inst-99", tool_name: "my-tool" }),
903
+ }),
904
+ );
905
+
906
+ const result = kit.handleToolCall("redeploy_ability", {
907
+ node_id: "node-a",
908
+ tool_name: "my-tool",
909
+ command_template: "echo updated",
910
+ });
911
+
912
+ assert.equal(result.isError, false);
913
+ assert.equal(result.payload.ok, true);
914
+ assert.equal(result.payload.status, "redeployed");
915
+ assert.equal(result.payload.node_id, "node-a");
916
+ assert.equal(result.payload.tool_name, "my-tool");
917
+ assert.equal(result.payload.install_id, "inst-99");
918
+ });
919
+
920
+ // -----------------------------------------------------------------
921
+ // export_ability_skill verifies SKILL.md structure
922
+ // -----------------------------------------------------------------
923
+ it("export_ability_skill verifies SKILL.md structure", () => {
924
+ const kit = new RemoteControlCaseKit(
925
+ {
926
+ endpoint: "http://127.0.0.1:50051",
927
+ tenant: "tenant-a",
928
+ connectTimeoutMs: 5000,
929
+ signatureBase64: "sig",
930
+ },
931
+ () => makeOrchestrator(),
932
+ );
933
+
934
+ const result = kit.handleToolCall("export_ability_skill", {
935
+ name: "my-skill",
936
+ command_template: "echo hello",
937
+ target: "claude",
938
+ });
939
+
940
+ assert.equal(result.isError, false);
941
+ assert.ok(result.payload.ability_md.includes("---\nname:"), "ability_md should contain frontmatter start");
942
+ assert.ok(result.payload.ability_md.includes("## Parameters"), "ability_md should contain Parameters section");
943
+ assert.ok(result.payload.ability_md.includes("| Name |"), "ability_md should contain parameter table header");
944
+ assert.ok(result.payload.invoke_script.includes("curl -sS -X POST"), "invoke_script should contain curl POST");
945
+ });
946
+
947
+ // -----------------------------------------------------------------
948
+ // build_ability_descriptor with Unicode name
949
+ // -----------------------------------------------------------------
950
+ it("build_ability_descriptor with Unicode name", () => {
951
+ const kit = new RemoteControlCaseKit(
952
+ {
953
+ endpoint: "http://127.0.0.1:50051",
954
+ tenant: "tenant-a",
955
+ connectTimeoutMs: 5000,
956
+ signatureBase64: "sig",
957
+ },
958
+ () => makeOrchestrator(),
959
+ );
960
+
961
+ const result = kit.handleToolCall("build_ability_descriptor", {
962
+ name: "数据分析",
963
+ command_template: "python3 analyze.py",
964
+ });
965
+
966
+ // Unicode-only names are rejected by sanitizeId (ASCII identifiers only).
967
+ assert.equal(result.isError, true);
968
+ assert.ok(result.payload.error);
969
+ });
970
+
971
+ // -----------------------------------------------------------------
972
+ // forget_all confirmed removes and reports failures
973
+ // -----------------------------------------------------------------
974
+ it("forget_all confirmed removes and reports failures", () => {
975
+ let uninstallCalls = [];
976
+ const kit = new RemoteControlCaseKit(
977
+ {
978
+ endpoint: "http://127.0.0.1:50051",
979
+ tenant: "tenant-a",
980
+ connectTimeoutMs: 5000,
981
+ signatureBase64: "sig",
982
+ },
983
+ () => ({
984
+ ...makeOrchestrator(),
985
+ listMcpTools: () => [
986
+ { tool_name: "tool-a", description: "ok", capability_name: "cap.a", install_id: "inst-1" },
987
+ { tool_name: "tool-b", description: "no id", capability_name: "cap.b", install_id: "" },
988
+ { tool_name: "tool-c", description: "whitespace id", capability_name: "cap.c", install_id: " " },
989
+ ],
990
+ uninstallAbility: (nodeId, installId, reason) => {
991
+ uninstallCalls.push({ nodeId, installId, reason });
992
+ return { node_id: nodeId, install_id: installId, reason };
993
+ },
994
+ }),
995
+ );
996
+
997
+ const result = kit.handleToolCall("forget_all", {
998
+ node_id: "node-a",
999
+ confirm: true,
1000
+ });
1001
+
1002
+ assert.equal(result.isError, false);
1003
+ assert.equal(result.payload.ok, true);
1004
+ assert.equal(result.payload.removed.length, 1);
1005
+ assert.equal(result.payload.failed.length, 2);
1006
+ assert.equal(uninstallCalls.length, 1);
1007
+ assert.equal(uninstallCalls[0].installId, "inst-1");
1008
+ });
1009
+
1010
+ // -----------------------------------------------------------------
1011
+ // build_ability_descriptor rejects empty command_template
1012
+ // -----------------------------------------------------------------
1013
+ it("build_ability_descriptor rejects empty command_template", () => {
1014
+ const kit = new RemoteControlCaseKit(
1015
+ {
1016
+ endpoint: "http://127.0.0.1:50051",
1017
+ tenant: "tenant-a",
1018
+ connectTimeoutMs: 5000,
1019
+ signatureBase64: "sig",
1020
+ },
1021
+ () => makeOrchestrator(),
1022
+ );
1023
+
1024
+ const result = kit.handleToolCall("build_ability_descriptor", {
1025
+ name: "test",
1026
+ });
1027
+
1028
+ assert.equal(result.isError, true);
1029
+ assert.ok(result.payload.error);
1030
+ });
1031
+
1032
+ // -----------------------------------------------------------------
1033
+ // Spec validation tests
1034
+ // -----------------------------------------------------------------
1035
+ it("all tool specs have unique names", () => {
1036
+ const specs = remoteControlToolSpecs();
1037
+ const names = specs.map((s) => s.name);
1038
+ const unique = new Set(names);
1039
+ assert.equal(names.length, unique.size, `Duplicate spec names found: ${names.filter((n, i) => names.indexOf(n) !== i)}`);
1040
+ });
1041
+
1042
+ it("all specs have required inputSchema fields", () => {
1043
+ const specs = remoteControlToolSpecs();
1044
+ for (const spec of specs) {
1045
+ assert.ok(spec.inputSchema, `spec ${spec.name} is missing inputSchema`);
1046
+ assert.equal(spec.inputSchema.type, "object", `spec ${spec.name} inputSchema.type should be "object"`);
1047
+ }
1048
+ });
1049
+
505
1050
  it("uses default timeout when timeout_ms is missing", () => {
506
1051
  let capturedOptions = null;
507
1052
  const kit = new RemoteControlCaseKit(
@@ -15,6 +15,7 @@ export interface RemoteOrchestrator {
15
15
  callMcpToolStream(toolName: string, targetNodeId: string, argumentsJson: JsonRecord, options?: {
16
16
  timeoutMs?: number;
17
17
  }): DendriteServerStream;
18
+ drainNode(nodeId: string, reason: string): JsonRecord;
18
19
  disconnectDevice(nodeId: string, reason: string): JsonRecord;
19
20
  uninstallAbility(nodeId: string, installId: string, reason: string): JsonRecord;
20
21
  deployAbilityPackage(descriptor: AbilityPackageDescriptor, nodeId: string, cleanupOnActivateFailure: boolean): JsonRecord;
@@ -1,3 +1,27 @@
1
+ // EasyNet Axon for AgentNet
2
+ // =========================
3
+ //
4
+ // File: sdk/node/src/presets/remote_control/orchestrator.ts
5
+ // Description: Node remote-control orchestrator adapter that translates preset calls into DendriteBridge operations.
6
+ //
7
+ // Protocol Responsibility:
8
+ // - Wraps low-level bridge operations in remote-control terminology such as ability, device, and tool workflows.
9
+ // - Centralizes publish, install, invoke, cleanup, and list flows shared by remote-control handlers.
10
+ //
11
+ // Implementation Approach:
12
+ // - Keeps bridge semantics explicit while translating internal capability and node naming into public preset APIs.
13
+ // - Holds connection and timeout policy close to the transport boundary so handler code stays declarative.
14
+ //
15
+ // Usage Contract:
16
+ // - Callers should construct or reuse orchestrators with valid endpoint, tenant, and native-library context.
17
+ // - Close or release orchestrator resources when the preset is no longer serving requests.
18
+ //
19
+ // Architectural Position:
20
+ // - Mid-layer adapter between remote-control handlers and bridge or client transport implementations.
21
+ //
22
+ // Author: Silan.Hu
23
+ // Email: silan.hu@u.nus.edu
24
+ // Copyright (c) 2026-2027 easynet. All rights reserved.
1
25
  import { DendriteBridge, } from "../../dendrite_bridge.js";
2
26
  import { DEFAULT_EXECUTION_MODE, DEFAULT_INSTALL_TIMEOUT_SECONDS } from "./config.js";
3
27
  export function buildOrchestrator(config, tenant) {
@@ -34,6 +58,9 @@ class OrchestratorAdapter {
34
58
  };
35
59
  return this.bridge.callMcpToolStream(this.tenant, toolName, streamOptions);
36
60
  }
61
+ drainNode(nodeId, reason) {
62
+ return this.bridge.drainNode(this.tenant, nodeId, reason);
63
+ }
37
64
  disconnectDevice(nodeId, reason) {
38
65
  return this.bridge.deregisterNode(this.tenant, nodeId, reason);
39
66
  }