agenticros 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/__tests__/robot-config.test.d.ts +2 -0
  2. package/dist/__tests__/robot-config.test.d.ts.map +1 -0
  3. package/dist/__tests__/robot-config.test.js +374 -0
  4. package/dist/__tests__/robot-config.test.js.map +1 -0
  5. package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
  6. package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
  7. package/dist/__tests__/transport-shorthand.test.js +88 -0
  8. package/dist/__tests__/transport-shorthand.test.js.map +1 -0
  9. package/dist/commands/robots.d.ts +57 -0
  10. package/dist/commands/robots.d.ts.map +1 -0
  11. package/dist/commands/robots.js +678 -0
  12. package/dist/commands/robots.js.map +1 -0
  13. package/dist/commands/skills.d.ts.map +1 -1
  14. package/dist/commands/skills.js +125 -2
  15. package/dist/commands/skills.js.map +1 -1
  16. package/dist/index.js +29 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/menu.js +22 -4
  19. package/dist/menu.js.map +1 -1
  20. package/dist/util/marketplace.d.ts +57 -0
  21. package/dist/util/marketplace.d.ts.map +1 -0
  22. package/dist/util/marketplace.js +144 -0
  23. package/dist/util/marketplace.js.map +1 -0
  24. package/dist/util/mcp-discovery.d.ts +57 -0
  25. package/dist/util/mcp-discovery.d.ts.map +1 -0
  26. package/dist/util/mcp-discovery.js +153 -0
  27. package/dist/util/mcp-discovery.js.map +1 -0
  28. package/dist/util/robot-config.d.ts +167 -0
  29. package/dist/util/robot-config.d.ts.map +1 -0
  30. package/dist/util/robot-config.js +375 -0
  31. package/dist/util/robot-config.js.map +1 -0
  32. package/dist/util/skills.d.ts +6 -6
  33. package/dist/util/skills.d.ts.map +1 -1
  34. package/dist/util/skills.js +14 -9
  35. package/dist/util/skills.js.map +1 -1
  36. package/dist/util/transport-shorthand.d.ts +66 -0
  37. package/dist/util/transport-shorthand.d.ts.map +1 -0
  38. package/dist/util/transport-shorthand.js +102 -0
  39. package/dist/util/transport-shorthand.js.map +1 -0
  40. package/package.json +12 -10
  41. package/runtime/BUNDLE.json +1 -1
  42. package/runtime/README.md +62 -1
  43. package/runtime/package.json +6 -1
  44. package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
  45. package/runtime/packages/agenticros/package.json +2 -1
  46. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
  47. package/runtime/packages/agenticros/src/config-page.ts +1 -1
  48. package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
  49. package/runtime/packages/agenticros/src/service.ts +120 -2
  50. package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
  51. package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
  52. package/runtime/packages/agenticros/src/tools/index.ts +49 -9
  53. package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
  54. package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
  55. package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
  56. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
  57. package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
  58. package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
  59. package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
  60. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
  61. package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
  62. package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
  63. package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
  64. package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
  65. package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
  66. package/runtime/packages/agenticros-claude-code/README.md +52 -9
  67. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
  68. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
  69. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
  70. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
  71. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
  72. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
  73. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
  74. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
  75. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
  76. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
  77. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
  78. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
  79. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
  80. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
  81. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
  82. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
  83. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
  84. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
  85. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
  86. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
  87. package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
  88. package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
  89. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
  90. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
  91. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
  92. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
  93. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
  94. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  95. package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
  96. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  97. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
  98. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
  99. package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
  100. package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
  101. package/runtime/packages/agenticros-claude-code/package.json +2 -1
  102. package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
  103. package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
  104. package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
  105. package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
  106. package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
  107. package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
  108. package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
  109. package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
  110. package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
  111. package/runtime/packages/agenticros-gemini/package.json +1 -0
  112. package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
  113. package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
  114. package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
  115. package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
  116. package/runtime/packages/core/LICENSE +192 -0
  117. package/runtime/packages/core/README.md +88 -0
  118. package/runtime/packages/core/package.json +36 -3
  119. package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
  120. package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
  121. package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
  122. package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
  123. package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
  124. package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
  125. package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
  126. package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
  127. package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
  128. package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
  129. package/runtime/packages/core/src/capabilities.ts +314 -0
  130. package/runtime/packages/core/src/config.ts +221 -39
  131. package/runtime/packages/core/src/discovery.ts +193 -0
  132. package/runtime/packages/core/src/find-robots-for.ts +164 -0
  133. package/runtime/packages/core/src/index.ts +67 -1
  134. package/runtime/packages/core/src/mission-registry.ts +142 -0
  135. package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
  136. package/runtime/packages/core/src/mission.ts +574 -0
  137. package/runtime/packages/core/src/planner/index.ts +564 -0
  138. package/runtime/packages/core/src/robots.ts +282 -0
  139. package/runtime/packages/core/src/topic-utils.ts +47 -9
  140. package/runtime/packages/core/src/transport-pool.ts +171 -0
  141. package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
  142. package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
  143. package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
  144. package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
  145. package/runtime/scripts/configure_agenticros.sh +0 -0
  146. package/runtime/scripts/configure_for_sim.sh +0 -0
  147. package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
  148. package/runtime/scripts/install_cli.sh +0 -0
  149. package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
  150. package/runtime/scripts/onboard_robot.sh +0 -0
  151. package/runtime/scripts/pack-runtime.mjs +0 -0
  152. package/runtime/scripts/refresh-skill-deps.mjs +381 -0
  153. package/runtime/scripts/run_demo_native.sh +0 -0
  154. package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
  155. package/runtime/scripts/run_robot_rosbridge.sh +0 -0
  156. package/runtime/scripts/setup_gateway_plugin.sh +32 -5
  157. package/runtime/scripts/setup_robot.sh +0 -0
  158. package/runtime/scripts/setup_workspace.sh +0 -0
  159. package/runtime/scripts/sim/run_sim.sh +0 -0
  160. package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
  161. package/runtime/scripts/start_demo.sh +0 -0
  162. package/runtime/scripts/sync-skill-tools.mjs +69 -12
  163. package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
  164. package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Capability registry — Phase 1.a/1.b of the AgenticROS strategy.
