Release: merge development into beta - #1711
Open
github-actions[bot] wants to merge 2743 commits into
Open
Conversation
…ommission) Hermiq is the agent engine's canonical home (hydra ADR-034 Amendment 2026-07-05). This makes the #305 compat proxy the default answerer, removes OR's own AI Chat + Agents SPA, and fixes the getChatStats multi-tenant leak in the remaining fallback engine. - ChatProxyHandler: chat.proxyTo defaults to 'hermiq' when unset; operators opt out with any other value (e.g. 'off'). Fail-open fall-through to OR's local engine is preserved (#305). - getChatStats: no active organisation -> zeros, never instance-wide totals (multi-tenant information leak). - Removed OR chat/agents SPA: chat + agents manifest pages and nav, ChatIndex/AgentsIndex, ChatSideBar, agent modals, conversation/agent stores, message/conversation/agent entities, ui#chat route, UiController::chat(), AgentsController::page(). BREAKING CHANGE: OR no longer ships its own chat/agents UI; users chat via the fleet AI companion (hermiq) and manage agents in hermiq. The OR chat REST routes remain as a proxied compat window. Full engine deletion (ChatService/Chat handlers/tables) is a tracked follow-up. NOTE: hermiq's MigrateAgentData still reads the legacy OR chat tables, so they are intentionally retained until the migration window closes.
- ImportServiceMigrationPackTest: ImportService gained a container param (live-updates flush) after this test was written on a parallel branch. - ObjectServiceTest::testFindSkipsRenderingWhenRenderFalse: stubbed the getCachedEntities no-op that the request-hygiene branch removed; schema resolution now goes through SchemaMapper::find directly. - bootstrap-unit-local: load NextcloudInternalStubs (OC\Hooks\Emitter et al) like bootstrap-unit does — mocking IRootFolder no longer depends on suite load order (clears ~900 order-dependent errors in the bare-container run).
The fast list path now redacts write-only fields through RenderObject::redactWriteOnlyFromRows; the test's OC server stub must resolve it like it already does MagicMapper.
…+ stub load-order fix' (#415) from fix/post-merge-test-integration into development
Add encryption-at-rest for schema-flagged object property values as an OpenRegister data-platform primitive, so every app inherits it instead of reimplementing crypto per app. Reuses OCP\Security\ICrypto (the same primitive TenantKeyService / SourcesController / WorkflowEngineRegistry already use for credentials) — no new crypto. - Schema flag x-openregister-encrypted: true (Schema::hasEncryptedProperties/ getEncryptedProperties, mirroring the writeOnly helpers). - FieldEncryptionHandler: versioned envelope (openregister:enc:v1:<ciphertext>) so mixed plaintext/ciphertext during rollout is unambiguous; idempotent. - Encrypt-on-save in SaveObject::prepareObjectData() (last step before persist). - Decrypt-on-read in RenderObject::renderEntity()/redactWriteOnlyFromRows(), strictly AFTER the writeOnly/property-authorization redaction and only on properties still present — an unauthorized caller never sees ciphertext or plaintext, only the same absence every redacted field gets. - Search/facet exclusion: no dedicated magic-table column for a flagged property; excluded from full-text scan and facetable fields; filtering on it throws EncryptedFieldFilterException -> HTTP 400 via a new middleware, never a silent zero-row result (explicit rethrows added to MagicMapper's catch-and-degrade layers so fail-loud survives them). - Migration: occ openregister:encrypt-field --property=<name> [--dry-run] [--register] [--schema], idempotent, fails loud on per-row failure. - Decryption failure never silent: structured @openregister_decryption_error marker + ERROR log on the read path, or a thrown FieldDecryptionException for strict callers (the migration command). No behaviour change for any schema that does not use the flag. Tests: 41 new unit tests across FieldEncryptionHandler, Schema, SaveObject, RenderObject (authorized/unauthorized/decryption-failure/mixed-rollout), MagicSearchHandler filter rejection, and the migration command. Full openspec change + threat model in design.md.
…l-object-encryption
…enregister-encrypted)' (#417) from wip/field-level-object-encryption into development
Move the completed change to openspec/changes/archive/2026-07-14-field-level-object-encryption/ and materialise the new field-level-encryption capability into openspec/specs/. Quality-gate tasks (7.x) marked done: PHPCS/PHPMD/PHPStan/Psalm clean on changed files, 41 new tests green, full suite diffed byte-identical against a pristine origin/development baseline (zero new failures).
…tion change' (#418) from wip/archive-field-encryption into development
…413) from feat/or-chat-engine-removal into development
… wipe the whole configuration (#419) Schema::validateConfigurationArray() validated every configuration key in a single pass and let the first InvalidArgumentException propagate out of setConfiguration(). On a direct API save that surfaces as a 400, but on APP IMPORT the throw is silently swallowed upstream — so a single invalid value (observed: pipelinq `lead` carried linkedTypes `["mail","decidesk-decisions"]`, and `decidesk-decisions` is not a valid Nextcloud linked type) caused the schema to lose its ENTIRE configuration, including x-openregister-mcp, x-openregister-lifecycle, x-openregister-calculations and mailObjectTemplate. The visible symptom was lead deriving no MCP tools at all. Extract the per-key if-chain into validateConfigurationEntry() and wrap the call in a try/catch inside the loop: an invalid value now drops only the offending key (recorded in droppedKeys so SchemaMapper can warn) and every other key is preserved. Adds a regression test asserting a bad linkedTypes value drops only linkedTypes while objectNameField and x-openregister-mcp survive.
While the dashboard is scoped to a register+schema via the sidebar
filter, DashboardIndex holds one or-collection subscription through the
package object store's liveUpdatesPlugin (same API as the object list
view). Events are refetch hints only: a 750ms trailing debounce
coalesces bulk-import bursts into a single refetch of the
object-CRUD-derived widget data (register totals, objects-by-* charts,
audit-trail statistics). Subscription re-scopes on filter change and is
released on unmount, with the proven pending-type + epoch race guards.
Unscoped ('all registers') view, register/schema CRUD counters and
search-trail widgets stay one-shot — no matching push event key exists
in the dialect; documented in the openspec change.
Refs openregister#410
Adopts the beta.212 default-on liveUpdatesPlugin in createObjectStore. Our explicit liveUpdatesPlugin() instance in src/store/modules/object.js wins the by-name dedupe, so existing ObjectsList/ObjectDetails subscribe wiring (PR #402) is unchanged. Also picks up the transport fix where the first subscription was stranded when the push probe reports unavailable.
Resolve all referential-integrity CASCADE targets with one batched cross-table lookup, soft-delete them with one CASE-UPDATE per magic table via MagicMapper::softDeleteMultipleObjectEntities (per-object updating/updated event pairs preserved), and write one multi-row audit INSERT via AuditTrailMapper::insertAuditTrails with rows carrying the canonical cascade-context fold (triggerObject/triggerSchema/action_type/ property) — instead of a full cross-magic-table scan and a single-row audit INSERT per target inside the delete transaction. Targets the batch lookup cannot resolve, and every target when the batched resolve or write fails, fall back to the unchanged per-object pipeline (kept verbatim as applyPerObjectCascadeDelete). RESTRICT, SET_NULL and SET_DEFAULT handling and the SET_NULL -> SET_DEFAULT -> CASCADE (deepest first) execution order are untouched. The previously unused organisationId parameter now feeds the deletion attribution metadata. Closes #409
Fixes openregister#406. sealRow() and sealRows() both read a predecessor hash and then UPDATE, so interleaved passes could chain a boundary row over a stale predecessor and trigger a false tamper alarm at the next verifyChain(). Both now take the same exclusive ILockingProvider lock (key openregister/audit-seal) around the whole critical section, with bounded fail-soft acquisition: on sustained contention a warning is logged and rows stay unsealed for a later pass instead of blocking the write path; the lock is always released, including on throw. getHashBefore() now returns the nearest PRIOR SEALED row's hash (filtering NULL/empty), exactly mirroring verifyChain()'s skip-null walk — sealing after an unsealed fail-soft leftover no longer chains from genesis and permanently breaks verification at that link. This also fixes ranged verification starting right after an unsealed row. New tests cover lock acquire/release, lock-unavailable fail-soft for both seal paths, release-on-throw, retry after a transient conflict, and unsealed-predecessor chaining verified end-to-end against verifyChain(). OpenSpec change harden-audit-seal-concurrency records the delta on the audit-hash-chain spec (validated --strict).
Fixes openregister#407. The class had zero callers in lib/ and appinfo/ — SaveObjects performs bulk chunking through its own internal processObjectsChunk(), making the handler an orphaned capability: implemented, tested, DI-resolvable, invoked by nothing. Removes the class, its unit tests, and its section in the SaveObject handlers integration test, and corrects the object-lifecycle REQ-004, data-import-export, and method-decomposition spec references that presented it as the bulk chunking implementation.
… descriptors Adds two host-locked credential-broker providers for api.anthropic.com so Hermiq's anthropic chat provider can inject the secret via BrokerHttpClient: - 'anthropic' — API key, injected as x-api-key (authMode: api_key) - 'anthropic-oauth' — Claude Max/Pro OAuth token, injected as Authorization: Bearer (authMode: oauth). PERSONAL-scope only per the Anthropic ToS. Both allowRule POST /v1/messages + GET /v1/models. Catalogue version 1.4.0 -> 1.5.0. 89 credential unit tests green.
…oker providers' (#428) from feat/anthropic-credential-provider into development
…ionally (#420) The #389 fix made the writeOnly BODY strip a hard, unconditional render-boundary rule, but the `relations` search-index mirror (surfaced as `@self.relations`) was only stripped on the older `_rbac === true` path. OpenRegister mirrors any reference-shaped scalar property (a UUID/URL value) into that index, so a `writeOnly: true` secret whose value is reference-shaped still leaked via `@self.relations` on an admin / `_rbac: false` read — the same class of bug as #389, one copy deeper. Reproduced live (NC34, admin GET+create): a writeOnly `secretToken` was absent from the body but returned in full under `@self.relations`. After the fix both create and GET return `@self.relations` without the value, while a genuine non-writeOnly reference still appears there. Fix: in the unconditional writeOnly path in `renderEntity()`, also run `stripWriteOnlyProperties()` over `getRelations()` and write it back — same boundary as the body strip. Two regression tests go through the real `renderEntity()` `_rbac` gate (not the handler helper directly): one asserts the mirror is stripped under `_rbac=false`, one asserts a non-writeOnly relation survives.
Design + spec delta for capturing the acting context at dispatch time and re-establishing it inside chunk-level QueuedJobs, unblocking the three session-entangled listeners the optimize-write-path-performance audit could not defer. Extends ADR-009 Rule 5 (async outbound I/O) with an explicit identity-forwarding contract: impersonate the captured user, always restore in finally, never resurrect stale organisation authority, at-least-once delivery with stale no-op idempotency.
#408) - DeferredListenerContext: serializable acting-context snapshot (userId, active organisation uuid, per-object entries) that round-trips loss-free through oc_jobs.argument; malformed payloads degrade to logged no-ops. - ListenerDeferralService: captures the actor once per request, buffers per-job-class entries and enqueues chunk-level jobs (flush at chunk size and at request shutdown), with per-entry dedupe keys and an openregister/listenerDeferral kill switch (inline restores synchronous behaviour). Fail-soft: capture/enqueue failures never break the save. - ActorForwardedJob: abstract QueuedJob that re-establishes the captured user via IUserManager + IUserSession::setUser() and ALWAYS restores the previous session user in a finally block so a cron worker never leaks identity across jobs; unresolvable users skip (never run under a wrong identity); organisation drift is logged, jobs run under the user's CURRENT authority. - DeferredEntryObjectResolver: stale-safe re-fetch by (uuid, register, schema); gone or soft-deleted objects resolve to null so at-least-once re-runs converge instead of resurrecting state.
…ns and threshold evaluation to actor-forwarded jobs (#408) The three listeners the write-path audit flagged as session-entangled now run their heavy work in background jobs under the forwarded actor; each keeps only a request-cached schema-config gate inline: - TranslationProjectionListener: created/updated/transitioned projection moves to TranslationProjectionJob (translator attribution now correct in cron); delete-time purge stays inline (bounded DELETE, entity not re-fetchable post-delete); schemas without translatable properties keep the inline stale-row prune for behaviour parity. - AnnotationNotificationListener: rule evaluation + delivery (sync HTTP / mail) move to AnnotationNotificationDispatchJob; updated entries carry the pre-update snapshot (old state is unrecoverable later) while new data is re-fetched at run time; the updated + calculatedChange dispatch pair is preserved. - AggregationThresholdListener: evaluation logic extracted verbatim into ThresholdEvaluationService (shared by job, delete path and kill switch); created/updated/transitioned evaluations move to AggregationThresholdJob with entries deduped per (register, schema) so a bulk save of one schema triggers one evaluation; deletes evaluate inline (delete-driven lt/lte crossings would otherwise be lost). All jobs are idempotent reconciliations against current state (stale entries no-op); duplicate delivery on at-least-once re-runs is suppressed by the dispatcher's dispatch-log dedup and the distributed rising-edge state cache.
…tionService (#408) Ports the former listener rising-edge tests (fire on below-to-above only, no refire while above, refire after dip, non-threshold triggers ignored) to the extracted service and adds hasThresholdNotifications gate coverage and evaluation-failure logging.
…rom nearest sealed row; drop dead chunk handler' (#425) from feat/audit-seal-hardening into development
…er-object fallback' (#424) from feat/integrity-cascade-batch into development
…heavy save listeners off the write path' (#430) from feat/actor-forwarded-jobs into development
…oped dashboard widgets' (#422) from feat/dashboard-live-updates into development
…ter-resolver integration collections (#2142) All three tests/integration/*.postman_collection.json collections were red against NC stable32, live-verified on a disposable NC32 instance: - apphost-observability: real bug. GET /api/health and /api/metrics both 503'd with "App controller is not enabled" — Application.php's registerAppHostObservability() wired every supporting AppHost service (ManifestLoader, HealthCheckExecutor, MetricsEngine, metric sources) but never registered the GenericHealthController/GenericMetricsController aliases routes.php's 'AppHost\Controller\GenericHealth#index' / 'AppHost\Controller\GenericMetrics#index' route names resolve to, so self-dogfooding of OpenRegister's own ADR-006/ADR-040 observability contract was dead on arrival. Fixed by registering both controllers under the exact DI key NC's RouteParser derives. Also fixed the collection's health_path/metrics_path, which pointed at a never-existed /api/apphost/health path and the bare (no /index.php/) URL form that 404s on the CI PHP built-in server. - magic-mapper-import: env/setup gap + stale test. The collection referenced dev-only fixtures never committed to the repo (softwarecatalogus_register_magic.json, magic-mapper-data/module.csv) and posted to a route that no longer exists (/configurations?force=true; the real route is /api/configurations/import). Rewritten to self-provision its own register + schema and exercise RegistersController::import's actual status codes (400 missing schema, 404 unknown register, 200 success) against a committed CSV fixture, then verifies the imported rows via GET /api/objects (object storage is unconditionally backed by OpenRegister's per-schema MagicMapper tables per SettingsService::getSearchBackendConfig()). Added a .gitignore exception so the CSV fixture (blocked by the blanket `*.csv` rule) ships with the app. - register-resolver: real bug + URL fix. GET /api/registers/{unknown} threw an uncaught DoesNotExistException, 500ing with an HTML error page instead of a clean 404 (the assertion already tolerated 500, masking it). RegistersController::show() had no try/catch, unlike the identical DoesNotExistException guard SchemasController::show() already has. Added the same catch(DoesNotExistException -> 404) / catch(Exception -> 500) pattern, tightened the collection's assertion to require the clean 404, and updated the unit test (testShowThrowsWhenNotFound -> testShowReturns404WhenNotFound) that had documented the broken behaviour as expected. Also fixed every request URL: base_url is overridden by CI to a bare host (http://localhost:8080), so paths must hardcode /index.php/apps/openregister/api/... the way every other collection in this directory does — this collection's base_url default baked in the app path instead, which the CI override silently stripped. All three collections pass 0 failures on a disposable nextcloud:32-apache instance, run exactly as the Code Quality "Integration Tests (Newman)" job invokes them. tests/Unit/Controller/RegistersControllerTest.php and tests/Unit/AppHost/* (152 + 121 tests) pass; phpcs clean on both touched lib/ files.
Sync the 3 new requirements into the canonical data-import-export spec and archive the change directory per repo convention.
…ug-uniqueness fix(import): scope schema slug uniqueness to a register's own set, not global/per-app
…andler (#2151) The Newman "openregister-crud" and "rbac" integration collections passed in isolation but failed in the CI job's combined sequential run (all ~14 collections against one shared NC32 instance, no DB reset). Root cause was a register-resolution gap in the authorization path, exposed by cross-collection state pollution. PermissionHandler::getRegisterForSchema() and getRegisterAuthorization() loaded the register via RegisterMapper::find($id) with the default org-scoped multitenancy filter. Their sibling id lookup (getAllRegisterIdsWithSchema) is deliberately global/unscoped, so once the id is known the entity fetch must be too — it is an authorization-policy read, not a tenant-scoped data read. When an earlier collection (openregister-crud) left multitenancy enabled and switched the admin's active organisation (never restoring it, and deleting its test org), a legitimately-linked register whose organisation no longer matched the caller's active-org pointer became unresolvable: find() threw DoesNotExistException, which was re-thrown as AuthorizationUnresolvableException and FAIL-CLOSED — denying even a non-admin creating/reading their OWN object in an OPEN schema (404s, IDOR fail-closed firing on a legit register). Fix: load the register with _rbac:false, _multitenancy:false in both resolvers. This does NOT weaken tenant isolation — object-row multitenancy is enforced separately on the data reads (organisation column filters + MagicRbacHandler's active-organisation condition). Verified: rbac section 3 (cross-org isolation, "admin org-Z object filtered out", "cross-org GET denied 404 no existence leak") still passes. The fail-closed remains intact for genuinely-unresolvable schemas (no register link → null → open baseline is unchanged; real DB errors still throw). Verified: full 14-collection sequential run on a fresh NC32 = 0 failures (baseline reproduced rbac failing). Unit tests: 114 PermissionHandler + RegisterMapper tests green on nextcloud:34.
…uses) (#2152) Both the openregister-crud and rbac Newman collections failed in the Code Quality "Integration Tests (Newman)" job. Reproduced exactly as CI (newman on the host runner, CWD = tests/integration, one fresh NC32+Postgres container, no reset). Root causes and fixes: 1. sabre/xml vendor-shadowing (the real cause of BOTH failures) OpenRegister's composer.lock pinned sabre/xml 4.1.0 (pulled transitively by the sabre/vobject dev-dep). 4.1.0's XmlSerializable declares `xmlSerialize(): void`; core Nextcloud ships sabre/xml 2.2.11 (no return type). OR's autoloader registers 4.1.0 first, so core's CalDAV classes fail the signature check with a PHP fatal the moment any calendar is provisioned. - rbac: POST /ocs/v1.php/cloud/users (create e2euser) -> calendar home provisioning -> CalDAV -> fatal -> HTTP 500 (S0). - crud: "File 1: Create File on Object" is the first file op on a fresh install, so OR creates its "openregister" system user on the fly -> same calendar fatal -> 500, and File 3-6 then 404. Fix: pin sabre/xml to ^2.2 (matches the platform) in composer.json and regenerate the lock (sabre/xml 2.2.11, sabre/uri 2.3.4). sabre/vobject 4.5.8 is unaffected (it allows sabre/xml ^2.1). 2. rbac 1a: core first-login skeleton-copy lock race e2euser's first authenticated request triggers Nextcloud core's one-time skeleton copy (completeLogin -> copySkeleton), which under the DB locking provider (CI has no Redis) raises a LockedException on "/<uid>/files/Documents" -> 500 on whatever endpoint is first. Added an "S0b" warm-up request in the rbac Setup folder that logs e2euser in once (tolerant of any status) so the RBAC assertions see a settled home. No RBAC behaviour is weakened; 1a still asserts 403/404. 3. crud file-upload fixture path (hygiene) The Import requests referenced an absolute container path (/var/www/html/custom_apps/...) that never resolves on the CI host runner, so the file silently failed to load and Import tested nothing. Committed a small tests/integration/test-import.csv fixture (with a .gitignore exception) and switched the form-data src to a collection-relative path; Import now returns 200. 4. UserService quota query (real bug, fired in CI on every /user/me) getUsedSpaceMemorySafe queried a non-existent oc_storages.size column and joined oc_storages.id (varchar) = oc_mounts.storage_id (bigint), which PostgreSQL rejects (SQLSTATE[42883] operator does not exist). Rewrote it to read oc_filecache.size for the user's home-mount storage root (bigint join, no cast needed, works on Postgres/MySQL/SQLite) and hardened the catch to \Throwable so a stray Error can never turn a quota lookup into a 500. Fixes 36 pre-existing UserServiceTest errors. Verified from a CI-faithful host-CWD reproduction: crud 194/194 and rbac 41/41 pass, full 14-collection sequence 0 failures, UserService unit suites green.
…tandard (#2154) The change captured only the initial design (5 requirements, most tasks open). It now reflects everything shipped and merged across the fleet: - 6 new requirements — schema-marker mechanism (REQ-FCS-006), Ed25519 signing + verification (007), per-org group RBAC (008), config sets (009), repo-creation + topic-tagging + fetch bridge (010), and installable config-set repositories / OpenBuild convergence (011). Each with scenarios; `openspec validate --strict` passes. - tasks.md updated to the shipped reality with PR references across openregister (#2134/#2135/#2140/#2147/#2148/#2149), nldesign #192, the 8 app markers, hermiq #126/#127, openbuild #181, nc-vue #242 — plus the real-GitHub end-to-end verification and the two external follow-ups.
…t config (#2155) Two gaps in the store's governance/usability: - **Publish now honors visibility.** `publish(..., private: bool)` and the controller's `visibility: private` param let a freshly created store repo be private (previously every store repo was forced public). Defaults to public (backward-compatible); the token needs rights to create private repos. - **The trust controls are API-manageable, not occ-only.** New admin-gated `GET/PUT /api/federated-config/trust` read and write the org's source allowlist, trusted publisher keys, and publish/install group lists — plus a `trustKey` convenience that appends a public key to the trusted-keys list (idempotent). Non admins get 403; an unknown field is a 400. This is the backend a governance settings UI needs (the config keys previously required `occ`). Unit: trust read/write round-trip (set fields, append keys idempotently, unknown field throws). Live-verified on 8080: admin GET/PUT + trustKey append work, non-admin GET is 403, unknown field is 400.
End-user + admin guide for the federated configuration store: concepts (types, bundles, topics, provenance), choosing the store credential, publish, discover/fetch/install, the admin trust/governance API (allowlist, trusted keys, publish/install groups), and configuration sets / installable app repos. Auto-listed via the Docusaurus autogenerated sidebar.
…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.
…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
fixed webpack config so building does not crash
…Y (or#2164) The per-schema agent-context allowlist was absent from ANNOTATION_VOCABULARY, so validateConfigurationEntry() silently dropped it on save. Because Hermiq's AgentContextBuilder (and its JS twin src/utils/agentContext.js) read exactly that key, EVERY agent leaf on EVERY schema fleet-wide resolved an EMPTY context. Fail-closed, so never a data leak — an absent allowlist means 'expose nothing' — but the capability was wholly inert, and invisible by construction: the schema saved with HTTP 200, no validation error reached the caller, and the only signal was a log line. This is the FOURTH recurrence of the same defect class in this list, after x-openregister-processing, x-openregister-contextchat and x-openregister-shareable, each of which carries its own comment saying so. Adds two regression tests (flat allowlist and the per-property refinement form) asserting both that the key survives the round-trip and that it is not reported as dropped. Verified live: PUT of an allowlist onto the hydra-cache 'finding' schema now reads back intact where it previously returned ABSENT.
fix(schema): x-openregister-agent-context was dropped at save — every agent leaf resolved an empty context
…node docs(openspec): or-flow-object-write-node — flows can write objects (create/update/upsert/guarded delete)
…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).
fix(flow,credentials): attribute flow runs to their owner + permit forge label writes
FlowRunController::test() queued its run without the acting user — the same defect as or#2158 in FlowMcpToolProvider::runFlow(), in the one dispatch path that has a session by definition. Consequence: the run was ownerless, so context['triggeredBy'] was null and every attribution-requiring node refused. ObjectWriteNode returned 'This flow run has no owner (triggeredBy); an object write must be attributable.' The interactive 'run this flow now' button could therefore never exercise a write node — the only runs that appeared to work were harness runs with a hand-injected context. Live-verified end to end after this fix, on a naturally triggered run with no injected context: status=completed, triggeredBy=admin, log shows write1 completed itemsIn=1 itemsOut=1, and the object landed in hydra-cache (findings 7 -> 8, owner=admin) read back through the API.
fix(flow): attribute interactive test runs to the caller (3rd instance of the ownerless-run defect)
…gistered
Nextcloud writes oc_jobs only when an app is INSTALLED or UPGRADED. Add a <job>
to info.xml without bumping the app version and it is silently never registered
— no error, no warning, and there is no occ background-job:add to correct it.
Measured on a live instance: 24 of 31 declared jobs were absent. Not only flows —
scheduled workflows, schedule reconciliation, sync, webhook retries, notification
flushing, archival retention and DBAL introspection were all inert. 59 flow runs
sat queued and could never execute.
It hides well: the synchronous counterparts keep working. POST /api/flow-runs/test
calls FlowRunService::execute() directly, so interactive flow testing was green
while every asynchronous trigger queued into a void, staying "queued" forever
rather than failing.
This repair step parses the <job> declarations out of info.xml and adds any that
IJobList does not already have. Because it is a repair step it runs on
occ maintenance:repair, so an instance can be corrected without inventing a
version bump. Idempotent, and one unresolvable job is logged and skipped rather
than aborting the run — the point of the step is to recover an instance.
Two traps this hit while being written, both worth knowing:
1. SimpleXML cannot reach a hyphenated element as $xml->background_jobs; it
needs $xml->{'background-jobs'}. The mismatch returns nothing rather than
erroring.
2. simplexml_load_file() SILENTLY RETURNS FALSE inside the Nextcloud runtime.
NC installs a restrictive libxml external-entity loader at boot, and PHP 8
routes file access through it. The same parse works in a bare php -r process
and fails under require lib/base.php — so the step reported "no <job>
declarations found" against an info.xml holding 31 of them. Fixed by reading
the file and parsing a string. Any app code parsing XML by path is suspect
for the same reason.
Verified by controlled experiment on a live instance: deleted FlowScheduleWorker
from oc_jobs, ran the step, and it reported "Registered missing background job:
OCA\OpenRegister\Cron\FlowScheduleWorker / 1 added, 0 skipped, 31 declared" with
the row restored. Separately, a version bump plus occ upgrade took the instance
from 9 to 34 distinct registered job classes.
Refs or#2170.
fix(jobs): reconcile declared background jobs Nextcloud never registered (24 of 31 were missing)
Adds `executionMode: async|sync` so a trigger can run a flow inline, and a run-level FlowToken that propagates into sub-flows, returns to the parent, and survives pause/resume. The token is a mutable object at context['token']: an object handle survives the by-value array copy, so nodes write to it with ZERO change to the IFlowNode signature — all registered nodes and both leaf apps are untouched. Pause/resume came free: persistResult() already wrote context back on the SUSPENDED path. Verified: 21 unit tests, 236/236 flow suite after merging development, phpcs 0 errors, openspec --strict, and Playwright e2e 4/4 live on 8080. Live testing also surfaced ConductionNL/hermiq#53 — hermiq's resolver claimed every OR flow, so OR's own resolver was never asked and executionMode was silently dropped.
Fourth instance of the or#2158 defect class, after FlowMcpToolProvider::runFlow(), FlowRunService::execute() and FlowRunController::test(). FlowScheduleService::fire() queued with no user, so every natively-scheduled flow ran ownerless: context['triggeredBy'] was null and every attribution-requiring node refused. ObjectWriteNode returns 'This flow run has no owner (triggeredBy); an object write must be attributable.' A scheduled flow could therefore never write anything. A scheduled run has no session, so the owner comes from the flow object itself — the person who created and enabled it, matching the decision that a dispatch is owned by whoever made and activated the flow. This became urgent rather than theoretical: FlowRunWorker and FlowScheduleWorker were registered for the first time in #2172, so scheduled flows now actually execute. Without this fix they would execute and refuse, producing a steady stream of failed runs. Test asserts queue() receives the owner; 7/7 green in FlowScheduleServiceTest.
…tion fix(flow): scheduled runs were ownerless — 4th instance of the attribution defect
… credential providers apphost-schedule-flow-action — manifest schedules[] can only ever run ONE thing: ScheduleActionAllowList::MAP has a single entry, openconnector:synchronization. A virtual app therefore cannot schedule a flow, even though the flow engine can now both call external APIs (openconnector.source-call) and write objects (openregister.object-write). Adds a flow-run action while keeping the closed allow-list — the point of that design is that an app cannot name an arbitrary class. Attribution is mandatory and fail-closed: a scheduled run has no session, so the owner comes from the schedule declaration and the action refuses rather than running ownerless. Writing this spec found the same defect a fourth time in the adjacent scheduler (FlowScheduleService::fire queued with no user) — fixed separately in #2173. app-declared-credential-providers — the provider catalogue is runtime-immutable, so an app author cannot register the credentials their app needs. Two costs observed while building hydra-console: no codeberg/forgejo provider exists, and the github provider permitted no issue-label write (fixed in #2165 only because we could edit OpenRegister itself). The design keeps the security boundary real rather than removing it. Two lanes: a narrowing declaration (same host and auth scheme, provably-subset allow-rules) is auto-admitted because it grants strictly less than the base provider the app could already use; anything introducing a new host or path requires administrator approval. Approval is pinned to a per-entry content digest, so an app that ships benign, gets approved, then widens is returned to pending. Declarations are always app-scoped and namespaced so they cannot shadow or be borrowed from, and inject_only declarations are rejected outright — a declared inject_only entry would be unbounded secret egress authored by the app receiving the secret. NEEDS PO CONFIRMATION: whether the narrowing lane should really skip approval. Recorded as the first deferred question rather than buried in the design.
docs(openspec): schedulable flows + app-declared credential providers
) 15454 tests: 51 errors + 2 failures -> 0 and 0. 25 errors: AttributeMcpDiscoveryTest assigned \OC::$server to a service-less fake and never restored it, so every later test saw a locator whose get() returns null and Response::getHeaders() died on $request->getId(). All passed in isolation — one leak reading as 21 broken tests. Restored in tearDown so a failing test cannot leak either. 19 errors: no stubs for OCP\ContextChat\* (optional app seam, absent from a bare composer install). Added guarded stubs beside the Doriath ones, surface taken from the call sites. 4 errors: FlowRunController gained an IUserSession param; its test still passed five arguments. 2 errors: CardDavBackend stub lacked getAddressBookById(), which ContactService calls. 1 error: bootstrap STDERR output corrupted a @runInSeparateProcess worker's result channel. Emitted once now, with the existing skip switch set for inherited children. 1 failure — REAL PRODUCTION BUG: RelationsController documents that a missing/disabled app is silently skipped, but Server::get() returns NULL rather than throwing, and probing null was recorded in $errors — so a fully successful response still carried an _errors key for all 13 leaf integrations. Resolution is now separated from the call. phpcs clean; each fix verified in isolation before the full run.
…pes (#2177) FlowNodeRegistry::palette() already returned exactly what a flow builder needs and nothing exposed it over HTTP — FlowController shipped only eventCatalog(), so a client could discover what STARTS a flow but not what a flow can DO. A flow's edges[].type names a registered node, so with no palette endpoint a builder hardcodes the list, and it goes stale silently: an unknown type only surfaces when the flow runs. App contributions (openconnector's source-call/synchronization-run, hermiq's agent-step) were invisible to every HTTP client. Adds GET /api/flow/node-catalog mirroring the trigger catalog's envelope and reusing palette() unchanged. Scope-filtered — ?scope=user returns only what a non-admin may actually run. Closes #2176. 5 unit tests; phpcs clean; 15460 Unit tests, 0 errors, 0 failures.
…P surface (#2178) The registry assertion skipped because OpenRegister exposed no palette endpoint — palette() was in-process only. #2177 added /api/flow/node-catalog, so the check is now real. It immediately earned its keep: openconnector's leaves were silently failing to register on a clean install (class_exists load-order guard in register(), openconnector#1076) and no test in either repo could see it. 4/4 against an isolated NC 34 + Postgres instance on merged development.
…ork (#2182) * wip: preserve uncommitted working tree (AppHost settings plane, flow wiring, schema dedup) Snapshot of the local working tree taken before reconciling with origin/development, which had moved 69 commits ahead. Much of this tree is already upstream (27 of 36 new PHP files and 13 of 49 modified files are byte-identical to origin/development). The genuinely new work preserved here is: - lib/AppHost/{Controller,Service}: generic settings plane (GenericSettingsControllerBase, GenericSettingsService, RegisterConfigResolver) - lib/Command/DedupCollidedSchemasCommand.php - unit tests for the above plus HandlesExceptionsTrait - four openspec changes (apphost-settings-plane, apphost-schedule-flow-action, app-declared-credential-providers, or-flow-object-write-node) - lib/Settings/flow_register.json and flow e2e coverage Committed on a stale base on purpose so the subsequent merge of origin/development is a real three-way merge rather than a tree overwrite. * docs(openspec): audit-seal-backlog-repair — drain the unsealed audit backlog Measured on the shared dev instance: 108,151 of 227,063 audit rows (48%) carry no hash, interleaved across the whole id range (31,518..290,493 = min..max id). Three defects, all evidenced: 1. insertHashChained() seals per row under a global exclusive advisory lock (3 attempts x 50ms, fail-soft). Under any concurrency each insert pays up to 150ms and then abandons the seal anyway. Measured ~152 rows/min during the maintenance:repair that had to be killed after 74 minutes with ~12h left. The batched sealRows() path already exists but single inserts never reach it. 2. The backfill that specs/audit-hash-chain/spec.md:105 requires does not exist. harden-audit-seal-concurrency (12/12 complete) made the lock fail-soft on the explicit promise that a "later seal pass chains them" -- that pass was never built, so every contended write permanently degrades the chain. 3. verifyChain() skips ANY null hash and still returns valid: true. The comment claims "pre-migration entries" but no cutover marker exists in lib/, so the tamper-evidence check currently passes over a table that is 48% unverified. The change specifies a windowed driver around sealRows() (one lock per window, not per row), a two-phase read so sealed rows do not have their ~5.3KB payloads fetched just to contribute a chain link, a partial index for the backlog cursor (today it pkey-scans with a filter at 784ms/2000 ids), a hard-capped background job plus an occ command, and a cutover marker so unsealed rows stop hiding behind a passing verification. Design records why a naive bulk call is impossible: sealRowsLocked() SELECTs * over [min,max], which for this backlog is ~227k rows x 5,270B ~= 1.14GB in PHP. * docs(perf): plan to bring object writes under 500ms + fix features.json drift A single-object create currently takes 13-99s on the dev instance (six runs on larpingapp/character, two-field payload each time: 13.6/17.8/20.4/41.0/62.8/99.1s). This is NOT the CloudEvent storm — that was openconnector's inert recursion guard, fixed separately, and a create now emits 1 event rather than 255. The remainder is our own write path. Counter deltas across one HTTP 201 create (the 41.0s run): sequential scans of oc_openregister_schemas 5,135 sequential scans of oc_openregister_registers 6 transactions committed 12,541 Four measured costs: 1. 5,135 schema resolutions against a 1,917-row table. SchemaMapper::find() HAS a request cache and is a shared service, so this is either a cache key too specific (rbac/multitenancy flags multiply it 4x) or an uncached sibling on the hot path. The query is a seq scan by construction — SELECT * hydrates a ~2KB properties blob and LOWER(slug) defeats any index ('Rows Removed by Filter: 1916'). ~4ms x 5,135 = ~20s, half the run. 2. Resolving an object reference whose table is unknown emits a UNION ALL with one branch per magic table. At 2,728 tables that is 690KB of SQL: planning 3,404.9ms, execution 546.1ms. 86% of the cost is PARSING a statement that returns zero rows, so no index can help — and it is usually avoidable, since character's six relation properties each already declare their target schema. 3. 12,541 commits for one create: essentially every statement autocommits, and the request waits on each fsync. 4. CloudEvent fan-out, audit-trail sealing (228,932 rows), notification history and oc_activity all run before the response is returned. Target p95 <500ms with the 2,728-table shape unchanged — fixing the write path, not shrinking the dataset. Task 1 is deliberately 'attribute the 5,135 calls before changing anything': guessing at a hot path is how the CloudEvent guard stayed inert for so long. Also fixes an unrelated red gate: openspec/specs/saved-search-views/spec.md was missing the blank line before '## Requirements', so the features-manifest generator swallowed the heading into the feature summary and docs/features.json drifted. 'quality / Features Check' fails on development because of it. * style(dedup): satisfy phpcs on the schema-dedup command Conduction's standard requires named arguments on internal calls, forbids inline IFs, and has its own spacing///end conventions — the command shipped with 21 violations and turned 'quality / PHP Quality (phpcs)' red. 14 fixed by phpcbf; the rest by hand: setName/setDescription/addOption -> named arguments (four calls) splitOne/splitOneLocked/findCollisions/pickOwner call sites -> named arguments the two ternaries in execute() -> explicit if blocks All 11 PHP files changed on this branch now pass phpcs. * ci: regenerate docs/features.json from openspec/specs/ [skip ci] * style(phpcs): clear the 15 pre-existing violations that kept phpcs red lib/ was 15 errors over 9 files before this branch, so 'quality / PHP Quality (phpcs)' failed on development and on every PR opened against it. 6x curl_close($ch) deprecated since PHP 8.0 and a no-op — CurlHandle is an object freed when it leaves scope, not a resource needing an explicit close. Removed, with a comment so nobody re-adds them. 3x missing @param FlowRunController::__construct($userSession), FederatedConfigService::publish($private), FlowScheduleService::fire($owner) 6x file header ReconcileDeclaredBackgroundJobs.php put its docblock AFTER declare(strict_types=1), so phpcs read it as a stray inline block rather than the file header; tag order was @author/@license/@copyright. Moved above the declare and reordered to @author/@copyright/@license. phpcs now reports 0 errors across all 72 files in lib/. phpstan is unchanged — the same 7 findings exist with and without this commit (verified by stashing), so nothing here introduced or masked one. --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.