refresh: audit hooks/mcp/settings projection reads against config layers - #552
Open
NikashPrakash wants to merge 11 commits into
Open
NikashPrakash wants to merge 11 commits into
NikashPrakash wants to merge 11 commits into
Conversation
A field report claimed refresh dropped hook projections because the
projection path read the flat repo-local boolean instead of the
layer-resolved effective value, ignoring an org layer's hooks:true.
The audit found a different shape: refresh reads hooks/mcp/settings
neither flat nor effective. They participate in the layer merge (so
config explain reports them correctly) but no projection path consumes
the result — hooks, MCP configs, and settings project from whatever
exists under ~/.agents/{hooks,mcp,settings}/<scope>/.
Pin both halves together across five manifest/layer combinations: the
merge precedence the pointer migration exists to protect, and the
projection artifacts that must land regardless. The absent-key +
org-layer-true row is the reported shape and the one that guards
against a flat-read regression.
…ting The AgentsRC field comment and the three schema descriptions asserted that an explicit repo-local false disables the org layer's projection. It does not: it wins the layer merge and changes what config explain reports, but no projection path consults the resolved value. State the actual scope, and record why wiring it in is a behavior change rather than a fix — manifests written before the pointer migration carry an injected hooks:false their authors never wrote, so honoring it would disable hook projection for every such repo at once.
master's refresh scoping work replaced runRefresh's string project filter with the refreshScope struct. Carry the merge through the new regression test's single call site.
The hooks/mcp/settings design + audit notes sat wedged between field declarations. That prose is cross-cutting type-level documentation (layer-merge vs projection semantics), not per-field wire encoding, so it reads better as part of the AgentsRC doc comment with a short pointer left at the field group. Also keeps this change out of the pre-existing agentsRCCore mirror duplication span, which was charging the comment edit as new duplicated lines against the new-code quality gate.
…ce plan LoadEffectiveAgentsRC fixes one read-only, offline access mode for every consumer that is not already holding a resolved snapshot, so an org/team layer's keys are visible without turning best-effort paths into lock writers. ResourceSourcePlan derives the reachable source roots from the per-layer raw declarations instead of the ordered-replace Effective.Sources, keeping inherited roots searchable while excluding user-scope and synthesized-home roots from project resource materialization.
…m the effective config MaintainManagedGitignore now takes the resolved snapshot install/refresh already hold, so a layer-supplied gitignore_projections governs the managed block; an unreadable manifest or unreplayable layer stack still skips rather than guessing. Install resolves resource roots from the source plan, which surfaces roots an org/team layer declared. http/oci declarations reach install for the first time through those inherited layers; they resolve to a fetched blob rather than a resource tree, so they are named and skipped non-fatally.
The command surface (login, status, sync, outbox drain) and the workflow publication hook read the flat repo-local manifest, so a fleet that supplies the observability block from an org/team layer got 'not configured' and queued nothing. All of them now route through LoadEffectiveAgentsRC.
commands/workflow/graph.go still reads kg.graph_home flat, so a layer-supplied KG graph home is invisible to the native namespace commands. Routed as a task note on graph-backend-adapter-contract/t6-bridge-decommission rather than widening this change.
goreleaser 2.18.1 fails `goreleaser check` (exit 2) on homebrew_casks.url.verified, which breaks the GoReleaser validation step on every branch carrying current master. The url block held nothing else, so the anchor and its reference go with it.
Split the provenance/eligibility assertions into named helpers (S3776: cognitive complexity 16 > 15) and rename apiURL's local url copy so it no longer shadows the predeclared `copy` (S978).
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Ground-truth audit of a field report claiming
da refreshdropped hookprojections because the projection path consulted the flat repo-local
hooksboolean instead of the layer-resolved effective value — the same"layered-consumer-left-behind" class as the earlier relevance /
loadFlatSnapshotgap, and framed as completing #535 (which fixed onlythe marshal side).
The report does not hold, and the reason is not the one either side expected.
What the audit found
hooks,mcp, andsettingsare read at projection time neither flatnor effective — they are not read at all. The three keys participate in
the layer merge (so
da config explain hookscorrectly reports an orglayer's
true), but no projection path consumes the resolved value. Hook,MCP, and settings projection is driven entirely by what exists under
~/.agents/{hooks,mcp,settings}/<scope>/, viainternal/platform'sresolveHookSpec/resolveScopedFile. Neitherinternal/platformnorinternal/linksloads a manifest at all.Every non-test read of the three fields:
commands/internal/lifecycle/install.go:586-588rc.Hooks/MCP/Settingsinstall --generate --dry-runpreviewinternal/config/agentsrc.go:1496-1498pickStringsOrBool/pickBoolinternal/config/agentsrc.go:1112-1114,:1185-1187There is no
Snapshot.Effective.Hooks/MCP/Settingsread anywhere, noExtraFields["hooks"]-style read (all three are inagentsRCKnown, sothey can never land there), and no
StringsOrBool.Containscall outsidetests. The last behavioral consumer was a
da statusdisplay line removedin 70f23a3. There is no shell implementation left to hide one.
Verified end to end in a sandboxed
HOME/AGENTS_HOME, reproducing thereported shape: manifest omits
hooks, a local extends layer supplieshooks: true.Same result with
hooks:false,hooks:true, or the key absent, with orwithout a layer: the projected artifacts are byte-identical. The flag gates
nothing.
What this PR does
No behavior change. Two commits:
TestRunRefresh_ProjectionIgnoresHooksMCPSettingsFlags— drives the realrunRefreshover five manifest/layer combinations and asserts both halvestogether: the merge precedence (via
ResolveLocked, the surfaceconfig explainuses) and the projected artifacts (rendered hook command in.claude/settings.local.json,.mcp.json,.cursor/settings.json).The absent-key + org-layer-
truerow is the reported shape and is whatwould fail if anyone later wired projection to a flat read.
Corrects the
AgentsRCfield comment and the three schema descriptions,which asserted that an explicit repo-local
false"disables the orglayer's projection." It does not — it wins the merge and changes what
config explainreports.Why not just wire the flags in
Honoring the flags would be a behavior change, not a fix. Manifests written
before the pointer migration carry an injected
"hooks": falsetheir authorsnever wrote (that injection is exactly what #535 stopped). Gating projection
on the resolved value would disable hook projection for every such repo the
next time it refreshes. That needs a deliberate decision plus a migration
story; this PR pins the current contract so the change shows up as a
reviewed diff rather than a silent one.
Same-class flat reads found nearby (reported, not changed)
gitignore_projections—commands/internal/lifecycle/gitignore.go:52,61.The one real flat-read behavioral gate at projection time, reached from
both refresh and install. Deliberate and documented as such; left alone.
sourcesat install —commands/internal/lifecycle/install.go:184resolvesflat
rc.Sourceswhile skills/agents link fromSnapshot.Effective(
:196). Mitigated by design:linkInstallResourcesalways appendsconfig.AgentsHome(), so a layer-supplied skill still resolves from thehome store. Worth a look, not obviously broken.
kg.graph_home(commands/workflow/graph.go:87) andobservability.*(
commands/observability/cmd.go:64,317-326,commands/workflow/observability_hook.go:94,171) are flat reads oflayer-mergeable keys, so an org layer setting them is invisible. Outside
the projection/lifecycle layer and adjacent to in-flight
commands/kgwork, so untouched here.
featureshas no consumer at all outsideconfig explain --flags.Verification
go test ./internal/... -skip TestConfigLoadSave— passgo test ./commands/...— passgo test ./... -skip TestConfigLoadSave— passgofmt,go vet ./commands/... ./internal/config/...— cleanHOME/AGENTS_HOMEE2E as above; no real repo or home touched