3
+ *
4
+ * A capability is a named, declarable verb an LLM can plan against
5
+ * (`follow_person`, `find_object`, `take_snapshot`, `drive_base`).
6
+ * It's deliberately shaped to match inter-agent protocol agent cards
7
+ * (ACP / A2A) so a skill's capability today is readable as an agent
8
+ * capability when Phase 4 lands — no rewrite required.
9
+ *
10
+ * Two kinds of capabilities:
11
+ * - **Intrinsic** — built into every AgenticROS deployment (drive_base,
12
+ * take_snapshot, measure_depth, list_topics, publish, subscribe). Lives
13
+ * in this file as `BUILTIN_CAPABILITIES`.
14
+ * - **Skill-declared** — comes from a skill package's `package.json`
15
+ * under `agenticrosSkill.capabilities[]` (or a sibling
16
+ * `capabilities.json`). Read at adapter startup.
17
+ *
18
+ * See: docs/strategy-ai-agents-plus-ros.md §4 (Phase 1).
19
+ */
20
+
21
+ import { createRequire } from "node:module";
22
+ import { readFileSync, existsSync } from "node:fs";
23
+ import { join, resolve as resolvePath } from "node:path";
24
+ import type { AgenticROSConfig } from "./config.js";
25
+
26
+ /** Where a capability came from. */
27
+ export type CapabilitySource =
28
+ | { kind: "builtin" }
29
+ | { kind: "skill"; skillId: string; package: string; path?: string };
30
+
31
+ /** Implementation hint — in-process Node.js skill vs external ROS node. */
32
+ export type CapabilityImplementation =
33
+ | { kind: "in_process" }
34
+ | {
35
+ kind: "external_ros_node";
36
+ package?: string;
37
+ launch?: string;
38
+ action?: string;
39
+ service?: string;
40
+ topic?: string;
41
+ msg_type?: string;
42
+ };
43
+
44
+ /** Typed input/output schema — minimal, JSON-Schema-ish. */
45
+ export interface CapabilityField {
46
+ type: string;
47
+ description?: string;
48
+ optional?: boolean;
49
+ default?: unknown;
50
+ }
51
+
52
+ /**
53
+ * One capability the agent can plan against.
54
+ *
55
+ * Required: `id`, `verb`, `description`.
56
+ * Everything else is optional so a skill can adopt the manifest
57
+ * incrementally.
58
+ */
59
+ export interface Capability {
60
+ /** Unique identifier within a robot (e.g. `follow_person`, `find_object`). */
61
+ id: string;
62
+ /** High-level verb (`follow`, `find`, `navigate`, `manipulate`, `map`, `detect`). */
63
+ verb: string;
64
+ /** One-line, human-readable description. */
65
+ description: string;
66
+ /** Optional structured inputs the agent can fill. */
67
+ inputs?: Record<string, CapabilityField>;
68
+ /** Optional structured outputs the skill emits. */
69
+ outputs?: Record<string, CapabilityField>;
70
+ /** Free-form preconditions ("depthTopic available", "person detected"). */
71
+ preconditions?: string[];
72
+ /** Can be canceled mid-execution? Default true for intrinsic, varies for skills. */
73
+ interruptible?: boolean;
74
+ /** Owns the robot base (cmd_vel); other base-owning skills should yield. */
75
+ blocks_base?: boolean;
76
+ /** How the capability is implemented. Defaults to `in_process` when unset. */
77
+ implementation?: CapabilityImplementation;
78
+ /** Set by the registry, not the skill author. */
79
+ source?: CapabilitySource;
80
+ }
81
+
82
+ /**
83
+ * Built-in robot verbs — wrap the raw MCP tools in agent-meaningful
84
+ * capability names. The intrinsic set is intentionally small and stable
85
+ * so the agent's planning surface stays understandable as skill catalogs
86
+ * grow.
87
+ */
88
+ export const BUILTIN_CAPABILITIES: readonly Capability[] = [
89
+ {
90
+ id: "drive_base",
91
+ verb: "drive",
92
+ description:
93
+ "Drive the robot base by publishing geometry_msgs/Twist to the configured cmd_vel topic. " +
94
+ "Linear/angular velocities are clamped server-side by safety.maxLinearVelocity / maxAngularVelocity.",
95
+ inputs: {
96
+ linear_x: { type: "number", description: "Forward velocity (m/s).", optional: true },
97
+ angular_z: { type: "number", description: "Yaw velocity (rad/s).", optional: true },
98
+ },
99
+ interruptible: true,
100
+ blocks_base: true,
101
+ source: { kind: "builtin" },
102
+ },
103
+ {
104
+ id: "take_snapshot",
105
+ verb: "see",
106
+ description:
107
+ "Capture one frame from the configured camera topic. Returns base64-encoded JPEG/PNG.",
108
+ inputs: {
109
+ topic: { type: "string", description: "Camera topic (defaults to robot.cameraTopic).", optional: true },
110
+ },
111
+ outputs: {
112
+ format: { type: "string", description: "Image format (jpeg / png)." },
113
+ width: { type: "number", description: "Image width in pixels." },
114
+ height: { type: "number", description: "Image height in pixels." },
115
+ },
116
+ interruptible: true,
117
+ source: { kind: "builtin" },
118
+ },
119
+ {
120
+ id: "measure_depth",
121
+ verb: "measure",
122
+ description:
123
+ "Sample the center of a depth image and return a distance in meters. " +
124
+ "Use to answer 'how far am I from X' style questions.",
125
+ inputs: {
126
+ topic: { type: "string", description: "Depth topic.", optional: true },
127
+ },
128
+ outputs: {
129
+ distance_m: { type: "number", description: "Distance in meters at image center." },
130
+ median_m: { type: "number", description: "Median distance across sampled pixels." },
131
+ },
132
+ interruptible: true,
133
+ source: { kind: "builtin" },
134
+ },
135
+ {
136
+ id: "list_topics",
137
+ verb: "introspect",
138
+ description:
139
+ "List all reachable ROS 2 topics with their message types. Use for discovery when " +
140
+ "no other capability fits or to confirm what the robot currently publishes.",
141
+ interruptible: true,
142
+ source: { kind: "builtin" },
143
+ },
144
+ {
145
+ id: "publish_topic",
146
+ verb: "publish",
147
+ description:
148
+ "Generic ROS 2 publish escape hatch. Prefer a named capability when one exists; " +
149
+ "publishes are safety-checked but otherwise unmediated.",
150
+ inputs: {
151
+ topic: { type: "string", description: "ROS 2 topic name." },
152
+ type: { type: "string", description: "ROS 2 message type (e.g. geometry_msgs/msg/Twist)." },
153
+ message: { type: "object", description: "Message payload matching the type schema." },
154
+ },
155
+ interruptible: true,
156
+ source: { kind: "builtin" },
157
+ },
158
+ {
159
+ id: "subscribe_once",
160
+ verb: "read",
161
+ description: "Read one message from a ROS 2 topic. Use for one-shot sensor reads or state checks.",
162
+ inputs: {
163
+ topic: { type: "string", description: "ROS 2 topic name." },
164
+ type: { type: "string", description: "ROS 2 message type (optional, often inferred).", optional: true },
165
+ timeout: { type: "number", description: "Milliseconds to wait (default 5000).", optional: true },
166
+ },
167
+ interruptible: true,
168
+ source: { kind: "builtin" },
169
+ },
170
+ ];
171
+
172
+ function isCapabilityLike(value: unknown): value is Partial<Capability> & { id: string } {
173
+ if (!value || typeof value !== "object") return false;
174
+ const v = value as Record<string, unknown>;
175
+ return typeof v.id === "string" && v.id.length > 0;
176
+ }
177
+
178
+ function normalizeCapability(
179
+ raw: Partial<Capability> & { id: string },
180
+ source: CapabilitySource,
181
+ ): Capability {
182
+ return {
183
+ id: raw.id,
184
+ verb: typeof raw.verb === "string" && raw.verb ? raw.verb : raw.id,
185
+ description: typeof raw.description === "string" ? raw.description : "",
186
+ ...(raw.inputs ? { inputs: raw.inputs } : {}),
187
+ ...(raw.outputs ? { outputs: raw.outputs } : {}),
188
+ ...(raw.preconditions ? { preconditions: raw.preconditions } : {}),
189
+ ...(typeof raw.interruptible === "boolean" ? { interruptible: raw.interruptible } : {}),
190
+ ...(typeof raw.blocks_base === "boolean" ? { blocks_base: raw.blocks_base } : {}),
191
+ ...(raw.implementation ? { implementation: raw.implementation } : { implementation: { kind: "in_process" } }),
192
+ source,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Read the `agenticros.capabilities[]` array from a skill's package.json.
198
+ * Returns `null` if the package doesn't declare a valid `agenticros` block.
199
+ *
200
+ * The legacy `agenticrosSkill: true | { capabilities }` form is NOT supported
201
+ * — every skill must use the single `agenticros` block as its source of truth.
202
+ */
203
+ function readSkillManifestFromDir(
204
+ packageDir: string,
205
+ ): { packageName: string; skillId: string; rawCaps: Array<Partial<Capability> & { id: string }> } | null {
206
+ const pkgJsonPath = join(packageDir, "package.json");
207
+ if (!existsSync(pkgJsonPath)) return null;
208
+ let pkg: {
209
+ name?: string;
210
+ agenticros?: { id?: unknown; capabilities?: unknown };
211
+ };
212
+ try {
213
+ pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
214
+ } catch {
215
+ return null;
216
+ }
217
+ if (!pkg.agenticros || typeof pkg.agenticros !== "object") return null;
218
+ if (typeof pkg.agenticros.id !== "string") return null;
219
+
220
+ const packageName = pkg.name ?? "unknown";
221
+ const skillId = pkg.agenticros.id;
222
+ const rawCaps: Array<Partial<Capability> & { id: string }> = [];
223
+
224
+ const caps = pkg.agenticros.capabilities;
225
+ if (Array.isArray(caps)) {
226
+ for (const c of caps) if (isCapabilityLike(c)) rawCaps.push(c);
227
+ }
228
+
229
+ return { packageName, skillId, rawCaps };
230
+ }
231
+
232
+ function resolvePackageDir(packageName: string, searchPaths: string[]): string | null {
233
+ const req = createRequire(import.meta.url);
234
+ for (const base of [...searchPaths, process.cwd()]) {
235
+ try {
236
+ const entry = req.resolve(`${packageName}/package.json`, { paths: [base] });
237
+ return entry.replace(/\/package\.json$/, "");
238
+ } catch {
239
+ }
240
+ }
241
+ try {
242
+ const entry = req.resolve(`${packageName}/package.json`);
243
+ return entry.replace(/\/package\.json$/, "");
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Read every capability declared by every skill referenced in `config`.
251
+ * Sources both `skillPaths` (directories with a package.json) and
252
+ * `skillPackages` (resolvable npm names).
253
+ *
254
+ * Failures are silent — a missing skill is logged elsewhere by the
255
+ * skill loader; this function focuses on returning the capabilities
256
+ * it can read.
257
+ */
258
+ export function readSkillCapabilities(config: AgenticROSConfig): Capability[] {
259
+ const out: Capability[] = [];
260
+ const seen = new Set<string>();
261
+
262
+ const skillPaths = config.skillPaths ?? [];
263
+ const skillPackages = config.skillPackages ?? [];
264
+
265
+ for (const dir of skillPaths) {
266
+ const absDir = resolvePath(dir);
267
+ const manifest = readSkillManifestFromDir(absDir);
268
+ if (!manifest) continue;
269
+ for (const raw of manifest.rawCaps) {
270
+ const key = `${manifest.skillId}:${raw.id}`;
271
+ if (seen.has(key)) continue;
272
+ seen.add(key);
273
+ out.push(
274
+ normalizeCapability(raw, {
275
+ kind: "skill",
276
+ skillId: manifest.skillId,
277
+ package: manifest.packageName,
278
+ path: absDir,
279
+ }),
280
+ );
281
+ }
282
+ }
283
+
284
+ for (const pkgName of skillPackages) {
285
+ const packageDir = resolvePackageDir(pkgName, skillPaths.map((p) => resolvePath(p)));
286
+ if (!packageDir) continue;
287
+ const manifest = readSkillManifestFromDir(packageDir);
288
+ if (!manifest) continue;
289
+ for (const raw of manifest.rawCaps) {
290
+ const key = `${manifest.skillId}:${raw.id}`;
291
+ if (seen.has(key)) continue;
292
+ seen.add(key);
293
+ out.push(
294
+ normalizeCapability(raw, {
295
+ kind: "skill",
296
+ skillId: manifest.skillId,
297
+ package: manifest.packageName,
298
+ path: packageDir,
299
+ }),
300
+ );
301
+ }
302
+ }
303
+
304
+ return out;
305
+ }
306
+
307
+ /**
308
+ * Return the full capability list: built-in robot verbs first, then
309
+ * skill-declared capabilities. This is the shape returned by
310
+ * `ros2_list_capabilities` across every adapter.
311
+ */
312
+ export function listAllCapabilities(config: AgenticROSConfig): Capability[] {
313
+ return [...BUILTIN_CAPABILITIES, ...readSkillCapabilities(config)];
314
+ }
@@ -7,6 +7,130 @@ const IceServerSchema = z.object({
7
7
  credential: z.string().optional(),
8
8
  });
9
9
 
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Transport sub-schemas. Extracted so both the top-level config and the
12
+ // per-robot override in `robots[i].transport` can share them without
13
+ // drifting. The shapes mirror packages/core/src/transport/types.ts.
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ const ZenohSettingsSchema = z.object({
17
+ /** WebSocket URL for zenoh-ts (zenoh-plugin-remote-api). Not tcp/ — use e.g. ws://localhost:10000 */
18
+ routerEndpoint: z.string().default("ws://localhost:10000"),
19
+ domainId: z.number().default(0),
20
+ /** "ros2dds" = zenoh-bridge-ros2dds key format (slashes kept). "rmw_zenoh" = rmw_zenoh key format (domain + %). */
21
+ keyFormat: z.enum(["ros2dds", "rmw_zenoh"]).default("ros2dds"),
22
+ /**
23
+ * Must match zenoh-bridge-ros2dds `plugins.ros2dds.namespace` when it is not the default "/".
24
+ * Example: bridge has `namespace: "/bot1"` → set `bridgeNamespace` to "/bot1" or "bot1".
25
+ * Omit or use "/" when the bridge uses the default (only the ROS topic path is the zenoh key).
26
+ */
27
+ bridgeNamespace: z.string().optional(),
28
+ });
29
+
30
+ const RosbridgeSettingsSchema = z.object({
31
+ url: z.string().default("ws://localhost:9090"),
32
+ reconnect: z.boolean().default(true),
33
+ reconnectInterval: z.number().default(3000),
34
+ });
35
+
36
+ const LocalSettingsSchema = z.object({
37
+ domainId: z.number().default(0),
38
+ });
39
+
40
+ const WebrtcSettingsSchema = z.object({
41
+ signalingUrl: z.string().default(""),
42
+ apiUrl: z.string().default(""),
43
+ robotId: z.string().default(""),
44
+ robotKey: z.string().default(""),
45
+ iceServers: z
46
+ .array(IceServerSchema)
47
+ .default([{ urls: "stun:stun.l.google.com:19302" }]),
48
+ });
49
+
50
+ /**
51
+ * Per-robot transport override sub-schemas (Phase 1.d-resolve).
52
+ *
53
+ * These intentionally MIRROR the top-level shapes but drop every
54
+ * `.default(...)`. Why: when `getTransportConfigForRobot` merges an
55
+ * override over the global config it does a per-field merge
56
+ * (`{ ...global, ...override }`). If the override schema applied
57
+ * defaults (e.g. `domainId: 0`), those defaults would silently clobber
58
+ * the global values whenever the user omitted them — making the
59
+ * "merge only the fields I actually wrote" contract impossible.
60
+ *
61
+ * Every field is optional here. Validation still rejects wrong types
62
+ * (e.g. `routerEndpoint: 42`), which is what we want.
63
+ */
64
+ const ZenohOverrideSettingsSchema = z.object({
65
+ routerEndpoint: z.string().optional(),
66
+ domainId: z.number().optional(),
67
+ keyFormat: z.enum(["ros2dds", "rmw_zenoh"]).optional(),
68
+ bridgeNamespace: z.string().optional(),
69
+ });
70
+
71
+ const RosbridgeOverrideSettingsSchema = z.object({
72
+ url: z.string().optional(),
73
+ reconnect: z.boolean().optional(),
74
+ reconnectInterval: z.number().optional(),
75
+ });
76
+
77
+ const LocalOverrideSettingsSchema = z.object({
78
+ domainId: z.number().optional(),
79
+ });
80
+
81
+ const WebrtcOverrideSettingsSchema = z.object({
82
+ signalingUrl: z.string().optional(),
83
+ apiUrl: z.string().optional(),
84
+ robotId: z.string().optional(),
85
+ robotKey: z.string().optional(),
86
+ iceServers: z.array(IceServerSchema).optional(),
87
+ });
88
+
89
+ /**
90
+ * Per-robot transport override schema (Phase 1.d-resolve).
91
+ *
92
+ * Discriminated union on `mode`, each variant `.strict()` so that a
93
+ * typo like `{ mode: "zenoh", local: {...} }` fails fast at parse time
94
+ * instead of being silently stripped. Sub-sections are optional —
95
+ * `{ mode: "zenoh" }` is a valid override that inherits everything
96
+ * from the top-level `config.zenoh`. A partial sub-section like
97
+ * `{ mode: "zenoh", zenoh: { routerEndpoint: "ws://field:10000" } }`
98
+ * is merged per-field over the global config — see the resolver.
99
+ *
100
+ * Why a discriminated union (vs the top-level shape of sibling fields):
101
+ * - Keeps the override self-contained at one JSON property
102
+ * - Matches TransportConfig's runtime discriminated union 1:1
103
+ * - Trivially validatable per-robot without leaking across siblings
104
+ */
105
+ const RobotTransportOverrideSchema = z.discriminatedUnion("mode", [
106
+ z
107
+ .object({
108
+ mode: z.literal("rosbridge"),
109
+ rosbridge: RosbridgeOverrideSettingsSchema.optional(),
110
+ })
111
+ .strict(),
112
+ z
113
+ .object({
114
+ mode: z.literal("local"),
115
+ local: LocalOverrideSettingsSchema.optional(),
116
+ })
117
+ .strict(),
118
+ z
119
+ .object({
120
+ mode: z.literal("zenoh"),
121
+ zenoh: ZenohOverrideSettingsSchema.optional(),
122
+ })
123
+ .strict(),
124
+ z
125
+ .object({
126
+ mode: z.literal("webrtc"),
127
+ webrtc: WebrtcOverrideSettingsSchema.optional(),
128
+ })
129
+ .strict(),
130
+ ]);
131
+
132
+ export type RobotTransportOverride = z.infer<typeof RobotTransportOverrideSchema>;
133
+
10
134
  export const AgenticROSConfigSchema = z.object({
11
135
  transport: z
12
136
  .object({
@@ -19,47 +143,13 @@ export const AgenticROSConfigSchema = z.object({
19
143
  })
20
144
  .default({}),
21
145
 
22
- zenoh: z
23
- .object({
24
- /** WebSocket URL for zenoh-ts (zenoh-plugin-remote-api). Not tcp/ — use e.g. ws://localhost:10000 */
25
- routerEndpoint: z.string().default("ws://localhost:10000"),
26
- domainId: z.number().default(0),
27
- /** "ros2dds" = zenoh-bridge-ros2dds key format (slashes kept). "rmw_zenoh" = rmw_zenoh key format (domain + %). */
28
- keyFormat: z.enum(["ros2dds", "rmw_zenoh"]).default("ros2dds"),
29
- /**
30
- * Must match zenoh-bridge-ros2dds `plugins.ros2dds.namespace` when it is not the default "/".
31
- * Example: bridge has `namespace: "/bot1"` → set `bridgeNamespace` to "/bot1" or "bot1".
32
- * Omit or use "/" when the bridge uses the default (only the ROS topic path is the zenoh key).
33
- */
34
- bridgeNamespace: z.string().optional(),
35
- })
36
- .default({}),
146
+ zenoh: ZenohSettingsSchema.default({}),
37
147
 
38
- rosbridge: z
39
- .object({
40
- url: z.string().default("ws://localhost:9090"),
41
- reconnect: z.boolean().default(true),
42
- reconnectInterval: z.number().default(3000),
43
- })
44
- .default({}),
148
+ rosbridge: RosbridgeSettingsSchema.default({}),
45
149
 
46
- local: z
47
- .object({
48
- domainId: z.number().default(0),
49
- })
50
- .default({}),
150
+ local: LocalSettingsSchema.default({}),
51
151
 
52
- webrtc: z
53
- .object({
54
- signalingUrl: z.string().default(""),
55
- apiUrl: z.string().default(""),
56
- robotId: z.string().default(""),
57
- robotKey: z.string().default(""),
58
- iceServers: z
59
- .array(IceServerSchema)
60
- .default([{ urls: "stun:stun.l.google.com:19302" }]),
61
- })
62
- .default({}),
152
+ webrtc: WebrtcSettingsSchema.default({}),
63
153
 
64
154
  robot: z
65
155
  .object({
@@ -70,6 +160,98 @@ export const AgenticROSConfigSchema = z.object({
70
160
  })
71
161
  .default({}),
72
162
 
163
+ /**
164
+ * Phase 1.d multi-robot list. When non-empty, this is the source of
165
+ * truth for `ros2_list_robots` and (in a follow-up) the per-tool
166
+ * `robot_id` parameter. When empty, the legacy single-robot
167
+ * `config.robot` above is synthesised into a one-entry list at
168
+ * resolution time (see packages/core/src/robots.ts). Existing configs
169
+ * keep working unchanged; this field is opt-in.
170
+ *
171
+ * Mark one entry with `default: true` to make it the active robot
172
+ * even when it isn't the first in the list. Otherwise the first entry
173
+ * wins.
174
+ */
175
+ robots: z
176
+ .array(
177
+ z.object({
178
+ /** Stable, human-readable identifier referenced by `robot_id` arguments. */
179
+ id: z.string(),
180
+ name: z.string().default("Robot"),
181
+ namespace: z.string().default(""),
182
+ cameraTopic: z.string().default(""),
183
+ default: z.boolean().optional(),
184
+ /**
185
+ * Phase 1.e robot kind ("amr" | "arm" | "drone" | "rover" | …).
186
+ *
187
+ * Free-form string so users can invent their own taxonomy, but the
188
+ * documented set is `amr` (autonomous mobile robot), `arm`,
189
+ * `drone`, and `rover`. Consumed by `ros2_find_robots_for(kind=…)`
190
+ * so an agent can ask "find me an AMR with a depth camera that
191
+ * can follow_person" and the platform filters the fleet.
192
+ *
193
+ * Defaults to "amr" (back-compat: every existing robot today is
194
+ * an AMR), so the field stays useful even when the user hasn't
195
+ * tagged their robots explicitly.
196
+ */
197
+ kind: z.string().default("amr"),
198
+ /**
199
+ * Phase 1.e sensor/hardware tags.
200
+ *
201
+ * Lets `ros2_find_robots_for` answer questions like "which
202
+ * robots have a depth camera" without subscribing to topics or
203
+ * inspecting the URDF. All flags default to false — set them
204
+ * explicitly per robot via the CLI (`--sensors=has_realsense`)
205
+ * or by hand-editing `~/.agenticros/config.json`.
206
+ *
207
+ * The list is intentionally short for Phase 1; expand it as we
208
+ * accumulate seed-catalog skills that depend on a specific
209
+ * hardware capability (lidar SLAM, arm grasping, etc.).
210
+ */
211
+ sensors: z
212
+ .object({
213
+ has_realsense: z.boolean().default(false),
214
+ has_lidar: z.boolean().default(false),
215
+ has_arm: z.boolean().default(false),
216
+ })
217
+ .default({}),
218
+ /**
219
+ * Phase 1.e optional per-robot capability allowlist.
220
+ *
221
+ * When set, restricts which capabilities `ros2_find_robots_for`
222
+ * considers this robot capable of. When unset (the common
223
+ * case), the robot inherits the gateway's global capability
224
+ * registry (the union of built-in verbs plus every loaded
225
+ * skill's capabilities). Useful in heterogeneous fleets where
226
+ * one robot has the arm skill loaded and another doesn't.
227
+ */
228
+ capabilities: z.array(z.string()).optional(),
229
+ /**
230
+ * Phase 1.d-resolve per-robot transport override.
231
+ *
232
+ * When set, this robot uses its own transport instead of the
233
+ * top-level `config.transport.*`. Useful when one host drives
234
+ * BOTH a local sim (mode: "local") AND a real robot reached via
235
+ * an off-robot bridge (mode: "zenoh" / "rosbridge") — each
236
+ * robot connects through the right path with no global config
237
+ * juggling.
238
+ *
239
+ * Sub-sections (`zenoh`, `rosbridge`, `local`, `webrtc`) are
240
+ * optional inside the override: omit them to inherit the global
241
+ * `config.<section>` defaults; set them to override only the
242
+ * fields you need (e.g. a different router endpoint).
243
+ *
244
+ * Consumed by `getTransportConfigForRobot(config, robotId)` in
245
+ * robots.ts. Adapters opt in to multi-transport pools — when
246
+ * the override is absent they keep using the single global
247
+ * transport, so existing single-robot deployments are
248
+ * unaffected.
249
+ */
250
+ transport: RobotTransportOverrideSchema.optional(),
251
+ }),
252
+ )
253
+ .default([]),
254
+
73
255
  /** Phase 3 teleop web app: camera + twist controls. */
74
256
  teleop: z
75
257
  .object({
@@ -231,7 +413,7 @@ export const AgenticROSConfigSchema = z.object({
231
413
  /** Per-skill config. Keys are skill ids (e.g. followme). Each skill validates its own slice. */
232
414
  skills: z.record(z.string(), z.unknown()).default({}),
233
415
 
234
- /** Directories to scan for skill packages (package.json with "agenticrosSkill": true). Resolved at gateway start. */
416
+ /** Directories to scan for skill packages (package.json with an `agenticros` block). Resolved at gateway start. */
235
417
  skillPaths: z.array(z.string()).default([]),
236
418
 
237
419
  /** Npm (or local) package names to load as skills. Resolved via require.resolve from plugin context. */