Skip to content

Commit 317066a

Browse files
Merge branch 'Th0rgal:master' into master
2 parents b114598 + 7e94303 commit 317066a

282 files changed

Lines changed: 33391 additions & 15609 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ secrets.json
3232
.open_agent_calibration/
3333
/tmp/
3434
/dashboard/console.log
35+
/.playwright-console.log
36+
/automations-*.png
37+
/control-bubbles.png
38+
/lifetime-cost-fixed.png
39+
/msg-feedback-failed.png
40+
/stats-responsive-*.png
41+
/thoughts-history-*.png
42+
/unified-bubbles.png
3543

3644
# Claude Code per-project session state
3745
.claude/

ASK_ASSISTANT_DESIGN.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Ask Assistant — Design
2+
3+
> A fast, read-mostly **sidecar assistant** that rides alongside a running
4+
> mission. It can read the full mission history, run bash in the live workspace,
5+
> and answer questions — **without interrupting the main agent's turn loop or
6+
> queue**. Powered by a configurable fast model (default: Cerebras
7+
> `gpt-oss-120b`).
8+
9+
Status: **M1 backend implemented** (store + client + loop + HTTP routes,
10+
compiles, storage unit-tested). M2–M5 pending.
11+
Owner: Thomas
12+
Related: conversation-anchored snapshot (#477, #482), thinking capture (#481),
13+
Cerebras title fix (#489), `metadata_llm.rs` provider ladder.
14+
15+
---
16+
17+
## 1. Goals & non-goals
18+
19+
### Goals
20+
- Let the operator **chat about what a mission is doing** at any time, in a
21+
separate lane that never touches the harness lock, the message queue, or the
22+
mission transcript fed back to the working agent.
23+
- Give the assistant **real capability**: full read+write `bash` in the live
24+
workspace plus history retrieval.
25+
- **Persistent, browsable Ask threads** per mission, clearable, **never merged
26+
into mission history** (the working agent cannot see the Ask conversation).
27+
- A **dedicated, configurable Ask model** (separate from the metadata model),
28+
optimized for *smart + fast + large context*.
29+
- One **quiet "operator-note"** bridge: when Ask **writes** to the workspace, the
30+
working agent gets a passive heads-up on its next turn.
31+
- Works across web, iOS, and Android (shared API).
32+
33+
### Non-goals (v1)
34+
- No worktree / sandbox-copy mode (usage is read-dominant).
35+
- No destructive-write confirmation gate. Full bash, no friction; a visual
36+
"agent idle/working" indicator is informational only.
37+
- ~~Ask never **starts** a main-agent turn. Strictly non-interrupting.~~
38+
**Superseded (v1.1):** the Copilot now has explicit steering tools —
39+
`stop_agent` (CancelMission, same as the Stop button) and `send_to_agent`
40+
(a real composer-equivalent UserMessage, optionally interrupting the
41+
current turn first). The system prompt frames these as interventions to
42+
use when the operator asks to stop/steer, or in clearly harmful loops;
43+
the default posture stays read-mostly.
44+
- Not every harness *heeds* the operator-note identically — delivery is
45+
harness-agnostic, faithfulness is best-effort (§9).
46+
47+
---
48+
49+
## 2. Concept & lanes
50+
51+
Two lanes share one workspace and one mission identity, otherwise isolated:
52+
53+
- **Driver lane** — the working agent: `mission_events`, history, harness lock,
54+
message queue.
55+
- **Co-pilot lane** — the Ask assistant: `ask_threads` / `ask_messages` (separate
56+
`ask.db`), no lock, no queue, its own response path.
57+
58+
Hard rule: **nothing ever reads `ask_*` rows into the working agent's prompt.**
59+
The only cross-lane bridges are the operator-note (Ask write → working agent,
60+
passive) and "Send to agent" (manual: Ask answer → real composer).
61+
62+
---
63+
64+
## 3. Data model (implemented)
65+
66+
Separate `ask.db` (rusqlite), keyed by `mission_id` (ownership enforced at the
67+
HTTP layer via the user's per-user mission store).
68+
69+
- `ask_threads(id, mission_id, title, model, created_at, updated_at)`
70+
- `ask_messages(id, thread_id, seq, role, content, tool_name, tool_call_id,
71+
metadata, created_at)``role ∈ {user, assistant, tool_call, tool_result}`,
72+
FK → `ask_threads ON DELETE CASCADE`
73+
- `operator_notes(id, mission_id, body, source_thread_id, created_at,
74+
flushed_at)` — the M2 bridge buffer
75+
76+
Store: `src/api/ask/store.rs` (`AskStore`). Global singleton via tokio
77+
`OnceCell` at `<working_dir>/.sandboxed-sh/missions/ask.db`.
78+
79+
---
80+
81+
## 4. Backend: the Ask agentic loop (implemented)
82+
83+
`src/api/ask/` — net-new, small, in-process:
84+
- `store.rs` — persistence (above).
85+
- `client.rs` — OpenAI-compatible chat client **with tool-calling** (reuses
86+
`MetadataLlmConfig`; forwards `reasoning_effort` so reasoning models return
87+
visible content).
88+
- `mod.rs``run_ask_turn`: persist user msg → seed system prompt (recent
89+
events + summary) → tool loop (≤6 iterations) → persist assistant answer.
90+
- `http.rs` — handlers.
91+
92+
Tools (read-on-demand so the 128K context ceiling never bites):
93+
- `bash(command)``WorkspaceExec::output(work_dir, "/bin/bash", ["-lc", cmd])`
94+
**full** read+write, live workspace, host or nspawn. No gate.
95+
- `read_history(limit)``MissionStore::get_latest_events`.
96+
- `read_file(path)` → bash `cat`.
97+
98+
---
99+
100+
## 5. Model configuration (implemented)
101+
102+
`metadata_llm.rs`:
103+
- Lifted `resolve_provider_api_key` to module scope.
104+
- Added `build_assistant_llm_config()` → prefers Cerebras `gpt-oss-120b`
105+
(`reasoning_effort=low`), overridable via `ASK_ASSISTANT_MODEL` env; falls back
106+
to the metadata provider ladder.
107+
108+
Cerebras caps context at ~128K regardless of model — seed recent window +
109+
summary, let the model pull more via grep/`read_history`. (M3 adds a Settings
110+
picker with a `supports_tools` capability flag.)
111+
112+
---
113+
114+
## 6. HTTP API (implemented)
115+
116+
Routes (protected) in `routes.rs`, handlers in `src/api/ask/http.rs`:
117+
118+
```
119+
POST /api/control/missions/:id/ask send (returns final answer + messages)
120+
GET /api/control/missions/:id/ask/threads list threads
121+
GET /api/control/missions/:id/ask/threads/:tid thread + messages
122+
DELETE /api/control/missions/:id/ask/threads/:tid clear/delete thread
123+
```
124+
125+
These never call `queue_message` or acquire the harness lock.
126+
127+
> M1 returns the final answer synchronously (JSON). Token-level **SSE streaming**
128+
> (`AgentEvent::Ask*` variants on a separate lane) is folded into M3 when the web
129+
> panel needs it.
130+
131+
---
132+
133+
## 7. Context strategy (the 128K ceiling)
134+
135+
Seed: recent mission events + summary + the thread's prior messages. Then the
136+
model pulls more via `read_history` / `bash grep`. Keeps every request under
137+
128K and scales to arbitrarily long missions.
138+
139+
---
140+
141+
## 8. The quiet operator-note (M2)
142+
143+
Delivery is harness-agnostic; faithfulness is best-effort.
144+
145+
- **Detect** an Ask write by wrapping `bash`: `git status --porcelain` snapshot
146+
before/after (non-git fallback deferred to M5). On change →
147+
`enqueue_operator_note`.
148+
- **Deliver** by prepending a `<operator-note>` block into the working agent's
149+
**next** `user_message` (all backends take the next turn as a single string:
150+
Claude Code `--` arg, OpenCode prompt file, Codex `send_message_streaming`).
151+
Centralized helper `prepend_pending_operator_notes(mission_id, user_message)`.
152+
- **Invariant:** notes are passive — flushed only when a turn is already about to
153+
run. Ask can never wake the working agent.
154+
- **Audit:** a flushed note records an `operator_note` mission event (the *fact*
155+
of a write touches the mission; the *chat* does not).
156+
157+
Compatibility: all backends *see* the note; Claude Code *heeds* `<…>`-tagged
158+
blocks best.
159+
160+
---
161+
162+
## 9. Frontend — web (M3)
163+
164+
Right-side collapsible **Ask panel**: thread switcher, co-pilot styling (distinct
165+
from the indigo Bot), tool-call rendering, slim composer, informational
166+
"agent idle/working" banner, per-item "ask about this" spark, "Send to agent"
167+
bridge. `Ask` button beside Queue/Stop + a keyboard toggle (not a Cmd+W variant).
168+
169+
## 10. Frontend — iOS / Android (M4)
170+
171+
Same endpoints. iOS: `.sheet` with medium/large detents. Android: bottom sheet.
172+
173+
---
174+
175+
## 11. Milestones
176+
177+
- **M1 — Backend core** ✅ tables + store (unit-tested), Ask loop + tools,
178+
`build_assistant_llm_config`, routes. Synchronous JSON responses.
179+
- **M2 — Operator-note bridge** — git-porcelain detection +
180+
`prepend_pending_operator_notes` in all backend turn-prep + audit event.
181+
- **M3 — Web panel** — Ask store + (optional SSE), drawer, picker, spark, bridge.
182+
- **M4 — Mobile** — iOS sheet + Android bottom sheet.
183+
- **M5 — Polish** — per-thread cost, auto-titling, non-git write detection,
184+
sandbox-copy mode.
185+
186+
---
187+
188+
## 12. Verification
189+
190+
- M1 storage: `cargo test --lib ask::store` (thread/message roundtrip, cascade
191+
delete, operator-note take-once) — passing.
192+
- M1 end-to-end: deploy to **dev**, then
193+
`curl -X POST /api/control/missions/<id>/ask -d '{"content":"what is the agent doing?"}'`
194+
with a Cerebras key configured. (Pending live run.)

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sandboxed_sh"
3-
version = "0.12.0"
3+
version = "1.3.0"
44
edition = "2021"
55
rust-version = "1.91"
66
description = "Self-hosted orchestrator for AI coding agents (formerly Open Agent)"
@@ -106,6 +106,10 @@ path = "src/bin/automation_manager_mcp.rs"
106106
name = "orchestrator-mcp"
107107
path = "src/bin/orchestrator_mcp.rs"
108108

109+
[[bin]]
110+
name = "assistant-mcp"
111+
path = "src/bin/assistant_mcp.rs"
112+
109113
[[bin]]
110114
name = "fido-agent-proxy"
111115
path = "src/bin/fido_agent_proxy.rs"

DEBUGGING.md

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,42 @@
99

1010
## Backend Services (Thomas)
1111

12-
Thomas's server runs two instances:
12+
Thomas's server runs two sandboxed.sh instances plus the Hermes assistant stack:
1313

1414
| Service | Port | Domain | Binary |
1515
|---------|------|--------|--------|
1616
| `sandboxed-sh-prod` | 3000 | agent-backend.thomas.md | `/usr/local/bin/sandboxed-sh-prod` |
1717
| `sandboxed-sh-dev` | 3002 | agent-backend-dev.thomas.md | `/usr/local/bin/sandboxed-sh-dev` |
18+
| `hermes-assistant` | 8642 (loopback) | agent-backend.thomas.md `/hermes-remote` | `/usr/local/bin/hermes` (gateway) |
19+
| `hermes-dashboard` | 9130 (loopback) | agent-backend.thomas.md `/hermes-desktop` | `/usr/local/bin/hermes` (dashboard) |
20+
21+
**Hermes endpoints** (nginx config in
22+
`/etc/nginx/sites-enabled/agent-backend.thomas.md` — NOT tracked in git):
23+
24+
- `/hermes-remote` → the gateway's OpenAI-compatible API server (loopback 8642),
25+
re-exposed by the sandboxed.sh backend itself (`hermes_remote_proxy` in
26+
`src/api/system.rs`). `/v1/*` + SSE only, no WebSocket. Used for *split mode*
27+
(a local Hermes sets `GATEWAY_PROXY_URL`/`GATEWAY_PROXY_KEY` and delegates
28+
agent work). Key = `API_SERVER_KEY` in `/etc/sandboxed-sh/hermes-assistant.env`,
29+
readable via `GET /api/system/hermes-assistant/remote`.
30+
- `/hermes-desktop` → the full Hermes dashboard backend (loopback 9130) for the
31+
desktop app's **native Remote-gateway mode** (`/api/status` + `wss /api/ws`).
32+
Unit: `/etc/systemd/system/hermes-dashboard.service` (`hermes dashboard
33+
--no-open --host 127.0.0.1 --port 9130 --skip-build`, same `HERMES_HOME`
34+
`/var/lib/hermes-assistant` as the gateway so sessions/memory are shared,
35+
auth via `HERMES_DASHBOARD_SESSION_TOKEN` = same key, stub web dist at
36+
`/var/lib/hermes-assistant/web_dist`). The nginx location pins
37+
`Host 127.0.0.1:9130` (host-header check) and must NOT add `X-Forwarded-For`
38+
(the WS gate requires a loopback peer).
39+
40+
Hermes gotchas:
41+
42+
- After a prod deploy, `systemctl restart hermes-assistant` so the gateway
43+
respawns the freshly installed `/usr/local/bin/assistant-mcp`.
44+
- When testing the WS handshake with curl, force `--http1.1` — ALPN negotiates
45+
h2, which has no `Upgrade` header, and the resulting 401 is a red herring.
46+
- When rotating the `hsk_` key, update `API_SERVER_KEY`,
47+
`HERMES_DASHBOARD_SESSION_TOKEN`, and the desktop's `connection.json` together.
1848

1949
```bash
2050
# Production
@@ -175,8 +205,26 @@ auto-resumes from the backend but loses the in-flight turn's progress.
175205
Worse, an agent in a retry loop can chainsaw the host (the original
176206
incident: 5 deploys + 2 rollbacks in 90 minutes).
177207

178-
The orchestrator MCP exposes a `deploy_sandboxed_sh` tool that hits the
179-
internal `/api/system/deploy` endpoint with the safety rails:
208+
The orchestrator MCP exposes a `deploy_sandboxed_sh` tool that hits an
209+
internal `/api/system/deploy` endpoint with safety rails.
210+
211+
**Targeting rule:** `deploy_sandboxed_sh` deploys the backend API it is sent
212+
to. If `target_environment` is omitted, the tool uses the API that launched the
213+
mission. A prod mission therefore deploys prod by default; a dev mission deploys
214+
dev by default. To deploy a different Sandboxed.sh environment, set
215+
`target_environment` explicitly:
216+
217+
| Target | Tool argument | API URL | Service restarted |
218+
|--------|---------------|---------|-------------------|
219+
| Current mission API | omit `target_environment` | mission `API_URL` | current API's service |
220+
| Production | `"prod"` | `http://127.0.0.1:3000` | `sandboxed-sh-prod.service` |
221+
| Development | `"dev"` | `http://127.0.0.1:3002` | `sandboxed-sh-dev.service` |
222+
223+
The backend also receives an `expected_service` guard from the MCP and refuses
224+
with HTTP 409 if the request reaches the wrong service. This prevents a prod
225+
mission that intends to test dev from accidentally deploying prod.
226+
227+
Safety rails:
180228

181229
- **Self-protection** — refuses by default if the calling mission lives on
182230
the service being restarted. Override with `force=true` only if you
@@ -193,6 +241,7 @@ From inside an agent that has the orchestrator MCP, the call shape is:
193241
{
194242
"tool": "deploy_sandboxed_sh",
195243
"arguments": {
244+
"target_environment": "dev", // optional; "dev" or "prod"; omit for current API
196245
"git_ref": "origin/master", // optional; omit to use current checkout
197246
"skip_build": false, // optional; true if you built elsewhere
198247
"force": false // override safety rails
@@ -210,9 +259,22 @@ Successful response:
210259
}
211260
```
212261

213-
A refusal (HTTP 409 self-target or 429 debounce) surfaces a message that
214-
explains the override knob, so an agent can decide whether to retry with
215-
`force=true` or back off.
262+
A refusal surfaces a message that explains the next step:
263+
264+
- HTTP 409 target mismatch: the request reached the wrong service/API URL.
265+
- HTTP 409 self-target: pass `force=true` only if killing the caller is acceptable.
266+
- HTTP 429 debounce: wait or pass `force=true` for an urgent deploy.
267+
268+
For a dev-only restart without rebuilding, use the explicit service command:
269+
270+
```bash
271+
ssh -i ~/.ssh/cursor root@95.216.112.253 "systemctl restart sandboxed-sh-dev"
272+
```
273+
274+
Do not call `deploy_sandboxed_sh` from a prod mission unless you intend to
275+
deploy a Sandboxed.sh backend. For dev testing from prod, prefer
276+
`target_environment: "dev"` so the request goes to the dev API and carries the
277+
`sandboxed-sh-dev.service` guard.
216278

217279
## Locking down the SSH backdoor
218280

@@ -374,15 +436,15 @@ Then restart the backend. Workspace-scoped MCPs must be in PATH for both host wo
374436

375437
**Config profile not being applied:** Check that:
376438
1. The configs exist in the **correct library path** (`.sandboxed-sh/library/` for production, not `.openagent/library/` which was the old path)
377-
2. The library has the `configs/<profile>/.opencode/oh-my-opencode.json` file
439+
2. The library has the expected `configs/<profile>/.opencode/` files
378440
3. Pull latest library: `cd ~/.sandboxed-sh/library && git pull`
379441

380442
**Missions not using correct settings:**
381443

382444
**Missions stuck:** Look for running CLI processes:
383445

384446
```bash
385-
ps aux | grep -E "claude|oh-my-opencode"
447+
ps aux | grep -E "claude|opencode"
386448
machinectl list # For container workspaces
387449
```
388450

DEMO.md

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)