Skip to content

Harden inline script cache lifecycle coordination - #1735

Merged
Stella Huang (StellaHuang95) merged 8 commits into
microsoft:mainfrom
StellaHuang95:copilot/pep723-clear-root-hardening
Aug 26, 2026
Merged

Harden inline script cache lifecycle coordination#1735
Stella Huang (StellaHuang95) merged 8 commits into
microsoft:mainfrom
StellaHuang95:copilot/pep723-clear-root-hardening

Conversation

@StellaHuang95

@StellaHuang95 Stella Huang (StellaHuang95) commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Context

Inline-script environments live in a shared extension cache that may be accessed by more than one extension host. Clearing that cache is destructive and must coordinate with lock recovery, persistent-state clears, and inline association persistence.

The dedicated inline clear command remains hidden behind the undeclared, default-off inline-script flag and is not contributed to the Command Palette.

What changed

This PR is scoped to three coordinated hardening changes:

  1. Atomic canonical lock retirement on release (src/common/lockfile.apis.ts) — release() renames the owner marker to a transient .release-* marker and atomically renames the lock directory to a .retired-* sibling before cleaning it up out of band. Transient EPERM/EBUSY/EACCES retirement failures retry while ownership is retained; a terminal failure restores ownership and throws ELOCKRELEASEFAILED. Lock inspection recognizes the transient .release-* marker and tolerates the directory being retired between lstat and readdir.

  2. Serialized persistent-state clears (src/common/persistentState.ts) — every clear request is serialized through a failure-isolated queue tail, each request's key options are preserved, and clearPersistentState gains preserveWorkspaceKeys/preserveGlobalKeys (ClearPersistentStateOptions).

  3. Generic Clear Cache preserves the inline association key (src/common/constants.ts, src/features/envCommands.ts, src/extension.ts, and an import-only move of INLINE_SCRIPT_ENVS_KEY in the inline-script manager) — python-envs.clearCache now routes through clearEnvironmentCachesCommand, which preserves the inline association workspace-state key (built on the serialized-clear preserveKeys plumbing from change 2) while retaining existing behavior for all other state and managers.

Behavior and compatibility

  • No setting, command, menu, view, or status-bar contribution is added.
  • Default-off users do not register the inline manager or dedicated command.
  • Generic clear continues clearing existing non-inline managers and state. The only inline-specific difference is that dormant associations are no longer forgotten while their cache directories remain.
  • Production lock API callers remain confined to the hidden inline manager.

Reviewer guide

  1. Review release-side lock retirement, ownership restoration, and inspection tolerance in lockfile.apis.ts.
  2. Review persistent-clear serialization and preserved keys in persistentState.ts.
  3. Review generic clear routing and inline-association preservation (envCommands.ts, extension.ts, constants.ts).

Validation

  • npm run compile-tests
  • npm run compile
  • npm run lint
  • Unit suite: 1844 passing

Note: This PR was reduced in scope from an earlier revision. Root-generation/admission changes, reclaim-side lock retirement, workspace-root settings protection, create-counting order, and the typed generic-inline unsupported error were reverted to the base; the net diff now covers only the three changes above.

@StellaHuang95 Stella Huang (StellaHuang95) added the feature-request Request for new features or functionality label Aug 25, 2026
@heejaechang

Heejae Chang (heejaechang) commented Aug 25, 2026

Copy link
Copy Markdown

🔒 Automated review in progress — Heejae Chang (@heejaechang) is auto-reviewing this PR.

Comment thread src/common/lockfile.apis.ts Outdated
Comment thread src/managers/builtin/inlineScript/envManager.ts Outdated
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved via Review Center.

Comment thread src/common/lockfile.apis.ts Outdated
Comment thread src/common/lockfile.apis.ts
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

@heejaechang Heejae Chang (heejaechang) added review-auto:changes-requested Automated review: posted blocking findings to address. and removed review-auto:approved Automated review: no blocking findings (approval posted). labels Aug 25, 2026
Comment thread src/common/persistentState.ts Outdated
Comment thread src/common/lockfile.apis.ts Outdated
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Comment thread src/common/lockfile.apis.ts Outdated
Comment thread src/common/persistentState.ts
Comment thread src/common/lockfile.apis.ts
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-key preservation

Reduce this PR's net diff versus its merge-base to only three logical changes:

- #3 atomic canonical lock retirement on release (src/common/lockfile.apis.ts)
- #6 serialized persistent-state clears (src/common/persistentState.ts)
- #7 generic Clear Cache preserves the inline association key
  (constants.ts, envCommands.ts, extension.ts, and an import-only change in
  the inline-script envManager)

Revert the other seven changes to the merge-base content:
- #1 root-generation nonce, #2 root/entry admission decoupling,
  #9 create-counting order (inline-script envManager)
- #4/#5 reclaim-side lock retirement + generation-specific inspection
  (keep only the minimal release-side companions inspect/reclaim need to
  stay correct: .release-* recognition and ENOENT-on-readdir tolerance)
- #8 workspace-root protection (settingHelpers)
- #10 typed ClearCacheNotSupported (envManagers, NotSupportedError)

Also revert the associated test changes for the removed items, keeping the
new persistentState suite (#6), the new Clear Environment Caches suite (#7),
and the release-side lock-retirement tests (#3).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…iew feedback)

release() could wedge a lock handle when both the canonical-directory
retirement AND the ownership-restoration rename failed: it threw with the
handle still 'held' and the owner marker already renamed away, so every
later release() hit ENOENT -> ECOMPROMISED forever.

Introduce a 'releasing' LockState. release() now transitions the owner
marker to the '.release-*' marker, sets state 'releasing', then retires the
canonical directory. If retirement fails:
- restore succeeds -> revert to 'held' and throw ELOCKRELEASEFAILED
  (unchanged behavior), or
- restore fails -> stay 'releasing' and throw ELOCKRELEASEFAILED so a later
  release() on the same handle resumes retirement instead of wedging.

Retired-directory scavenging is intentionally unchanged (a leaked '.retired-*'
artifact still persists, per the existing interrupted-artifact test).

Add a unit test that fails both the retirement and the restoration rename,
asserts the handle reports 'held' with a live '.release-*' marker, then
resumes and completes release() on the same handle.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ng clear queue (review feedback)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
@StellaHuang95
Stella Huang (StellaHuang95) force-pushed the copilot/pep723-clear-root-hardening branch from c3544ce to bddd428 Compare August 26, 2026 00:17
Comment thread src/common/persistentState.ts
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

…review feedback)

- persistentState.set() now joins the clear queue as the new tail so a later
  clear() cannot be resurrected by a lagging write (review r3858573315 / r3858651583).
- Inline lock release() is serialized through a shared in-flight promise so two
  concurrent releases no longer race and spuriously report ECOMPROMISED; the
  handle stays retryable after a failed release (review r3858573008 / r3858434379).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

@heejaechang Heejae Chang (heejaechang) added review-auto:approved Automated review: no blocking findings (approval posted). and removed review-auto:changes-requested Automated review: posted blocking findings to address. labels Aug 26, 2026
Merge latest upstream microsoft/main (9e44ce1) into the PR branch to resolve
the conflict GitHub reported (mergeable=false / dirty).

Only one file conflicted: src/test/features/envCommands.unit.test.ts, where both
sides inserted different imports at the same location. Resolved by keeping both
sets of imports (ours: shellProviders + ShellStartupScriptProvider for the Clear
Environment Caches suite; upstream: terminalRunner + TerminalManager for the new
terminal tests), ordered alphabetically. All four imports are used in the file.

src/features/envCommands.ts and src/managers/builtin/inlineScript/envManager.ts
auto-merged cleanly and were verified semantically: our clearEnvironmentCachesCommand
and INLINE_SCRIPT_ENVS_KEY re-export are preserved alongside upstream's terminal
return-fix and uv interpreter/version handling.

Net PR diff vs upstream/main remains the intended 9-file scope plus review fixes:
atomic lock retirement with resumable/concurrent release, persistent-state clear
and set serialization, and generic Clear Cache preserving inline associations. No
new files were introduced into the PR diff by conflict resolution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
Revert the broad PersistentState mutation queue so non-inline consumers keep
their pre-PR get/set/clear behavior. The general concurrency hardening is
replaced by the pre-PR clearing gate; only a no-op `preserveKeys` filter and
`clearPersistentState(options)` remain so the generic Clear Cache command can
preserve the inline association key without changing any other caller.

Move the dedicated inline-cache deletion off the shared
`PersistentState.clear([key])` (which coalesces onto an in-flight generic clear
and could be silently dropped) and onto the inline-owned persistence queue via
`set(key, undefined)`. Generic-preserve and inline-delete stay key-disjoint and
the dedicated deletion cannot be lost or resurrected. The atomic-rename lock
release hardening and its tests are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
const _keys = (keys ?? this.momento.keys()).filter((key) => !preservedKeys.has(key));
await Promise.all(_keys.map((key) => this.momento.update(key, undefined)));
this.clearing.resolve();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue · Please address or respond

This is still a coalescing gate rather than a failure-isolated queue: concurrent callers await the first request and their own keys/preservation options are never applied. More importantly, if any momento.update rejects, this.clearing.resolve() is skipped, leaving the deferred pending so every later clear() waits forever. Queue each captured request independently and ensure a failed request cannot block later clears.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right on both counts: the shared PersistentState.clear() is a coalescing gate, and a rejected Memento.update skips this.clearing.resolve(), leaving the deferred pending so every later clear() waits forever. Rather than reshape that shared implementation in this PR, I've taken the PR out of it entirely.

  • src/common/persistentState.ts is now reverted to base — this PR has no production diff in that file. Its general coalescing/wedge defect is unchanged and explicitly out of scope for this inline-only PR (happy to track it separately).
  • Inline PEP 723 associations moved to a new inline-owned, Memento-backed InlineScriptAssociationStore that owns only INLINE_SCRIPT_ENVS_KEY. It has its own failure-isolated FIFO queue: each caller awaits its own enqueued operation and receives that operation's success/failure, and a rejected op still advances the tail (this.tail = run.then(ok, ignore)) so a failed read/write/delete can't block later inline operations. The store never awaits the shared clear() deferred, so a wedged generic clear cannot stall inline work.
  • Generic "Clear Cache" no longer needs any preserve option in shared code: at the command it reads the current workspace keys from the Memento, excludes INLINE_SCRIPT_ENVS_KEY, and calls the existing PersistentState.clear(explicitKeys) plus the existing global clear(). Generic clear and the inline store are key-disjoint (generic clears every key except the inline key; the store touches only the inline key), so neither can drop the other, and the dedicated inline delete is a queued direct update of that one key.

Net: this PR leaves the shared clear() behavior untouched, and inline association reliability no longer depends on it.

@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Result: could-not-verify

Summary: No meaningful test execution was possible because the verification container could not start and local execution was not authorized. The PR adds 14 targeted unit tests covering lock retirement, persistent-state clearing, generic cache clearing, and inline persistence coordination. Their pass/fail status remains unverified.

Test runs: 2 not run

  • ⚠️ Not run | Lockfile and persistent-state unit suites | npm run compile-tests && npm run unittest -- --grep "(lockfile APIs|persistent state clearing)"
  • ⚠️ Not run | Environment cache command and inline manager unit suites | npm run compile-tests && npm run unittest -- --grep "(Clear Environment Caches Command Tests|InlineScriptEnvManager)"
⚠️ Lockfile and persistent-state unit suites diagnostic output
Container verification could not start: no trusted sandbox image is configured for microsoft/vscode-python-environments; local execution was not authorized.
⚠️ Environment cache command and inline manager unit suites diagnostic output
Container verification could not start: no trusted sandbox image is configured for microsoft/vscode-python-environments; local execution was not authorized.

@heejaechang Heejae Chang (heejaechang) added review-auto:changes-requested Automated review: posted blocking findings to address. and removed review-auto:approved Automated review: no blocking findings (approval posted). labels Aug 26, 2026
Stop this PR from modifying or depending on the shared PersistentState
implementation. src/common/persistentState.ts is reverted to base (no
production diff); its general coalescing/failure defect is left unchanged
and out of scope for this inline-only PR.

Inline PEP 723 script-to-environment associations now live behind a new
inline-owned InlineScriptAssociationStore (Memento-backed). It owns only
INLINE_SCRIPT_ENVS_KEY, exposes no arbitrary keys, and serializes every
read/mutation/deletion on its own failure-isolated FIFO queue: each caller
gets its own operation's result or rejection, and a failed operation still
advances the tail so later operations run. Verified writes mirror the prior
PersistentState.set read-back semantics. All inline association paths
(activation load, reads/rehydration, invalid-entry removal, updates,
snapshots, and dedicated deletion) route through the store; the manager no
longer depends on PersistentState. The dedicated clear is a queued direct
update of only INLINE_SCRIPT_ENVS_KEY to undefined, so it cannot be
coalesced onto (and dropped by) an overlapping generic clear.

Generic "Clear Cache" preserves the inline key without changing
PersistentState: it reads the current workspace keys from the injected
Memento, excludes INLINE_SCRIPT_ENVS_KEY, and calls the existing
PersistentState.clear(explicitKeys) plus the existing global clear() in
parallel, keeping the prior command ordering (persistent state, then
managers, then shell profile cache). Generic clear and the inline store are
therefore key-disjoint and deterministic, and a wedged shared clear cannot
block inline association work.

Lock-release hardening and non-inline environment managers are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b7253a6a-a3c5-4606-b9cf-ec52db1e605f
@heejaechang

Copy link
Copy Markdown

Verification: The relevant tests could not be fully run in the isolated environment; this review is not fully verified.

Result: could-not-verify

Summary: No tests could execute because the verification container failed to start: no trusted sandbox image is configured for this repository, and local execution was not authorized. The PR adds 19 targeted unit tests covering lock retirement, cache preservation, and inline association serialization. Because no meaningful test command ran, the verdict is **could-not-verify**.

Test runs: 1 not run

  • ⚠️ Not run | Targeted test discovery and dependency check | git status --short && git branch --show-current && git --no-pager diff --name-status upstream/main...HEAD && node -e "const p=require('./package.json'); console.log(JSON.stringify({scripts:p.scripts,devDependencies:p.devDependencies&&{mocha:p.devDependencies.mocha,tsnode:p.devDependencies['ts-node']}},null,2))" && if [ -f .github/instructions/testing-workflow.instructions.md ]; then sed -n '1,220p' .github/instructions/testing-workflow.instructions.md; fi && printf '\nNODE_MODULES=' && if [ -d node_modules ]; then echo present; else echo missing; fi && printf 'PROFILE=%s\n' "${AUTOMATION_SANDBOX_PROFILE:-}" && find src/test -type f ( -name 'lockfiletest.ts' -o -name 'envCommandstest.ts' -o -name 'associationStoretest.ts' -o -name 'inlineScripttest.ts' ) -print
⚠️ Targeted test discovery and dependency check diagnostic output
Container verification could not start and local execution was not authorized for this PR HEAD: No trusted sandbox image is configured for microsoft/vscode-python-environments.

@heejaechang Heejae Chang (heejaechang) added review-auto:approved Automated review: no blocking findings (approval posted). and removed review-auto:changes-requested Automated review: posted blocking findings to address. labels Aug 26, 2026
@StellaHuang95
Stella Huang (StellaHuang95) merged commit d0d52d1 into microsoft:main Aug 26, 2026
48 checks passed
@StellaHuang95
Stella Huang (StellaHuang95) deleted the copilot/pep723-clear-root-hardening branch August 26, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature-request Request for new features or functionality review-auto:approved Automated review: no blocking findings (approval posted).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants