You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A domain returned FAIL. audit-application.md opened with VERDICT: FAIL — read that domain's section first. A domain's own verdict outranks the merged one.
Lines that decided this verdict
Lifted out of the fragments so truncation cannot cut them. Each domain's full section follows for as far as the body reaches.
audit-supply-chain.md: VERDICT: PASS
audit-ci-secrets.md: VERDICT: PASS
audit-application.md: VERDICT: FAIL
audit-hosted.md: VERDICT: PASS
audit-application.md: - FAIL: [local/Loopback] Lint-coverage clause (scans all tracked JS/TS and prints every bind; new unguarded non-test listener fails the build) — hosted/server/dev.ts:45 server.listen(Number(process.env.PORT || 0), "127.0.0.1") is a loopback bind the node-positional BIND_FORM (\.listen\(\s*[^,)]+,) cannot see because [^,)]+ stops at the inner ); node scripts/loopback-lint.mjs output omits it. Listener itself is guarded, so impact is lint drift, not exposure.
audit-application.md: - FAIL: [remote/One-time connection] Local networks: the offer the Burrow applies CAN keep a candidate outside the allowed networks — evidence: lib/src/host/remote/local-networks.ts CANDIDATE_LINE=/^a=candidate:\S+ \S+ \S+ \S+ (\S+) / and keepAllowed keeps every non-matching line; split on CRLF only when present. Verified: a candidate with a doubled space or tab before the address, or one after a bare LF inside a CRLF SDP, does not match (line kept), and node-datachannel 0.33.4 setRemoteDescri
audit-application.md: - FAIL: [remote/Network posture] (low impact) Tailscale CLI output piped into head -1 for a verify decision on macOS and Linux — evidence: deploy/local/install-macos.sh:902 and deploy/local/install-linux.sh:1131 tsip="$(ts ip -4 2>/dev/null | head -1 || true)"; the plaintext-reachability verdict is reached from that pipe rather than from captured text. The trailing || true absorbs a 141 and ts ip -4 output is tiny, so no wrong verdict is reachable today, but the rule text forbids the shape; Wi
audit-application.md: - WARNING: [local/Terminal output] OSC 367 open / Tool designation gate trusts a PTY-forgeable command line. use-dor-control.ts:1110-1116 admits oscOpen when getTerminalPaneState(id).currentCommand.rawCommandLine === tool.command, but rawCommandLine comes from OSC 633;E (terminal-protocol.ts:814-823, the VS Code nonce field is discarded, never checked) or OSC 133;C cmdline, then any OSC 633/133;C commandStart (terminal-state.ts:181-195, :446-452) — all accepted from any program output. A later
audit-application.md: - WARNING: [local/Loopback] scripts/loopback-lint.mjs misses a real loopback bind: hosted/server/dev.ts:45 server.listen(Number(process.env.PORT || 0), "127.0.0.1") — the node-positional BIND_FORM \.listen\(\s*[^,)]+, stops at the inner ) of Number(...), so the lint output omits the file. The listener is guarded (hosted/server/dev-host-guard.ts allowedDevRequest), but security-local.md claims the lint prints every bind and fails an unguarded new one; any port expression containing a call
audit-application.md: - WARNING: Tailscale CLI output is piped into head -1 for the manage verify plaintext-reachability decision on macOS and Linux (deploy/local/install-macos.sh:902, deploy/local/install-linux.sh:1131 tsip="$(ts ip -4 2>/dev/null | head -1 || true)"), the shape security-remote.md Network posture forbids; Windows captures first (install-windows.ps1:1579-1581). The || true and the one-line output make a wrong verdict unreachable today, but scripts/deploy-lint.mjs does not catch this site (lint green)
audit-application.md: - WARNING: [Local networks / One-time + Pocket direct path] localNetworksPath.acceptRemote filters remote candidates with a single-space regex (lib/src/host/remote/local-networks.ts CANDIDATE_LINE and keepAllowed) and splits only on CRLF when present; a candidate line with doubled whitespace/tab before the address, or placed after a bare LF in a CRLF SDP, is kept unfiltered and node-datachannel 0.33.4 accepts it (verified with setRemoteDescription). An approved peer (one-time phone after the cod
audit-application.md: - WARNING: External-link validation is a blocklist, not an allowlist: inspectExternalUri (lib/src/lib/external-links.ts:1,38) only rejects javascript:/data:/blob:/about:, so an OSC 8 link printed by untrusted terminal output (e.g. cat of a repo file) can carry file:///…, vscode:///…, ms-*: or any OS-registered protocol handler, and after the one-click confirmation the VS Code host passes it to vscode.env.openExternal (vscode-ext/src/message-router.ts:608-611), which hands non-http sch
audit-application.md: - WARNING: File-path paste/drop quotes but never rejects C0/DEL in the filename: pasteFilePaths (lib/src/lib/clipboard.ts:61-69) -> shellEscapePath (lib/src/lib/shell-escape.ts:11-31) -> writePasteToPty, whose unbracketed branch (clipboard.ts:50) writes the bytes raw. On a POSIX shell without bracketed paste (dash, bash with enable-bracketed-paste off, many REPLs), dropping a file named e.g. $'x\x15touch /tmp/pwned\n' yields '''x^Utouch /tmp/pwned''' — ^U kills the line including the ope
Security audit
Supply chain
VERDICT: PASS
FAIL IF results
PASS: generate-deps.js against clean tree after install changed none of the three data files (git status clean; 70 npm, 12 direct + 478 transitive cargo, 1 runtime).
PASS: .github/workflows/ci.yml:39-46 runs node website/scripts/generate-deps.js and exits 1 on git diff --quiet -- website/src/data/; install precondition is earlier in the job.
PASS: root completeness. productDependencyFilters (generate-deps.js:23) = dor, dormouse, dormouse-standalone, dormouse-lib, dormouse-sidecar, relay; exclusions (line 33) = canopy, dormouse-website, dormouse-hosted. All 13 pnpm-workspace.yaml packages covered (remote-lib-common, dor-lib-common, dor-tools-builtin, dor-tools-lib via workspace edges). Sidecar ships via tauri.conf.json bundle.resources "../sidecar/**/*". Excluded packages ship no installed artifact; the only non-registry lockfile tarball (@diffplug/xterm-addon-webgl-sdf) is consumed solely by canopy (excluded).
PASS: build.rs:43 calls verify_node_version, which runs --version and errors on mismatch (lines 207-224); only skip is host != target. release.yml standalone matrix (x86_64-unknown-linux-gnu on ubuntu, aarch64-apple-darwin on macos-latest, x86_64-pc-windows-msvc on windows-latest) is all host-native.
PASS: release.yml build-standalone uses node-version-file: package.json (line 48); root package.json has no volta or engines.node.
PASS: pnpm-workspace.yaml has minimumReleaseAge: 1440.
PASS: minimumReleaseAgeExclude = pgstencil, @pgstencil/* only; the only renovate minimumReleaseAge: null rule matches pgstencil and @pgstencil/**.
PASS: renovate.json enabledManagers includes npm and cargo; minimumReleaseAge package rules exist for ["npm","cargo"] (patch/minor/major).
PASS: secret_scanning enabled, secret_scanning_push_protection enabled (AUDIT_PAT); vulnerability-alerts returned HTTP 204.
Qualitative findings
INFO: Lockfile resolves one non-registry package (@diffplug/xterm-addon-webgl-sdf GitHub release tarball, integrity-hashed), used only by canopy, which is excluded and not in any production build.
INFO: No hasInstallScript/requiresBuild entries in pnpm-lock.yaml; build scripts governed by allowBuilds in pnpm-workspace.yaml.
INFO: No new product runtime dependencies identified beyond what the regenerated disclosure already matches.
CI and secrets
VERDICT: PASS
FAIL IF results
PASS rulesets: Merge access (16757376) branch ~DEFAULT_BRANCH rules update/deletion/creation, sole bypass RepositoryRole 5; Tag operations (16757382) ~ALL tag creation+update, same bypass; both active.
PASS dormouse-bot permission: permission=write, role_name=write (no maintain/admin).
PASS workflow-audit.yaml active; last successful runs 2026-09-30T13:37Z (<48h) and daily before.
PASS environments: all 7 custom branch policies; vscode-extension-publish v* tag; release-attest v* tag; security-audit main + v* tag; tend main; hosted-production/hosted-release-tag main only; hosted-preview main + refs/pull/*/merge. All admin-gated by rulesets.
PASS secret inventory: repo = ARGOS_TOKEN, CHROMATIC_PROJECT_TOKEN only; org total 0; AUDIT_PAT only in security-audit; TEND_BOT_TOKEN only tend; CLAUDE_CODE_OAUTH_TOKEN in tend and security-audit only; OVSX_PAT/VSCE_PAT only vscode-extension-publish; no ANTHROPIC_API_KEY; release-attest 0 secrets, 0 vars.
PASS Hosted envs: three envs have branch restrictions, reviewers nedtwigg/edgartwigg, can_admins_bypass false; no hosted creds at repo/org; hosted-preview holds CLOUDFLARE_API_TOKEN/NEON_API_KEY/PREVIEW_AUTH_SECRET (names differ from production's DATABASE_URL/BACKUP_AGE_IDENTITY; values unreadable).
PASS HOSTED_TAG_TOKEN only in hosted-release-tag (API); workflow-usage check below.
PASS tend pin: all 8 at max-sixty/tend/claude@0.3.5 (>=0.1.19). Tag pins only inside tend-*.yaml; every other workflow uses 40-hex SHA pins (grep found none unpinned).
PASS secrets.allowed in .config/tend.yaml lists CHROMATIC_PROJECT_TOKEN and ARGOS_TOKEN.
PASS Renovate: .github/renovate.json packageRule disables github-actions manager for .github/workflows/tend-*.yaml.
PASS workflow-audit.yaml lower bound: previous run created_at (line 89); WINDOW (line 147) includes .github/audit/ and .vscode/.
PASS VS Code release: publish-vscode has environment vscode-extension-publish (release.yml:317); VSCE_PAT/OVSX_PAT referenced only release.yml:347,359 in that job; no production signing secrets in release.yml (only GITHUB_TOKEN); ephemeral Tauri key generated (release.yml:70-81).
PASS Hosted workflows: preview deploy requires same-repo head and needs verify; cleanup checks out refs/heads/main; production tag job needs: deploy whose last step is live smoke; HOSTED_TAG_TOKEN used only in tag job (hosted-production.yml:117) bound to hosted-release-tag.
PASS sign-and-deploy.sh: attestation verify (l.455), sha256 manifest (l.422), PIV jsign (l.763); no --private-key on argv (only a comment l.834); jsign --storepass env:EV_SIGN_PIN. scripts/sign-and-deploy.test.mjs passes.
PASS security-audit.md: security-audit.yaml active, dispatch/watch/needs edge present (release.yml:249-310); scopes in .github/audit/ files cover security.md, security-ci.md, security-audit.md; opus agents for application-security/hosted in claude_args (l.152, --model sonnet l.149) and security-audit-local.sh l.69-70; prompt files all exist; orchestrator until-loop with persisted audit-deadline and sentinel; BASH_DEFAULT_TIMEOUT_MS 600000, timeout-minutes 40; redactor step covers fragments, report, transcript, fails closed (l.211-255); reporting step exact-verdict handling (l.396-402); AUDIT_PAT pre-check present (l.69-76); scripts/security-audit.test.mjs passes; spec-lint OK.
Not individually re-derived in depth: security-audit.md top-level-path coverage by union of qualitative scopes and the per-prompt wording of the reporting/orchestrator FAIL IF bullets were spot-checked via greps and the passing security-audit.test.mjs, not read line by line.
Qualitative findings
INFO: environments tend and release-attest have can_admins_bypass=true; security-ci.md requires disabled bypass only for hosted and vscode-extension-publish, so not a violation.
INFO: .vscode/tasks.json has no runOn/folderOpen task; .claude/settings.json allowlist is narrow and read-only/test commands.
INFO: working tree shows an unrelated modification to lib/src/remote/direct/ice-servers.ts (not from this audit); no finding.
INFO: audit-*.md fragments are written at repo root by this run; they are the audit's intended outputs.
Application security
VERDICT: FAIL
FAIL IF results
PASS: [local/Terminal output] FAIL IF maybe: drop node-pty for a Rust backend #1 clause (a) isKnownUnsupportedIterm2Osc consumes OSC 52 (and 50) — evidence: lib/src/lib/terminal-protocol.ts:912-920 returns true for 52/52;; called at :319 so parseOsc returns []; 50/52 also in OSC_CONSUMED_IDS :106; test lib/src/lib/terminal-protocol.test.ts:248,435
PASS: [local/Terminal output] FAIL IF maybe: drop node-pty for a Rust backend #1 clause (b) every parse site runs TerminalProtocolParser before pty:data leaves it — evidence: standalone sidecar sidecar-entry.ts:185 sends pty:data from createOwnerPtyStream onChunk (processed-pty-stream.ts:87-97 parser.process first); vscode-ext message-router.ts:248 feeds raw data to getOwnerPtyStream and :487 posts only visibleData from onProcessedPtyData; replay via replay-parse.ts:17; fake-adapter.ts:187,468
PASS: [local/Terminal output] FAIL IF Leaky-bucket mechanism for soft-TODOs #2 clause (a) retained parser values bounded and control-stripped — evidence: TITLE_LIMIT=256/BODY_LIMIT=4096 terminal-protocol.ts:91-92 applied via sanitizeText (osc-sanitize.ts:17 strips C0/C1, clamps by code point) at :337,371-372,451-452,900,997; MAX_CWD_LENGTH=4096 with boundedCwdValue stripping [\x00-\x1f\x7f-\x9f] terminal-state.ts:768-789 used at :252,261,265,279,697
PASS: [local/Terminal output] FAIL IF Leaky-bucket mechanism for soft-TODOs #2 clause (b) COMMAND_LINE_LIMIT binds after unescape with a pre-decode width bound — evidence: commandLineEvents terminal-protocol.ts:721-729 truncateText(encoded, COMMAND_LINE_LIMIT*encodedWidth) then decode then sanitizeCommandLine(...,COMMAND_LINE_LIMIT); encodedWidth 4 for \xNN shell-quoted (:709), 12 for percent-encoded; matches 4x bound for \xNN
PASS: [local/Terminal output] FAIL IF Leaky-bucket mechanism for soft-TODOs #2 clause (c) no new unbounded retained value observed — evidence: OSC99 pending accumulators bounded by OSC99_PENDING_TITLE_LIMIT/BODY_LIMIT via appendLimited terminal-protocol.ts:100-101,438-440; id bounded :1008
PASS: [local/Loopback] FAIL IF origin rewrite (handleRequest) — upstreamRequestHeaders rewrites Origin only when isOwnOrigin (lib/src/host/iframe-proxy.ts:226); foreign Origin left untouched in the spread copy.
PASS: [local/Loopback] FAIL IF origin rewrite (handleUpgrade) — handleUpgrade uses the same upstreamRequestHeaders (lib/src/host/iframe-proxy.ts:462).
PASS: [local/Loopback] FAIL IF Cookie forwarded upstream (HTTP + WS) — delete headers.cookie in shared upstreamRequestHeaders (lib/src/host/iframe-proxy.ts:227), used by both paths.
PASS: [local/Loopback] FAIL IF Set-Cookie downstream (HTTP) — sanitizeResponseHeaders skips set-cookie (lib/src/host/iframe-proxy.ts:399); passThrough and streamHtml both go through it.
PASS: [local/Loopback] FAIL IF Set-Cookie downstream (WS 101 and refused upgrade) — 101 rawHeaders loop skips set-cookie (iframe-proxy.ts:477); refused upgrade goes via passThrough->sanitizeResponseHeaders (iframe-proxy.ts:488-492).
PASS: [local/Loopback] FAIL IF Host check dropped — isLoopbackHost(req.headers.host, grant.port) gates handleRequest (iframe-proxy.ts:232, 421) and handleUpgrade (iframe-proxy.ts:456, socket.destroy).
PASS: [local/Loopback] FAIL IF framing headers dropped without exact replacement — FRAMING_RESPONSE_HEADERS dropped only when embedderOrigins!==null and then frameAncestorsCsp(chain) = frame-ancestors self + chain appended (iframe-proxy.ts:405-415; iframe-proxy-rewrite.ts:84-86); preserve-csp only retains stricter upstream CSP and still appends.
PASS: [local/Loopback] FAIL IF shim targets other than own proxy origin + innermost chain origin — send() posts only to location.origin and TARGET (iframe-proxy-rewrite.ts:119), TARGET = embedderOrigins[0] = webview location.origin (iframe-proxy.ts:268, lib/src/lib/embedder-origins.ts).
PASS: [local/Loopback] FAIL IF no-chain case injects or strips — normalizeEmbedderOrigins all-or-nothing returns null (iframe-proxy-rewrite.ts:67-77); replaceFraming false keeps headers; streamHtml skipped when embedder undefined (iframe-proxy.ts:273); error page instrumented only with chain (iframe-proxy.ts:519-524).
PASS: [local/Loopback] FAIL IF foreign Origin refreshes idle timer / absent must refresh — guarded by !isForeignOrigin on both paths (iframe-proxy.ts:245, 460); isForeignOrigin false for absent Origin (lib/src/host/loopback-guard.ts:79-81); Host refusal precedes refresh.
PASS: [remote/Network posture] deploy-lint, deploy-lint-selftest, installer-verify-test, ps1-cmdlet-lint all run green — evidence: deploy-lint OK (3 installers, 35 rules, 93 checks); selftest OK (124 load-bearing checks); installer-verify-test OK (64 checks); ps1-cmdlet-lint OK (677 calls)
PASS: [local/Terminal output] FAIL IF Fix terminal spawning #3 clause (a) no OSC 8 activation reaches openExternal without the dialog — evidence: linkHandler.activate terminal-lifecycle.ts:156-161 calls only activateTerminalLink; terminal-link-activation.ts:35-37,51 routes every non-preview link and every refused preview to requestExternalLinkConfirmation; the only dialog-side openExternal is ExternalLinkModalHost.tsx:26 inside confirm; other openExternal callers (ExternalTextLink.tsx:19, Wall.tsx:2092 port menu) are not OSC 8 paths
PASS: [local/Terminal output] FAIL IF Fix terminal spawning #3 clause (b) dialog renders no open action for a deceptive verdict — evidence: ExternalLinkModal.tsx:106-127 deceptive branch renders only Close + Copy deceptive URL, initialFocusRef=primaryButtonRef (copy) at :81; classifyDisplayMatch external-links.ts:70-83 returns deceptive when URL-shaped display host differs; host also refuses: ExternalLinkModalHost.tsx:25 requires verdict !== deceptive
PASS: [local/Terminal output] FAIL IF Fix terminal spawning #3 clause (c) second normalizeExternalUri pass before opening — evidence: tauri-adapter.ts:477-478, browser-sidecar-adapter.ts:268-269, vscode-ext message-router.ts:608-610 (extension host normalizes, drops on null)
PASS: [local/Terminal output] FAIL IF split a pane and keep the cwd #4 clause (a) preview path only when display text is a whole-component suffix of the decoded target — evidence: localFileLinkPreviewPath external-links.ts:99-119 decodes pathname, rejects controls, and accepts only path===text or path.endsWith(/+text) (one ls -F classifier stripped); activateTerminalLink terminal-link-activation.ts:33-37 sends null to the dialog
PASS: [local/Terminal output] FAIL IF split a pane and keep the cwd #4 clause (b) host opens a file: URL only for empty, localhost, or this machine — evidence: resolveLocalToolTarget tool-input.ts:23 routes scheme input to localFileUrlPath :45-61 which throws unless namesThisHost(url.hostname) :65-70 (empty after WHATWG localhost folding, own hostname, or its short form); webview-side PLAIN_HOSTNAME_RE external-links.ts:106 is only a shape filter
PASS: [local/Loopback] FAIL IF browser viewer upgrades without own loopback Host — upgrade handler requires isLoopbackHost(req.headers.host, port) before consume (lib/src/host/browser-viewer.ts:101-108); plain HTTP always 403.
PASS: [local/Loopback] FAIL IF browser viewer upgrades without single-use 60s grant for one view — BrowserStreamGrants.consume deletes on read, expires now+60_000, 32-byte random token (lib/src/host/browser-stream-guard.ts); path regex /view/[a-f0-9]{64} (browser-viewer.ts:89).
PASS: [local/Loopback] FAIL IF provider receives a webview message not rebuilt — every inbound message goes through parseViewerInput (browser-viewer.ts:332), which builds fresh objects field by field (browser-viewer.ts:476-525) before upstream.input (browser-viewer.ts:344).
PASS: [local/Loopback] FAIL IF host dials agent-browser stream off loopback — viewStream hardcodes ws://127.0.0.1:${port} (lib/src/host/agent-browser-host.ts:560).
PASS: [local/Loopback] FAIL IF host dials CDP off loopback — askCdpEndpoint refuses unless /^ws://(127.0.0.1|localhost):\d+// (agent-browser-host.ts:347); all CDP dials (cdpEndpoint, browserCall) go through it.
PASS: [remote/What crosses the boundary] push registration rejects non-public endpoints (https only, no userinfo, no localhost, non-public IP literals) — evidence: relay/src/push-endpoint.ts:102-113, called at relay/src/app.ts:1115
PASS: [remote/What crosses the boundary] delivery uses createPublicPushAgent/createPublicLookup — evidence: relay/src/push.ts:260 agent=createPublicPushAgent(), passed at :278; push-endpoint.ts:165-166 Agent({lookup: createPublicLookup()})
PASS: [remote/What crosses the boundary] mixed public/blocked DNS answers rejected wholesale — evidence: relay/src/push-endpoint.ts:150 addresses.find(!isPublicNetworkAddress) -> error; empty answer also rejected :146
PASS: [remote/What crosses the boundary] /api/push/send takes burrowId from token — evidence: relay/src/app.ts:1232 const { burrowId } = c.get(burrow) (requireBurrow)
PASS: [remote/What crosses the boundary] send does not select recipients when absent/empty, clamps at MAX_PUSH_QUERY_DELIVERY_IDS — evidence: relay/src/app.ts:1213-1217 (!Array.isArray || length===0 || length>MAX -> 400)
PASS: [remote/What crosses the boundary] read endpoints report only presented delivery ids — evidence: relay/src/app.ts:1145-1173 pushSubscriptionsQuery uses listForDeliveryIds(presented ids); delete always 204 :1180-1187; pushDevices lists ids only to the Burrow token (requireBurrow) :1189
PASS: [local/Loopback] FAIL IF bridge drops application/json on non-GET — enforced in the gate itself (dev-host-guard.mjs:42, isJsonRequest :62-65), so body-less routes are covered.
PASS: [local/Loopback] FAIL IF bridge drops exact-origin ACAO — corsHeaders echoes only one of two vite spellings, else viteOrigin, never * (dev-host-guard.mjs:78-85).
PASS: [local/Loopback] FAIL IF browser-dev Vite permits cross-origin reads or disables Host check — startDevVite sets cors:false and allowedHosts:[] (standalone/scripts/dev-run.mjs:24-49).
PASS: [remote/Trust boundary] FAIL IF Burrow stops being final authority: challenge consumed by Burrow before verify (#challenges.consume) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1357
PASS: [remote/Trust boundary] FAIL IF Burrow stops being final authority: verifyPresenceProof against binding of own burrowId, connectionId, burrowChallenge, handshakeHash — evidence: lib/src/remote/burrow/burrow-runtime.ts:1358-1366
PASS: [remote/Trust boundary] FAIL IF Burrow stops being final authority: one active BurrowAclRecord holds account, credential, key hash, IK client static (#aclRecord -> acl.authorize on credential+static, then accountId and passkeyPublicKeyHash compared on same record) before #promoteConnection; no Relay claim used (accountId/credential come from verified proof, static from IK handshake) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1374-1405, remote-lib-common/src/security/acl.ts:189
PASS: [remote/Trust boundary] FAIL IF BurrowAcl.approve has a caller other than #approvePairing — grep of non-test .approve( on BurrowAcl finds only burrow-runtime.ts:1166 inside #approvePairing (service.ts:802/1019 are PendingPairing.approve closures) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1130-1199
PASS: [remote/Trust boundary] FAIL IF comparison not constant-time / not exactly once: constantTimeEqual after pending.attempted=true is set before comparison — evidence: lib/src/remote/burrow/burrow-runtime.ts:1133-1150
PASS: [remote/Trust boundary] FAIL IF comparison matched against mutable clientId alone: #approvePairing requires pending.pairingId === pairingId (closure-captured, randomBase64Url minted at reserve) — evidence: lib/src/remote/burrow/burrow-runtime.ts:1119,1132,1050
PASS: [remote/Trust boundary] FAIL IF expected code leaves Burrow: PairingQueueItem is exactly {kind, clientId, pairingId, label, requestedAt}; #queueSnapshot projects those five field-by-field — evidence: lib/src/host/remote/service-protocol.ts:110-123, lib/src/host/remote/service.ts:1296-1311
PASS: [remote/Trust boundary] FAIL IF answers not routed by kind / missing kind not pairing: approvalKind returns one-time only for kind===one-time; #pendingRequest routes one-time to #oneTimeApproval by random ticket pairingId — evidence: lib/src/host/remote/service-protocol.ts:95-97, lib/src/host/remote/service.ts:814-826,1008-1013
PASS: [remote/Trust boundary] FAIL IF clientId unbounded at frame boundary / failed handshake allocates: isBoundedString(MAX_CLIENT_ID_LENGTH) on client-gone and isE2eRelayToBurrowFrame before enqueue; #onPairingInit catch returns before #clientState — evidence: lib/src/remote/burrow/burrow-runtime.ts:908,921,1017-1021
PASS: [remote/Credentials at rest] AES-GCM confined to the Pocket at-rest wrapper — evidence: grep -rli gcm over remote-lib-common/src, lib/src, relay/src (non-test) returns only lib/src/remote/client/pocket-private-key.ts
PASS: [remote/Credentials at rest] relay state dir created 0o700 — evidence: relay/src/state.ts:175 mkdir(stateDir,{recursive:true,mode:0o700}) in writeAtomic
PASS: [remote/Credentials at rest] every relay state file written via writeAtomic at 0o600 — evidence: relay/src/state.ts:177 writeFile(tmp,...,mode:0o600)+rename; grep writeFile/appendFile/createWriteStream over relay/src finds only state.ts:177 and runtime-file.ts:39 (runtime file is DORMOUSE_RUNTIME_FILE under run/, not state dir, also 0o600 atomic)
PASS: [remote/Credentials at rest] relay retired hosts.json deleted unread at boot — evidence: relay/src/state.ts:404-406 rm(hosts.json,{force:true}); called relay/src/start.ts void forgetRetiredState(stateDir); pinned relay/test/state-records.test.mjs:139
PASS: [remote/Credentials at rest] start.ts obtains setup password from SetupPasswordStore.loadOrCreate(generateSetupPassword) — evidence: relay/src/start.ts:48
PASS: [remote/Credentials at rest] readConfig reads no DORMOUSE_SETUP_PASSWORD or other setup-password input — evidence: grep SETUP_PASSWORD/setupPassword in relay/src/config.ts returns nothing; env reads at config.ts:96-139 are PORT, BIND_HOST, ORIGIN, STATE_DIR, POCKET_DIR, VAPID_*, RUNTIME_FILE, ENROLL_TOKEN_FILE, RELEASE_ID, REQUIRE_USER_VERIFICATION
PASS: [remote/Credentials at rest] SetupPasswordStore refuses persisted or generated values outside 64 lowercase hex — evidence: relay/src/state.ts:286-295 isStoredSetupPassword uses isSetupPassword (HEX_ENCODED_32_BYTES_PATTERN /^[0-9a-f]{64}$/, remote-lib-common/src/remote/enroll-offer.ts:29); loadRecord throws CorruptStateError (state.ts:224); generated mint validated before write (state.ts:245-247); start.ts exits 1 on CorruptStateError
PASS: [remote/Credentials at rest] createApp refuses a non-64-lowercase-hex setup password — evidence: relay/src/app.ts:490-495 throws unless isSetupPassword(config.setupPassword)
PASS: [remote/What crosses the boundary] push sealed per recipient to that ACL record own Client static — evidence: lib/src/remote/burrow/push-delivery.ts:172-175 loop over records, deps.seal(record.clientStaticPublicKey, plaintext); one envelope per deliveryId
PASS: [remote/What crosses the boundary] fresh salt per message, key minted per message (HKDF over X25519), zero nonce spent once; no CipherState/group key — evidence: remote-lib-common/src/security/push-seal.ts sealPush getRandomValues(32) -> sealKey -> chacha20poly1305(key, ZERO_NONCE); test remote-lib-common/test/push-seal.test.mjs:80-105,204
PASS: [remote/What crosses the boundary] BurrowRuntime.sealPushForClient hands push-delivery a seal capability, not the private key — evidence: lib/src/remote/burrow/burrow-runtime.ts:451-465; lib/src/host/remote/service.ts:1350 seal closure; AlertPushDeps.enrollment typed Pick<relayUrl|burrowToken> push-delivery.ts:89 (runtime object is the full enrollment, see qual INFO); key imported nonextractable remote-lib-common/src/security/noise.ts:321-326
PASS: [remote/What crosses the boundary] the Pocket service worker is the only openPush caller — evidence: grep openPush( in lib/src and remote-lib-common/src (non-test) finds only lib/src/remote/pocket-app/sw.ts:89 and the definition push-seal.ts:144
PASS: [remote/What crosses the boundary] push text bounded with boundedPushText on the Burrow before sealing — evidence: lib/src/remote/burrow/push-delivery.ts:84 toPushText, :166-170 title/tag bounded before utf8Encode and seal
PASS: [remote/What crosses the boundary] push text re-bounded with boundedPushText in sw.ts before showNotification — evidence: lib/src/remote/pocket-app/sw.ts:102-110 title/body/tag via boundedPushText; showNotification at :148 receives only that output or GENERIC
PASS: [local/Terminal output] FAIL IF UI improvements #5 clause (a) OSC 367 open never reaches surface.tool from replay — evidence: parseReplay lib/src/lib/platform/replay-parse.ts:16-21 calls only recordToolEvents (tool-events.ts:23-29 handles announce/state/commandStart, never toolOpen); dispatchToolOpens reached only from applyLiveToolEvents tool-events.ts:33-36, called from live terminal:toolEvents handlers vscode-adapter.ts:126, tauri-adapter.ts:157, browser-sidecar-adapter.ts:332, fake-adapter.ts:477
Truncated to fit: the full body is 121381 characters. The untruncated audit-report.md is in this run's audit-transcript artifact (download).
Audit failed at 2026-10-01T11:05Z. Run · Transcript
FAIL. audit-application.md opened withVERDICT: FAIL— read that domain's section first. A domain's own verdict outranks the merged one.Lines that decided this verdict
Lifted out of the fragments so truncation cannot cut them. Each domain's full section follows for as far as the body reaches.
audit-supply-chain.md: VERDICT: PASSaudit-ci-secrets.md: VERDICT: PASSaudit-application.md: VERDICT: FAILaudit-hosted.md: VERDICT: PASSaudit-application.md: - FAIL: [local/Loopback] Lint-coverage clause (scans all tracked JS/TS and prints every bind; new unguarded non-test listener fails the build) — hosted/server/dev.ts:45server.listen(Number(process.env.PORT || 0), "127.0.0.1")is a loopback bind the node-positional BIND_FORM (\.listen\(\s*[^,)]+,) cannot see because[^,)]+stops at the inner); node scripts/loopback-lint.mjs output omits it. Listener itself is guarded, so impact is lint drift, not exposure.audit-application.md: - FAIL: [remote/One-time connection] Local networks: the offer the Burrow applies CAN keep a candidate outside the allowed networks — evidence: lib/src/host/remote/local-networks.ts CANDIDATE_LINE=/^a=candidate:\S+ \S+ \S+ \S+ (\S+) / and keepAllowed keeps every non-matching line; split on CRLF only when present. Verified: a candidate with a doubled space or tab before the address, or one after a bare LF inside a CRLF SDP, does not match (line kept), and node-datachannel 0.33.4 setRemoteDescriaudit-application.md: - FAIL: [remote/Network posture] (low impact) Tailscale CLI output piped into head -1 for a verify decision on macOS and Linux — evidence: deploy/local/install-macos.sh:902 and deploy/local/install-linux.sh:1131 tsip="$(ts ip -4 2>/dev/null | head -1 || true)"; the plaintext-reachability verdict is reached from that pipe rather than from captured text. The trailing || true absorbs a 141 and ts ip -4 output is tiny, so no wrong verdict is reachable today, but the rule text forbids the shape; Wiaudit-application.md: - WARNING: [local/Terminal output] OSC 367 open / Tool designation gate trusts a PTY-forgeable command line. use-dor-control.ts:1110-1116 admits oscOpen when getTerminalPaneState(id).currentCommand.rawCommandLine === tool.command, but rawCommandLine comes from OSC 633;E (terminal-protocol.ts:814-823, the VS Code nonce field is discarded, never checked) or OSC 133;C cmdline, then any OSC 633/133;C commandStart (terminal-state.ts:181-195, :446-452) — all accepted from any program output. A lateraudit-application.md: - WARNING: [local/Loopback] scripts/loopback-lint.mjs misses a real loopback bind: hosted/server/dev.ts:45server.listen(Number(process.env.PORT || 0), "127.0.0.1")— the node-positional BIND_FORM\.listen\(\s*[^,)]+,stops at the inner)of Number(...), so the lint output omits the file. The listener is guarded (hosted/server/dev-host-guard.ts allowedDevRequest), but security-local.md claims the lint prints every bind and fails an unguarded new one; any port expression containing a callaudit-application.md: - WARNING: Tailscale CLI output is piped into head -1 for the manage verify plaintext-reachability decision on macOS and Linux (deploy/local/install-macos.sh:902, deploy/local/install-linux.sh:1131 tsip="$(ts ip -4 2>/dev/null | head -1 || true)"), the shape security-remote.md Network posture forbids; Windows captures first (install-windows.ps1:1579-1581). The || true and the one-line output make a wrong verdict unreachable today, but scripts/deploy-lint.mjs does not catch this site (lint green)audit-application.md: - WARNING: [Local networks / One-time + Pocket direct path] localNetworksPath.acceptRemote filters remote candidates with a single-space regex (lib/src/host/remote/local-networks.ts CANDIDATE_LINE and keepAllowed) and splits only on CRLF when present; a candidate line with doubled whitespace/tab before the address, or placed after a bare LF in a CRLF SDP, is kept unfiltered and node-datachannel 0.33.4 accepts it (verified with setRemoteDescription). An approved peer (one-time phone after the codaudit-application.md: - WARNING: External-link validation is a blocklist, not an allowlist: inspectExternalUri (lib/src/lib/external-links.ts:1,38) only rejects javascript:/data:/blob:/about:, so an OSC 8 link printed by untrusted terminal output (e.g.catof a repo file) can carry file:///…, vscode:///…, ms-*: or any OS-registered protocol handler, and after the one-click confirmation the VS Code host passes it to vscode.env.openExternal (vscode-ext/src/message-router.ts:608-611), which hands non-http schaudit-application.md: - WARNING: File-path paste/drop quotes but never rejects C0/DEL in the filename: pasteFilePaths (lib/src/lib/clipboard.ts:61-69) -> shellEscapePath (lib/src/lib/shell-escape.ts:11-31) -> writePasteToPty, whose unbracketed branch (clipboard.ts:50) writes the bytes raw. On a POSIX shell without bracketed paste (dash, bash with enable-bracketed-paste off, many REPLs), dropping a file named e.g. $'x\x15touch /tmp/pwned\n' yields '''x^Utouch /tmp/pwned''' — ^U kills the line including the opeSecurity audit
Supply chain
VERDICT: PASS
FAIL IF results
node website/scripts/generate-deps.jsand exits 1 ongit diff --quiet -- website/src/data/; install precondition is earlier in the job.--versionand errors on mismatch (lines 207-224); only skip is host != target. release.yml standalone matrix (x86_64-unknown-linux-gnu on ubuntu, aarch64-apple-darwin on macos-latest, x86_64-pc-windows-msvc on windows-latest) is all host-native.minimumReleaseAge: nullrule matches pgstencil and @pgstencil/**.Qualitative findings
CI and secrets
VERDICT: PASS
FAIL IF results
on:use outside tend-*.yaml (only a comment, hosted-preview.yml:75).merge: restricted; all 8 tend-*.yamlmerge: restricted.needs: deploywhose last step is live smoke; HOSTED_TAG_TOKEN used only intagjob (hosted-production.yml:117) bound to hosted-release-tag.Qualitative findings
tendandrelease-attesthave can_admins_bypass=true; security-ci.md requires disabled bypass only for hosted and vscode-extension-publish, so not a violation.Application security
VERDICT: FAIL
FAIL IF results
Truncated to fit: the full body is 121381 characters. The untruncated
audit-report.mdis in this run'saudit-transcriptartifact (download).