Repository navigation
feat: add Zoo remote control bridge #2014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bb89ace
adcec9e
9da3bfb
9fa0c7e
54fa33c
d15a943
cb5b3a1
11439c3
07d8ccd
60bf2c1
78863aa
7756a0b
fadc3b4
3ad2b75
d8dc8b3
6a71087
9166132
968abc3
32ed542
152d356
88ec4e1
8d6e9db
89b457e
7ff937c
76c3d82
19d822f
56be30d
68def5a
b4be1d5
6d94c81
f3ff16e
0e70c76
9e298e8
78be7d0
4447743
7af6ae1
c925f24
b52fb68
e409180
e4170b6
7d3ea2a
56e3c07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,3 +59,6 @@ qdrant_storage/ | |
| plans/ | ||
|
|
||
| roo-cli-*.tar.gz* | ||
|
|
||
| # Local Cursor agent rules/settings (not shared) | ||
| .cursor/ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { partitionRemoteCommands } from "./bridgeHelpers.js" | ||
| import type { | ||
| ZooRemoteCommandAck, | ||
| ZooRemoteCommandsResponse, | ||
| ZooRemoteEvent, | ||
| ZooRemotePresence, | ||
| ZooRemoteSnapshot, | ||
| } from "./types.js" | ||
|
|
||
| const REQUEST_TIMEOUT_MS = 30_000 | ||
|
|
||
| export type ZooRemoteControlClientOptions = { | ||
| getBaseUrl: () => string | ||
| getSessionToken: () => string | ||
| fetchFn?: typeof fetch | ||
| log?: (message: string) => void | ||
| } | ||
|
|
||
| /** | ||
| * Thin HTTP client for /api/extension/remote/* using the Zoo session token. | ||
| * Callers treat route-missing 404 as "server not ready" and back off. | ||
| * Ack 404 (command_not_found) must not use the same backoff path. | ||
| */ | ||
| export class ZooRemoteControlClient { | ||
| private readonly getBaseUrl: () => string | ||
| private readonly getSessionToken: () => string | ||
| private readonly fetchFn: typeof fetch | ||
| private readonly log: (message: string) => void | ||
|
|
||
| constructor(options: ZooRemoteControlClientOptions) { | ||
| this.getBaseUrl = options.getBaseUrl | ||
| this.getSessionToken = options.getSessionToken | ||
| this.fetchFn = options.fetchFn ?? fetch | ||
| this.log = options.log ?? (() => {}) | ||
| } | ||
|
|
||
| async postPresence(presence: ZooRemotePresence): Promise<{ ok: boolean; status: number }> { | ||
| return this.request("POST", "/api/extension/remote/presence", presence) | ||
| } | ||
|
|
||
| async postEvents(instanceId: string, events: ZooRemoteEvent[]): Promise<{ ok: boolean; status: number }> { | ||
| return this.request("POST", "/api/extension/remote/events", { instanceId, events }) | ||
| } | ||
|
|
||
| async postSnapshot(snapshot: ZooRemoteSnapshot): Promise<{ ok: boolean; status: number }> { | ||
| return this.request("POST", "/api/extension/remote/snapshot", snapshot) | ||
| } | ||
|
|
||
| async getCommands(instanceId: string): Promise<{ | ||
| ok: boolean | ||
| status: number | ||
| body?: ZooRemoteCommandsResponse | ||
| }> { | ||
| const result = await this.requestJson( | ||
| "GET", | ||
| `/api/extension/remote/commands?instanceId=${encodeURIComponent(instanceId)}`, | ||
| undefined, | ||
| { parse: (data: unknown) => data }, | ||
| ) | ||
| if (!result.ok || result.body === undefined) return { ok: result.ok, status: result.status } | ||
|
|
||
| const batch = partitionRemoteCommands(result.body) | ||
| if (!batch) { | ||
| this.log(`[ZooRemoteControl] GET /api/extension/remote/commands failed: invalid command envelope`) | ||
| return { ok: false, status: 0 } | ||
| } | ||
| return { ok: true, status: result.status, body: batch } | ||
| } | ||
|
|
||
| async ackCommand(ack: ZooRemoteCommandAck): Promise<{ ok: boolean; status: number }> { | ||
| return this.request("POST", `/api/extension/remote/commands/${encodeURIComponent(ack.commandId)}/ack`, ack) | ||
| } | ||
|
|
||
| private async request(method: string, path: string, body?: unknown): Promise<{ ok: boolean; status: number }> { | ||
| const result = await this.requestJson(method, path, body) | ||
| return { ok: result.ok, status: result.status } | ||
| } | ||
|
|
||
| private async requestJson<T>( | ||
| method: string, | ||
| path: string, | ||
| body: unknown | undefined, | ||
| schema?: { parse: (data: unknown) => T }, | ||
| ): Promise<{ ok: boolean; status: number; body?: T }> { | ||
| const token = this.getSessionToken() | ||
| if (!token) { | ||
| return { ok: false, status: 401 } | ||
| } | ||
|
|
||
| const url = `${this.getBaseUrl().replace(/\/$/, "")}${path}` | ||
|
|
||
| try { | ||
| const response = await this.fetchFn(url, { | ||
| method, | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| Accept: "application/json", | ||
| ...(body !== undefined ? { "Content-Type": "application/json" } : {}), | ||
| }, | ||
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | ||
| ...(body !== undefined ? { body: JSON.stringify(body) } : {}), | ||
| }) | ||
|
|
||
| if (!response.ok) { | ||
| if (response.status !== 404) { | ||
| this.log(`[ZooRemoteControl] ${method} ${path} -> ${response.status}`) | ||
| } | ||
| return { ok: false, status: response.status } | ||
| } | ||
|
|
||
| if (!schema) { | ||
| return { ok: true, status: response.status } | ||
| } | ||
|
|
||
| const json: unknown = await response.json() | ||
| return { ok: true, status: response.status, body: schema.parse(json) } | ||
|
Comment on lines
+115
to
+116
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: set -o pipefail
printf '%s\n' '--- changed paths ---'
git diff --name-only 05d01f33d03a7fd54baf8b7c61acf53824c56358 b52fb68c87ea07cf8166c0807eef03ea41b38c83 -- packages/core/src/zoo-remote-control
printf '%s\n' '--- diff ---'
git diff --unified=80 05d01f33d03a7fd54baf8b7c61acf53824c56358 b52fb68c87ea07cf8166c0803eef03ea41b38c83 -- packages/core/src/zoo-remote-control
printf '%s\n' '--- relevant references ---'
rg -n -F --glob '*.{ts,tsx}' -e 'zooRemoteCommandsResponseSchema' -e 'pollCommands' -e 'getCommands(' -e 'executeRemoteCommand' packages/core/src packages/core/src/zoo-remote-control || test "$?" -eq 1
printf '%s\n' '--- client source ---'
nl -ba packages/core/src/zoo-remote-control/ZooRemoteControlClient.ts
printf '%s\n' '--- schema/type sources ---'
for f in packages/core/src/zoo-remote-control/types.ts packages/core/src/zoo-remote-control/ZooRemoteControlBridge.ts packages/core/src/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts packages/core/src/zoo-remote-control/__tests__/ZooRemoteControlClient.spec.ts; do
if test -f "$f"; then
printf '\n--- %s ---\n' "$f"
nl -ba "$f"
fi
doneRepository: Zoo-Code-Org/Zoo-Code Length of output: 29384 🏁 Script executed: set -o pipefail
printf '%s\n' '--- changed paths ---'
git diff --name-only 05d01f33d03a7fd54baf8b7c61acf53824c56358 b52fb68c87ea07cf8166c0807eef03ea41b38c83 -- packages/core/src/zoo-remote-control
printf '%s\n' '--- diff ---'
git diff --unified=80 05d01f33d03a7fd54baf8b7c61acf53824c56358 b52fb68c87ea07cf8166c0807eef03ea41b38c83 -- packages/core/src/zoo-remote-control
printf '%s\n' '--- relevant references ---'
rg -n -F --glob '*.{ts,tsx}' -e 'zooRemoteCommandsResponseSchema' -e 'pollCommands' -e 'getCommands(' -e 'executeRemoteCommand' packages/core/src packages/core/src/zoo-remote-control || test "$?" -eq 1
printf '%s\n' '--- client source ---'
nl -ba packages/core/src/zoo-remote-control/ZooRemoteControlClient.ts
printf '%s\n' '--- schema/type sources ---'
for f in packages/core/src/zoo-remote-control/types.ts packages/core/src/zoo-remote-control/ZooRemoteControlBridge.ts packages/core/src/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts packages/core/src/zoo-remote-control/__tests__/ZooRemoteControlClient.spec.ts; do
if test -f "$f"; then
printf '\n--- %s ---\n' "$f"
nl -ba "$f"
fi
doneRepository: Zoo-Code-Org/Zoo-Code Length of output: 43055 🏁 Script executed: set -o pipefail
printf '%s\n' '--- changed paths ---'
git diff --name-only 05d01f33d03a7fd54baf8b7c61acf53824c56358 b52fb68c87ea07cf8166c0807eef03ea41b38c83 -- packages/core/src/zoo-remote-control
printf '%s\n' '--- diff ---'
git diff --unified=80 05d01f33d03a7fd54baf8b7c61acf53824c56358 b52fb68c87ea07cf8166c0807eef03ea41b38c83 -- packages/core/src/zoo-remote-control
printf '%s\n' '--- relevant references ---'
rg -n -F --glob '*.{ts,tsx}' -e 'zooRemoteCommandsResponseSchema' -e 'pollCommands' -e 'getCommands(' -e 'executeRemoteCommand' packages/core/src || test "$?" -eq 1
printf '%s\n' '--- source files ---'
for f in packages/core/src/zoo-remote-control/ZooRemoteControlClient.ts packages/core/src/zoo-remote-control/types.ts packages/core/src/zoo-remote-control/ZooRemoteControlBridge.ts packages/core/src/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts packages/core/src/zoo-remote-control/__tests__/ZooRemoteControlClient.spec.ts; do
if test -f "$f"; then
printf '\n--- %s ---\n' "$f"
nl -ba "$f"
fi
doneRepository: Zoo-Code-Org/Zoo-Code Length of output: 42586 🏁 Script executed: set -o pipefail
printf '%s\n' '--- all remote command consumers ---'
rg -n -F --glob '*.{ts,tsx,js,jsx,mts,mjs,cts,cjs}' \
-e 'getCommands(' -e 'ackCommand(' -e 'pollCommands' -e 'executeRemoteCommand' \
-e 'classifyRemoteControlHttpResult' -e 'ZooRemoteControlClient' . || test "$?" -eq 1
printf '%s\n' '--- remote-control files ---'
rg --files | rg '(^|/)(ZooRemoteControlBridge|remote-control|zoo-remote-control)([^/]*)'
printf '%s\n' '--- endpoint and response-contract references ---'
rg -n -F --glob '*.{ts,tsx,js,jsx,mts,mjs,cts,cjs,json,md}' \
-e '/api/extension/remote/commands' -e 'command_not_found' -e 'invalid_command' -e 'commands: z.array' \
-e 'redeliver' -e 'un-ack' -e 'unack' . || test "$?" -eq 1Repository: Zoo-Code-Org/Zoo-Code Length of output: 16683 🏁 Script executed: set -o pipefail
for f in src/services/zoo-remote-control/ZooRemoteControlBridge.ts src/services/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts packages/core/src/zoo-remote-control/bridgeHelpers.ts; do
printf '\n--- %s ---\n' "$f"
if test -f "$f"; then
case "$f" in
src/services/zoo-remote-control/ZooRemoteControlBridge.ts)
nl -ba "$f" | sed -n '300,380p;480,675p' ;;
src/services/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts)
nl -ba "$f" | sed -n '1,220p' ;;
*)
nl -ba "$f" ;;
esac
fi
done
printf '\n--- relevant test names and command fixtures ---\n'
rg -n -F -e 'unknown command' -e 'unknown_command' -e 'getCommands' -e 'ackCommand' -e 'pollCommands' -e 'status: 0' src/services/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.tsRepository: Zoo-Code-Org/Zoo-Code Length of output: 26994 🏁 Script executed: set -o pipefail
for f in src/services/zoo-remote-control/ZooRemoteControlBridge.ts src/services/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts packages/core/src/zoo-remote-control/bridgeHelpers.ts; do
printf '\n--- %s ---\n' "$f"
if test -f "$f"; then
case "$f" in
src/services/zoo-remote-control/ZooRemoteControlBridge.ts)
nl -ba "$f" | sed -n '300,380p;480,675p' ;;
src/services/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.ts)
nl -ba "$f" | sed -n '1,240p' ;;
*)
nl -ba "$f" ;;
esac
fi
done
printf '\n--- relevant test names and command fixtures ---\n'
rg -n -F -e 'unknown command' -e 'unknown_command' -e 'getCommands' -e 'ackCommand' -e 'pollCommands' -e 'status: 0' src/services/zoo-remote-control/__tests__/ZooRemoteControlBridge.spec.tsRepository: Zoo-Code-Org/Zoo-Code Length of output: 27745 Validate remote commands individually.
Parse the envelope, then validate each entry with 🤖 Prompt for AI Agents |
||
| } catch (error) { | ||
| this.log( | ||
| `[ZooRemoteControl] ${method} ${path} failed: ${error instanceof Error ? error.message : String(error)}`, | ||
| ) | ||
| return { ok: false, status: 0 } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What prevents a half-open connection from permanently stranding this remote operation without an abort deadline?