diff --git a/.cursor/plans/README.md b/.cursor/plans/README.md index 2c4e4db..947b9b8 100644 --- a/.cursor/plans/README.md +++ b/.cursor/plans/README.md @@ -36,3 +36,13 @@ Do not delete unrelated plans in those directories. Same bytes in all three copi | L — PyPI release | [compass_track_l_pypi_release_43bd556a.plan.md](compass_track_l_pypi_release_43bd556a.plan.md) | | M — Probe credentials | [compass_track_m_probe_credentials_acfb34f5.plan.md](compass_track_m_probe_credentials_acfb34f5.plan.md) | | N — Paid pillars | [compass_track_n_paid_pillars_fae9b18d.plan.md](compass_track_n_paid_pillars_fae9b18d.plan.md) | + +## Phase 3 — Browser agent + status handoff + +| Track | Plan | +| --- | --- | +| O — Generic LLM adapter | [compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md](compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md) | +| P3 Status — Grok→Cursor handoff | [compass_phase3_status_handoff_20260907.plan.md](compass_phase3_status_handoff_20260907.plan.md) | + +Canonical narrative status: [`../../docs/CURSOR-HANDOFF.md`](../../docs/CURSOR-HANDOFF.md) + diff --git a/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md b/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md new file mode 100644 index 0000000..33b7d68 --- /dev/null +++ b/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md @@ -0,0 +1,73 @@ +--- +name: comPASS Phase 3 — Status handoff (Grok Bot → Cursor) +overview: "Authoritative 2026-09-07 status for Cursor agents: Phase 1–2 complete; Phase 3 browser Wasmer + ENI6MA Gate + Track O adapter + Docker challenge client (PR #2). Next work is six-color ceremony, verify/burn, and full agent boot." +todos: + - id: p3-merge-pr2 + content: "Review/merge https://github.com/soltrinox/comPASS/pull/2 (Docker browser-client challenge UI :8088) and land .dockerignore if still local" + status: pending + - id: p3-smoke-compose + content: "Confirm docker compose up — agy-bridge :8791 healthy + browser-client :8088 challenge.html?handle=demo-wasm Load&Prove + Ask" + status: pending + - id: p3-ceremony-six-color + content: "Build interactive six-color ENI6MA ceremony UX (challenge→colors→proof→burn-before-validate); replace Path-B stub/minimal proof as product auth" + status: pending + - id: p3-verify-burn + content: "Wire real verify ABI + Control burn ledger (DEMO-MINT is prove-only today; Gate = digest + abi_probe stub)" + status: pending + - id: p3-agent-boot + content: "Full in-tab agent boot page — ceremony→policy bind→trigger loop→adapter→agy-bridge" + status: pending + - id: p3-compress-real + content: "Replace adapter default_compress_hook with real comPREssOR hop-safe inject on model change" + status: pending + - id: p3-live-agy + content: "Optional compose profile live-agy with real Antigravity CLI (default remains fake-agy)" + status: pending + - id: p3-docs-sync + content: "Keep docs/CURSOR-HANDOFF.md + AUDIT-GOALS-VS-BROWSER-STACK.md + WASMER-DEPLOYMENT.md aligned after each milestone" + status: pending +isProject: true +--- + +# comPASS Phase 3 — Status handoff (Grok Bot → Cursor) + +**Date:** 2026-09-07 (PT) +**Handoff doc:** [`docs/CURSOR-HANDOFF.md`](../../docs/CURSOR-HANDOFF.md) +**Audit:** [`docs/AUDIT-GOALS-VS-BROWSER-STACK.md`](../../docs/AUDIT-GOALS-VS-BROWSER-STACK.md) +**Stack map:** [`docs/WASMER-DEPLOYMENT.md`](../../docs/WASMER-DEPLOYMENT.md) + +## One-line status + +Lab-grade **browser Wasmer + ENI6MA digest Gate + generic adapter + Docker challenge client** is up; **six-color ceremony / verify+burn / full sovereign agent boot** are not shipped. + +## What is done + +- Phase 1–2 offline tiers, Wasmer artifacts, Track O adapter (`src/compass/serve/adapter.py`), ADRs 0005–0007 +- `services/agy-bridge` Gate (cache/fetch/digest/abi_probe) + Docker Compose fake-agy on **:8791** +- Path-B `wasmer/browser/` circuitLoader + wasmerRunner minimal proof stub +- **PR #2** `feat/docker-browser-challenge`: nginx `browser-client` on **:8088**, `challenge.html` (handle / binary_url → digest-pin → prove → Ask → :8791) + +## What is next (priority) + +1. Merge PR #2 + smoke compose +2. Six-color ceremony UX +3. Verify + burn ledger +4. Agent boot shell (policy bind → loop → adapter) +5. Real comPREssOR hop inject + +## Operator smoke + +```bash +cd /Users/rosario/work/comPASS +docker compose up --build -d +open "http://127.0.0.1:8088/challenge.html?handle=demo-wasm" +curl -s http://127.0.0.1:8791/healthz +``` + +Prefer **`gh`** on the Mac for branch/PR (auth lives there). + +## Non-goals (still) + +- No Cursor/IDE product path +- No provider keys in browser/WASM +- Not production SaaS / not OpenRouter replacement diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..810aaa3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.git +**/node_modules +**/.venv +**/__pycache__ +**/*.pyc +test-results +**/target +**/.pytest_cache +**/dist +**/build +*.tgz +.env +.env.* diff --git a/PLANS.md b/PLANS.md index a9a870e..b76798b 100644 --- a/PLANS.md +++ b/PLANS.md @@ -1,6 +1,6 @@ # comPASS program plans -**Date:** 2026-09-05 +**Date:** 2026-09-07 **Ground truth:** [`/Users/rosario/work/comPASS/PROTOTYPE.md`](/Users/rosario/work/comPASS/PROTOTYPE.md) **Summary:** [`/Users/rosario/work/comPASS/SUMMARY/2026-09-03-comPASS-prototype-session.md`](/Users/rosario/work/comPASS/SUMMARY/2026-09-03-comPASS-prototype-session.md) **Canonical compressor:** `git@github.com:soltrinox/comPREssOR.git` at [`/Users/rosario/work/comPREssOR`](/Users/rosario/work/comPREssOR) (engine 0.2.0, `main` @ `44460ba`, CC-1..CC-10) @@ -70,3 +70,14 @@ Browser-only Wasmer appliance (ADR 0005) and generic LLM adapter (ADR 0006). | Track | Name | Cursor plan (work) | User plans | Repo copy | | --- | --- | --- | --- | --- | | O | Generic LLM adapter | [`/Users/rosario/work/.cursor/plans/compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md`](/Users/rosario/work/.cursor/plans/compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md) | [`/Users/rosario/.cursor/plans/compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md`](/Users/rosario/.cursor/plans/compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md) | [`/Users/rosario/work/comPASS/.cursor/plans/compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md`](/Users/rosario/work/comPASS/.cursor/plans/compass_phase3_track_o_generic_adapter_a7c3e91f.plan.md) | +| P3 Status | Phase 3 status handoff (Grok→Cursor) | [`/Users/rosario/work/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md`](/Users/rosario/work/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md) | [`/Users/rosario/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md`](/Users/rosario/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md) | [`/Users/rosario/work/comPASS/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md`](/Users/rosario/work/comPASS/.cursor/plans/compass_phase3_status_handoff_20260907.plan.md) | + +### Phase 3 progress notes (2026-09-07 PT) + +- **Handoff:** [`docs/CURSOR-HANDOFF.md`](docs/CURSOR-HANDOFF.md) — open first in Cursor after leaving Grok Bot. +- **Audit:** [`docs/AUDIT-GOALS-VS-BROWSER-STACK.md`](docs/AUDIT-GOALS-VS-BROWSER-STACK.md); stack map [`docs/WASMER-DEPLOYMENT.md`](docs/WASMER-DEPLOYMENT.md). +- Track O generic adapter **completed** (ADR 0006 / `src/compass/serve/adapter.py` + tests). +- ADRs **0005** (browser ENI6MA agent), **0007** (agy behind Gate) Accepted; agy-bridge + Compose on `:8791`. +- **PR #2** Docker `browser-client` challenge UI on `:8088` (handle / binary_url → digest-pin → minimal proof → Ask Gate) — merge + smoke still open. +- **Outstanding:** six-color ceremony UX, verify+burn ledger, full agent boot page, real comPREssOR hop inject, live-agy optional. + diff --git a/docker-compose.yml b/docker-compose.yml index 93362ca..c4a4796 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ -# comPASS local stack — default agy-bridge with fake-agy +# comPASS local stack — agy-bridge + browser challenge client # docker compose up --build -d +# Challenge UI: http://127.0.0.1:8088/ # See docs/DOCKER.md services: @@ -23,8 +24,24 @@ services: start_period: 10s restart: unless-stopped + browser-client: + build: + context: . + dockerfile: services/browser-client/Dockerfile + ports: + - "8088:80" + depends_on: + agy-bridge: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/healthz || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 5s + restart: unless-stopped + # Optional: docker compose --profile live-agy up --build -d - # Mount host agy + credentials (advanced). Not required for default up. agy-bridge-live: profiles: ["live-agy"] build: ./services/agy-bridge diff --git a/docs/CURSOR-HANDOFF.md b/docs/CURSOR-HANDOFF.md new file mode 100644 index 0000000..6f93a94 --- /dev/null +++ b/docs/CURSOR-HANDOFF.md @@ -0,0 +1,128 @@ +# Cursor handoff — comPASS (2026-09-07 PT) + +**Purpose:** Close out the Grok Bot (WASMER) session and give Cursor agents a single source of truth for **current status**, **what to open**, and **what to build next**. + +**Repo:** [`soltrinox/comPASS`](https://github.com/soltrinox/comPASS) +**Local tree:** `/Users/rosario/work/comPASS` +**Compressor sibling:** `soltrinox/comPREssOR` @ `main` (CC-1..CC-10) +**Open PR:** [#2 Docker browser challenge client](https://github.com/soltrinox/comPASS/pull/2) @ `5082367` (`feat/docker-browser-challenge`) + +**Workspace plan (open this in Cursor):** +[`.cursor/plans/compass_phase3_status_handoff_20260907.plan.md`](../.cursor/plans/compass_phase3_status_handoff_20260907.plan.md) + +Also mirrored under `/Users/rosario/work/.cursor/plans/` and `/Users/rosario/.cursor/plans/` when synced. + +--- + +## 1. Product posture (locked) + +| Decision | ADR / doc | +|---|---| +| Browser-only Wasmer appliance; no Cursor/IDE product path | [ADR 0005](adr/0005-eni6ma-gated-browser-agent.md), [ARCHITECTURE.md](ARCHITECTURE.md) | +| Generic LLM adapter: decide / catalog / proxy_override | [ADR 0006](adr/0006-generic-llm-adapter.md), [API.md](API.md) §6 | +| Google Antigravity (`agy`) stays **native** behind ENI6MA-gated Express bridge | [ADR 0007](adr/0007-agy-behind-eni6ma-gate.md) | +| Digest is trust root; deny-by-default egress; Probe owns keys | AUDIT + WASMER-DEPLOYMENT | + +Pass+ Vercel reference face (not copied wholesale): `https://circuit.eni6ma.com/passplus/?handle=ALICE` / `?binary_url=…` + +--- + +## 2. Phase completion + +| Phase | Status | +|---|---| +| **1** Offline A–E (specs, compressor CCs, Graph/Route, Wasmer cut, GTM ADRs) | **Complete** | +| **2** Test-ready F–N | **Complete** (lab/test-ready ≠ production) | +| **3** Browser agent + Gate + adapter + Docker client | **In progress** — scaffold + lab deploy; ceremony product incomplete | + +--- + +## 3. What works today (lab) + +### Docker Compose + +```bash +docker compose up --build -d +``` + +| Service | Port | Role | +|---|---|---| +| `agy-bridge` | **8791** | OpenAI-shaped Gate → fake-agy (or live-agy profile) | +| `browser-client` | **8088** | Challenge-first static SPA + `/circuit-proxy` + `/artifacts` | + +Smoke: + +- http://127.0.0.1:8088/ → redirects to `challenge.html` +- http://127.0.0.1:8088/challenge.html?handle=demo-wasm +- http://127.0.0.1:8791/healthz + +Demo circuit pin: + +- URL: `https://raw.githubusercontent.com/eni6ma/REGISTRY/feat/wasm-circuits/circuits/demo-wasm/v1/eni6ma_wasm.wasm` +- SHA-256: `853717e421a36fc93d0791d3f2718ecf3e9c449fb3c60d4084dedab3af75c389` +- Local mirror: `wasmer/artifacts/eni6ma/demo-wasm/v1/` + `wasmer/artifacts/pins.json` + +### Libraries / paths + +| Area | Path | Notes | +|---|---|---| +| Adapter | `src/compass/serve/adapter.py` | Track O done; tests `tests/test_generic_adapter.py` | +| Proxy | `src/compass/serve/proxy.py` | Delegates to adapter | +| Bridge allowlist | `wasmer/browser/bridge.js` | Deny-by-default hosts | +| Circuit load | `wasmer/browser/circuitLoader.js` | SHA-256 fail-closed; `loadPinnedRemote` + proxy | +| Minimal proof | `wasmer/browser/wasmerRunner.js` | DEMO-MINT `build_minimal_proof` | +| Challenge UI | `wasmer/browser/challenge.html` + `challenge.js` | Pass+-style handle / binary_url | +| Stub agent | `wasmer/browser/agent.html` | Older Path-B buttons | +| Gate | `services/agy-bridge/src/circuitGate.js` | Digest + abi_probe; prove-only ABI | +| Docs audit | `docs/AUDIT-GOALS-VS-BROWSER-STACK.md` | Goals → Done/Partial/Not | +| Deploy map | `docs/WASMER-DEPLOYMENT.md` | Zones A–D lifecycle | +| Docker ops | `docs/DOCKER.md` | Compose cheat sheet | + +### Readiness (rough) + +- Auth→route→LLM **engine** path: ~55–65% testable (compose Gate + adapter + pin) +- Ceremony / verify / burn / full agent boot: ~15–25% +- Grade: **lab/demo**, not production-deploy ready + +--- + +## 4. Not done (Cursor should prioritize) + +1. **Merge PR #2** and confirm compose smoke on a clean machine +2. **Six-color interactive ceremony** (challenge → colors → proof → burn-before-validate) — only Path-B / challenge form exists +3. **Real verify ABI + burn ledger** — Gate stub / DEMO-MINT prove-only +4. **Full Wasmer agent boot page** — ceremony → policy bind → triggers → loop → adapter +5. **Real comPREssOR hop inject** — adapter still has placeholder `default_compress_hook` +6. Live Probe keys / PyPI TestPyPI / mobile farm — still gated / NOT_RUN as in Phase 2 evidence +7. Optional: apply leftover `services/agy-bridge-gate-harden.tgz` if still uncommitted on disk + +--- + +## 5. How Cursor should work this repo + +- Prefer **`gh`** on the Mac (`soltrinox` keyring) for branch/PR; box/cloud may lack GitHub auth +- Cloud Agent may need **GitHub reconnect in Cursor** if launch fails +- Plans live in **three places** (keep bytes aligned when editing): + - `/Users/rosario/work/comPASS/.cursor/plans/` (repo) + - `/Users/rosario/work/.cursor/plans/` + - `/Users/rosario/.cursor/plans/` +- Index: [`PLANS.md`](../PLANS.md), [`.cursor/plans/README.md`](../.cursor/plans/README.md) +- Refuse edits under archived `CHAT-COMPRESSOR*` (ADR 0002/0003) + +--- + +## 6. Suggested first Cursor prompt + +> Open `docs/CURSOR-HANDOFF.md` and `.cursor/plans/compass_phase3_status_handoff_20260907.plan.md`. Merge or finish PR #2, smoke `docker compose` challenge UI on :8088 against agy-bridge :8791, then implement Track P: six-color ENI6MA ceremony UX bound to digest-pinned circuit + burn-before-validate, replacing the minimal-proof stub for product auth. + +--- + +## 7. Session trail (Grok Bot WASMER, Sep 6–7) + +- Rewrote architecture for browser-only + ENI6MA; ADRs 0005–0007 +- Implemented Track O generic adapter + tests +- Built agy-bridge Gate + Compose fake-agy +- Wrote WASMER-DEPLOYMENT + AUDIT-GOALS-VS-BROWSER-STACK +- Built Docker `browser-client` challenge UI (PR #2) via `gh` after Cloud Agent GitHub reconnect failed + +**End of Grok Bot handoff.** diff --git a/docs/DOCKER.md b/docs/DOCKER.md index ff54fd9..d05420f 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -1,6 +1,6 @@ -# Docker Compose — agy-bridge +# Docker Compose — agy-bridge + browser challenge client -Bring up the local **agy-bridge** (ENI6MA Gate → chat completions) without installing Google Antigravity. +Bring up the local **agy-bridge** (ENI6MA Gate → chat completions) and the **browser challenge client** (Pass+-style handle / binary URL → digest-pin → prove → ask). **Prerequisite:** Docker Desktop must be running (`docker info` succeeds). On macOS: `open -a Docker`, then wait until the daemon is ready. @@ -11,14 +11,21 @@ Bring up the local **agy-bridge** (ENI6MA Gate → chat completions) without ins docker compose up --build -d ``` -Health check: +| Surface | URL | +|---------|-----| +| **Challenge UI** | http://127.0.0.1:8088/ | +| agy-bridge health | http://127.0.0.1:8791/healthz | +| browser-client health | http://127.0.0.1:8088/healthz | -```bash -curl -s http://127.0.0.1:8791/healthz -# {"ok":true,"service":"agy-bridge",...} -``` +### Challenge entry (like Pass+ / `circuit.eni6ma.com/passplus`) + +- Handle: http://127.0.0.1:8088/challenge.html?handle=demo-wasm +- Binary URL: http://127.0.0.1:8088/challenge.html?binary_url=https://raw.githubusercontent.com/eni6ma/REGISTRY/feat/wasm-circuits/circuits/demo-wasm/v1/eni6ma_wasm.wasm&sha256=853717e421a36fc93d0791d3f2718ecf3e9c449fb3c60d4084dedab3af75c389 +- Or click **Use local pin** to load `wasmer/artifacts` digest without GitHub. -Sample completion (Gate DEV mode; circuit URL+sha256 for cache/fetch): +Flow: first paint is the challenge form → **Load & Prove** fetches via same-origin `/circuit-proxy` (allowlisted GitHub hosts) → SHA-256 fail-closed → DEMO-MINT `build_minimal_proof` in-tab → **Ask / execute** posts to `:8791` with `compass.circuit`. + +Sample completion (Gate DEV mode): ```bash curl -s http://127.0.0.1:8791/v1/chat/completions \ @@ -36,9 +43,7 @@ curl -s http://127.0.0.1:8791/v1/chat/completions \ }' ``` -Default compose uses `scripts/fake-agy.js` (`AGY_BIN=/app/scripts/fake-agy.js`) so no live `agy` install is needed. Bind is `0.0.0.0` inside the container (`AGY_BRIDGE_HOST`); host port `8791`. - -Circuit WASM cache persists in volume `agy-bridge-circuits` → `/data/circuits`. +Default compose uses `scripts/fake-agy.js` so no live `agy` install is needed. ## Stop @@ -48,11 +53,4 @@ docker compose down ## Live Antigravity (advanced) -Profile `live-agy` is documented in `docker-compose.yml`. It expects host networking and mounts for the real `agy` binary plus credentials. Default `docker compose up` does **not** require this. - -```bash -# after editing mounts in docker-compose.yml -docker compose --profile live-agy up --build -d -``` - -Local-only default without Docker still binds `127.0.0.1` (`AGY_BRIDGE_HOST` unset). +Profile `live-agy` is documented in `docker-compose.yml`. Default `docker compose up` does **not** require it. diff --git a/docs/README.md b/docs/README.md index 6a5ea2f..45faeb8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ Track A deliverables. Implementation is Tracks B–D. Product/GTM is Track E. ** | [`WASMER.md`](WASMER.md) | Wasmer artifacts/ABI + Phase 3 browser appliance notes | | [`WASMER-DEPLOYMENT.md`](WASMER-DEPLOYMENT.md) | **Phase 3 lifecycle:** zones A–D, Gate auth, adapter→bridge→agy, module maps, ports, operator cheat sheet | | [`AUDIT-GOALS-VS-BROWSER-STACK.md`](AUDIT-GOALS-VS-BROWSER-STACK.md) | **Goals vs build:** charter + Phase 3 goals mapped to Done/Partial/Not; browser stack tree + module audit | +| [`CURSOR-HANDOFF.md`](CURSOR-HANDOFF.md) | **Cursor handoff (2026-09-07):** Phase 3 status, compose ports, PR #2, next todos | | [`RELEASE.md`](RELEASE.md) | Track L: version scheme, tag policy, TestPyPI/PyPI publish (no secrets) | | [`abi/host-abi.v1.md`](abi/host-abi.v1.md) | Host ABI v1 (storage/clock/log/config; keys forbidden) | | [`INTEGRATION.md`](INTEGRATION.md) | CC-1–CC-10 touchpoints; ingestion; classification reuse; bundle pointer | diff --git a/services/browser-client/Dockerfile b/services/browser-client/Dockerfile new file mode 100644 index 0000000..aaab068 --- /dev/null +++ b/services/browser-client/Dockerfile @@ -0,0 +1,13 @@ +# Serve wasmer/browser + artifacts for the challenge-first appliance. +FROM nginx:1.27-alpine +COPY services/browser-client/nginx.conf /etc/nginx/nginx.conf +COPY wasmer/browser/ /usr/share/nginx/html/ +# challenge.html expects ../artifacts from browser/ → map artifacts at /artifacts and also sibling path +COPY wasmer/artifacts/ /usr/share/nginx/artifacts/ +# Mirror layout expected by relative ../artifacts from /challenge.html +RUN mkdir -p /usr/share/nginx/wasmer && \ + ln -sfn /usr/share/nginx/artifacts /usr/share/nginx/wasmer/artifacts && \ + # Pages live at /; relative ../artifacts from /challenge.html resolves to /artifacts — good. + true +EXPOSE 80 +HEALTHCHECK --interval=10s --timeout=3s --retries=5 CMD wget -qO- http://127.0.0.1/healthz || exit 1 diff --git a/services/browser-client/nginx.conf b/services/browser-client/nginx.conf new file mode 100644 index 0000000..0f74947 --- /dev/null +++ b/services/browser-client/nginx.conf @@ -0,0 +1,63 @@ +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; + +events { worker_connections 1024; } + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + server_tokens off; + resolver 8.8.8.8 1.1.1.1 valid=300s ipv6=off; + + map $arg_url $circuit_proxy_ok { + default 0; + "~*^https://raw\.githubusercontent\.com/" 1; + "~*^https://github\.com/" 1; + "~*^https://objects\.githubusercontent\.com/" 1; + } + + server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index challenge.html; + + add_header Cross-Origin-Opener-Policy same-origin always; + add_header Cross-Origin-Embedder-Policy require-corp always; + add_header Cross-Origin-Resource-Policy cross-origin always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' http://127.0.0.1:8791 http://localhost:8791 https://raw.githubusercontent.com https://github.com https://objects.githubusercontent.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'" always; + + location = / { + return 302 /challenge.html; + } + + location / { + try_files $uri $uri/ =404; + } + + location /artifacts/ { + alias /usr/share/nginx/artifacts/; + add_header Cross-Origin-Resource-Policy cross-origin always; + types { application/wasm wasm; } + } + + location = /circuit-proxy { + if ($circuit_proxy_ok = 0) { return 403; } + proxy_ssl_server_name on; + proxy_http_version 1.1; + set $upstream_url $arg_url; + proxy_pass $upstream_url; + proxy_hide_header Content-Security-Policy; + add_header Access-Control-Allow-Origin * always; + add_header Cross-Origin-Resource-Policy cross-origin always; + add_header Cache-Control "public, max-age=60"; + } + + location = /healthz { + default_type application/json; + return 200 '{"ok":true,"service":"browser-client"}'; + } + } +} diff --git a/wasmer/browser/README.challenge.md b/wasmer/browser/README.challenge.md new file mode 100644 index 0000000..1d61c3d --- /dev/null +++ b/wasmer/browser/README.challenge.md @@ -0,0 +1,10 @@ +# Challenge client (Pass+-style) + +Entry: `challenge.html` (Docker default `/`). + +- `?handle=demo-wasm` or `?binary_url=https://raw.githubusercontent.com/eni6ma/REGISTRY/.../eni6ma_wasm.wasm` +- Digest fail-closed via `circuitLoader.loadPinnedRemote` (+ `/circuit-proxy` for CORS) +- Minimal proof via DEMO-MINT wasm-bindgen pkg +- Ask panel → `agy-bridge` `:8791` with `compass.circuit` + +See `docs/DOCKER.md` in the PR. diff --git a/wasmer/browser/challenge.html b/wasmer/browser/challenge.html new file mode 100644 index 0000000..aab96f4 --- /dev/null +++ b/wasmer/browser/challenge.html @@ -0,0 +1,69 @@ + + + + + + comPASS · ENI6MA challenge + + + +
+

ENI6MA challenge

+

Pass+-style entry: load a circuit by handle or remote binary URL, digest-pin in the browser, prove, then ask the local Gate.

+ + + + + + + + + + +
+ + +
+

Query params: ?handle= · ?binary_url= · ?sha256=

+ +

idle

+

+
+    
+

Ask / execute

+

Unlocked after a successful digest + minimal proof. Posts to agy-bridge with compass.circuit.

+ + + + +
+ +
+

+    
+
+ + + + + diff --git a/wasmer/browser/challenge.js b/wasmer/browser/challenge.js new file mode 100644 index 0000000..8733ddf --- /dev/null +++ b/wasmer/browser/challenge.js @@ -0,0 +1,205 @@ +/* Challenge-first boot: handle | binary_url → digest-pin → prove → ask Gate. */ +const DEMO_WASM_URL = + "https://raw.githubusercontent.com/eni6ma/REGISTRY/feat/wasm-circuits/circuits/demo-wasm/v1/eni6ma_wasm.wasm"; +const DEMO_SHA256 = + "853717e421a36fc93d0791d3f2718ecf3e9c449fb3c60d4084dedab3af75c389"; + +/** Handle → raw GitHub REGISTRY URL (Pass+-style). Override with window.COMPASS_HANDLE_MAP. */ +const DEFAULT_HANDLE_MAP = { + "demo-wasm": DEMO_WASM_URL, + demowasm: DEMO_WASM_URL, + demo: DEMO_WASM_URL, + alice: + "https://raw.githubusercontent.com/eni6ma/REGISTRY/main/circuits/alice/v1/eni6ma", + bob: "https://raw.githubusercontent.com/eni6ma/REGISTRY/main/circuits/bob/v1/eni6ma", +}; + +const statusEl = document.getElementById("status"); +const outEl = document.getElementById("out"); +const askSection = document.getElementById("askSection"); +const askOut = document.getElementById("askOut"); +const btnAsk = document.getElementById("btnAsk"); + +/** @type {{ url: string, sha256: string, proof: any } | null} */ +let session = null; + +function setStatus(msg, kind) { + statusEl.textContent = msg; + statusEl.className = + kind === true ? "ok" : kind === false ? "err" : kind === "busy" ? "busy" : ""; +} + +function unlockAsk(ok) { + askSection.classList.toggle("locked", !ok); + btnAsk.disabled = !ok; +} + +function qs() { + return new URLSearchParams(location.search); +} + +function resolveHandle(handle) { + const h = String(handle || "").trim(); + if (!h) return null; + const map = Object.assign({}, DEFAULT_HANDLE_MAP, window.COMPASS_HANDLE_MAP || {}); + const key = h.toLowerCase(); + if (map[key]) return map[key]; + if (map[h]) return map[h]; + // Generic Path-B: REGISTRY main circuits//v1/eni6ma + const safe = encodeURIComponent(h.toLowerCase()); + return `https://raw.githubusercontent.com/eni6ma/REGISTRY/main/circuits/${safe}/v1/eni6ma`; +} + +function applyQueryDefaults() { + const p = qs(); + if (p.get("handle")) document.getElementById("handle").value = p.get("handle"); + if (p.get("binary_url")) + document.getElementById("binaryUrl").value = p.get("binary_url"); + if (p.get("sha256")) document.getElementById("sha256").value = p.get("sha256"); +} + +async function runProofFromPinned(pinned, label) { + const art = location.pathname.includes("/browser/") + ? new URL("../artifacts/", location.href) + : new URL("/artifacts/", location.origin); + const pkgBase = new URL("eni6ma/demo-wasm/v1/pkg/", art).href; + const mod = await import(pkgBase + "eni6ma_wasm.js"); + await mod.default(); + const challenge = JSON.stringify({ + timestamp: Date.now(), + matrix_data: { + rows: [{ values: [1, 2, 3], row_hash: "x", row_index: 0 }], + }, + }); + const bearings = JSON.stringify(["U", "L", "R", "U"]); + const proof = mod.build_minimal_proof(challenge, bearings); + return { + label, + path_b: { + expected: pinned.sha256, + actual: pinned.sha256, + published_bytes: pinned.bytes.byteLength, + url: pinned.url || pinned.remoteUrl || null, + }, + proof, + challenge: JSON.parse(challenge), + bearings: JSON.parse(bearings), + }; +} + +async function loadAndProve({ useLocalPin }) { + unlockAsk(false); + session = null; + outEl.textContent = ""; + setStatus("Loading circuit…", "busy"); + + try { + let pinned; + let sourceUrl; + let expected; + + if (useLocalPin) { + const pinsUrl = location.pathname.includes("/browser/") + ? new URL("../artifacts/pins.json", location.href).href + : new URL("/artifacts/pins.json", location.origin).href; + pinned = await CircuitLoader.loadPinnedById(pinsUrl, "eni6ma_demo_wasm_v1"); + sourceUrl = + pinned.pin && pinned.pin.source_ref + ? DEMO_WASM_URL + : new URL("../" + pinned.pin.path, location.href).href; + expected = pinned.sha256; + } else { + const handle = document.getElementById("handle").value; + const binaryUrl = document.getElementById("binaryUrl").value.trim(); + expected = document.getElementById("sha256").value.trim(); + sourceUrl = binaryUrl || resolveHandle(handle); + if (!sourceUrl) { + throw Object.assign(new Error("Enter a handle or binary_url"), { + code: "input_missing", + }); + } + if (!expected && sourceUrl === DEMO_WASM_URL) expected = DEMO_SHA256; + pinned = await CircuitLoader.loadPinnedRemote(sourceUrl, expected, { + useProxy: true, + }); + expected = pinned.sha256; + } + + setStatus("Digest OK — proving…", "busy"); + const result = await runProofFromPinned(pinned, useLocalPin ? "local-pin" : "remote"); + session = { + url: sourceUrl || DEMO_WASM_URL, + sha256: expected, + proof: result.proof, + }; + setStatus("Digest OK · proof OK — ask unlocked", true); + outEl.textContent = WasmerRunner.jsonSafe({ + mode: result.label, + path_b: result.path_b, + proof: result.proof, + }); + unlockAsk(true); + } catch (e) { + setStatus( + e.code === "digest_mismatch" ? "DIGEST MISMATCH — fail closed" : "Error", + false + ); + outEl.textContent = (e.stack || String(e)) + + (e.expected + ? "\n" + JSON.stringify({ expected: e.expected, actual: e.actual, bytes: e.bytes }, null, 2) + : ""); + unlockAsk(false); + } +} + +async function askBridge() { + if (!session) return; + askOut.textContent = ""; + setStatus("Sending to agy-bridge…", "busy"); + const base = document.getElementById("bridgeUrl").value.replace(/\/$/, ""); + const prompt = document.getElementById("prompt").value; + const body = { + model: "agy", + messages: [{ role: "user", content: prompt }], + compass: { + circuit: { + url: session.url, + sha256: session.sha256, + proof: session.proof, + }, + }, + }; + try { + const res = await fetch(base + "/v1/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify(body), + }); + const text = await res.text(); + let parsed; + try { + parsed = JSON.parse(text); + } catch (_) { + parsed = text; + } + askOut.textContent = typeof parsed === "string" ? parsed : JSON.stringify(parsed, null, 2); + setStatus(res.ok ? "Bridge response OK" : "Bridge HTTP " + res.status, res.ok); + } catch (e) { + setStatus("Bridge error (is compose up?)", false); + askOut.textContent = e.stack || String(e); + } +} + +document.getElementById("btnLoad").addEventListener("click", () => + loadAndProve({ useLocalPin: false }) +); +document.getElementById("btnLocal").addEventListener("click", () => + loadAndProve({ useLocalPin: true }) +); +document.getElementById("btnAsk").addEventListener("click", askBridge); + +applyQueryDefaults(); +const p = qs(); +if (p.get("handle") || p.get("binary_url") || p.get("autoload") === "1") { + loadAndProve({ useLocalPin: false }); +} diff --git a/wasmer/browser/circuitLoader.js b/wasmer/browser/circuitLoader.js index 7599851..2ae490c 100644 --- a/wasmer/browser/circuitLoader.js +++ b/wasmer/browser/circuitLoader.js @@ -55,15 +55,76 @@ err.code = "pin_missing"; throw err; } - const url = new URL("../" + pin.path, location.href).href; + // pin.path is relative to wasmer/ (e.g. artifacts/...). Resolve from pins.json URL parent/parent or /artifacts sibling. + let url; + try { + const pinsBase = new URL(pinsUrl, location.href); + // pins at .../artifacts/pins.json → artifact root is dirname + const artRoot = new URL("./", pinsBase); + const rel = String(pin.path).replace(/^artifacts\//, ""); + url = new URL(rel, artRoot).href; + } catch (_) { + url = new URL("../" + pin.path, location.href).href; + } const loaded = await loadPinned(url, pin.sha256); return Object.assign({ pinId: pinId, pin: pin, pinsDoc: pinsDoc }, loaded); } + async function fetchSidecarSha256(url) { + try { + const res = await fetch(url); + if (!res.ok) return null; + const text = (await res.text()).trim().split(/\s+/)[0].toLowerCase(); + return /^[0-9a-f]{64}$/.test(text) ? text : null; + } catch (_) { + return null; + } + } + + function viaProxy(url) { + try { + const u = new URL(url, location.href); + if (u.origin === location.origin) return u.href; + return new URL( + "/circuit-proxy?url=" + encodeURIComponent(u.href), + location.href + ).href; + } catch (_) { + return url; + } + } + + /** + * Load remote bytes with pin. Empty expectedSha256 → try .sha256 sidecar (proxied). + */ + async function loadPinnedRemote(url, expectedSha256, opts) { + opts = opts || {}; + const useProxy = opts.useProxy !== false; + let expected = String(expectedSha256 || "") + .trim() + .split(/\s+/)[0] + .toLowerCase(); + const fetchUrl = useProxy ? viaProxy(url) : url; + if (!/^[0-9a-f]{64}$/.test(expected)) { + const sideUrl = useProxy ? viaProxy(url.replace(/\?.*$/, "") + ".sha256") : url + ".sha256"; + expected = (await fetchSidecarSha256(sideUrl)) || ""; + } + if (!/^[0-9a-f]{64}$/.test(expected)) { + const err = new Error("circuitLoader: missing pin and no .sha256 sidecar"); + err.code = "pin_invalid"; + throw err; + } + const loaded = await loadPinned(fetchUrl, expected); + return Object.assign({ remoteUrl: url }, loaded); + } + const api = { sha256Hex: sha256Hex, loadPinned: loadPinned, loadPinnedById: loadPinnedById, + fetchSidecarSha256: fetchSidecarSha256, + viaProxy: viaProxy, + loadPinnedRemote: loadPinnedRemote, }; if (typeof module !== "undefined" && module.exports) module.exports = api; root.CircuitLoader = api;