Skip to content

fix(plugin-catalog): the repair lane re-asserts only from a source trusted to be the recorded one - #6533

Merged
rbuergi merged 1 commit into
mainfrom
fix/6512-repair-binds-a-trusted-source
Oct 12, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/6512-repair-binds-a-trusted-source

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

What was wrong

InstanceAutoRegistrationService.ReassertInstalled, the boot repair lane, chose the listing to re-assert from by package id. Since #6496 it preferred a listing whose PackageManifest.Source equals the install record's source. A directly read repository can author that field in its package.json. So another direct repository listing the same id, and claiming the recorded source's name, could be chosen. For a free package, Land then fetched and wrote that repository's files over the installed package. (No commercial authority leaked: ProvenanceIsTrusted already withheld it.)

The change

RepairCandidate(listings, recordedSource):

  • If the record names a source, take only a listing that is provenance-trusted to be that source: ProvenanceIsTrusted (a registry's stamp, or the candidate's Source equals the name its source was configured under) and the same source name.
  • If no such listing serves the package, it is not re-asserted, and a Warning names the id, the recorded source, and Repair lane: bind the re-assert candidate to a provenance-trusted source, not an authored one #6512.
  • Only a legacy record with no source keeps the first-listing fallback.

The partition, module-version and config-authority checks after selection are unchanged.

Tests

ARepairBindsToATrustedSourceTest (Memex.Portal.Shared.Test) has two direct sources listing the same id at the same module version:

  • the honest one, configured registered-1, served the install, so the record names registered-1;
  • the impostor, configured registered-0 and listed FIRST, authors "source": "registered-1" and serves a different Guide.md.

After a lost Guide, the repair restores the honest body. Control: the record names registered-1.

Falsified: with the old selection restored, the test fails — the restored Guide reads IMPOSTOR body.

Local: dotnet build test/Memex.Portal.Shared.Test -c Release -warnaserror → 0 warnings, 0 errors. The new test plus IncompleteInstallIsReassertedTest (the legacy no-source path), ADeploymentNamedCommercialPackage*, AnAuthoredSourceClaimsNoAuthorityTest, InstallCompletenessTest and InstallRecordFollowsItsPartitionTest → 45/45 passed.

Doc: Doc/Architecture/InstallCompleteness — new paragraph …and only from the source the record names, by provenance.

Closes #6512

Pairs-with: none — no public type or member removed.

🤖 Generated with Claude Code

…usted to be the recorded one

