Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d4e6313
feat: Phase 4a same-origin embed reverse-proxy (no iframes)
benjsmith Sep 18, 2026
eb5faa5
fix(embed): wait for proxied flag before mounting Agents/Graph
benjsmith Sep 19, 2026
679c067
feat(embed): keep CE viewer alive when proxied embeds are on
benjsmith Sep 19, 2026
1bd8d6f
feat(embed): soft-reload proxied Graph on wiki files_changed
benjsmith Sep 19, 2026
bf39c06
feat(core-skills): always auto-start CE+okstratr; rail host_notify
benjsmith Sep 19, 2026
b2edd23
feat(embed): v2 same-document mount + core-skills wait chrome
benjsmith Sep 19, 2026
245fc11
feat(settings): thin client over okstratr harness registry SSOT
benjsmith Sep 19, 2026
4c9865a
feat(packs): drain CE pack-runs queue into Switchbay rail LLM
benjsmith Sep 19, 2026
700ed61
feat(ingest): prefer CE /embed/ce drop-ingest endpoint
benjsmith Sep 19, 2026
c7f3f09
feat(ingest): drain CE ingest-runs via local_ingest (rail opt-in)
benjsmith Sep 19, 2026
084b7a4
fix(workspaces): allow /workspace (+ env roots) in home-gate sandbox
benjsmith Sep 20, 2026
50e2da5
docs: align v0.12.19 with skill import gate
benjsmith Sep 20, 2026
1a6e19d
Render map: procedure/execution palette + [proc]/[exec] prefixes
benjsmith Sep 23, 2026
b902374
fix(graph): serve data.json fast; enrich + progress off hot path
benjsmith Sep 27, 2026
8308b40
fix(graph): run wiki_render under workspace .venv for kuzu edges
benjsmith Sep 27, 2026
8256a5e
fix(ce): retarget viewer on workspace switch so Graph follows tip
benjsmith Sep 27, 2026
082f69d
fix(embed): gzip large JSON + guard soft-remount during CE load
benjsmith Sep 27, 2026
b787478
fix(graph): CE atlas embed skin — no full HTML remount
benjsmith Sep 27, 2026
1019c9f
feat(embed): dual-mount CE session — shell sidebar + Graph canvas
benjsmith Sep 27, 2026
3718338
feat(embed): wire CEEmbed.create handle for dual sidebar+canvas mounts
benjsmith Sep 27, 2026
c2724e9
fix(embed): Switchbay dual-mount sidebar bugs after CE Graph mount
benjsmith Sep 27, 2026
a6c6b28
fix(vault): serve missing extracts from vault.db + clearer 404 UX
benjsmith Sep 27, 2026
069336a
fix(embed): keep CE atlas warm across Graph tab switches
benjsmith Sep 27, 2026
56e2e65
fix(okstratr): set OKSTRATR_HOSTED=switchbay on supervised serve
benjsmith Sep 27, 2026
bed3386
feat(embed): load atlas-cache.js + pass workspace for cold Graph cache
benjsmith Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/ADR-004-same-origin-embed-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ADR-004: Same-origin embed reverse-proxy (no iframes)

- **Status:** Accepted (Phase 4a); Embed v2 mount → [ADR-004b](./ADR-004b-embed-v2-same-document-mount.md)
- **Date:** 2026-09-18
- **Deciders:** Ben / skill-shell rationalization charter

## Context

Switchbay hosts Graph (curiosity-engine atlas/wiki) and Agents (okstratr
observer/desk) surfaces. Cross-origin iframes pointed at
`127.0.0.1:8766` / `:8767` break hosted-mode control (okstratr HTML
settings must hide when `host=switchbay`) and create an opaque nested
browsing context.

Charter locked decision #1: shells **same-origin reverse-proxy** skill
daemons; in-app panels load first-party proxied routes — **not nested
frames**.

## Decision

1. **Daemon reverse-proxy** (always on; independent of the UI flag):
- `/embed/ce/*` → `http://127.0.0.1:8766/*` (override:
`SWITCHBAY_CE_UPSTREAM`, must remain loopback)
- `/embed/okstratr/*` → `http://127.0.0.1:8767/*` (override:
`SWITCHBAY_OKSTRATR_UPSTREAM`, must remain loopback)
- Upstream allowlist is **loopback-only** (`127.0.0.1` / `::1` /
`localhost`). Non-loopback upstreams are rejected (502).
- Inject hosted-shell headers on every proxied request:
- `X-CE-Host: switchbay`
- `X-Okstratr-Host: switchbay`

