Skip to content

feat(approvals): out-of-office auto-skip + self-service delegation (#1322)#3235

Merged
os-zhuang merged 2 commits into
mainfrom
claude/enterprise-delegation-ooo-proxy-1v3m8l
Jul 18, 2026
Merged

feat(approvals): out-of-office auto-skip + self-service delegation (#1322)#3235
os-zhuang merged 2 commits into
mainfrom
claude/enterprise-delegation-ooo-proxy-1v3m8l

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes the community-core part of #1322 — the "审批不冻结" continuity floor so approvals don't freeze while an approver is on leave. Enterprise governance (act-as inbox, SoD, admin-over-others, compliance reporting) is tracked separately in objectstack-ai/cloud#855.

What & why

An approval routes to a specific person, and people go on leave — freezing the flow and tanking SLAs. Position-routed leave was already covered by ADR-0091 (职务代理); the gap was individually-routed approvers (type: user / field / manager). This PR closes it at the natural resolution seam.

Changes

M1 — OOO auto-skip

  • New self-service object sys_approval_delegation ({delegator_id, delegate_id, valid_from, valid_until, reason}), modelled as its own object rather than a scalar on the better-auth-locked sys_user, mirroring the sys_user_position delegation precedent (ADR-0091).
  • ApprovalService.expandApprovers reroutes individually-routed approvers onto an active delegate at resolution time, reusing the shared isGrantActive half-open [from, until) predicate — never a background job (ADR-0049). Chain-following (A→B→C) is bounded (OOO_MAX_CHAIN) and cycle/self-reference safe. Group/graph approvers (position/team/department/tier) are deliberately left untouched.

M2 — self-service task delegation

  • The existing reassign service method + POST /approvals/requests/:id/reassign route already hand a single pending slot to another user with audit. Added approvals.reassign to the client SDK so a "delegate to X" button has a clean entry point. (Standing OOO delegation is CRUD on the new object via the generic data API — no bespoke endpoint.)

M4 — audit + notification

  • Each substitution hop is recorded as a new ooo_substitute audit action (A → B — reason) and notifies both the delegate and the skipped approver via the existing messaging surface.

Also: registered the action kind in the spec contract + object enum, a Setup nav entry for self-service, and annotated gap doc #20 (community part) per its maintenance rule.

Semantics note

expandApprovers snapshots concrete approver ids at request-open time, so a delegation that starts after a request opens does not retroactively apply (position/tier are re-evaluated per decision; individual routing is not). Called out in code + issue.

Tests

14 new unit tests: user/field/manager skip, valid_from/valid_until window boundaries (half-open), chain, cycle, self-delegation no-op, tenant scope (scoped vs null-org), the group-approver boundary, and audit+notify emission. Full suite green (114 in plugin-approvals, 6763 in spec); full monorepo build clean (71/71).

Scope boundary (→ objectstack-ai/cloud#855)

act-as / proxy inbox · delegation governance + SoD · org-wide admin-over-others console · compliance reports / attestation. Multi-step / parallel approval is #5 (P1), separate.

🤖 Generated with Claude Code


Generated by Claude Code

…1322)

Community-core continuity floor so approvals don't freeze when an approver
is on leave. Enterprise governance (act-as inbox, SoD, admin-over-others,
compliance reporting) is tracked separately in objectstack-ai/cloud#855.

M1 — OOO auto-skip:
- New self-service object `sys_approval_delegation`
  ({delegator_id, delegate_id, valid_from, valid_until, reason}), modelled
  as its own object (not a scalar on the better-auth-locked sys_user),
  mirroring the sys_user_position delegation precedent (ADR-0091).
- ApprovalService.expandApprovers reroutes INDIVIDUALLY-routed approvers
  (type: user / field / manager) onto an active delegate at resolution
  time, reusing the shared isGrantActive half-open window predicate — never
  a background job (ADR-0049). Chain-following is bounded and cycle/self
  safe. Group/graph approvers (position/team/department/tier) are left
  untouched; position-routed leave stays ADR-0091's job.

M2 — self-service task delegation:
- The existing reassign service method + REST route already hand a single
  pending slot to another user with audit. Exposed it on the client SDK
  (approvals.reassign) so a "delegate to X" button has a clean entry point.

M4 — audit + notification:
- Each substitution hop is recorded as a new `ooo_substitute` audit action
  ("A -> B -- reason") and notifies both the delegate and the skipped
  approver via the existing messaging surface.

Also: register the action kind in the spec contract + object enum, nav
entry for self-service, and 14 unit tests covering user/field/manager
skip, window boundaries, chain, cycle, self-delegation, tenant scope, and
the group-approver boundary. Gap doc #20 annotated (community part).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 18, 2026 4:53pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests size/m labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/plugin-approvals, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/client, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/client)
  • content/docs/api/environment-routing.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals, packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/client, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/client)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/client, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/client)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/client, @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/client)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/client, @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Add an "Out-of-office delegation" section to the approvals guide: how to
declare a sys_approval_delegation row, which approver types reroute
(individual: user/field/manager), the half-open resolution-time window,
chain/cycle behaviour, the ooo_substitute audit + notification, and the
group-approver boundary (position leave stays ADR-0091). Notes reassign
for single in-flight requests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA
@github-actions github-actions Bot added size/l and removed size/m labels Jul 18, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 18, 2026 16:23
@os-zhuang
os-zhuang merged commit 8b94a45 into main Jul 18, 2026
27 of 29 checks passed
@os-zhuang
os-zhuang deleted the claude/enterprise-delegation-ooo-proxy-1v3m8l branch July 18, 2026 20:44
os-zhuang added a commit that referenced this pull request Jul 19, 2026
Follow-up hardening on the merged OOO delegation feature (#3235).

Security — delegator forge guard:
sys_approval_delegation is apiEnabled CRUD, but as a system object it gets no
owner_id anchor and defaults to a `public` sharing model, so an unguarded
member could create a delegation naming SOMEONE ELSE as delegator and reroute
that victim's individually-routed approvals to themselves. bindDelegationWrite-
Guard (plugin-approvals beforeInsert/beforeUpdate, mirroring the ADR-0092
identity write-guard) forces a normal user's writes to name themselves as
delegator: system context bypasses, admins (roles include 'admin') may set any
delegator, everyone else is stamped-to-self on insert and rejected on a foreign
delegator. Row ownership on update/delete is already covered by member_default's
wildcard `created_by == current_user.id` RLS.

i18n:
Register sys_approval_delegation in the plugin's i18n extract config and add
its object/field/view translations. zh-CN fully translated; en/ja-JP/es-ES
carry the English baseline pending translation. (Blocks hand-added to avoid a
full re-extract dropping existing enum/view keys.)

Tests: 9 new guard cases (self-create, forge reject on insert/update, stamp on
omit, unauthenticated reject, system + admin bypass, batch). plugin-approvals
123 passed.


Claude-Session: https://claude.ai/code/session_016ypkQikZ55oWUHUnMebwXA

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants