fix(flow,credentials): attribute flow runs to their owner + permit forge label writes - #2169
Merged
Merged
Conversation
…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).
Contributor
Quality Report — ConductionNL/openregister @
|
| 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.
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.
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()calledqueue()without the acting user. Fixed viaIUserSession; null stays null rather than a fabricated uid.The deeper cause, found while fixing it:
FlowRunService::execute()built the node context fromrunUuid+resumingand never copied the run's owner into it. Nodes readcontext['triggeredBy']to attribute what they do —ObjectWriteNoderefuses to write without it,SubFlowNodepropagates it to child runs, Hermiq's agent node runs the turn as that user. Nothing inlib/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-writelooked 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/flowRunStatusnow declare ADR-063 hintsreadOnlyHint/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/DELETEon issue labels andPATCHon the issue, plus the GitLab equivalent (a singlePUT, since GitLab sets labels as issue fields rather than a sub-resource).ProviderCatalogueTest::testNoProviderGrantsDeletewas 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:flowRunStatusnow reports"triggeredBy": "admin"where it wasnull.Note: catalogue changes need no forced re-import —
ProviderCataloguereads the JSON off disk per request.🤖 Generated with Claude Code