Skip to content

docs(approval-gate): mark role/quorum fields as advisory; runtime warns when set#34

Merged
gitsad merged 2 commits into
MobileReality:fix/approval-gatesfrom
aziolek:docs/approval-gate-advisory-fields
May 28, 2026
Merged

docs(approval-gate): mark role/quorum fields as advisory; runtime warns when set#34
gitsad merged 2 commits into
MobileReality:fix/approval-gatesfrom
aziolek:docs/approval-gate-advisory-fields

Conversation

@aziolek
Copy link
Copy Markdown

@aziolek aziolek commented May 25, 2026

Summary

Aligns the approval-gate documentation and schema descriptions with what the runtime actually enforces. Adds a console.warn (once per gate at store init) when a document declares allowedRoles, requiredApprovers > 1, or requireReason: true, pointing embedders at the security-model section of the component catalog.

No behaviour change beyond the new warning.

Motivation

The current docs describe the approval-gate's allowedRoles / requiredApprovers / requireReason fields as enforced controls. Reading the runtime, they are not:

  1. packages/renderer-react/src/components/ApprovalGateRenderer.tsx:30-49 dispatches APPROVAL_GRANTED with a hardcoded actor: { id: 'current-user' } straight to store.dispatch, bypassing approvalGateHandler.onAction. The handler's ctx.policy.enforce('approval_grant') is therefore unreachable from the UI.
  2. packages/runtime/src/core/document-store.ts:194-201 handles APPROVAL_GRANTED by setting status: 'approved' and writing the actor into bindings, without invoking policy.enforce, without consulting allowedRoles / requiredApprovers / requireReason, and without validating the actor.
  3. packages/runtime/src/core/document-store.ts:20 accepts options.registry?: AttachableRegistry but the function body never reads it (compare options.sessionId, options.documentId, options.environment, options.policy, all of which are read).
  4. ButtonRenderer, FormRenderer, and WebhookRenderer do not read componentState.disabled / componentState.visible, so even a disabled: "{{gate.status}}" binding wouldn't prevent a downstream action from firing.

packages/runtime/tests/document-store.test.ts:150 codifies the current behaviour as the desired contract — a direct store.dispatch({ type: 'APPROVAL_GRANTED', componentId: 'gate1', actor: { id: 'user-1', role: 'manager' } }) flips status to 'approved' against a gate that has no role/quorum constraints, with no assertion that any policy check ran.

There are two coherent ways to address this. This PR is the smaller of the two — make the docs honest. The larger alternative is to add a host-identity API and wire approvalGateHandler.onAction into dispatch; that requires an API surface decision (where verified identity comes from) that this PR doesn't preempt.

Changes

  1. packages/spec/src/schemas/components/approval-gate.ts — add .describe() text on requiredApprovers, allowedRoles, requireReason marking them advisory.
  2. packages/runtime/src/core/document-store.ts — add warnIfAdvisoryApprovalFields(comp) helper, called from the init loop and from updateAst's new-component branch. Emits console.warn once per gate the first time it enters the store.
  3. docs/reference/component-catalog.md — drop "Supports role-based access control" from the section opener; add an "Important — security model" callout above the property table explaining the trust boundary; update the field table to mark the three fields advisory; rewrite the example description to drop the "SOX compliance requirement" framing.
  4. docs/guides/creating-blueprints.md — change two "Approval gate enforces … role" checklist items to "declares" with a note that the host application enforces.
  5. docs/guides/creating-documents.md — add advisory callouts after the two approval-gate examples (the manager-gate visibility example and the compliance-approval example).
  6. docs/guides/enterprise-features.md — annotate the "Putting It All Together" example: the // 3. Create store with policy enforcement comment now notes that policy enforcement currently applies to the webhook (webhook_call) only, and points at the approval-gate security model.

Out of scope (happy to follow up)

  1. Wiring approval-gate-handler into document-store's dispatch path. Requires deciding how the host injects a verified actor identity (proposed shape: MdmaProvider currentUser={{id, role}} flowing into AttachableContext).
  2. Per-blueprint compliance claims in blueprints/change-management, blueprints/clinical-ops, blueprints/kyc-case, blueprints/incident-triage, blueprints/customer-escalation. Their manifests/READMEs name the approval-gate as the implementation of specific controls (SOX 404, clinical-staff restriction, etc.). Those edits probably warrant the maintainers' judgement.
  3. SECURITY.md's in-scope list, which currently classifies approval-gate bypass as a reportable vulnerability — that classification only fits a runtime that ships enforcement.
  4. Prompt-pack author guidance (packages/prompt-pack/src/prompts/mdma-author/_shared.ts) that tells the LLM these fields restrict approvers. Should track whatever (1) decides.

Disclosure note

SECURITY.md asks for private disclosure of approval-gate bypass via open-source@mobilereality.pl. This PR addresses only the documentation/schema-description side and intentionally does not disclose anything not already readable in the public source. If you'd prefer to handle the conversation about (1) — wiring real enforcement — privately first, I'm happy to email and treat (2)/(3)/(4) as follow-ups gated on that decision.

…ns when set

