Add phased-execution planning structure
Build and Push Container / build-and-push (push) Successful in 11s

Introduce .agents/ (PLAN.md with locked architectural anchors,
phase roadmap under phases/todo/) and AGENTS.md rules for
agents working in the repo. Queues the pending phases: fix
history XSS, simulate fetch commands, and nginx security
headers.
This commit is contained in:
2026-09-18 17:23:41 -04:00
parent 010758b3e9
commit a2f331e526
13 changed files with 963 additions and 0 deletions
@@ -0,0 +1,44 @@
# Phase 02 — Remove unvalidated client fetch from curl/wget (audit M-2)
**Story:** n/a
**Context:** `.agents/PLAN.md` §2 (principle 5 — fun is safe) & §3 (terminal
modules), `.agents/remediation_plan.md` finding M-2
## Objective
The fake terminal's `curl`/`wget` commands run a real, unvalidated
`fetch(url)` from the visitor's browser — a client-side request-forgery
primitive (and cookie-attached once the site ever has auth) that is also
silently broken in production (CSP `connect-src` falls back to
`default-src 'self'`, blocking every external fetch). Replace the real
fetch with **simulated output** consistent with the rest of the fake
terminal: zero network I/O, deterministic, echoes the requested URL.
**Chosen approach** (audit recommendation, owner-approved 2026-09-18):
simulate. The rejected alternative (real fetch + https-only allowlist +
`connect-src` + `credentials: 'omit'`) is documented for the record but out
of scope.
## Dependencies
— (none)
## Tasks
1. `01_simulate_fetch_output.md` — Add `simulatedCurlOutput(url)` / `simulatedWgetOutput(url)` to `terminal-commands.js`; replace both `fetch()` calls in `terminal.js` with the simulated outputs.
2. `02_fetch_regression_test.md` — Add Playwright tests proving curl/wget render simulated output, make **zero** non-origin network requests, and still unlock the `web_navigator` achievement.
## Testing & Quality
- Unit/integration: the regression tests must cover (a) `curl <url>`
renders the simulated block and echoes the URL, (b) `wget <url>` same,
(c) no request to any non-origin host occurs (request listener),
(d) `web_navigator` achievement/toast still fires.
- Coverage: **>90%** on new/modified code (the two simulator functions +
both branches).
- E2E: full Playwright suite green.
## Completion Criteria
- [ ] `grep -n "fetch(" src/terminal.js` returns no matches (the only
`fetch(` in `src/` must be gone)
- [ ] `simulatedCurlOutput` / `simulatedWgetOutput` present in
`src/terminal-commands.js`
- [ ] new regression tests present in `tests/e2e/` and passing
- [ ] `./build.sh && npm test` fully green (46+ tests)
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
@@ -0,0 +1,81 @@
# Task 01 — Replace real fetch with simulated output
**Phase:** `02_simulate_fetch_commands` · **Story:** n/a
## Objective
Remove the only real network I/O from the fake terminal: the `curl` and
`wget` commands stop calling `fetch(url)` and instead render a
deterministic, simulated response that echoes the requested URL. This kills
the client-side request-forgery primitive (audit M-2) and makes the feature
work identically in production (where CSP blocked the real fetch anyway).
## Work
1. `src/terminal-commands.js` — append two pure functions (no DOM, no
network). Keep them deterministic (the wget timestamp is the only
intentional non-determinism):
```js
// Simulated fetch output. The fake terminal performs NO real network
// I/O by design — these produce a deterministic, clearly-simulated
// response that echoes the requested URL.
function _simulatedBody(url) {
return 'simulated body for ' + url;
}
function simulatedCurlOutput(url) {
const body = _simulatedBody(url);
return [
' % Total % Received % Xferd Average Speed Time Time Time Current',
' Dload Upload Total Spent Left Speed',
'100 ' + body.length + ' 100 ' + body.length + ' 0 0 ' + (body.length * 40) + ' 0 --:--:-- --:--:-- --:--:--',
body,
].join('\n');
}
function simulatedWgetOutput(url) {
const body = _simulatedBody(url);
return [
'-- ' + new Date().toISOString().replace('T', ' ').slice(0, 19) + '-- ' + url,
'Resolving host... (simulated)',
'Connecting... connected (simulated).',
'HTTP request sent, awaiting response... 200 OK',
'Length: ' + body.length + ' [text/plain]',
"Saving to: 'index.html'",
'',
body,
'',
'Finished (simulated). No real network request was made.',
].join('\n');
}
```
2. `src/terminal.js` — in the `curl ` branch, delete the `fetch(url).then(...)
.catch(...)` chain and replace it with:
```js
// Simulated fetch — the fake terminal never performs real network I/O.
setTimeout(() => {
outLine.textContent = simulatedCurlOutput(url);
content.scrollTop = content.scrollHeight;
}, 400);
```
3. `src/terminal.js` — in the `wget ` branch, make the identical change but
call `simulatedWgetOutput(url)`.
Keep the existing `url` extraction regexes, the `Downloading...` line, and
the `whiteSpace`/`color` styling. The `web_navigator` achievement (keyed
on the `curl `/`wget ` prefix) is untouched and still fires.
## Testing & Quality
- Unit/integration: no unit test needed for the pure output functions beyond
the E2E assertions in task 02 (they are string builders exercised through
the terminal). If desired, a tiny `node -e` sanity check:
`node -e "eval(require('fs').readFileSync('src/terminal-commands.js','utf8')); console.log(simulatedCurlOutput('https://x'))"`.
- Coverage: **>90%** on the two new functions and both modified branches
(both covered by the task-02 E2E test).
## Completion Criteria
- [ ] `grep -n "fetch(" src/terminal.js` returns **no** matches
- [ ] `simulatedCurlOutput` and `simulatedWgetOutput` defined in
`src/terminal-commands.js`
- [ ] `./build.sh && npm test` fully green (existing 44 tests unaffected)
- [ ] no behavior change in completed work (terminal, vim, achievements, menu)
@@ -0,0 +1,59 @@
# Task 02 — Add the simulated-fetch regression test
**Phase:** `02_simulate_fetch_commands` · **Story:** n/a
## Objective
Add a Playwright E2E test proving `curl`/`wget` render the simulated output,
make **zero** non-origin network requests, and still unlock the
`web_navigator` achievement.
## Work
1. `tests/e2e/02-terminal.spec.js` — add inside the
`test.describe('Fake terminal — state and special commands', ...)` block:
```js
test('curl/wget are simulated — no real network request leaves the origin', async ({ page }) => {
await openTerminal(page);
// Track every request that is NOT aimed at the test origin.
const external = [];
page.on('request', (r) => {
if (!r.url().startsWith('http://127.0.0.1:8123')) external.push(r.url());
});
await typeCommand(page, 'curl https://example.com/hello');
// achievement fires on the first curl/wget
await expect(page.locator('.toast').filter({ hasText: 'Web Navigator' })).toBeVisible();
const curlLine = page.locator('.terminal-content > div').nth(-2);
await expect(curlLine).toContainText('simulated body for https://example.com/hello');
await typeCommand(page, 'wget https://example.org/file.txt');
const wgetLine = page.locator('.terminal-content > div').nth(-2);
await expect(wgetLine).toContainText('No real network request was made');
await expect(wgetLine).toContainText('https://example.org/file.txt');
expect(external).toEqual([]);
});
```
Notes for the executor:
- The origin prefix `http://127.0.0.1:8123` must match `baseURL` in
`playwright.config.js`; keep them in sync if the port ever changes.
- The `web_navigator` toast is asserted right after the curl command
(toasts auto-remove after ~4.3 s).
- If `expect(external).toEqual([])` is flaky on your network (e.g. DNS
prefetch noise), tighten it to assert that no request URL contains
`example.com` or `example.org` instead — the point is that the
simulated URLs were never contacted.
## Testing & Quality
- Unit/integration: this test is the regression coverage for the M-2 fix —
it fails on the pre-fix code (a real `fetch` to example.com/example.org
would appear in `external`) and passes on the simulated code.
- Coverage: **>90%** on both modified branches (both exercised).
## Completion Criteria
- [ ] new test present in `tests/e2e/02-terminal.spec.js`
- [ ] `./build.sh && npm test` fully green (45+ tests)
- [ ] (sanity) temporarily reverting task 01's change makes this test fail —
confirms the request tracking works, then re-apply