mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-10 00:14:27 +00:00
## Summary Yolo (permission auto-accept) currently lives entirely in the UI. Each browser keeps its own toggle in `localStorage` and auto-replies to permission requests over a 4-hop path (`OpenCode → server SSE → UI → server proxy → OpenCode`). The server — which already sits on the event stream that carries every `permission.asked` — does none of the work. This PR makes the **server authoritative**: it owns the toggle state, resolves family-root inheritance, and performs the auto-reply in-process via loopback using the same `"once"` semantics the UI used to send. The UI becomes a pure view: it toggles via REST and mirrors state from a `yolo.stateChanged` SSE event. ## Why - **Correctness**: the server already consumes the instance SSE stream (`InstanceEventBridge`); auto-accepting there is the natural choke point instead of bouncing to the UI and back. - **Multi-client**: previously each browser had independent `localStorage` state and never synced. Toggles now broadcast to all connected clients in real time. - **Headless**: Yolo keeps auto-accepting even when no UI is connected (useful for long autonomous runs). - **Latency**: drops from 4 hops to a single in-process loopback call. ## What changed **Server (new, authoritative)** - `permissions/auto-accept-store.ts` — in-memory state keyed by family root. Faithful port of `resolvePermissionAutoAcceptFamilyRoot`: fork/`revert` sessions root at themselves; enabling any member enables the whole family. - `permissions/auto-accept-manager.ts` — subscribes to `instance.event`, builds the session tree from `session.created/updated/deleted` (`properties.info`), intercepts `permission.v2.asked` / `permission.asked`, dedupes in-flight replies, emits `yolo.stateChanged` / `yolo.autoAccepted`, clears per-instance state on `workspace.stopped/error`. - `permissions/opencode-replier.ts` — default replier calling OpenCode directly (`getInstancePort` + auth header), mirroring `background-processes/manager.ts`. - `server/routes/yolo.ts` — `GET/POST /workspaces/:id/yolo/sessions/:sid[/toggle]`, following existing route conventions. - `api-types.ts` / `events/bus.ts` — `YoloStateResponse` + the two new event types registered in `onEvent` so they flow over `/api/events`. **UI (pure view)** - `permission-auto-accept.ts` — removed `localStorage`, persistence, and drain logic; now a runtime (non-persisted) projection. `resolvePermissionAutoAcceptFamilyRoot` is **retained as a display aid** so the badge still lights up for child/sub-sessions of an enabled family (preserving the inheritance UX). - `instances.ts` — toggle calls REST (optimistic, reconciled on success, reverted on failure); subscribes to `yolo.stateChanged`; `ensureYoloStateSynced` backfills the active session's state on first connect (deduped per session, reset on SSE reconnect so it re-syncs after a server restart). - `session-events.ts` — removed the three `drainAutoAcceptPermissionsForInstance` hooks (the server drains now). - `api-client.ts` — `getYoloState` / `toggleYolo`. ## Behavior parity | Aspect | Before | After | |---|---|---| | Reply semantics | `"once"` | `"once"` (unchanged) | | Inheritance | whole family (root + non-fork descendants) | **same** | | Fork isolation | `revert` session is its own root | **same** | | Persistence | UI `localStorage` | none (intentional, see Notes) | | Multi-client sync | ❌ independent per browser | ✅ real-time via SSE | | Works with UI closed | ❌ | ✅ | | Auto-reply path | 4 hops | 1 in-process hop | ## Testing 32 unit tests added (`node:test`), all passing. Server + UI typecheck clean. - **Store (16)**: inheritance (parent/child/sibling), fork isolation, cyclic parent chains, late parent discovery, `revert` re-rooting, per-instance independence, tree maintenance. - **Manager (13)**: real `properties.info` event shapes, v2 vs legacy reply, in-flight dedup + retry-after-resolve, `yolo.stateChanged` emission, `session.deleted` keeps toggle, `workspace.stopped` clears state, `stop()` unsubscribes. - **UI (3)**: retained `resolvePermissionAutoAcceptFamilyRoot` display-projection tests. ## Notes for reviewers - **No persistence is intentional** for this milestone — server restart resets all Yolo state (matches the agreed scope). The UI mirror self-heals via SSE reconnect + `ensureYoloStateSynced`. Persistence can be layered on later (e.g. into `~/.config/codenomad/config.json`) without touching the manager. - **Family-root resolution lives in two places on purpose**: the server resolves it to decide whether to auto-reply; the UI resolves the same pure function to render the badge instantly (synchronous memo). Both are faithful ports; no network round-trip is added for display. - **`properties.info` nesting**: OpenCode wraps session records under `properties.info` for `session.*` events (permission events are flat). The manager handles both and the tests use the real nested shape to guard against regressions. - `getYoloState` / `yolo.autoAccepted` are wired but `yolo.autoAccepted` is not yet consumed by the UI — it's available on the wire for future observability (e.g. an audit log / toast). ## Risk / rollback The change is additive on the server and the UI gracefully degrades to the SSE mirror. If the server lacks the new routes (mixed-version), the UI's optimistic toggle still flips locally and `toggleYolo` failures are logged + reverted, so no hard breakage. --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
51 lines
1.5 KiB
JSON
51 lines
1.5 KiB
JSON
{
|
|
"name": "@neuralnomads/codenomad",
|
|
"version": "0.18.0",
|
|
"description": "CodeNomad Server",
|
|
"license": "MIT",
|
|
"author": {
|
|
"name": "Neural Nomads",
|
|
"email": "codenomad@neuralnomads.ai"
|
|
},
|
|
"repository": {
|
|
"type": "git",
|
|
"url": "https://github.com/NeuralNomadsAI/CodeNomad.git"
|
|
},
|
|
"type": "module",
|
|
"main": "dist/index.js",
|
|
"bin": {
|
|
"codenomad": "dist/bin.js"
|
|
},
|
|
"scripts": {
|
|
"build": "npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && node ./scripts/copy-auth-pages.mjs && npm run prepare-plugin",
|
|
"build:ui": "npm run build --prefix ../ui",
|
|
"prepare-ui": "node ./scripts/copy-ui-dist.mjs",
|
|
"prepare-plugin": "node ./scripts/package-opencode-plugin.mjs",
|
|
"dev": "cross-env CODENOMAD_DEV=1 CODENOMAD_SERVER_PASSWORD=codenomad-dev CLI_UI_DEV_SERVER=http://localhost:3000 CLI_HTTPS=false CLI_HTTP=true tsx src/index.ts",
|
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
},
|
|
"dependencies": {
|
|
"@fastify/cors": "^8.5.0",
|
|
"@fastify/reply-from": "^9.8.0",
|
|
"@fastify/static": "^7.0.4",
|
|
"@opencode-ai/sdk": "^1.17.8",
|
|
"commander": "^12.1.0",
|
|
"fastify": "^4.28.1",
|
|
"fuzzysort": "^2.0.4",
|
|
"node-forge": "^1.3.3",
|
|
"openai": "^6.27.0",
|
|
"pino": "^9.4.0",
|
|
"undici": "^6.19.8",
|
|
"yaml": "^2.4.2",
|
|
"yauzl": "^2.10.0",
|
|
"zod": "^3.23.8"
|
|
},
|
|
"devDependencies": {
|
|
"@types/node-forge": "^1.3.14",
|
|
"@types/yauzl": "^2.10.0",
|
|
"cross-env": "^7.0.3",
|
|
"ts-node": "^10.9.2",
|
|
"tsx": "^4.20.6",
|
|
"typescript": "^5.6.3"
|
|
}
|
|
}
|