300 lines
23 KiB
Markdown
300 lines
23 KiB
Markdown
---
|
|
phase: 13-aiui-functional-conversational-node-control-and-content-surf
|
|
plan: 09
|
|
type: execute
|
|
wave: 4
|
|
depends_on: ["13-02"]
|
|
files_modified:
|
|
- scripts/build-aiui.sh
|
|
- scripts/verify-aiui-deploy.sh
|
|
- scripts/aiui.pin
|
|
- scripts/deploy-to-target.sh
|
|
- image-recipe/configs/nginx-archipelago.conf
|
|
- neode-ui/src/views/Chat.vue
|
|
autonomous: false
|
|
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)"
|
|
- "`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)"
|
|
artifacts:
|
|
- path: "scripts/build-aiui.sh"
|
|
provides: "The one way AIUI is built for a node: base-path enforced, commit pinned, output verified"
|
|
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"
|
|
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: "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"
|
|
pattern: "Content-Security-Policy"
|
|
---
|
|
|
|
<objective>
|
|
Two things that are currently held together by memory rather than by machinery.
|
|
|
|
**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.
|
|
|
|
**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
|
|
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
|
|
plan does not claim a property it does not implement.
|
|
|
|
**The mechanism, decided (Open Question 2):** a `/aiui/`-scoped `Content-Security-Policy` whose
|
|
`connect-src` permits only the AIUI path prefix, plus the G-B3 rate-limit/anomaly counter as the
|
|
compensating control. The `sandbox` attribute is **rejected** for this phase: AIUI needs
|
|
`allow-scripts`, and `allow-scripts` together with `allow-same-origin` is the well-known escape
|
|
pattern, while dropping `allow-same-origin` moves AIUI to an opaque origin and breaks its
|
|
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.
|
|
</objective>
|
|
|
|
<flagged_assumptions>
|
|
**FLAGGED — unresolved edge probe, AIUI-04, category `unclassified`.** Not auto-resolved and not
|
|
auto-backstopped. Surfaced for a human read: AIUI-04's requirement text ("sandboxed by
|
|
construction, permissioned by the user") does not itself say what "by construction" must mean —
|
|
browser-enforced, or enforced by the node regardless of what the browser does. This plan reads
|
|
it as browser-enforced-where-possible plus node-side compensating controls, and says so. If the
|
|
intent was a hard origin split (serving AIUI from a different origin entirely), that is a larger
|
|
change than this phase scoped and should be raised now rather than at seal time.
|
|
|
|
**FLAGGED — unresolved edge probe, AIUI-05, category `unclassified`.** Not auto-resolved and not
|
|
auto-backstopped. Surfaced for a human read: the requirement says AIUI needs "a delivery path an
|
|
operator can actually receive updates through", but does not say whether that means the OTA
|
|
update path specifically (so an existing node self-updates AIUI), or only that a maintainer
|
|
deploy is reliable. This plan delivers the second and makes the first *checkable*; if the first
|
|
is required, it needs an `update.rs` change this phase has not scoped.
|
|
</flagged_assumptions>
|
|
|
|
<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/verify-aiui-deploy.sh`: `resolve_live_chunks`, `fetch_and_grep`
|
|
- New file `scripts/aiui.pin` (data: branch + commit SHA)
|
|
- `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
|
|
</artifacts_this_phase_produces>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/STATE.md
|
|
@CLAUDE.md
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-CONTEXT.md
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-RESEARCH.md
|
|
@.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-02-SUMMARY.md
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<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`.
|
|
- `.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.
|
|
</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`.
|
|
|
|
Add a comment above the header stating in one sentence what the policy does and does not
|
|
guarantee — that it prevents AIUI's own JavaScript from issuing a same-origin fetch to the RPC
|
|
surface, and that it is *not* an origin split. The previous comment in this file
|
|
("no session gate needed") is the reasoning error that produced 13-02's exposure; do not leave a
|
|
comment here that could be read the same optimistic way.
|
|
|
|
In `Chat.vue`, do **not** add a `sandbox` attribute. Add `referrerpolicy="no-referrer"` to the
|
|
iframe (so a media URL or a page path never leaks upstream through a Referer header) and a
|
|
comment above the element recording, in three lines: that `sandbox` was considered and rejected
|
|
for this phase; that `allow-scripts` + `allow-same-origin` together is a known escape while
|
|
dropping `allow-same-origin` breaks AIUI's storage and its origin-checked bridge; and that the
|
|
enforced boundary is the `/aiui/`-scoped CSP plus the node-side rate limit, with the residual
|
|
risk named in `13-AI-SPEC.md` §6.
|
|
|
|
**Do not claim more than this implements.** If any acceptance check below fails on device, the
|
|
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>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>
|
|
</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 -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
|
|
- `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)
|
|
</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>
|
|
<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.
|
|
- `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`.
|
|
</read_first>
|
|
<action>
|
|
Create `scripts/build-aiui.sh`, the single supported way to build AIUI for a node.
|
|
|
|
`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.
|
|
|
|
`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.
|
|
|
|
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`.
|
|
|
|
`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.
|
|
|
|
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.
|
|
|
|
Also update `scripts/setup-aiui-server.sh`'s comments to point at `build-aiui.sh` rather than restating the env var.
|
|
</action>
|
|
<verify>
|
|
<automated>bash -n scripts/build-aiui.sh && bash -n scripts/deploy-to-target.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>
|
|
</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/`)
|
|
- `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
|
|
</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>
|
|
</task>
|
|
|
|
<task type="checkpoint:human-verify" gate="blocking">
|
|
<name>Task 3: 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
|
|
chunks by fetching the service worker manifest over HTTP, then fetches each live chunk and greps
|
|
the **fetched bytes** for a string the new build introduced.
|
|
|
|
This exists because the node's `assets/` directory is a never-pruned graveyard: a disk grep over
|
|
it reports "deployed" before the deploy, because a dead chunk from an old build still contains
|
|
the string. The only honest check fetches what the browser would actually load.
|
|
</what-built>
|
|
<how-to-verify>
|
|
1. Write `scripts/verify-aiui-deploy.sh` following `tests/production-quality/lnd-cors-test.sh`'s
|
|
shape. It takes a host and an expected marker string, fetches the service worker manifest
|
|
over HTTP to resolve live chunk URLs, fetches each, and greps the fetched bytes. It exits
|
|
non-zero when the marker is absent from every live chunk. It must **not** ssh in and grep
|
|
`/opt/archipelago/web-ui/aiui/assets/`.
|
|
2. Build with `bash scripts/build-aiui.sh` and deploy to archi-dev-box per `CLAUDE.md` (dev pair
|
|
before any OTA).
|
|
3. Run `bash scripts/verify-aiui-deploy.sh <node> "<a string only the new build contains>"`.
|
|
Expect exit 0.
|
|
4. Negative control: run it again with a string that does not exist in any build. Expect a
|
|
non-zero exit. A check that always passes is not a check.
|
|
5. Load neode-ui's Chat view on that node in a desktop browser. Confirm AIUI renders — not a
|
|
black page. A black page means the router base broke; confirm by fetching the node's
|
|
`/aiui/index.html` and reading its asset hrefs.
|
|
6. Open the browser devtools console **inside the AIUI frame** and attempt a POST to `/rpc/v1`.
|
|
Confirm the browser blocks it with a CSP violation. Then run the same fetch from the
|
|
top-level neode-ui frame and confirm it succeeds — that difference is the boundary this plan
|
|
claims, and step 6 is the only place it is actually observed.
|
|
7. Exercise the embedded chat and one content grid to confirm the CSP did not break AIUI's own
|
|
runtime.
|
|
</how-to-verify>
|
|
<acceptance_criteria>
|
|
- `bash scripts/verify-aiui-deploy.sh <node> "<new-build marker>"` exits 0
|
|
- The same script with a non-existent marker exits non-zero (negative control recorded)
|
|
- `grep -c 'sw.js' scripts/verify-aiui-deploy.sh` returns ≥ 1 and `grep -ci 'ssh' scripts/verify-aiui-deploy.sh` returns 0 — the check is an HTTP fetch, not a disk grep
|
|
- `curl -s http://<node>/aiui/index.html | grep -c 'src="/aiui/'` returns ≥ 1
|
|
- AIUI renders in the embedded iframe on the node — not a black page (screenshot in the summary)
|
|
- A `fetch('/rpc/v1', {method:'POST'})` from inside the AIUI frame is blocked with a CSP violation; the same call from the top-level frame succeeds (both console outputs recorded in the summary)
|
|
- Embedded chat still answers and one content grid still populates after the CSP landed
|
|
</acceptance_criteria>
|
|
<resume-signal>Type "approved" with the two console results from step 6, or describe what the CSP broke.</resume-signal>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| 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 |
|
|
| 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-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-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 |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `bash -n scripts/build-aiui.sh && bash -n scripts/verify-aiui-deploy.sh && bash -n scripts/deploy-to-target.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
|
|
- 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>
|
|
|
|
<success_criteria>
|
|
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.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-SUMMARY.md` when done
|
|
</output>
|