You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 13e85be
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec,plugin-auth): a demoStaff entry names its line manager, and os dev links it (#26763)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBCFW2vVVh9Bwz714cxX7PDq)_Fixes#26616
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: #26764
## 维护者速读
- 改了什么:`demoStaff` 条目新增可选键 `manager`(填同一列表里另一条目的 email);`os dev`
启动时在所有演示账号建好之后、种子数据之前写入 `sys_user.manager_id`。
- 为什么改:新起的 demo 里每个账号都没有直属上级,审批流的 `manager` 一级找不到人,只能手工去 Setup → Users →
Set Manager 补。
- 风险与回滚:只影响本地 `os dev` 启动且声明了 `manager` 的应用;已有上级(含手工改过的)一律不覆盖。回滚即 revert
本 PR,未声明 `manager` 的栈行为不变。
- 先看:`packages/plugins/plugin-auth/src/demo-staff.ts` 的
`linkDemoStaffManagers`;`packages/spec/src/stack.zod.ts` 的
`collectDemoStaffManagerErrors`;`packages/spec/src/system/demo-staff.zod.ts`
的 `manager`。
- 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run
src/demo-staff-provisioning.test.ts`,应见 11 个用例全绿(含 8、9 两个上级用例)。
- 需要你做的:无,席位落地。
## What changes
**Spec (`packages/spec/src/system/demo-staff.zod.ts`).** A `demoStaff`
entry gains one optional key, `manager`: the `email` of another entry of
the same `demoStaff` list, compared without regard to case. A value that
is not an address (the likely mistake: writing the manager's display
name) is refused at the entry's path with a message saying it takes
another entry's email. `reportsTo`, `managerEmail`, `managerId` (which
also catches `manager_id`, one probe) and `supervisor` are aliases that
point to `manager`.
**Why the email and not the name.** The address is the identity key on
every path that provisions an account: the provisioning finds each
entry's account by it (`findUserIdByEmail`), it is what makes a re-run
idempotent, and the admin bulk import resolves its `manager_id` column
by an identity key too, never a display name. A display name is a Tier-1
profile field the person can edit on their own profile, and it is the
seed's reference key, not the account's. The schema already checks the
address's shape, so a typo in it is caught at the entry before the stack
check runs.
**Stack check (`packages/spec/src/stack.zod.ts`,
`collectDemoStaffManagerErrors`, called from
`collectDemoStaffErrors`).** `defineStack` (and so `os validate` and
every load) refuses, with `STACK_CROSS_REFERENCE_INVALID` 422, each of:
- a `manager` that is the address of no entry;
- an entry that names itself (case-insensitive);
- managers that form a loop, reported once per loop with every member
and its `stack.demoStaff[i]` origin; an entry that only hangs from a
loop is not named in it.
None of these is skipped: each would leave a manager link that the boot
cannot write.
**Provisioning (`packages/plugins/plugin-auth/src/demo-staff.ts`, the
first reader).** `provisionDemoStaff` keeps its first pass unchanged and
adds a second one, `linkDemoStaffManagers`, after every entry has had
its turn and still on the same `kernel:ready`, so before the application
seed. For each entry with a `manager`:
- no account for the entry: skipped, because the first pass already
reported why it has none;
- no account for the manager: reported on `failed` under the
subordinate's address, naming whether the manager's entry was not
provisioned or is not an entry at all;
- the account's `manager_id` already equals the declared manager:
nothing written;
- the account already has a different manager: left as it is and listed
on the new `managersKept` (an `info` line on the boot log names them);
- no manager: written through `applyUserManagerLink`, the one derivation
behind `POST /api/v1/auth/admin/set-user-manager`. Its refusals (self,
cycle, depth, cross-organization, directory-owned) apply unchanged and
are reported with their `code` and `reason`; this module carries no copy
of them.
`DemoStaffOutcome` gains `managersLinked` and `managersKept`.
`auth-plugin.ts` adds both to the boot's summary lines.
**A re-run never overwrites a manager set by hand.** The declaration
wins only over absence. A manager that an operator cleared by hand reads
as no manager, so the next boot writes the declared one again; the
alternative (remembering which links the boot wrote) needs state outside
`sys_user` that nothing else reads, for the one case where an operator
wants a demo account to have no manager at all and also kept `manager`
in the stack.
**Reference docs.** `content/docs/references/system/demo-staff.mdx`
regenerated (`gen:docs`), `authorable-surface/system.json` gains
`system/DemoStaff:manager`.
## Why
A fresh demo's approval ladder starts at the requester's manager, and
`manager_id` was NULL on every demo account (measured in hotclm on
18.2.0: 11 of 11), so rung 1 reached nobody until someone set it by
hand. Only the platform can write `manager_id`, so the application could
not declare it.
## Tests
Runs through `scripts/pm/os-verify-lock.sh`. Source as of `8e1dde389`;
head `a516ef709` adds only the engine-double ledger line.
<details><summary>Spec: src/stack-demo-staff.test.ts (9 new cases) and
the full suite</summary>
- New `describe('manager — …')`: accepts a manager declared after the
entry, case-insensitively; accepts a three-link chain that reaches a
top; refuses a display name at `demoStaff.0.manager` with the message;
refuses `reportsTo` / `managerEmail` naming `manager`; refuses a manager
naming no entry (`STACK_CROSS_REFERENCE_INVALID` 422 + first sentence);
refuses self-reference (case-insensitive); refuses a two-entry loop
once, naming both; refuses a three-entry loop that a fourth entry hangs
from, naming only the loop.
- `pnpm --filter @objectstack/spec test` on `8e1dde389`'s parent tree:
`Test Files 1 failed | 730 passed (731)`, `Tests 1 failed | 21928 passed
| 1 todo`. The one failure was `src/shared/alias-integrity.test.ts`: my
`managerId` and `manager_id` aliases share the probe `managerid`. Fixed
by dropping `manager_id` (commit `8e1dde389`); re-run of
`alias-integrity.test.ts` + `stack-demo-staff.test.ts`: `Test Files 2
passed`, `Tests 42 passed`.
- `pnpm --filter @objectstack/spec typecheck`: exit 0.
</details>
<details><summary>plugin-auth: unit and real-boot pins, and the full
suite</summary>
- `src/demo-staff.test.ts`, new `describe('provisioning — each declared
manager is linked …')`, 6 cases on an engine double that records every
update: fresh (the manager declared after the entry is linked, one
`manager_id` update); re-run with the link in place (no update); re-run
with a manager set by hand (no update, `managersKept:
['counsel1@legal.example']`); re-run after a manager was cleared
(written again); the manager's entry not provisioned (reported on the
subordinate, exact message, no update); the set-manager write refusing
(`[PERMISSION_DENIED, idp_provisioned]`, no update).
- `src/demo-staff-provisioning.test.ts` (real `ObjectKernel`, ObjectQL
on in-memory SQLite, real `AuthPlugin` + `SecurityPlugin`), new cases 8
and 9: a fresh boot leaves `manager_id` set for both orders of
declaration (`managersLinked: 2`); the next boot keeps a manager
re-pointed by hand and re-writes a cleared one (`managersLinked: 1`,
`managersKept: ['para1@legal.example']`, no `warn`). `Test Files 1
passed`, `Tests 11 passed`.
- `pnpm --filter @objectstack/plugin-auth exec vitest run
--maxWorkers=2`: `Test Files 198 passed (198)`, `Tests 3788 passed | 10
skipped`.
- `pnpm --filter @objectstack/plugin-auth typecheck` (after building
plugin-auth's own dist, which `examples/basic-usage.ts` imports): exit
0.
</details>
<details><summary>Ablation (one, on the behaviour the card asks to
keep)</summary>
`node scripts/ablation-replace.mjs --file
packages/plugins/plugin-auth/src/demo-staff.ts --anchor " if (current
!== null) {" --replacement " if (current !== null && false) {" -- …
vitest run src/demo-staff.test.ts`: anchor x1 → x0, blob `5ed3b373be04`
→ `7c15b3750852`; `× a re-run leaves a manager set by hand as it is, and
says so`, `Tests 1 failed | 19 passed`; restored, blob == HEAD and `git
diff HEAD` empty. The test imports `./demo-staff.js` from `src`, so no
rebuild was needed for the mutation to reach it.
</details>
## Acceptance notes
- **No example app declares `demoStaff`.** `git grep -c demoStaff --
examples` answers 0 files on `8e1dde389` (control: `git grep -l
"positions:" -- examples` answers 9), so no example is pinned. The
consumers are hotclm (objectstack-ai/hotclm#28 and its PR
objectstack-ai/hotclm#171, whose README keeps the manual Set Manager
step for this alone) and HotCRM's sales managers.
- **The manager resolves within the list one `defineStack` sees.**
`demoStaff` is refused inside `packages[].manifest`, so the list is the
stack's own top level. Under `composeStacks`, each input was already
`defineStack`-ed, so an entry naming a manager that only another
publisher declares is refused at its own stack. The card asks for the
same list, and nothing composes demo staff across publishers today.
- **The 20-link depth cap does not bound a declared chain.**
`MAX_MANAGER_CHAIN_DEPTH` lives in plugin-auth, outside `packages/spec`,
so the load-time check counts no links. At the boot the set-manager
write walks upward from the proposed manager only: declared top-first,
the link that crosses 20 is refused and reported on the subordinate;
declared subordinate-first, each link is written while the chain above
it is still short, and a longer chain lands.
- **Attribution.** At boot there is no request, so
`authSystemWriteContext()` answers `{ isSystem: true }` with no
`attributedUserId`; the write is a system write, as the account creation
itself is.
- **Skills-docs.** `skills/objectstack-platform/SKILL.md` (ceiling 5997
tokens) and `skills/objectstack-platform/evals/config-plugins-ops.json`
(ceiling 1469) sit at zero headroom in
`scripts/check-skills-token-ratchet.mjs`, and raising a ceiling is not
this card's call, so the teaching line (`manager` names another entry's
email; `os dev` links it and never overwrites one set by hand) and an
eval case are owed as a follow-up. The reference page is regenerated
here.
## Verification
<details><summary>Derived local gates: 120 of 120 run, all exit 0
(dispatch-gates --ran reconciled)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands` derived 112 families
on `8e1dde389`; run serially with no build in flight: 103 exit 0, 7 exit
3 (prerequisite: `packages/spec/dist` was older than the last source
edit, plus the two whole-repo built-output gates), 2 exit 1:
- `check:engine-double-contract`: `demo-staff.test.ts now pins 2 engine
double(s), ledger records 1` (coverage grew). Fixed with `--write`,
committed in `a516ef709`.
- `spec check:generated`: `api-surface/` stale against the stale dist.
After `pnpm --filter @objectstack/spec build`, it is green with no
regenerated file.
- After rebuilding spec, the nine non-green ones were re-run: all exit 0
(`check:engine-double-contract`, `spec check:generated`,
`check:api-surface`, `check:browser-reachable-entries`,
`check:dual-source-exports`, `check:entry-nameability`,
`check:exported-any`, `check:skill-examples`,
`check:dual-build-cjs-loads`).
- The ledger commit `a516ef709` added 8 families to the derivation
(`check:agent-test-spelling`, `check:bash32-floor`,
`check:cli-command-ids`, `check:entry-guard`, `check:parse-guard`,
`check:pnpm-filter-targets`, `check:pnpm-flag-spellings`,
`check:scripts-symbol-anchors`): all run, all exit 0.
- Final: `✓ dispatch-gates --ran: 120 derived famil(ies) accounted for —
120 run, 0 NOT-MEASURED`.
- Not run locally, declared to CI: `pnpm --filter @objectstack/spec
test:repo` (no test in that project changed) and the repo-wide `pnpm
lint`.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint on the seven changed source files
exits 0 (the repo-wide `pnpm lint` is CI's).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzehKYBxrrAG5Jid68s)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(spec,plugin-auth): a `demoStaff` entry names its line manager, and the development boot writes it
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: added
10
+
11
+
A `demoStaff` entry declares one optional key, `manager`: the `email` of another entry of the same `demoStaff` list, compared without regard to case. It is named by address because the address identifies an account everywhere the platform provisions one; a display name is a label a person can edit. Before, the entry was strict and refused the key, so a fresh demo's `manager` approval rung reached nobody until someone ran Setup → Users → Set Manager by hand.
12
+
13
+
`defineStack` (and so `os validate`) refuses, with `STACK_CROSS_REFERENCE_INVALID` 422, a `manager` that is the address of no entry, an entry that names itself, and managers that form a loop. A manager written as a display name is refused at the entry's path, saying it takes an address. `reportsTo`, `managerEmail`, `managerId` (and so `manager_id`) and `supervisor` are pointed to `manager`.
14
+
15
+
On an `os dev` boot, `@objectstack/plugin-auth` links each declared manager in a second pass, once every entry has its account and still before the application seed runs. Each link goes through the same checks as `POST /api/v1/auth/admin/set-user-manager`. A link is written only when the account has no manager, so a manager someone set by hand is left as it is on every later boot; a manager cleared by hand reads as no manager and is written again. A link that cannot be written (the manager's own entry was not provisioned, or the set-manager checks refuse it) is reported on the boot log under the subordinate's address. `DemoStaffOutcome` gains `managersLinked` and `managersKept`.
Copy file name to clipboardExpand all lines: content/docs/references/system/demo-staff.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,6 +32,7 @@ const result = DemoStaffSchema.parse(data);
32
32
|**email**|`string`| ✅ | Sign-in address of the demo account |
33
33
|**position**|`string`| optional | A position this application declares in `positions`, assigned to the demo account |
34
34
|**permissionSets**|`string[]`| optional | Permission sets this application declares in `permissions`, granted to the demo account |
35
+
|**manager**|`string`| optional | Line manager of the demo account: the email of another demoStaff entry, written as its manager_id by the development boot |
0 commit comments