From 9ac47707733af47df44727e2cc8505dfd0146bf9 Mon Sep 17 00:00:00 2001 From: archipelago Date: Mon, 3 Aug 2026 17:07:07 -0400 Subject: [PATCH] docs(13-09): retarget plan to in-repo aiui/ (D-19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substantive rework, not a path swap: - Retires D-15's pin-and-verify model. scripts/aiui.pin, pin_commit and --update-pin are dropped outright — there is no second repository left to pin, so build-aiui.sh now attributes a build to this repo's own `git rev-parse HEAD` instead. - Re-derives the build: aiui/ is an in-repo pnpm/turbo workspace with its own package.json and lockfile but no committed node_modules, so build-aiui.sh must `pnpm install --frozen-lockfile` before it can build (new requirement; the old model assumed a developer's separate AIUI clone was already installed). - Retargets deploy-to-target.sh (both its primary and --both/secondary AIUI sections) and setup-aiui-server.sh off the stale $PROJECT_DIR/../AIUI/packages/app/dist path, which still resolves on disk to a stale pre-migration clone and would otherwise silently ship old bytes instead of failing loudly. - Carries the /aiui/-scoped CSP sandbox work (AIUI-04) through unchanged per D-19, and fixes two acceptance-criteria drifts discovered while verifying the plan against deploy-to-target.sh's post-13-02 state and nginx-archipelago.conf's post-pentest-hardening state (CSP header count and the "no session gate needed" grep), neither of which is a D-19 effect. - Folds in a real defect found while doing this work: the 2026-07-31 same-host deploy guard only catches path containment, not sibling directories — the exact shape this worktree's own topology exhibits (archy-phase13 as a sibling of the main checkout, reachable over loopback SSH). New Task 3 widens it to refuse any same-host source/destination mismatch, extracted into a testable assert_safe_same_host_deploy in scripts/lib/common.sh and pinned by tests/production-quality/deploy-guard-same-host.sh. The checkpoint task is renumbered Task 3 -> Task 4 accordingly. Co-Authored-By: Claude Opus 5 (1M context) --- .../13-09-PLAN.md | 284 ++++++++++++++---- 1 file changed, 225 insertions(+), 59 deletions(-) diff --git a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-PLAN.md b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-PLAN.md index 3be4867b..35186f0a 100644 --- a/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-PLAN.md +++ b/.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-PLAN.md @@ -7,8 +7,10 @@ depends_on: ["13-02"] files_modified: - scripts/build-aiui.sh - scripts/verify-aiui-deploy.sh - - scripts/aiui.pin - scripts/deploy-to-target.sh + - scripts/setup-aiui-server.sh + - scripts/lib/common.sh + - tests/production-quality/deploy-guard-same-host.sh - image-recipe/configs/nginx-archipelago.conf - neode-ui/src/views/Chat.vue autonomous: false @@ -17,25 +19,34 @@ requirements: [AIUI-04, AIUI-05] must_haves: truths: - "An operator receives AIUI updates through a build and deploy path that fails loudly rather than shipping a black page (AIUI-05, D-15)" - - "The AIUI commit shipped by a given Archy build is pinned in this repo and recorded in the deployed artifact, so 'which AIUI is on this node' is answerable (D-15)" + - "The AIUI bytes a node runs are attributable to this repo's own commit — no separate pin file and no second checkout to go stale, because D-19 made AIUI's commit this repo's own commit (D-15's delivery half survives; its pinning half is retired)" - "`VITE_BASE_PATH=/aiui/` is enforced by the build script, not remembered — the script exits non-zero when it is unset or wrong (D-15)" - "The post-deploy check fetches a live asset over HTTP resolved through sw.js, never trusting a directory listing — the node's assets/ is a never-pruned graveyard that reports 'deployed' before the deploy" - "AIUI's own JavaScript is browser-prevented from reaching /rpc/v1 with the ambient session cookie — the sandbox is an enforced boundary, not only a code-discipline convention (AIUI-04, RESEARCH Open Question 2)" - "AIUI keeps its standalone mode and its own fast dev loop — none of this requires a node to work on the UI (D-17)" + - "A same-host deploy whose resolved source and destination differ is refused before rsync --delete can run, whether the mismatch is containment or sibling directories — the 2026-07-31 data-loss guard now covers the shape it originally missed" artifacts: - path: "scripts/build-aiui.sh" - provides: "The one way AIUI is built for a node: base-path enforced, commit pinned, output verified" + provides: "The one way AIUI is built for a node: base-path enforced, deps installed from the committed lockfile, output verified and attributed to this repo's own commit" contains: "VITE_BASE_PATH" - path: "scripts/verify-aiui-deploy.sh" provides: "Post-deploy live-asset fetch check resolved via sw.js" contains: "sw.js" - - path: "scripts/aiui.pin" - provides: "The AIUI commit + branch this repo ships" + - path: "scripts/lib/common.sh" + provides: "assert_safe_same_host_deploy — the shared, unit-testable same-host guard deploy-to-target.sh calls" + contains: "assert_safe_same_host_deploy" + - path: "tests/production-quality/deploy-guard-same-host.sh" + provides: "Regression pin for the sibling-directory data-loss gap: identical/contained/containing/sibling/unrelated path fixtures against the guard function" + contains: "assert_safe_same_host_deploy" key_links: - from: "scripts/deploy-to-target.sh" to: "scripts/build-aiui.sh" via: "the deploy path calls the build script instead of inlining a pnpm build with a remembered env var" pattern: "build-aiui\\.sh" + - from: "scripts/deploy-to-target.sh" + to: "scripts/lib/common.sh" + via: "the same-host guard is a shared, testable function instead of two inline containment-only case blocks" + pattern: "assert_safe_same_host_deploy" - from: "image-recipe/configs/nginx-archipelago.conf" to: "neode-ui/src/views/Chat.vue" via: "a /aiui/-scoped Content-Security-Policy connect-src that the iframe document cannot widen" @@ -43,20 +54,36 @@ must_haves: --- -Two things that are currently held together by memory rather than by machinery. +Three things: two carried over unchanged (delivery, sandbox), and one folded in as a directly +relevant defect surfaced while re-deriving the delivery half for D-19. -**Delivery (AIUI-05, D-15).** AIUI is a `*-ui` app outside the signed catalog; it reaches nodes -on the frontend rsync, which is how the `/assets` 404 happened. D-15 keeps the rsync path -because it is the one that works, but makes it deliberate: AIUI's commit pinned in this repo, -`VITE_BASE_PATH=/aiui/` enforced by the build script rather than remembered, and a post-deploy -check that **fetches a live asset** instead of trusting a directory listing. Today -`deploy-to-target.sh` inlines the base path at line 716 and `setup-aiui-server.sh` documents it -in a comment — both are the "remembered" form D-15 rejects. Making AIUI a signed-catalog app was -considered and rejected for this phase. +**Delivery (AIUI-05, D-15 as amended by D-19).** AIUI is a `*-ui` app outside the signed catalog; +it reaches nodes on the frontend rsync, which is how the `/assets` 404 happened. D-15 keeps the +rsync path because it is the one that works. **D-19 (2026-08-03) changed what "deliberate" means +for it**: AIUI is no longer a second repository at `git.tx1138.com/lfg2025/AIUI` — it was migrated +in-repo to `aiui/` via `git subtree`, full history intact. **D-15's pinning half is retired**: +`scripts/aiui.pin` made sense when "which AIUI is on this node" meant tracking a second +repository's HEAD; now the answer is simply this repo's own commit, so there is no separate pin +file, no `--update-pin` flag, and no dirty-second-tree refusal. **D-15's delivery half still +stands** and is what this plan still builds: `VITE_BASE_PATH=/aiui/` enforced by the build script +rather than remembered, and a post-deploy check that **fetches a live asset** instead of trusting +a directory listing. Both `deploy-to-target.sh` and `scripts/setup-aiui-server.sh` still point at +the old `$PROJECT_DIR/../AIUI/packages/app/dist` sibling-checkout path — a directory that, on +this machine, happens to still physically exist at its last pre-migration commit, which would let +the old code silently ship stale bytes instead of failing. Both get retargeted to the in-repo +`$PROJECT_DIR/aiui/packages/app/dist` and rewired to call `scripts/build-aiui.sh` — AIUI builds +from its own `pnpm`/`turbo` workspace under `aiui/`, which this repo has no root `package.json` +to collide with (D-19; D-17's standalone mode is unaffected). A fresh checkout of this repo has +no `aiui/node_modules` — unlike the old world, where a developer's separate AIUI clone was +assumed already `pnpm install`ed as part of their normal AIUI workflow — so `build-aiui.sh` must +install from the committed lockfile before it can build; that is new, not carried over. Making +AIUI a signed-catalog app was considered and rejected for this phase: `*-ui` apps are outside the +catalog by design today, and changing that platform rule mid-phase is its own work. -**The sandbox (AIUI-04, RESEARCH Open Question 2).** Verified: the AIUI iframe in `Chat.vue` -has no `sandbox` attribute, is served same-origin under `/aiui/`, and the site CSP does not -restrict same-origin fetches. So "AIUI never gets an RPC session" is a **code-discipline +**The sandbox (AIUI-04, RESEARCH Open Question 2) — unaffected by the migration** (D-19: "a +build-time/runtime property, not a repository-location property"). Verified: the AIUI iframe in +`Chat.vue` has no `sandbox` attribute, is served same-origin under `/aiui/`, and the site CSP does +not restrict same-origin fetches. So "AIUI never gets an RPC session" is a **code-discipline convention today, not an enforced boundary** — AIUI's own JavaScript, running in the operator's authenticated session, is not browser-prevented from calling `/rpc/v1` directly. D-11's whole premise assumes the postMessage channel is the only channel. This plan makes that true, and the @@ -70,8 +97,21 @@ pattern, while dropping `allow-same-origin` moves AIUI to an opaque origin and b storage, its cookies and its origin-checked bridge — a change of a different size than this phase budgeted. That rejection is recorded here rather than left implicit. -Output: `scripts/build-aiui.sh`, `scripts/verify-aiui-deploy.sh`, `scripts/aiui.pin`, a -`/aiui/`-scoped CSP, and the deploy path rewired to use them. +**Folded in: widen the same-host deploy guard — a real gap found while verifying this plan +against the file's current state, not a D-19 effect.** 13-02 already rewrote large parts of +`deploy-to-target.sh` (see its SUMMARY); reading its *current* state for this retarget surfaced +that the 2026-07-31 data-loss guard only refuses a same-host deploy when one resolved path +*contains* the other. A **sibling** directory on the same host — for example this very worktree, +`archy-phase13`, deploying onto `$TARGET_DIR`'s resolved symlink target +(`/home/archipelago/Projects/archy`, the main checkout) — is neither contained by nor containing +of the destination, so the existing guard lets it through, and `rsync --delete` would mirror the +sibling onto the main checkout and delete everything the sibling lacks: the same incident class +the guard exists to prevent, through the one shape it does not cover. This plan widens it from +containment-only to any resolved-path mismatch. + +Output: `scripts/build-aiui.sh`, `scripts/verify-aiui-deploy.sh`, a `/aiui/`-scoped CSP, the +deploy path (`deploy-to-target.sh` and `setup-aiui-server.sh`) retargeted to the in-repo `aiui/` +location, and the same-host deploy guard widened and pinned by a regression test. @@ -93,14 +133,24 @@ is required, it needs an `update.rs` change this phase has not scoped. Symbols created by **this plan**: -- New file `scripts/build-aiui.sh`: `require_base_path`, `pin_commit`, `verify_dist` +- New file `scripts/build-aiui.sh`: `require_base_path`, `verify_dist` (no `pin_commit`, no + `--update-pin` — D-19 retires the pin; `verify_dist` now attributes the build to this repo's + own `git rev-parse HEAD` instead of a second repo's pinned SHA) - New file `scripts/verify-aiui-deploy.sh`: `resolve_live_chunks`, `fetch_and_grep` -- New file `scripts/aiui.pin` (data: branch + commit SHA) +- New function in `scripts/lib/common.sh`: `assert_safe_same_host_deploy(local_src, remote_dst)` + — pure, no SSH inside it, callable directly from a test with fixed inputs +- New file `tests/production-quality/deploy-guard-same-host.sh`: the fixture matrix over + `assert_safe_same_host_deploy` (identical / contained / containing / sibling / unrelated) - `image-recipe/configs/nginx-archipelago.conf`: a `Content-Security-Policy` header on the `location /aiui/` blocks (both server blocks) - `neode-ui/src/views/Chat.vue`: a `referrerpolicy` attribute and an explanatory comment on the iframe recording why `sandbox` is absent -- `scripts/deploy-to-target.sh`: call sites for the two new scripts, replacing the inline build +- `scripts/deploy-to-target.sh`: call sites for the two build/verify scripts (replacing the + inline build, in both the primary AIUI deploy section and the `--both`-secondary section), + retargeted from `$PROJECT_DIR/../AIUI` to `$PROJECT_DIR/aiui`, and its same-host guard now + calling `assert_safe_same_host_deploy` instead of two inline containment-only `case` blocks +- `scripts/setup-aiui-server.sh`: `AIUI_DIST` retargeted to the in-repo path; calls + `scripts/build-aiui.sh` when the dist is missing or stale instead of printing a manual command @@ -123,11 +173,20 @@ Symbols created by **this plan**: Task 1: Make the sandbox an enforced boundary, and say exactly what it enforces image-recipe/configs/nginx-archipelago.conf, neode-ui/src/views/Chat.vue -- `image-recipe/configs/nginx-archipelago.conf` lines 36-48 and 955-962 — **both** `location /aiui/` blocks, and the existing site-wide CSP wherever it is set. A change to one block only leaves the boundary open on whichever block serves the request. -- `neode-ui/src/views/Chat.vue` lines 33-42 — the iframe element: `:src="aiuiUrl"`, `allow="microphone"`, no `sandbox`. +- `image-recipe/configs/nginx-archipelago.conf` — **both** `location /aiui/` blocks (currently + ~line 38 in the HTTP server block, ~line 959 in the HTTPS block; search `location /aiui/ {` + rather than trusting these numbers — they have already drifted once, from 36-48/955-962 to + here, because 13-02 rewrote large parts of this file). A change to one block only leaves the + boundary open on whichever block serves the request. +- The **site-wide** `add_header Content-Security-Policy "default-src 'self'; ... connect-src + 'self' ws: wss: http://$host:* https:; ..."` already present in both server blocks (from the + 22-item pentest hardening pass, commit `6656d2f1` — this predates this plan's own creation + commit and is not a D-19/migration effect, but it means the file already carries 2 + `add_header Content-Security-Policy` lines before this task adds its own). +- `neode-ui/src/views/Chat.vue` — the iframe element around lines 33-42: `:src="aiuiUrl"`, `allow="microphone"`, no `sandbox`. - `.planning/phases/13-.../13-RESEARCH.md` Pitfall 2 ("Assuming the iframe boundary is a hard sandbox") in full, and Open Question 2. - `.planning/phases/13-.../13-AI-SPEC.md` §6 "Residual risks" — the first row is exactly this, and names G-B3 as the compensating control. -- `/home/archipelago/Projects/AIUI/packages/app/src/services/archyBridge.ts` — what AIUI actually needs to reach at runtime when embedded, so the policy does not break it. +- `aiui/packages/app/src/services/archyBridge.ts` — what AIUI actually needs to reach at runtime when embedded, so the policy does not break it. Add a `Content-Security-Policy` response header to **both** `location /aiui/` blocks. Its `connect-src` directive permits `'self'`-equivalent access only under the AIUI path prefix, built from nginx's `$scheme` and `$host` variables so it stays correct across http/https, LAN IP, hostname, Tailscale and onion access. Include `blob:` and `data:` where AIUI's runtime needs them, keep `script-src`/`style-src`/`img-src`/`font-src`/`media-src` permissive enough that the existing bundle still runs, and set `frame-ancestors` to the node's own origin so the AIUI document cannot itself be framed by a third party. The load-bearing directive is `connect-src`: it must not include a source expression that resolves to `/rpc/v1`. @@ -150,66 +209,169 @@ risk named in `13-AI-SPEC.md` §6. correct outcome is to record the residual risk explicitly rather than to relax the check. - grep -c 'Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf | grep -qvx 0 + test "$(grep -c 'add_header Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf)" -ge 4 cd neode-ui && npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts && npx vue-tsc --noEmit - grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf | grep -qx 0 + test "$(grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf)" -eq 1 -- `grep -c 'Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf` returns 2 — one per server block -- The CSP's `connect-src` value contains the AIUI path prefix and does not contain a bare `'self'` — verify by reading the directive +- `grep -c 'add_header Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf` returns 4 — the 2 pre-existing site-wide headers (commit `6656d2f1`, unrelated to this plan) plus the 2 new `/aiui/`-scoped ones this task adds, one per server block +- The new `/aiui/`-scoped CSP's `connect-src` value contains the AIUI path prefix and does not contain a bare `'self'` — verify by reading the directive - `grep -q 'referrerpolicy' neode-ui/src/views/Chat.vue` - `grep -ci 'sandbox=' neode-ui/src/views/Chat.vue` returns 0, and the comment explaining why is present -- `grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf` returns 0 +- `grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf` returns exactly 1 — the pre-existing 13-02 citation of the old, discredited comment (already correctly framed as a past reasoning error), not a second bare instance introduced by this task's own new comment - `cd neode-ui && npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts` exits 0 -- On a deployed node, `fetch('/rpc/v1', {method:'POST'})` executed from the AIUI frame's console is blocked by CSP and logs a violation; the same fetch from the top-level neode-ui console succeeds (recorded in Task 3) +- On a deployed node, `fetch('/rpc/v1', {method:'POST'})` executed from the AIUI frame's console is blocked by CSP and logs a violation; the same fetch from the top-level neode-ui console succeeds (recorded in Task 4) This is the enforcement mechanism AIUI-04's "sandboxed by construction" claim rests on. A CSP header is a config change and reverting is trivial, but the *claim* it supports is load-bearing for D-11's threat model — weakening it later silently invalidates the phase's security story rather than just its config. Flagged, not gated. AIUI's document carries a policy that browser-prevents a direct RPC fetch, both nginx server blocks carry it, and the iframe records why `sandbox` is absent rather than implying it is present. - Task 2: One way to build AIUI, and it refuses to build it wrong - scripts/build-aiui.sh, scripts/aiui.pin, scripts/deploy-to-target.sh + Task 2: One way to build AIUI from its new in-repo home, and it refuses to build it wrong + scripts/build-aiui.sh, scripts/deploy-to-target.sh, scripts/setup-aiui-server.sh -- `scripts/deploy-to-target.sh` lines 703-735 — the current AIUI build and rsync section, including the inline `VITE_BASE_PATH=/aiui/ pnpm build` at 716 and the `demo/aiui/` fallback at 721-723. Note that 13-02 already removed the proxy machinery from this file; read the current state, not the pre-13-02 state. -- `scripts/setup-aiui-server.sh` lines 17 and 47 — the base-path requirement stated as a comment, which is the "remembered" form D-15 rejects. +- `scripts/deploy-to-target.sh` — search for `Build and deploy AIUI` for the primary section + (currently ~line 708-737: `AIUI_DIR="$PROJECT_DIR/../AIUI"`, the inline + `VITE_BASE_PATH=/aiui/ pnpm build`, and the `demo/aiui/` fallback), and search for + `Deploy AIUI — prefer a sibling dist` for the secondary/`--both` section (currently + ~line 369-387: `AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist"` with a fallback to + streaming from .228). **Both** reference the old out-of-repo sibling checkout and both need + retargeting; 13-02 already removed the proxy machinery from this file (see its SUMMARY) — read + the file's current state, not the pre-13-02 or pre-migration version described in either plan. +- `scripts/setup-aiui-server.sh` in full (87 lines) — its header comment already documents 13-02's + changes; `AIUI_DIST="$PROJECT_DIR/../AIUI/packages/app/dist"` and the "Build it first: cd + ../AIUI/packages/app && ..." error message both need retargeting to the in-repo location. - `CLAUDE.md` — "Frontend: `neode-ui/` → `npm run build` outputs to `web/dist/neode-ui/`. **Grep the built bundle for new strings before shipping** — the build can silently no-op." The same rule applies to AIUI's dist and is what this script automates. -- `/home/archipelago/Projects/AIUI/packages/app/package.json` — the real scripts: `build` is `vue-tsc --noEmit && vite build`; the workspace runs under `pnpm`/`turbo`. +- `aiui/packages/app/package.json` — the real scripts: `build` is `vue-tsc --noEmit && vite build`; confirmed unchanged by the migration (subtree import preserved the file byte-for-byte). +- `aiui/pnpm-workspace.yaml`, `aiui/package.json`, `aiui/pnpm-lock.yaml` — this is a real pnpm/turbo + workspace with its own lockfile, in-repo, and **no `node_modules` is committed** (confirmed + absent in a fresh checkout of this worktree). Unlike the old world, where a developer's + separate AIUI clone was assumed already `pnpm install`ed, `build-aiui.sh` must install from + this lockfile before it can build — this is new, not carried over from the pre-migration plan. -Create `scripts/build-aiui.sh`, the single supported way to build AIUI for a node. +Create `scripts/build-aiui.sh`, the single supported way to build AIUI for a node, operating on +the in-repo `aiui/` directory (D-19 — there is no second repository to check out or pin). -`require_base_path` exits non-zero with a plain-language message when `VITE_BASE_PATH` is unset or is not exactly the AIUI mount path. The script sets it itself for the normal case; the check exists so an operator overriding it with a wrong value fails loudly instead of shipping a black page. D-15's point is that the requirement is enforced, not documented. +`require_base_path` exits non-zero with a plain-language message when `VITE_BASE_PATH` is unset +or is not exactly the AIUI mount path. The script sets it itself for the normal case; the check +exists so an operator overriding it with a wrong value fails loudly instead of shipping a black +page. D-15's point is that the requirement is enforced, not documented — that half of D-15 +survives the migration unchanged. -`pin_commit` reads `scripts/aiui.pin` (a two-line file: branch, then commit SHA), checks out that commit in the AIUI working tree, and refuses to proceed if the tree is dirty — a build from an uncommitted AIUI tree cannot be reproduced or attributed. Add a `--update-pin` flag that rewrites the pin from the AIUI tree's current HEAD, so bumping the pin is a deliberate, committed act in this repo. Create `scripts/aiui.pin` with AIUI's `development` branch and its current HEAD. +Before building, run `pnpm install --frozen-lockfile` at the `aiui/` workspace root (fast no-op +when already satisfied; hard failure, not a silent `pnpm install` fallback, when the lockfile and +`package.json` disagree — that disagreement is exactly the kind of unreviewed drift D-15 exists +to catch, and it should fail the build rather than quietly resolve it). **Do not add `pin_commit`, +`--update-pin`, or any dirty-tree refusal** — D-19 retires that mechanism outright: there is no +second working tree to check for dirtiness, and this repo's own ordinary commit discipline +(`CLAUDE.md`) is what keeps its history honest, not a second-repo-specific check. -The build runs AIUI's real command (`vue-tsc --noEmit && vite build`) so a type error fails the build rather than producing a stale `dist`. +The build runs AIUI's real command (`vue-tsc --noEmit && vite build`, invoked from +`aiui/packages/app`) so a type error fails the build rather than producing a stale `dist`. -`verify_dist` then asserts, before anything is copied anywhere: `dist/index.html` exists; every `