feat(capability): relocate the tool-grant grammar from hermiq (ADR-099 §5) - #2868
Merged
Conversation
…9 §5) The OpenRegister half of the pair. hermiq's companion PR repoints its consumers and deletes the originals; it cannot merge until a build carrying this ships. ADR-099 §5 keeps TWO grant axes and forbids merging them: * DELEGATION — may principal P act as user B. New, built in lib/Service/Delegation. * CAPABILITY — may agent X use tool T. Mature, and the subject of this commit. The capability system is not agent-specific and does not belong to hermiq. It resolves against `ToolRegistryFacade`, which already lives here, so it moves to sit beside what it resolves against. 🔴 A RELOCATION WITH ITS TESTS, NOT A REWRITE. Nothing below the docblock changed except the namespace. That codec carries a measured scar — 35 of 87 tools parsed wrong — and ADR-095's persistence constraint, and a rewrite-while-moving reopens both. The five classes arrived by `cp` and were edited only where the namespace, the @Package tag, the @Covers tag and the class TITLE named hermiq. 🔴 NAMED `Capability`, NEVER `Grant`. Once both axes live in one codebase the conflation risk rises rather than falls, and a user approving a tool must not be able to widen whose identity the agent wears. The directory name is the control.⚠️ FOUR OF FIVE TESTS MOVED. `ToolGrantResolverTest` did NOT, and that is deliberate rather than an omission: it builds its catalog from hermiq's real tool providers — HermiqToolProvider, MemoryService, MailReadService, NcNativeWriteService, WebFetchService and four more — so it is an integration test against a catalog, not a unit test of the grammar. It stays where the catalog is, repointed at this namespace in the companion PR. Moving it would have meant rewriting its fixtures, which is the one thing this relocation must not do. Verified: 58 tests, 153 assertions green here — the same assertions that were green in hermiq. phpcs, phpmd (both rulesets), psalm and phpstan clean.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-26 11:16 UTC
Download the full PDF report from the workflow artifacts.
The relocation moved five classes and left all 51 of their `@spec` tags pointing at hermiq's openspec tree. Gate-46 (spec-anchor-existence) reported 94 unresolved findings — and every one of them was mine: the count matched exactly when the checker was run over the relocated files alone. A tag whose target does not exist is worse than no tag. It reads as traceability and dereferences to nothing, which is the shape this fleet already carries ~300 of from archived changes. WHAT MOVED WITH THE CODE. Five spec files, copied VERBATIM into openspec/specs/: agent-capability-reach, agent-tool-governance, governed-cli-mcp-transport, structured-tool-grants, agent-object-leaf. Verbatim matters mechanically as well as editorially — the headings ARE the anchors, so rewording one would break the tags the copy exists to serve. Three of them had only ever existed as change deltas in hermiq. Two of the targets were ALREADY DEAD THERE: `agent-capability-reach` and `agent-tool-governance-and-disclosure` have been archived, so those tags dereferenced to nothing in hermiq too, and the relocation is what surfaced it. They now point at promoted specs that exist. THREE TAGS POINTED AT tasks.md, which is a change artefact that dies on archive by construction. Those are repointed at the requirement each one implements — a task number is not a behavioural contract and cannot be one. Verified with the gate's own checker rather than by inspection: 94 findings before, 0 after, and 0 across every tracked lib/**/*.php in the repository, so nothing else was disturbed. 58 tests / 153 assertions still green; phpcs clean; `npm run check:specs` passes.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-26 14:33 UTC
Download the full PDF report from the workflow artifacts.
…lements The previous commit brought five hermiq specs across whole so every `@spec` anchor would resolve. That was correct and too broad: ~1,534 lines duplicated across two repos, most of it describing behaviour hermiq still owns. MEASURED WHICH REQUIREMENTS BELONG HERE rather than judging by title. For each `### Requirement:` block, whether any anchor the relocated code cites falls inside it — computed with BOTH slug rules gate-46 accepts, since they part company on punctuation inside a word. 14 blocks kept, 18 dropped; 1,534 lines down to 920.⚠️ A FIRST MEASUREMENT SAID SOMETHING ELSE, and was wrong. Counting hermiq citations by requirement SLUG reported zero for every requirement in all five specs, which would have licensed moving them wholly. Counting by spec PATH instead reported 11, 14, 10, 15 and 1. The slug pass was silently failing to match — `writedestructive` against `write-destructive` — so its zeros meant "my normalisation differs", not "nobody cites this". Two instruments disagreed and the one that could not be fooled by normalisation won. So hermiq stays canonical for all five. Its specs are untouched and its ~50 references keep resolving. What is here is a bounded subset, and each file says so in a banner naming how many requirements were left behind and where. WHY DUPLICATE AT ALL. A `@spec` tag is dereferenced by gate-46 against the repository it sits in, so a cross-repo reference is not expressible — an openregister class citing a hermiq spec resolves to nothing, which is the ~300-dead-tag shape this fleet already carries from archived changes. The duplication is structural, not an oversight, and it is now bounded to exactly the requirements the moved code implements. Verified with the gate's own checker: 0 unresolved anchors after the trim, same as before it. The trim removed text, not traceability.
Contributor
Quality Report — ConductionNL/openregister @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 175/175 | |||
| npm | ✅ | ✅ 545/545 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-26 22:08 UTC
Download the full PDF report from the workflow artifacts.
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.
The OpenRegister half of a PR pair. hermiq's companion repoints its consumers and deletes the originals; it cannot merge until a build carrying this ships.
Two axes, never merged
ADR-099 §5 keeps them apart deliberately:
lib/Service/Delegation/(new, #2864)lib/Service/Capability/— this PRThe capability system is not agent-specific and does not belong to hermiq. It resolves against
ToolRegistryFacade, which already lives here, so it moves to sit beside what it resolves against.🔴 Named
Capability, neverGrant. Once both axes live in one codebase the conflation risk rises rather than falls, and a user approving a tool must not be able to widen whose identity the agent wears. The directory name is the control.🔴 A relocation with its tests, not a rewrite
Nothing below the docblock changed except the namespace. The five classes arrived by
cpand were edited only where thenamespace,@package,@coversand the class title named hermiq.That constraint is not stylistic. The codec carries a measured scar — 35 of 87 tools parsed wrong — and ADR-095's persistence constraint. A rewrite-while-moving reopens both, and the ADR says so in as many words.
ToolGrantResolverTestdid not, and that is deliberate rather than an omission.It builds its catalog from hermiq's real tool providers —
HermiqToolProvider,MemoryService,MailReadService,NcNativeWriteService,WebFetchService,CourseRecommendationEngine,DelegationService,ToolAccessRequestService,WebSearchClient. It is an integration test against a catalog, not a unit test of the grammar. It stays where the catalog is, repointed at this namespace in the companion PR.Moving it would have meant rewriting its fixtures — the one thing this relocation must not do.
What stays in hermiq
The
Agent.toolsbinding, per ADR-099 §5. This PR moves the grammar that reads it, not the field.Verification
tests/Unit/Service/Capability--warning-severity=0)That the assertion count carried over unchanged is the point of the check: a relocation that quietly dropped a case would show up here as a smaller number, not as a failure.
🤖 Generated with Claude Code