2. **No iframes** for Graph/Agents skill surfaces. Feature flag
`proxied_skill_embeds` (default **false**) switches Graph → CE panel
and Agents → okstratr panel that navigate `/embed/*` via same-origin
`fetch` + same-document rendering. Phase 4a was script-stripped HTML / JSON; Embed v2 (ADR-004b) executes skill scripts in-panel.
Built-in GraphTab / AgentDashboardTab / filebrowser remain the
default and are **not deleted**.

3. **Settings → okstratr registry (done — ADR-005).** Switchbay settings
is a thin client over okstratr's harness/model registry
(`harnesses.toml` via `/api/okstratr/harness…` / embed proxy). No
second Switchbay allowlist. See
[ADR-005](./ADR-005-okstratr-harness-registry-client.md).

## Consequences

- Dev Vite must proxy `/embed` to the daemon (`vite.config.ts`).
- CE and okstratr should honor public-base + `X-*-Host` (okstratr
Phase 1a `public_base.py`; CE equivalent TBD).
- Full atlas/observer chrome parity is gated by the parity checklist
before any Switchbay duplicate deletion (Phase 4b+).
- WebSocket upgrade through the embed proxy is deferred; HTTP(S) first.

## Alternatives considered

| Option | Why not |
|--------|---------|
| Cross-origin iframe to :8766/:8767 | Opaque origin; hosted settings leak; charter forbid |
| Same-origin iframe to `/embed/*` | Still a nested frame; charter: no iframes |
| Delete built-in Graph/Agents now | Violates parity checklist / dual-stack gate |
78 changes: 78 additions & 0 deletions docs/ADR-004b-embed-v2-same-document-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# ADR-004b: Embed v2 — same-document interactive mount

- **Status:** Accepted (Embed v2)
- **Date:** 2026-09-19
- **Parent:** [ADR-004](./ADR-004-same-origin-embed-proxy.md)
- **Contract:** umbrella `CONTRACT-EMBED-V2.md`

## Context

Phase 4a (`ProxiedSkillPanel`) fetched `/embed/*` HTML and injected a
**script-stripped** body. That proved the reverse-proxy path but left Graph
and Agents inert (no pan/zoom, no desk controls). Charter + ADR-004 forbid
`<iframe>` / nested frames for skill surfaces.

CE and okstratr already honor `CE_PUBLIC_BASE` / `OKSTRATR_PUBLIC_BASE`
(`_/embed/ce_`, `_/embed/okstratr_`) and inject `window.*_PUBLIC_BASE` so
API helpers resolve under the proxy. Relative asset tags (`static/main.js`,
`observer.css`) still need rewriting when the HTML is mounted into a
Switchbay panel rather than navigated as a top-level document.

## Decision

**Same-document microfrontend mount** (no nested browsing context):

1. **Wait chrome first.** While the panel is open, poll
`GET /api/core-skills/status` (~2s). Banner states: `starting` /
`unhealthy` / `building_wiki` / `live`. Do **not** flash a full 502
error while the supervisor is still `starting` or `stopped` —
status chrome explains the wait (`suppressFetchError`).
2. **Fetch** proxied HTML from `/embed/ce/…` or `/embed/okstratr/…` only
when the skill slice is `healthy` (`allowMount`).
3. **Rewrite** relative / root-relative / loopback asset URLs onto the
public base (`frontend/src/widgets/embed/embedMount.ts`).
4. **Extract** `<script>` tags (classic + `type=module`); inject remaining
markup (body + head stylesheets) into a dedicated panel root via
`innerHTML`.
5. **Execute** scripts by `document.createElement("script")` + append
(with rewritten `src`, or inline `textContent`). Browsers never run
scripts inserted via `innerHTML` alone.
6. **Tear down** before soft-reload (`sy:files-changed`) or unmount:
remove tracked script nodes and clear the panel root so listeners /
duplicate roots do not accumulate.
7. **JSON** responses keep the pretty-print path (no script mount).

Built-in `GraphTab` is **not** deleted (parity checklist / Ben lock).

## Consequences

- Interactive CE / okstratr chrome can run under `/embed/*` without iframes.
- Skill UIs share the Switchbay document: global ID collisions and
`document`-level assumptions are residual risks; skills should prefer
scoped queries when possible.
- **WebSocket upgrade** through the embed proxy remains deferred (HTTP
first). Live features that require WS may be limited until proxyed.
- Unit tests cover URL rewrite, script extraction, and status-banner
mapping (`embedMount.test.ts`).

## Alternatives considered

| Option | Why not |
|--------|---------|
| Keep script-stripping | Inert UI; fails Embed v2 acceptance |
| Same-origin `<iframe src="/embed/…">` | Nested frame; charter / ADR-004 forbid |
| `srcdoc` iframe | Still a nested browsing context |
| Shadow DOM isolation | Extra complexity; CE/okstratr expect light DOM + `getElementById` |

## Mount algorithm (short)

```
poll status → if !healthy: banner only
else:
teardown prior mount
html = fetch(/embed/…/)
{ markup, scripts } = prepareEmbedHtml(html, publicBase, pagePath)
root.innerHTML = markup
for s in scripts: createElement(script); set src|text; append; await load
on files_changed / unmount: teardown
```
57 changes: 57 additions & 0 deletions docs/ADR-005-okstratr-harness-registry-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ADR-005: Settings → okstratr harness registry (SSOT client)

- **Status:** Accepted
- **Date:** 2026-09-19
- **Parent:** [ADR-004](./ADR-004-same-origin-embed-proxy.md) (Settings TODO)
- **Deciders:** Ben / skill-shell rationalization charter

## Context

okstratr owns the harness+model registry (`~/.config/okstratr/harnesses.toml`
via `GET/POST /api/harness…`). Charter: shells are **config UIs** over that
registry — no second Switchbay allowlist. Hosted mode
(`X-Okstratr-Host: switchbay`) already disables okstratr HTML settings, so
Switchbay Settings must become the write path.

## Decision

1. **Thin client module** `switchbay.okstratr_harness`:
- Server-side calls loopback `SWITCHBAY_OKSTRATR_UPSTREAM`
(default `http://127.0.0.1:8767`) with hosted-shell header.
- Loopback guard reused from `embed_proxy` (non-loopback → 502).
- `normalize_registry()` maps okstratr `list_for_api` JSON; never
invents an allowlist.
- Browser may also hit same-origin `/embed/okstratr/api/harness…`.

2. **Daemon routes** (shells/UI need not know embed paths):
- `GET /api/okstratr/harness`
- `POST /api/okstratr/harness/enable` `{ "id": "…" }`
- `POST /api/okstratr/harness/disable` `{ "id": "…" }`
- `POST /api/okstratr/harness/set` `{ "key": "…", "value": "…" }`
- `POST /api/okstratr/harness/reload`

3. **Settings UI** — section **Harness registry · okstratr**: list rows,
enable/disable, set `harness.<id>.default_model`. Existing Pi / local
LLM panels remain as **rail** surfaces and are labeled as not the
desk/agent SSOT.

4. **okbay** mirrors the same thin-client pattern (doc note; full UI
follow-up) — see okbay `docs/HERDR-AND-REGISTRY.md` and Switchbay
`/api/okstratr/host-notify` as the precedent for path-native okstratr
integration.

## Consequences

- okstratr must be healthy (core-skill supervisor) for Settings toggles
to succeed; UI surfaces a clear 502 when upstream is down.
- No `harnesses.toml` (or equivalent) under Switchbay state dirs.
- Pi harness (`/api/llm/harness`) and local LLM panels stay Switchbay-local
for rail chat; they must not claim desk registry ownership.

## Alternatives considered

| Option | Why not |
|--------|---------|
| Copy allowlist into Switchbay `app_settings` | Second SSOT; charter forbid |
| Settings UI calls `:8767` cross-origin | Breaks hosted-mode headers / CORS |
| Only document CLI (`okstratr harness …`) | Users need in-app toggles when HTML settings are hosted-off |
54 changes: 54 additions & 0 deletions docs/ADR-006-ce-pack-run-drain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ADR-006: CE pack-queue → Switchbay rail LLM drain

- **Status:** Accepted
- **Date:** 2026-09-19
- **Parent:** skill-shell rationalization (CE filebrowser packs Phase 2b)
- **Deciders:** Ben / skill-shell rationalization charter

## Context

Curiosity Engine's proxied filebrowser accepts
`POST /api/packs/<pack>/action/<action>` and, after sandbox checks,
writes only:

```text
.workbench/pack-runs/<run_id>.json # status: "queued"
```

It does **not** seat an LLM agent. Switchbay already has
`handle_pack_action` which seats `_dispatch_chat` with skill
`<pack>-<action>`, but the CE path never hits that handler — so
CE-queued runs stayed `queued` forever.

Both processes share the workspace tree, so the queue files are the
handoff surface.

## Decision

1. **Switchbay drains the shared JSON** (no CE `PATCH` required).
Module `switchbay.pack_run_drain`:
- `list_queued_runs` / `write_run_status` (atomic JSON rewrite)
- `build_pack_prompt` — same text as `handle_pack_action`
- `drain_once` — validate pack enabled + skill exists, mark
`running`, spawn `_dispatch_chat` (injectable for tests), on
task completion mark `done` / `failed`
- In-memory `app["pack_run_inflight"]` dedupes concurrent drains

