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>
39 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-aiui-functional-conversational-node-control-and-content-surf | 09 | execute | 4 |
|
|
false |
|
|
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 installed 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) — 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
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.
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.
<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,verify_dist(nopin_commit, no--update-pin— D-19 retires the pin;verify_distnow attributes the build to this repo's owngit rev-parse HEADinstead of a second repo's pinned SHA) - New file
scripts/verify-aiui-deploy.sh:resolve_live_chunks,fetch_and_grep - 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 overassert_safe_same_host_deploy(identical / contained / containing / sibling / unrelated) image-recipe/configs/nginx-archipelago.conf: aContent-Security-Policyheader on thelocation /aiui/blocks (both server blocks)neode-ui/src/views/Chat.vue: areferrerpolicyattribute and an explanatory comment on the iframe recording whysandboxis absentscripts/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/../AIUIto$PROJECT_DIR/aiui, and its same-host guard now callingassert_safe_same_host_deployinstead of two inline containment-onlycaseblocksscripts/setup-aiui-server.sh:AIUI_DISTretargeted to the in-repo path; callsscripts/build-aiui.shwhen the dist is missing or stale instead of printing a manual command </artifacts_this_phase_produces>
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_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 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` — **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. - `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`.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. 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 test "$(grep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.conf)" -eq 1 <acceptance_criteria>
grep -c 'add_header Content-Security-Policy' image-recipe/configs/nginx-archipelago.confreturns 4 — the 2 pre-existing site-wide headers (commit6656d2f1, unrelated to this plan) plus the 2 new/aiui/-scoped ones this task adds, one per server block- The new
/aiui/-scoped CSP'sconnect-srcvalue 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.vuegrep -ci 'sandbox=' neode-ui/src/views/Chat.vuereturns 0, and the comment explaining why is presentgrep -c 'no session gate needed' image-recipe/configs/nginx-archipelago.confreturns 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 commentcd neode-ui && npx vitest run src/views/__tests__/chatAiuiEmbed.test.tsexits 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 4) </acceptance_criteria> 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 whysandboxis absent rather than implying it is present.
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.
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, 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 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 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.
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.
bash -n scripts/build-aiui.sh && bash -n scripts/deploy-to-target.sh && bash -n scripts/setup-aiui-server.sh
VITE_BASE_PATH=/wrong/ bash scripts/build-aiui.sh; test $? -ne 0
bash scripts/build-aiui.sh && grep -c 'src="/aiui/' aiui/packages/app/dist/index.html | grep -qvx 0
<acceptance_criteria>
bash -n scripts/build-aiui.shexits 0 and the file is executableVITE_BASE_PATH=/wrong/ bash scripts/build-aiui.shexits non-zero with a message naming the required value- After a successful run, every
src=/href=inaiui/packages/app/dist/index.htmlstarts with the AIUI mount path —grep -cE '(src|href)="/(?!aiui/)' dist/index.htmlfinds 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.shreturns ≥ 1 andgrep -c 'VITE_BASE_PATH=/aiui/ pnpm build' scripts/deploy-to-target.shreturns 0 — the inline build is gonegrep -c '\.\./AIUI' scripts/deploy-to-target.sh scripts/setup-aiui-server.shreturns 0 across both files — no reference to the retired out-of-repo checkout survives in either script this task touchesgrep -c 'build-aiui.sh' scripts/setup-aiui-server.shreturns ≥ 1 — it calls the one true build script instead of printing a manual commandgrep -cE 'aiui\.pin|pin_commit|update-pin' scripts/build-aiui.shreturns 0 — the retired pinning mechanism is not present in any form </acceptance_criteria> 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) andsetup-aiui-server.shboth build and deploy AIUI from its in-repo location.
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).
bash -n scripts/lib/common.sh && bash -n scripts/deploy-to-target.sh
bash tests/production-quality/deploy-guard-same-host.sh
<acceptance_criteria>
grep -q 'assert_safe_same_host_deploy' scripts/lib/common.shandgrep -q 'assert_safe_same_host_deploy' scripts/deploy-to-target.shgrep -cE '"\$_LOCAL_SRC/" in|"\$_REMOTE_DST/" in' scripts/deploy-to-target.shreturns 0 — the two old containment-onlycaseblocks are removed, not left dead alongside the new callbash tests/production-quality/deploy-guard-same-host.shexits 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.shexits 0 — the refactor did not break the script's syntax </acceptance_criteria> 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. 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.
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.
- Write
scripts/verify-aiui-deploy.shfollowingtests/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/. - Build with
bash scripts/build-aiui.shand deploy to archi-dev-box perCLAUDE.md(dev pair before any OTA). - Run
bash scripts/verify-aiui-deploy.sh <node> "<a string only the new build contains>". Expect exit 0. - 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.
- 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.htmland reading its asset hrefs. - 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. - Exercise the embedded chat and one content grid to confirm the CSP did not break AIUI's own runtime. <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.shreturns ≥ 1 andgrep -ci 'ssh' scripts/verify-aiui-deploy.shreturns 0 — the check is an HTTP fetch, not a disk grepcurl -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> Type "approved" with the two console results from step 6, or describe what the CSP broke.
<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 |
| 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 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 | 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 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> |
<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. 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>
Create `.planning/phases/13-aiui-functional-conversational-node-control-and-content-surf/13-09-SUMMARY.md` when done