diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_m2ldjnpmev99/summary.md b/.agentworkforce/trajectories/completed/2026-06/traj_m2ldjnpmev99/summary.md new file mode 100644 index 000000000..a825b0b93 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_m2ldjnpmev99/summary.md @@ -0,0 +1,33 @@ +# Trajectory: Fix PR CI command snapshot + +> **Status:** ✅ Completed +> **Confidence:** 95% +> **Started:** June 18, 2026 at 05:12 PM +> **Completed:** June 18, 2026 at 05:13 PM + +--- + +## Summary + +Fixed PR CI by adding the newly registered fleet workspace CLI commands to the bootstrap expected command list. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Updated bootstrap expected CLI commands for fleet workspace controls + +- **Chose:** Updated bootstrap expected CLI commands for fleet workspace controls +- **Reasoning:** CI failed because createProgram now registers fleet config/enable/disable/inherit, but the bootstrap test expected list still omitted them. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Updated bootstrap expected CLI commands for fleet workspace controls: Updated bootstrap expected CLI commands for fleet workspace controls diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_m2ldjnpmev99/trajectory.json b/.agentworkforce/trajectories/completed/2026-06/traj_m2ldjnpmev99/trajectory.json new file mode 100644 index 000000000..7c4a50b8e --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_m2ldjnpmev99/trajectory.json @@ -0,0 +1,53 @@ +{ + "id": "traj_m2ldjnpmev99", + "version": 1, + "task": { + "title": "Fix PR CI command snapshot" + }, + "status": "completed", + "startedAt": "2026-06-18T21:12:34.284Z", + "completedAt": "2026-06-18T21:13:09.669Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-06-18T21:13:06.028Z" + } + ], + "chapters": [ + { + "id": "chap_4j4606o10yjt", + "title": "Work", + "agentName": "default", + "startedAt": "2026-06-18T21:13:06.028Z", + "endedAt": "2026-06-18T21:13:09.669Z", + "events": [ + { + "ts": 1781817186029, + "type": "decision", + "content": "Updated bootstrap expected CLI commands for fleet workspace controls: Updated bootstrap expected CLI commands for fleet workspace controls", + "raw": { + "question": "Updated bootstrap expected CLI commands for fleet workspace controls", + "chosen": "Updated bootstrap expected CLI commands for fleet workspace controls", + "alternatives": [], + "reasoning": "CI failed because createProgram now registers fleet config/enable/disable/inherit, but the bootstrap test expected list still omitted them." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Fixed PR CI by adding the newly registered fleet workspace CLI commands to the bootstrap expected command list.", + "approach": "Standard approach", + "confidence": 0.95 + }, + "commits": [], + "filesChanged": [], + "projectId": "AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "6399193c4713e29ae6a9c280720c7d4d4edb4ee1", + "endRef": "6399193c4713e29ae6a9c280720c7d4d4edb4ee1" + } +} diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_ws73z8bvitw0/summary.md b/.agentworkforce/trajectories/completed/2026-06/traj_ws73z8bvitw0/summary.md new file mode 100644 index 000000000..439605c09 --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_ws73z8bvitw0/summary.md @@ -0,0 +1,33 @@ +# Trajectory: Expose workspace fleet node flag in Relay SDK and CLI + +> **Status:** ✅ Completed +> **Confidence:** 82% +> **Started:** June 18, 2026 at 01:47 PM +> **Completed:** June 18, 2026 at 01:47 PM + +--- + +## Summary + +Added Relay SDK workspace.fleetNodes get/set/inherit, CLI fleet config/enable/disable/inherit commands, docs and changelog entries, and updated Relay workspaces to @relaycast/sdk 4.1.1, which ships the Relaycast SDK workspace fleet-node method. SDK tests/build and focused fleet CLI tests pass; full CLI build is blocked by existing current-main cloud auth StoredAuth type errors. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Require Relaycast SDK workspace fleet config API + +- **Chose:** Require Relaycast SDK workspace fleet config API +- **Reasoning:** Relay should not duplicate the Relaycast REST call. The missing workspace.fleetNodes method belongs in @relaycast/sdk, so Relay delegates to the SDK surface and the Relaycast fix is tracked separately. + +--- + +## Chapters + +### 1. Work + +_Agent: default_ + +- Require Relaycast SDK workspace fleet config API: Require Relaycast SDK workspace fleet config API diff --git a/.agentworkforce/trajectories/completed/2026-06/traj_ws73z8bvitw0/trajectory.json b/.agentworkforce/trajectories/completed/2026-06/traj_ws73z8bvitw0/trajectory.json new file mode 100644 index 000000000..d0bf8ed0f --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-06/traj_ws73z8bvitw0/trajectory.json @@ -0,0 +1,53 @@ +{ + "id": "traj_ws73z8bvitw0", + "version": 1, + "task": { + "title": "Expose workspace fleet node flag in Relay SDK and CLI" + }, + "status": "completed", + "startedAt": "2026-06-18T17:47:07.114Z", + "completedAt": "2026-06-18T17:47:15.340Z", + "agents": [ + { + "name": "default", + "role": "lead", + "joinedAt": "2026-06-18T17:47:11.173Z" + } + ], + "chapters": [ + { + "id": "chap_a3bv93ofiw3n", + "title": "Work", + "agentName": "default", + "startedAt": "2026-06-18T17:47:11.173Z", + "endedAt": "2026-06-18T17:47:15.340Z", + "events": [ + { + "ts": 1781804831174, + "type": "decision", + "content": "Require Relaycast SDK workspace fleet config API: Require Relaycast SDK workspace fleet config API", + "raw": { + "question": "Require Relaycast SDK workspace fleet config API", + "chosen": "Require Relaycast SDK workspace fleet config API", + "alternatives": [], + "reasoning": "Relay should not duplicate the Relaycast REST call. The missing workspace.fleetNodes method belongs in @relaycast/sdk, so Relay delegates to the SDK surface and the Relaycast fix is tracked separately." + }, + "significance": "high" + } + ] + } + ], + "retrospective": { + "summary": "Added Relay SDK workspace.fleetNodes get/set/inherit, CLI fleet config/enable/disable/inherit commands, docs and changelog entries, and updated Relay workspaces to @relaycast/sdk 4.1.1, which ships the Relaycast SDK workspace fleet-node method. SDK tests/build and focused fleet CLI tests pass; full CLI build is blocked by existing current-main cloud auth StoredAuth type errors.", + "approach": "Standard approach", + "confidence": 0.82 + }, + "commits": [], + "filesChanged": [], + "projectId": "AgentWorkforce/relay", + "tags": [], + "_trace": { + "startRef": "e4cbf8385499b9da734a2a36e99acc2dbc4f9bf5", + "endRef": "e4cbf8385499b9da734a2a36e99acc2dbc4f9bf5" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1cf1a55..041c13075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `@agent-relay/config` `CLI_AUTH_CONFIG` adds an `xai` provider (Grok CLI): `grok login --device-auth` device-code connect, `~/.grok/auth.json` credential capture, and the official x.ai installer as the sandbox fallback — so cloud sandboxes can authenticate the `grok` harness from a connected account instead of an API key. - `@agent-relay/sdk` wires the durable delivery surface to the Relaycast backend: `inbox.list`, `inbox.subscribe`, `inbox.ack/fail/defer`, and `deliveries.ack/fail/defer` now use the hosted delivery ledger, agent-scoped capabilities report `serverDeliveryState: true`, and `DeliveryRunner` works against Relaycast-backed inbox items. - `@agent-relay/sdk` adds `placement.spawn({ capability, node?, repo? })` — node-targeted/`self`/least-eligible placement that gates on advertised capability and repo-key map, queues with a bounded TTL until an eligible live node appears, and surfaces queue/fail visibility through `onReconcile` events. A `spawn:` capability pins the broker harness — a mismatched `input.cli` is rejected — and the exported `RelayPlacementError` reports `capability_mismatch` / `placement_queue_full` / `placement_ttl_expired` / `unmapped_repo`. +- `agent-relay fleet config|enable|disable|inherit` and `@agent-relay/sdk` `workspace.fleetNodes` expose the per-workspace fleet node rollout flag. - Two-node fleet E2E (`tests/e2e/fleet`, `npm run test:e2e`, `Fleet E2E` CI workflow): boots a real relaycast engine plus two `agent-relay fleet serve` nodes (real Rust broker + sidecar each) and asserts the live control wire — boot/register (real broker `Authorization: Bearer` node auth), negative auth, capability-filtered roster, cross-node action dispatch + ack, declarative trigger fire-once with loop guard, end-to-end spawn completion (token mint+inject), capability-routed + least-loaded + resume placement, `capability_mismatch` failure, in-flight reschedule on node death + restart reconcile, and bounded-mailbox TTL dead-letter. ### Changed diff --git a/package-lock.json b/package-lock.json index 5d30e293d..ef3ed990c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-relay/monorepo", - "version": "8.8.3", + "version": "8.8.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/monorepo", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0", "workspaces": [ "packages/*", @@ -2104,6 +2104,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -2538,7 +2539,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -2556,7 +2556,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2574,7 +2573,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2592,7 +2590,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -2610,7 +2607,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2628,7 +2624,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -2646,7 +2641,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2664,7 +2658,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2682,7 +2675,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2700,7 +2692,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2718,7 +2709,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2736,7 +2726,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2754,7 +2743,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2772,7 +2760,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2790,7 +2777,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2808,7 +2794,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2826,7 +2811,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -2844,7 +2828,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2862,7 +2845,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2880,7 +2862,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2898,7 +2879,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -2916,7 +2896,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -2934,7 +2913,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -2952,7 +2930,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2970,7 +2947,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -2988,7 +2964,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -9417,7 +9392,6 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", - "dev": true, "optional": true, "engines": { "node": ">=10.0.0" @@ -9941,7 +9915,6 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { @@ -15301,7 +15274,6 @@ "version": "2.27.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", - "dev": true, "license": "MIT", "optional": true }, @@ -19633,46 +19605,46 @@ }, "packages/brand": { "name": "@agent-relay/brand", - "version": "8.8.3" + "version": "8.8.5" }, "packages/broker-darwin-arm64": { "name": "@agent-relay/broker-darwin-arm64", - "version": "8.8.3", + "version": "8.8.5", "license": "MIT" }, "packages/broker-darwin-x64": { "name": "@agent-relay/broker-darwin-x64", - "version": "8.8.3", + "version": "8.8.5", "license": "MIT" }, "packages/broker-linux-arm64": { "name": "@agent-relay/broker-linux-arm64", - "version": "8.8.3", + "version": "8.8.5", "license": "MIT" }, "packages/broker-linux-x64": { "name": "@agent-relay/broker-linux-x64", - "version": "8.8.3", + "version": "8.8.5", "license": "MIT" }, "packages/broker-win32-x64": { "name": "@agent-relay/broker-win32-x64", - "version": "8.8.3", + "version": "8.8.5", "license": "MIT" }, "packages/cli": { "name": "agent-relay", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0", "dependencies": { - "@agent-relay/cloud": "8.8.3", - "@agent-relay/config": "8.8.3", - "@agent-relay/fleet": "8.8.3", - "@agent-relay/harness-driver": "8.8.3", - "@agent-relay/sdk": "8.8.3", - "@agent-relay/utils": "8.8.3", + "@agent-relay/cloud": "8.8.5", + "@agent-relay/config": "8.8.5", + "@agent-relay/fleet": "8.8.5", + "@agent-relay/harness-driver": "8.8.5", + "@agent-relay/sdk": "8.8.5", + "@agent-relay/utils": "8.8.5", "@modelcontextprotocol/sdk": "^1.0.0", - "@relaycast/sdk": "^4.0.0", + "@relaycast/sdk": "^4.1.1", "@relayflows/cli": "^1.0.1", "@xterm/headless": "^6.0.0", "commander": "^12.1.0", @@ -19693,11 +19665,46 @@ "node": ">=20.9.0" } }, + "packages/cli/node_modules/@relaycast/sdk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-4.1.1.tgz", + "integrity": "sha512-+9c21F4nRpmOtP4pYTUVJ60Vqju2dA9ADuCHY5D+Q/AtZCmG0zGZLmCXqsdStC7FJHo8e+SfiObvVxcFao+DEg==", + "dependencies": { + "@relaycast/types": "4.1.1", + "zod": "^4.3.6" + } + }, + "packages/cli/node_modules/@relaycast/sdk/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/cli/node_modules/@relaycast/types": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@relaycast/types/-/types-4.1.1.tgz", + "integrity": "sha512-99K+pGTzuW8exa2gu0p3a9EpVywK6SHuDSnGUqmXdIbxoTgF0r73QkP+qBhZ+2qPacwcWo2Cjn0IW5RKuzsIwA==", + "dependencies": { + "zod": "^4.3.6" + } + }, + "packages/cli/node_modules/@relaycast/types/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/cloud": { "name": "@agent-relay/cloud", - "version": "8.8.3", + "version": "8.8.5", "dependencies": { - "@agent-relay/config": "8.8.3", + "@agent-relay/config": "8.8.5", "@aws-sdk/client-s3": "3.1020.0", "ignore": "^7.0.5", "tar": "^7.5.10" @@ -19713,7 +19720,7 @@ }, "packages/config": { "name": "@agent-relay/config", - "version": "8.8.3", + "version": "8.8.5", "dependencies": { "zod": "^3.23.8", "zod-to-json-schema": "^3.23.1" @@ -19726,60 +19733,60 @@ }, "packages/evals": { "name": "@agent-relay/evals", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "8.8.3", - "@agent-relay/integration-prompts": "8.8.3" + "@agent-relay/harness-driver": "8.8.5", + "@agent-relay/integration-prompts": "8.8.5" } }, "packages/fleet": { "name": "@agent-relay/fleet", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "8.8.3", - "@agent-relay/harnesses": "8.8.3", - "@agent-relay/sdk": "8.8.3", + "@agent-relay/harness-driver": "8.8.5", + "@agent-relay/harnesses": "8.8.5", + "@agent-relay/sdk": "8.8.5", "zod": "^3.23.8" } }, "packages/harness-driver": { "name": "@agent-relay/harness-driver", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "8.8.3", + "@agent-relay/sdk": "8.8.5", "ws": "^8.18.3", "zod": "^3.23.8" }, "optionalDependencies": { - "@agent-relay/broker-darwin-arm64": "8.8.3", - "@agent-relay/broker-darwin-x64": "8.8.3", - "@agent-relay/broker-linux-arm64": "8.8.3", - "@agent-relay/broker-linux-x64": "8.8.3", - "@agent-relay/broker-win32-x64": "8.8.3" + "@agent-relay/broker-darwin-arm64": "8.8.5", + "@agent-relay/broker-darwin-x64": "8.8.5", + "@agent-relay/broker-linux-arm64": "8.8.5", + "@agent-relay/broker-linux-x64": "8.8.5", + "@agent-relay/broker-win32-x64": "8.8.5" } }, "packages/harnesses": { "name": "@agent-relay/harnesses", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "8.8.3", - "@agent-relay/sdk": "8.8.3" + "@agent-relay/harness-driver": "8.8.5", + "@agent-relay/sdk": "8.8.5" } }, "packages/integration-prompts": { "name": "@agent-relay/integration-prompts", - "version": "8.8.3", + "version": "8.8.5", "license": "Apache-2.0" }, "packages/policy": { "name": "@agent-relay/policy", - "version": "8.8.3", + "version": "8.8.5", "dependencies": { - "@agent-relay/config": "8.8.3" + "@agent-relay/config": "8.8.5" }, "devDependencies": { "@types/node": "^22.19.3", @@ -19788,24 +19795,50 @@ }, "packages/sdk": { "name": "@agent-relay/sdk", - "version": "8.8.3", + "version": "8.8.5", "dependencies": { - "@relaycast/sdk": "^4.0.0" + "@relaycast/sdk": "^4.1.1" }, "devDependencies": { "@types/node": "^22.13.10" } }, + "packages/sdk/node_modules/@relaycast/sdk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@relaycast/sdk/-/sdk-4.1.1.tgz", + "integrity": "sha512-+9c21F4nRpmOtP4pYTUVJ60Vqju2dA9ADuCHY5D+Q/AtZCmG0zGZLmCXqsdStC7FJHo8e+SfiObvVxcFao+DEg==", + "dependencies": { + "@relaycast/types": "4.1.1", + "zod": "^4.3.6" + } + }, + "packages/sdk/node_modules/@relaycast/types": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@relaycast/types/-/types-4.1.1.tgz", + "integrity": "sha512-99K+pGTzuW8exa2gu0p3a9EpVywK6SHuDSnGUqmXdIbxoTgF0r73QkP+qBhZ+2qPacwcWo2Cjn0IW5RKuzsIwA==", + "dependencies": { + "zod": "^4.3.6" + } + }, + "packages/sdk/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/telemetry": { "name": "@agent-relay/telemetry", - "version": "8.8.3", + "version": "8.8.5", "deprecated": "@agent-relay/telemetry is deprecated. Telemetry is now internal to the agent-relay CLI." }, "packages/utils": { "name": "@agent-relay/utils", - "version": "8.8.3", + "version": "8.8.5", "dependencies": { - "@agent-relay/config": "8.8.3", + "@agent-relay/config": "8.8.5", "compare-versions": "^6.1.1" }, "devDependencies": { diff --git a/packages/cli/package.json b/packages/cli/package.json index e4a59dfd9..5f845f1bf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,7 +50,7 @@ "@agent-relay/sdk": "8.8.5", "@agent-relay/utils": "8.8.5", "@modelcontextprotocol/sdk": "^1.0.0", - "@relaycast/sdk": "^4.0.0", + "@relaycast/sdk": "^4.1.1", "@relayflows/cli": "^1.0.1", "@xterm/headless": "^6.0.0", "commander": "^12.1.0", diff --git a/packages/cli/src/cli/bootstrap.test.ts b/packages/cli/src/cli/bootstrap.test.ts index 50712c212..b09bdf43d 100644 --- a/packages/cli/src/cli/bootstrap.test.ts +++ b/packages/cli/src/cli/bootstrap.test.ts @@ -30,6 +30,10 @@ const expectedLeafCommands = [ 'telemetry', 'mcp', // fleet + 'fleet config', + 'fleet disable', + 'fleet enable', + 'fleet inherit', 'fleet serve', 'fleet nodes', 'fleet status', diff --git a/packages/cli/src/cli/commands/fleet.test.ts b/packages/cli/src/cli/commands/fleet.test.ts index 827423bf7..2e4fb33ad 100644 --- a/packages/cli/src/cli/commands/fleet.test.ts +++ b/packages/cli/src/cli/commands/fleet.test.ts @@ -11,6 +11,58 @@ import { loadNodeDefinition, registerFleetCommands, stripEnrollmentFlags } from import { startFleetSidecar, serveFleetSidecar } from '../lib/fleet-sidecar.js'; describe('fleet command support', () => { + it.each([ + ['config', 'get', undefined], + ['enable', 'set', true], + ['disable', 'set', false], + ['inherit', 'inherit', undefined], + ] as const)('fleet %s delegates to workspace fleet node config API', async (command, method, value) => { + const fleetNodes = { + get: vi.fn(async () => ({ enabled: false, defaultEnabled: false, override: null })), + set: vi.fn(async (enabled: boolean) => ({ enabled, defaultEnabled: false, override: enabled })), + inherit: vi.fn(async () => ({ enabled: false, defaultEnabled: false, override: null })), + }; + const createWorkspaceRelay = vi.fn(() => ({ workspace: { fleetNodes } })); + const logs: string[] = []; + const program = new Command(); + program.exitOverride(); + registerFleetCommands(program, { + sdk: { + createAgentRelay: vi.fn() as never, + createWorkspaceRelay: createWorkspaceRelay as never, + createWorkspace: vi.fn() as never, + log: (message: unknown) => logs.push(String(message)), + error: vi.fn(), + exit: vi.fn(() => { + throw new Error('__exit__'); + }) as never, + }, + log: () => undefined, + warn: () => undefined, + error: () => undefined, + }); + + await program.parseAsync( + ['fleet', command, '--workspace-key', 'rk_live_test', '--base-url', 'https://relay.example'], + { from: 'user' } + ); + + expect(createWorkspaceRelay).toHaveBeenCalledWith({ + workspaceKey: 'rk_live_test', + token: undefined, + baseUrl: 'https://relay.example', + }); + if (method === 'set') { + expect(fleetNodes.set).toHaveBeenCalledWith(value); + } else { + expect(fleetNodes[method]).toHaveBeenCalledTimes(1); + } + expect(JSON.parse(logs[0]!)).toMatchObject({ + enabled: method === 'set' ? value : false, + defaultEnabled: false, + }); + }); + it('loads the example TS node file', async () => { const node = await loadNodeDefinition(path.resolve(process.cwd(), 'examples/relay-node.ts')); diff --git a/packages/cli/src/cli/commands/fleet.ts b/packages/cli/src/cli/commands/fleet.ts index a0d77a9fb..b5747b8b0 100644 --- a/packages/cli/src/cli/commands/fleet.ts +++ b/packages/cli/src/cli/commands/fleet.ts @@ -107,6 +107,42 @@ export function registerFleetCommands( }); }); + addSdkOptions(group.command('config').description('Show workspace fleet node configuration')).action( + async (options: Record) => { + await runSdk(deps.sdk, async () => { + const relay = deps.sdk.createWorkspaceRelay(sdkOptionsFromOpts(options)); + printJson(deps.sdk, await relay.workspace.fleetNodes.get()); + }); + } + ); + + addSdkOptions(group.command('enable').description('Enable fleet nodes for the workspace')).action( + async (options: Record) => { + await runSdk(deps.sdk, async () => { + const relay = deps.sdk.createWorkspaceRelay(sdkOptionsFromOpts(options)); + printJson(deps.sdk, await relay.workspace.fleetNodes.set(true)); + }); + } + ); + + addSdkOptions(group.command('disable').description('Disable fleet nodes for the workspace')).action( + async (options: Record) => { + await runSdk(deps.sdk, async () => { + const relay = deps.sdk.createWorkspaceRelay(sdkOptionsFromOpts(options)); + printJson(deps.sdk, await relay.workspace.fleetNodes.set(false)); + }); + } + ); + + addSdkOptions( + group.command('inherit').description('Use the deployment default for workspace fleet nodes') + ).action(async (options: Record) => { + await runSdk(deps.sdk, async () => { + const relay = deps.sdk.createWorkspaceRelay(sdkOptionsFromOpts(options)); + printJson(deps.sdk, await relay.workspace.fleetNodes.inherit()); + }); + }); + group .command('status') .description('Show local fleet broker and sidecar status') diff --git a/packages/sdk/package.json b/packages/sdk/package.json index b9a987aff..b543dc1a6 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -59,6 +59,6 @@ "@types/node": "^22.13.10" }, "dependencies": { - "@relaycast/sdk": "^4.0.0" + "@relaycast/sdk": "^4.1.1" } } diff --git a/packages/sdk/src/__tests__/facade.test.ts b/packages/sdk/src/__tests__/facade.test.ts index 8c4ba4f1a..cc2da2cf8 100644 --- a/packages/sdk/src/__tests__/facade.test.ts +++ b/packages/sdk/src/__tests__/facade.test.ts @@ -32,8 +32,16 @@ function createMessagingMock() { channels: [], })), }; - const messaging = { messages, agents, events: {} } as unknown as RelayMessaging; - return { messaging, messages, agents }; + const workspace = { + info: vi.fn(async () => ({ id: 'ws_1', name: 'Ops' })), + fleetNodes: { + get: vi.fn(async () => ({ enabled: false, defaultEnabled: false, override: null })), + set: vi.fn(async (enabled: boolean) => ({ enabled, defaultEnabled: false, override: enabled })), + inherit: vi.fn(async () => ({ enabled: false, defaultEnabled: false, override: null })), + }, + }; + const messaging = { messages, agents, workspace, events: {} } as unknown as RelayMessaging; + return { messaging, messages, agents, workspace }; } describe('AgentRelay facade (Phase A)', () => { @@ -72,6 +80,31 @@ describe('AgentRelay facade (Phase A)', () => { expect(client.token).toBe('tok-self'); }); + it('workspace.fleetNodes delegates fleet node config calls', async () => { + const { messaging, workspace } = createMessagingMock(); + const relay = new AgentRelay({ messaging }); + + await expect(relay.workspace.fleetNodes.get()).resolves.toEqual({ + enabled: false, + defaultEnabled: false, + override: null, + }); + await expect(relay.workspace.fleetNodes.set(true)).resolves.toEqual({ + enabled: true, + defaultEnabled: false, + override: true, + }); + await expect(relay.workspace.fleetNodes.inherit()).resolves.toEqual({ + enabled: false, + defaultEnabled: false, + override: null, + }); + + expect(workspace.fleetNodes.get).toHaveBeenCalledTimes(1); + expect(workspace.fleetNodes.set).toHaveBeenCalledWith(true); + expect(workspace.fleetNodes.inherit).toHaveBeenCalledTimes(1); + }); + it('sendMessage routes #channel to messages.send and a bare name to direct', async () => { const { messaging, messages } = createMessagingMock(); const relay = new AgentRelay({ messaging, createAgentMessaging: () => messaging }); diff --git a/packages/sdk/src/__tests__/messaging.test.ts b/packages/sdk/src/__tests__/messaging.test.ts index 48d7d7b00..5e8705939 100644 --- a/packages/sdk/src/__tests__/messaging.test.ts +++ b/packages/sdk/src/__tests__/messaging.test.ts @@ -143,6 +143,14 @@ function createWorkspace() { load: 0, })), }, + workspace: { + info: vi.fn(async () => ({ id: 'ws_1', name: 'Ops' })), + fleetNodes: { + get: vi.fn(async () => ({ enabled: true, default_enabled: false, override: true })), + set: vi.fn(async (enabled: boolean) => ({ enabled, default_enabled: false, override: enabled })), + inherit: vi.fn(async () => ({ enabled: false, default_enabled: false, override: null })), + }, + }, }; } @@ -359,6 +367,31 @@ describe('RelaycastMessagingClient', () => { }); }); + it('delegates workspace fleet node config calls to Relaycast', async () => { + const workspace = createWorkspace(); + const client = new RelaycastMessagingClient({ relaycast: workspace }); + + await expect(client.workspace.fleetNodes.get()).resolves.toEqual({ + enabled: true, + defaultEnabled: false, + override: true, + }); + await expect(client.workspace.fleetNodes.set(false)).resolves.toEqual({ + enabled: false, + defaultEnabled: false, + override: false, + }); + await expect(client.workspace.fleetNodes.inherit()).resolves.toEqual({ + enabled: false, + defaultEnabled: false, + override: null, + }); + + expect(workspace.workspace.fleetNodes.get).toHaveBeenCalledTimes(1); + expect(workspace.workspace.fleetNodes.set).toHaveBeenCalledWith(false); + expect(workspace.workspace.fleetNodes.inherit).toHaveBeenCalledTimes(1); + }); + it('normalizes fleet node roster fields and passes node query options through', async () => { const workspace = createWorkspace(); const client = new RelaycastMessagingClient({ relaycast: workspace }); diff --git a/packages/sdk/src/facade.ts b/packages/sdk/src/facade.ts index 145ea019c..88cb24f37 100644 --- a/packages/sdk/src/facade.ts +++ b/packages/sdk/src/facade.ts @@ -11,6 +11,7 @@ import type { RelayMessageMode, RelaySendChannelMessageInput, RelayWorkspaceInfo, + RelayWorkspaceFleetNodesConfig, } from './messaging/index.js'; import { actionSchemaToJsonSchema, @@ -196,6 +197,11 @@ export interface RelayWorkspace { ): Promise; reconnect(input: { apiToken: string }): Promise; info(): Promise; + fleetNodes: { + get(): Promise; + set(enabled: boolean): Promise; + inherit(): Promise; + }; } export interface NotifyOptions { @@ -353,6 +359,7 @@ export function createWorkspaceFacade(messaging: RelayMessaging, deps?: Workspac return { info: () => messaging.workspace.info(), + fleetNodes: messaging.workspace.fleetNodes, register: register as RelayWorkspace['register'], reconnect: async ({ apiToken }) => { if (!deps) { diff --git a/packages/sdk/src/messaging/relaycast.ts b/packages/sdk/src/messaging/relaycast.ts index d82bf3748..f95ecb81d 100644 --- a/packages/sdk/src/messaging/relaycast.ts +++ b/packages/sdk/src/messaging/relaycast.ts @@ -47,6 +47,7 @@ import type { RelayRegisterCapabilityInput, RelayWebhook, RelayWorkspaceInfo, + RelayWorkspaceFleetNodesConfig, InboxAckInput, InboxDeferInput, InboxFailInput, @@ -243,6 +244,15 @@ function readRecord(record: Record, ...keys: string[]): Record< return undefined; } +function toRelayWorkspaceFleetNodesConfig(raw: unknown): RelayWorkspaceFleetNodesConfig { + const record = asRecord(raw); + return { + enabled: readBoolean(record, 'enabled') ?? false, + defaultEnabled: readBoolean(record, 'defaultEnabled', 'default_enabled') ?? false, + override: readBoolean(record, 'override') ?? null, + }; +} + type PlacementReconcileReason = 'no_eligible_node' | 'target_offline' | 'unmapped_repo'; type PlacementSelection = @@ -466,6 +476,11 @@ type RelaycastWorkspaceLike = { }; workspace?: { info(): Promise; + fleetNodes?: { + get(): Promise; + set(enabled: boolean): Promise; + inherit(): Promise; + }; }; as?: (agentToken: string, options?: AgentClientOptions) => RelaycastAgentLike; // Workspace-scoped realtime stream (relaycast 2.5+): lets a workspace-key @@ -1250,6 +1265,17 @@ export class RelaycastMessagingClient implements RelayMessagingClient { } return (await this.relaycast.workspace.info()) as RelayWorkspaceInfo; }, + fleetNodes: { + get: async (): Promise => { + return toRelayWorkspaceFleetNodesConfig(await this.requireWorkspaceFleetNodes().get()); + }, + set: async (enabled: boolean): Promise => { + return toRelayWorkspaceFleetNodesConfig(await this.requireWorkspaceFleetNodes().set(enabled)); + }, + inherit: async (): Promise => { + return toRelayWorkspaceFleetNodesConfig(await this.requireWorkspaceFleetNodes().inherit()); + }, + }, }; private resolvePlacementNode(node: string | 'self' | undefined, selfNodeName?: string): string | undefined { @@ -1397,6 +1423,17 @@ export class RelaycastMessagingClient implements RelayMessagingClient { return this.relaycast.triggers; } + private requireWorkspaceFleetNodes(): NonNullable< + NonNullable['fleetNodes'] + > { + if (!this.relaycast.workspace?.fleetNodes) { + throw new Error( + 'RelaycastMessagingClient.workspace.fleetNodes requires @relaycast/sdk with the workspace fleet nodes API.' + ); + } + return this.relaycast.workspace.fleetNodes; + } + private requireAgentActions(operation: string): NonNullable { const actions = this.agentClient?.actions; if (!actions) { diff --git a/packages/sdk/src/messaging/types.ts b/packages/sdk/src/messaging/types.ts index 516183691..b0976baaa 100644 --- a/packages/sdk/src/messaging/types.ts +++ b/packages/sdk/src/messaging/types.ts @@ -593,6 +593,12 @@ export interface RelayWorkspaceInfo { [key: string]: unknown; } +export interface RelayWorkspaceFleetNodesConfig { + enabled: boolean; + defaultEnabled: boolean; + override: boolean | null; +} + export type InboxItemState = 'queued' | 'delivered' | 'failed' | 'deferred' | 'read'; export interface InboxItem { @@ -917,6 +923,11 @@ export interface RelayMessagingClient { }; readonly workspace: { info(): Promise; + fleetNodes: { + get(): Promise; + set(enabled: boolean): Promise; + inherit(): Promise; + }; }; } diff --git a/web/content/docs/fleets.mdx b/web/content/docs/fleets.mdx index 635651fa4..7fd0e21fb 100644 --- a/web/content/docs/fleets.mdx +++ b/web/content/docs/fleets.mdx @@ -13,8 +13,8 @@ One `fleet serve` = one node. A node advertises which harnesses it can run; the nodes that have the harness you asked for. - Fleets are behind a per-workspace flag (`fleet_nodes_enabled`), **off by default**. Ask your - operator to enable it before serving a node. + Fleets are behind a per-workspace flag (`fleet_nodes_enabled`), **off by default**. Enable it with + `agent-relay fleet enable --workspace-key "$RELAY_WORKSPACE_KEY"` before serving a node. ## Set up a node