Skip to content

docs(openspec): hydra-console-agent-leaves — triage agent + argument-scoped attributed tool grants - #52

Merged
rubenvdlinde merged 2 commits into
developmentfrom
spec/hydra-console-agent-leaves
Jul 27, 2026
Merged

docs(openspec): hydra-console-agent-leaves — triage agent + argument-scoped attributed tool grants#52
rubenvdlinde merged 2 commits into
developmentfrom
spec/hydra-console-agent-leaves

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

Fourth link of the hydra-console chain (hydra repo PR: spec/hydra-console-chain). OpenSpec change hydra-console-agent-leaves — flows-first, no bespoke forge code:

  • Deliverable = the missing generic abstraction: argument-scoped, attributed tool grants (openregister.runFlow?flowId=…&label=in:a,b,c) enforced at FacadeToolInvoker
  • Seeded Hydra Triage agent (read-only grants) + triage loop as one seeded agentflow object
  • Leaf surface-vocabulary parity fix + visible empty-context behavior; behavioral acceptance for the development→feat/agent-graph-builder merge
  • MODIFIED deltas against agent-object-leaf, agent-tool-governance, nc-native-tools (canonical specs updated)

⚠️ Security note

Until argument-scoped grants land, openregister.runFlow must not be granted to any agent — an exact-id grant authorizes every flow on the instance, and runs are unattributed (ConductionNL/openregister#2158, ConductionNL/openregister#2159).

Validates openspec validate --strict.

🤖 Generated with Claude Code

…coped attributed tool grants, agentflow triage loop

Fourth link of the hydra-as-OpenBuild-app chain (depends on hydra repo changes
hydra-register-data-plane + hydra-console-openbuild-app). Flows-first: no bespoke
forge code — the deliverable is the missing generic abstraction (argument-scoped,
attributed grants for openregister.runFlow enforced at FacadeToolInvoker), a seeded
Hydra Triage agent + agentflow, and the leaf surface-parity/empty-context fixes.

SECURITY NOTE: until argument-scoped grants land, openregister.runFlow must not be
granted to any agent (grant = every flow on the instance; runs unattributed —
openregister#2158/#2159). Canonical specs agent-tool-governance + nc-native-tools
updated with the MODIFIED-delta entries.
@rubenvdlinde
rubenvdlinde requested a review from Rem-Dam as a code owner July 27, 2026 08:35
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 585a78c

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ❌ 1/117 denied
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

❌ Denied composer licenses

Package Version License
smalot/pdfparser v2.12.5 LGPL-3.0

Quality workflow — 2026-07-27 08:37 UTC

Download the full PDF report from the workflow artifacts.

… leaf surface parity

Argument-scoped grants (ToolGrantResolver, FacadeToolInvoker, ToolLoop)
- Agent.tools stays a string[] (ADR-035 Decision 4); an entry may now narrow an
  exact tool id by the ARGUMENTS it may be invoked with, via a `?k=v&k=in:a,b`
  grammar. Some tools pick their target from an argument rather than their id —
  openregister.runFlow runs ANY flow from a flowId — so before this the only way
  to grant one flow was to grant all of them.
- A narrowed grant resolves to the SAME catalog id (ToolLoop strips constraints
  before the single facade call), so disclosure and default-deny are untouched
  and narrowing never downgrades a write/destructive classification.
- Constraints are enforced BEFORE dispatch at FacadeToolInvoker — the chokepoint
  that already holds the guardrail, approval and dry-run short-circuits — and
  refuse structurally (grant_constraint_violated), never by throwing.
- A constrained wildcard resolves to NOTHING: fail closed.

Flow-run owner attribution
- ToolLoop resolves the owning UID (session user, then the agent's actingUser,
  then the agent's owner) and FacadeToolInvoker REFUSES a flow-queueing tool
  when none resolves (owner_unresolved) rather than queueing an unattributed
  run — a flow's terminal step may command an external system.
- Upstream gap documented: openregister#2158 (FlowMcpToolProvider::runFlow drops
  the user FlowRunService::queue already accepts).

Leaf surface parity
- Both halves now declare the same surface set EXPLICITLY:
  RegisterAgentLeafListener::SURFACES and the SURFACES const in
  src/integration-leaf.js. The JS half declared surfaces by omission while
  shipping a dashboard-sized widget and the PHP half said the leaf was not
  dashboard-placeable, so the widget was unplaceable and a parity check had
  nothing to compare. LeafSurfaceParityTest now compares them.

Fail-closed context is now visible
- CnAgentChatTab states in text when the allowlist resolves to zero properties
  and marks each reply produced in that state as not grounded. Fail-closed
  context is correct security; presenting an ungrounded answer as grounded is
  not. Strings added to l10n/en.json and l10n/nl.json.

Seeded triage agent + flow
- SeedHydraTriageAgent / SeedHydraTriageFlow repair steps (flow before agent,
  both idempotent by name), registered on install and post-migration.
- AgentFlow schema 0.1.2 declares triggerRegister (already read by
  HermiqFlowResolver but undeclared, so silently dropped on save — every flow
  matched every register) and owner.

Change: hydra-console-agent-leaves
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 3290029

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ❌ 1/117 denied
npm
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

❌ Denied composer licenses

Package Version License
smalot/pdfparser v2.12.5 LGPL-3.0

Quality workflow — 2026-07-27 10:43 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Implementation landed (commit 480d8b73)

The core deliverable — argument-scoped, attributed tool grants — is implemented generically; nothing in it names hydra, a forge or a label.

  • ToolGrantResolver gained the tool.id?arg=value&arg2=in:a,b,c grammar, plus baseToolIds(), argumentConstraints() and violationFor(). Multiple grants over the same tool are alternatives whose arguments stay paired (?flowId=A&label=x + ?flowId=B&label=y permits (A,x) and (B,y) but not (A,y)) — merging per-argument would have silently widened the grant. Omitting a constrained argument is a violation, not a bypass. A constrained wildcard is refused outright.
  • FacadeToolInvoker enforces constraints as a fifth short-circuit in the existing ordered chain, so there is no second path around it. Refusals are structured and traced, never thrown.
  • openregister.runFlow is refused when no owning UID resolves; when one does it is injected as triggeredBy.

Also: leaf surface-vocabulary parity (PHP and JS now declare the same four surfaces explicitly), a visible empty-context notice when the allowlist resolves to nothing, and the Hydra Triage agent + agentflow seeds.

Verification

1146 tests, 3427 assertions — OK. New suites: ToolGrantResolverArgumentScope 23/23, FacadeToolInvokerConstraint 11/11, SeedHydraTriageAgent 8/8, SeedHydraTriageFlow 7/7, LeafSurfaceParity 3/3. phpcs 6/6 clean, phpstan [OK] No errors. Seeds verified live and idempotent — exactly 1 agent + 1 flow after three separate repair triggers.

Two things reviewers should look at

  1. Task 1 (the origin/development merge) is still not done, and it has a live consequence: the checkout's src/integration-leaf.js still carries the pre-merge SFC form, which reverts the ADR-066 mount/unmount hand-off (hermiq#44/feat(agent-leaf): render via mount(el,props) hand-off under a Vue 2.7 host (fixes #44) #47). A rebuild from that source silently un-deploys the fix — that happened during this work and was caught and restored. This branch keeps the mount hand-off; the merge still needs doing.
  2. Because of the above, LeafSurfaceParityTest asserts on the component name rather than the literal widget: CnAgentRunsWidget string, which only exists in the pre-merge form. That is the one place test semantics were adjusted rather than propagated verbatim — flagged deliberately.

Known-inert until upstream lands

The seeded agent has no command grant on a live instance: it requires the hermiq.hydra.commandFlowId app config and a state enum on hydra's vocabulary schema. That is the specified fail-closed behaviour — guessing a vocabulary or granting an unconstrained runFlow would be worse — but it means the command path is unexercised. Separately, ConductionNL/openregister#2164 means x-openregister-agent-context is dropped at save, so every agent leaf fleet-wide currently resolves an empty context; the allowlist requirement here cannot be satisfied until that one-line vocabulary fix lands.

@rubenvdlinde
rubenvdlinde merged commit 84ea80a into development Jul 27, 2026
14 of 27 checks passed
rubenvdlinde added a commit that referenced this pull request Jul 28, 2026
Second WIP-preserve on this branch. Snapshots everything uncommitted so the
origin/development merge cannot lose it: another session's federated-config work
(ShareableConfigTypeListener, HermiqSkillShareableConfigType,
FederatedStoreService), the hydra-console agent-leaf implementation and its
seeds, and two openspec change directories that are already merged to
development via PRs #52 and #56.

No judgement is made here about whether any of it is finished — this is a
preservation commit, not a claim of completeness.
@rubenvdlinde
rubenvdlinde deleted the spec/hydra-console-agent-leaves branch August 14, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant