Release: merge development into beta - #29
Conversation
Two more settings-section pages had their heading rendering under the
Nextcloud navigation toggle (same class of issue as the dashboard/store/
guardrail pages — the manifest-v2 renderer provides no toggle clearance,
so a page's own top heading collides with the 44px toggle):
- McpTools (type:custom): heading text started at 23px → now 79px.
- Compliance (type:index) below-header widget ComplianceOperations: its
FIRST section heading ("Incidents") started at 21px → now 77px. Scoped
with `:first-of-type` so only the topmost heading shifts; "EU AI Act
audit export" and "Retention" keep full width (verified 21px, unmoved),
matching nc-vue's "only the header shifts right" rule.
Algorithm register needs no fix — it's a pure index page with no colliding
top heading.
…+ Compliance headings' (#115) from wip/hermiq-settings-heading-margins into development
The AI companion widget sends no agentUuid (no agent picker in v1), so ChatStreamController picks one server-side. Until now that was purely 'the first agent in register order the user can access' — arbitrary on a busy instance, and it can land on a misconfigured agent (e.g. one whose model is not available on the active provider, which then fails silently with an empty reply). Add an app-config key 'companion_agent_uuid'. When set AND the current user may access it, pickFallbackAgentForUser() returns that agent; a missing/inaccessible configured agent logs a warning and falls through to the existing access-scoped register-order scan (unchanged behaviour when unset). Set with: occ config:app:set hermiq companion_agent_uuid <agent-uuid> Verified live: with the key pointed at a claude-opus-4-8 agent, the companion turn dispatched with model=claude-opus-4-8 (the configured agent) instead of the register-order qwen2.5 it picked before. Tests: configured-agent-preferred + unconfigured-falls-back-to-first; full ChatStreamController suite 8/8 green.
…debar The chat page is legitimately custom (streaming SSE + tool-call consumption, not expressible via index/detail page types), but its conversation-history sidebar rendered each conversation as a bordered card with a primary-outlined active state — visually inconsistent with the flush index-sidebar look used by cases/skills/agents. Restyle the rows to the nc-vue index-sidebar / NC app-navigation convention: no per-item border, subtle rounded hover, a filled active pill, and tighter 2px row spacing. Purely visual — the chat behaviour is unchanged. Live-verified against procest /cases; 0 console errors.
… the nc-vue index sidebar' (#117) from wip/hermiq-chat-sidebar-parity into development
Proposes how document-shaped context (a project design.md, standards doc,
persona brief) reaches an agent, building on the existing Context/
ContextAssembler model.
Core proposal:
- Three distinct concepts, one assembly seam: Skill (capability), Context
(situation/project reference), Memory (learned state) — all converge in
one budgeted preamble at run start.
- design.md-style context becomes a first-class Context 'documents' source
(inline markdown, {name, body, format, description}) — self-contained,
versioned (OR AuditTrail), authored with the SKILL.md markdown editor
pattern — distinct from bare NC 'files' refs.
- No new trust surface: context is untrusted prompt input, subject to the
org guardrail policy; inherits run identity (ADR-023).
- Reuse the SkillFormModal editor pattern; GitHub-shareable context +
conversational context-creator parked as follow-ons.
Status: proposed — no code until accepted. If accepted, builds as a
config→code chain (Context.documents schema; ContextAssembler + editor).
…ed)' (#118) from wip/hermiq-adr-context-concepts into development
Two feature docs for the Docusaurus site: - approvals.md — human-in-the-loop gates: what they are, the four gate points (scheduled/flow/webhook/tool, incl. guardrail 'confirm' classification), the requested→decided→consumed lifecycle, and where to find the Approvals inbox. - incidents.md — human-authored records of agent malfunctions under EU AI Act art.12: the fields, how they link to agents/runs, the three-layer audit story (runs + approvals + incidents), and the Compliance audit export. Grounded in the real schemas + ApprovalService/agent-lifecycle-governance behaviour. Autogenerated sidebar picks them up by position (6, 7). NOTE: hermiq.conduction.nl deploys from the 'documentation' branch, which needs a human push — these land on development as the source.
…nduction.nl)' (#119) from wip/hermiq-docs-approvals-incidents into development
…ifacts
Flip ADR-024 to accepted; add the config→code chain implementing it:
- hermiq-context-documents-schema (config): Context schema gains a
'documents' array ({name, body, format=markdown, description}) — inline
document-shaped context (design.md et al), distinct from files/objectQueries.
- hermiq-context-documents (code, depends_on ^): ContextAssembler gains
resolveDocuments() (titled sections, same budget); a Context editor modal
(mirrors SkillFormModal) + a Contexts management page.
Both validate via status; task counts 4 and 6.
… core)
A tool-carrying cli turn was refused outright (link 2's fail-loud), because
`claude -p` accepts no tool schema — custom tools reach a vendor CLI ONLY
via MCP. This gives the CLI tools WITHOUT any governance leaving Hermiq.
Hermiq now serves a governed MCP endpoint the containerised CLI dials:
- McpRunController (POST /api/mcp/run) — JSON-RPC MCP server. tools/list
returns exactly ToolGrantResolver::resolve(agent.tools, catalog) (default
deny, wildcards read-only); tools/call dispatches through FacadeToolInvoker
so guardrails, the approval gate, redaction, model-policy, budgets and
tracing all still apply — no second execution path. Identity comes from the
run token only; the body cannot redirect the run. Guardrail-deny / pending
approval / ungranted tool => 200 with result.isError, nothing executed.
- RunTokenService — 256-bit ISecureRandom tokens bound to (runId,agentId,
userId), ~150s TTL in ICache, hash_equals compare, consumed in a finally,
never logged.
- EgressAuthorizeController (POST /api/egress/authorize) — the egress PDP,
answering straight from WebResearchEgressGuard::assertSafe(), the SAME
policy source hermiq.webFetch uses. Default DENY.
- ProviderFactory cli branch — mints the token, assembles the MCP server
config, dispatches. Fails LOUDLY (503, naming the cause) when the endpoint
is unreachable, the token cannot be minted, or the granted tool set is
EMPTY — never a silent text-only downgrade (the trap at :624-635).
- Runner — writes the token-bearing MCP config to a 0600 scratch file (never
argv), and locks the CLI down with --tools "" --strict-mcp-config
--mcp-config <file> --allowedTools mcp__hermiq__*; refuses to spawn if the
boundary flags are missing.
Both new routes are #[PublicPage]+#[NoCSRFRequired]: the per-run token IS the
authorization (no NC session exists in the container), stated in-docblock so
semantic-auth review reads it as intentional.
Task 1 (verify the CLI's http-MCP client BEFORE building) is recorded in
discovery.md as verified fact: type:"http" works and custom headers ARE
forwarded, but the CLI's MCP client is a STRICT schema validator — it rejected
a whole tools/list over one argument-less tool serialising
inputSchema.properties as [] instead of {} (fixed centrally in
openregister#456). McpRunController therefore emits properties as an object.
Also corrects the superseded llm-cli-runner-exapp spec: the tool-schema
dispatch requirement is impossible and is gone, and 'no Nextcloud access' is
narrowed to exactly the token-gated Hermiq origins.
Tests: 16/16 new (McpRun, EgressAuthorize, RunToken); ProviderFactory suite
72/210 green (http + text-only cli paths bit-for-bit unaffected); runner 16/16
incl. argv lockdown, 0600 config, token-off-argv. openspec validate --strict
passes.
Task 8 (egress proxy sidecar + container network + docs) intentionally NOT in
this commit — it is the network-layer backstop and is tracked separately.
Implements hermiq-context-documents-schema (config, chain head).
Document-shaped context — a project design.md, standards doc, persona
brief — becomes a first-class Context source: `documents` is an array of
{ name, body, format(default markdown), description } authored inline on
the Context bundle. Self-contained and versioned (OR AuditTrail), and
distinct from `files` (Nextcloud file refs) and `objectQueries` (live
data), per ADR-024's concept split (Skill=capability, Context=situation
reference, Memory=learned state).
Config only — no code. The ContextAssembler render + editor are the
dependent change (hermiq-context-documents).
Register 0.14.0 → 0.15.0, Context schema 0.1.0 → 0.1.1, app 0.1.78 →
0.1.79 (both version fields gate the re-import).
Live-verified: `documents` present in the deployed Context schema;
imported_config_hermiq_version = 0.15.0.
…ts schema' (#121) from wip/hermiq-context-documents-specs into development
linkToRouteAbsolute() returns the URL Nextcloud publishes to BROWSERS (overwrite.cli.url / trusted domain). The CLI dials the governed MCP endpoint from INSIDE the runner container, where that host routinely is not Nextcloud: a stock dev instance publishes 'http://localhost', which inside the container is the CONTAINER — so every tool call would fail with a connection error that reads like a broken endpoint, not a wrong URL. AppAPI already records the container-facing origin (its daemon's nextcloud_url, e.g. http://nextcloud). Add an app-config override 'mcp_run_base_url' to pin the same value; unset keeps the published URL (correct whenever Nextcloud's public origin IS container-reachable). occ config:app:set hermiq mcp_run_base_url --value=http://nextcloud Found by live deployment, not unit tests — the tests mock IURLGenerator, so no amount of unit coverage would have caught it.
…ix AppAPI auth' (#109) from fix/llm-runner-exapp-deploy into development
link 3 structurally depends on hermiq#109: without it the image has no /app/deploy (the egress-jail entrypoint dies with exit 127) and the runner still speaks the wrong AppAPI auth (AA-SIGNATURE HMAC instead of AppAPI 34's base64 AUTHORIZATION-APP-API), so AppAPI could never authenticate to it. test.sh conflict: both branches appended an '(e)' section. Kept both — the /enabled lifecycle tests stay (e), the governed-MCP argv tests become (f). Runner suite 18/18 green.
Implements hermiq-context-documents (code half of the ADR-024 chain).
- ContextAssembler gains resolveDocuments(): each `documents[]` entry is
rendered as a titled section merged into the SAME $sections array as
files/objectQueries, so it inherits the existing charBudget/
needsConsolidation contract — no new budget. Prefix is byte-identical to
resolveFiles' ("Source: {name}\n{body}") so all three source kinds render
uniformly. Malformed entries are skipped non-fatally (one bad document
must never blank the preamble). No per-document byte cap; `format` is
carried but not branched on (all treated as plain text for now).
- ContextFormModal.vue (mirrors SkillFormModal's #form-dialog contract):
name/description/charBudget + a documents list (name + CnMarkdownEditor
body + description, add/remove) + files and objectQueries editors. Edit
merges the existing payload so viewRefs/needsConsolidation survive.
- Contexts management page: type:index over schema `context` + a "Contexts"
nav item, mirroring SkillsCatalog.
No new trust code — guardrail input filters already apply to the assembled
preamble (ADR-024 Rule 3). No schema fields added (the schema is the
dependency change).
Live-verified end-to-end: created a Context with a design.md document via
the editor; it persisted with name/format=markdown/body intact (newlines
preserved). Suite 1062 tests green (3 new ContextAssembler tests incl. the
budget nudge), phpcs + phpstan clean, 0 console errors.
… editor (ADR-024)' (#122) from wip/hermiq-context-documents-code into development
Archives hermiq-context-documents-schema + hermiq-context-documents; spec deltas merged into context-documents. 43 specs validate. Fixed a real validation error the ff missed: the schema change's requirement put its MUST on line 3 — the validator only scans the FIRST line after the '### Requirement:' header, so it read as having no MUST/SHALL. Added a leading MUST sentence. (The '## Why' proposal warning is non-blocking: this repo's conduction schema uses Summary/Motivation, as every archived change does.)
…ents chain' (#123) from wip/hermiq-context-documents-archive into development
…rified The spec's flag lockdown was self-defeating and no unit test could see it: every layer was green while the model still answered 'I don't have that tool'. Two flag facts, both VERIFIED against the real CLI (2.1.x): 1. `--tools` EXCLUDES MCP tools. It selects from the BUILT-IN set only, so `--tools ""` yields NO tools at all — MCP included — and --allowedTools does not rescue them. The spec required exactly this, so the transport was destroying the tools it exists to deliver, silently: exit 0, empty stderr, model says it has no such tool. Replaced with an explicit `--disallowedTools <builtins>` denylist, which strips shell/filesystem/ native-web while LEAVING MCP intact. assertGovernedArgs now REFUSES `--tools` outright so the regression cannot come back. 2. The CLI DEFERS MCP tools and loads them on demand via ToolSearch. The first denylist denied ToolSearch — which silently made every governed MCP tool unreachable even though tools/list served them correctly. ToolSearch is now deliberately NOT denied, with a comment saying why; it can only surface servers named by --strict-mcp-config (Hermiq's endpoint alone), so it grants no extra reach. A denylist is weaker than an allowlist-of-none: a NEW built-in arrives un-denied. That residual risk is carried by the container (no default route off the egress allowlist, read-only fs, no mounts, non-root) — the backstop layer the design already argues for. LIVE END-TO-END VERIFIED: a chat turn on a Claude Max subscription, through the official CLI in the jailed runner, called Hermiq's governed MCP endpoint (run-token verified, tools/list = the agent's grant, tools/call via FacadeToolInvoker) and CREATED an OpenBuild schema — 'mcp-test-app-production-author' (schema id 4598, required [name]), confirmed in the database, not merely claimed by the model. Tests: 88 PHP / 250 assertions; runner 18/18 (argv lockdown now asserts --disallowedTools and refuses --tools).
… escalate Two silent-degradation defects on the grant-resolution path, both found while closing the governed-CLI gaps. 1. PRIVILEGE ESCALATION. resolveFunctions() passed the RESOLVED id set straight to ToolRegistryFacade::listTools(). An empty whitelist means "all tools allowed" there, so a grant set that expanded to nothing — e.g. a wildcard whose derived verb ids the catalog does not carry — silently became a grant of the ENTIRE catalog, destructive tools included. It now returns the honest empty set instead. 2. SILENT ZERO. An agent whose grants were configured but matched nothing produced the same empty function list as a deliberately tool-less agent, so the turn continued text-only while every layer reported success — the exact degradation the governed-CLI transport exists to prevent. "No tools on purpose" is now a first-class sentinel (ToolGrantResolver::NO_TOOLS_SENTINEL, which AssistantService's copy aliases rather than re-spells), letting ToolLoop tell the two apart and raise ToolGrantResolutionException naming the unresolved ids. expandLegacyIds() leaves the sentinel alone — expanding it to openregister.__none__ would have made valid tool-less agents throw. The empty($functions) gate in ProviderFactory's cli branch could never see this distinction (both cases arrive as an empty array), which is why it is enforced at the resolution site instead; its comment now says so. Three ToolLoop tests returned [] from listTools() as a don't-care value that now carries meaning; they assert which whitelist reaches the facade, so they return a resolving descriptor and keep their original intent. Also fixes pre-existing phpcs errors in ProviderFactory (the $appConfig param added for mcp_run_base_url was undocumented). 1089 tests / 3347 assertions green; phpcs clean.
…them Two defects found live, both making a governance surface useless in silence. 1. TOOL GRANTS WERE READ-ONLY FOR EVERYONE. AgentToolGovernanceWidget read `agent.owner`, but the store returns an OpenRegister object whose own properties are the agent's schema fields — the owner lives in `@self`. So isOwner compared `undefined === uid` for every user and canEdit was always false: the grant editor shipped permanently read-only, including for the one person it is meant to be writable for. Verified live: the agent's owner IS "admin" at `@self.owner` while `agent.owner` is absent, and feeding the computed the resolved value flips isOwner false→true and enables Grant/Save. 2. THE FAIL-LOUD WAS MUTE TO THE USER. ChatStreamController masks every exception message behind "An internal error occurred." — correct by default, since a message can carry a connection string or key fragment. But a ToolGrantResolutionException message is safe by construction: it names the agent's own grant ids, configuration its owner already reads in the grant editor. Masking it left the owner with a generic error for a misconfiguration only they can fix — loud in the log but mute to the person who can act is not loud enough. Now caught by type and emitted verbatim under a distinct `tool_grants_unresolved` code; every other failure stays masked. Verified live through the chat UI: an agent granted `openregister.schemas` (plural — the tool is `openregister.schema`) now answers in the thread with "This agent's tool grants resolve to no tools: openregister.schemas. Check the ids against the tool catalog ..." where it previously ran text-only and reported success. 1089 tests / 3347 assertions green; phpcs clean. Note: hermiq has no frontend test tooling, so the Vue fix is covered by live verification, not a unit test.
The one message this controller shows a user verbatim was English-only. It is now rendered through IL10N at the CONTROLLER — the right seam: the exception carries structured data (getGrants()), and a service throwing it has no business knowing the reader's locale. The grant ids are interpolated, never translated; they are identifiers. Key is the English source string (per convention), added to l10n/en.json and l10n/nl.json. The literal is concatenated only to satisfy the 150-char line limit — the assembled runtime string is byte-identical to the catalogue key, verified by extracting it from the source and comparing; drift there would make every locale silently fall back to English. Also adds the test that locks the security boundary this touches: the new `tool_grants_unresolved` frame names the ids and the remedy and is NOT masked, while the existing test still proves every other exception IS masked (an `sk-SECRET` in an exception message never reaches the wire). That test immediately earned its keep: it caught a missing `use ToolGrantResolver` import in the code above, which resolved `ToolGrantResolver::NO_TOOLS_SENTINEL` to `OCA\Hermiq\Controller\...` — a runtime fatal on the exact path that exists to help the user. Two test files constructed the controller positionally and broke on the new param (ArgumentCountError) — both updated; the ChatStreamControllerTest mock interpolates like the real translator so assertions read the user's message. 1090 tests / 3352 assertions green; phpcs clean.
…(Task 8)
Completes the last unbuilt task of link 3, and MIGRATES the deployment off the
old Option A iptables jail rather than adding a second option beside it.
Why the migration: the jail carried its own copy of the allowlist. A second copy
is a second policy — it drifts from the one the agent's webFetch tool obeys, and
iptables cannot express "may THIS run reach that host?" because it has no idea
what a run is. Now the runner sits on an `internal: true` network with NO default
route, and its only way out is a sidecar that asks Hermiq's PDP about every
single connection. WebResearchEgressGuard stays the one policy source for both
layers. The runner needs no NET_ADMIN and no root any more: there is no jail to
install because there is no route to jail.
The proxy is deliberately dependency-free (Node stdlib only) — the component
every connection passes through should not carry a supply chain of its own — and
DENIES by default: `allowed: true` is the only permit signal, and an
unreachable / slow / erroring / unparseable PDP all deny. With no EGRESS_PDP_URL
it refuses to start rather than run as an open relay. Each of those paths has a
test; a truthy-but-not-true `allowed` ("yes") is tested too, since JS would
happily read that as a permit.
🔥 EVERY cli turn now mints a run token, not just a tool-requiring one: the token
is also the identity the proxy presents to the PDP, and without it a text-only
turn cannot reach api.anthropic.com at all. The two mints differ in strictness on
purpose — and that distinction is load-bearing: minting with the GOVERNED rules
everywhere would have demanded an agent, and conversation-title generation calls
this path with `agentId: null` (ConversationManagementHandler), so every title
would have 503'd the moment executionMode:cli was switched on. Text-only turns
get a tolerant egress-only identity (agentId '' ⇒ resolves zero tools ⇒ fails
closed) that never blocks a turn it cannot help. Regression test added.
The run token reaches the CLI as `HTTPS_PROXY=http://run:<token>@host:port` in
its ENVIRONMENT — never argv, where the process table would expose it — and is
assigned after the passthrough so a stray static proxy var cannot shadow it.
NO_PROXY is never set: an exemption list would be a hole in the only route out.
Docs state the revised model in full: non-root, no mounts, no default route,
per-call env-only credentials, the two token-gated Hermiq origins, the two
complementary layers, the CONNECT host-granularity limitation (and why
terminating TLS to fix it is a worse trade), the migration steps with a
verification that actually proves the route is gone, and that Claude Max is
PERSONAL-SCOPE ONLY per the Anthropic ToS.
1091 PHP tests / 3353 assertions; 19/19 runner checks (14 new egress cases).
phpcs clean; `openspec validate --strict` passes.
…g notes The docs assumed the reader already knew what an agent, skill, memory, context, MCP or RAG is. Nothing explained them, so anyone new had nowhere to start — and intro.md opened with "manifest-first Vue 2 frontend rendered by CnAppRoot", which is a sentence for us, not for a reader. Add a Concepts section, written for someone who has never used an AI system: - concepts/index.md — "Agentic concepts, explained". The overview: starts from the problem (a chatbot forgets; you want a colleague), a one-line table of every concept with an everyday analogy, how they fit together as "hiring someone", and the two distinctions people trip over (skill vs context, memory vs context). - concepts/agents.md — what an agent is made of; the prompt as a job description; choosing a model; why an agent is a record, not a setting. - concepts/skills.md — reusable instruction sheets; agentskills.io; both authoring routes; the quarantine review gate and why it exists. - concepts/memory.md — what persists between conversations; memory vs context; per-user isolation; why it gets consolidated. - concepts/context.md — documents vs files vs object queries and when each is right; the budget; context is material, never instructions. - concepts/tools-and-mcp.md — what MCP actually means and what it buys you; tools are granted, not assumed; auto/confirm/deny risk classification. - concepts/rag.md — retrieve → augment → generate; RAG vs context vs memory; search modes; sources spend budget; it cannot see past your permissions. - concepts/runs-and-schedules.md — the four triggers; what happens in a run; why unattended runs need delivery, gates and cost limits. intro.md now points newcomers at the overview first. Also unpublish four internal engineering notes that were live on the public docs site (draft: true — files kept, reversible): - canonical-files.md, fleet-extras-audit.md, fleet-drift-deeper.md — fleet template/audit notes with ZERO mentions of hermiq across 664 lines. - PORT-PLAN.md — the internal Hermes→Nextcloud port plan. openregister.conduction.nl 404s on the same paths, confirming the fleet norm is not to publish them. Live-verified: 8 concept pages 200, 4 internal notes now 404.
docs: explain the agentic concepts + unpublish internal engineering notes
Answers the question everyone actually has, and leads with it: "What if I want an assistant that summarises my mailbox every morning — but can't read my files, and can't delete my mail?" The architecture, stated as the design it is: - The model runs in a hardened container on your own hardware. - That container has NO outside access and needs none — no internet, no files, no tools, no network. It only thinks. A brain in a jar. - Hermiq has the hands. Every file read, record lookup, internet fetch and tool call goes through the Hermiq layer, which does it AS YOU and only if you granted it. So "summarise my mail but never touch my files" is enforceable configuration the model cannot see or change — not a hope written into a prompt. Two independent reasons it cannot go around: it has no network (the egress jail DROPs outbound traffic at the kernel before privileges drop), and it has no tools (access is something Hermiq performs, not something the model holds). Includes a mermaid diagram of exactly that mailbox assistant (granted: mail; not granted: files, records, internet), a second diagram of the grant → guardrail → approval → audit chain, a concrete build table for the mailbox case, the three model-hosting paths, and why it adds up to EU AI Act compliance. Diagrams are deliberately emoji-free: the meaning is carried by colour, line style and words, so it cannot degrade to tofu boxes on a viewer lacking emoji fonts (verified: this environment renders emoji at exactly the tofu width, i.e. not at all — so their appearance was unverifiable). Live-verified in a browser: both diagrams render as real SVGs (820x459, 820x213), zero parse errors.
docs: add The safe setup — sovereign architecture with diagrams
Hermiq is the AI assistant across the fleet — the CnAiCompanion floating button and chat surfaces already use Material Design "Creation" (the two-star sparkle), but the app itself showed a generic document glyph in the apps menu and header. Use the same icon so Hermiq reads as the AI everywhere. Kept white-fill: Nextcloud recolours app-menu icons and expects a single-colour white glyph.
Picks up the two releases that landed after 2.2.0-vue3.1:
2.2.0-vue3.2 four dashboard defects — date-range chip shows its dates and
calendar-aligned presets, a dangling labelResolve no longer
renders a raw UUID, and the table's "View all" pins to the
bottom instead of scrolling away
2.2.0-vue3.3 gridstack's stylesheet now ships with the library that
requires it; CnFormDialog splits over-long schema descriptions
behind an info popover; CnContextMenu closes again on outside
press and stops hijacking every popper with a cursor transform
Lockfile regenerated with npm 10.8.2 to match CI's node 20 toolchain — local
npm 11 prunes optional entries that do not apply to the current platform, which
makes CI's `npm ci` fail with "Missing: ... from lock file". Running `npm ci`
locally does not reproduce it, because npm 11 accepts its own lockfile.
Verified: `npx npm@10.8.2 ci --dry-run` exits 0, and `USE_LOCAL_LIB=false
npm run build` exits 0 with no unresolved modules and no reference to a sibling
nextcloud-vue checkout. USE_LOCAL_LIB=false is load-bearing: webpack aliases
@conduction/nextcloud-vue to ../nextcloud-vue/src when that sibling exists, so
a plain build can silently compile the sibling instead of the package under test.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 14:54 UTC
Download the full PDF report from the workflow artifacts.
…failing CI (#157) v1.0.1 is `f4d9756` (2026-08-03) and predates three gate fixes, so every Hydra Gates run this repo has ever made executed a script in which 16 gates reported PASS when their helper never ran (ConductionNL/.github#147), gate-33 had no axe report to read and never said so (#148), and gates 6 and 7 reported PASS on an empty scope (#149). The tick was identical either way, which is why nothing in this repo's history shows it. That pin is now also RED, and the mechanism is worth writing down. quality.yml is referenced `@main` while this package is PINNED, so the two can desync. #164 flipped `hydra-gates-require-full-coverage` to default true in the shared workflow, and that flag requires a gate to DECLARE itself not-applicable. v1.0.1 contains ZERO `_skip` calls; v1.3.0 has 36. v1.0.1 has no vocabulary to declare, so every absent prerequisite became "DID NOT RUN" and failed the job — for gates the repo has no subject matter for. Measured on this branch, diff-scoped against origin/development exactly as CI scopes it, in a private mount namespace with a private tmpfs (the runner's ~50 /tmp/hydra-gate-*.log paths are shared state and two concurrent runs corrupt each other's counts, .github#158 item 6): v1.0.1 exit 98 FAIL — "GATES THAT DID NOT RUN: 24 33" v1.3.0 exit 0 PASS — those gates named NOT APPLICABLE, with reasons Independently confirmed end-to-end: doriath#160 changed this one line and nothing else, and its Hydra Gates job went failure -> success. v1.3.0 is `f7eaf2a` = .github@main at the time it was cut. Refs ConductionNL/.github#159
hermiq registers an integration leaf on BOTH faces — the `hermiq-agent`
`LeafDescriptor` contributed through `RegisterLeafProvidersEvent`, and the
`registerIntegration({ id: 'hermiq-agent' })` mount pair in
`src/integration-leaf.js` — but shipped no `scripts/check-integration-parity.sh`,
so hydra gate-24 reported:
[gate-24] integration-parity: SKIPPED (structural) — ... server↔JS leaf
parity (ADR-066 Decisions 4/7 ...) is UNVERIFIED
Nothing correlated the two halves. The listener's own comment says the
`surfaces` list is written out on both halves "so the cross-layer parity gate
(gate-24) has two explicit sets to compare" — there was no gate comparing them.
This adds the checker gate-24 invokes. It is SELF-CONTAINED on purpose: the
canonical Node check in @conduction/nextcloud-vue validates that library's own
built-ins, its ADR-066 cross-reference is WARN-only, and its `scripts/` dir is
not published to npm — so the historic wrapper shape resolves nothing in CI and
exits 0 having checked nothing. Every way this one can fail to check exits
non-zero with a named reason instead.
Rules, all hard (ADR-019 AD-11/AD-13, ADR-066 decisions 4 and 7):
R1 complete render pair for the declared renderMode (mount+unmount / tab+widget)
R2 server↔JS id correlation, both ways (phantom leaf / orphan registration)
R3 renderMode agreement across layers under a shared id
R4 metadata agreement (label, icon, group, requiredApp, referenceType, surfaces)
R5 a spread-inherited identity must come from the leaf-owning package
R6 an offlineConfig must name schemas/properties the repo actually declares
hermiq exercises R1 (1), R2 (2), R3 (1) and R4 (5) — the counts are printed, so
"verified" and "nothing to verify" are distinguishable in the log. A run where
every rule has zero subject matter fails rather than passes.
Positive control (proved it can fail, then restored):
* renderMode drift — PHP RENDER_MODE_MOUNT -> RENDER_MODE_COMPONENT
=> exit 1, "[R3 renderMode] leaf "hermiq-agent" declares renderMode
"component" server-side ... but "mount" in its JS registration"
* surfaces drift — dropped 'single-entity' from the PHP SURFACES const
=> exit 1, "[R4 metadata] leaf "hermiq-agent" field `surfaces` mismatch
across layers"
Both restored; the check passes on the unmodified tree.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 18:55 UTC
Download the full PDF report from the workflow artifacts.
fix(gates): a real server↔JS leaf parity check — gate-24 was verifying nothing
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 19:27 UTC
Download the full PDF report from the workflow artifacts.
…o its own ruleset (#155) * fix(phpmd): scope the lib/Migration UnusedFormalParameter exclusion to its own ruleset The nested <exclude-pattern> inside the UnusedFormalParameter <rule> was inert: PHPMD 2.15 honours exclude-patterns only as direct children of <ruleset>, so lib/Migration was scanned by the very rule the pattern was written to spare. Hoisting the pattern to the top level of phpmd.xml would have worked but is applied at file-collection time, dropping lib/Migration from EVERY rule and silently swallowing real complexity, StaticAccess and method-length findings. UnusedFormalParameter now lives alone in phpmd-unusedparams.xml with a top-level */Migration/* exclude, and the phpmd composer script runs both legs keeping the worst exit code. * fix(phpmd): narrow the exclude-pattern to */lib/Migration/* */Migration/* matches any directory segment named Migration, so it would also exempt ordinary classes under lib/Service/Migration/ and similar, which have no interface-mandated signature and must stay analysed. Measured on openconnector, that broader form hides a genuine UnusedFormalParameter finding in lib/Service/Migration/. Only the app's own lib/Migration/ holds IMigrationStep implementations, so only that directory is exempted. Re-verified after the change: the lib/Migration probe is still excluded, the non-UnusedFormalParameter Migration probe is still reported by leg 1, and the retired-finding counts are unchanged.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-05 21:12 UTC
Download the full PDF report from the workflow artifacts.
…d refactors (#160) CnPageRenderer only looks a component up in `customComponents` for a page whose `type` is "custom"; every other type is resolved from the lib's `defaultPageTypes`. Three components were left registered for, or written against, page shapes that no longer route to them, so they were bundled (or not even imported) but could never mount. - `views/AlgorithmRegister.vue` — the `AlgorithmRegister` manifest page is `type: "index"`, so this key is never consulted. Its Publish/Withdraw write path is not lost: `views/AiFeatureRegister.vue`, mounted live from `settings.js` -> `AdminRoot.vue`, offers both actions over the identical `publishAiFeature()` / `withdrawAiFeature()` endpoints. - `utils/algoritmeregisterReadiness.js` — sole importer was the above. AiFeatureRegister carries its own readiness logic. - `views/FeaturesRoadmap.vue` — the `FeaturesRoadmap` page is `type: "roadmap"`, a built-in that mounts the lib's CnFeaturesAndRoadmapPage. That page reads the very same `features_roadmap_repo` / `_features` / `_disabled` initial-state keys, so the wrapper added nothing. It was never imported at all. - `components/settings/AgentCredentialsSettings.vue` — never imported by anything, so never bundled. Both surfaces it was written to provide are already live: `scope="personal"` in `App.vue`, `scope="organisation"` in `AdminRoot.vue`. `views/ComplianceDashboard.vue` is deliberately KEPT despite being equally unmountable: it is the only caller of `getComplianceExport()`, the compliance auditor's-pack export whose route `/api/compliance/export` is still registered. The `compliance-operations` widget that replaced the page ships the EU AI Act audit export, a different endpoint. Removing it would silently drop a user-facing capability, so the decision to re-home or retire it is left open and documented in `customComponents.js`. Evidence: production build before/after. `algorithm-register__heading` disappears from `hermiq-main.js` (-21128 bytes); `agent-credentials-settings` was absent both times; `features_roadmap_repo` is unchanged because it comes from the library, not this wrapper. Live markers `run-flow-dialog`, `ai-feature-register__heading` and `compliance-operations` are all still present. A deliberate break (keeping the import while the file is gone) fails the build with "Can't resolve ./views/AlgorithmRegister.vue", so the green build is a real signal.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 05:15 UTC
Download the full PDF report from the workflow artifacts.
…ere it goes (#159) * feat(flows)!: the card says what the node does, the line says only where it goes The canvas still read the PRE-inversion model after the documents had been migrated. Nothing errored. It looked for `type` on each EDGE, found none — which is exactly what a correctly migrated flow looks like — and rendered the words "No step type" onto all 16 lines of the Hydra sequencer, while the cards showed place names for places that no longer exist. The engine ran the flow perfectly the whole time, which is why nothing below the DOM ever went red. WHAT MOVED node card the STEP the node runs (its catalogue name), plus one line of the config it actually reads, so two object-reads are told apart without opening either. A node with no type is called out as a warning instead of drawn as an ordinary card — the engine refuses such a document. line its own title, and nothing else. The place a step used to arrive at became the line's title on migration, so the words authors wrote ("scoped", "Gates passed") are what show. An untitled line draws no chip rather than an empty one. PORTS Role is carried by the ABSENCE of a port — a start has no in-port, an exit has no out-port — so which end of the flow you are looking at survives greyscale and does not depend on telling two hues apart (WCAG 1.4.1). Colour stays as redundant encoding, never as the only carrier. A routing node exposes one NAMED out-port per branch, read from the keys the ENGINE reads (`config.rules[].output` plus `config.default`). `config.routes` is deliberately NOT honoured: it is the most common way to author the node wrong, and drawing ports for it would make a broken flow look correct. The registered type is `openregister.route`, not `...router`. Guessing the id from the class name (RouterNode) yields a type no flow uses, which would have meant branch ports silently never rendered — caught by reading the stored document rather than the class list. A loop's body hangs off the TOP as a visible sub-list, kept clear of the left-to-right run of the main chain. SAVE WARNING AND LAST RUN The save response now carries OpenRegister's own connectivity verdict, so a dead end is reported at the moment of saving rather than on the next run — which for a scheduled flow could be hours later and would present as "it ran and did nothing". The dialog says the save SUCCEEDED first: an author who reads "cannot finish" and assumes rejection will redo the work. The flow list gains Last run and Status. Neither could be answered there before: a flow refused for a dead end produces no run at all, so "refused" and "nobody has triggered it" looked identical. E2E `flow-builder-dialect.spec.ts` was already failing on current data — it asserts 17 places and 19 lines, which is the pre-inversion shape of a document that now has 16 nodes and 16 connections. Rewritten around the inverted model, with the fixture's numbers MEASURED from the stored document rather than assumed: one start (`scope`), three sinks (`release` via `exit: true`; `stop-idle` and `stop-full` via the terminal type), three routes with two branches each. Both ways of ending a path are therefore covered by real nodes. The test that asserted "configures the step, not the place" is dropped: it pinned the old model, where a node carried no configuration. * i18n(flows): the strings the port canvas and the dead-end dialog added Fifteen new source strings, extracted with the repo's own writer so the key is the English source, plus Dutch for all of them — this is a Dutch government-facing product and an untranslated warning dialog is exactly the surface where that shows. `%n entry` / `%n entries` are a plural pair (the config summary counts array entries), so both forms are present rather than one string with a number interpolated into it. * docs(spec): the canonical flow-canvas spec, and @SPEC on the methods that implement it gate-16 flagged 15 changed methods with no @SPEC. There was nowhere correct to point them: hermiq had no canonical spec for the canvas, and a @SPEC must target openspec/specs/, never a change dir — a change dir is archived and the link rots. So the spec is written where it belongs, covering the surface this PR builds: the card names the step, the line carries only its title, role is expressed by the ABSENCE of a port, a routing node names each branch, the save warns without refusing, and the list distinguishes 'never run' from 'refused'. The e2e spec's own @SPEC is repointed from the change dir to the canonical path for the same reason. * fix(gates): scenario-level @e2e traceability, and regenerate features.json gate-19 reads SCENARIOS, not requirements. The @e2e pointers were on the requirement headings, where the gate never looks, so all four covered scenarios counted as untraced. Moved to where the gate reads them: - the four scenarios the browser really asserts are referenced from the e2e file itself as '// @e2e flow-canvas::<slug>', which is the direction the chain is meant to run — the TEST claims the scenario, so a deleted test breaks the link instead of silently leaving a spec pointing at nothing - the two that cannot be asserted in a browser carry a reason-bearing '@e2e exclude' on the scenario line, naming what covers them instead (OpenRegister's FlowDeadEndTest and Newman) features.json was stale because the pre-commit hook could not fetch the extractor (offline), which it warned about and I did not act on. Regenerated with the shared script. * ci(quality): move hydra-gates-ref v1.3.0 -> v1.5.0, the pin the floating workflow needs The Hydra Gates job failed with a message that says outright it is not about this repository: hydra-gates-ref 'v1.3.0' does not contain: scripts/axe-run.cjs scripts/lib/check_spec_anchors.py scripts/lib/check_form_labels.py scripts/lib/check_license_triangle.py The reusable workflow floats on @main and calls those scripts BY PATH inside the PINNED package. So a pin older than the scripts cannot run the gates that implement them, and the job fails on the pin while reporting nothing about the code. A pinned ref is a silent expiry date on every upstream change. v1.5.0 is the first tag containing all four — verified by reading each path at that tag rather than assuming the newest tag has everything. This also un-blocks the two gates the previous commit fixed: gate-19 (e2e-coverage) and gate-16 (spec-coverage) were already satisfied, and were not what this run was failing on. * fix(gate-19): put the @e2e excludes on their own line, where the gate reads them Appending '@e2e exclude ...' to the '#### Scenario:' heading left both scenarios untraced — the gate expects the marker as its own line beneath the heading. The count went 4 -> 2 after the file-side references landed, and these are the remaining 2. Both are excluded as pure-backend API contracts, with the reason naming what covers them instead: the save warning and the refused-flow fields are produced by OpenRegister and asserted by FlowDeadEndTest and Newman. The browser only renders a verdict the response already made, so a Playwright test here would assert the rendering of a fixture, not the behaviour.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 06:23 UTC
Download the full PDF report from the workflow artifacts.
* chore(ci): move hydra-gates-ref v1.3.0 -> v1.4.0 A pinned `hydra-gates-ref` is a silent expiry date on every upstream fix: this repo cannot receive a gate-package change until this line moves. v1.4.0 is the latest tag and the first one that carries `hydra-gates/scripts/axe-run.cjs` (verified absent at v1.3.0), so it is also the first that has ConductionNL/.github#168 axe DOM scoping and ConductionNL/.github#165 gate-46 fix. `enable-axe` is deliberately NOT enabled in this commit. Ordering matters: the ref lands first, enabling axe is a separate decision. * chore(ci): stop pinning hydra-gates — track the package at @main Removes the `hydra-gates-ref` input from the `quality.yml` caller. The shared workflow already defaults it to `main`, and this repo consumes `quality.yml` itself at `@main`, so dropping the override makes both sides move together: a gate-package fix lands here without a commit here. A pin is a silent expiry date on every upstream fix, and we have paid for that twice already: - .github#159 — 22 repos sat on v1.0.1, which predated the gate fixes. 16 gates were dead fleet-wide and every single one reported PASS. A gate that never runs emits a tick identical to one that did, so nothing in any repo's history showed it. - .github#173 — the shared side flipped a default at @main while the package stayed pinned per caller. Old runners lacked the coverage accounting the new default assumed, so they went red on gates they had no subject matter for. Removing the pin closes both shapes at once. Rolling back is a revert on ConductionNL/.github main, which reaches the whole fleet in one commit; holding this one repo still is still possible by setting the input explicitly, with a reason. `enable-hydra-gates: true` is unchanged. `enable-axe` remains unset. The comment block that justified the pin is replaced with a short note saying why there is no pin.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ❌ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 08:43 UTC
Download the full PDF report from the workflow artifacts.
…ale copy (#163) `quality.yml` probes `scripts/coverage-guard.php --capabilities` for the word `against` before it will trust the ratchet, and errors out when the probe comes back empty: ##[error]scripts/coverage-guard.php predates merge-base comparison. Update it from ConductionNL/.github before enabling the ratchet. hermiq's copy was the pre-`--against` 59-line version, so the probe exited 2 with no output and `PHPUnit (PHP 8.3, NC stable33)` failed on every pull request — job 92563882183 of run 31085325766, and identically on the re-run, so it is not a flake. This is the floating-caller / stale-callee shape one layer up from the version pins: `quality.yml` is consumed `@main` and moved on, the per-repo script is a copy that did not. Every other repo adopted the new script (openbuild#137, pipelinq#716, scholiq#286, decidesk#415, portaliq#49, shillinq#448, hrmq#71); hermiq is the last one. The file is byte-identical to the copy on openbuild `development` and scholiq `development`, so there is one version of this script in the fleet again. Verified locally, both directions: --capabilities -> against/update-baseline/capabilities, exit 0 (the stale copy: no output, exit 2) 499/1000 vs base 500/1000 -> FAIL, exit 1 501/1000 vs base 500/1000 -> OK, exit 0 base with 0 statements -> exit 2, refuses to set the floor to 0% Nothing is silenced: no baseline entry, no continue-on-error, no re-pin.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 09:14 UTC
Download the full PDF report from the workflow artifacts.
| uses: ConductionNL/.github/.github/workflows/release-beta.yml@main | ||
| with: | ||
| app-name: hermiq | ||
| channel: dev | ||
| secrets: inherit |
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 11:27 UTC
Download the full PDF report from the workflow artifacts.
…ommand injection) (#165) quality / Security (composer) is red on every PR here as of today: Advisory ID: PKSA-rdkp-vv9z-mjkg CVE: CVE-2026-67434 — OS Command injection Affected versions: <3.13.6|>=4.0.0,<4.0.2 Reported at: 2026-08-05T23:53:11+00:00 The advisory was published YESTERDAY and roave/security-advisories installs as dev-latest each run, so the same lockfile was clean on 2026-08-05 and is vulnerable on 2026-08-06 with no commit in between. The last green run is evidence of when it ran, not that the lockfile is safe. composer.json's existing constraint already permits the fixed version, so this is a lockfile move only: 1 update, 0 installs, 0 removals. Verified the diff touches exactly two lines, both the version string, and no other file.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ❌ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ⏭️ | ||||
| Newman | ⏭️ | ||||
| Playwright | ⏭️ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 12:11 UTC
Download the full PDF report from the workflow artifacts.
#166) OS command injection in PHP_CodeSniffer, GHSA-hmqg-cxww-wqhq, reported 2026-08-05. Affected: <3.13.6 | >=4.0.0,<4.0.2. This repo was on 3.13.5. All 16 repos checked across the fleet are on 3.13.5 and equally affected. The advisory is live in the audit DB, so this repo's Security (composer) gate is failing until this lands. Verified - composer audit --locked: 'No security vulnerability advisories found' (was 1). - vendor/bin/phpcs --version -> 3.13.6. - composer phpcs: rc=0. - Positive control: a deliberately non-conforming file under lib/ made phpcs exit 2 with 13 findings, so the green above is a real pass and not a checker that no-ops. Probe removed; only composer.lock is modified.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-06 12:43 UTC
Download the full PDF report from the workflow artifacts.
…that made either visible (#162) * feat(hermiq): rail-based agent detail, fuller dashboard, and the CSS that made either visible The layout the manifest already described was never reaching the screen. Hermiq shipped no GridStack stylesheet: nc-vue imports it as a side-effect import from an aliased package and webpack tree-shakes those away, and it is absent from dist/nextcloud-vue.css too (0 occurrences of its base rules, while leaflet's 174 ARE there). GridStack >= 11 sets left/top INLINE but takes `position: absolute` on items and `position: relative` on the container from that sheet, so every widget fell into normal flow: correct widths, no horizontal placement, all 11 cells stacked in one column down the left third of a 1600px viewport. Measured before the fix: gs-x="3" carried `left: calc(3 * var(--gs-column-width))` resolving to 25% while `position` computed to `static`, and every item reported left=371px. procest (same renderer, gridstack 10) positioned correctly, which is what localised it to hermiq. The stylesheet is fixed IN THE LIBRARY, not here — @conduction/nextcloud-vue's src/css/index.css now @imports it so postcss-import folds it into the extracted sheet, covering both the published-dist and aliased-to-src resolution paths. An app-level import would duplicate those rules in every consumer. main.js carries only a note recording why it is deliberately absent. Layout, on top of that: - AgentDetail adopts procest CaseDetail's 8/4 rail — an 8-column main column (Configuration, Run operations, Memory) beside a persistent 4-column rail (the four KPI tiles stacked, Skills, Eval baseline), both closing exactly at row 16. 33 rows -> 28, zero unfilled cells, and zero widgets whose scrollHeight exceeds their cell (ADR-062), all re-measured in a browser. - Tool governance halves from gridHeight 14 to 8: its two surfaces are peers over one capability, so they are now tabbed rather than stacked, with a real tablist (roving tabindex, aria-selected/aria-controls, arrow-key nav). It draws its own heading and sets showTitle:false, because with the chrome title on that title is a SIBLING of the widget root inside a display:block grid item, so height:100% resolved against the full box and ignored it — a permanent 6px overflow no gridHeight could remove. - Dashboard closes its grid (the bottom row left 6 of 12 columns empty) and gains a runs-by-agent chart, an agents table, header actions and a KPI drilldown. 12 rows, zero unfilled cells. AnalyticsService enriches perAgent with `name`: the aggregate carried only agentId, and a chart labelled with UUIDs is unreadable. This instance has zero runs, so the chart was verified by a positive control — the endpoint was intercepted with a synthetic payload and it rendered 6 bars across 3 name-labelled categories. Also fixes a pre-existing 500: AgentsController::stats bound PHP's (string)false — the empty string — against a boolean column, so every call died with SQLSTATE[22P02] and the agent counters had never worked. Booleans are now normalised to 'true'/'false'. Verified against DB ground truth with a deliberately-inactive probe agent (18/17/1), since with 17/17 active a broken filter and a working one are indistinguishable. Security postures declared rather than inherited (hydra gates 5/7/9/30): - SettingsController::create, SetupController::saveConfig and ::runAction were admin-only by Nextcloud's default for an un-attributed method, which is silently lost the moment anyone adds #[NoAdminRequired]. Now #[AuthorizedAdminSetting(AdminSettings::class)]. runAction matters most: test-llm makes the server issue an outbound request, so unauthenticated it is an SSRF primitive. - MetricsController::index stays admin-authed per ADR-006 ("/api/metrics Prometheus text, admin auth" vs "/api/health JSON, public") and gains #[NoCSRFRequired], which is what a scraper actually needs. - Four endpoints taking no caller-supplied object id carry reason-bearing @no-admin-idor-exempt tags. create() was checked first: `owner` and `organisation` are in PROTECTED_KEYS and assigned server-side. Accessibility: scope="col" on every column header across four components, a visually-hidden label on a bare <th />, and prefers-reduced-motion fallbacks (WCAG 2.2 AA 2.3.3). The chat typing indicator is an infinite bounce — the pattern that triggers vestibular symptoms — and is replaced with a static opacity rather than removed, so "assistant is typing" survives the motion. The e2e login no longer waits for 'networkidle', which never settles on Nextcloud (ADR-074 rule 4): the wait always ran to its timeout and only worked because .catch() swallowed it — a disguised fixed delay, not a readiness signal. It now waits for the submit control to be present and enabled. * test(agents): pin the normalised filter, which stats() depends on testStatsUsesPaginatedTotals still matched `$query['active'] === true`, the contract from BEFORE countAgents() started normalising booleans. The branch sends the string 'true', so that arm never matched, the callback fell through to the inactive branch, and the assertion read: total 10, active 4, inactive 4 counts that do not add up — which is the tell that the mock, not the code, was wrong. The normalisation is the fix, not the defect. A bool bound as a query parameter casts to '1' for true and the EMPTY STRING for false, and Postgres rejects '' on a boolean column with SQLSTATE[22P02] — so stats() was a hard 500 on every call and the dashboard's agent counters showed nothing. The callback now asserts the value is a string rather than only matching one, so a regression to raw booleans fails here with a readable message instead of quietly returning the wrong count. * fix(gates): the four findings gate v1.5.0 surfaced — labels, spec anchors, exception translation, icons All four ran for the first time on this repo when hydra-gates-ref moved v1.3.0 -> v1.5.0, so this is newly-VISIBLE debt rather than newly-created. gate-40 form-label-association — two raw <textarea> elements in Chat.vue (the feedback box and the message composer) carried only a :placeholder. A placeholder is not an accessible name: it disappears the moment the user types, so anyone on a screen reader loses the field's description mid-entry (WCAG 3.3.2, 4.1.2). Both now carry an aria-label; the placeholder stays as the hint it always was. gate-46 spec-anchor-existence — three unresolved targets: - MetricsController pointed at openspec/changes/example-change/tasks.md, a directory that does not exist. Repointed to the canonical observability spec's REQ-OBS-001, which is exactly what the controller implements (Prometheus metrics, admin only). - AgentsController and its test used #task-4-1, which is a LIST ITEM, not a heading, so it never resolved. The file's own convention is number + kebab-cased heading (#1-port-the-chat-engine resolves today), so these become #4-mirror-the-routes. Also fixed HealthController's three example-change refs while here — same broken placeholder, and it is outside this diff only by accident of which files this PR touched. REQ-OBS-002 is the health endpoint. gate-49 controller-exception-translation — SettingsController::index/create/load had no try/catch and no @throws. An uncaught throwable there becomes a framework 500 with a stack trace: no use to the caller, and index() is #[NoAdminRequired], so it leaks internals to a non-admin. All three now translate to a JSON error and log why. This needed a logger on the constructor, so the hand-built controller in SettingsControllerTest gains a mock — checked for, rather than discovered by a red build. gate-55 detail-page-discipline — six widget icons named MDI glyphs the shared registry cannot render, so each drew the "?" fallback (ADR-062 rule 8). Mapped to registry entries by MEANING, not by nearest spelling: PackageVariantClosed -> Package, CogOutline -> Cog, ChartTimelineVariant -> Timeline ("Total runs recorded"), SchoolOutline -> School ("Learnings"), PlayCircleOutline -> RocketLaunch (the run-operations card — the registry has no play glyph). Verified by running check_detail_page_discipline.py locally: 6 findings -> 0, and the manifest still passes Ajv validation. * i18n: the accessible names added for gate-40 'Feedback details' and 'Message' are now the accessible names of the two chat textareas, so they are user-facing strings and need translating like any other. Dutch supplied — an aria-label left in English is exactly the string a Dutch screen-reader user hears. * fix(lint): drop src/Chat.vue — a stray duplicate my own 'git add -A' swept in src/Chat.vue is an older copy of src/views/Chat.vue. I had looked at it earlier, established it is NOT the file the app loads (src/registry.js imports ./views/Chat.vue) and is strictly worse — it lacks the data-testid the e2e suite selects on and the prefers-reduced-motion block — and decided to leave it untracked. Then I staged the gate fixes with 'git add -A src' and committed it anyway. Its relative imports are written for src/views/, so from src/ none of the seven resolve, and eslint failed with exactly those seven import/no-unresolved errors. Untracked again and added to .gitignore, so the same accident cannot repeat. The file stays on disk — it is untracked, so git could not give it back. * test(settings): exercise the three catch paths the coverage guard flagged The coverage guard failed the PR: 24 statements added, coverage 68.25% -> 68.20%. The added statements were the try/catch blocks from the gate-49 fix, and nothing exercised them. That is the right complaint. An untested catch block is indistinguishable from no catch at all — it is the 'a check that never runs looks exactly like one that passed' shape, and the whole point of the block is what happens on the path nobody normally takes. Each test now asserts the TRANSLATED response, not merely that the method survives: index() 500 + an 'error' key, and the internal exception message is NOT echoed back — that leak is what the translation exists to stop, and index() is #[NoAdminRequired] so it would reach a non-admin create() success:false, the shape the UI already branches on load() says nothing was changed, which is the one fact the admin who clicked reload needs * chore(gates): record the gate-49 opt-out where the gate actually reads it [hydra-gate-controller-exception-translation exclude] SettingsController::index/create/load each catch \Throwable, return a translated JSON error and log the cause; the gate matches only nine named domain exceptions and does not recognise \Throwable, which is strictly broader and covers all nine. Upstream: ConductionNL/.github#204. The same text is in the PR body, but a re-run did not pick it up there — the commit-message path is the one that reliably reaches the gate.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-07 09:50 UTC
Download the full PDF report from the workflow artifacts.
) The 32 floor was raised on the premise that nothing tested below it. That is false here: this repo's own CI runs stable31, and min-version is enforced at install time, so occ app:enable refuses on 31 and the e2e seed fails with "is not installed or enabled". The original reason for a 32 floor no longer holds either. It came from openregister implementing OCP\ContextChat\IContentProvider, an interface absent before NC 32. openregister#2372 removed every eager reference to that class, so it is only loaded inside interface_exists() guards and the header is never read on an older server. openregister#2380 restored its own 28 floor on that evidence.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ⏭️ | ||||
| phpcs | ⏭️ | ||||
| phpmd | ⏭️ | ||||
| psalm | ⏭️ | ||||
| phpstan | ⏭️ | ||||
| phpmetrics | ⏭️ | ||||
| eslint | ⏭️ | ||||
| stylelint | ⏭️ | ||||
| build | ⏭️ | ||||
| composer | ⏭️ | ⏭️ | |||
| npm | ⏭️ | ⏭️ | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-07 11:14 UTC
Download the full PDF report from the workflow artifacts.
Quality Report — ConductionNL/hermiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-specs | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 117/117 | |||
| npm | ✅ | ✅ 744/744 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ✅ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-07 11:30 UTC
Download the full PDF report from the workflow artifacts.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.