Skip to content

docs(openspec): or-flow-object-write-node — flows can write objects (create/update/upsert/guarded delete)#2160

Merged
rubenvdlinde merged 2 commits into
developmentfrom
spec/or-flow-object-write-node
Jul 27, 2026
Merged

docs(openspec): or-flow-object-write-node — flows can write objects (create/update/upsert/guarded delete)#2160
rubenvdlinde merged 2 commits into
developmentfrom
spec/or-flow-object-write-node

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

OpenSpec change or-flow-object-write-node — the flow engine's nine built-in nodes transform items but none can persist an object, so "no code, just flows" cannot materialise anything. Adds openregister.object-write:

  • create / update / upsert / guarded delete (explicit composite match, exactly-one resolution, confirmDelete: true, normal delete path with audit)
  • composite (ANDed) match keys; ambiguity always fails
  • per-step write cap (default 1000, fail-on-exceed, never silent truncation) — the downstream bound for openconnector's per-object sync fan-out
  • RBAC + attribution through the normal ObjectService path; fail-closed on ownerless runs (FlowMcpToolProvider::runFlow drops the acting user — agent-dispatched flow runs are unattributed #2158)
  • hardens the existing ObjectService::patchObject() (uuid-safe, RBAC forwarded, acting user, PATCH merge semantics) and widens deleteObject() for sessionless attribution

First consumers: hydra-cache maintenance + triage results (hydra-console chain, hydra PR#384 / hermiq PR#52). Sibling: ConductionNL/openconnector spec/openconnector-flow-nodes.

16 requirements / 66 scenarios; validates openspec validate --strict; tasks at the 20-checkbox cap. PO-reviewed 2026-07-27 (delete-in-v1, ObjectService PATCH, multi-match, write cap all explicit PO decisions).

🤖 Generated with Claude Code

…ects (create/update/upsert/guarded delete)

The flow engine's nine built-in nodes transform items but none persists anything;
'no code, just flows' cannot materialise an object. Adds openregister.object-write:
per-item create/update/upsert/delete with composite match keys, a per-step write cap
(default 1000, fail-on-exceed), RBAC + attribution through the normal ObjectService
path (fail-closed on ownerless runs — see #2158), and explicit item-level error
semantics. Hardens the existing ObjectService::patchObject() (uuid-safe, RBAC
forwarded, acting user, merge semantics) and widens deleteObject() for sessionless
attribution. First consumers: hydra-cache maintenance + triage results
(hydra-console chain); sibling change: openconnector-flow-nodes.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 4f39d91

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 09:27 UTC

Download the full PDF report from the workflow artifacts.

…elete objects

Adds ObjectWriteNode, the built-in flow node that lets a graph flow write back
to OpenRegister rather than only read and branch. Supports create, update,
upsert and a guarded delete, resolving its target by uuid/slug/id scoped to a
register+schema pair.

Threads an explicit acting user through the write path so a sessionless caller
(flow run, cron, import pipeline) is attributable rather than anonymous:

- ObjectService::deleteObject() takes an optional $currentUser and evaluates the
  RBAC `delete` check against it instead of passing userId: null.
- ObjectService::patchObject() is rewritten from a thin array_merge facade into
  the fleet's supported PATCH-semantic write path: RFC 7386-shaped recursive
  merge (absent key preserves, explicit null clears, lists replace wholesale),
  register/schema scoping, and no int cast on the identifier (the #1638 defect
  class). The merged result still goes through saveObject(), so validation,
  audit trail and events all apply.

Registers the node on FlowNodeRegistrationListener alongside the other built-ins.

Covered by ObjectWriteNodeTest and ObjectServicePatchObjectTest.

Change: or-flow-object-write-node
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Implementation landed (commit 6cf2d416b)

ObjectWriteNode (openregister.object-write) is implemented and registered as the tenth built-in flow node, together with the PO-mandated scope: create / update / upsert / guarded delete (explicit match, exactly-one resolution, confirmDelete), composite ANDed match keys, and a per-step maxWrites cap that raises rather than truncates.

Two core-service changes ride along, both driven by verified defects rather than design preference:

  • patchObject() already existed at lib/Service/ObjectService.php:4204 with four real defects — an (int) $objectId cast that breaks UUID ids, _rbac/_multitenancy accepted but forwarded nowhere, no ?IUser $currentUser, and a shallow unscoped array_merge. All four fixed; merge is now RFC-7386-shaped (provided wins, omitted preserved, explicit null clears).
  • deleteObject() had no acting-user parameter and passed userId: null into checkPermission(), resolving the actor from the session. A sessionless flow run therefore could not attribute a delete at all. Signature widened; default null preserves session resolution for every existing caller (pinned by a test).

Verification

  • 387/387 green across the flow + patch/delete suites
  • A wider regression sweep over all 25 test files referencing deleteObject/patchObject: 345/345 green
  • php -l clean, phpcs clean, psalm "No errors found" on both changed lib/ files

Known limitation, recorded not hidden

RBAC acting-user threading is only complete on the delete path. saveObject()/patchObject() still resolve their RBAC subject from IUserSession, and findAll() takes no acting user — so under a sessionless FlowRunWorker (webcron) run the match read and save-side check evaluate as anonymous, while CLI cron masks it via the PHP_SAPI trust short-circuit. Widening that touches ImportService, TransitionEngine and Configuration\ImportHandler, so it is deliberately out of scope here and filed as #2162.

Also filed while building: #2163 (flow nodes cannot see their step id or onError policy — execute() receives step['config'], not the step).

Still outstanding

A live flow run proving the node registers in the running palette and executes. There is no HTTP endpoint exposing the node registry, so unit tests cannot answer this — it is in progress.

@rubenvdlinde
rubenvdlinde merged commit 32c7d11 into development Jul 27, 2026
3 checks passed
@rubenvdlinde
rubenvdlinde deleted the spec/or-flow-object-write-node branch July 27, 2026 14:41
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