Skip to content

[security-audit] INCONCLUSIVE on 2026-09-22 #747

Closed

Description

@github-actions

Audit reached no usable verdict at 2026-09-22T09:33Z. This is not a security finding: the run ended without deciding. Run · Transcript

  • A domain could not determine every check. audit-application.md returned VERDICT: INCONCLUSIVE; read its UNVERIFIABLE checks. Those checks do not count as passing.
  • The audit wrote no verdict. audit-status.txt was absent, empty, or not PASS/FAIL, so the run ended without deciding. Start at the report's own markers: UNVERIFIABLE is a check the agent could not determine, _Incomplete …_ heads a domain that was cut off mid-report, and a domain section reading _No report …_ is one that never reported — which is what an expired wait deadline looks like.

Security audit

Supply chain

VERDICT: PASS

FAIL IF results

  • PASS: node website/scripts/generate-deps.js run against a clean tree after pnpm install --frozen-lockfile produces no diff in website/src/data/dependencies-{npm,cargo,runtime}.json (66 npm deps, 13 direct + 478 transitive cargo deps, 1 runtime dep; verified twice, before and after a fresh frozen install). Disclosure -> FAIL IF maybe: drop node-pty for a Rust backend #1.
  • PASS: .github/workflows/ci.yml "Dependency disclosure is current" step (lines 39-46) runs pnpm install --frozen-lockfile (line 25) then node website/scripts/generate-deps.js, and exit 1s on git diff --quiet -- website/src/data/ failing. Disclosure -> FAIL IF Leaky-bucket mechanism for soft-TODOs #2.
  • PASS: pnpm-workspace.yaml -> "assertWorkspaceCoverage" ran successfully as part of the generator (it throws on any unclassified/missing workspace package) and manual cross-check confirms all 11 pnpm-workspace.yaml packages are accounted for: 6 product roots (dor, dormouse/vscode-ext, dormouse-standalone/standalone, dormouse-lib/lib, dormouse-sidecar/standalone/sidecar, relay), 3 explicit exclusions (canopy, dormouse-website/website, dormouse-hosted/hosted — confirmed none of the three is depended on by any other workspace package via grep for their package names as dependency values), and 2 workspace-edge-only packages (dor-lib-common, remote-lib-common) reachable transitively from roots. dor and dormouse-lib are correctly listed as independent roots even though dor's files reach disk via scripts/stage-dor-cli.mjs (docs/specs/dor-cli.md -> "Bundling And PATH") rather than an npm dependency edge — confirmed neither standalone/package.json nor vscode-ext/package.json lists dor as a dependency. Confirmed standalone/src-tauri/tauri.conf.json's bundle.resources: ["../sidecar/**/*"] is the sidecar's route onto disk. Disclosure -> FAIL IF Fix terminal spawning #3 (root-completeness).
  • PASS: root package.json has devEngines.runtime.version = "24.18.0", an exact MAJOR.MINOR.PATCH; no volta.node or engines.node field present. Bundled runtime -> FAIL IF maybe: drop node-pty for a Rust backend #1 and part of Fix terminal spawning #3.
  • PASS: standalone/src-tauri/build.rs's bundle_node_runtime() unconditionally calls read_pinned_node_version + verify_node_version before every bundle (line 42-43), which runs <node> --version and fails the build on any mismatch (lines 207-224); the one deliberate skip is host != target (lines 197-205). Checked .github/workflows/release.yml's build-standalone matrix (lines 26-36): all three entries (ubuntu-22.04/x86_64-unknown-linux-gnu, macos-latest/aarch64-apple-darwin, windows-latest/x86_64-pc-windows-msvc) are host-native for their GitHub-hosted runners, so the skip path is never exercised in the release matrix. Bundled runtime -> FAIL IF Leaky-bucket mechanism for soft-TODOs #2.
  • PASS: build-standalone (release.yml lines 46-48) and build-vscode (lines 191-193) both install Node via actions/setup-node with node-version-file: package.json; root package.json carries no volta.node/engines.node alternate pin. Bundled runtime -> FAIL IF Fix terminal spawning #3.
  • PASS: pnpm-workspace.yaml line 40 sets minimumReleaseAge: 1440. Cooldown and alerts -> FAIL IF maybe: drop node-pty for a Rust backend #1.
  • PASS: .github/renovate.json line 3 enabledManagers includes both npm and cargo; lines 122-135 (packageRules) set minimumReleaseAge for patch (1 day), minor (3 days), and major (14 days) update types matching both npm and cargo managers. Cooldown and alerts -> FAIL IF Leaky-bucket mechanism for soft-TODOs #2.
  • PASS: .github/renovate.json has a vulnerabilityAlerts block (lines 8-21) that explicitly sets "minimumReleaseAge": "1 day" (line 18) rather than omitting the key. Cooldown and alerts -> FAIL IF Fix terminal spawning #3.
  • PASS (evidence via GH_TOKEN=$AUDIT_PAT): gh api repos/diffplug/dormouse --jq .security_and_analysis reports secret_scanning.status: enabled and secret_scanning_push_protection.status: enabled; gh api repos/diffplug/dormouse/vulnerability-alerts -i returns HTTP/2.0 204 No Content (Dependabot alerts enabled, not the 404 that would indicate disabled). Cooldown and alerts -> FAIL IF split a pane and keep the cwd #4 (all three clauses).

