From abbbc3cc867d19a36b5961dbb859bd6ba90c4472 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 02:43:17 +0000 Subject: [PATCH] skills(automation): the wall-clock unit on the escalation row, and a hook-side `runAs` paragraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two localized additions to the published automation skill. 1. The `escalation` row of the per-node table now states the unit with the declaration: `timeoutHours` is calendar (wall-clock) hours — nights, weekends and holidays count, and the platform ships no business-hours calendar. Mirrors the `ApprovalEscalationSchema.timeoutHours` describe, so the schema and the skill read the same. 2. The "Failure routing & `runAs`" section carried flow-side `runAs` only. It now also states the hook-side rule: a hook declares its own `runAs: 'system' | 'user' | 'inherit'` (default `'inherit'`, the context of the write that fired it), scoping that hook's `ctx.api` data operations only on both the in-process handler and the sandboxed body; a `'user'` hook with no trigger user has its data operations refused rather than run unscoped; `sudo` is not a hook key. Every fact is read off `HookSchema.runAs` in `packages/spec/src/data/hook.zod.ts`. No other file moves; `skills/objectstack-data/references/data-hooks.md` is deliberately untouched. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox --- skills/objectstack-automation/SKILL.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index e9db245919..c22393f0d0 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -253,6 +253,15 @@ run reports success. `objectstack validate` names the offending template. > organization predicate reads and writes across every tenant. The tenant column > is platform-injected — filter on it, never re-declare it per object. +> **A hook elevates itself with `runAs`, never with `sudo`.** An object hook +> (objectstack-data) declares its own `runAs: 'system' | 'user' | 'inherit'` — +> default `'inherit'`, the context of the write that fired it — scoping that +> hook's `ctx.api` data operations only, on the in-process `handler` and the +> sandboxed `body` alike. A `'user'` hook whose trigger resolved no user has +> nothing to scope to: its `ctx.api` data operations are refused +> (`HOOK_UNSCOPED_DATA_ACCESS`, 403) rather than run unscoped — declare +> `runAs: 'system'` when the elevation is intended. `sudo` is not a hook key. + ### Filter tokens (`config.filter`) The one slot where two `{…}` dialects meet, and the one whose failure **widens** @@ -717,7 +726,7 @@ Object-hook `ctx` is a different vocabulary — see **objectstack-data** | `approvalStatusField` | Business-object field to mirror `pending`/`approved`/`rejected`/`recalled` onto (should be readonly) | | `onEmptyApprovers` | What an EMPTY resolved slate does: `admin_rescue` (default — request opens, only a privileged admin can act via Reassign; never waves through, never kills the run), `fail` (node fails — treat an empty slate as a config bug), `auto_approve` (skip the request, continue down `approve` with `output.autoApproved = true` — opt-in because it silently waves the record through). Declare it explicitly on any node with an `expression` approver (linted) | | `decisionOutputs` | Decision outputs a decision may carry (author declares, approvers fill values). Entries are bare keys (free-text input) **or typed declarations** `{ key, label?, type: 'text'\|'user'\|'department'\|'position'\|'team', multiple?, required? }` — a typed entry renders the matching record picker in the decision dialog (`multiple` collects an id array). Accepted outputs resume the run as `.` variables; undeclared keys reject the decision; `decision`/`requestId` reserved | -| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }`. `escalateTo` is a **position machine name** (expanded to its holders via `sys_user_position`, ADR-0090 D3) or a specific user id — never a membership tier. `reassign` without `escalateTo` degrades to notify (linted) | +| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }`. `timeoutHours` is **calendar (wall-clock) hours** — nights, weekends and holidays count; the platform ships no business-hours calendar. `escalateTo` is a **position machine name** (expanded to its holders via `sys_user_position`, ADR-0090 D3) or a specific user id — never a membership tier. `reassign` without `escalateTo` degrades to notify (linted) | | `maxRevisions` | ADR-0044 — max **send-backs-for-revision** per run before auto-reject. Default `3`; `0` disables send-back. Only meaningful when the node has a `revise` out-edge | ### Branching, side-effects & rejection