feat: activation on demand — detect a package this project wants, and repeat a decision the operator already made #90

Closed
opened 2026-09-03 12:42:17 +02:00 by buildagent · 1 comment
Member

Spec: _prdoc/specs/86-activation-on-demand.md — the filename's number is a collision with the existing #86 and will be renamed to this issue's number once the in-flight lanes stop reading it.

The problem

A project holds .xaml files. The operator installed and enabled the xaml package for a DIFFERENT project last week. Today they open this one, get text-only rows, and nothing tells them a package they already trust and already run would claim these files. The product knows every fact needed to say so and says none of them.

plugin_add (v0.25.0) made the ACT easy. It did not make the NEED visible, and an agent cannot ask for what it has not been told exists.

Phase A — detect and offer (implemented, in review)

Detection is entirely local: a package in the operator's store whose manifest claims an extension this project actually holds files for, and whose digest this project's ApprovalRecord does not approve. No network, no registry. The census is the same population symbol_blind_extensions reports, so the two cannot disagree.

project_overview gains activation_available (three-state, like package_set_consulted) and activation_auto_enabled.

The line: the first grant is a decision, the second is a repetition

"Installing grants nothing — enable is the grant" is stated in the product's docs and in the v0.25.0 release notes an operator has already read. So a silent enable is permitted only on capability parity:

  1. signature verifies against the CURRENT trust set, checked at ENABLE time — not the offer's earlier check;
  2. conformance verdict is a pass;
  3. the operator already approved this digest in another project;
  4. the grant written is the INTERSECTION of what they granted there and what the package requests NOW — never wider.

Where (3) fails there is no parity basis and the row stays an offer. The first grant of a package remains a human decision, on every machine, forever.

Forbidden absolutely

  • An in-tree .cip never auto-enables. It is the one input any contributor to a repository can write, and S2 exists because cloning a repo must not activate code.
  • No capability may be granted that no operator granted. Parity is an intersection, never a union.
  • A withdrawn publisher is not a parity basis.

Round 2 — what the adversarial review found

Two HIGH defects, both reproduced, sharing one root cause: the approval record stores no negative decision and no provenance for a grant.

  • plugin disable was silently undone within minutes. No tombstone, so the next pass re-granted — and auto_enable runs twice per process, the daemon idle-exits at 30 min, and under --no-daemon the next invocation re-grants. We SHIP undo_command in the disclosure, so this made a published promise false. A disclosure that names an undo which does not hold is worse than silence.
  • Parity keyed by package id enabled digests nobody ever approved — including superseded versions, with both generations activating at once. This was encoded as intended in an e2e expectation; a wrong decision written down as an expectation is not caught by a gate, it IS the gate.

Fixes in flight: ApprovalRecord.declined (tombstone), ParityGrant.basis_digest, Approval.auto_enabled_from, plus basis-record identity verification and narrowest-basis selection.

Phase B — lazy runtime start (not started)

Approved packages start at daemon startup. Phase B starts a package's worker on first demand and idles it out. Phase A deliberately keeps "is it approved" and "is it running" as separate questions in every payload it adds, so Phase B changes only the second.

Relationship to the epic

Independent of #75's chain: it makes an ALREADY-approved package reach a new project, and changes nothing about what a package can do. It does not advance #84's genericity proof.

Spec: `_prdoc/specs/86-activation-on-demand.md` — **the filename's number is a collision with the existing #86 and will be renamed to this issue's number once the in-flight lanes stop reading it.** ## The problem A project holds `.xaml` files. The operator installed and enabled the xaml package for a DIFFERENT project last week. Today they open this one, get text-only rows, and nothing tells them a package they already trust and already run would claim these files. **The product knows every fact needed to say so and says none of them.** `plugin_add` (v0.25.0) made the ACT easy. It did not make the NEED visible, and an agent cannot ask for what it has not been told exists. ## Phase A — detect and offer (implemented, in review) Detection is entirely local: a package in the operator's store whose manifest claims an extension this project actually holds files for, and whose digest this project's `ApprovalRecord` does not approve. No network, no registry. The census is the same population `symbol_blind_extensions` reports, so the two cannot disagree. `project_overview` gains `activation_available` (three-state, like `package_set_consulted`) and `activation_auto_enabled`. ### The line: the first grant is a decision, the second is a repetition "Installing grants nothing — `enable` is the grant" is stated in the product's docs and in the v0.25.0 release notes an operator has already read. So a silent enable is permitted **only** on capability parity: 1. signature verifies against the CURRENT trust set, checked at ENABLE time — not the offer's earlier check; 2. conformance verdict is a pass; 3. the operator already approved **this digest** in another project; 4. the grant written is the INTERSECTION of what they granted there and what the package requests NOW — never wider. Where (3) fails there is no parity basis and the row stays an offer. **The first grant of a package remains a human decision, on every machine, forever.** ### Forbidden absolutely - An in-tree `.cip` never auto-enables. It is the one input any contributor to a repository can write, and S2 exists because cloning a repo must not activate code. - No capability may be granted that no operator granted. Parity is an intersection, never a union. - A withdrawn publisher is not a parity basis. ## Round 2 — what the adversarial review found Two HIGH defects, both reproduced, sharing one root cause: **the approval record stores no negative decision and no provenance for a grant.** - **`plugin disable` was silently undone within minutes.** No tombstone, so the next pass re-granted — and `auto_enable` runs twice per process, the daemon idle-exits at 30 min, and under `--no-daemon` the next invocation re-grants. We SHIP `undo_command` in the disclosure, so this made a published promise false. *A disclosure that names an undo which does not hold is worse than silence.* - **Parity keyed by package id enabled digests nobody ever approved** — including superseded versions, with both generations activating at once. This was **encoded as intended** in an e2e expectation; a wrong decision written down as an expectation is not caught by a gate, it IS the gate. Fixes in flight: `ApprovalRecord.declined` (tombstone), `ParityGrant.basis_digest`, `Approval.auto_enabled_from`, plus basis-record identity verification and narrowest-basis selection. ## Phase B — lazy runtime start (not started) Approved packages start at daemon startup. Phase B starts a package's worker on first demand and idles it out. Phase A deliberately keeps "is it approved" and "is it running" as separate questions in every payload it adds, so Phase B changes only the second. ## Relationship to the epic Independent of #75's chain: it makes an ALREADY-approved package reach a new project, and changes nothing about what a package can do. It does not advance #84's genericity proof.
dhoyer referenced this issue from a commit 2026-09-04 07:02:49 +02:00
Author
Member

Shipped in v0.26.0

Detection is local: a package in the operator's store whose manifest claims an extension this project actually holds files for, and whose digest this project has not approved. The census is the same population symbol_blind_extensions reports, so the two cannot disagree.

A silent enable requires the operator to have already approved that digest in another project, and writes the intersection of what they granted there with what the package requests now — never wider. Where there is no basis the row is offered, not enabled: the first grant of a package stays a human decision on every machine. An in-tree .cip never auto-enables, because cloning a repository must not activate code.

What the adversarial review caught, and it was right twice

  • plugin disable was silently undone within minutes. cmd_disable cleared the row and left no tombstone, so the next pass re-granted it — and auto_enable runs twice per process, the daemon idle-exits at 30 minutes, and under --no-daemon the next invocation re-grants. We ship undo_command in the disclosure, so this made a published promise false. ApprovalRecord.declined is the tombstone, and set/clear deliberately do not touch it, because a tombstone the machine can clear is not a tombstone.
  • Parity keyed by package id enabled digests nobody ever approved. With v1 and v2 in the store and the basis project approving only v2, v1 got a basis naming a project that never saw those bytes. Keyed by digest now. This had been encoded as intended in an e2e expectation — a wrong decision written down as an expectation is not caught by a gate, it is the gate.

Live on the installed build

project_overview -> "activation_available": {"availability":"reported","offers":[],"auto_enabled":[]}

Reported as a measurement — an empty list, not an absent field — which is the distinction that makes "nothing to offer here" different from "this build does not answer".

One defect this shipped with, found and fixed in the same cycle

The approval record's key was derived from canonicalize()'s output when the project directory resolved and the raw path when it did not — two spellings of one project on Windows. Since that key is the record's filename, any moment a project directory was unresolvable (a disconnected share, an unmounted volume) the lookup computed a different key and every grant appeared to vanish, returning when the share reconnected. Corrected with a migration that moves existing records on first use, so no operator is asked to re-approve anything.

Spec: _prdoc/specs/90-activation-on-demand.md (renamed from 86- — it had been filed under another issue's number).

## Shipped in v0.26.0 Detection is local: a package in the operator's store whose manifest claims an extension this project actually holds files for, and whose digest this project has not approved. The census is the **same population** `symbol_blind_extensions` reports, so the two cannot disagree. A silent enable requires the operator to have already approved **that digest** in another project, and writes the **intersection** of what they granted there with what the package requests now — never wider. Where there is no basis the row is offered, not enabled: the first grant of a package stays a human decision on every machine. An in-tree `.cip` never auto-enables, because cloning a repository must not activate code. ## What the adversarial review caught, and it was right twice - **`plugin disable` was silently undone within minutes.** `cmd_disable` cleared the row and left no tombstone, so the next pass re-granted it — and `auto_enable` runs twice per process, the daemon idle-exits at 30 minutes, and under `--no-daemon` the next invocation re-grants. We ship `undo_command` in the disclosure, so this made a *published promise* false. `ApprovalRecord.declined` is the tombstone, and `set`/`clear` deliberately do not touch it, because a tombstone the machine can clear is not a tombstone. - **Parity keyed by package id enabled digests nobody ever approved.** With v1 and v2 in the store and the basis project approving only v2, v1 got a basis naming a project that never saw those bytes. Keyed by digest now. This had been **encoded as intended in an e2e expectation** — a wrong decision written down as an expectation is not caught by a gate, it *is* the gate. ## Live on the installed build ``` project_overview -> "activation_available": {"availability":"reported","offers":[],"auto_enabled":[]} ``` Reported as a **measurement** — an empty list, not an absent field — which is the distinction that makes "nothing to offer here" different from "this build does not answer". ## One defect this shipped with, found and fixed in the same cycle The approval record's key was derived from `canonicalize()`'s output when the project directory resolved and the raw path when it did not — two spellings of one project on Windows. Since that key *is* the record's filename, any moment a project directory was unresolvable (a disconnected share, an unmounted volume) the lookup computed a different key and every grant appeared to vanish, returning when the share reconnected. Corrected with a migration that moves existing records on first use, so no operator is asked to re-approve anything. Spec: `_prdoc/specs/90-activation-on-demand.md` (renamed from `86-` — it had been filed under another issue's number).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
h-dv/code-index#90
No description provided.