… 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
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:openregister.runFlow?flowId=…&label=in:a,b,c) enforced atFacadeToolInvokeragentflowobjectagent-object-leaf,agent-tool-governance,nc-native-tools(canonical specs updated)Until argument-scoped grants land,
openregister.runFlowmust 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