Qualitative findings

  • INFO: Two file-tarball, non-registry overrides in pnpm-workspace.yaml (pgstencil: file:vendor/pgstencil-0.1.0.tgz, @pgstencil/auth: file:vendor/pgstencil-auth-0.1.0.tgz), pinned by sha256 in vendor/build.json. Traced with pnpm why: both resolve only into dormouse-hosted (hosted/package.json dependencies), which is correctly excluded from disclosure per docs/specs/security-supply-chain.md -> "Disclosure" (dormouse-hosted — "no installed desktop or selfhost artifact imports it") and per docs/specs/security.md ("Only the self-hosted remote-control deployment ships. Hosted account code is implemented with production provisioning pending"). No FAIL today. Worth a forward note: pgstencil@0.1.0's own dependencies (not devDependencies) pull in testcontainers -> dockerode -> ssh2/cpu-features/protobufjs (all correctly absent from the npm disclosure since they never reach a product root), which is unusual for a library's runtime dependency tree and worth revisiting when Hosted goes to production and hosted potentially becomes a disclosed root.
  • INFO: One GitHub-Releases-tarball (non-registry) resolution in pnpm-lock.yaml: @diffplug/xterm-addon-webgl-sdf@0.20.0-sdf304.0 (tarball: https://github.com/diffplug/xterm.js/releases/download/...), the project's own xterm.js fork per AGENTS.md -> canopy. Traced with pnpm why: depended on only by canopy, which website/scripts/generate-deps.js's excludedWorkspacePackages and docs/specs/security-supply-chain.md -> "Disclosure" both correctly exclude ("a Storybook-only rendering lab no shipped build imports"). No FAIL; this is the one first-party non-registry package and it never reaches a user's disk.
  • INFO: node-pty is the only production (shipped) dependency in the graph carrying allowBuilds: true in pnpm-workspace.yaml (native addon, required for the sidecar/vscode-ext PTY) and it is correctly present in the current disclosure. Traced every other allowBuilds-listed package (sharp, esbuild, @swc/core, @vscode/vsce-sign, keytar, cpu-features, protobufjs, ssh2, workerd) with pnpm why: all resolve exclusively through devDependencies or through the excluded dormouse-hosted/dormouse-website workspaces (storybook, vsce packaging, wrangler/miniflare, testcontainers), never through a product root's dependencies/optionalDependencies. No production dependency with an install script is undisclosed.
  • INFO (spot check, not exhaustive): sampled recent history via gh pr list/gh pr diff. Two recent feature PRs added genuinely new production dependencies to product-root workspaces — PR dor open: dispatch local files through user Tool associations #669 added picomatch to lib/package.json (root dormouse-lib) and PR Move an authorized Pocket session off the Relay onto a direct WebRTC channel #613 added detect-libc + node-datachannel + five @node-datachannel/* platform optionals to standalone/sidecar/package.json (root dormouse-sidecar) — and both PRs included the regenerated website/src/data/dependencies-npm.json in the same PR; all of picomatch, detect-libc, node-datachannel, and @node-datachannel/darwin-arm64 are present with correct metadata in the current disclosure file. This corroborates the CI gate (Disclosure FAIL IF Leaky-bucket mechanism for soft-TODOs #2) working in practice, not just in principle. Routine Renovate dependency-bump PRs sampled (10+ over the last day of history) either updated website/src/data/dependencies-npm.json in the same PR when the package was disclosure-reachable (e.g. hono, tailwind-merge, yaml) or correctly omitted that update for dev-only/excluded-workspace bumps (wrangler, tsx, @types/node, @tauri-apps/cli, all confined to dormouse-hosted devDependencies or root devDependencies never reaching a product root's runtime graph) — no undisclosed production dependency found in the sample.
  • Caveat: this repository checkout is a shallow, single-commit clone (git log shows only the tip commit), so no local git diff/git log comparison against "the last audit" was possible; the dependency-history review above relies entirely on gh pr list/gh pr diff against GitHub's API rather than local git history.

CI and secrets

VERDICT: PASS

FAIL IF results

  • PASS — security.md: private vulnerability reporting is enabled. gh api repos/diffplug/dormouse/private-vulnerability-reporting → {"enabled":true}.
  • PASS — security-ci.md "GitHub Actions Policies": pull_request_target appears as an on: trigger only in .github/workflows/tend-review.yaml (an exempt tend-*.yaml file); grepped every file under .github/workflows/*.y*ml for the string, the only other hit is a comment in hosted-preview.yml line 75, not a trigger.
  • PASS — security-ci.md "Admin-gating rulesets": GET repos/diffplug/dormouse/rulesets returns exactly two active rulesets. Merge access (id 16757376): conditions.ref_name.include=["~DEFAULT_BRANCH"], rules=[{"type":"update"}] only, bypass_actors=[{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"exempt"}] — sole bypass is admin. Tag operations (id 16757382): conditions.ref_name.include=["~ALL"], rules=[{"type":"creation"},{"type":"update"}], same sole admin bypass actor.
  • PASS — security-ci.md "dormouse-bot permission": GET repos/diffplug/dormouse/collaborators/dormouse-bot/permission → permission:"write", role_name:"write", permissions.admin:false, permissions.maintain:false. Neither maintain nor admin present.
  • PASS — security-ci.md environment ref-admission: enumerated GET .../environments (7 total) and each one's deployment-branch-policies. hosted-preview (excepted by the rule) admits main + refs/pull/*/merge. Every other environment admits only refs covered by the two admin-only rulesets above: release-attest→v* tag; security-audit→main branch + v* tag; tend→main branch; hosted-production→main branch; hosted-release-tag→main branch; vscode-extension-publish→v* tag.
  • PASS — security-ci.md secret placement inventory. Enumerated actions/secrets (repo level: exactly CHROMATIC_PROJECT_TOKEN), actions/organization-secrets (empty), and each of the 7 environments' own secret listings: security-audit→{AUDIT_PAT, CLAUDE_CODE_OAUTH_TOKEN}; tend→{CLAUDE_CODE_OAUTH_TOKEN, TEND_BOT_TOKEN}; vscode-extension-publish→{OVSX_PAT, VSCE_PAT}; release-attest→{} (0 secrets, 0 variables); hosted-preview/hosted-production/hosted-release-tag→{} (0 secrets each — Hosted production credentials are not yet provisioned, consistent with docs/specs/security.md's "production provisioning pending"). No ANTHROPIC_API_KEY anywhere. Matches the spec's normative table exactly.
  • PASS — security.md default_workflow_permissions: gh api repos/diffplug/dormouse/actions/permissions/workflow → {"default_workflow_permissions":"read","can_approve_pull_request_reviews":false}.
  • PASS — security-ci.md non-agent-managed workflow write-permission bound. Read every workflow-level and job-level permissions: block in ci.yml (contents:read only), chromatic.yml (contents:read only), hosted-preview.yml (workflow contents:read; job changes adds pull-requests:read; no job grants write), hosted-production.yml (contents:read throughout, no job overrides), and release.yml (build-standalone/build-vscode jobs: contents:read + the explicitly allowed id-token:write/attestations:write; security-audit job: contents:read + the explicitly allowed actions:write; publish-vscode: no job override, inherits contents:read). No non-agent-managed workflow has an effective write scope outside the two named exceptions.
  • PASS — security-ci.md agent-managed workflow permission bound (contents:write, pull-requests:write, issues:write, id-token:write, actions:read, or any read). Checked security-audit.yaml (contents:read, actions:read, issues:write, id-token:write), workflow-audit.yaml (contents:read, issues:write, actions:read, pull-requests:read), and every tend-*.yaml job-level block (all combinations of contents:read/write, pull-requests:write, actions:read, issues:write). None exceeds the allowed set.
  • PASS — security-ci.md tend version floor: every tend-*.yaml file's generated header and its max-sixty/tend/claude@ pin both read 0.2.13, above the 0.1.19 floor.
  • PASS — security-ci.md unpinned-action bound inside tend-*.yaml: every uses: line in the nine tend-*.yaml files carries an explicit tag (actions/checkout@v7, astral-sh/setup-uv@v10.1.0, max-sixty/tend/claude@0.2.13) — none unpinned (no bare @main/no ref).
  • PASS — security-ci.md pull_request_target restricted to tend-*.yaml: confirmed above, only tend-review.yaml (exempt) uses it.
  • PASS — security-ci.md Renovate cannot update tend-*.yaml: .config/tend.yaml's... correction, .github/renovate.json's packageRules includes {"matchManagers":["github-actions"],"matchFileNames":[".github/workflows/tend-*.yaml"],"enabled":false}.
  • PASS — security-ci.md CHROMATIC_PROJECT_TOKEN in secrets.allowed: .config/tend.yaml → secrets.allowed: [CHROMATIC_PROJECT_TOKEN].
  • PASS — security-ci.md workflow-audit.yaml liveness: state: "active"; latest successful run 2026-09-21T14:00:40Z, ~19h before the current audit time (2026-09-22T09:22:50Z UTC) — within 48h.
  • PASS — security-ci.md workflow-audit.yaml gap-resistant window: read the workflow source — SINCE is drawn from gh api .../workflow-audit.yaml/runs?status=success created_at (server-set), never from a pusher-controlled field; the fallback (25 hours ago) only applies when there is no prior successful run.
  • PASS — security-ci.md .github/audit//.vscode/ diff-window coverage: WINDOW=(.github/workflows/ .config/tend.yaml .github/audit/ .vscode/) is the single array; the commit list (git log ... -- "${WINDOW[@]}"), own_changes(), and both classifiers' WINDOW_NON_WORKFLOW=("${WINDOW[@]:1}") refusal all derive from it.
  • PASS — security-ci.md Hosted Deployments environment protections. GET /environments shows hosted-preview, hosted-production, hosted-release-tag all have can_admins_bypass:false and nonempty required_reviewers (nedtwigg, edgartwigg); branch policies restrict each to main (hosted-preview also admits refs/pull/*/merge, expected for PR previews per the workflow's own fork/ready checks).
  • PASS — security-ci.md HOSTED_TAG_TOKEN placement: grep -rn HOSTED_TAG_TOKEN .github/ shows exactly one reference, hosted-production.yml's tag job, which runs in the hosted-release-tag environment; no repo/org-level secret listing contains it (all Hosted secrets are unprovisioned — 0 secrets in all three Hosted environments and at repo/org level, consistent with docs/specs/security.md's "production provisioning pending").
  • PASS — security-ci.md Hosted preview/production workflow-graph bounds: hosted-preview.yml's deploy and cleanup jobs both gate on github.event.pull_request.head.repo.full_name == github.repository (fork exclusion); deploy has needs: verify; cleanup checks out ref: refs/heads/main, never the PR ref; hosted-production.yml's tag job has needs: deploy, and deploy's last step (Verify live production revision and auth boundary) runs before that job's outputs are consumed.
  • PASS — security-ci.md VS Code extension release gating: vscode-extension-publish environment has nonempty required reviewers, prevent_self_review:true, can_admins_bypass:false (from the environments API dump above). release.yml's publish-vscode job carries environment: {name: vscode-extension-publish}; grep -rn "VSCE_PAT|OVSX_PAT" .github/workflows/ shows both secrets referenced only inside that job.
  • PASS — security-ci.md release.yml desktop-signing-in-CI bound: grep for secrets.TAURI_SIGNING_PRIVATE_KEY/secrets.APPLE_SIGN_PASS/secrets.EV_SIGN_PIN across .github/workflows/ returns nothing; release.yml's "Generate ephemeral Tauri updater key" step calls tauri signer generate --ci and writes the key to a $RUNNER_TEMP file, never a repo secret.
  • PASS — security-ci.md sign-and-deploy.sh three-part bound: check_gh_attestation_support + gh attestation verify (attestations), check_sha256_manifest (SHA-256 manifests), and jsign --storetype PIV (PIV-backed Windows signing) are all present in scripts/sign-and-deploy.sh.
  • PASS — security-ci.md signing-credential argv bound: TAURI_SIGNING_PRIVATE_KEY is passed only via an env-var prefix (TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" ... tauri signer sign), never on the tauri signer sign argv; EV_SIGN_PIN reaches jsign only via --storepass env:EV_SIGN_PIN (a reference), never a literal value on argv. (APPLE_SIGN_PASS remains argv-only, which is the spec's own accepted/known gap, not a new finding.)
  • PASS — security-audit.md release-gate wiring: .github/workflows/security-audit.yaml exists, is not disabled, and is triggered by schedule+workflow_dispatch; release.yml's security-audit job does gh workflow run security-audit.yaml then gh run watch "$run_id" --exit-status; publish-vscode declares needs: [build-standalone, build-vscode, security-audit].
  • PASS — security-audit.md domain-ownership: node scripts/spec-lint.mjs → spec-lint: OK (35 specs, 69 files checked), which enforces every docs/specs/security*.md is claimed by exactly one domain's **Scope block and that every named scope file exists.
  • PASS — security-audit.md dedicated application-security subagent: .github/audit/orchestrator.md §1 spawns exactly three Task-tool subagents (supply-chain, ci-and-secrets, application-security) concurrently in one message; application-security.md is a separate file, not merged into either mechanical domain's prompt.
  • PASS — security-audit.md model floor: .github/workflows/security-audit.yaml's claude_args sets --model sonnet as the session default and overrides application-security to "model":"opus" inside --agents; scripts/security-audit-local.sh's run_domain() sets model_args="--model sonnet" and overrides to "--model opus" only for application-security. Both agree.
  • PASS — security-audit.md prompt-file/local-runner consistency: .github/audit/ contains _preamble.md, orchestrator.md, supply-chain.md, ci-and-secrets.md, application-security.md — every file security-audit.yaml's --agents and the orchestrator prompt name; scripts/security-audit-local.sh loops over the same five basenames and errors out if any is missing.
  • PASS — security-audit.md domain qualitative-scope coverage: application-security.md states it is "the catch-all domain... defined by subtraction, not by a list" over what supply-chain.md and ci-and-secrets.md explicitly claim, and instructs ls -A recomputation rather than trusting any stale enumeration — this makes full top-level coverage structural rather than a maintained list.
  • PASS — security-audit.md orchestrator wait/merge/verdict mechanics, verified by reading .github/audit/orchestrator.md and running node --test scripts/security-audit.test.mjs (41/41 passing) and node --test scripts/workflow-audit.test.mjs (6/6 passing): non-turn-ending Bash until loop with a persisted $RUNNER_TEMP/audit-deadline, self-breaking under the Bash cap (BASH_DEFAULT_TIMEOUT_MS: "600000" in security-audit.yaml, above the loop's 540s break); orchestrator prompt forbids ending the turn before audit-report.md exists; merge/verdict treat the <!-- END OF REPORT --> sentinel, not fragment existence, as "reported."
  • PASS — security-audit.md reporting-step guards (read .github/workflows/security-audit.yaml's "Surface result, file or close issue" step and confirmed by the same test suite): the missing-fragment loop and the no-sentinel/verdict-parsing loop both run unconditionally (not gated on $STATUS); issue prose is emitted as one independent note per condition (DISSENTING, MISSING_FRAGMENTS, UNREADABLE_VERDICTS, UNFINISHED, INCONCLUSIVE_DOMAINS, FILE_STATUS=MISSING), never a combination-keyed block; only exact VERDICT: PASS is accepted as passing, VERDICT: FAIL (with any suffix) is recognized as dissent and forces STATUS=FAIL, VERDICT: INCONCLUSIVE and any unrecognized first line are both tracked as non-passing; only literal PASS/FAIL file contents are honored for FILE_STATUS, anything else becomes MISSING.
  • PASS — security-audit.md redaction fail-closed bound: the Redact secrets from agent output step's Node script covers audit-report.md, all three $AUDIT_FRAGMENTS, and the transcript file in one argv list, and its shell wrapper (|| { rm -f ...; exit 1; }) deletes that same sink set on any script error.
  • PASS — security-audit.md AUDIT_PAT provisioning gate: the Verify AUDIT_PAT is provisioned step runs after checkout/setup-node/install and before the audit step, exits 1 and writes FAIL to audit-status.txt when the secret is empty.
  • PASS — private vulnerability reporting: see first entry above (security.md scope).

Qualitative findings

No BLOCKER or WARNING findings in the ci-and-secrets domain (.github/, .config/, .claude/, .vscode/, scripts/, website/public/, and code touching secrets).

  • INFO: .vscode/tasks.json and .vscode/launch.json were inspected for runOn: "folderOpen" or similar checkout-time execution (the risk .github/audit/ci-and-secrets.md calls out by name) — neither file defines any such trigger; tasks.json has one manual build-dormouse-vscode task with no runOptions.
  • INFO: .claude/settings.json grants a small, benign Bash(...) allowlist (test/build/screenshot commands) and defines no hooks block, so nothing there executes automatically on session start or checkout.
  • INFO: eight untracked frag-*.md scratch files and one dormouse.yml sit in the repo root during this run. dormouse.yml is a tracked, legitimate dor tool config unrelated to the audit. The frag-*.md files are untracked working-directory artifacts (matching the application-security domain's per-topic scratch-file naming) from the concurrently running application-security subagent sharing this checkout — not a repository or workflow issue, and outside this domain's scope to characterize further.
  • INFO: release-attest and tend environments have can_admins_bypass:true (unlike the Hosted and VS Code publish environments, which require false). Neither docs/specs/security-ci.md nor docs/specs/security-audit.md states a bypass requirement for these two environments specifically, and release-attest holds no secrets while tend sends its GITHUB_TOKEN-adjacent secrets through admin-gated main-only deployment anyway — so this is not a FAIL IF violation, only a documentation gap worth naming since the spec is explicit about bypass being false everywhere it does state a requirement.

Application security

VERDICT: INCONCLUSIVE

FAIL IF results

Scope: docs/specs/security-local.md, docs/specs/security-remote.md,
docs/specs/security-hosted.md, plus the catch-all remainder of the tree.

Every FAIL IF in scope was decomposed to its individual clauses and each
clause given its own verdict against code, with an absolute path and line
number or command output recorded at the time of determination.
377 clause verdicts: 375 PASS, 0 FAIL, 2 UNVERIFIABLE (both in
security-hosted.md, both external-to-repo by design). The per-clause
evidence lines are grouped below by spec section; the bracketed number is how
many clauses that bullet was decomposed into.

security-local.md — Terminal output / Browser panes / dor control socket (31 clauses, all PASS)

  • PASS — terminal-output/osc52 [4] — isKnownUnsupportedIterm2Osc still consumes OSC 52/50/1337 and every pty:data parse site runs TerminalProtocolParser first — lib/src/lib/terminal-protocol.ts
  • PASS — terminal-output/bounds [9] — TITLE_LIMIT/BODY_LIMIT/COMMAND_LINE_LIMIT/sanitizeText present and applied, COMMAND_LINE_LIMIT binds after the \xNN unescape with a 4x pre-bound; MAX_CWD_LENGTH/boundedCwdValue in lib/src/lib/terminal-state.ts; OSC-incomplete cap 16,384 at terminal-protocol.ts:209-212; OSC 99 pending map capped 64 with a 60 s TTL at :83-84,:426-448
  • PASS — terminal-output/osc8 [6] — linkHandler (lib/src/lib/terminal-lifecycle.ts:164) always routes through the confirmation; classifyDisplayMatch deceptive verdict renders no open action (ExternalLinkModal.tsx), and ExternalLinkModalHost.tsx:24-27 rejects a deceptive confirmation host-side
  • PASS — browser/shim-target [6] — iframeShim/normalizeEmbedderOrigins in lib/src/host/iframe-proxy-rewrite.ts target only the proxy origin plus the validated innermost origin, relay no nested location, no foreign-origin and no unregistered message, and the proxy uses no unvalidated chain
  • PASS — browser/vscode-token [3] — isHostMessage runs before any dispatch in lib/src/lib/platform/vscode-adapter.ts:154-158; token minted per serve and attached only by WebviewChannel.post in vscode-ext/src/webview-messaging.ts
  • PASS — dor/control-dir [5] — ensureControlDir requires real directory, non-symlink, this uid, exactly 0700 before bind (standalone/sidecar/dor-control-server.js:88-96); resolveControlSocketPath yields null otherwise, and both hosts then never export the control env (standalone/sidecar/main.js:86-124, vscode-ext/src/pty-host.js:18-76)
  • PASS — dor/proofs [4] — raw token never written to a socket; both sides use SHA-256-then-timingSafeEqual in proofMatches (dor-control-server.js:19-34, dor/src/control-client.ts:70-78); 24 CSPRNG bytes per host process

security-local.md — Loopback listeners, file viewer, persisted state, Dor Tools (74 clauses, all PASS)

  • PASS — loopback/lint-inventory [1] — node scripts/loopback-lint.mjs green; every non-test listener it prints inspected, plus a manual sweep for createServer / .listen( / serve( / WebSocket. The two non-test listeners outside the named bullets are guarded: scripts/direct-interop/run.mjs:169,181 (isAuthorized + isOwnOrigin, per-run token) and hosted/server/dev.ts:26,37 (allowedDevRequest on request and upgrade)
  • PASS — iframe/origin-rewrite [3] — Origin rewritten to the upstream's own only for own-origin callers, on handleRequest and handleUpgrade; a foreign Origin is forwarded untouched, not blocked
  • PASS — iframe/cookies [4] — Cookie and Set-Cookie stripped on HTTP and on WebSocket handshakes, refused upgrades included
  • PASS — iframe/host-check [2] — Host must name the grant's own port on both paths
  • PASS — frame-ancestors [5] — upstream XFO/CSP replaced with exactly frame-ancestors 'self' <validated chain>, no other source admitted, shim targeted only at the proxy origin and the chain's innermost origin; with no usable chain the upstream headers are preserved and nothing is injected (lib/src/host/iframe-proxy.ts:365-371)
  • PASS — idle-timer [3] — a foreign Origin does not refresh a grant's idle timer; an absent Origin does
  • PASS — stream-relay [5] — grant single-use, TTL-bounded, pinned to one target port, Origin dropped rather than rewritten
  • PASS — browser-dev/gates [6] — all four gates present (per-run token, loopback Host, application/json on every non-GET, exact-origin ACAO), the first three in the pre-routing gate; Vite server denies cross-origin reads of token-bearing modules and keeps its DNS-rebinding Host check
  • PASS — file-viewer [20] — fresh 256-bit URL capability compared by SHA-256 then timingSafeEqual including malformed lengths, case-insensitive loopback Host, absent-or-same-listener Origin, GET/HEAD only, allowsFileViewerRequest gating every route, no foreign CORS, no caching, no capability in a referrer; grant construction permits regular files only, rejects escaping symlinks, bounds static dependency discovery, retains descriptors, and the viewer's CSP is own-origin plus inline script/style and data images
  • PASS — persisted/atomic-writes [5] — write_file_atomically + restrict_to_owner in standalone/src-tauri/src/lib.rs restrict directory and file on every platform arm (0700/0600 unix; one-ACE protected DACL on Windows), mode reaching the temp file before any bytes; callers enumerated from the file
  • PASS — notepad/settings-sync [1] — no setKeysForSync( call anywhere in vscode-ext/src/, so NOTEPAD_ARCHIVE_KEY cannot be opted in
  • PASS — terminal-context-dirs [5] — context arguments validated as existing absolute directories and passed as one canonical argv element with no shell interpretation (standalone/sidecar/pty-core.js, vscode-ext/src/message-router.ts)
  • PASS — dor-tools [14] — repo-local named Tools inert until chrome-granted trust (no trust-grant verb on the control socket, pending approval spawns nothing), inputs stay argv until the renderer quotes them, C0/DEL rejected in argv, substituted argv and local-file targets including after symlink canonicalization, grant key derived in the host, config reads bounded and repo-config symlinks refused

security-remote.md — Trust boundary (70 clauses, all PASS)

  • PASS — burrow-final-authority [10] — #onConnectionTransport consumes its own challenge and verifies with verifyPresenceProof against a binding built from the Burrow's own burrowId, connection id, challenge and handshake hash, then requires one active BurrowAclRecord carrying account + passkey credential + key hash + IK-authenticated static, all before any session — no Relay-supplied claim substitutes for any of them
  • PASS — local-confirmation-mints [4] — BurrowAcl.approve (remote-lib-common/src/security/acl.ts:145) has exactly one production caller, BurrowRuntime.#approvePairing (lib/src/remote/burrow/burrow-runtime.ts:1161-1201); comparison constant-time, single-attempt latch at :1136 so it happens exactly once per ceremony, matched against the immutable pairingId
  • PASS — expected-code-never-leaves [3] — PairingQueueItem is the four-field projection at lib/src/host/remote/service.ts:705-710 and the type at service-protocol.ts:90-97; no invitation private half crosses
  • PASS — pending-maps-bounded [6] — MAX_PENDING_PAIRINGS on both the runtime map and the mirrored service queue with oldest-first eviction, MAX_PENDING_CONNECTION_HANDSHAKES, MAX_TOKENS_PER_BURROW, MAX_CLIENT_ID_LENGTH applied at the frame boundary before any map is touched, and a failed decrypt allocating nothing (burrow-runtime.ts:1020-1024,:1308-1311)

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

Activity

  1. dormouse-bot commented on Sep 22, 2026

    @dormouse-bot
    Collaborator

    No security finding here, and the two undetermined checks are undeterminable rather than unlucky: both are conditions on state outside the repository, so no nightly run can ever settle them while Hosted is unprovisioned. #748 moves them out of the FAIL IF list so the audit can reach a verdict again.

    The application-security fragment returned 377 clause verdicts — 375 PASS, 0 FAIL, 2 UNVERIFIABLE, both in docs/specs/security-hosted.md:

    • The Cloudflare script-injection exclusion, a zone setting on a hostname that does not exist yet. It rides as a trailing sentence on a FAIL IF whose actual condition — no marketing scripts, analytics, provider avatars, or remote fonts in the Hosted frontend — is fully determinable and passed.
    • The closing sentence about production activation, which is not a FAIL IF at all and states its own answer ("checked-in placeholders prove none of them").

    What makes this worth fixing rather than re-running is that the previous night read the same two sentences the other way. In run 35586089654 (2026-09-21) the sub-auditors also returned both as UNVERIFIABLE, and the domain lead resolved both to PASS on the ground that the audited condition is the in-repo half — so the audit passed. Nothing in the tree changed between that night and this one. Until Hosted is provisioned, the release gate publish-vscode depends on closes or opens on which reading a given night produces.

    #746 does not reach it: the new hosted domain inherits the same two clauses and the same "treat a checked-in placeholder as no evidence about an external control" instruction, and an inconclusive domain still blocks a merged pass.

    #748 is a draft and needs review before it lands; every gating check on its head commit settled green. Leaving this issue open until then seems right — a passing audit closes it automatically.

  2. github-actions commented on Sep 23, 2026

    @github-actions
    Author

    Audit reached no usable verdict at 2026-09-23T09:31Z. This is not a security finding: the run ended without deciding. Run · Transcript

    • A domain could not determine every check. audit-ci-secrets.md returned VERDICT: INCONCLUSIVE; read its UNVERIFIABLE checks. Those checks do not count as passing.
    • The audit wrote no verdict. audit-status.txt was absent, empty, or not PASS/FAIL, so the run ended without deciding. Start at the report's own markers: UNVERIFIABLE is a check the agent could not determine, _Incomplete …_ heads a domain that was cut off mid-report, and a domain section reading _No report …_ is one that never reported — which is what an expired wait deadline looks like.

    Security audit

    Supply chain

    VERDICT: PASS

    FAIL IF results

    1. FAIL IF node website/scripts/generate-deps.js changes the three dependency JSON files after pnpm install --frozen-lockfile. PASS — ran the generator against the pre-installed workspace (node_modules already matched the lockfile); git status --short website/src/data/ and git diff --stat website/src/data/ were both empty before and after. Output: "Wrote 66 dependencies...", "Wrote 13 direct and 478 transitive Cargo dependencies...", "Wrote 1 bundled runtime...".

    2. FAIL IF .github/workflows/ci.yml stops running the generator under the install precondition, or stops failing on a diff. PASS — .github/workflows/ci.yml:22-46: pnpm install --frozen-lockfile (line 25) precedes the Dependency disclosure is current step (lines 39-46), which runs node website/scripts/generate-deps.js then git diff --quiet -- website/src/data/ and exit 1 on any diff.

    3. FAIL IF the disclosure omits a shipped workspace's graph or excludes a shipped package. PASS — derived independently from pnpm-workspace.yaml's 11 packages (not the spec's enumeration): six product roots by package name — dor (dor), dormouse (vscode-ext, VS Code extension), dormouse-standalone (standalone), dormouse-lib (lib, named independently since vscode-ext's own dependencies are only node-pty/ws and never reach lib via a workspace edge — confirmed by reading vscode-ext/package.json), dormouse-sidecar (standalone/sidecar), relay (relay); two workspace edges confirmed reachable from those roots via real dependencies entries — dor-lib-common (workspace:* in both dor/package.json and lib/package.json) and remote-lib-common (workspace:* in both lib/package.json and relay/package.json); three exclusions matching excludedWorkspacePackages in website/scripts/generate-deps.js and justified by AGENTS.md's own description of each package's route (or lack of one) onto a user's disk — canopy (Storybook-only lab, not in the production build), dormouse-website (hosted, website, runs in a visitor's browser), dormouse-hosted (hosted, runs on Workers/browser, no installed artifact imports it). 6 roots + 2 edges + 3 exclusions = 11, matching pnpm-workspace.yaml exactly. assertWorkspaceCoverage also ran clean as part of check 1 (no "Unclassified workspace packages" / "reachable from a product root" throw), corroborating the independent derivation.

    4. FAIL IF root package.json is missing devEngines.runtime.version or it is not an exact MAJOR.MINOR.PATCH. PASS — package.json:6-11: devEngines.runtime.version is "24.18.0", exact three-part version.

    5. FAIL IF standalone/src-tauri/build.rs stops verifying the bundled Node binary's --version against the pin, or skips the check for a matrix entry that isn't host-native. PASS — verify_node_version (build.rs:191-227) runs node --version and fails the build (bundle_node_runtime .expect(...) at build.rs:24) on a mismatch; the only skip is if host != target (build.rs:197-205), a cargo:warning plus early return. Checked .github/workflows/release.yml's build-standalone matrix (lines 26-36): ubuntu-22.04/x86_64-unknown-linux-gnu, macos-latest/aarch64-apple-darwin (macos-latest is an Apple Silicon runner), windows-latest/x86_64-pc-windows-msvc — all three are host-native (host == target), so the skip path is never exercised by the release matrix.

    6. FAIL IF build-standalone doesn't install the pinned runtime via node-version-file: package.json, or root package.json gains volta.node/engines.node. PASS — .github/workflows/release.yml:46-48 uses actions/setup-node with node-version-file: package.json in the build-standalone job. package.json (full file read) has no volta or engines key at all — only devEngines.

    7. FAIL IF pnpm-workspace.yaml is missing minimumReleaseAge: 1440. PASS — pnpm-workspace.yaml:44: minimumReleaseAge: 1440.

    8. FAIL IF .github/renovate.json is missing npm/cargo from enabledManagers, or missing minimumReleaseAge package rules for those managers. PASS — enabledManagers: ["github-actions", "npm", "cargo"] (line 3); packageRules includes three rules matching matchManagers: ["npm", "cargo"] with matchUpdateTypes patch/minor/major setting minimumReleaseAge to "1 day"/"3 days"/"14 days" respectively (lines 121-135).

    9. FAIL IF .github/renovate.json has no vulnerabilityAlerts block, or it doesn't set minimumReleaseAge explicitly. PASS — vulnerabilityAlerts block (lines 8-21) sets "minimumReleaseAge": "1 day" explicitly, with an inline comment explaining why omitting it would silently drop the cooldown.

    10. FAIL IF secret scanning or push protection is disabled, or Dependabot alerts are off. PASS — GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse --jq .security_and_analysis returned secret_scanning: {"status":"enabled"} and secret_scanning_push_protection: {"status":"enabled"}; GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/vulnerability-alerts -i returned HTTP/2.0 204 No Content (Dependabot alerts on). (dependabot_security_updates reads "disabled" but that field is unrelated to the alert feed the FAIL IF names.)

    Qualitative findings

    INFO — no new/upgraded runtime dependency of concern since the last audit. Git history in this checkout is a single flattened commit, so I cross-checked against the previous nightly run instead: issue #747 (2026-09-22, INCONCLUSIVE overall but supply-chain sub-verdict PASS) recorded the same counts this run reproduced independently (66 npm deps, 13 direct + 478 transitive cargo deps, 1 runtime dep). gh pr list --state merged --limit 20 shows the only dependency-shaped change since then is #737 "Update cargo to v2.11.6" (mistitled by its Renovate branch name — the diff is standalone/src-tauri/Cargo.lock only, bumping tauri 2.11.5→2.11.6 (patch) and tauri-plugin-updater 2.11.0→2.12.0 (minor), both first-party Tauri crates, both still resolving from registry+https://github.com/rust-lang/crates.io-index per Cargo.lock:3430-3433,3580-3583). Nothing else in the recent merge list (#744 react-router, #743/#740 pnpm, #739 wrangler, #738 tailwind-merge, #735 yaml, #732 hono, #734 tsx) touches a product root's runtime graph in a way the disclosure snapshot doesn't already reflect (snapshot regenerated clean in check 1).

    INFO — three lockfile entries resolve outside their package registry, all correctly kept off a product root's graph. grep -nE "resolution:\s*\{.*(tarball|git|commit)" pnpm-lock.yaml (after discarding one false-positive hash substring match on strip-indent@3.0.0) found: (1) @diffplug/xterm-addon-webgl-sdf resolving to a GitHub Releases tarball on the diffplug/xterm.js fork — declared only by canopy/package.json, and canopy is an explicit, justified exclusion (Storybook-only lab, matches docs/specs/webgl-text.md and AGENTS.md). (2)/(3) pgstencil and @pgstencil/auth, both file:vendor/*.tgz vendored tarballs pinned via pnpm-workspace.yaml's overrides — declared only by hosted/package.json, and dormouse-hosted is an explicit, justified exclusion (runs on Workers/in-browser, no installed artifact imports it). Confirmed via grep -rl that no product-root package.json declares any of the three. Provenance of the vendored pgstencil tarballs is security-hosted.md's concern (the hosted domain), not this one; noting it here only because it's the kind of thing this bullet asks me to surface.

    INFO — pgstencil pulls a testcontainers→dockerode→ssh2/cpu-features/protobufjs chain into dormouse-hosted's real dependencies (not devDependencies). pnpm why cpu-features/ssh2/protobufjs -r all bottom out at pgstencil@0.1.0 (itself a dormouse-hosted dependencies entry, not dev) pulling in testcontainers — normally a test-only tool — as a production edge. Since dormouse-hosted is a correctly-excluded workspace this does not affect the disclosure (no FAIL IF violated), but it does mean pnpm install in that workspace resolves Docker/SSH tooling as "production." Flagging for the hosted domain's awareness rather than rating it here — it's their vendored dependency's own classification, and support for it (or not) is security-hosted.md's call.

    PASS — every install-time script in the resolved dependency graph is accounted for in pnpm-workspace.yaml's allowBuilds allowlist, and only one that is actually allowed reaches a shipped product. Walked every package.json under node_modules/.pnpm/*/node_modules/** for a preinstall/install/postinstall script: exactly 8 packages have one — @vscode/vsce-sign, cpu-features, esbuild (both versions), keytar, node-pty, protobufjs, ssh2, workerd — and every one of those 8 names is present in allowBuilds. Cross-referencing each against the workspace package.jsons with pnpm why <pkg> -r: @vscode/vsce-sign and keytar are transitive devDependencies of vscode-ext via @vscode/vsce/ovsx (packaging tooling, both blocked — false — in allowBuilds so their scripts don't run); cpu-features/ssh2/protobufjs are the pgstencil→testcontainers chain discussed above (all blocked — false); esbuild is a devDependencies entry in dor, standalone, vscode-ext, hosted (bundler tooling, never a shipped dependency, allowed — true); workerd is a transitive devDependencies entry via miniflare/wrangler in dormouse-hosted/dormouse-website (Cloudflare local-dev runtime, allowed — true, never ships). node-pty (allowed — true) is the only one of the 8 that is a real dependencies entry of a product root (vscode-ext and standalone/sidecar), and it is already correctly disclosed (dependencies-npm.json entry node-pty 1.2.0-beta.15, MIT, Microsoft Corporation). pnpm install --frozen-lockfile produced no "ignored build scripts" warning beyond vscode-ext's own (non-third-party) postinstall (chmod +x on the node-pty prebuild). sharp and @swc/core are named in allowBuilds but currently resolve to versions with no install script of their own in this lockfile (sharp ships prebuilt binaries via optionalDependencies, matching the node-datachannel pattern the spec already documents; @swc/core is not presently resolved at all) — a defensive/future-proofing allowlist entry, not a gap.

    PASS — node-datachannel's optional-sibling disclosure mechanism is working as specified. dependencies-npm.json lists all five prebuilt platform packages (darwin-arm64, darwin-x64, linux-arm64-gnu, linux-x64-gnu, win32-arm64-msvc, win32-x64-msvc) plus the core node-datachannel package, all at the identical pinned version 0.33.4 — matching optionalSiblingsAtSameVersion's lockstep guarantee.

    PASS — agent-browser is not declared as a dependency anywhere in the workspace. grep -rn '"agent-browser"' --include=package.json . (excluding node_modules) returned nothing, consistent with the spec's claim that it is "a dependency of nothing here" and resolved off PATH by the user's own global install.

    INFO — website/scripts/ and the website build config carry no build-time network fetch or shell-out beyond the already-accounted-for cargo metadata call in generate-deps.js. Grepped website/scripts/*.js, website/vite.config.ts, website/react-router.config.ts for fetch(, exec*(, spawn(, and URL literals: the only executed subprocess is execFileSync("cargo", ...) in generate-deps.js (already covered above); every other match is a static string literal (GitHub URLs used as data, not fetched).

    CI and secrets

    VERDICT: INCONCLUSIVE

    FAIL IF results

    • PASS: pull_request_target appears only in .github/workflows/tend-review.yaml (agent-managed, exempt) and as a comment mention in hosted-preview.yml:75. No violation in any other workflow. (security-ci.md "GitHub Actions Policies" FAIL IF maybe: drop node-pty for a Rust backend #1)

    • PASS: Every action reference in non-tend-*.yaml workflows (chromatic.yml, ci.yml, hosted-preview.yml, hosted-production.yml, release.yml, security-audit.yaml, workflow-audit.yaml) is pinned by full 40-char commit SHA — grep for uses:\s lines lacking a @[0-9a-f]{40} suffix returned none.

    • PASS: Non-agent-managed workflows' effective write permissions are within the allowed exceptions. release.yml build-standalone/build-vscode: contents: read, id-token: write, attestations: write (environment release-attest). release.yml security-audit job: contents: read, actions: write (explicit exception). publish-vscode job has no job-level override, inherits workflow-level contents: read. chromatic.yml, ci.yml: contents: read only. hosted-preview.yml changes job: contents: read, pull-requests: read (read-only, not a write grant). hosted-production.yml, hosted-preview.yml workflow-level: contents: read; no other job overrides seen. (security-ci.md "GitHub Actions Policies" FAIL IF Leaky-bucket mechanism for soft-TODOs #2)

    • PASS: dormouse-bot collaborator permission is permission: write, role_name: write, admin: false, maintain: false via GET /repos/diffplug/dormouse/collaborators/dormouse-bot/permission. (security-ci.md FAIL IF "dormouse-bot holds maintain or admin")

    • PASS: Merge access ruleset (id 16757376): target branch, conditions.ref_name.include: ["~DEFAULT_BRANCH"], rules: [{"type":"update"}] only, bypass_actors: [{actor_type: RepositoryRole, actor_id: 5, bypass_mode: exempt}] — sole bypass is admin role, blocks nothing beyond update.

    • PASS: Tag operations ruleset (id 16757382): target tag, conditions.ref_name.include: ["~ALL"], rules: [creation, update], bypass_actors sole entry RepositoryRole 5 (admin). Both rulesets match security-ci.md's admin-gating requirement exactly. (security-ci.md FAIL IF "either admin-gating ruleset is missing or weakened")

    • PASS: Repo-level secrets (GET /repos/.../actions/secrets): exactly one, CHROMATIC_PROJECT_TOKEN. Org-level secrets visible to repo (GET /repos/.../actions/organization-secrets): zero. Matches security-ci.md's required placement for both lines.

    • PASS: Environment secret listings match the required placement exactly:

      • security-audit: AUDIT_PAT, CLAUDE_CODE_OAUTH_TOKEN (both, none at repo level)
      • tend: CLAUDE_CODE_OAUTH_TOKEN, TEND_BOT_TOKEN
      • vscode-extension-publish: OVSX_PAT, VSCE_PAT
      • release-attest: 0 secrets, 0 variables (empty environment, confirmed via GET /environments/release-attest/secrets and /variables)
      • No ANTHROPIC_API_KEY at repo or org level (confirmed above)
        (security-ci.md FAIL IF "secret inventory departs from this placement")
    • PASS: VSCE_PAT and OVSX_PAT are referenced only in .github/workflows/release.yml lines 347 and 359, both inside the publish-vscode job (job starts line 310, bound to environment: vscode-extension-publish), and nowhere else under .github/workflows/**. (security-ci.md "VS Code Extension Releases" FAIL IF Leaky-bucket mechanism for soft-TODOs #2)

    • PASS: vscode-extension-publish environment via API: required_reviewers protection rule present with prevent_self_review: true, reviewers nedtwigg + edgartwigg (nonempty), can_admins_bypass: false. (security-ci.md "VS Code Extension Releases" FAIL IF maybe: drop node-pty for a Rust backend #1)

    • PASS: release.yml build-standalone/build-vscode jobs generate an ephemeral Tauri updater key at runtime (tauri signer generate into $RUNNER_TEMP/tauri-ci-updater.key, line 70-81) and set TAURI_SIGNING_PRIVATE_KEY to that ephemeral path. grep -n "secrets\." release.yml shows only GITHUB_TOKEN, VSCE_PAT, OVSX_PAT — no production desktop signing secret (TAURI_SIGNING_PRIVATE_KEY, EV_SIGN_PIN, APPLE_SIGN_PASS) is referenced from secrets.* anywhere in the workflow. (security-ci.md "VS Code Extension Releases" FAIL IF Fix terminal spawning #3)

    • PASS: scripts/sign-and-deploy.sh verifies GitHub artifact attestations (gh attestation verify, lines 440-446, gated by check_gh_attestation_support), verifies SHA-256 manifests (check_sha256_manifest / sha256sum -c, lines 402-411), and uses PIV-backed Windows signing (jsign --storetype PIV, lines 739-762). (security-ci.md "Desktop Releases" FAIL IF maybe: drop node-pty for a Rust backend #1)

    • PASS: TAURI_SIGNING_PRIVATE_KEY is passed only via environment-variable prefix (line 824: TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" pnpm --dir ... exec tauri signer sign "$bundle" — the key itself never appears as a CLI argument). EV_SIGN_PIN is passed to jsign as --storepass env:EV_SIGN_PIN (an env-var reference, not the literal value) at lines 747-749 and 760-762. APPLE_SIGN_PASS is passed via --password "$APPLE_SIGN_PASS" (argv) at line 677 — this matches the spec's own documented weakest-of-three/known-gap characterization, not a new finding. (security-ci.md "Desktop Releases" FAIL IF Leaky-bucket mechanism for soft-TODOs #2)

    • PASS: dormouse-bot collaborator permission (re-confirmed): permission: write, role_name: write; admin: false, maintain: false. (security-ci.md "Automated Maintainer" FAIL IF)

    • PASS: .github/workflows/workflow-audit.yaml's SINCE is derived exclusively from gh api .../actions/workflows/workflow-audit.yaml/runs?status=success&per_page=1 --jq '.workflow_runs[0].created_at' (a GitHub server-set timestamp of the previous successful run), falling back to date -u -d '25 hours ago' only when no prior successful run exists. Nothing pusher-controlled (e.g. committer date) feeds the lower bound. (security-ci.md FAIL IF "workflow-audit.yaml starts deriving its lower bound from anything the pusher controls")

    • PASS: workflow-audit.yaml's WINDOW=(.github/workflows/ .config/tend.yaml .github/audit/ .vscode/) is the single source array; WINDOW_NON_WORKFLOW=("${WINDOW[@]:1}") is derived from it, and all four consumers (COMMITS git log, own_changes(), is_renovate_pin_bump's refusal, is_tend_regen's refusal) reference WINDOW/WINDOW_NON_WORKFLOW rather than a separately hand-maintained list. (security-audit.md FAIL IF ".github/audit/ or .vscode/ is outside any consumer of workflow-audit.yaml's diff window")

    • PASS: workflow-audit.yaml last successful run created_at: 2026-09-22T12:33:14Z; current time 2026-09-23T09:22:41Z — approximately 20h49m elapsed, under 48 hours. Workflow state: active. (security-ci.md FAIL IF "workflow-audit.yaml is missing, disabled, or has not produced a successful run in the last 48 hours")

    • PASS: .config/tend.yaml secrets.allowed contains exactly CHROMATIC_PROJECT_TOKEN. (security-ci.md FAIL IF "CHROMATIC_PROJECT_TOKEN is missing from secrets.allowed in .config/tend.yaml")

    • PASS: .github/renovate.json packageRules contains {"matchManagers":["github-actions"],"matchFileNames":[".github/workflows/tend-*.yaml"],"enabled":false} (line 142-147), disabling Renovate's github-actions manager for tend-*.yaml. (security-ci.md FAIL IF "Renovate's github-actions manager can update .github/workflows/tend-*.yaml")

    • PASS: Every uses: line in all 9 tend-*.yaml workflows carries an explicit version tag (actions/checkout@v7, astral-sh/setup-uv@v10.2.0, max-sixty/tend/claude@0.2.15) — none unpinned (@main/no version). max-sixty/tend/claude is pinned at 0.2.15, above the 0.1.19 floor. (security-ci.md FAIL IF "any tend-.yaml pins max-sixty/tend below 0.1.19" and "any tend-.yaml workflow uses an unpinned action reference")

    • PASS: Every agent-managed workflow's effective GITHUB_TOKEN permissions (workflow- and job-level, grepped across all tend-*.yaml, workflow-audit.yaml, security-audit.yaml) are within {contents: write, pull-requests: write, issues: write, id-token: write, actions: read, contents: read} — no packages, deployments, checks, statuses, discussions, or security-events write grants found anywhere. (security-ci.md FAIL IF "any job in an agent-managed workflow has effective GITHUB_TOKEN permissions beyond...")

    • PASS: GET /repos/diffplug/dormouse/actions/permissions/workflow returns {"default_workflow_permissions":"read","can_approve_pull_request_reviews":false}. (security-ci.md FAIL IF on default_workflow_permissions)

    • PASS: GET /repos/diffplug/dormouse/private-vulnerability-reporting returns {"enabled":true}. (security.md FAIL IF on private vulnerability reporting)

    • PASS: GET /repos/diffplug/dormouse/environments (7 environments) cross-checked against GET .../deployment-branch-policies for each: hosted-preview → main, refs/pull/*/merge; hosted-production → main; hosted-release-tag → main; release-attest → v* (tag); security-audit → main, v* (tag); tend → main; vscode-extension-publish → v* (tag). Every one of these refs is admin-gated by the Merge access (targets ~DEFAULT_BRANCH) or Tag operations (targets ~ALL tags) rulesets, both confirmed sole-bypass-actor admin (RepositoryRole id 5). hosted-preview's refs/pull/*/merge entry is explicitly excepted by the spec. (security-ci.md FAIL IF "any GitHub environment except hosted-preview admits a ref that is not admin-gated")

    • PASS: vscode-extension-publish, hosted-preview, hosted-production, hosted-release-tag environments all have can_admins_bypass: false; hosted-preview/hosted-production/hosted-release-tag each have a required_reviewers protection rule listing nedtwigg and edgartwigg (nonempty), matching "Ned or Edgar's review... administrator bypass disabled" (self-review explicitly allowed for Hosted, confirmed prevent_self_review: false there, consistent with spec text). (security-ci.md "Hosted Deployments" FAIL IF maybe: drop node-pty for a Rust backend #1)

    • PASS: release.yml build-standalone and build-vscode jobs both declare environment: {name: release-attest} (lines 19 and 182), matching the OIDC-token-bounding rationale in security-ci.md; release-attest environment has 0 secrets and 0 variables via API (confirmed above), keeping id-token: write/attestations: write the only credential those jobs can reach. Note: release-attest's own can_admins_bypass is true (unlike the four environments above) — this is not named as a requirement anywhere in security-ci.md for this environment specifically (only vscode-extension-publish and the three Hosted environments have an explicit can_admins_bypass:false requirement), so not recorded as a FAIL IF violation; flagged separately below as a qualitative observation.

    • INFO: hosted-production and hosted-release-tag environments currently have zero secrets configured via the GitHub API, even though hosted-production.yml's deploy job references secrets.DATABASE_URL, secrets.CLOUDFLARE_API_TOKEN, secrets.BACKUP_AGE_IDENTITY, and the tag job references secrets.HOSTED_TAG_TOKEN. This is consistent with docs/specs/security.md's stated status ("Hosted account code is implemented with production provisioning pending") — not a new finding — but it means the FAIL IF HOSTED_TAG_TOKEN appears outside hosted-release-tag and the "production credentials appear in hosted-preview" checks are currently vacuously true (the secrets don't exist anywhere yet, so they cannot appear anywhere) rather than affirmatively verified against real credential placement. Re-check this once Hosted production is actually provisioned.

    • PASS (as far as checkable pre-provisioning): HOSTED_TAG_TOKEN does not appear in any GitHub secret listing at repo, org, or any environment scope today; it is referenced in workflow code only inside the tag job of hosted-production.yml, bound to environment: hosted-release-tag, and nowhere else under .github/workflows/**.

    • PASS: scripts/spec-lint.mjs (check 16) run locally: spec-lint: OK (35 specs, 69 files checked) — confirms every docs/specs/security*.md non-rationale spec (security.md, security-ci.md, security-audit.md, security-hosted.md, security-local.md, security-remote.md, security-supply-chain.md) is claimed by exactly one .github/audit/*.md domain scope, matching the Domains table in security-audit.md exactly. (security-audit.md FAIL IF "a docs/specs/security*.md spec is in no domain's scope, or in two, or a scope names a file that does not exist")

    • PASS: .github/workflows/security-audit.yaml's claude_args sets --model sonnet as the session floor and its inline --agents JSON sets "model":"opus" for both application-security and hosted (not for supply-chain/ci-and-secrets). scripts/security-audit-local.sh's run_domain() sets model_args="--model sonnet" by default and overrides to --model opus only for application-security|hosted. Both CI and local agree. (security-audit.md FAIL IF on model split)

    • PASS: .github/audit/ contains all four domain files named by both the workflow's --agents JSON and security-audit-local.sh's for f in _preamble orchestrator supply-chain ci-and-secrets application-security hosted existence loop: _preamble.md, orchestrator.md, supply-chain.md, ci-and-secrets.md, application-security.md, hosted.md — all present and read successfully. (security-audit.md FAIL IF on missing prompt file)

    • PASS: Union of the four domains' qualitative "Qualitative pass" scope statements covers every top-level repo path (ls -A at root: .claude→ci-and-secrets, .config→ci-and-secrets, .github→ci-and-secrets, .vscode→ci-and-secrets, scripts→ci-and-secrets, website/public→ci-and-secrets explicit carve-out; rest of website/→supply-chain (explicit "all of website/ except website/public/"); root package.json/pnpm-lock.yaml/pnpm-workspace.yaml→supply-chain ("the dependency graph, the lockfile"); hosted→hosted, vendor→hosted; canopy, deploy, docs, dor, dor-lib-common, lib, relay, remote-lib-common, standalone, vscode-ext, .impeccable, and remaining root files (AGENTS.md, CLAUDE.md, etc.)→application-security catch-all by subtraction). No top-level path found with no owner. (security-audit.md FAIL IF on qualitative scope coverage)

    • PASS: .github/workflows/security-audit.yaml — all three release-gate pieces present: gh workflow run "$workflow" ... dispatch (line 282), gh run watch "$run_id" ... --exit-status (line 308), and publish-vscode's needs: [..., security-audit] edge (lines 312-315). (security-audit.md "Schedule and gate" FAIL IF)

    • PASS: timeout-minutes: 40 on the audit job in security-audit.yaml, above the orchestrator's 32-minute wait deadline (.github/audit/orchestrator.md DEADLINE_FILE sets now + 1920 seconds = 32 min). BASH_DEFAULT_TIMEOUT_MS: "600000" (600s) is set above the wait loop's 540-second self-break in both _preamble.md and orchestrator.md.

    • PASS: claude_args includes --allowed-tools "Read,Write,Edit,Bash,Grep,Glob,Task,Agent" and --disallowed-tools "Workflow" — Task/Agent allowed, Workflow denied, matching security-audit.md's Orchestration bullet.

    • PASS: environment: {name: security-audit} on the audit job; confirmed above this environment's deployment-branch-policy admits only main and v* (both admin-gated).

    • PASS: "Verify AUDIT_PAT is provisioned" step (lines 69-77) runs after checkout/setup-node/pnpm-install and before "Audit against the security specs"; it writes audit-report.md+audit-status.txt+FAIL and exits 1 if AUDIT_PAT is empty. (security-audit.md "Environment and AUDIT_PAT" FAIL IF)

    • PASS: "Redact secrets from agent output" step is if: always(), covers $RUNNER_TEMP/claude-execution-output.json, audit-report.md, and $AUDIT_FRAGMENTS (all four fragment filenames, sourced from the single job-level AUDIT_FRAGMENTS env var also used by the artifact-upload and reporting steps), and its trailing || { rm -f ...; exit 1; } deletes every one of those sinks and fails the job if the Node redactor throws. (security-audit.md FAIL IF on the redact step)

    • PASS: "Archive audit transcript" step is if: always(), uploads unconditionally (retention-days: 14), matching "Every run uploads the audit-transcript artifact."

    • PASS: "Surface result, file or close issue" step: (a) writes one NOTES line per condition that holds via independent if blocks, never an if/elif chain over combinations; (b) both fragment-guard loops (MISSING_FRAGMENTS check, and the sentinel/verdict-line check producing UNFINISHED/DISSENTING/INCONCLUSIVE_DOMAINS/UNREADABLE_VERDICTS) run unconditionally, never gated on $STATUS; (c) accepts only the exact string VERDICT: PASS as passing (case statement), recognizes any VERDICT: FAIL* prefix as dissent, treats VERDICT: INCONCLUSIVE and any unrecognized first line as non-passing, and requires the literal last-non-blank-line <!-- END OF REPORT --> sentinel before treating a fragment as finished; (d) parses audit-status.txt accepting only literal PASS/FAIL, defaulting anything else (including missing/empty) to MISSING; (e) escalation is FAIL > MISSING > PASS, in the single block at lines 420-425. All 52 assertions in node --test scripts/security-audit.test.mjs (reporting, redaction, local-runner, orchestrator-wait, merge, workflow-audit regeneration-boundary suites) pass. (security-audit.md "Outcomes and reporting" FAIL IFs, all clauses)

    • PASS: node --test scripts/sign-and-deploy.test.mjs — all 33 tests pass, including "Windows signatures pass the PIV PIN through the environment only".

    • PASS: node --test scripts/workflow-audit.test.mjs (bundled in the security-audit.test.mjs run above) — "accepts exact regeneration...", "never materializes an unrelated symlink...", "rejects symlink workflow inputs...", "does not let gitignore conceal unexpected generated workflows...", "reports a config change even when its workflows reproduce" all pass, backing is_tend_regen's materialization boundary. (security-ci.md FAIL IF "tend regeneration materializes anything except regular .config/tend.yaml and workflow YAML blobs")

    • PASS: .vscode/tasks.json contains one task (build-dormouse-vscode, pnpm build:vscode) with no runOn key anywhere in .vscode/ — confirms security-ci.md's qualitative-pass claim "There is no such task today" is still accurate. .claude/settings.json has no hooks key; no root .mcp.json exists. No checkout-time-executing persistence mechanism found in either directory.


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

  3. dormouse-bot commented on Sep 23, 2026

    @dormouse-bot
    Collaborator

    This run went inconclusive on one clause, and the clause is already gone on #748's head — but the posted report could not say so, because the line that decided it was truncated away.

    What made it INCONCLUSIVE. audit-ci-secrets.md returned one UNVERIFIABLE, against docs/specs/security-ci.md's FAIL IF on Hosted credential placement. GitHub-side placement passed in full; what the domain could not determine is the clause's second half — "inspect… Cloudflare/Neon token scope; names alone do not isolate resources" — which needs the live CLOUDFLARE_API_TOKEN / NEON_API_KEY values the audit correctly never reads. It also recorded that GET /actions/variables and /actions/organization-variables returned 403 Resource not accessible by personal access token, consistent with AUDIT_PAT's documented Administration + Secrets + Environments scope rather than drift from it, so that avenue was closed too.

    This is the third instance of the class #748 is about, and 15c11da2 on that PR already deletes the token-scope sub-clause, leaving - **FAIL IF** Hosted credentials appear at repository/org scope, or production credentials appear in \hosted-preview`; inspect GitHub secret placement.` Tonight's audit should get past it once #748 lands. Nothing here needs a separate fix.

    What does need one. The comment above states VERDICT: INCONCLUSIVE for audit-ci-secrets.md and tells the reader to read its UNVERIFIABLE checks — and the body contains none. The composed body was 226,302 characters; scripts/clamp-issue-body.mjs keeps the head and cuts at 32,000, so what posted was ## Supply chain, ## CI and secrets cut mid-list, and nothing else: ## Application security, ## Hosted accounts and ## Summary are absent along with their verdicts. Everything quoted above comes from the audit-transcript artifact, not from what was posted.

    #764 fixes that path: each fragment's verdict line and its UNVERIFIABLE / FAIL: / BLOCKER / WARNING lines are lifted into the head ahead of the report, where truncation cannot reach them. Replayed against this run's four fragments it emits 8 lines and 1,571 characters — every verdict, the clause above, and three WARNING entries from audit-application.md that also never posted.

    Posted comment vs. the artifact, for this run
    Posted here audit-report.md in the artifact
    Size 31,662 chars 227,480 chars
    Sections ## Supply chain, ## CI and secrets (cut mid-list) + ## Application security, ## Hosted accounts, ## Summary
    UNVERIFIABLE lines 0 1 (audit-ci-secrets.md, line 60)
    Domain verdicts shown 2 of 4 4 of 4 — supply-chain PASS, ci-secrets INCONCLUSIVE, application PASS, hosted PASS

    The three WARNING entries that did not post are all in audit-application.md, which returned PASS: two spec-drift notes on the state-root writer enumeration and the default <tmpdir>/dormouse.log gap, plus a ### WARNING section in its consolidated findings.

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