docs(13-09): retarget plan to in-repo aiui/ (D-19)
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6989b1d387
commit
9ac4770773
+225
-59
@@ -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:
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<flagged_assumptions>
|
||||
@@ -93,14 +133,24 @@ is required, it needs an `update.rs` change this phase has not scoped.
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
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
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<execution_context>
|
||||
@@ -123,11 +173,20 @@ Symbols created by **this plan**:
|
||||
<name>Task 1: Make the sandbox an enforced boundary, and say exactly what it enforces</name>
|
||||
<files>image-recipe/configs/nginx-archipelago.conf, neode-ui/src/views/Chat.vue</files>
|
||||
<read_first>
|
||||
- `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.
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c 'Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf | grep -qvx 0</automated>
|
||||
<automated>test "$(grep -c 'add_header Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf)" -ge 4</automated>
|
||||
<automated>cd neode-ui && npx vitest run src/views/__tests__/chatAiuiEmbed.test.ts && npx vue-tsc --noEmit</automated>
|
||||
<automated>grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf | grep -qx 0</automated>
|
||||
<automated>test "$(grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf)" -eq 1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `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)
|
||||
</acceptance_criteria>
|
||||
<reversibility rating="costly">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.</reversibility>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: One way to build AIUI, and it refuses to build it wrong</name>
|
||||
<files>scripts/build-aiui.sh, scripts/aiui.pin, scripts/deploy-to-target.sh</files>
|
||||
<name>Task 2: One way to build AIUI from its new in-repo home, and it refuses to build it wrong</name>
|
||||
<files>scripts/build-aiui.sh, scripts/deploy-to-target.sh, scripts/setup-aiui-server.sh</files>
|
||||
<read_first>
|
||||
- `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.
|
||||
</read_first>
|
||||
<action>
|
||||
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 `<script>`/`<link>` href in it begins with the AIUI mount path (a hand-built bundle with the wrong base path gives a black page, and the router base is what actually breaks, not the assets); the built asset filenames differ from the previous build when the source changed; and the pinned commit SHA appears somewhere in the emitted output so a deployed node can be attributed. Emit the SHA as a build-time define or a small `dist/BUILD-INFO` file, whichever is simpler in this build.
|
||||
`verify_dist` then asserts, before anything is copied anywhere: `dist/index.html` exists; every
|
||||
`<script>`/`<link>` href in it begins with the AIUI mount path (a hand-built bundle with the wrong
|
||||
base path gives a black page, and the router base is what actually breaks, not the assets); the
|
||||
built asset filenames differ from the previous build when the source changed; and **this repo's
|
||||
own current commit** (`git -C "$PROJECT_DIR" rev-parse HEAD`) appears somewhere in the emitted
|
||||
output, so a deployed node is attributable to a commit of this repo — not to a second repo's
|
||||
pinned SHA, since D-19 made those the same thing. Emit the SHA as a build-time define or a small
|
||||
`dist/BUILD-INFO` file, whichever is simpler in this build.
|
||||
|
||||
Rewire `scripts/deploy-to-target.sh` to call `scripts/build-aiui.sh` instead of building inline, and to call `scripts/verify-aiui-deploy.sh` after the copy. Keep the existing `demo/aiui/` fallback path but make it print a loud warning naming that it is shipping a checked-in dist rather than a fresh build, so that path stops being silent.
|
||||
Rewire **both** AIUI sections of `scripts/deploy-to-target.sh`. In the primary section, replace
|
||||
the inline `AIUI_DIR`/`pnpm build` with a call to `scripts/build-aiui.sh`, retarget `AIUI_DIST` to
|
||||
`$PROJECT_DIR/aiui/packages/app/dist`, and call `scripts/verify-aiui-deploy.sh` after the copy.
|
||||
Keep the existing `demo/aiui/` fallback path but make it print a loud warning naming that it is
|
||||
shipping a checked-in dist rather than a fresh build, so that path stops being silent. In the
|
||||
secondary/`--both` section, retarget `AIUI_DIST` to `$PROJECT_DIR/aiui/packages/app/dist` and
|
||||
update its explanatory comment — it no longer talks about "a machine that doesn't have an
|
||||
../AIUI checkout" (impossible now; `aiui/` is part of this repo), just "a machine that hasn't run
|
||||
`scripts/build-aiui.sh` yet" — and leave its check-then-fall-back-to-.228 behavior otherwise
|
||||
unchanged; this task retargets the path, it does not change that section's design.
|
||||
|
||||
Also update `scripts/setup-aiui-server.sh`'s comments to point at `build-aiui.sh` rather than restating the env var.
|
||||
Update `scripts/setup-aiui-server.sh`: retarget `AIUI_DIST` to
|
||||
`$PROJECT_DIR/aiui/packages/app/dist`, and when it's missing or older than AIUI's source, **call
|
||||
`scripts/build-aiui.sh`** instead of printing a manual `cd ../AIUI/packages/app && ...` command
|
||||
and exiting — D-15's "enforced, not remembered" applies here too, and a script that only prints
|
||||
instructions is the remembered form. Update the header comment's "Prerequisites" line to match.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bash -n scripts/build-aiui.sh && bash -n scripts/deploy-to-target.sh</automated>
|
||||
<automated>bash -n scripts/build-aiui.sh && bash -n scripts/deploy-to-target.sh && bash -n scripts/setup-aiui-server.sh</automated>
|
||||
<automated>VITE_BASE_PATH=/wrong/ bash scripts/build-aiui.sh; test $? -ne 0</automated>
|
||||
<automated>bash scripts/build-aiui.sh && grep -c 'src="/aiui/' /home/archipelago/Projects/AIUI/packages/app/dist/index.html | grep -qvx 0</automated>
|
||||
<automated>bash scripts/build-aiui.sh && grep -c 'src="/aiui/' aiui/packages/app/dist/index.html | grep -qvx 0</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `bash -n scripts/build-aiui.sh` exits 0 and the file is executable
|
||||
- `VITE_BASE_PATH=/wrong/ bash scripts/build-aiui.sh` exits non-zero with a message naming the required value
|
||||
- `scripts/aiui.pin` exists and contains the branch name and a 40-character commit SHA
|
||||
- Running the script with a dirty AIUI tree exits non-zero
|
||||
- After a successful run, every `src=`/`href=` in `/home/archipelago/Projects/AIUI/packages/app/dist/index.html` starts with the AIUI mount path — `grep -cE '(src|href)="/(?!aiui/)' dist/index.html` finds no non-AIUI-prefixed local asset
|
||||
- The pinned SHA is discoverable in the built output (`grep -rq "<pinned-sha>" dist/`)
|
||||
- After a successful run, every `src=`/`href=` in `aiui/packages/app/dist/index.html` starts with the AIUI mount path — `grep -cE '(src|href)="/(?!aiui/)' dist/index.html` finds no non-AIUI-prefixed local asset
|
||||
- This repo's own current commit SHA is discoverable in the built output (`grep -rq "$(git rev-parse HEAD)" aiui/packages/app/dist/`)
|
||||
- `grep -c 'build-aiui.sh' scripts/deploy-to-target.sh` returns ≥ 1 and `grep -c 'VITE_BASE_PATH=/aiui/ pnpm build' scripts/deploy-to-target.sh` returns 0 — the inline build is gone
|
||||
- `grep -c '\.\./AIUI' scripts/deploy-to-target.sh scripts/setup-aiui-server.sh` returns 0 across both files — no reference to the retired out-of-repo checkout survives in either script this task touches
|
||||
- `grep -c 'build-aiui.sh' scripts/setup-aiui-server.sh` returns ≥ 1 — it calls the one true build script instead of printing a manual command
|
||||
- `grep -cE 'aiui\.pin|pin_commit|update-pin' scripts/build-aiui.sh` returns 0 — the retired pinning mechanism is not present in any form
|
||||
</acceptance_criteria>
|
||||
<done>A wrong base path, a dirty AIUI tree, or a type error each fail the build loudly; a successful build is attributable to a pinned commit recorded in this repo.</done>
|
||||
<done>A wrong base path or a type error fails the build loudly; a successful build is attributable to this repo's own commit (no separate pin file, no second-repo dirty check — D-19 retired both); `deploy-to-target.sh` (both its primary and secondary AIUI sections) and `setup-aiui-server.sh` both build and deploy AIUI from its in-repo location.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Widen the same-host deploy guard from containment-only to any resolved-path mismatch</name>
|
||||
<files>scripts/lib/common.sh, scripts/deploy-to-target.sh, tests/production-quality/deploy-guard-same-host.sh</files>
|
||||
<behavior>
|
||||
- Identical resolved source and destination on the same host: allowed (exit 0) — the normal in-place deploy from the main checkout onto its own symlinked destination.
|
||||
- Source path is inside (a subdirectory of) the destination path: refused (non-zero) — the original 2026-07-31 containment case.
|
||||
- Destination path is inside the source path: refused (non-zero) — the mirror-image containment case.
|
||||
- Sibling directories that share a parent but neither contains the other — for example `/home/archipelago/Projects/archy-phase13` as source and `/home/archipelago/Projects/archy` (this worktree's own resolved `$TARGET_DIR` symlink target) as destination — are refused (non-zero). This is the gap: the pre-existing guard's two `case` patterns match only containment and let this shape through.
|
||||
- Two completely unrelated same-host paths with no shared parent at all are refused (non-zero) — same-host plus any mismatch is refused, not just the two containment shapes.
|
||||
- A refused case prints a message naming both resolved paths and pointing at the 2026-07-31 incident, so a future operator understands why rather than reflexively retrying with a force flag.
|
||||
</behavior>
|
||||
<read_first>
|
||||
- `scripts/deploy-to-target.sh` — search for `GUARD (2026-07-31 incident)` for the current guard: it computes `_LOCAL_SRC` and, only when the remote machine-id matches the local one, `_REMOTE_DST`, then refuses only when one `case` pattern matches the other path as a prefix. Read the full existing comment; it already documents the 2026-07-31 incident this task closes a gap in.
|
||||
- `scripts/lib/common.sh` in full — the double-source guard (`_ARCHY_COMMON_LOADED`) and the existing function shapes (`ssh_cmd`, `scp_cmd`, the `log_*` helpers), so the new function matches this file's style and naming rather than inventing a new convention.
|
||||
- This very worktree's own topology: `archy-phase13`, whose `$TARGET_DIR` resolves via a symlink to the main checkout at `/home/archipelago/Projects/archy`. The sibling-directory shape this task fixes is not hypothetical — it is this session's own layout, and is exactly what the deploy guard would need to catch if this worktree's `deploy-to-target.sh` were ever run.
|
||||
</read_first>
|
||||
<action>
|
||||
Add `assert_safe_same_host_deploy(local_src, remote_dst)` to `scripts/lib/common.sh`. It takes two
|
||||
**already-resolved** (`readlink -f`) absolute paths and makes no SSH calls itself — same-host
|
||||
detection stays in `deploy-to-target.sh`, which already does it via `/etc/machine-id`. The
|
||||
function returns 0 when the two paths are equal, and returns non-zero with a message on stderr
|
||||
naming both paths and the 2026-07-31 incident in every other case — it does not special-case
|
||||
containment. This is the actual fix: the old code refused only two specific shapes
|
||||
(source-in-destination, destination-in-source); replacing that with an unconditional
|
||||
"refuse unless equal" is both the widening and a simplification, since any resolved-path mismatch
|
||||
on the same host is the identical `rsync --delete` hazard regardless of shape.
|
||||
|
||||
In `deploy-to-target.sh`, replace the guard block's two `case` statements with a single call to
|
||||
`assert_safe_same_host_deploy "$_LOCAL_SRC" "$_REMOTE_DST"` (the script already sources
|
||||
`scripts/lib/common.sh` near the top), still gated behind the existing same-machine-id check.
|
||||
Update the surrounding comment to say the guard now covers any mismatch, not just containment,
|
||||
and name the sibling-directory case as the reason — do not leave the old two-shape explanation
|
||||
standing next to a function that no longer works that way.
|
||||
|
||||
Write `tests/production-quality/deploy-guard-same-host.sh`, sourcing `scripts/lib/common.sh`
|
||||
directly (no SSH, no rsync, no real deploy) and asserting all five `<behavior>` cases against
|
||||
`assert_safe_same_host_deploy` with literal path strings — including the exact
|
||||
`archy-phase13` vs. main-checkout pair as the sibling-directory regression pin, so this specific
|
||||
incident shape cannot silently regress. Follow `tests/production-quality/lnd-cors-test.sh`'s
|
||||
overall shape for structure (this test takes no host argument, since it needs no live node).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bash -n scripts/lib/common.sh && bash -n scripts/deploy-to-target.sh</automated>
|
||||
<automated>bash tests/production-quality/deploy-guard-same-host.sh</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -q 'assert_safe_same_host_deploy' scripts/lib/common.sh` and `grep -q 'assert_safe_same_host_deploy' scripts/deploy-to-target.sh`
|
||||
- `grep -cE '"\$_LOCAL_SRC/" in|"\$_REMOTE_DST/" in' scripts/deploy-to-target.sh` returns 0 — the two old containment-only `case` blocks are removed, not left dead alongside the new call
|
||||
- `bash tests/production-quality/deploy-guard-same-host.sh` exits 0, and its output shows all five cases from `<behavior>` — including the sibling-directory pin — passing
|
||||
- Deliberately flipping the sibling-directory fixture's expectation to "allow" and re-running makes the test fail (checked by hand during execution to confirm the test is not vacuously green; not left as a permanent artifact)
|
||||
- `bash -n scripts/deploy-to-target.sh` exits 0 — the refactor did not break the script's syntax
|
||||
</acceptance_criteria>
|
||||
<reversibility rating="reversible">A stricter guard than before; the only behavior change is refusing deploys that were already unsafe. Nothing that was safe before becomes refused now, and nothing unsafe becomes newly allowed.</reversibility>
|
||||
<done>The same-host guard refuses any resolved source/destination mismatch, not only containment; the sibling-directory shape this session's own worktree topology exhibits is pinned by a regression test; the old, narrower logic is removed rather than left alongside the new call.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Fetch the bytes off a real node — the directory listing lies</name>
|
||||
<name>Task 4: Fetch the bytes off a real node — the directory listing lies</name>
|
||||
<files>scripts/verify-aiui-deploy.sh</files>
|
||||
<what-built>
|
||||
`scripts/verify-aiui-deploy.sh <node-host>` — a post-deploy check that resolves the *live* asset
|
||||
@@ -263,27 +425,29 @@ the string. The only honest check fetches what the browser would actually load.
|
||||
|----------|-------------|
|
||||
| AIUI document → `/rpc/v1` | **The boundary this plan enforces.** Same-origin today, so only a policy can stop it |
|
||||
| maintainer workstation → node filesystem | The rsync deploy path; what lands is what runs |
|
||||
| AIUI repo → Archy build | A second repository's HEAD becomes part of this repo's shipped artifact |
|
||||
| same-host deploy source → deploy destination | Any same-host path mismatch is an `rsync --delete` hazard, not only a containing/contained one — the fold-in this plan widens |
|
||||
| node `assets/` → verification | The graveyard that makes a disk grep lie |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-13-54 | Elevation of Privilege | AIUI's JS calling `/rpc/v1` with the ambient session cookie | high | mitigate | `/aiui/`-scoped CSP `connect-src` excluding the RPC path; verified in the browser, per-frame, in Task 3 step 6. `sandbox` explicitly rejected with reasons recorded |
|
||||
| T-13-54 | Elevation of Privilege | AIUI's JS calling `/rpc/v1` with the ambient session cookie | high | mitigate | `/aiui/`-scoped CSP `connect-src` excluding the RPC path; verified in the browser, per-frame, in Task 4 step 6. `sandbox` explicitly rejected with reasons recorded |
|
||||
| T-13-55 | Elevation of Privilege | Residual: a browser that ignores or partially enforces CSP | medium | accept | Named residual (AI-SPEC §6 row 1). Compensating control is G-B3's rate limit and anomaly counter on `assistant.chat`, landing in 13-12. Recorded, not silently assumed away |
|
||||
| T-13-56 | Information Disclosure | Media URL or page path leaking upstream via Referer | medium | mitigate | `referrerpolicy="no-referrer"` on the iframe; complements 13-06's no-credential-in-URL rule |
|
||||
| T-13-57 | Tampering | A wrong `VITE_BASE_PATH` ships a black page to every node | high | mitigate | `require_base_path` exits non-zero; `verify_dist` asserts every asset href carries the mount path before anything is copied |
|
||||
| T-13-58 | Tampering | An unattributable AIUI build from a dirty second-repo tree | medium | mitigate | `scripts/aiui.pin` + refuse-on-dirty + the SHA emitted into the built output |
|
||||
| T-13-58 | Tampering | An unattributable AIUI build | medium | mitigate | D-19 retired the second-repo pin-and-dirty-check mechanism (there is no second working tree to go stale); `verify_dist` now embeds **this repo's own current commit SHA** in the built output, so a deployed node is attributable to a commit of this repo directly |
|
||||
| T-13-59 | Repudiation | A disk grep over the node's asset graveyard reports a deploy that did not happen | high | mitigate | `verify-aiui-deploy.sh` resolves live chunks via the service worker manifest and greps the **fetched** bytes; asserted by the no-ssh grep and by a negative control |
|
||||
| T-13-60 | Denial of Service | CSP breaks AIUI's runtime and the chat surface goes dark | medium | mitigate | Task 3 steps 5 and 7 exercise chat and a content grid after the policy lands; a break is recorded as a residual rather than papered over by relaxing the check |
|
||||
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | **Zero** packages added. The build script runs AIUI's existing `pnpm`/`vite` toolchain and installs nothing new. No install task, so no legitimacy checkpoint required |
|
||||
| T-13-60 | Denial of Service | CSP breaks AIUI's runtime and the chat surface goes dark | medium | mitigate | Task 4 steps 5 and 7 exercise chat and a content grid after the policy lands; a break is recorded as a residual rather than papered over by relaxing the check |
|
||||
| T-13-61 | Tampering | A same-host deploy whose source and destination are sibling directories (not containment) is not refused, so `rsync --delete` mirrors the wrong source onto a real checkout and deletes what it lacks | high | mitigate | `assert_safe_same_host_deploy` refuses any resolved-path mismatch on the same host, not only containment; pinned by `tests/production-quality/deploy-guard-same-host.sh`'s sibling-directory fixture (this session's own worktree topology) |
|
||||
| T-13-SC | Tampering | npm/pip/cargo installs | high | mitigate | **Zero new** packages. The build script runs AIUI's existing in-repo `pnpm`/`turbo`/`vite` toolchain, installing only what `aiui/pnpm-lock.yaml` already pins (`--frozen-lockfile`, hard failure on drift rather than a silent resolve). No install task adds a package, so no legitimacy checkpoint required |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `bash -n scripts/build-aiui.sh && bash -n scripts/verify-aiui-deploy.sh && bash -n scripts/deploy-to-target.sh`
|
||||
- `bash -n scripts/build-aiui.sh && bash -n scripts/verify-aiui-deploy.sh && bash -n scripts/deploy-to-target.sh && bash -n scripts/setup-aiui-server.sh && bash -n scripts/lib/common.sh`
|
||||
- `VITE_BASE_PATH=/wrong/ bash scripts/build-aiui.sh` exits non-zero
|
||||
- `grep -c 'Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf` == 2
|
||||
- `grep -c 'add_header Content-Security-Policy' image-recipe/configs/nginx-archipelago.conf` >= 4 (2 pre-existing site-wide + 2 new `/aiui/`-scoped)
|
||||
- `bash tests/production-quality/deploy-guard-same-host.sh` exits 0, covering the sibling-directory regression
|
||||
- On archi-dev-box: `verify-aiui-deploy.sh` passes with the real marker and fails with a fake one; AIUI renders; an RPC fetch from inside the frame is CSP-blocked while the same call from the top-level frame succeeds
|
||||
</verification>
|
||||
|
||||
@@ -291,7 +455,9 @@ the string. The only honest check fetches what the browser would actually load.
|
||||
AIUI cannot be built wrong silently, cannot be deployed unverifiably, and cannot reach the RPC
|
||||
surface from inside its own frame — and where the boundary is not absolute, the plan says so in
|
||||
the config comment, in the iframe comment and in the threat register rather than claiming a
|
||||
property it did not implement.
|
||||
property it did not implement. Separately, the same-host deploy guard folded into this plan
|
||||
refuses any resolved-path mismatch on the same host, closing the sibling-directory gap in the
|
||||
2026-07-31 incident's original fix.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user