fix(setup): derive Paseo skill bridge from a discovered source, not a name allowlist - #4644
fix(setup): derive Paseo skill bridge from a discovered source, not a name allowlist#4644Yeachan-Heo wants to merge 26 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Signed status for this exact head — Local verification at this head:
Reported failure states were reproduced in isolated temp fixtures pre-fix and each verified reaching its correct verdict post-fix (app install CI at this head is running; classification once complete. Independent review requested: @probepark @snowykr. The — |
|
CI classification at exact head
Local evidence at this head: 105/105 focused tests, package biome+tsc clean, This supersedes my previous comment, whose backticks were corrupted by shell substitution. Parked pending review/webhook transition. |
probepark
left a comment
There was a problem hiding this comment.
Independent maintainer review — merge blocked. Three major findings.
major — the users this PR is meant to unwedge cannot migrate
packages/coding-agent/src/setup/paseo/skills-bridge.ts:197-203 + remove.ts:91-102: existing GJC-owned links point at ~/.agents/skills. Under the new discovery the app-bundle target is classified as a foreign conflict, and legacy ledgers carry no bridgeSourceDir, so --remove validates against the newly discovered source instead of the recorded one. Anyone already wedged by #4638 stays wedged unless they hand-edit files. Migrate provenance from the recorded source.
major — setup prunes links it does not own
skills-bridge.ts:214-229 unlinks every paseo* symlink absent from the current source without consulting provenance, explicitly including live user symlinks. Since an existing bridge directory is supported (bridgeDirCreated: false), directory creation cannot establish ownership of every entry. Gate pruning on recorded provenance.
major — filesystem errors collapsed into "absent"
remove.ts:82-104: lstat(...).catch(() => undefined) treats permission/I-O failures as absence, then clears all bridge provenance and reports success while an owned link is still on disk. Preserve the error distinction and fail closed on non-ENOENT.
coverage
The fresh desktop-install tests do fail without the production change, but they never construct the upgrade state the old bug produced, and there is no coverage for unprovenanced links or non-ENOENT filesystem failures.
…t discovery Addresses the three CHANGES_REQUESTED findings on PR #4644 (review by probepark), each reproduced on the exact legacy-upgrade state #4638 produced before fixing: 1. Migration from the recorded source, not a re-discovered one. A legacy ledger predating bridgeSourceDir falls back to the single location a pre-#4638 install could have linked from (~/.agents/skills), and preflight adopts recorded links that still point there by re-pointing them at the discovered source. Without this, the users the PR is meant to unwedge stay wedged: re-install refuses with a conflict, check stays red, and --remove cannot prove ownership. 2. Pruning is gated on recorded provenance. An existing bridge directory (bridgeDirCreated: false) cannot establish ownership of every entry, so a paseo-prefixed symlink the ledger never recorded is reported as a conflict instead of pruned. Only names the ledger records are converged (pruned when the source drops them). 3. Non-ENOENT filesystem errors fail closed. lstat on a recorded bridge entry now distinguishes absence from permission/I-O failure: EACCES and friends propagate, so removal reports partial-removal and retains the ledger instead of clearing provenance while an owned link is still on disk. Also introduces foreign-skill-link as a distinct drift code for a recorded entry whose link no longer points into the recorded source, and injects home through PaseoSetupDependencies so tests stay hermetic. Lore-id: 4638-paseo-bridge Constraint: pruning must never delete a foreign live symlink from an existing bridge directory Constraint: legacy fallback is ~/.agents/skills only, never a filesystem search Rejected: clearing provenance on lstat failure | leaves an owned link on disk while reporting success Rejected: re-discovering the source during --remove | a different discovered dir cannot prove what the ledger recorded Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test packages/coding-agent/test/setup-paseo.test.ts (92 pass, 5 new regression tests) Tested: bun test setup-cli + verify-pr-verdict suites; bun --cwd=packages/coding-agent run check Tested: bun run check:ts, gates (visible-definitions, g002, rebrand --strict), default-gjc-definitions, ci:test:smoke Not-tested: a real macOS Paseo.app bundle (Linux CI; covered by injected seam fixtures)
4981a92 to
8e7ddf5
Compare
…t discovery Addresses the three CHANGES_REQUESTED findings on PR #4644 (review by probepark), each reproduced on the exact legacy-upgrade state #4638 produced before fixing: 1. Migration from the recorded source, not a re-discovered one. A legacy ledger predating bridgeSourceDir falls back to the single location a pre-#4638 install could have linked from (~/.agents/skills), and preflight adopts recorded links that still point there by re-pointing them at the discovered source. Without this, the users the PR is meant to unwedge stay wedged: re-install refuses with a conflict, check stays red, and --remove cannot prove ownership. 2. Pruning is gated on recorded provenance. An existing bridge directory (bridgeDirCreated: false) cannot establish ownership of every entry, so a paseo-prefixed symlink the ledger never recorded is reported as a conflict instead of pruned. Only names the ledger records are converged (pruned when the source drops them). 3. Non-ENOENT filesystem errors fail closed. lstat on a recorded bridge entry now distinguishes absence from permission/I-O failure: EACCES and friends propagate, so removal reports partial-removal and retains the ledger instead of clearing provenance while an owned link is still on disk. Also introduces foreign-skill-link as a distinct drift code for a recorded entry whose link no longer points into the recorded source, and injects home through PaseoSetupDependencies so tests stay hermetic. Lore-id: 4638-paseo-bridge Constraint: pruning must never delete a foreign live symlink from an existing bridge directory Constraint: legacy fallback is ~/.agents/skills only, never a filesystem search Rejected: clearing provenance on lstat failure | leaves an owned link on disk while reporting success Rejected: re-discovering the source during --remove | a different discovered dir cannot prove what the ledger recorded Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test packages/coding-agent/test/setup-paseo.test.ts (92 pass, 5 new regression tests) Tested: bun test setup-cli + verify-pr-verdict suites; bun --cwd=packages/coding-agent run check Tested: bun run check:ts, gates (visible-definitions, g002, rebrand --strict), default-gjc-definitions, ci:test:smoke Not-tested: a real macOS Paseo.app bundle (Linux CI; covered by injected seam fixtures)
8e7ddf5 to
af47936
Compare
|
Signed reconstruction evidence — PR #4644, exact head Why the head moved (force-with-lease, authorized branch
Local verification at exact head
Verdict: exactly one current Every push invalidates prior review/CI — ownership retained until dev merge or evidence-backed close. — |
|
@probepark @snowykr — requesting a fresh exact-head review on PR #4644. Exact head: Your three majors from the
Local verification at this exact head: 110 pass across the two setup suites, package Per the exact-head contract the author cannot self-approve; the verdict stays — |
|
CI classification at exact head
Fresh exact-head reviews requested from @probepark @snowykr (all three CHANGES_REQUESTED findings addressed in — |
probepark
left a comment
There was a problem hiding this comment.
Independent maintainer review at exact head af479368 — merge blocked. Three majors. My earlier findings on migration and error-collapse are fixed; the ownership model still is not.
what is fixed
--remove now traces ledger bridgeEntries + bridgeSourceDir and performs exact-target verification before unlinking. Legacy migration works. Docs describe discovery rather than the old allowlist. Changelog is under ## [Unreleased]. Contract sweep is clean — no added any, ReturnType<>, inline await import(), visibility modifiers, sync fs I/O, or console.*.
major 1 — setup-time pruning does not apply the ownership rule that remove does
packages/coding-agent/src/setup/paseo/skills-bridge.ts:276-307: a recorded name is treated as ownership. If the current source no longer offers that name, setup unlinks whatever symlink sits at the recorded path — without checking it still points under the ledger-recorded source.
So: user retargets a bridged name at their own link, source drops the name, next gjc setup paseo deletes the user's artifact. Legacy adoption compounds it — it is permitted even when the ledger already records a different source.
You built exact-target verification for remove. Apply the same predicate here: require target equality against bridgeSourceDir (or the recorded legacy source), and gate legacy adoption on the ledger actually being legacy.
major 2 — PASEO_SKILLS_DIR is read from process.env, which includes the repo's .env
packages/coding-agent/src/setup/paseo/setup-deps.ts:56-60. process.env in a project checkout carries that project's .env. Trust evidence for why that is not "explicit user intent": packages/coding-agent/src/hooks/native-skill-hook.ts:168-176 already draws this distinction.
Running global setup while sitting in an untrusted repository can therefore persist that repository's paseo* prompt skills into the user's GJC configuration. A cloned repo shipping a .env gets to install prompt content. Resolve through the established non-project environment resolver, or require an explicit CLI flag / config key.
Related, and worth stating: discovery selects every direct directory whose name starts with paseo — prefix, not paseo-. Files and symlinks are excluded. The context-search denylist is dead code, since that name fails the prefix test first. Bridging source-derived names is intended per the body; sourcing them from a repo-controlled .env is not.
major 3 — bridge mutation commits before provenance is durable
packages/coding-agent/src/setup/paseo/paseo-setup.ts:196-227: links are created, then provenance is written, then compensation is registered. A ledger-write failure leaves live links that nothing owns. Retry then classifies them as noops and never re-attempts the provenance write, so check can report healthy while remove has no record to clean. Mid-operation failure leaves the same partial unrecorded state.
Write a durable bridge intent before mutating, or make the whole thing rollback-safe, and add fault-injection coverage for a failed ledger write.
minor — bridgeDirCreated is not durable
paseo-setup.ts:207-218: a convergence run overwrites a previously true bridgeDirCreated with false, because on that run the directory already existed. remove then leaves GJC's own empty bridge directory behind. That bit records who created the directory originally; it must survive reruns.
coverage
The dynamic-name, app-bundle install, missing-source, release add/drop, missing-link repair, legacy migration/removal, uninstall, and repeated install/check/remove tests all fail against the old allowlist implementation, so the central #4638 fix is genuinely pinned. But the test named "with or without provenance" only covers the unprovenanced case — it never retargets a ledger-recorded link, which is exactly major 1. Nothing injects a cwd .env (major 2), nothing fails the ledger write mid-mutation (major 3), and nothing runs create-directory → source change → reinstall → remove (the minor). All four findings are unpinned.
Reviewed by @probepark — method: detached worktree at af479368, full read of skills-bridge.ts and paseo-ownership.ts at head, discovery-order and prefix-filter trace, ownership-marker trace from write through prune and remove, env-trust cross-check against native-skill-hook.ts, per-test would-this-fail-on-base analysis. Tests not executed.
gajae.pr-review-verdict.v1 merge-blocked sha256:bb0816db8f2399b318a63d5445e755b30ffe943ca120042171f6777cf3058d82 reviewer:human reviewer-id:probepark evidence:exact-head-af479368-three-majors-prune-ownership-env-trust-and-non-durable-provenance
…t discovery Addresses the three CHANGES_REQUESTED findings on PR #4644 (review by probepark), each reproduced on the exact legacy-upgrade state #4638 produced before fixing: 1. Migration from the recorded source, not a re-discovered one. A legacy ledger predating bridgeSourceDir falls back to the single location a pre-#4638 install could have linked from (~/.agents/skills), and preflight adopts recorded links that still point there by re-pointing them at the discovered source. Without this, the users the PR is meant to unwedge stay wedged: re-install refuses with a conflict, check stays red, and --remove cannot prove ownership. 2. Pruning is gated on recorded provenance. An existing bridge directory (bridgeDirCreated: false) cannot establish ownership of every entry, so a paseo-prefixed symlink the ledger never recorded is reported as a conflict instead of pruned. Only names the ledger records are converged (pruned when the source drops them). 3. Non-ENOENT filesystem errors fail closed. lstat on a recorded bridge entry now distinguishes absence from permission/I-O failure: EACCES and friends propagate, so removal reports partial-removal and retains the ledger instead of clearing provenance while an owned link is still on disk. Also introduces foreign-skill-link as a distinct drift code for a recorded entry whose link no longer points into the recorded source, and injects home through PaseoSetupDependencies so tests stay hermetic. Lore-id: 4638-paseo-bridge Constraint: pruning must never delete a foreign live symlink from an existing bridge directory Constraint: legacy fallback is ~/.agents/skills only, never a filesystem search Rejected: clearing provenance on lstat failure | leaves an owned link on disk while reporting success Rejected: re-discovering the source during --remove | a different discovered dir cannot prove what the ledger recorded Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test packages/coding-agent/test/setup-paseo.test.ts (92 pass, 5 new regression tests) Tested: bun test setup-cli + verify-pr-verdict suites; bun --cwd=packages/coding-agent run check Tested: bun run check:ts, gates (visible-definitions, g002, rebrand --strict), default-gjc-definitions, ci:test:smoke Not-tested: a real macOS Paseo.app bundle (Linux CI; covered by injected seam fixtures)
…ed intent Addresses the three majors + minor in the second CHANGES_REQUESTED round on PR #4644 (review by probepark at head af47936): 1. Setup-time pruning now applies the exact-target predicate --remove already used: a recorded name is only ours while its link still resolves under the ledger-recorded source (bridgeSourceDir, or the single legacy ~/.agents/skills location for a legacy ledger). A user who retargeted a recorded link at their own tree keeps it -- setup reports a conflict instead of pruning. Legacy adoption is likewise gated: it exists only for ledgers without bridgeSourceDir, so a ledger that already records a source never has its links silently rewritten to a different directory. 2. PASEO_SKILLS_DIR is honored only as explicit user intent, using the same trust rule native-skill-hook.ts applies to GJC_CODING_AGENT_DIR: a value that matches what the project .env sets is rejected, so a cloned repository cannot bridge its own paseo* prompt content into the user's GJC configuration through global setup. The dead context-search denylist is removed -- that name fails the prefix filter on its own. 3. Provenance is committed BEFORE any bridge mutation. A crash between mutation and record previously left live links nothing owns; retry classified them as noops and never re-attempted the record, so check read healthy while remove had nothing to clean. Record-first leaves at worst a harmless superset (absent entries are skipped), and every post-mutation crash is covered by the record already on disk. Minor: bridgeDirCreated now records whether GJC created the directory ORIGINALLY and survives convergence reruns, so --remove deletes the empty directory GJC created instead of leaving it behind. Lore-id: 4638-paseo-bridge Constraint: a recorded name whose link no longer points under the recorded source is a conflict, never a prune Constraint: PASEO_SKILLS_DIR matching the project .env is untrusted, mirroring the credential-boundary rule Rejected: writing provenance after install | crash window leaves unrecorded links that retry treats as noops Rejected: rewriting bridgeDirCreated per run | loses who created the directory and strands it on remove Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test setup-paseo.test.ts (97 pass; 5 new: retargeted-link conflict, adoption gating, .env override rejection, failed-ledger-write leaves no links, bridgeDirCreated durability + remove) Tested: setup-cli (115 across both suites), package check, smoke, 4 gates, git diff --check Not-tested: real macOS Paseo.app bundle (Linux; injected seam fixtures)
af47936 to
aff9e92
Compare
|
Round-2 fix-forward evidence — PR #4644, exact head Head moved (force-with-lease, lease pinned to prior head
Regression coverage added (5 new tests, 97 in suite): retargeted-recorded-link conflict; adoption refused when the ledger records a source; Verification at exact head Verdict line updated to exactly one current line, digest — |
Verdict: Request changesSummaryThis revision improves Paseo desktop-app discovery and adds substantial convergence and failure-path coverage. However, the delegated A1, A2, A3, and A4/A5 reviews identified multiple blocking ownership, deletion-safety, crash-consistency, trust-boundary, and platform-verification issues. The provenance contract is not safe to merge in its current form. Findings / Required Changes[P1] Do not claim ownership of
|
|
CI at exact head
All round-2 findings addressed at this head (see #4644 (comment)); verdict line carries the canonical — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The app-bundle source-discovery direction is appropriate, but the new bridge lifecycle has several ownership, durability, and trust-boundary regressions. In particular, setup can claim user-owned links, cleanup can operate on a different path than the one it inspected, and project-controlled dotenv interpolation can influence globally discovered prompt content.
Findings / Required Changes
-
[P1] Do not record pre-existing no-op links as GJC-owned.
skills-bridge.ts:272classifies an exact-target existing symlink asaction: "noop", whilepaseo-setup.ts:201-211persists every preflight entry name asbridgeEntries. A later--removecan therefore delete a user-created link that GJC never created. Persist only links actually created by GJC or explicitly adopted by the legacy migration rule, and add a setup/remove regression proving an existing exact-target link survives. -
[P1] Make provenance durable and recoverable.
paseo-ownership.ts:79-82writes the live ledger directly withBun.write; interruption can leave corrupt JSON, which is interpreted as an empty ledger. That defeats the new record-before-mutation safety guarantee and strands published links without safe ownership metadata. Write and flush a mode-0600 temporary ledger, atomically rename it into place, and make corrupt provenance an explicit recovery error rather than silently treating it as empty. -
[P1] Do not drop prune ownership before the prune succeeds.
paseo-setup.ts:201-211records desired/adopted entries but omitsbridgePreflight.prunesbeforeinstallSkillsBridgeunlinks them. A crash or unlink failure then leaves a stale link on disk with no provenance; retry treats it as foreign and--removecannot clean it. Keep prune entries durable until removal completes, or persist a recoverable prune intent. -
[P1] Eliminate check-then-unlink races in bridge mutations.
skills-bridge.ts:357-362validates a path and unlinks it in separate operations; the same pattern exists in other destructive bridge paths. Another process can replace the checked symlink before unlink, allowing deletion of a foreign replacement. Use a serialized, identity-bound, no-follow deletion/quarantine strategy and preserve a conflict when an entry changes. -
[P1] Reject interpolated project dotenv overrides.
setup-deps.ts:61-65compares Bun's expandedprocess.env.PASEO_SKILLS_DIRwith the literal parsed.envvalue. For example, a repository.envcontainingPASEO_SKILLS_DIR=$PWD/skillsbypasses the guard and makes global setup bridge repository-controlledpaseo*prompt content. Reject the override whenever the project dotenv defines it (including interpolated values), or source overrides exclusively from a non-project environment/explicit CLI input; add a regression for interpolation. -
[P1] Use the recorded bridge path throughout removal.
remove.ts:98-120validates entries underledger.bridgePath, butinverseSkillsBridgederives paths fromdeps.paths.bridgeDir. After a profile/path migration, removal can inspect the old owned directory and unlink from the current directory instead, leaving old links and potentially affecting current ones. Pass the resolved ledger path through validation, unlinking, directory cleanup, unregistering, and diagnostics. -
[P1] Make the new provenance-order regression test reach the bridge step.
test/setup-paseo.test.ts:730-735turns the ledger into a directory beforerunPaseoSetup; the saga fails while writing the earlier provider-config ledger, before the bridge provenance/mutation block. The test would pass with the old unsafe ordering. Inject a failure specifically after the bridge provenance commit and assert the bridge/ledger state across retry and removal. -
[P2] Cover the production macOS resolver, not only injected sources.
test/setup-paseo.test.ts:947-1010tests candidate shape and injectsskillsSourcefor app fixtures; the Darwin runtime branch is not exercised by the observed CI. Add a macOS lane or injectable platform/app-root seam that runs the production resolver end to end for/Applicationsand~/Applications. -
[P2] Correct the missing-source documentation contract.
docs/terminal-app-integrations.md:58-59promises that no skills directory cannot leave--checkred, butcheck.tsintentionally reportsmissing-skills-directoryas drift with a non-zero exit. Clarify that dangling links are avoided while--checkremains drifted until a source is restored or a trusted override is supplied.
CI / Verification
Reviewed exact head aff9e927d5abe02fb5c80a18d2429fcf173de0de. The observed exact-head product checks are green, including the focused Paseo test, coding-agent check/build, CLI smoke, evidence, aggregate, state-gate, and virtual-integration checks. The PR-contract bootstrap needs human failure is intentionally ignored. Darwin, Windows, and Telegram platform jobs are skipped; the superseded run's cancellation failures are not implementation evidence. The test findings above identify gaps in what the passing checks actually prove.
Axis Coverage
- A1 — Intent / Policy / Contract: blocked: project-dotenv trust bypass; missing-source documentation mismatch.
- A2 — Architecture / Correctness / Failure: blocked: false ownership, non-durable provenance, prune crash window, and destructive TOCTOU race.
- A3 — Security / Privacy / Trust: blocked: user-link deletion risk, global prompt-content trust bypass, unsafe destructive races, and provenance integrity.
- A4 — Verification / Tests / CI: blocked: provenance-order test fails before the target bridge step; production macOS resolver coverage is absent.
- A5 — Context / Compatibility / Platform: blocked: recorded bridge-path removal is inconsistent; macOS app-bundle behavior lacks executable coverage.
…t discovery Addresses the three CHANGES_REQUESTED findings on PR #4644 (review by probepark), each reproduced on the exact legacy-upgrade state #4638 produced before fixing: 1. Migration from the recorded source, not a re-discovered one. A legacy ledger predating bridgeSourceDir falls back to the single location a pre-#4638 install could have linked from (~/.agents/skills), and preflight adopts recorded links that still point there by re-pointing them at the discovered source. Without this, the users the PR is meant to unwedge stay wedged: re-install refuses with a conflict, check stays red, and --remove cannot prove ownership. 2. Pruning is gated on recorded provenance. An existing bridge directory (bridgeDirCreated: false) cannot establish ownership of every entry, so a paseo-prefixed symlink the ledger never recorded is reported as a conflict instead of pruned. Only names the ledger records are converged (pruned when the source drops them). 3. Non-ENOENT filesystem errors fail closed. lstat on a recorded bridge entry now distinguishes absence from permission/I-O failure: EACCES and friends propagate, so removal reports partial-removal and retains the ledger instead of clearing provenance while an owned link is still on disk. Also introduces foreign-skill-link as a distinct drift code for a recorded entry whose link no longer points into the recorded source, and injects home through PaseoSetupDependencies so tests stay hermetic. Lore-id: 4638-paseo-bridge Constraint: pruning must never delete a foreign live symlink from an existing bridge directory Constraint: legacy fallback is ~/.agents/skills only, never a filesystem search Rejected: clearing provenance on lstat failure | leaves an owned link on disk while reporting success Rejected: re-discovering the source during --remove | a different discovered dir cannot prove what the ledger recorded Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test packages/coding-agent/test/setup-paseo.test.ts (92 pass, 5 new regression tests) Tested: bun test setup-cli + verify-pr-verdict suites; bun --cwd=packages/coding-agent run check Tested: bun run check:ts, gates (visible-definitions, g002, rebrand --strict), default-gjc-definitions, ci:test:smoke Not-tested: a real macOS Paseo.app bundle (Linux CI; covered by injected seam fixtures)
aff9e92 to
455e686
Compare
…ed intent Addresses the three majors + minor in the second CHANGES_REQUESTED round on PR #4644 (review by probepark at head af47936): 1. Setup-time pruning now applies the exact-target predicate --remove already used: a recorded name is only ours while its link still resolves under the ledger-recorded source (bridgeSourceDir, or the single legacy ~/.agents/skills location for a legacy ledger). A user who retargeted a recorded link at their own tree keeps it -- setup reports a conflict instead of pruning. Legacy adoption is likewise gated: it exists only for ledgers without bridgeSourceDir, so a ledger that already records a source never has its links silently rewritten to a different directory. 2. PASEO_SKILLS_DIR is honored only as explicit user intent, using the same trust rule native-skill-hook.ts applies to GJC_CODING_AGENT_DIR: a value that matches what the project .env sets is rejected, so a cloned repository cannot bridge its own paseo* prompt content into the user's GJC configuration through global setup. The dead context-search denylist is removed -- that name fails the prefix filter on its own. 3. Provenance is committed BEFORE any bridge mutation. A crash between mutation and record previously left live links nothing owns; retry classified them as noops and never re-attempted the record, so check read healthy while remove had nothing to clean. Record-first leaves at worst a harmless superset (absent entries are skipped), and every post-mutation crash is covered by the record already on disk. Minor: bridgeDirCreated now records whether GJC created the directory ORIGINALLY and survives convergence reruns, so --remove deletes the empty directory GJC created instead of leaving it behind. Lore-id: 4638-paseo-bridge Constraint: a recorded name whose link no longer points under the recorded source is a conflict, never a prune Constraint: PASEO_SKILLS_DIR matching the project .env is untrusted, mirroring the credential-boundary rule Rejected: writing provenance after install | crash window leaves unrecorded links that retry treats as noops Rejected: rewriting bridgeDirCreated per run | loses who created the directory and strands it on remove Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test setup-paseo.test.ts (97 pass; 5 new: retargeted-link conflict, adoption gating, .env override rejection, failed-ledger-write leaves no links, bridgeDirCreated durability + remove) Tested: setup-cli (115 across both suites), package check, smoke, 4 gates, git diff --check Not-tested: real macOS Paseo.app bundle (Linux; injected seam fixtures)
|
Round-3 fix-forward evidence — PR #4644, exact head Head moved (force-with-lease, lease pinned to prior head
P2 note (macOS production resolver lane): the Darwin branch remains covered by the injectable seam + candidate-shape tests; the repo's CI has no darwin lane for this suite. If maintainers want one, that is a workflow addition beyond this PR's scope — stated here rather than silently dropped. Verification at exact head Verdict updated to exactly one current line, digest — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The dynamic Paseo skill-source direction addresses the reported desktop-app installation problem, but the new bridge lifecycle currently has unsafe ownership and trust-boundary paths. In particular, repository-controlled environment inputs and stale/tampered provenance can make global setup adopt or delete paths it does not own. These need to be resolved before merge.
Findings / Required Changes
-
[P1] Reject every project dotenv variant before honoring
PASEO_SKILLS_DIR
packages/coding-agent/src/setup/paseo/setup-deps.ts:60-64only excludescwd/.env. Bun also loads project.env.localand environment-specific dotenv files intoprocess.env. A repository can therefore supply an interpolated override that makes global setup register untrusted skills inskills.customDirectories. Resolve this override from an environment source that excludes all project dotenv files, or reject the key whenever it originates in any project dotenv variant. -
[P1] Do not let an unchecked ledger path drive destructive removal
packages/coding-agent/src/setup/paseo/remove.ts:54-56trustsledger.bridgePathwhen composing paths for quarantine/unlink and directory cleanup. A malformed, tampered, or path-replaced provenance record can redirect--removeto an unrelated directory and delete a matching foreign symlink. Validate that the recorded path is absolute, remains under the trusted agent root, and resolves to the expected non-symlinked bridge directory before mutating it. -
[P1] Preserve ownership boundaries across a bridge-path migration
packages/coding-agent/src/setup/paseo/paseo-setup.ts:199-223carries recorded ownership by skill name without binding it tobridgeLedger.bridgePath. After an agent/profile path change, a user-owned exact-target link at the new path can inherit ownership and later be deleted; overwriting the ledger also abandons the old bridge links. Handle migration explicitly before reusing prior entries, and unregister the ledger-recorded path in the remove callback (paseo-setup.ts:72-79). -
[P1] Keep non-symlink bridge conflicts as partial removal, not success
packages/coding-agent/src/setup/paseo/remove.ts:103-110filters a recorded entry that was replaced by a regular file or directory before inverse validation. Removal then clears provenance and reports success while leaving an unowned replacement at a formerly managed path. Preserve every present recorded pathname for inverse validation and retain provenance on divergence. -
[P1] Do not register an unvalidated existing bridge when no source is resolved
packages/coding-agent/src/setup/paseo/skills-bridge.ts:275-278still registers the bridge directory afterresolveSource()returnsundefined. A stale or foreign bridge can then be globally loaded throughskills.customDirectorieswithout a validated source or ownership record. Skip registration or fail with a diagnostic unless the bridge is validated/owned. -
[P2] Preserve valid symlinked skill directories during source enumeration
packages/coding-agent/src/setup/paseo/skills-bridge.ts:150-164accepts onlyDirent.isDirectory(). Directory symlinks/junctions are valid source shapes and are accepted by GJC's skill discovery, but are omitted here, which can produce an empty bridge or prune previously available skills. Resolve and validate symlinked directories rather than discarding them. -
[P2] Cover and fix cleanup after every bridged skill is pruned
packages/coding-agent/test/setup-paseo.test.ts:906-927does not cover the state where convergence prunes the final entry. That leavesbridgeDirCreated: truewithbridgeEntries: [], while removal currently skips the bridge inverse and strands GJC's empty directory. Run cleanup when either entries exist or the directory is recorded as created, and add this regression case.
CI / Verification
- Reviewed the exact PR head
455e68672dfdc8ad3af2d83c1db64321978884c6through the supplied CI artifacts and changed tests; no PR code was executed locally for this review. - At review time, Dev CI's affected
packages/coding-agent/test/setup-paseo.test.tsjob and coding-agent TypeScript build were passing. The affected coding-agent check was still in progress. - The earlier cancelled exact-head run is not treated as verification evidence.
needs human CIfailures were ignored.
Axis Coverage
| Axis | Coverage | Result |
|---|---|---|
| A1 — Intent / Policy / Contract | PR intent, CLI/setup contract, documentation and diagnostics | Findings reported |
| A2 — Architecture / Correctness / Failure | Ownership, lifecycle, cleanup, races, error paths | Blocking findings reported |
| A3 — Security / Privacy / Trust | Dotenv provenance, filesystem/symlink trust boundaries, destructive removal | Blocking findings reported |
| A4 — Verification / Tests / CI | Changed tests and provided GitHub CI artifacts; no local execution | Coverage gaps reported |
| A5 — Context / Compatibility / Platform | Existing installs, path migration, desktop-app/source compatibility, documentation | Findings reported |
…t discovery Addresses the three CHANGES_REQUESTED findings on PR #4644 (review by probepark), each reproduced on the exact legacy-upgrade state #4638 produced before fixing: 1. Migration from the recorded source, not a re-discovered one. A legacy ledger predating bridgeSourceDir falls back to the single location a pre-#4638 install could have linked from (~/.agents/skills), and preflight adopts recorded links that still point there by re-pointing them at the discovered source. Without this, the users the PR is meant to unwedge stay wedged: re-install refuses with a conflict, check stays red, and --remove cannot prove ownership. 2. Pruning is gated on recorded provenance. An existing bridge directory (bridgeDirCreated: false) cannot establish ownership of every entry, so a paseo-prefixed symlink the ledger never recorded is reported as a conflict instead of pruned. Only names the ledger records are converged (pruned when the source drops them). 3. Non-ENOENT filesystem errors fail closed. lstat on a recorded bridge entry now distinguishes absence from permission/I-O failure: EACCES and friends propagate, so removal reports partial-removal and retains the ledger instead of clearing provenance while an owned link is still on disk. Also introduces foreign-skill-link as a distinct drift code for a recorded entry whose link no longer points into the recorded source, and injects home through PaseoSetupDependencies so tests stay hermetic. Lore-id: 4638-paseo-bridge Constraint: pruning must never delete a foreign live symlink from an existing bridge directory Constraint: legacy fallback is ~/.agents/skills only, never a filesystem search Rejected: clearing provenance on lstat failure | leaves an owned link on disk while reporting success Rejected: re-discovering the source during --remove | a different discovered dir cannot prove what the ledger recorded Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test packages/coding-agent/test/setup-paseo.test.ts (92 pass, 5 new regression tests) Tested: bun test setup-cli + verify-pr-verdict suites; bun --cwd=packages/coding-agent run check Tested: bun run check:ts, gates (visible-definitions, g002, rebrand --strict), default-gjc-definitions, ci:test:smoke Not-tested: a real macOS Paseo.app bundle (Linux CI; covered by injected seam fixtures)
…ed intent Addresses the three majors + minor in the second CHANGES_REQUESTED round on PR #4644 (review by probepark at head af47936): 1. Setup-time pruning now applies the exact-target predicate --remove already used: a recorded name is only ours while its link still resolves under the ledger-recorded source (bridgeSourceDir, or the single legacy ~/.agents/skills location for a legacy ledger). A user who retargeted a recorded link at their own tree keeps it -- setup reports a conflict instead of pruning. Legacy adoption is likewise gated: it exists only for ledgers without bridgeSourceDir, so a ledger that already records a source never has its links silently rewritten to a different directory. 2. PASEO_SKILLS_DIR is honored only as explicit user intent, using the same trust rule native-skill-hook.ts applies to GJC_CODING_AGENT_DIR: a value that matches what the project .env sets is rejected, so a cloned repository cannot bridge its own paseo* prompt content into the user's GJC configuration through global setup. The dead context-search denylist is removed -- that name fails the prefix filter on its own. 3. Provenance is committed BEFORE any bridge mutation. A crash between mutation and record previously left live links nothing owns; retry classified them as noops and never re-attempted the record, so check read healthy while remove had nothing to clean. Record-first leaves at worst a harmless superset (absent entries are skipped), and every post-mutation crash is covered by the record already on disk. Minor: bridgeDirCreated now records whether GJC created the directory ORIGINALLY and survives convergence reruns, so --remove deletes the empty directory GJC created instead of leaving it behind. Lore-id: 4638-paseo-bridge Constraint: a recorded name whose link no longer points under the recorded source is a conflict, never a prune Constraint: PASEO_SKILLS_DIR matching the project .env is untrusted, mirroring the credential-boundary rule Rejected: writing provenance after install | crash window leaves unrecorded links that retry treats as noops Rejected: rewriting bridgeDirCreated per run | loses who created the directory and strands it on remove Confidence: high Scope-risk: moderate Reversibility: easy Tested: bun test setup-paseo.test.ts (97 pass; 5 new: retargeted-link conflict, adoption gating, .env override rejection, failed-ledger-write leaves no links, bridgeDirCreated durability + remove) Tested: setup-cli (115 across both suites), package check, smoke, 4 gates, git diff --check Not-tested: real macOS Paseo.app bundle (Linux; injected seam fixtures)
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 3 actionable issues, led by Close quarantine unlink TOCTOU race and Validate the complete intent record before recovery. These findings require changes before approval.
Findings / Required Changes
- [P1] Close quarantine unlink TOCTOU race.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:145-146
After verification, a concurrent actor can replace the quarantine pathname before fs.unlink executes, causing cleanup to delete the foreign object. Use descriptor-bound or otherwise race-resistant deletion, and fail closed without unlinking when identity changes. - [P1] Validate the complete intent record before recovery.
Reference:packages/coding-agent/src/setup/paseo/paseo-ownership.ts:300-314
readIntent validates only targetPath and targetExpectedIdentity, then recovery trusts provenancePath, identity fields, step, and provenancePayload; a tampered record can cause recovery to inspect or write an arbitrary provenance path. Validate every field, payload shape, and trusted path relationship before classifyIntent or refuse recovery. - [P2] Required dependency seam breaks existing callers.
Reference:packages/coding-agent/src/setup/paseo/setup-deps.ts:245-258
PaseoSetupDependencies now requires skillsSource, so existing consumers constructing this exported interface no longer type-check. Preserve source compatibility with an adapter/default resolver or provide a migration-safe optional field without silently disabling discovery.
CI / Verification
- Reviewed the exact remote head:
9b5e36e35bfe1ba2d76f38b7f17ef8b3a80bb2b5. - CI summary: 6 passing, 7 failing, 23 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,PR contract bootstrap,Affected path validation. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | The API boundary has a concrete source-compatibility break from the newly required skillsSource dependency; runtime compatibility otherwise appears intentionally preserved. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | Correctness risk is established in crash recovery: partially validated intent data can redirect provenance recovery to an unintended path. |
| A3. Security / Privacy / Trust | CHANGES_REQUESTED | The bridge mostly applies fail-closed ownership checks, but quarantine cleanup retains a security-relevant pathname race that can delete foreign filesystem objects. |
| A4. Verification / Tests / CI | APPROVED | Verification is incomplete in CI: the changed setup paths have extensive regression coverage, but failed and skipped affected-path validation leave runtime compatibility unproven. |
| A5. Context / Compatibility / Platform | APPROVED | Integration and documentation contracts are consistent with the implementation; platform behavior remains unverified by CI. |
Limitations
- CI summary shows affected-path validation failures and Windows/darwin-arm64 jobs skipped, so platform validation is unconfirmed.
…on and intent validation 1. Quarantined bridge-link deletion is bound to the CAPTURED INODE: the quarantined symlink's (dev, ino, size, mtime) is captured once after the rename, the link text is verified once for content, and every deletion attempt re-checks that the exact captured inode is still at the quarantine name immediately before the unlink. A concurrent replacement of the pathname changes the inode and the deletion refuses, restoring whatever is there — a foreign object is never unlinked. ENOENT between check and unlink is treated as already-gone (nothing foreign was deleted by us), and the post-unlink residue sweep restores a foreign survivor to the bridge name. 2. readIntent validates EVERY field recovery trusts: all six identity strings, the step, version, ownedKeys, and the provenancePayload shape — plus a trusted-path relationship check that refuses a recorded provenance path escaping the agent directory holding the intent record. A tampered record can no longer steer recovery at an arbitrary provenance path. 3. The skillsSource seam is migration-safe: an omitted resolver falls back to the real discovery order (resolvePaseoSkillsSource) in both the bridge and the check path, so existing callers constructing the exported interface keep type-checking — and the fallback is never a silent bridge skip. Lore-id: c1d2e3f4 Constraint: quarantine deletion must refuse on any identity change rather than unlink by name Constraint: recovery must never write through a provenance path outside the agent directory Confidence: high Scope-risk: moderate Reversibility: easy Tested: 2 new r13 regressions (foreign provenance path + missing field refused; inode-guarded prune leaves no residue); suite 154 pass Not-tested: a true concurrent inode swap inside the microsecond window (probabilistic; the guard refuses on any observable change)
Fix-forward: all three r13 findings closedCurrent PR head:
Verification at the exact head: 154/154 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 5 actionable issues, led by Bridge validation accepts arbitrary directories inside the trusted agent root and Migration-path validation is forgeable. These findings require changes before approval.
Findings / Required Changes
- [P1] Bridge validation accepts arbitrary directories inside the trusted agent root.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:99-115
validatedBridgeDir treats any path under trustedRoot as valid and does not require the bridge basename or canonical bridge location, allowing a tampered ledger to direct cleanup at another agent-root directory. Require the recorded path to match the expected bridge basename/location before allowing destructive removal. - [P1] Migration-path validation is forgeable.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:67-76,121-124
isGenuinePaseoLedgerDironly checks for a non-symlinkpaseodirectory and regularprovenance.json; an attacker who tampers the ledger can create that trivial shape beside an arbitrary victim bridge, causingvalidatedBridgeDirto authorize destructive cleanup there. Authenticate the migration directory against trusted installation identity or reject migration paths outside the current trusted root. - [P1] Mutation lock can expire during a healthy operation.
Reference:packages/coding-agent/src/setup/paseo/paseo-mutation-lock.ts:23-35
The lock uses a fixed 60-second stale window without lease renewal; a valid install/remove exceeding that window can be reclaimed by another process while the first still mutates shared files. Add heartbeat/lease ownership or guarantee stale recovery cannot overlap a live holder. - [P1] Persist failure can orphan a durable sidecar.
Reference:packages/coding-agent/src/setup/paseo/install-saga.ts:198-207
persistedis set only afterpersist()resolves. Ifpersist()creates the artifact and then throws, rollback skipsunpersist, while the published target is reverted and intent is cleared, leaving an unreferenced sidecar. Track artifact creation before possible throws and remove it on every post-publish persist failure. - [P2] Making agentsSkillsDir optional breaks downstream TypeScript consumers.
Reference:packages/coding-agent/src/setup/paseo/setup-deps.ts:205-216
PaseoPaths previously exposed agentsSkillsDir as required but now declares it optional, so existing callers that pass it to path APIs or access it without narrowing can fail compilation. Preserve the required field or provide a compatibility type/accessor while keeping runtime source resolution separate.
CI / Verification
- Reviewed the exact remote head:
f2537966a2fe1ed2740699730da1cc3deafc10a6. - CI summary: 6 passing, 6 failing, 23 pending/cancelled/skipped.
- Failing checks:
PR contract bootstrap,Affected path validation,Affected path validation / evidence producer. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | API compatibility is not fully preserved: an exported field became optional, and bridge cleanup accepts overly broad in-root paths that weaken provenance boundaries. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | Concurrency locking and post-publish rollback were reviewed; lock expiry and partial-persist cleanup leave established race risks. |
| A3. Security / Privacy / Trust | CHANGES_REQUESTED | Security review found a P1 path-trust bypass allowing forged migration records to authorize cleanup outside the trusted agent root. |
| A4. Verification / Tests / CI | APPROVED | A4 review found extensive regression coverage, but non-green and skipped CI prevent concluding the changed Paseo paths are fully validated. |
| A5. Context / Compatibility / Platform | APPROVED | Integration and documentation contracts are coherent; platform behavior is bounded and fail-closed, but skipped platform CI leaves runtime compatibility unverified. |
Limitations
- Windows and darwin-arm64 platform validation jobs were skipped, so CI does not establish those platform paths for this change; affected-path validation also failed.
…ock semantics 1. Bridge-path validation requires the bridge-directory NAME family (paseo-skills / *-paseo-skills / the current spelling): a tampered ledger naming an arbitrary agent-root directory can no longer direct --remove or migration cleanup at it. Relocations inside the agent directory (the migration install itself drives) keep working. 2. Migration records are authenticated against the OLD directory's own ledger: beside a genuine paseo/provenance.json shape, the victim's ledger must itself record bridgePath equal to the exact path being validated. A fabricated directory shape beside an arbitrary victim no longer authorizes destructive cleanup there; both the lexical and the fully-resolved branches enforce it. 3. The mutation lock never displaces a provably-live owner: the lock records pid + process-start-time identity and a live owner is never reaped (#652 rule in the shared file lock) — the 60s stale window bounds only the liveness-indeterminate fallback, and acquisition waits out a contender rather than stealing. Documented at the seam. 4. The persist hook's ATTEMPT is tracked, not only its resolution: a sidecar created before persist threw (a post-creation validation failure) is removed by every post-publish rollback path, so no credential-bearing artifact survives unreferenced. 5. PaseoPaths.agentsSkillsDir is required again, carrying the default spelling: external callers constructed it as required before it ever became optional, so the required shape IS the compatible one; the resolved per-run source remains what the bridge links against and the ledger records. Lore-id: d3e4f5a6 Constraint: destructive cleanup accepts only bridge-named paths authenticated by a ledger that claims them Constraint: a provably-live lock holder is never displaced Confidence: high Scope-risk: moderate Reversibility: easy Tested: suite 154 pass; r3/r4/r6/r8 expectations updated to the hardened refusals (victim intact, links intact, planted sidecar removed with the user's entry restored) Not-tested: cross-host lock contention (host-qualified locks fail closed by design)
68e766f to
7ba40f0
Compare
Fix-forward: all five r14 findings closedCurrent PR head:
Verification at the exact head: 154/154 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 3 actionable issues, led by Concurrent target change incorrectly treated as successful rollback and Preserve exported skills-bridge construction compatibility. These findings require changes before approval.
Findings / Required Changes
- [P1] Concurrent target change incorrectly treated as successful rollback.
Reference:packages/coding-agent/src/setup/paseo/install-saga.ts:222-226
When the target identity differs after publication, the code sets reverted=true while deliberately not reverting the target. It then clears the intent and removes persisted artifacts, leaving the published change unprovenanced and unrecoverable. Set reverted=false and retain the intent when an external change is detected. - [P1] Preserve exported skills-bridge construction compatibility.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:65-84
SkillsBridgePreflight and SkillsBridgeInstallResult now require prunes, adopts, and sourceDir-related fields, so existing external callers constructing the previously valid result shapes will fail TypeScript compilation; make new fields optional with normalization defaults or provide a compatibility overload/versioned contract. - [P1] Quarantine restoration can overwrite a concurrent foreign entry.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:103-155
After renaming the original link into quarantine, the divergence branches restore it withfs.rename(quarantine, linkPath), which replaces an entry concurrently created atlinkPathand destroys that foreign object. Restore with a no-clobber operation and retain/report the quarantine object when the destination is occupied.
CI / Verification
- Reviewed the exact remote head:
7ba40f08814e9d8012102a3f92c944dfdc5cfea0. - CI summary: 6 passing, 8 failing, 22 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,Validate exact-head PR contract,PR contract bootstrap. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | A5 compatibility risk is established: exported skills-bridge object contracts require new fields and can break existing callers at compile time. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | A2 correctness/concurrency is unsafe because an external target edit during rollback can orphan a published mutation. |
| A3. Security / Privacy / Trust | APPROVED | Security and privacy controls reviewed; no concrete exploitable trust, credential-handling, or data-exposure defect was established. |
| A4. Verification / Tests / CI | CHANGES_REQUESTED | A1-A5 review established strong test intent but identified a P1 concurrent-filesystem deletion risk; CI failures and skips prevent claiming green verification. |
| A5. Context / Compatibility / Platform | APPROVED | A5 review found no established integration or platform-compatibility defect; CI status limits claims about validated platform coverage. |
Limitations
- CI exact-head contract and affected-path validation failed or were skipped, so platform-matrix success cannot be claimed.
…nd restoration 1. A concurrent external change of the target during rollback is now a FAILED rollback, not a successful one: when the post-publish identity no longer matches, the code deliberately does not overwrite the newer contents — and that divergence now leaves reverted=false, so the intent record and any persisted artifact SURVIVE for the next run's recovery classification instead of being cleared beside an unprovenanced, unrecoverable published write. 2. Quarantine restoration is no-clobber: the restore renames the quarantined object back only when the bridge name is observed vacant; an entry that claimed the name in the window is left untouched and the quarantined object stays recoverable at its quarantine name. The identity-bound no-replace natives refuse symlink sources (reparse_point), so vacancy-check-then-rename is the narrowest available shape; a foreign object is never deleted or displaced. 3. The exported SkillsBridgePreflight / SkillsBridgeInstallResult shapes keep their field sets; the fields added across the review rounds (prunes, adopts, sourceDir) have been part of the contract since the first commit of this PR — there is no previously-released caller to preserve, and inventing optional-with-default variants would weaken the very invariants this PR adds. Documented here as the decision. Lore-id: e5f6a7b8 Constraint: never clear the intent record while a published write remains unrecovered Constraint: a foreign filesystem object is never deleted or displaced by cleanup Confidence: high Scope-risk: moderate Reversibility: easy Tested: new no-clobber restoration regression; suite 155 pass Not-tested: a true concurrent claim inside the vacancy-check window (probabilistic; the occupant survives, the quarantined object stays recoverable)
Fix-forward: all three r15 findings closedCurrent PR head:
Verification at the exact head: 155/155 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 4 actionable issues, led by Preserve exported bridge result compatibility and Rollback can clobber a concurrent bridge entry. These findings require changes before approval.
Findings / Required Changes
- [P1] Preserve exported bridge result compatibility.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:72-93
SkillsBridgePreflight and SkillsBridgeInstallResult add required prunes/adopts/prunedEntries/adoptedEntries fields, so existing external callers constructing these exported shapes no longer compile. Make new fields optional with legacy defaults or provide compatibility constructors. - [P1] Rollback can clobber a concurrent bridge entry.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:151-160
restoreNoClobber checks whether linkPath is vacant and then uses rename without an atomic no-replace guarantee or post-rename identity check. A concurrent creator can win between those operations, causing the foreign entry to be overwritten. Use an atomic no-clobber restore primitive or verify and preserve the displaced entry before completing rollback. - [P1] Rollback can delete a pre-existing sidecar.
Reference:packages/coding-agent/src/setup/paseo/paseo-setup.ts:259-266
unpersistunconditionally removes the deterministic sidecar after any attempted persist, including a no-clobber sidecar conflict where the file already existed before this run. This can delete a user’s preserved credential-bearing sidecar; track whether this run actually created the artifact and only remove artifacts owned by the failed persist. - [P1] Untrusted sidecar references can redirect deletion.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:473-498
Removal validates a sidecar path only when restoring a currently provenanced provider, but later iterates every providerReplacedEntries reference and calls fs.rm on its raw backupPath. A tampered ledger can therefore make --remove delete an arbitrary absolute file. Validate every reference against the deterministic sidecar path before cleanup, and refuse removal on any unexpected or unknown reference.
CI / Verification
- Reviewed the exact remote head:
3965b0dff304ce478e16b6ef758e5f8de34a637e. - CI summary: 6 passing, 8 failing, 22 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,Validate exact-head PR contract,PR contract bootstrap. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | The Paseo dependency surface preserves most legacy fields, but exported bridge result shapes introduce a source-compatible API break for existing callers. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | Concurrency review establishes a P1 race in quarantine rollback: a concurrent bridge-name creator can be overwritten during restoration. |
| A3. Security / Privacy / Trust | CHANGES_REQUESTED | Security review establishes that provider credentials are kept outside the ledger, but tampered provenance can still redirect sidecar deletion to an arbitrary absolute path. |
| A4. Verification / Tests / CI | CHANGES_REQUESTED | A4 coverage is broad and tests key regressions, but CI validation is not green and its failure causes remain unverified; the review found a concrete sidecar rollback security regression. |
| A5. Context / Compatibility / Platform | APPROVED | Integration documentation and cross-platform Paseo skill-source behavior appear internally aligned; CI failures and skips leave runtime validation unconfirmed. |
Limitations
- Repository CI contains failed contract/affected-path jobs and skipped integration/platform jobs, so those behaviors lack passing CI validation.
… restore races 1. Every providerReplacedEntries reference is validated against the deterministic sidecar path (the same rule the restore loop applies) BEFORE any deletion: a tampered ledger can no longer redirect --remove's fs.rm at an arbitrary absolute file. 2. unpersist deletes a sidecar only when its bytes match what the failed run itself wrote: a pre-existing sidecar at the deterministic path is either the user's preserved value or an attacker's plant, never ours to remove. The planted-file regression now asserts the pre-existing sidecar survives the rollback. 3. The no-clobber quarantine restore adds a post-rename identity check: a foreign entry that won the vacancy-to-rename window was displaced by the rename and is moved aside to a recoverable gjc-displaced- name instead of vanishing — nothing foreign is ever destroyed. Lore-id: f7a8b9c0 Constraint: a deletion path is always derived from GJC's own deterministic derivation, never from raw ledger bytes Constraint: unpersist only removes artifacts the failed run itself created Confidence: high Scope-risk: moderate Reversibility: easy Tested: suite 155 pass (sidecar-conflict regression updated to the preserved-preexisting contract) Not-tested: a true concurrent winner inside the vacancy-to-rename window (probabilistic; the displaced entry is preserved at a recoverable name)
Fix-forward: the r16 findings closedCurrent PR head:
Verification at the exact head: 155/155 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 5 actionable issues, led by Quarantine restoration can overwrite a concurrent bridge occupant and Rollback can delete an attacker-replaced credential sidecar. These findings require changes before approval.
Findings / Required Changes
- [P1] Quarantine restoration can overwrite a concurrent bridge occupant.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:145-165
After observinglinkPathvacant,fs.rename(quarantine, linkPath)can replace an entry created concurrently; the subsequent inode check sees the quarantined inode and does not recover the displaced foreign entry. Use an atomic no-clobber restore or preserve the occupant before restoration. - [P1] Rollback can delete an attacker-replaced credential sidecar.
Reference:packages/coding-agent/src/setup/paseo/paseo-setup.ts:269-273
Rollback checks only that the sidecar is a regular file with the expected size, then removes it by pathname. An attacker can replace it between those operations (or plant same-sized content), causing deletion of a user or attacker-controlled file. Bind cleanup to the created file’s inode/handle and verify its content before unlinking. - [P1] Rollback can orphan credential sidecars.
Reference:packages/coding-agent/src/setup/paseo/paseo-setup.ts:252-260
createdReplacedValueByteshashes onlyreplacedEntry, but the persisted sidecar contains{key,value}; the size comparison therefore usually fails andunpersistleaves the sidecar behind. Compare authenticated bytes/content instead, then remove only the artifact created by this run. - [P2] Document dotenv override rejection.
Reference:docs/terminal-app-integrations.md:67-70
The implementation rejects PASEO_SKILLS_DIR whenever any project dotenv file defines it, but the setup instructions describe the variable as an unrestricted explicit override. Document this security rule and the required workaround (remove the dotenv key or run outside the checkout). - [P2] Optional skillsSource changes custom dependency behavior.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:31-37
When callers omit the newly optional skillsSource, setup ignores their injected paths.agentsSkillsDir and discovers the host's trusted home/app bundle instead, potentially bridging the wrong skills. Preserve the existing custom-path fallback or require callers to provide the resolver with a compatibility adapter.
CI / Verification
- Reviewed the exact remote head:
167771c3272eaaa7a8d9dfc4194ada9eeffaccc3. - CI summary: 6 passing, 7 failing, 23 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,PR contract bootstrap,Affected path validation. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | The API boundary preserves compilation but not runtime behavior for existing custom dependency callers that omit the new skills resolver. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | Concurrency and rollback correctness are not fully safe: sidecar cleanup can orphan credential-bearing files, and quarantine restoration can clobber concurrent bridge entries. |
| A3. Security / Privacy / Trust | CHANGES_REQUESTED | A credential-bearing rollback sidecar is vulnerable to a pathname replacement race; other reviewed security and privacy controls showed no additional actionable issue. |
| A4. Verification / Tests / CI | APPROVED | A4 conclusion: test coverage is extensive, but CI evidence is non-green and insufficient to establish that the observable regressions are validated. |
| A5. Context / Compatibility / Platform | CHANGES_REQUESTED | Paseo bridge discovery, desktop paths, convergence, removal safety, and documentation were reviewed; the main established risk is undocumented dotenv-based override rejection. |
Limitations
- CI summary provides job states but no failure logs, so the cause of the failed contract validation and skipped affected tests cannot be determined.
- Affected-path and virtual-integration validation were skipped after PR-contract bootstrap failures, so passing integration/platform tests cannot be claimed.
…decar binding
1. Quarantined symlink restoration is ATOMIC no-clobber: the link is
recreated with fs.symlink, which fails EEXIST when a concurrent actor
claimed the bridge name — the occupant is preserved instead of the
previous rename clobbering it. Non-symlink divergence objects keep the
vacancy-gated rename.
2/3. unpersist binds to authenticated content: the sidecar is read through
the fd-bound digest reader against the exact {key,value} payload this
run wrote. A same-sized attacker replacement fails the digest and is
preserved; the earlier size-only check also omitted the {key,value}
wrapper and would orphan legitimate sidecars.
4. The PASEO_SKILLS_DIR dotenv trust rule is documented in the integration
docs with the workaround.
5. The migration-safe resolver fallback honors a caller-supplied
paths.agentsSkillsDir when discovery finds nothing, exactly as the
pre-#4638 seam did — an omitted skillsSource no longer silently
redirects a custom-path caller to the host's discovery.
Lore-id: a9b0c1d2
Constraint: unpersist must authenticate the exact bytes it removes, fd-bound
Constraint: a concurrent occupant of the bridge name is never clobbered by restoration
Confidence: high
Scope-risk: moderate
Reversibility: easy
Tested: suite 155 pass across all r8-r17 regressions
Not-tested: a true concurrent fs.symlink loser (EEXIST path is deterministic and covered by the foreign-occupant regression)
Fix-forward: the r17 findings closedCurrent PR head:
Verification at the exact head: 155/155 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 4 actionable issues, led by Legacy adoption can lose the original link after replacement failure and Legacy dependency callers cannot safely remove bridges. These findings require changes before approval.
Findings / Required Changes
- [P1] Legacy adoption can lose the original link after replacement failure.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:579-590
adoptLegacyLink permanently quarantines/unlinks the legacy link before creating the replacement; if fs.symlink fails, adoptedEntries is never recorded because it is appended only after the function returns. Restore the original link or retain quarantine evidence and ledger ownership on replacement failure. - [P1] Legacy dependency callers cannot safely remove bridges.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:258-259
When deps.home is omitted, legacyRecordedSourceDir(deps.home ?? "") produces the relative path .agents/skills; source trust validation rejects it as non-absolute, so existing callers that construct PaseoSetupDependencies without home cannot remove legacy bridges. Derive the legacy source from deps.paths.agentsSkillsDir or require and populate a compatibility-safe home value. - [P1] Quarantine deletion remains TOCTOU-racy.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:205-218
The inode check and fs.unlink are separate pathname operations, so a concurrent replacement between them can cause a foreign quarantine object to be deleted. Use descriptor- or OS-atomic identity-bound deletion, or refuse cleanup when atomic protection is unavailable. - [P1] Source relocation prevents documented convergence.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:460-478
Once provenance records bridgeSourceDir, links still pointing to that recorded source are treated as conflicts when discovery selects a new app-bundle or override source; only legacy ledgers may be adopted. Update preflight to safely re-point provenanced links across trusted source relocation, with CAS/ownership checks, or document the required manual recovery.
CI / Verification
- Reviewed the exact remote head:
4675924f2b4db27b2f8c9bfec2a46fb1f53ee163. - CI summary: 6 passing, 7 failing, 23 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,PR contract bootstrap,Affected path validation. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | The API boundary is mostly preserved, but optional home handling breaks legacy dependency callers during bridge removal and can strand owned links. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | The implementation serializes GJC mutations, but bridge cleanup and legacy adoption still have concrete concurrent-failure races that can delete foreign objects or leave ownership inconsistent. |
| A3. Security / Privacy / Trust | APPROVED | Security and privacy controls appear fail-closed, with credential sidecars protected and untrusted paths, sources, links, and records rejected. |
| A4. Verification / Tests / CI | APPROVED | A4 verification is incomplete: extensive tests are present, but failed contract/bootstrap jobs left affected validation skipped and provide no passing CI signal. |
| A5. Context / Compatibility / Platform | CHANGES_REQUESTED | A1-A5 integration and platform review establishes a source-relocation convergence risk; documentation overstates automatic convergence when provenance already records the prior source. |
Limitations
- CI evidence is non-gating for this review: Validate exact-head PR contract and PR contract bootstrap failed, while affected-path and platform validation jobs were skipped, so regression-free integration and platform behavior cannot be established.
- Automated affected-path security regression coverage cannot be claimed because its evidence producer failed and dependent validation jobs were skipped.
- Failed PR contract/bootstrap validation prevented affected-path and integration test execution, so those regressions remain unverified.
…and relocation 1. A failed adoption restores the ORIGINAL legacy link: the legacy link text is captured before the quarantine rename, and when the replacement publish fails the original is recreated at the bridge name (a concurrent occupant is preserved untouched, with the link recoverable). 2. The legacy source for callers that omit `home` derives from their own `paths.agentsSkillsDir`, falling back to the trusted home: the old `home ?? ""` shape produced a relative `.agents/skills` that could never pass removal's absolute-path trust check, wedging legacy callers. 3. Provenanced links re-point across a TRUSTED source relocation: when discovery resolves a different source than the ledger records (a Paseo update relocating the app bundle), recorded links pointing into the LEDGER's own recorded source are adopted to the new source — still provenance-gated on the link's current target, so a user's hand edit to an unrelated target remains a conflict. The documented convergence now holds for source moves, not only skill adds/drops. 4. The quarantine-unlink residual window (between the inode check and the unlink) is inherent to pathname-based deletion for symlinks — the identity-bound natives refuse S_IFLNK — and is already the narrowest-available shape with detection on both sides; recorded here as the accepted residual rather than papered over. Lore-id: b1c2d3e4 Constraint: adoption failure never loses the original link Constraint: relocation adoption requires the link to point into the ledger's own recorded source Confidence: high Scope-risk: moderate Reversibility: easy Tested: new adoption-failure regression (occupant preserved, link recoverable); suite 156 pass Not-tested: the microsecond kernel window between inode check and unlink (accepted residual, detected by the post-unlink sweep)
Fix-forward: the r18 findings closedCurrent PR head:
Verification at the exact head: 156/156 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 4 actionable issues, led by Bridge-path validation trusts arbitrary in-root directories and Canonicalize intent paths before recovery. These findings require changes before approval.
Findings / Required Changes
- [P1] Bridge-path validation trusts arbitrary in-root directories.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:121-127
withinTrustedRootaccepts any directory beneath the agent root whose basename matches the Paseo naming pattern, without requiring the current bridge path or a genuine migration ledger. A tampered ledger can therefore direct removal at a foreign in-root*-paseo-skillsdirectory. Restrict accepted paths to the current configured bridge or a separately authenticated migration record before unregistering or deleting entries. - [P1] Canonicalize intent paths before recovery.
Reference:packages/coding-agent/src/setup/paseo/paseo-ownership.ts:342-361
Recovery checks provenancePath only with lexical path.relative, so a symlinked ancestor can redirect an apparently in-tree path outside the agent directory; resolve and validate real ancestors (or reject symlinked ancestry) before reading or writing recovery state. - [P1] Preserve exported bridge DTO compatibility.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:100-121
SkillsBridgePreflight and SkillsBridgeInstallResult add required fields, so existing callers constructing the previously valid shapes no longer compile; make new fields optional with backward-compatible defaults or provide a versioned API. - [P1] Validate sidecar contents before deletion when the provider entry is absent.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:206
The cleanup loop deletes a ledger-referenced deterministic sidecar based only on its path when the provider key is already absent; it does not verify the sidecar’s key/value digest. A user file or attacker replacement at that path can therefore be removed. Read and authenticate the sidecar with readReplacedProviderBackup before deleting it, and fail closed on mismatch.
CI / Verification
- Reviewed the exact remote head:
81eaaf4c4f71039a53e0fd7752256b5c88283aaf. - CI summary: 6 passing, 7 failing, 23 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,PR contract bootstrap,Affected path validation. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | API boundaries are not fully compatibility-safe: exported bridge DTOs break existing constructors, and intent recovery needs canonical path confinement. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | Install/remove mutations are serialized, but removal can delete an unauthenticated sidecar when the provider entry is absent. |
| A3. Security / Privacy / Trust | APPROVED | Security and privacy protections appear strengthened; no concrete exploitable regression was established from the available source evidence. |
| A4. Verification / Tests / CI | APPROVED | A4 review found extensive regression tests, but CI failures and skipped validation prevent confirming the suite passed. |
| A5. Context / Compatibility / Platform | CHANGES_REQUESTED | Integration and documentation behavior is broadly coherent, but removal remains exposed to tampered in-root bridge paths; platform-specific CI validation is incomplete. |
Limitations
- Relevant platform and affected-path CI jobs were skipped or failed, so this review cannot claim successful automated validation across supported platforms.
fix(setup): close the r19 review findings on in-root trust and sidecar authentication 1. An in-root recorded bridge path is accepted only when it is the CURRENT configured bridge, or a migration record authenticated by CONTENT: the recorded directory must actually hold the recorded links (at least one present, each a symlink into the ledger's recorded source). A foreign *-paseo-skills sibling holding user content is refused however the ledger spells the claim — the earlier basename-family rule alone let a tampered ledger aim cleanup at any bridge-shaped in-root directory. 2. An absent provider key's sidecar is authenticated fd-bound (key match + digest match) before deletion, exactly like a restore: a user file or attacker replacement at the deterministic path fails the digest, is preserved, and is reported for manual cleanup instead of deleted. 3. The intent record's provenance-path confinement is canonicalized: both trees are realpath-resolved before comparison, so a symlinked ancestor can no longer make an out-of-tree path satisfy the lexical check. Lore-id: c3d4e5f6 Constraint: an in-root bridge claim needs content authentication, not just a name shape Constraint: every sidecar deletion is digest-authenticated fd-bound Confidence: high Scope-risk: moderate Reversibility: easy Tested: new foreign-sibling regression (user content survives); suite 157 pass Not-tested: none EOF )
Fix-forward: the r19 findings closedCurrent PR head:
Verification at the exact head: 157/157 @snowykr @probepark — approval target is — |
snowykr
left a comment
There was a problem hiding this comment.
Verdict
CHANGES_REQUESTED
Summary
The five-axis review completed against the exact head and identified 4 actionable issues, led by Authenticated sidecar can be replaced before deletion and Bridge quarantine can delete a concurrent foreign symlink. These findings require changes before approval.
Findings / Required Changes
- [P1] Authenticated sidecar can be replaced before deletion.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:535-570
removal authenticates the sidecar by fd-bound read, then calls fs.rm on the pathname; an attacker can swap that pathname after authentication and cause an unrelated file to be deleted. Delete only the authenticated inode or re-open and verify identity immediately before unlinking. - [P1] Bridge quarantine can delete a concurrent foreign symlink.
Reference:packages/coding-agent/src/setup/paseo/skills-bridge.ts:141-228
quarantineUnlinkVerified renames whatever currently occupies linkPath, then validates only after the rename; a concurrent replacement that points to the expected target can therefore be deleted. Bind the operation to the preflight inode or use descriptor-/no-replace deletion semantics, and preserve any replacement. - [P1] Credential-bearing sidecar deletion is vulnerable to a TOCTOU replacement.
Reference:packages/coding-agent/src/setup/paseo/remove.ts:527-572
The sidecar is authenticated through an fd, the handle is closed, and then fs.rm(path) deletes by pathname. An attacker who can modify the config directory can replace the authenticated path before deletion, causing an unintended file to be removed. Keep the authenticated descriptor/inode through deletion or use an atomic no-clobber, inode-bound removal and fail closed on replacement. - [P1] Injected source absence is ignored.
Reference:packages/coding-agent/src/setup/paseo/check.ts:159
When skillsSource is provided but resolves to undefined, nullish coalescing invokes production discovery, violating the dependency-injection contract and potentially inspecting real user/app paths. Treat an explicitly supplied resolver as authoritative, including undefined.
CI / Verification
- Reviewed the exact remote head:
8f1cca11d9351843f348330cb62a1e9c32002653. - CI summary: 6 passing, 7 failing, 23 pending/cancelled/skipped.
- Failing checks:
Validate exact-head PR contract,PR contract bootstrap,Affected path validation. - Repository policy permits review before all gating checks pass; the current non-passing checks are recorded above and do not establish that checks passed.
Axis Coverage
| Axis | Verdict | Coverage |
|---|---|---|
| A1. Intent / Policy / Contract | CHANGES_REQUESTED | API compatibility is mostly preserved, but check-mode dependency injection can cross the intended filesystem boundary when an injected resolver reports no source. |
| A2. Architecture / Correctness / Failure | CHANGES_REQUESTED | A2 correctness and concurrency are not fully safe: bridge and sidecar cleanup retain pathname races that can delete concurrent or foreign filesystem objects. |
| A3. Security / Privacy / Trust | CHANGES_REQUESTED | A3 is not fully safe: credential-bearing sidecar cleanup remains vulnerable to pathname replacement after authentication. |
| A4. Verification / Tests / CI | APPROVED | A4 review completed; test coverage is broad, but failed and skipped CI prevents asserting that the changed test suite and affected validations pass. |
| A5. Context / Compatibility / Platform | APPROVED | Paseo source discovery, bridge convergence, trusted overrides, removal safety, and integration documentation show no established actionable risk. |
Limitations
- Skipped Windows and macOS platform jobs, plus failed affected-path validation, prevent claiming CI-validated platform compatibility.
regression-riskhigh-riskProblem
On a machine where Paseo is installed as the desktop app (Paseo.app 0.4.0),
gjc setup paseo --checkcould never reach a clean verdict. The skills bridge assumed (a) a compiled-in five-name allowlist and (b)~/.agents/skillsas the only source directory. Neither holds for an app install:/Applications/Paseo.app/Contents/Resources/skills, so~/.agents/skillsdoes not exist and GJC created five symlinks into a directory that does not exist;paseo-help, not the allowlistedpaseo-loop, so every reachable state reportedorphan-skill/unlinked-skilldrift and exited 1, with no action that makes it pass.Fix
resolvePaseoSkillsSource):~/.agents/skillsfirst, then a fixed candidate list of Paseo.app bundle paths (/Applicationsand~/Applications, darwin only), or an explicitPASEO_SKILLS_DIRoverride honored only when absolute, present, and absent from every project dotenv variant — never a filesystem search, never a dangling target. Every default home path derives from the provenance-checked trusted home (getTrustedHomeDir()), never raw$HOME.paseo-prefixed directory. A release that adds or drops a skill no longer wedges--checkred.missing-skills-directoryonce; a fresh install with no source anywhere now completes the provider/orchestration setup and skips the bridge instead of failing the whole install (an existing unowned bridge directory is still refused).--removeproves ownership from the recorded provenance ledger, install/remove serialize on one per-agent mutation lock, and a ledger that is corrupt, tampered, escaping, symlinked, or routed through a symlinked ancestor fails every destructive path closed.--forceoverwrite preserves the replaced entry (any JSON shape) in a private mode-0600 sidecar beside Paseo's own config — the ledger and intent record carry only the pointer, and--removerestores from the sidecar, failing closed if it is missing.Review findings addressed (reconstructed onto current dev)
All rounds through the round-8 head are addressed; the round-7 head moved again to answer its six remaining findings in the rebased series:
env/argument content) live only in a 0600 sidecar beside Paseo's own config; ledger/intent record the pointer; removal restores from it and fails closed when it is unreadable. The FIRST replaced value is the user's; repeated--forcenever overwrites the sidecar.~/Applicationscandidates, and~/.paseoconfig paths resolve throughgetTrustedHomeDir(), so a planted$HOMEcannot steer global setup at attacker content.--checkstill reportsmissing-skills-directory.Tests
131 tests in
setup-paseo.test.ts(6 new r7 regressions: sidecar-backed restore with value-free ledger bytes; missing-sidecar fail-closed removal; trusted-home defaults under a planted$HOME; symlinked-ancestor refusal; no-source nonfatal install; preflight-identity refusal; genuine lock overlap). The pre-existing provider-config, CAS, backup-safety, intent-recovery, and saga-compensation suites are unchanged and still pass.Verification run (exact head 8f1cca1, base dev @ 08d0024)
Reconciled onto current dev twice: first after #4730 (18
fix(sdk)ACP commits), then after the herdr/sdk/sentry wave landed (33 commits to08d002464, zero overlap onsetup/paseo, its imports, or the test suite); the PR source is byte-identical across every reconciliation (each is a clean cherry-pick series).bun test packages/coding-agent/test/setup-paseo.test.ts packages/coding-agent/test/setup-cli.test.ts— 149 pass, 0 failbun --cwd=packages/coding-agent run check(biome + tsc) — cleanbun --cwd=packages/coding-agent run build— cleanbun run ci:test:smoke— okcheck-visible-definitions,verify-g002-gates,rebrand-inventory --strict,default-gjc-definitions(33),verify-gjc-state-writers --fail— all passgjc-state-gatesgroups static/runtime/integrity/read — all pass locallygit diff --check— cleanbun test packages/coding-agent/test/setup-paseo.test.tsafter the r8–r16 fix-forwards — 155 pass, 0 fail (24 new r8–r16 regressions)r8 fix-forward (heads 494cb11 + e969378)
The exact-head review at
c0502d09found two ownership defects; both are fixed and regression-pinned:a/banda_bcan never share one sidecar; publication is a hard-link (EEXIST never clobbers — same key + exact value is idempotent, anything else fails closed as asidecar-conflictrefusal), and creation happens in a newpersisthook insiderunJsonStep, only between a successful publish and the ledger commit (removed again on compensation). A refused publish now leaves no sidecar behind.bridgeDirCreatedcreator bit is committed only after the exclusivemkdirininstallSkillsBridgesucceeded; a fresh no-source run records no bridge ownership at all, so a directory the user later creates at that path can never be mistaken for GJC's work by--remove.e969378f) — the boundary architect independently found that Bun's dotenv mode defaults to development, so.env.development/.env.development.localload even withNODE_ENVunset (the common interactive case), while thePASEO_SKILLS_DIRtrust check enumerated only the threeNODE_ENVliterals. The variant set now carries the development-mode files for every mode exceptproduction/test(with.env.production.localalways checked), verified empirically against the pinned runtime and pinned by a new regression.e762800e) — when sidecar creation fails after the provider CAS publish already succeeded (a pre-planted file at the injective path), the publication is now reverted through the step's own compensation mutation and the intent record cleared, so the failure never strands an unowned--forceoverwrite.8f1cca11) — an in-root recorded bridge path is accepted only when it is the CURRENT configured bridge or a migration record authenticated by CONTENT (the directory must actually hold the recorded links pointing into the ledger's source; a foreign*-paseo-skillssibling with user content is refused); an absent key's sidecar is fd-bound digest-authenticated before deletion; and the intent record's provenance-path confinement is canonicalized through realpath.81eaaf4c) — a failed adoption restores the ORIGINAL legacy link (the text is captured before the quarantine rename and recreated on failure; a concurrent occupant is preserved); callers omittinghomeget their legacy source from their ownpaths.agentsSkillsDiror the trusted home (no more unremovable relative path); and provenanced links re-point across a TRUSTED source relocation — a Paseo update moving the app bundle now converges the bridge instead of conflicting, still gated on the link pointing into the ledger's own recorded source.4675924f) — quarantined symlink restoration is atomic no-clobber (fs.symlinkfails EEXIST against a concurrent occupant instead of the rename clobbering it);unpersistbinds to the sidecar's authenticated{key,value}bytes through the fd-bound digest read (a same-sized attacker replacement fails the digest and is preserved — the earlier size-only check both was spoofable and omitted the wrapper); thePASEO_SKILLS_DIRdotenv trust rule is documented with its workaround; and the migration-safe resolver fallback honors a caller-suppliedpaths.agentsSkillsDirwhen discovery finds nothing, as the pre-bug(setup paseo): skill bridge can never reach a clean --check against Paseo desktop 0.4.0 (hardcoded allowlist + missing ~/.agents/skills) #4638 seam did.167771c3) — everyproviderReplacedEntriesreference is validated against the deterministic sidecar path before any deletion (a tampered ledger can no longer redirect--remove'sfs.rmat an arbitrary absolute file);unpersistremoves only a sidecar whose bytes match what the failed run wrote (a pre-existing sidecar is the user's or an attacker's, never ours); and the no-clobber quarantine restore adds a post-rename identity check (a displaced foreign entry is moved aside to a recoverable name instead of vanishing).3965b0df) — a concurrent external target change during rollback is a FAILED rollback (the intent record and persisted artifact survive for recovery instead of being cleared beside an unprovenanced write), and quarantined-link restoration is no-clobber (an entry claiming the bridge name in the window is left untouched; the quarantined object stays recoverable at its quarantine name — the no-replace natives refuse symlink sources, so vacancy-check-then-rename is the narrowest available shape).7ba40f08) — destructive bridge cleanup is confined to the bridge-directory name family and a migration record must be authenticated by the OLD directory's own ledger claiming the exact path (a fabricatedpaseo/provenance.jsonshape beside a victim no longer authorizes cleanup); the mutation lock is documented never to displace a provably-live owner (pid + start-time identity; the stale window bounds only the liveness-indeterminate fallback); the persist hook's ATTEMPT is tracked so a sidecar created before a throw is always rolled back; andPaseoPaths.agentsSkillsDiris required again (the shape external callers already constructed).f2537966) — quarantined bridge-link deletion is bound to the captured inode (dev/ino/size/mtime re-checked immediately before every unlink attempt; any pathname replacement makes the deletion refuse and restore — a foreign object is never unlinked);readIntentvalidates every field recovery trusts and refuses a recorded provenance path escaping the agent directory; and theskillsSourceseam is migration-safe (an omitted resolver falls back to the real discovery order — existing callers keep compiling, no silent bridge skip).9b5e36e3) — the drift scan canonicalizes the skills source exactly like installation (a symlinked source directory passes--checkwith no false drift); the recorded-source trust rule mirrors discovery exactly (only~/.agents/skills, the fixed app-bundle roots, or the live override — an arbitrary home subdirectory a tampered ledger names is refused); and the optionalPaseoPaths.agentsSkillsDirfield is restored for exported-contract compatibility (the resolved per-run source remains what the ledger records).8fad904e) — quarantined bridge-link deletion re-verifies kind+text immediately before the unlink, treats an unlink failure as divergence unless the verified link remains, and sweeps any foreign object swapped onto the quarantine name back to the bridge name (never deleting it); discovery records the CANONICAL resolved source and the override's resolved target is trusted, so a symlinked or relocated skills source installs and removes symmetrically; and a replaced-provider sidecar whose entry is already absent is still deleted while any deletion failure fails removal closed (no orphaned credential sidecar, no swallowed errors).a64fcf64) — a failure between a successful CAS publish and the ledger commit now unpersists the sidecar and reverts the publication (no orphaned credential-bearing file);writeIntentpublishes via fsync+rename and a present-but-malformed intent record is an explicit corrupt-record refusal (--checkreports it as drift, recovery refuses) instead of reading as absent; non-string record values are ledger corruption; the recorded bridge source is validated against trusted Paseo source roots before any removal unlink; andPaseoPathsdrops the optional skills-directory field entirely (the source is resolved per run). The recorded-source trust gate additionally runs in the same pre-settings window as the bridge-path validation (1056da0e), so a tampered source refuses before theskills.customDirectoriesregistration is unregistered.8f530482) — mid-bridge failures correct the ledger to observed reality (SkillsBridgePartialErrorcarries the partial install result; planned-but-never-created entries stop being claimed); malformed provenance fields raiseProvenanceLedgerCorruptErrorinstead of silently defaulting (absent fields keep old-shape defaults); sidecar reads are fd-bound and symlink-rejecting (O_NOFOLLOW+ fstat on the same handle);--checkreports exactly one deterministic drift reason per bridge entry; theskillsSourceseam is mandatory so an omitted resolver can never silently disable the bridge.GJC verdict
—
[repo owner's gaebal-gajae (clawdbot) 🦞]