The approval-gate schema accepts `allowedRoles`, `requiredApprovers`, and
`requireReason`, and the documentation across `docs/reference/component-catalog.md`,
`docs/guides/creating-blueprints.md`, `docs/guides/creating-documents.md`, and
`docs/guides/enterprise-features.md` previously described these as enforced
controls ("Supports role-based access control", "Approval gate enforces
manager/director role", etc.). `SECURITY.md` classified approval-gate bypass
as a reportable vulnerability. The runtime does not, in fact, verify approver
identity, enforce role membership, or require multiple distinct approvers:

- `packages/renderer-react/src/components/ApprovalGateRenderer.tsx:30-49`
  dispatches `APPROVAL_GRANTED` with a hardcoded `actor: { id: 'current-user' }`
  straight to `store.dispatch`, bypassing `approvalGateHandler.onAction`. The
  handler's `ctx.policy.enforce('approval_grant')` is unreachable from the UI.
- `packages/runtime/src/core/document-store.ts:194-201` handles `APPROVAL_GRANTED`
  by setting `status: 'approved'` without invoking `policy.enforce` and without
  consulting `allowedRoles` / `requiredApprovers` / `requireReason`.
- `createDocumentStore` accepts `options.registry?: AttachableRegistry` but the
  function body never reads it.

This change aligns documentation, schema descriptions, and `SECURITY.md` with
shipped behaviour, and adds a runtime warning so embedders are signalled when
they declare advisory fields. No behaviour change beyond the new warning.

Schema (`packages/spec/src/schemas/components/approval-gate.ts`)
- `.describe()` text on `requiredApprovers`, `allowedRoles`, `requireReason`
  marks them advisory and points at the security-model callout. The describe
  for `requiredApprovers` makes explicit that the default value of `1` is
  equally advisory at the runtime layer.

Runtime (`packages/runtime/src/core/document-store.ts`)
- `warnIfAdvisoryApprovalFields(comp)` helper, called once per gate per store
  instance from the init loop and `updateAst`'s new-component branch.
- The warning lists which advisory fields the gate declares and states
  explicitly that ALL approval-gate role/quorum/reason fields are advisory at
  the runtime layer regardless of which one triggered the warning.
- JSDoc acknowledges the `console.warn` delivery limitation (suppressed in
  `vitest --silent`, SSR, structured-logger pipelines). A structured-logger
  integration is the natural follow-up but is intentionally deferred because
  `packages/runtime` takes no logger dependency today.

Tests (`packages/runtime/tests/document-store.test.ts`)
- Four new tests via `vi.spyOn(console, 'warn')` covering:
  (a) init with `allowedRoles` warns once,
  (b) `requiredApprovers > 1` + `requireReason: true` warns with both fields listed,
  (c) bare gate does NOT warn (regression guard),
  (d) gate added later via `updateAst` warns when it enters the store.

Docs
- `component-catalog.md`: dropped "Supports role-based access control" from
  the approval-gate opener. Added an "Important — security model" callout
  above the property table spelling out the trust boundary. Field table marks
  the three fields advisory. Rewrote the example description to drop the
  "SOX compliance requirement" framing.
- `creating-blueprints.md`: two "Approval gate enforces … role" checklist
  items changed to "declares … (host application enforces)".
- `creating-documents.md`: advisory callouts after both approval-gate examples.
- `enterprise-features.md`: the "Putting It All Together" code comment notes
  that runtime policy enforcement applies to `webhook_call` only and points
  at the approval-gate security model.

Security policy (`SECURITY.md`)
- Reworded the approval-gate-bypass bullet so it no longer classifies
  advisory-by-design behaviour as a reportable vulnerability. Reports that
  hinge on runtime-layer enforcement of `allowedRoles` / `requiredApprovers`
  / `requireReason` are closed not-a-bug.

Out of scope (intentionally — happy to follow up)
- Wiring `approval-gate-handler` into `document-store`'s dispatch path.
  Requires deciding how the host injects a verified actor identity
  (proposed shape: `MdmaProvider currentUser={{id, role}}` flowing into
  `AttachableContext`).
- Per-blueprint compliance claims in `blueprints/change-management`,
  `blueprints/clinical-ops`, `blueprints/kyc-case`, `blueprints/incident-triage`,
  `blueprints/customer-escalation`. Their manifests/READMEs name the
  approval-gate as the implementation of specific controls (SOX 404, clinical-
  staff restriction, etc.). Those edits warrant the maintainers' judgement.
- Prompt-pack author guidance (`packages/prompt-pack/src/prompts/mdma-author/_shared.ts`)
  telling the LLM these fields restrict approvers. Should track whatever the
  enforcement-wiring decision above decides.
- Refactoring advisory-fields checks into a `walkComponents(ast, visitor)`
  utility once more component types carry advisory metadata.
@aziolek aziolek force-pushed the docs/approval-gate-advisory-fields branch from 4442c1d to 69e71ff Compare May 25, 2026 20:56
@gitsad
Copy link
Copy Markdown
Member

gitsad commented May 27, 2026

@aziolek thanks for raising this. Yes, definitely you are right. MDMA's role is not to verify any app data that can be handled via approval-gate but for sure it should be passed through to be able to be handled by other functions. Looking forward to next PR or just let me know then I will provide necessary fixes related to this element.

@gitsad gitsad changed the base branch from main to fix/approval-gates May 27, 2026 15:51
@gitsad
Copy link
Copy Markdown
Member

gitsad commented May 27, 2026

changed target branch to fix/approval-gates

Copy link
Copy Markdown
Member

@gitsad gitsad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build failed in workflow

Biome lint/style/useTemplate flagged the multi-line string concatenation;
collapse to a single template literal to satisfy CI.
@aziolek
Copy link
Copy Markdown
Author

aziolek commented May 27, 2026

build failed in workflow

Should pass now.

@aziolek aziolek requested a review from gitsad May 27, 2026 19:29
@gitsad gitsad merged commit 9b9804c into MobileReality:fix/approval-gates May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants