BLOCKER: no shipped path can grant derived_names, so a package using #86 gap 1 can never be enabled #103
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#103
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found by #84 Phase 3 while porting Ruby to a package, and verified in source. This blocks #84's acceptance and therefore #75's final gate step 13.
The defect
#86 gap 1shipped the derived-ref marker: a package may emit a ref whose NAME is derived rather than copied from the source, so its span means "where this fact came from" rather than "the bytes of this name". Ruby's Rails DSL is the motivating case —has_many :postsemits atyperef namedPostat the span of the literal:posts.Using it requires the
derived_namesgrant. Nothing can give it.HostPolicy::default()setsderived_names: false(crates/plugin-supervisor/src/supervise.rs:173).PackageHost::discoverandconform::runboth build their policy with..HostPolicy::default().grep derived_names crates/cli/srcis empty.grep derived_names crates/indexer/src/approval.rsis empty.Supervisor::set_derived_namesexists but no shipped caller reaches it.The one place that passes
truesays so itself, and says it is not a grant:So the system can diagnose the missing grant precisely and cannot give it.
Why it is fatal rather than inconvenient
An ungranted
resolvercapability is INERT. An ungrantedderived_namesis FAIL-CLOSED.That asymmetry is the whole problem. The capability model's default-deny is safe for a capability whose absence means "your symbols appear in search but join no pool". For
derived_names, absence meansvalidaterefuses the whole file:Measured directly:
derived_names=false→fact.span_name_mismatch;derived_names=true→ 2 symbols, 7 refs.And through the shipped binaries, the chain stops:
So the acceptance package for #84 — the one that proves general language extensibility, which #75 says markup alone does not — cannot be enabled by an operator.
What the fix needs
An operator-facing way to answer yes, on the same footing as the other capability grants:
plugin enablealready takes capability grants — this belongs beside them);PackageHost::discoverandconform::runreading the grant instead of..HostPolicy::default();conform::runtoo, orplugin checkkeeps failing a package the operator has approved — C1's verdict is whatenablereads, so a package that cannot pass conformance cannot be enabled regardless of the runtime grant.The refusal witness is already excellent and should stay: it names the exact condition and does not guess. It just needs a reachable answer, which is the same defect class as
plugin_add's poll instruction naming a state that could never become true.Design question worth deciding explicitly
Should
derived_namesbe inert-on-deny rather than fail-closed — i.e. drop the derived refs and keep the rest of the file, disclosing what was dropped? That would match howresolverbehaves and would mean an ungranted package degrades instead of disappearing.Arguments against: a package that asserts names it did not copy is exactly the thing the span check exists to police, and silently dropping facts is its own honesty problem. Arguments for: losing 100% of a file's symbols because one ref in it is derived is a very large blast radius for a capability the operator was never asked about.
Either answer is defensible; the current state — fail-closed with no way to grant — is not.
Related
Blocks #84 (step 13 of #80's final gate) and therefore #75. Found alongside #102 (a real
private_class_methoddefect in the builtin) by the same porting exercise.Fixed. The grant is reachable, durable, per-project, and re-validated at read time.
The path, end to end
Approval.derived_names(#[serde(default)], absent reads as deny) plus a#[must_use] with_derived_names(bool)builder rather than a 5th parameter toApproval::granted, so all 16 existing call sites keep the honest default.StoredPackage.requested_derived_namesparses the manifest's ask and is the ceiling.verify_grantrule 5, checked first:derived_names && !entry.requested_derived_names→capability_not_granted. First because a package holding this can bind any name at any in-range span, so its refusal must not queue behind a typo in an unrelated capability list.plugin enable <digest> --derived-namesandplugin check <digest> --derived-names, beside--capabilities/--bridgesas a flag rather than a name inside them (it is not a resolver capability andRESOLVER_CAPABILITIESis a closed registry).--grant requestedanswers the manifest's whole request.plugin addneeded no flag — it resolves the grant before C1 and threads one answer into both.PackageHost::startreplacesHostPolicy::default()with the grant read from the set.derived_names_granted()is a three-way AND: project ceiling && manifest request && this package's grant. The third conjunct is load-bearing: oneSupervisorserves a whole lane pool, so without it the first package an operator granted would arm every other package that merely asked.effective_grantnow callsverify_grant, notvalidate_grant— a hand-editedderived_names = truefor a package that never asked empties the grant rather than conferring it.The conformance chicken-and-egg, resolved explicitly
C1 runs before approval by design, and
enablerefuses a package with no passing verdict. For every other capability that is free, because an ungranted resolver capability is inert.derived_namesis fail-closed, so a package whose fixtures contain a derived name cannot pass C1 at all and could never be enabled whatever the operator would have granted.Resolution: the answer is an argument to both commands, and the verdict records which answer the run held.
Verdict.derived_names,#[serde(default)], and noVERDICT_SCHEMAbump — bumping would silently un-enable every already-checked package, becauseVerdict::loadreturnsNonefor an unknown schema andNoneisconformance_not_run. Newrequire_verdict_covering_grantadds one clause: a verdict earned without the authority cannot license a grant that has it. The reverse direction is deliberately allowed — refusing it would deny an operator the right to say no.Both directions, mutation-proved (all RUN, all RED)
a package that did not ask must not be granted derived namesplan_grantRequested→ literalfalse`requested` must answer the manifest's whole requestplan_grantRequested→ literaltrueGrantSpec::named→truethe low-level form defaults to DENY#[serde(default)]a record written before #103 must still load&& self.derived_names_granta project ceiling must not arm a package this project did not grantrequire_verdict_covering_grantguard neuteredan operator must stay free to say noPackageHost::startback toHostPolicy::default()— this defect, reintroducedfact.span_name_mismatchrefusal againcheck_with_grantrecordsderived_names: falseset_derived_names(true)unconditionallyOne mutation SURVIVED and is recorded rather than hidden: the
check_with_grantmutation is green against the unit test, which builds its two verdicts by hand and never runs the writer. That measurement is written into the test's own doc, pointing at the e2e that does catch it.Acceptance through the shipped binaries
an_operator_can_grant_derived_names_and_the_rails_file_then_indexesdrives the realcode-indexbinary — deliberately through aCliharness rather than the internalfx::Operatorfixture, because this was a defect in the operator surface and a fixture reaching past it could neither catch it nor prove it fixed. pack → sign → trust add → install --sha256 →check(fails,rails.rb.rbx REFUSED) →check --derived-names(all five fixtures extract, verdict passes) →enable --derived-names→ real daemon: noparse_error,class Userindexes, and the derived reftype Post— four bytes that appear nowhere in the file — is present and returned bysearch_symbols. Both ungranted witnesses still pass unchanged.Pre-#103 approval records still load, graded against a hand-written TOML rather than this build's serializer agreeing with itself.
RECORD_SCHEMAandVERDICT_SCHEMAboth unmoved.The design question: keep fail-closed. Do not implement inert-on-deny.
resolution_gaps/name_fallback_countalready tell a reader what was not resolved. Dropping a derived ref changes the facts with no per-row channel to say so:find_callersonPostwould return a truthful-looking zero. That is the absent-reads-as-negative shape this project refuses everywhere.files.parse_errorcarries the whole story and it is measured —withheld_grant_witnessre-validates the same body with the grant and observes it pass. Inert-on-deny leaves no error, so it would need a new per-file "n refs dropped" column, a reader surface, and a rule binding every ref-reporting tool to disclose it.greeter.rb.rbxindexes fully with no grant. "The package disappears" is not what happens.Gates: fmt, clippy (native and
x86_64-pc-windows-gnu), rustdoc,cargo test --workspace(239 binaries), daemon-leg e2e,precision_gate7/7 — all green.tests/corpus/baseline.jsonunmoved at534084b856c22566c48e386bc41ed67e.Follow-ups filed separately rather than scope-crept into this fix: the re-grant reindex gap, and making the running cost of declining visible in aggregate.
derived_nameson an already-enabled package does not reindex, so the files stay at 0 symbols #104derived_namesis only visible by greppingfiles.parse_error#105plugin_addtool cannot grantderived_names, so an agent can never add a package that needs it #106plugin addgrantsderived_namesby default and never names it on the screen the operator answers #277