2. **Triggers**
- Background poll every ~4s while
`<workspace>/.workbench/pack-runs/` exists
- Explicit `POST /api/packs/drain` → `{drained, skipped, errors}`
- Opportunistic kick from `_broadcast_files_changed_soon` when
the pack-runs dir is present

3. **Out of scope (this ADR):** reveal-in-OS, drop-ingest, rewriting
CE's queue writer, and refactoring `handle_pack_action` to also
write pack-runs JSON (nice-to-have; CE files are the required
consumer).

## Consequences

- CE remains the sandbox/accept edge; Switchbay remains the skill/LLM
seat. Status fields on the shared JSON are the cross-process progress
UI.
- Tests mock `dispatch_fn` — no paid model seats in CI.
60 changes: 60 additions & 0 deletions docs/ADR-007-ce-ingest-run-drain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ADR-007: CE ingest-queue → Switchbay local_ingest / rail drain

- **Status:** Accepted
- **Date:** 2026-09-20
- **Parent:** skill-shell rationalization (CE drop-ingest + pack-run drain parity)
- **Related:** ADR-006 (CE pack-queue → rail LLM drain)
- **Deciders:** Ben / skill-shell rationalization charter

## Context

Curiosity Engine's drop-ingest (`POST /api/ingest/from-upload` /
`from-path`, filebrowser + `/embed/ce/`) stages bytes under
`vault/raw/` and writes only:

```text
.workbench/ingest-runs/<run_id>.json # status: "queued", mode: "staged"
```

It does **not** run `local_ingest.py` or seat an LLM agent. Pack-runs
already drain via Switchbay (`POST /api/packs/drain`, ADR-006). Without
a sibling drain, ingest-runs sit `queued` forever after drop.

Both processes share the workspace tree, so the queue files are the
handoff surface.

## Decision

1. **Switchbay drains the shared JSON** (no CE `PATCH` required).
Module `switchbay.ingest_run_drain`:
- `list_queued_runs` / `write_run_status` (atomic JSON rewrite)
- `resolve_run_vault_path` — refuse workspace escape / null bytes
- Prefer **deterministic** `local_ingest` (`ce_tools._ce_ingest`)
for normal vault sources (`mode: staged` and peers)
- Escalate to `_dispatch_chat` only when metadata opts in
(`mode`/`drain` ∈ {llm,rail,agent,dispatch} or
`prefer_rail` / `use_llm` truthy)
- `drain_once` — mark `running`, spawn local or rail task, on
completion mark `done` / `failed`
- In-memory `app["ingest_run_inflight"]` dedupes concurrent drains

2. **Triggers**
- Background poll every ~4s while
`<workspace>/.workbench/ingest-runs/` exists
- Explicit `POST /api/ingest/drain` → `{drained, skipped, errors}`
- Opportunistic kick from `_broadcast_files_changed_soon` when
the ingest-runs dir is present

3. **Out of scope:** rewriting CE's queue writer, changing
Switchbay's own multipart `/api/ingest/from-upload` (still seats
rail immediately for non-CE uploads), multimodal CURATE waves.

## Consequences

- CE remains the sandbox/stage edge; Switchbay remains the ingest
executor. Status fields on the shared JSON are the cross-process
progress UI.
- Default path stays cheap (no paid model seats). LLM only when the
run record asks for it.
- Tests mock `local_ingest_fn` / `dispatch_fn` — no CE binary or paid
model seats in CI.
25 changes: 25 additions & 0 deletions docs/ADR-008-workspace-sandbox-roots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# ADR-008: Workspace sandbox roots (stage-5 home rule + box `/workspace`)

**Status:** Accepted
**Date:** 2026-09-20
**Context:** Stage-5 restricted workspace paths to `$HOME` so file-ops /
cebridge never get a cwd of `/`, `/etc`, etc. On the Grok Bot box,
corpora live under `/workspace` (e.g. `/workspace/ce-cb-biocure`).
`Path.resolve()` follows symlinks, so a `~/Workspaces/...` symlink still
failed `is_within_home`; E2E had to `unshare`+bind-mount under home.

**Decision:** Keep the stage-5 sandbox, but allow a small explicit set of
roots via `allowed_workspace_roots()`:

1. Always `Path.home()`.
2. `/workspace` when that directory exists (agent-box scratch).
3. Extra dirs from `SWITCHBAY_WORKSPACE_ROOTS` (`os.pathsep`-separated;
expanduser + resolve; skip missing / non-dirs).

`is_within_home(path)` (name retained for call sites) returns true iff
the resolved path is under any of those roots. Error copy uses
`home_label()` → e.g. `must live inside /home/box or /workspace`.

**Consequences:** Box corpora register without bind-mount workarounds.
`/etc`, `/tmp`, `/`, and `..` escapes remain refused unless an operator
deliberately adds a root via the env var.
Loading
Loading