Skip to content

[security-audit] FAIL on 2026-09-09 #598

Description

@github-actions

Audit failed at 2026-09-09T09:18Z. Run · Transcript

  • 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.

Security audit

Supply chain

VERDICT: PASS

FAIL IF results

Disclosure

  • node website/scripts/generate-deps.js after pnpm install --frozen-lockfile produces no diff in website/src/data/PASS. Ran pnpm install --frozen-lockfile (already satisfied, "Already up to date"), then the generator: "Wrote 55 dependencies to src/data/dependencies-npm.json", "Wrote 13 direct and 478 transitive Cargo dependencies to src/data/dependencies-cargo.json", "Wrote 1 bundled runtime to src/data/dependencies-runtime.json". git status --porcelain -- website/src/data/ and git diff -- website/src/data/ were both empty. Tree left clean, no revert needed.
  • .github/workflows/ci.yml keeps running the generator under the frozen-lockfile precondition and fails on a diff — PASS. Job build-and-test runs pnpm install --frozen-lockfile (line 25), then step "Dependency disclosure is current" (lines 30–44) runs node website/scripts/generate-deps.js and does git diff --quiet -- website/src/data/ || exit 1.
  • The disclosure does not omit a shipped workspace's graph or exclude a shipped package — PASS. Derived from pnpm-workspace.yaml's 10 packages against productDependencyFilters/excludedWorkspacePackages in website/scripts/generate-deps.js: dor→root, dormouse-lib (lib)→root, relay→root, dormouse-standalone (standalone)→root, dormouse-sidecar (standalone/sidecar)→root, dormouse (vscode-ext)→root, canopy→excluded, dormouse-website (website)→excluded, dor-lib-common and remote-lib-common→reachable workspace edges (not named directly, per the spec's own note). assertWorkspaceCoverage (website/scripts/dependency-workspaces.js) enforces this and did not throw during the generator run (an "Unclassified workspace packages" or "Excluded workspace ... is reachable" error would have aborted it with exit code 1). npx vitest run scripts/dependency-workspaces.test.js (run from website/): 1 file passed, 6/6 tests passed.

Bundled runtime

  • Root package.json has devEngines.runtime.version as an exact MAJOR.MINOR.PATCHPASS. {"runtime":{"name":"node","version":"24.18.0","onFail":"download"}}.
  • standalone/src-tauri/build.rs runs --version on the binary and fails the build on mismatch, with the one permitted cross-arch skip — PASS. verify_node_version (lines 191–227) runs node_source --version, compares to read_pinned_node_version's parse of package.json, and returns Err on mismatch; it Ok-returns with a cargo:warning only when host != target (lines 197–205), matching the allowed skip.
  • release.yml's standalone matrix is entirely host-native (no skip is exercised in the release build) — PASS. All three build-standalone matrix entries pair native host/target: ubuntu-22.04x86_64-unknown-linux-gnu, macos-latestaarch64-apple-darwin, windows-latestx86_64-pc-windows-msvc (.github/workflows/release.yml lines 25–36).
  • build-standalone installs the pinned runtime via node-version-file: package.json, and package.json has no volta.node or engines.nodePASS. .github/workflows/release.yml line 48 (and lines 193, 324 for the other jobs) use node-version-file: package.json; grep -n "volta\|\"engines\"" package.json returned nothing.

Cooldown and alerts

  • pnpm-workspace.yaml has minimumReleaseAge: 1440PASS (confirmed present at end of file).
  • .github/renovate.json has npm and cargo in enabledManagers, with minimumReleaseAge package rules for both — PASS. "enabledManagers": ["github-actions", "npm", "cargo"]; packageRules include matchUpdateTypes: patch/minor/major entries scoped to ["npm", "cargo"] at 1/3/14 days respectively.
  • .github/renovate.json has a vulnerabilityAlerts block with minimumReleaseAge set explicitly — PASS. "vulnerabilityAlerts": {"enabled": true, "schedule": [], "minimumReleaseAge": "1 day", "draftPR": false, "labels": [...]}, with an explanatory comment array reiterating why omission would silently drop the cooldown.
  • Secret scanning, push protection, and Dependabot alerts are enabled — PASS (all three clauses individually verified via gh api with GH_TOKEN=$AUDIT_PAT, since the default token got 403): repos/diffplug/dormouse --jq .security_and_analysis"secret_scanning":{"status":"enabled"}, "secret_scanning_push_protection":{"status":"enabled"}; GET /repos/diffplug/dormouse/vulnerability-alertsHTTP/2.0 204 No Content (204, not 404, meaning Dependabot alerts are on).

Qualitative findings

INFO — Newly added/upgraded runtime dependencies since the last successful audit (2026-09-08T08:52 UTC, run 34206861498; the next scheduled run started in parallel with this one). Six Renovate PRs merged same-day: tailwind-variants v3.3.1 (#594), playwright-core v1.63.0 (#593), @tauri-apps/plugin-updater v2.11.0 (#592), hono v4.13.7 (#591), @types/react-dom v19.2.7 (#590), @tauri-apps/plugin-shell v2.3.6 (#589). Checked each: tailwind-variants, @tauri-apps/plugin-updater, hono, @tauri-apps/plugin-shell are production dependencies and each commit carries a matching website/src/data/dependencies-npm.json update (verified via git show --stat); playwright-core (vscode-ext/package.json devDependencies) and @types/react-dom (four packages, all devDependencies) correctly carry no disclosure-file change since they never reach a user's machine. All six are ordinary version bumps (no dependency additions/removals), all pnpm-managed and already reflected in the clean disclosure regeneration above. No BLOCKER or WARNING.

INFO — Lockfile resolutions outside the registry: exactly one, @diffplug/xterm-addon-webgl-sdf at canopy/package.json, resolved as a GitHub Releases tarball (https://github.com/diffplug/xterm.js/releases/download/sdf-v0.20.0-sdf304.0/...tgz) with an integrity: sha512-... pin in pnpm-lock.yaml. canopy is one of the two workspaces the spec explicitly excludes as a root ("Storybook-only rendering lab no shipped build imports"), and this is exactly the fork dependency AGENTS.md's Architecture section documents by name. Confirmed no such non-registry resolution is reachable from any of the six product roots: all 55 disclosed npm entries have ordinary semver-looking version fields (no http/github substrings), and standalone/src-tauri/Cargo.lock shows every one of its 491 non-root packages sourced from registry+https://github.com/rust-lang/crates.io-index (the only package with no source line is the local dormouse crate itself). No BLOCKER or WARNING.

INFO — Install scripts in production dependencies: pnpm-workspace.yaml's allowBuilds allowlist (@swc/core: true, esbuild: true, node-pty: true, sharp: true, and explicit denials @vscode/vsce-sign: false, keytar: false) is being enforced, not just declared. node_modules/.modules.yaml after a frozen-lockfile install shows "ignoredBuilds": [] and "pendingBuilds": [] — no package with a build/install script is sitting untriaged, which is what would happen if a newly bumped dependency introduced one Renovate hadn't been taught about. Both keytar@7.9.0 and @vscode/vsce-sign@2.0.9 are present in the lockfile (transitive, packaging-tool-only) and correctly build-denied rather than absent. No BLOCKER or WARNING.

INFO — Reachable-but-undisclosed check: no gap found. dor-lib-common and remote-lib-common (workspace edges, not named roots) are exercised by the coverage assertion and its pinned test; pnpm-workspace.yaml was not touched since the last audit, so no new workspace package needs classification.

No BLOCKER or WARNING findings. All FAIL IF checks in scope were determined (none UNVERIFIABLE).

CI and secrets

VERDICT: PASS

FAIL IF results

docs/specs/security.md

  • Private vulnerability reporting enabled — PASS. gh api repos/diffplug/dormouse/private-vulnerability-reporting{"enabled":true}.

docs/specs/security-ci.md — GitHub Actions Policies

  • pull_request_target appears only in tend-*.yaml — PASS. Repo-wide grep found exactly one hit: .github/workflows/tend-review.yaml:11. No other .github/workflows/** file uses it.
  • Non-agent-managed workflow effective write permissions restricted to the named exceptions — PASS. release.yml: top-level contents: read; build-standalone/build-vscode jobs add only id-token: write + attestations: write (the named exception); security-audit job adds only actions: write (the named exception); publish-vscode has no job-level block, inheriting contents: read. ci.yml and chromatic.yml each declare only contents: read, no job overrides.

Automated Maintainer (tend)

  • scripts/workflow-audit.test.mjs pins the tend-regen materialization boundary (mode-100644/100755-only, symlink rejection, no unrelated generated files) — PASS. node scripts/workflow-audit.test.mjs: 6/6 tests pass, including "never materializes an unrelated symlink" and "rejects symlink workflow inputs before running the generator."
  • workflow-audit.yaml's lower bound is the previous successful run's server-set created_at, never pusher-controlled — PASS. Read .github/workflows/workflow-audit.yaml: SINCE=$(gh api .../workflows/workflow-audit.yaml/runs?status=success...--jq '.workflow_runs[0].created_at // ""'), falling back only to a fixed 25 hours ago if no prior run exists. No --since derived from commit data.
  • Admin-gating rulesets — PASS, both clauses independently verified via GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/rulesets/16757376 and /16757382:
    • Merge access (16757376): target: branch, conditions.ref_name.include: ["~DEFAULT_BRANCH"], rules: [{type: update}], bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}] — exact match.
    • Tag operations (16757382): target: tag, conditions.ref_name.include: ["~ALL"], rules: [{type: creation}, {type: update}], bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}] — exact match.
  • dormouse-bot holds neither maintain nor admin — PASS. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/collaborators/dormouse-bot/permissionpermission: "write", role_name: "write", permissions: {admin: false, maintain: false, push: true, ...}.
  • Every environment's deployment-branch-policy admits only refs admin-gated by the two rulesets — PASS, checked all four environments individually:
    • release-attest: v* tag only (Tag operations).
    • security-audit: main branch + v* tag (Merge access + Tag operations).
    • tend: main only (Merge access).
    • vscode-extension-publish: v* tag only (Tag operations).
      All match the spec's "Today:" list exactly.
  • Secret inventory placement — PASS, verified with one gh api call per surface:
    • Repo-level (actions/secrets): only CHROMATIC_PROJECT_TOKEN.
    • Org-level (actions/organization-secrets): total_count: 0.
    • security-audit env secrets: AUDIT_PAT, CLAUDE_CODE_OAUTH_TOKEN — exact match.
    • tend env secrets: TEND_BOT_TOKEN, CLAUDE_CODE_OAUTH_TOKEN — exact match.
    • vscode-extension-publish env secrets: OVSX_PAT, VSCE_PAT — exact match.
    • release-attest env: secrets: [], variables: [] — empty as required.
    • No ANTHROPIC_API_KEY visible at repo or org level.
  • CHROMATIC_PROJECT_TOKEN present in .config/tend.yaml secrets.allowed — PASS. File contents: secrets:\n allowed:\n - CHROMATIC_PROJECT_TOKEN.
  • workflow-audit.yaml not missing/disabled, successful run within 48h — PASS. gh api .../actions/workflows shows it active; last 5 runs all success, most recent 2026-09-08T12:06:14Z (~21h before this audit at 2026-09-09T09:00Z).
  • Every tend-*.yaml pins max-sixty/tend at ≥0.1.19 — PASS. All 8 files pin max-sixty/tend/claude@0.2.0 and carry header # Generated by tend 0.2.0.
  • Unpinned action refs confined to tend-*.yaml — PASS. Repo-wide grep for unpinned (@vN/tag-style) uses: lines returned matches only inside tend-ci-fix.yaml, tend-mention.yaml, tend-nightly.yaml, tend-notifications.yaml, tend-review-runs.yaml, tend-review.yaml, tend-triage.yaml, tend-weekly.yaml (actions/checkout@v7, astral-sh/setup-uv@v10.0.1). Every other workflow's uses: lines carry a commit-SHA pin with a version comment (spot-checked release.yml, ci.yml, chromatic.yml, security-audit.yaml, workflow-audit.yaml).
  • Agent-managed workflows' effective GITHUB_TOKEN permissions stay within the allowed set — PASS. Read every job-level (and workflow-level, where no job override exists) permissions: block in security-audit.yaml, workflow-audit.yaml, and all 8 tend-*.yaml files: observed scopes are exactly {contents: write|read, pull-requests: write|read, actions: read, issues: write, id-token: write} — no scope outside the allowed list on any job.
  • default_workflow_permissions is read and can_approve_pull_request_reviews is false — PASS. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/permissions/workflow{"default_workflow_permissions":"read","can_approve_pull_request_reviews":false}.

VS Code Extension Releases

  • vscode-extension-publish environment protections — PASS. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/environments shows nonempty required_reviewers (2 named users), prevent_self_review: true on that protection rule, and top-level can_admins_bypass: false.
  • release.yml's VS Code publish job is bound to vscode-extension-publish, and VSCE_PAT/OVSX_PAT appear nowhere else — PASS. publish-vscode job declares environment: {name: vscode-extension-publish}; repo-wide grep for VSCE_PAT|OVSX_PAT under .github/workflows/ found both references only inside that job (lines 347, 359 of release.yml).
  • release.yml does not use production desktop signing secrets in CI and generates an ephemeral Tauri updater key — PASS. build-standalone runs tauri signer generate --ci --write-keys "$RUNNER_TEMP/tauri-ci-updater.key" --force and exports it as TAURI_SIGNING_PRIVATE_KEY for that job only; repo-wide grep for EV_SIGN_PIN/APPLE_SIGN_PASS under .github/workflows/ returned no hits, and the only TAURI_SIGNING_PRIVATE_KEY hit in workflows is this ephemeral-key assignment.

Desktop Releases

  • scripts/sign-and-deploy.sh verifies GitHub artifact attestations, verifies SHA-256 manifests, and uses PIV-backed Windows signing — PASS. verify_downloaded_artifact() runs gh attestation verify with --cert-identity/--cert-oidc-issuer/--source-ref/--source-digest, and check_sha256_manifest() runs sha256sum -c/shasum -a 256 -c; sign_windows() calls jsign --storetype PIV --storepass env:EV_SIGN_PIN.
  • TAURI_SIGNING_PRIVATE_KEY is env-only and EV_SIGN_PIN is passed to jsign --storepass by environment reference, not literally — PASS. sign_updates() invokes TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" ... tauri signer sign "$bundle" (env prefix, never on the tauri argv), and both jsign calls use the literal string env:EV_SIGN_PIN as the --storepass argument (jsign resolves it from the environment itself), never the PIN's value. APPLE_SIGN_PASS does travel on notarytool's argv — this is the spec's documented, already-accepted known gap, not a new finding.

docs/specs/security-audit.md

  • Every docs/specs/security*.md spec claimed by exactly one domain, no scope names a nonexistent file — PASS. node scripts/spec-lint.mjsspec-lint: OK (33 specs, 65 files checked); its check 16 is exactly this rule, and it passed. Manually cross-checked the 6 non-rationale security*.md files against the three domain **Scope blocks: each appears exactly once.
  • Dedicated application-security subagent, not merged into another domain's context — PASS. .github/audit/application-security.md exists standalone, scoped only to security-local.md/security-remote.md; .github/workflows/security-audit.yaml's --agents JSON defines it as a separate agent from ci-and-secrets/supply-chain.
  • application-security on a stronger model than the mechanical domains, in both CI and local — PASS. security-audit.yaml: top-level claude_args sets --model sonnet, and the --agents JSON overrides application-security alone with "model":"opus". scripts/security-audit-local.sh: run_domain() sets model_args="--model sonnet" unconditionally then overrides to --model opus only when domain = application-security.
  • .github/audit/ has every prompt file the workflow names, and the local runner uses the same files — PASS. All five files (_preamble.md, orchestrator.md, supply-chain.md, ci-and-secrets.md, application-security.md) exist; security-audit-local.sh loops over exactly that filename set at startup and fails closed if any is missing; both CI and local prompts are Read ".github/audit/_preamble.md" and ".github/audit/<domain>.md" pointers, not inlined copies.
  • Union of qualitative scopes covers every top-level path — PASS. ci-and-secrets owns .github/, .config/, .claude/, .vscode/, scripts/, website/public/, plus any secret-touching code; supply-chain owns the dependency graph, lockfile, and all of website/ except website/public/; application-security is defined as the recursive subtraction remainder, covering everything else including .impeccable/ and root files. ls -A of the repo root shows nothing outside this union (node_modules/ is gitignored and untracked, not a scope gap).
  • .github/audit/ and .vscode/ both inside workflow-audit.yaml's diff window — PASS. WINDOW=(.github/workflows/ .config/tend.yaml .github/audit/ .vscode/), and WINDOW_NON_WORKFLOW=("${WINDOW[@]:1}") is derived from it (not hand-duplicated), consumed identically by the commit list, own_changes(), and both classifiers' refusal checks.

Orchestration

  • orchestrator.md requires a non-turn-ending wait via a persisted-deadline Bash until loop, re-issued past the 10-minute cap, under a 25-minute deadline — PASS. §2 of .github/audit/orchestrator.md specifies exactly this, with DEADLINE_FILE="$RUNNER_TEMP/audit-deadline" persisted across re-issues.
  • security-audit.yaml's timeout-minutes: 40 exceeds the orchestrator's 25-minute wait deadline — PASS. Confirmed in the workflow file with an explanatory comment tying the two together.
  • Orchestrator cannot report PASS while a subagent left no fragment — PASS. §4 of orchestrator.md: "If no subagent returned FAIL but any domain returned INCONCLUSIVE, or a fragment is missing, empty, or has no exact verdict line, write no status file at all," landing on MISSING → INCONCLUSIVE, not PASS. security-audit.yaml's reporting step independently re-checks fragment presence and verdict-line exactness regardless of what the orchestrator wrote (defense in depth), and both paths exit non-zero.

Outcomes and reporting

  • Only literal PASS/FAIL honored, three real outcomes — PASS. case "${STATUS:-}" in PASS|FAIL) ;; *) STATUS=MISSING ;; esac in security-audit.yaml.
  • audit-report.md written before audit-status.txt — PASS. orchestrator.md §4 states this explicitly and orders the instructions accordingly.
  • Redact-secrets step covers every published sink and fails closed — PASS. Read the step: it processes $TRANSCRIPT, audit-report.md, and $AUDIT_FRAGMENTS (the three per-domain files, sourced from the job-level AUDIT_FRAGMENTS env var so the redactor and the reporting-step guard loop can't drift), and on any Node error runs rm -f on that exact same file set before exiting 1. node scripts/security-audit.test.mjs → 19/19 pass, including "redaction covers every published sink" and "redactor failure removes every published sink."
  • Reporting step writes one note per condition, not per combination — PASS. Five independent if [ -n "${X:-}" ]; then echo "..." >> "$NOTES"; fi blocks (DISSENTING, MISSING_FRAGMENTS, UNREADABLE_VERDICTS, INCONCLUSIVE_DOMAINS, STATUS_FILE_VERDICT), each appending independently.
  • Both fragment guards run unconditionally, not gated on status — PASS. Both for f in $AUDIT_FRAGMENTS loops run unconditionally before the single escalation block; comments in the file explicitly flag this ("Both loops now run UNCONDITIONALLY").
  • Reporting step accepts only exact VERDICT: PASS, recognizes VERDICT: FAIL as dissent (with or without suffix), flags VERDICT: INCONCLUSIVE, and treats anything else as unreadable; STATUS only literal PASS/FAIL — PASS, confirmed by direct read of the case statements plus node scripts/security-audit.test.mjs's "reporting" test group (10/10 pass, covering exact-match, prefix, whitespace, and combination cases).
  • Redaction/reporting regression suite — PASS. node scripts/security-audit.test.mjs: 19/19 tests pass (0 failing).

Environment and AUDIT_PAT

  • security-audit.yaml dispatch is a full release gate (dispatch + watch + needs:) — PASS. release.yml's security-audit job runs gh workflow run "$workflow" ..., then gh run watch "$run_id" ... --exit-status; publish-vscode declares needs: [build-standalone, build-vscode, security-audit].
  • Verify AUDIT_PAT is provisioned step present, after checkout/install, before the audit step — PASS. Confirmed at that exact position in security-audit.yaml, checking [ -n "$AUDIT_PAT" ] and failing closed (writes FAIL status, exits 1) otherwise.
  • security-audit.yaml itself active and producing successful runs — PASS. gh api .../actions/workflows shows state: active; last several scheduled runs all conclusion: success (one in_progress at check time, which is this run).

Qualitative findings

No BLOCKER or WARNING findings. Scope swept: .github/ (all workflows, .github/audit/), .config/tend.yaml, .claude/ (settings.json, skills, output-styles, commands), .vscode/ (tasks.json, launch.json), scripts/ (spot-checked every script that touches process.env/secrets.), and website/public/ (the Tauri updater manifest and static assets).

  • .vscode/tasks.json has one task (build-dormouse-vscode, a plain pnpm build:vscode shell command) and carries no "runOn": "folderOpen" or any other checkout-triggered automation — matches docs/specs/security-ci.md's statement that no such task exists today. INFO: worth re-checking on every future edit to this file, since adding runOn here is exactly the persistence class this domain watches for and it is not caught by any lint — only by this qualitative pass.
  • .claude/settings.json permission allowlist is narrow and read/build-oriented (agent-browser, npx tsc, magick identify, pnpm test/filtered variants, a preview MCP tool) — nothing that reaches a secret or performs a network write.
  • .claude/skills/* and .claude/commands/release-notes.md are prose/process guidance for the tend bot and release workflow; nothing executes with elevated privilege from these files themselves, and none references a secret value.
  • website/public/standalone-latest.json (the Tauri updater manifest) contains only public update-signature material (Tauri's public-key-verifiable minisign-style signatures, not secrets) and public download URLs — consistent with docs/specs/security-ci.md -> "Desktop Releases": the manifest is generated locally by sign-and-deploy.sh's sign_updates() from the (locally-held) private key, and committing the public signature is expected.
  • Scripts touching process.env/secrets. outside .github/workflows/ (scripts/csp-defaults.mjs, scripts/dogfood-vscode.mjs, scripts/free-dev-port.mjs, scripts/pairing-walkthrough/*.mjs) read only non-secret config/dev-environment variables (ports, CSP policy toggles, dev-harness flags) — no credential material observed.
  • No org-level secret is shared with this repo (confirmed via API, not just spec text), so the "Org-level secrets" re-evaluation obligation in docs/specs/security-ci.md has nothing pending.
  • All redaction/reporting/local-runner/workflow-audit regression suites (scripts/security-audit.test.mjs, scripts/workflow-audit.test.mjs, scripts/clamp-issue-body-selftest.mjs, scripts/spec-lint.mjs) ran clean in this session, giving mechanical (not just textual) confirmation for the checks they pin.

UNVERIFIABLE: none. Every FAIL IF in scope was independently determined via gh api (using AUDIT_PAT where admin scope was required), direct file reads, or running the pinned test scripts.

Application security

VERDICT: FAIL

Domain: application-security. Scope: docs/specs/security-local.md (17 FAIL IF
bullets) and docs/specs/security-remote.md (50 bullets), plus the catch-all
remainder. Run at bb5ff06 on main.

One FAIL IF clause is violateddocs/specs/security-local.md -> "Terminal
output", the bounded-and-control-stripped rule, for CwdState.host. Every other
clause in both specs PASSES. No BLOCKER-rated qualitative finding.

FAIL IF results

Lint gates, run at HEAD: spec-lint OK (33 specs), loopback-lint OK (3
listeners, 1 allowlisted), e2e-lint OK (13 rules, 1366 checks),
e2e-lint-selftest OK (20 load-bearing checks), deploy-lint OK (3 installers,
31 rules, 81 checks), deploy-lint-selftest OK (112 load-bearing checks),
installer-verify-test OK (62 checks), ps1-cmdlet-lint OK (674 calls),
ps1-cmdlet-lint-selftest OK (4 checks). Test suites: remote-lib-common 241
pass, relay 275 pass, lib remote subtree 585 pass (31 files), vscode-ext
171 pass.

docs/specs/security-local.md -> Terminal output (3 bullets)

  • L37 isKnownUnsupportedIterm2Osc still consumes OSC 52PASS.
    lib/src/lib/terminal-protocol.ts:708-717 returns true for 52 / 52; (and
    50 / 50;); reached from parseOsc at :294, which returns [] so the
    sequence never re-enters visibleData (:223-228). 52 and 50 are also in
    OSC_CONSUMED_IDS (:93), so an unterminated one is buffered, not forwarded.

  • L37 every parse site runs TerminalProtocolParser before pty:data leaves it
    PASS, 4 emitters, all covered. VS Code:
    vscode-ext/src/message-router.ts:231-234 routes every raw onData into
    getOwnerPtyStream(id).write(data); the only pty:data post is :532, fed
    from createProcessedPtyStream.onChunk (:288-291). Standalone:
    standalone/sidecar/main.js:46 withholds the data event, which reaches the
    wire only via lib/src/host/remote/sidecar-entry.ts:256-266 ->
    ownerStream(id).parsed.write(chunk) -> :167; a throw in the tap drops the
    chunk rather than emitting it raw (:41-45). lib/src/lib/platform/fake-adapter.ts:456-470
    parses before dataHandlers. vscode-ext/src/processed-pty-streams.ts
    refuses a second parser. The three webview-side new TerminalProtocolParser
    are the one-shot replay parsers the spec sanctions.

  • L38 TITLE_LIMIT bounds every retained title — PASS.
    lib/src/lib/terminal-protocol.ts:79 (256), applied :333, :387, :696, :793.

  • L38 BODY_LIMIT bounds every retained body — PASS. :80 (4096), applied
    :321, :334, :388.

  • L38 sanitizeText strips controls before storage — PASS. :821-828,
    [\x00-\x1f\x7f-\x9f]+ -> space, then truncateText (:834-837) counting
    code points, so no split surrogate; empty reduces to null.

  • L38 COMMAND_LINE_LIMIT binds after the \xNN unescape, 4x before it —
    PASS. :614-617
    sanitizeText(decodeOsc633Value(truncateText(rawCommand, COMMAND_LINE_LIMIT * 4)), COMMAND_LINE_LIMIT);
    COMMAND_LINE_LIMIT = 2048 at :86.

  • L38 MAX_CWD_LENGTH / boundedCwdValue bound and strip every CWD source —
    PASS for path and uri. lib/src/lib/terminal-state.ts:722 (4096) and
    :738-743; applied in cwdFromOsc7 (:266, :275), cwdFromOsc9_9 (:290),
    cwdFromDecodedPath (:679, serving osc633 / osc1337 / process / manual).

  • L38 no retained value stops being bounded and control-stripped —
    FAIL, one value: CwdState.host. See BLOCKER-adjacent detail below.

    cwdFromOsc7 (lib/src/lib/terminal-state.ts:263-286) bounds and strips
    rawUri at :265 and re-bounds the path after its percent-decode at :274
    — but :275 computes host = extractFileUriHost(rawUri) || parsed.hostname,
    and extractFileUriHost (:704-708) regex-slices [^/]* out of the URI and
    then safeDecodeURIComponents it. The percent-decode re-introduces control
    characters after the only strip, and nothing re-sanitizes. Reproduced
    against the shipped predicates:
    file://ok.example?a%1Bb%5D0;PWNED%07 yields
    host: "ok.example?a�b]0;PWNED�", isRemote: true. The URL parser
    rejects controls inside the host, but the ?/# tail is outside the host and
    the regex swallows it to end-of-string.

    Retained and rendered: cwd.host is spliced into pane-header labels at
    terminal-state.ts:340 and :369 and into grouping labels at :850, and is
    a component of cwdIdentity (:326). It is also persisted — the whole
    CwdState is written to the notepad archive on Surface closure and read back
    with a type check only (lib/src/lib/notepad/archive-model.ts:137-140, no
    length bound, no control strip).

    Blast radius is bounded: React renders these as text nodes, so there is no
    markup execution and no escape re-injection into a PTY, and the value does not
    cross a boundary — dor ls sends state.cwd?.path (lib/src/components/Wall.tsx:1085)
    and the remote directory sends pane.cwd?.path
    (lib/src/remote/burrow/directory.ts:33), neither host. So this is header
    and archive-record spoofing/corruption, not code execution. It is nonetheless
    a violation of the rule as written, and the code is internally inconsistent:
    the sibling path on the same line is re-bounded after its decode. One-line
    fix: wrap extractFileUriHost's return in boundedCwdValue.

  • L39 OSC 8 activation cannot reach openExternal without the dialog —
    PASS. lib/src/lib/terminal-lifecycle.ts:210-217 calls
    event.preventDefault() then requestExternalLinkConfirmation; the only
    consumer of that store is lib/src/components/ExternalLinkModalHost.tsx:24-28.
    The other two openExternal callers are app chrome
    (lib/src/components/Wall.tsx:1623, lib/src/components/ExternalTextLink.tsx:19).

  • L39 the dialog renders no open action for a deceptive verdict — PASS,
    all three render branches. lib/src/components/ExternalLinkModal.tsx:109-156:
    isDeceptive (:61) is tested first, so an openable-and-deceptive target
    takes the deceptive arm (Close + copy only, :111-126); branch 2
    (:128-146) is the sole onConfirm wiring; branch 3 (:147-156) is Close
    alone. Initial focus is the copy button when deceptive (:81).

  • L39 the host also rejects a deceptive confirmation — PASS.
    ExternalLinkModalHost.tsx:25-27 requires
    status === 'openable' && verdict !== 'deceptive'.

  • L39 second pass through normalizeExternalUri at every adapter — PASS.
    vscode-ext/src/message-router.ts:636-638, standalone/src/tauri-adapter.ts:419-421,
    standalone/src/browser-sidecar-adapter.ts:229-231.

docs/specs/security-local.md -> Browser panes (2 bullets)


Truncated to fit: the full body is 100009 characters. The untruncated audit-report.md is in this run's audit-transcript artifact (download).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions