feat(acl): show and set what an entry may do within its role - #173
Merged
Conversation
VTI#1279 made `AclEntry.capabilities` enforced — the agent re-reads the entry
on every gated call, so an entry may now hold strictly less than its role
implies. Nothing in this library could read or write that, so the console
showed every entry as holding everything its role allows whether or not it did:
the exact misreading #1279 was written to end, arriving through the surface an
operator uses to check.
## The one `acl/*` surface that cannot be generated
`AclEntry.role` is specified as "an opaque role identifier interpreted by the
ACL maintainer". The framework does not know what `admin` means and knows less
about capabilities, so both vocabularies are ecosystem-local and the narrowing
rides `ext` under `org.openvtc.capabilities` — which is what SPEC.md §4.5.1
reserves that member for.
That makes `src/admin/acl-capabilities.ts` a hand-kept copy of `vti-common::acl`,
and `acl.ts`'s own header is emphatic that a copy drifts. So it reuses the
answer `task-surface.json` already gives: `acl-capabilities.json` is a
committed snapshot, refreshed by `npm run acl:sync`, and the test fails when
the two disagree. The failure it guards is asymmetric — a role whose derived
set grows at the agent and not here makes the console UNDER-report authority,
which is the direction that matters.
## Three distinctions the types keep
- **A narrowing only ever subtracts.** Effective = the role's derived set
intersected with the entry's own, so `role` stays a true upper bound and no
path here can hand a reader an admin's authority. A narrowing naming a
capability outside the role intersects to nothing rather than granting it.
- **Absent, `[]` and populated are three instructions**, not two: leave alone,
clear, narrow. `capabilitiesIntoExt` therefore *writes* an empty array rather
than omitting it — the one place it deliberately differs from the agent's
helper of the same name, which serves the response path where absent is the
only spelling for "not narrowed". Conflating the first two is a silent
privilege increase.
- **An unknown role is `undefined`, never `[]`.** The two render identically as
"holds nothing" and one of them is a lie about an entry that may hold
everything. The console says it cannot tell, which is the honest answer when
the agent is newer than the console.
## The pane
A "Can do" column that stays one line for the unnarrowed case — every entry
until someone acts — because fourteen pills per row buries the one entry that
was narrowed. Detail appears only where a decision sits behind it.
Two states it names that nobody asks for directly: a stored narrowing this
build does not recognise ("this agent is newer than this console", not silently
dropped), and an entry whose narrowing intersects its role to NOTHING —
reachable without intent, since `acl/change-role` moves the role and leaves the
narrowing alone.
The editor preselects what the entry holds now, per the rule #165 paid for: an
affordance that opens on a blank selection turns "let me look" into "narrow to
nothing". `Clear narrowing` is a separate button and disabled when there is
none, because it is the one direction here that adds authority.
## Tests
Seventeen, split by what makes them fail: the tables against the snapshot, the
behaviour against fixtures. Every refusal is paired with the acceptance it
bounds, per VTI#1268 — a module that refused everything would otherwise pass.
Mutation-checked: omitting an empty narrowing fails two, dropping one
capability from `reader` fails the drift check.
`@openvtc/pnm-core` 0.8.0 -> 0.9.0 (additive), dependents and lockfile with it.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
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.
Closes the gap flagged in #167: VTI #1279 made
AclEntry.capabilitiesenforced — the agent re-reads the entry on every gated call, so an entry may now hold strictly less than its role implies. Nothing in this library could read or write that, so the console showed every entry as holding everything its role allows whether or not it did: the exact misreading #1279 was written to end, arriving through the surface an operator uses to check.The one
acl/*surface that cannot be generatedAclEntry.roleis specified as "an opaque role identifier interpreted by the ACL maintainer". The framework does not know whatadminmeans and knows less about capabilities, so both vocabularies are ecosystem-local and the narrowing ridesextunderorg.openvtc.capabilities— which is what SPEC.md §4.5.1 reserves that member for.That makes
src/admin/acl-capabilities.tsa hand-kept copy ofvti-common::acl, andacl.ts's own header is emphatic that a copy drifts. So it reuses the answertask-surface.jsonalready gives:acl-capabilities.jsonis a committed snapshot, refreshed bynpm run acl:sync -- /path/to/vti-common, andtests/acl.capabilities.mjsfails when the two disagree.The failure it guards is asymmetric, which is why it earns the machinery: a role whose derived set grows at the agent and not here makes the console under-report authority.
Three distinctions the types keep
rolestays a true upper bound and no path here can hand a reader an admin's authority. A narrowing naming a capability outside the role intersects to nothing rather than granting it.[]and populated are three instructions, not two: leave alone, clear, narrow.capabilitiesIntoExttherefore writes an empty array rather than omitting it — the one place it deliberately differs from the agent's helper of the same name, which serves the response path where absent is the only spelling for "not narrowed". Conflating the first two is a silent privilege increase.undefined, never[]. The two render identically as "holds nothing" and one of them is a lie about an entry that may hold everything.The pane
A "Can do" column that stays one line for the unnarrowed case — every entry until someone acts — because fourteen pills per row buries the one entry that was narrowed.
Two states it names that nobody asks for directly: a stored narrowing this build does not recognise ("this agent is newer than this console", not silently dropped), and an entry whose narrowing intersects its role to nothing — reachable without intent, since
acl/change-rolemoves the role and leaves the narrowing alone.The editor preselects what the entry holds now, per the rule #165 paid for: an affordance that opens on a blank selection turns "let me look" into "narrow to nothing".
Clear narrowingis a separate button, disabled when there is none, because it is the one direction here that adds authority.Tests
Seventeen, split by what makes them fail: the tables against the snapshot, the behaviour against fixtures. Every refusal is paired with the acceptance it bounds, per VTI#1268 — a module that refused everything would otherwise pass. Mutation-checked: omitting an empty narrowing fails two tests, dropping one capability from
readerfails the drift check.Workspace green: 832 tests, 0 failures.
@openvtc/pnm-core0.8.0 → 0.9.0 (additive), dependents and lockfile with it.