v1.81.0.0 feat: Aside is the browser gstack drives first; every browsing skill, the PDF/diagram renderer, and web research; the bundled browser stays the automatic fallback (#2810)

* feat(aside): browser-driver contract, cookbook, research and fallback resolvers

{{ASIDE_SETUP}} (readiness probe + ten rules for driving the user's real browser), {{ASIDE_COOKBOOK}} (script shapes verified live against Aside CLI 1.26: one flow per aside repl script, CDP console hook before navigation, evidence lines, session-directory artifact handoff, GSTACK_STEP_OK sentinel), {{ASIDE_RESEARCH}} (research through aside exec, WebSearch when Aside is absent, knowledge otherwise) and {{BROWSE_FALLBACK}} (the fifteen-row Aside-step to $B-command table plus the rules that differ, so every browsing skill keeps working on gstack's own headless browser). test/aside-driver.test.ts pins the sentences and asserts every browsing skill carries the Aside block followed by the fallback; test/helpers/aside-available.ts is the shared live-Aside probe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(render): Aside-first local-HTML renderer with the bundled browser as fallback

lib/aside-render.ts serves the HTML's directory on loopback (Aside refuses file:// URLs), opens it with waitUntil load, prints through CDP Page.printToPDF so tagged output, outlines, header/footer templates and page numbers survive, emulates device metrics for sized screenshots, and writes in-page evaluations to files; when Aside is absent it runs the same spec through the browse daemon (newtab, load, js, pdf, screenshot, closetab) and reports ENGINE=aside|browse. bin/gstack-render.ts is the CLI skill templates call. lib/claude-bin.ts and lib/error-handling.ts become the canonical copies (browse/src re-exports them).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(browse): /browse drives Aside first, with the $B reference behind the fallback

Contract, cookbook, mode choice (aside repl by default, aside exec for reading), report format, the fallback section, and the full command reference carved on demand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(qa): /qa and /qa-only drive Aside, fall back to $B

QA_METHODOLOGY runs every phase as Aside scripts (orient, explore, document, re-test, mobile viewport via CDP emulation, links via HEAD fetch); the authenticate phase is 'you are already signed in'; a 13th rule requires consent before mutating actions on non-local targets; the fallback section translates each step onto $B. The qa E2E tests run on whichever engine is present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(design): design-review, design-consultation, design-shotgun, plan-design-review, design-html drive Aside

Design-system extraction is one script printing FONTS/COLORS/HEADINGS/TOUCH_TARGETS/NAV; competitor research confirms the exact URLs before opening them in the real browser and runs on the bundled browser when Aside is absent; design-html's viewport screenshots, sketches and comparison boards render through gstack-render.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(deploy): benchmark, canary, land-and-deploy Step 7, devex-review drive Aside

One aside repl script per page prints NAV/PAINT/LCP/RESOURCES/SCRIPTS/CSS/SUMMARY (benchmark), CONSOLE_ERRORS/NAV/TEXT + screenshot (canary, re-run every 60s), and the post-deploy check reads responseStatus from the navigation entry; each carries the $B fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(third-party-actions): Aside is the recommended driver; gstack's visible browser stays the fallback

The readiness probe is lifted from {{ASIDE_SETUP}} at gen time (byte-identity pinned) and rule 3 points at browse/SKILL.md for how to drive; the consent question offers Aside first and gstack's own visible browser (handoff/resume for sign-in) as the fallback, as v1.72 framed it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(scrape): /scrape reads pages through Aside; the browser-skills runtime rides the fallback

Look-then-extract scripts build the JSON inside the page and print it between JSON_START/JSON_END; aside exec for fuzzy intents; on the $B fallback the browser-skills match/prototype flow and /skillify apply as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(make-pdf): print through Aside first, the bundled browser otherwise

asideClient.ts replaces the direct $B client with one render() call per PDF (the exact option mapping the browse pdf command had: paper, margins, header/footer/page numbers, tagged, outline, printBackground, preferCSSPageSize, Paged.js wait); the diagram pre-pass, oversized-image downscale and DOCX rasters each run as one render script with per-fence try/catch; exit 4 now means no browser is available and names both remedies; $P setup reports which engine it found. The e2e gates run on whichever engine is present, so the Linux lane exercises the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(diagram): the triplet is one gstack-render call

SVG, PNG and excalidraw from one invocation over the content-addressed bundle staged under /tmp/gstack-render; every diagram type gets an excalidraw export; gstack-render picks the engine and prints ENGINE=; the diagram E2E gates on either engine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(research): web research runs in Aside first, WebSearch second

The planning, review, design, security and investigate skills research through {{ASIDE_RESEARCH}}; WebSearch stays in allowed-tools as the fallback; testing.ts's bootstrap step follows; skeleton ceilings ratcheted for the research block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(setup,gen-skill-docs): prune renders of skills that no longer exist

setup gains _prune_stale_generated for every host tree and the doc generator removes gstack-* output dirs it did not write, so a skill removed from the source tree can never linger in an install.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: registries, budgets and suite reconciled for Aside-first with the $B fallback

Touchfiles + E2E tiers gain the Aside keys, coverage matrix and eval baselines updated, size budget re-baselined to parity-baseline-v1.80.0.0.json (the contract plus fallback ride in every browsing skill), parity ceilings ratcheted with measured values, LLM-judge prompts and the E2E fixtures speak Aside-first, browse-fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: Aside first, gstack browser fallback

README, BROWSER.md, docs/, CONTRIBUTING, CLAUDE.md, ARCHITECTURE, AGENTS.md, TODOS and the root router describe the one product story: Aside is the browser gstack drives first; the bundled headless browser is the automatic fallback (Linux, Windows, app closed) where cookie import, GStack Browser, pair-agent and browser-skills still apply.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: regenerate SKILL.md docs, llms.txt, agents digest, ship goldens, context-budget fixture

bun run gen:skill-docs over the templates; goldens re-rendered; context-budget ceilings recaptured.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* v1.80.0.0: Aside is the browser gstack drives first; the bundled browser is the fallback

MINOR: new capability across ten skills, the renderer and research; nothing removed. CHANGELOG release summary + itemized changes; VERSION 1.80.0.0; package.json 1.80.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(todos): file non-Claude host ownership-gate and version-heading pin follow-ups

Two follow-ups from the /plan-ceo-review + /plan-eng-review pass on merging
PR #2804 with main's v1.80.0.0 ownership gate: bring the Codex/Factory/
OpenCode/Cursor/Kiro copy loops and the stale-render prune under the
.gstack-owned marker rule, and a free test pinning that the CHANGELOG top
heading equals VERSION (the collision that git cannot see).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: pre-landing review fixes for the Aside-first branch

Review army + adversarial passes (Claude and Codex) on the merged branch:

setup
- _prune_stale_generated scans the host dirs too (the generator already
  removed the render before setup ran, so the host branch was dead), skips
  symlinks in the render tree (rm -rf on a slash-terminated link empties its
  target), removes a host symlink only when it resolves into gstack, cleans a
  bannered real dir through _cleanup_weak_dir, recognizes frontmatter-renamed
  skills, and logs through log. The always-run codex render passes every host
  dir that may link to it.
- NEEDS_BUILD checks all three binaries (with $_EXE) and lib/ sources; the
  browser hint and the bootstrap summary honor GSTACK_SKIP_ASIDE, treat a
  requested skip as a request, and derive one skill list.

lib/aside-render.ts + bin/gstack-render.ts
- The loopback server carries a per-render secret path, checks containment on
  the real path (symlink escapes are 403), and rejects malformed encoding.
- Inline eval results are one base64 line, so page text cannot forge
  ASIDE_DIR= or the sentinel; the last ASIDE_DIR wins.
- runProc escalates SIGTERM to SIGKILL, bounds every wait, and clears every
  timer (an uncleared one kept gstack-render alive after printing OK).
- renderTmpDir refuses a shared /tmp name owned by someone else; the work dir
  and server are created inside try; goto's budget follows the render budget.
- probeAside classifies a present-but-failing CLI as ASIDE_NOT_RUNNING like
  the skills' bash probe; render() retries on gstack's own browser when Aside
  could not start or its private CDP bridge is gone (never on a page error
  or a timeout of a running script); the CLI reports the engine that actually
  rendered, exits 0 on --help, rejects non-numeric flags, documents
  --wait-timeout, fences EVAL/PAGE_ERRORS as untrusted content, and names the
  daemon's cookie-import JS lock remedy.
- The browse path passes --scale only when asked (a scale change rebuilds
  the daemon context) and restores the viewport after a sized screenshot.

resolvers / templates
- The bash probe honors GSTACK_SKIP_ASIDE and has a perl deadline on stock
  macOS; .local is no longer LOCAL (mDNS); same-origin filters compare parsed
  origins; link status is HEAD-checked only on LOCAL targets; every
  aside exec goes through the receipted _aside_exec prelude
  ({{ASIDE_EXEC_PRELUDE}}), including nine template blocks that called it
  bare; the design sketch and diagram staging use private directories.
- The generator prunes only bannered renders and never a host whose
  generation failed.

Docs, stale comments and dead code cleaned; goldens re-rendered; tests
updated and added for every behavior above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test: coverage for the render CLI, setup rebuild check, make-pdf exit codes, and prose $B spans

New free tests from the ship coverage audit: test/gstack-render-cli.test.ts
(argv guards, --help, output contract with a fake daemon, failure and
serve-root paths, no-browser case, prompt exit), test/setup-needs-build.test.ts
(every binary and source set flips NEEDS_BUILD, Windows suffixes),
make-pdf/test/cli-exit-codes.test.ts and setup-smoke.test.ts (error to exit
code mapping, runSetup stages, renderPdf's engine), and prose-span cases for
extractBrowseCommands in test/skill-parser.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: CHANGELOG and TODOS cover the review fixes (v1.81.0.0)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: sync project docs with the v1.81.0.0 review fixes

BROWSER.md, ARCHITECTURE.md, CONTRIBUTING.md, README.md, CLAUDE.md,
docs/TESTING_INTERNALS.md and docs/PROJECT_STRUCTURE.md now describe the
shipped renderer and setup: the loopback render server's per-render secret
path and real-path containment, ENGINE= naming the engine that actually
rendered (mid-run retry on gstack's own browser), EVAL/PAGE_ERRORS fenced as
untrusted content, --wait-timeout and the CLI's argv guards, the receipted
_aside_exec prelude ({{ASIDE_EXEC_PRELUDE}} in the placeholder table), the
LOCAL host rule without .local, LOCAL-only HEAD checks in the links script,
GSTACK_SKIP_ASIDE across probe/renderer/setup, the ownership-gated
retired-skill prune, the widened NEEDS_BUILD check, and the new free tests
(gstack-render-cli, setup-prune-stale-generated, setup-browser-hint,
setup-needs-build, make-pdf cli-exit-codes and setup-smoke).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: CHANGELOG states the precise mid-run retry rule

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(test): skill-e2e-bws slices the $B setup block from the Browser fallback section

browse/SKILL.md no longer has '## SETUP' / '## Core QA Patterns' (Aside is the
primary driver; the $B block moved under 'Browser fallback'), so the gate test
sliced an empty block and handed the agent nothing to run. Anchor on
'### Find the `$B` binary' up to the next heading. 7/7 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(test): gate POSIX-only fixtures off Windows

windows-free-tests: the gstack-render CLI tests drive a shebang fake browse
that CreateProcess cannot exec, and two NEEDS_BUILD cases assert an execute
bit and a bare-name miss that MSYS bash does not have (test -x ignores mode
bits and resolves design -> design.exe). Those describes and cases now
self-skip on win32; argument guards, --help, the no-browser case, and every
other rebuild-check case still run there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(render): runProc waits for the exit code until the kill deadline; newtab retries once on a cold daemon

A process whose pipes have reached EOF is exiting, but runProc gave the exit
code only five seconds to arrive and then returned null, which run() reports
as a failed command. Under CI's six-shard load one such render failed with the
artifact already written. The SIGTERM/SIGKILL timers already bound the wait,
so the exit race now runs to the kill deadline.

The first CLI call auto-starts the browse daemon; on a cold start it can
answer 'Unable to connect' once while the server is still coming up. That
single case is retried after 1.5s; every other newtab failure is not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(aside-render): warm the daemon before live fallback cases; failures name the render error

- Live fallback cases run 'goto about:blank' up to twice before asserting and
  skip (never fail) when the daemon cannot come up.
- expectOk() puts r.error and the browse transcript into the assertion so a
  failed render is diagnosable from the CI log.
- The argv-contract cases dump the fake's log on a miss.
- File default timeout is 30s: the subject is the CLI contract, not latency.
- Two cases pin the cold-daemon newtab retry and that other errors are not
  retried.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: CHANGELOG notes the cold-start tolerance of the bundled-browser renderer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Sina <sdroid674+github@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-09-06 08:54:25 -07:00
committed by GitHub
co-authored by Claude Fable 5.1 Sina
parent c241216637
commit 0530392821
172 changed files with 12044 additions and 4676 deletions
+220 -5
View File
@@ -10,15 +10,39 @@ Waves BE2 of that plan are scheduled work, not TODOs; these are the items the
reviews deliberately deferred, each with rationale:
- **Shared `_gstack_owned_link` helper** — the ownership gate now exists in
six places (setup's `_claude_entry_is_ours` / `_claude_entry_owned_strongly`
seven places (setup's `_claude_entry_is_ours` / `_claude_entry_owned_strongly`
used by link_claude_skill_dirs and _install_alias_skill_md, while
cleanup_old_claude_symlinks and cleanup_prefixed_claude_symlinks inline their
own marker/cmp/banner chain and readlink `case`; bin/gstack-relink
`_entry_is_ours`; bin/gstack-uninstall's per-entry loop). Extract one sourced
`_entry_is_ours`; bin/gstack-uninstall's per-entry loop; and, since the
Aside-first wave, setup's `_prune_stale_generated`, which removes a retired
host entry behind the banner-only `_owned_for_windows_refresh` check —
symlinks outright, real dirs through `_cleanup_weak_dir` — and must route
through the same helper). Extract one sourced
helper so the destructive-path guard cannot drift, and while there: make the
`.gstack-owned` marker's recorded install path load-bearing (today any marker
counts, so a Windows fork copy carrying gstack's generated header is still
treated as ours on a mode flip). Effort S. Priority P2. Depends on: none.
- **Non-Claude host loops + stale-render prune under the marker rule** — the
Codex, Factory, OpenCode, Cursor and Kiro link loops (setup's
`link_*_skill_dirs`, the `_owned_for_windows_refresh` gate at each) still
`rm -rf` + re-copy a REAL host directory on banner-only proof, and
`_prune_stale_generated` routes a bannered real dir through
`_cleanup_weak_dir` only because those hosts never receive a `.gstack-owned`
marker. Write the marker for every host's copy install, then switch all five
loops and the prune to the strong/weak split the Claude host and
`gstack-relink` already use (#2119). Effort M (human ~2 days / CC ~1h).
Priority P2. Depends on: the shared `_gstack_owned_link` helper above (same
code motion; do them together).
- **Free test: CHANGELOG top heading equals VERSION** — a fork PR that claimed
a version main had since shipped auto-merged VERSION, package.json and the
digest header with no git conflict (both sides identical); only
`bin/gstack-next-version` and the PR-time queue check saw it. A tiny free
test asserting the first `## [X]` in CHANGELOG.md equals VERSION would make
the collision a red test on any branch. Decide first whether mid-branch
VERSION bumps without a CHANGELOG entry are a workflow the suite must
tolerate (`/ship` writes both in one step, so probably not). Effort S
(human ~2h / CC ~10min). Priority P3. Depends on: none.
- **Config-key reader tripwire** — `transcript_ingest_mode=off` sat unread for
months while setup-gbrain advertised it. A free test that asserts every key
in bin/gstack-config's default table is read by at least one binary (or is
@@ -511,8 +535,13 @@ references — include it in this fix's coverage list.
### QA logged-in-evidence path via Aside (Phase 2)
**What:** Consent-gated `aside repl` as an alternative evidence source in /qa,
/qa-only, and /browse when cookie-import can't reach a session (SSO,
**Landed (Aside-first):** `aside repl` is now the PRIMARY evidence source for
/qa, /qa-only, and /browse whenever Aside is installed and running; gstack's own
browser (with cookie import) is the automatic fallback when it is not. Kept for
the rationale; the remaining loose ends are under "Aside-first follow-ups".
**What:** Consent-gated `aside repl` as the evidence source in /qa, /qa-only,
and /browse for sessions a headless browser could never reach (SSO,
device-bound auth, Safari-side logins Chromium export can't see).
**Why:** Fills the exact gap `docs/designs/CHROME_VS_CHROMIUM_EXPLORATION.md`
@@ -3824,7 +3853,7 @@ path to the fixture during the run.
**What:** Cache rendered diagram SVG/PNG in `~/.gstack/cache/diagram-render/`,
keyed on `sha256(fence source + bundle version + render options)`, so repeat
`make-pdf` runs skip the browse render tab for unchanged diagrams.
`make-pdf` runs skip the render (Aside or the fallback browse tab) for unchanged diagrams.
**Why:** Every run currently re-renders every fence (~150-300ms each). Docs with
10+ diagrams pay seconds per iteration during write-preview loops. Codex
@@ -4006,3 +4035,189 @@ globs (D). What remains, re-filed individually:
bounded (GSTACK_PLAYWRIGHT_INSTALL_TIMEOUT, default 600s), lock contention
is a reason code, skills always register. Close #2233, #1900, #1901, #1902,
#913 with the receipt (test/setup-playwright-best-effort.test.ts).
## Aside-first follow-ups (filed when Aside became the primary browser)
Every gstack skill that touches a web page drives the Aside AI browser first
(`scripts/resolvers/aside.ts` is the contract; `lib/aside-render.ts` /
`bin/gstack-render.ts` render local HTML through it; `{{ASIDE_RESEARCH}}` runs
web research through it). gstack's own browser engine — the `browse` daemon,
GStack Browser headed mode, cookie import, `/pair-agent`, browser-skills /
`/skillify` — is kept as the automatic fallback whenever Aside is not installed
or not running (Linux, Windows, a closed Aside app), and web research falls
back to the WebSearch tool when the host provides one. Nothing was removed.
Loose ends:
### P1: Aside-first fallback parity — keep the `$B` equivalence table in sync with the cookbook
**What:** The fallback block (`BROWSE_FALLBACK` in the browser resolvers) maps
each verified `aside repl` cookbook shape (read a page, drive a flow, annotated
screenshot, responsive captures, links + status, performance, PDF, element
screenshot, `aside exec` research) to its `$B` equivalent so a skill produces
the same evidence lines on either path. Every time a cookbook shape is added,
renamed, or changes its output labels (`CONSOLE_ERRORS=`, `DIFF_START`,
`ASIDE_DIR=`, `GSTACK_STEP_OK`), update the table in the same commit and add a
pin in `test/aside-driver.test.ts` that the two lists name the same shapes.
**Why:** A skill that reads `DIFF_START` on the Aside path and gets nothing on
the fallback path "fixes" the missing output blindly. Parity is the whole point
of keeping the engine; a silent gap is worse than no fallback.
**Effort:** S per change (human ~half day, CC ~15min). **Priority:** P1. **Depends on:** nothing.
### P2: Aside CLI 1.26 lacks subcommands Aside's own skill doc lists
**What:** Aside's skill doc lists `session`, `memory`, `skills`, `host`, and
`--permission`; Aside CLI 1.26 has none of them (`aside --help`). Skills must
not depend on them until the CLI ships them. Re-probe on each Aside release;
when they land, evaluate `session` for multi-script flows and `--permission`
for the mutating-action consent gate.
**Why:** A skill written against the doc instead of the binary dies at runtime
on an unknown-command error the agent will then try to "fix" blindly.
**Effort:** S (human ~half day, CC ~20min per re-probe). **Priority:** P2. **Depends on:** Aside releases.
### P2: Aside E2E tests run only where Aside is installed
**What:** The Aside-only E2E lane — `test/skill-e2e-aside.test.ts`, the Aside
qa/design cases, the live render in `test/aside-render.test.ts` — self-skips
when `aside` is absent (`asideAvailable()` in `test/helpers/aside-available.ts`),
so CI's Linux runners never drive Aside; the make-pdf and /diagram render gates
already run there on the browse binary. The Aside path runs only on macOS dev
machines.
Evaluate a self-hosted macOS runner (or a scheduled job on a Mac mini) that
runs the Aside lane weekly under the same hermetic env as the other E2E lanes.
**Why:** A browser contract nobody runs in CI drifts silently — exactly the
class `test/aside-driver.test.ts` pins statically but cannot prove live.
**Effort:** M (human ~2 days, CC ~1h plus the machine). **Priority:** P2. **Depends on:** a macOS host with Aside signed in.
### P3: Evaluate `aside mcp` for multi-step flows
**What:** `aside repl` is one flow per script — a fresh session per call, tabs
closed when it ends. `aside mcp` keeps a persistent REPL page across calls.
Once the CLI stabilizes, measure whether an MCP path makes long QA audits
cheaper (no re-navigation per script) without losing the "leave the browser as
you found it" guarantee.
**Why:** Re-navigating from the URL per script is the honest tax of the current
model; a persistent page could cut it but adds a session that must be cleaned up.
**Effort:** M (human ~2 days, CC ~1h). **Priority:** P3. **Depends on:** Aside CLI stability.
### P3: Eval that skills treat `aside exec` output as untrusted
**What:** `aside exec "<task>"` returns another agent's answer. Add an LLM-judge
or E2E eval that plants an instruction inside an `aside exec` result and checks
the skill takes syntax from it, never scope, permissions, or consent.
**Why:** The rule is pinned as prose; nothing yet proves a skill obeys it when
the injected text arrives through the one channel that reads like a colleague.
**Effort:** S (human ~1 day, CC ~30min). **Priority:** P3. **Depends on:** the Aside E2E lane above.
### P1: make-pdf renders user documents inside the real browser profile — add a CSP
**What:** `/make-pdf` prints markdown-derived HTML through Aside (the user's
signed-in browser) on a `127.0.0.1` origin. The only barrier between a hostile
document (a README from a cloned repo) and script execution in that profile is
the regex sanitizer in `make-pdf/src/render.ts`, whose header assumes marked
output is never malformed — raw-HTML passthrough breaks that assumption. Inject
gstack's own CSP `<meta>` into the print template (`default-src 'none';
img-src data: 'self'; style-src 'unsafe-inline' 'self'; font-src data: 'self';
script-src 'nonce-<per-render>'` for Paged.js), since user `<meta>` is stripped
and gstack's is not; alternatively keep make-pdf on the bundled engine by
default.
**Why:** Under the old cookieless headless engine a sanitizer bypass was
near-harmless; in the real profile it is a CSRF-class primitive. Cross-model
finding (Claude adversarial + Codex).
**Effort:** M (human ~2 days, CC ~1h). **Priority:** P1. **Depends on:** none.
### P1: diagram pre-pass buffers every oversized image before downscaling
**What:** `make-pdf/src/diagram-prepass.ts` caps each image at 64 MB but keeps
every pending buffer in `downscales` and duplicates it as base64 before the
batch runs; a document referencing a few dozen large images can take gigabytes.
Cap total pending bytes (e.g. 256 MB) and process in bounded batches, or
downscale sequentially.
**Why:** A hostile or merely image-heavy document crashes the tool instead of
degrading.
**Effort:** S (human ~1 day, CC ~30min). **Priority:** P1. **Depends on:** none.
### P2: fallback renders die after any cookie import in the daemon's lifetime
**What:** `renderWithBrowse` drives readiness and evals through `$B js`, and the
daemon's cookie-import JS lock (`browse/src/read-commands.ts`) refuses `js` on
every origin outside the imported set — `127.0.0.1` included, forever (the set
is add-only). The renderer now names the remedy (`$B stop`), but the real fix is
a fresh incognito context for local-HTML renders, or a loopback exemption once
its threat model is written down.
**Why:** On Linux/Windows (no Aside) one `/setup-browser-cookies` run makes
every later `/diagram` and `/make-pdf` render fail.
**Effort:** M (human ~2 days, CC ~1h). **Priority:** P2. **Depends on:** none.
### P2: carve the Aside contract + fallback block into one shared section
**What:** `{{ASIDE_SETUP}}` (~5.6 KB) plus `{{BROWSE_FALLBACK}}` (~4.1 KB) are
rendered verbatim into ten browsing skills (~97 KB of identical prose loaded on
every invocation). Keep the probe and the three decision steps inline; move
"Rules for driving a real browser" and the Aside-to-`$B` translation table into
one carved reference (the `browse/sections/command-list.md` pattern), then
re-run `capture-context-budget.ts` so the ceilings ratchet back down.
**Why:** Every skill invocation pays for prose that is skill-invariant.
**Effort:** M (human ~2 days, CC ~1h). **Priority:** P2. **Depends on:** none.
### P2: `$B js` / `$B eval` output is not wrapped in the untrusted envelope
**What:** `js` and `eval` are not in `PAGE_CONTENT_COMMANDS`
(`browse/src/commands.ts`), so page-controlled return values reach the agent
unfenced while the fallback table routes exactly the page-controlled reads
through them. `gstack-render` now fences its own `EVAL`/`PAGE_ERRORS` lines and
the fallback prose says `$B js` is unwrapped; the durable fix is to add both
commands to the envelope set.
**Why:** A hostile page can deliver injection text through the one channel the
skills were told is fenced.
**Effort:** S (human ~half day, CC ~15min). **Priority:** P2. **Depends on:** none.
### P3: Aside-first renderer follow-ups (perf and DRY)
- **Readiness polling** spawns a `browse js` process every 150 ms; the daemon's
`wait <sel>` command blocks server-side in one spawn — use it for
`waitFor.selector`. Effort S.
- **Bundle re-staging:** every `runScript()` batch copies the ~9 MB diagram
bundle into a fresh mkdtemp and starts a new loopback server; stage once per
run (content-addressed) and, on the browse engine, keep one tab across the
fence/downscale/DOCX batches. Effort M.
- **Probe cost:** `probeAside()` runs two blocking spawns per process and the
engine cache is per-process; persist the outcome with a short TTL under
`GSTACK_HOME` and lower the repl probe timeout on the code path. Effort S.
- **DRY:** the console-error `HOOK` IIFE exists in seven copies across
`scripts/resolvers/*.ts` and `lib/aside-render.ts` (two divergent variants);
cookbook recipes (responsive loop, links, read-a-page) are duplicated across
`aside.ts`, `design.ts`, `utility.ts`; the readiness probe is recovered from
rendered markdown by regex in two places instead of a shared constant. Export
one source for each. Effort S each.
- **Egress scanner:** `test/egress-receipt-wiring.test.ts` scans `curl`, `git
push`, and `fetch`; add `aside exec` as a sink class so a bare call fails CI
the way the others do. Effort S.
- `_browser_hint` treats any `aside` on PATH as the Aside browser (no version
check). Effort S.
- **`gen-skill-docs --dry-run` is not write-free for external hosts:**
`processExternalHost` runs `mkdirSync(outputDir)` and writes
`agents/openai.yaml` with no `DRY_RUN` guard (only SKILL.md is skipped), so a
dry run against an empty `--out-dir` leaves 54 `openai.yaml` files behind.
Guard both writes. Effort S.
**Priority:** P3. **Depends on:** none.