Skip to content

fix(flow,credentials): attribute flow runs to their owner + permit forge label writes - #2169

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/command-plane-attribution
Jul 27, 2026
Merged

fix(flow,credentials): attribute flow runs to their owner + permit forge label writes#2169
rubenvdlinde merged 1 commit into
developmentfrom
fix/command-plane-attribution

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Fixes #2158, #2159, #2165.

Three linked fixes that together let a governed app command a forge.

or#2158 — flow runs were unattributed, in two places

FlowMcpToolProvider::runFlow() called queue() without the acting user. Fixed via IUserSession; null stays null rather than a fabricated uid.

The deeper cause, found while fixing it: FlowRunService::execute() built the node context from runUuid + resuming and never copied the run's owner into it. Nodes read context['triggeredBy'] to attribute what they do — ObjectWriteNode refuses to write without it, SubFlowNode propagates it to child runs, Hermiq's agent node runs the turn as that user. Nothing in lib/ ever wrote that key, so every trigger reached its nodes ownerless and only hand-injected contexts (tests, harnesses) worked.

That matters for how #2160 was verified: object-write looked proven because the harness injected the context. On the natural path it would have refused. An explicit context value still wins, so a caller can attribute a run to someone other than whoever queued it.

or#2159 — runFlow/flowRunStatus now declare ADR-063 hints

readOnlyHint / destructiveHint / idempotentHint / scope, so consumers classify by declaration rather than the fail-closed fallback.

or#2165 — github provider permitted no issue-label write

Adds POST/DELETE on issue labels and PATCH on the issue, plus the GitLab equivalent (a single PUT, since GitLab sets labels as issue fields rather than a sub-resource).

⚠️ Security-invariant change — please look at this explicitly

ProviderCatalogueTest::testNoProviderGrantsDelete was a hard invariant: no provider in the catalogue may grant DELETE. Label removal needs one. The invariant is retained but narrowed to an allowlist of exactly one sanctioned rule (github issue-label removal); any other DELETE still fails, and a positive test asserts the sanctioned rule is the only one. Renamed accordingly. If you would rather keep the absolute ban and model label removal differently, this is the place to say so.

Verification

New guards proven to fail without each fix (e.g. Failed asserting that null is identical to 'alice'). Full-suite delta 0 errors / 0 failures against a CI-red baseline (117 errors / 17 failures before and after; failing-test-name lists byte-identical). Live-verified on a running instance: flowRunStatus now reports "triggeredBy": "admin" where it was null.

Note: catalogue changes need no forced re-import — ProviderCatalogue reads the JSON off disk per request.

🤖 Generated with Claude Code

…forge label writes

Three linked fixes that together unblock a governed app commanding a forge.

or#2158 — FlowMcpToolProvider::runFlow() called FlowRunService::queue() without
the acting user, so triggeredBy was null on every agent-dispatched run. Resolves
the user from IUserSession and passes it through; null stays null rather than a
fabricated uid.

or#2158 (deeper cause) — FlowRunService::execute() built the node context from
runUuid + resuming and NEVER copied the run's owner into it. Nodes read
context['triggeredBy'] to attribute what they do: ObjectWriteNode refuses to
write without it, SubFlowNode propagates it to child runs, Hermiq's agent node
runs the turn as that user. Nothing in lib/ ever wrote that key, so EVERY
trigger reached its nodes ownerless and only hand-injected contexts (tests,
harnesses) worked — which is why object-write appeared verified while the
natural path would have refused. An explicit context value still wins, so a
caller can attribute a run to someone other than whoever queued it.

or#2159 — runFlow and flowRunStatus now declare ADR-063 readOnlyHint /
destructiveHint / idempotentHint / scope, so consumers classify them by
declaration rather than by the fail-closed fallback.

or#2165 — the github provider's allowRules permitted no issue-label write, so
the broker refused label-driven forge automation even with a valid PAT. Adds
POST/DELETE on issue labels plus PATCH on the issue, and the GitLab equivalent
(one PUT, since GitLab sets labels as issue fields rather than a sub-resource).

SECURITY-INVARIANT CHANGE, needs an explicit reviewer look: the catalogue
previously enforced 'no provider grants DELETE' as a hard test. That invariant
is retained but narrowed to an allowlist of exactly one sanctioned rule
(github issue-label removal); any other DELETE still fails the test.

Tests: new guards proven to fail without each fix. Full suite delta 0 errors /
0 failures against a CI-red baseline (117/17 before and after).
@rubenvdlinde
rubenvdlinde merged commit 6e4526e into development Jul 27, 2026
17 of 18 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/command-plane-attribution branch July 27, 2026 16:41
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ beb8ae6

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 174/174
npm ✅ 555/555
PHPUnit
Newman
Playwright ⏭️

Quality workflow — 2026-07-27 16:49 UTC

Download the full PDF report from the workflow artifacts.

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