The boot repair lane picked the listing to re-assert from by package id,
preferring one whose PackageManifest.Source equals the install record's.
A directly read repository can author that field, so another repository
listing the same id under the recorded source's name could land its
files over the installed package. Where the record names a source, the
lane now requires a provenance-trusted match (a registry stamp, or the
candidate's configured source name equals the recorded source), and
names the refusal when none serves it. A legacy record with no source
keeps the first listing.

Test: ARepairBindsToATrustedSourceTest — an impostor configured as
another source, listed first and authoring the recorded name, landed its
Guide body before the change; the recorded source's body comes back
after it.

Closes #6512

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 11, 2026 20:04
@rbuergi
rbuergi enabled auto-merge October 11, 2026 20:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The implementation and regression coverage are sound; only a non-blocking XML documentation placement issue remains.

1 open finding
What changed in this PR

Hardens plugin repair so packages are restored only from their recorded, provenance-trusted source.

Changes:

  • Adds trusted-source candidate selection and fail-closed logging.
  • Adds an impostor-source regression test.
  • Documents repair provenance requirements.
File Description
InstanceAutoRegistrationService.cs Enforces trusted repair provenance.
ARepairBindsToATrustedSourceTest.cs Covers source impersonation.
InstallCompleteness.md Documents the security behavior.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 1953 to 1970
/// <summary>Whether two source names are the same non-empty source (case-insensitive, like
/// <see cref="PluginGrantEntry.Matches"/>). Pure.</summary>
/// <summary>
/// The listing the repair lane re-asserts an installed package from (#6512), or null when the
/// record names a source and no listing is TRUSTED to be that source. Trusted means a registry's
/// stamp, or the candidate's source being the name its source was configured under
/// (<see cref="ProvenanceIsTrusted"/>); a manifest that merely authors the recorded name is not
/// enough. A legacy record with no source keeps the first listing. Pure.
/// </summary>
/// <param name="listings">Every listing of one package id, in source order.</param>
/// <param name="recordedSource">The source the install record was stamped with, or null.</param>
private static InstallCandidate? RepairCandidate(IReadOnlyList<InstallCandidate> listings, string? recordedSource) =>
string.IsNullOrWhiteSpace(recordedSource)
? listings.FirstOrDefault()
: listings.FirstOrDefault(c => ProvenanceIsTrusted(c) && SameSource(c.Package.Source, recordedSource));

private static bool SameSource(string? a, string? b) =>
!string.IsNullOrWhiteSpace(a) && string.Equals(a.Trim(), b?.Trim(), StringComparison.OrdinalIgnoreCase);

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.

Confirmed: the one-line SameSource summary (lines 1953-1954) now sits directly above RepairCandidate's own summary, so RepairCandidate carries two <summary> elements and SameSource carries none. It is a misplaced doc comment only: no behaviour, no compile warning (the CI build is -warnaserror and green on this head), and RepairCandidate's own summary, which states its actual contract, is intact.

Under this round's converge rule a comment-only correction does not earn a push (every push restarts the review and the dependent-suites run on a congested queue), so I am leaving the head as it is. The two lines move back above SameSource the next time this file is edited.

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   12 files   -    12     12 suites   - 12   36m 24s ⏱️ - 17m 26s
7 510 tests  - 4 379  7 510 ✅  - 4 186  0 💤  - 193  0 ❌ ±0 
7 522 runs   - 4 381  7 522 ✅  - 4 188  0 💤  - 193  0 ❌ ±0 

Results for commit 381a9c9. ± Comparison against base commit c8f1f54.

This pull request removes 4411 and adds 8 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
…
Memex.Portal.Shared.Test.ARepairBindsToATrustedSourceTest ‑ ARecordNamingItsSource_IsRepairedFromThatSource_NotFromOneThatAuthorsItsName
Memex.Portal.Shared.Test.InstanceIdRulesMatchTheRegistryTest ‑ TheSetupHostAgreesWithTheRegistry(candidate: "97d76930-ced2-4989-80e6-10c9703bf416")
Memex.Portal.Shared.Test.SessionDenialIsAnAnswerTest ‑ OnlyAVerdictReadsAsADenial(shape: "the same verdict nested, as a late denial dispatch"···, failure: System.InvalidOperationException: write failed
 ---> System.UnauthorizedAccessException: Access denied
   --- End of inner exception stack trace ---, isDenial: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's FIRST member", fault: System.AggregateException: One or more errors occurred. (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall as an aggregate's SECOND member", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---
, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall nested in an aggregate inside a wrapper", fault: System.InvalidOperationException: outer
 ---> System.AggregateException: One or more errors occurred. (Value does not fall within the expected range.) (Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.)
 ---> System.ArgumentException: Value does not fall within the expected range.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.<---

   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "a stall wrapped as InnerException", fault: System.InvalidOperationException: outer
 ---> MeshWeaver.Mesh.QueryProviderStalledException: Query provider(s) [pg] did not emit an Initial within the query fan-in's 16s bound for query 'nodeType:NodeType' (user 'system'). The merged Initial gates on EVERY provider, so this query has NO snapshot to answer with — it is reported as unavailable (retryable) rather than left hanging with no error. This is an availability failure, never a permission verdict: a consumer deciding access must fail CLOSED and say it could not establish the answer. Fix the stalled provider; never bump the consumer's timeout.
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Compiler.Pipeline.Test.SourcesWatcherStallBackoffTest ‑ The_production_classifier_recognises_a_stall_through_every_wrapping(because: "an aggregate of unrelated faults", fault: System.AggregateException: One or more errors occurred. (The operation has timed out.) (Value does not fall within the expected range.)
 ---> System.TimeoutException: The operation has timed out.
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.ArgumentException: Value does not fall within the expected range.<---
, expected: False)

♻️ This comment has been updated with latest results.

@rbuergi
rbuergi merged commit 761352c into main Oct 12, 2026
78 of 82 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Repair lane: bind the re-assert candidate to a provenance-trusted source, not an authored one

2 participants