Skip to content

Commit 00ac98d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17356-reachability-root-enumeration
2 parents c548dea + f3b41e8 commit 00ac98d

29 files changed

Lines changed: 2469 additions & 96 deletions
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): an app-declared capability token is not a platform system permission at the `everyone` anchor
6+
7+
`describeHighPrivilegeBits` counted **any** non-empty `systemPermissions` as a
8+
high-privilege bit, so a permission set carrying the capability token its own
9+
app declared could not be bound to the `everyone` audience anchor:
10+
11+
```
12+
FROM describeHighPrivilegeBits({ systemPermissions: ['clm_requester.access'] })
13+
-> 'system permissions' // the app's own navigation gate, refused
14+
TO describeHighPrivilegeBits({ systemPermissions: ['clm_requester.access'] },
15+
{ declaredCapabilities: ['clm_requester.access'] })
16+
-> null
17+
```
18+
19+
One list carries two unlike things: the platform's own powers (`manage_users`
20+
and friends) and a capability a package **declared for itself** (ADR-0066 D1,
21+
entering `sys_capability` with `managed_by: 'package'` + `package_id`
22+
provenance). An app whose navigation gates on its own token therefore could not
23+
ship the set every employee holds — the set's own gate made it unbindable — and
24+
authors were pushed toward declaring no gates at all, the opposite of what
25+
ADR-0066 D1 exists to encourage.
26+
27+
**The discriminator is provenance, not spelling.** Both predicates
28+
(`describeHighPrivilegeBits`, `describeAnchorForbiddenBits`) take a new optional
29+
`AnchorBindingContext` naming the capability names *this stack declared*; a
30+
token on that list is the app's own gate and is not counted. ⛔ A naming-syntax
31+
rule (dotted ⇒ app token) was considered and rejected: it misjudges in silence
32+
the first dotted platform permission — `setup.access` is one today — and the
33+
first undotted app token.
34+
35+
**What is still refused**, each pinned in `high-privilege.test.ts`:
36+
37+
- a platform capability name, **however it is declared** — a package declaring
38+
`manage_users` cannot launder it past the gate (the platform floor);
39+
- any token absent from the declared list, and every token when no list is
40+
passed — omission gets the pre-change verdict, so the narrowing fails closed;
41+
- a mixed set: one unexcused token still refuses the whole set;
42+
- the `guest` tier (ADR-0090 D9), which does not honour the excusal at all —
43+
D5 speaks for authenticated members, and anonymous visitors are not that.
44+
45+
**No shipped behaviour moves in this release.** Every current caller invokes the
46+
predicates with the old arity, and with no context the code path is identical —
47+
so this release widens the API, not any live anchor binding. The
48+
`@objectstack/plugin-security` boot refusal and the `@objectstack/lint`
49+
`security-anchor-high-privilege` rule pass the declared list in a follow-up, in
50+
the ruled order (protocol first).
51+
52+
ADR-0090 D5's offending-bit list is revised to match in its own governed PR
53+
(objectstack#17814), per the ruling's 「ADR-0090 修订单独受管 PR」: the offending
54+
bit is a `systemPermissions` entry naming a **platform** system permission.
55+
Both halves are phase ①; ⛔ neither lands without the other following.
56+
57+
**This is shipped, which is why it carries a changeset rather than
58+
`skip-changeset`.** `@objectstack/spec`'s published `files[]` ships `dist`, and
59+
the new code reaches it.
60+
61+
Counts below are taken on a **clean full build of this head** — an empty `dist`,
62+
then `pnpm --filter @objectstack/spec build` with both passes (JS and DTS): exit
63+
0, `check-dts-emitted` reporting 34/34 declaration files, and
64+
`dist/.build-input-hash` and `.build-input-hash-dts` both matching `src`. The
65+
build state is named because it changes the answer: on a JS-only `dist` — one
66+
still mid-DTS, or built under `OS_SKIP_DTS` — every declaration file is missing
67+
and each count below that reaches one is halved.
68+
69+
| identifier | built files | where |
70+
|---|---|---|
71+
| `declaredCapabilities` | **4** | `security/index.js`, `index.mjs`, `index.d.ts`, `index.d.mts` |
72+
| `AnchorBindingContext` | **2** | `index.d.ts`, `index.d.mts` — a type, so the declarations are its whole published reach |
73+
| `appDeclaredCapabilityNames` | **2** | `index.js`, `index.mjs` — module-private, so it has no declaration presence at all |
74+
| `describeHighPrivilegeBits` | **4** | the positive control: a symbol already known to ship |
75+
76+
Negative control: a sentence occurring **only** in the ADR revision — `As first
77+
written, the bullet above made` — occurs in **0** built files, and `docs/adr/**`
78+
is in no package's `files[]`. ⚠️ The control has to be a sentence the source
79+
does not also carry: `The platform floor is absolute` reads 2, not 0, because
80+
that sentence is in this predicate's JSDoc as well as in the ADR, and an emitted
81+
JSDoc reaches `index.d.ts` / `index.d.mts` like any other declaration text.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/platform-objects": patch
3+
"@objectstack/service-queue": patch
4+
---
5+
6+
`sys_job_queue`'s claim path no longer sorts the whole queue on every poll, and a job's due time is now a SQL predicate instead of a filter applied after `LIMIT` (#17612).
7+
8+
`DbQueueAdapter.claimBatch` — the 1s poll every `DbQueueAdapter` runs — read the queue as `WHERE queue = ? AND status = 'pending' ORDER BY priority ASC, scheduled_for ASC`, while `sys_job_queue` declared `['queue','status','scheduled_for']`. The sort's **first** key, `priority`, was in no declared index at all, so the equality prefix seeked and the planner then built a sorter over every pending row in the queue, every tick. Measured on both Turso faces:
9+
10+
```
11+
SEARCH sys_job_queue USING INDEX idx_sys_job_queue_queue_status_scheduled_for (queue=? AND status=?)
12+
USE TEMP B-TREE FOR ORDER BY
13+
```
14+
15+
- **The declared index becomes `['queue','status','priority','scheduled_for']`**, replacing `['queue','status','scheduled_for']` — the table still declares three. The full-queue sort is gone on both faces; what remains is a sorter bounded to rows tying on the whole indexed prefix, because a paged read carries one ORDER BY term the caller never writes — the unique tie-breaker of the deterministic-paging contract (ADR-0053 D-A1), here `id`. ⛔ That last term is deliberately **not** closed by appending `id` to the index: `id` is an unbounded `Field.text`, and a text column a declared index keys on without a `maxLength` makes MySQL reject the index DDL outright (`check:keyed-text-bounds`, ER_BLOB_KEY_WITHOUT_LENGTH).
16+
- **Due-ness moved into `where`** as `$or: [{ scheduled_for: null }, { scheduled_for: { $lte: now } }]`, the same shape `SqlOutboxStore.claim` uses. It had been a JS filter applied to rows `LIMIT` had already chosen, so a window full of not-yet-due high-priority jobs hid already-due work behind it indefinitely: at the default `batchSize: 10` (candidate window 30), 30 future-dated `priority: 1` rows plus one due `priority: 100` row claimed **0** per poll, forever. It now claims 1.
17+
- **`priority` still decides claim order.** The alternative — dropping it from the sort — would have left a declared, documented field (`Lower = higher priority`) with no runtime effect at all.
18+
- ⚠️ **On an existing database the superseded index is not dropped.** The retrofit adds `idx_sys_job_queue_queue_status_priority_scheduled_for` and leaves `idx_sys_job_queue_queue_status_scheduled_for` in place (measured: 3 indexes before, 4 after, no row touched), so a provisioned table carries one redundant index until an operator drops it through the migrate-plan path. A freshly created table gets three.

‎.changeset/plain-donkeys-repeat.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@
44

55
Declare the ASSEMBLED manifest stage on the installed-package read API.
66

7+
**BREAKING** — a TYPE-level break on two PUBLISHED response types. It ships
8+
`minor` under the pre-GA launch-window convention (ADR-0087, *Ratified: the
9+
pre-launch launch-window exemption*), where the npm level is deliberately not the
10+
carrier of breaking-ness; this banner and the ADR-0087 disposition at the bottom
11+
are. Runtime is untouched and stays additive — every payload that parsed before
12+
still parses — so the affected party is a TypeScript consumer and the channel is
13+
the compiler at their own call site. Reading a manifest field off
14+
`ListInstalledPackagesResponseSchema` or `GetInstalledPackageResponseSchema` can
15+
stop compiling, and assigning a malformed manifest to either can start compiling
16+
where the old annotation refused it. Both directions are measured against the
17+
built `.d.ts` under *The STATIC gain is one-sided* below, which is also where the
18+
point-of-use reading lives.
19+
720
`GET /api/v1/packages` and `GET /api/v1/packages/:packageId` serve whatever a
821
package was installed with, and two stages reach that table through declared
922
doors: `POST /api/v1/packages` installs an authoring manifest (`manifest.objects`
@@ -54,3 +67,5 @@ before `parse` — the unix-socket short-circuit and the refusal of the
5467
filesystem-reading `?sslcert=` / `?sslkey=` / `?sslrootcert=` query parameters
5568
are kept; the "is this a URL `pg` can open" arm answers "no findings". Datasource
5669
publish is a server-side act, so that arm never legitimately ran in a browser.
70+
71+
<!-- adr-0087: not-required (no-migration-prescription) nothing is removed or renamed: `ManifestSchema` is unchanged, no authorable key moves and no stored row shape moves, so `objectstack migrate meta` has nothing to rewrite and the ledger cannot carry this change. The break is confined to the declared TypeScript surface of two response schemas, where the compiler reaches every affected consumer. -->
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
---
4+
5+
Flow `end` nodes honour `outcome: 'refused'` — a terminal `refused` run, distinct from `failed`
6+
7+
`packages/spec` has declared the shape since 17.4.0: an `end` node accepts
8+
`outcome: 'completed' | 'refused'`, a `refused` end requires a `message`,
9+
`ExecutionStatus` carries `refused`, and `ExecutionLog` / `AutomationResult` /
10+
the trigger response carry `refusalMessage`. The engine produced none of it —
11+
it returned on every `end` node without reading its config — so an author who
12+
wrote a refusal shipped a plain completion: the run recorded `completed`, the
13+
caller got the flow's `successMessage`, and the authored reason reached nobody.
14+
15+
The `end` node now honours it:
16+
17+
- **The run terminates `refused`.** A refusal is a *successful evaluation that
18+
says no*, so the result is `success: true, status: 'refused'` with no `error`
19+
and no `errorMessage` — and, deliberately, no `successMessage`: the flow's
20+
completion toast is for a completion. All three terminal producers answer
21+
identically (a triggered run, a resumed screen flow, and an attempt under
22+
`errorHandling.strategy: 'retry'`, where a refusal also stops the ladder
23+
rather than consuming retry budget).
24+
- **The `message` is rendered per record**, through the same interpolation a
25+
`screen` node's `description` gets — one implementation (`interpolateText`),
26+
never a second template engine — so `'Refused: {record.name} is a confirmed
27+
duplicate'` reaches the caller naming the record.
28+
- **Both are persisted on the run.** `sys_automation_run.status` gains
29+
`refused` and a new `refusal_message` column carries the rendered text; the
30+
refusal is never folded into `error`, which would tell every reader the run
31+
broke. `RunRecord` gains `refusalMessage` and `TerminalRunStatus` gains
32+
`refused`, so history rows are written, aged and read back like any other
33+
terminal.
34+
- **A refused run is never resumed.** It writes no continuation, so `resume`
35+
answers `RUN_NOT_FOUND`.
36+
37+
Untouched on purpose: a paused run still returns `silent` with no
38+
`successMessage`, and a plain `end` — or one declaring `outcome: 'completed'` —
39+
completes exactly as before.
40+
41+
An `end` declaring `outcome: 'refused'` **inside a structured region** (a `loop`
42+
body, a `try`/`catch` region) is refused loudly rather than honoured: a refusal
43+
terminates the run and a region body cannot end one. Previously such a node was
44+
a silent no-op like every other `end` in a region, so nothing that ever worked
45+
stops working — put the refusing `end` on the top-level graph and route the
46+
region's exit to it.

‎.claude/skills/pm-dispatch/references/landing-operations.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,17 @@
2424
- 契约复核 PASS 落地的 PR 到窗口时已 ready 且 auto-merge 在挂,见 `contract-review.md`。
2525
- 窗口自身权责不变:跟到 MERGED、踢出处置、落地后对账。
2626
- 转 ready 或挂 auto-merge 前先判受管面:`node scripts/pm/check-governed-merges.mjs --test` 加变更路径。
27-
- 一条命中 ⇒ 整 PR 改走人工合并道:复核照写 + 留 draft + 向授权批准人请审 + 状态评论。
28-
- ⛔ 不翻 ready、不入队;清标即落地同受此闸,漏判会被队列守卫在 merge group 里拒收。
27+
- 受管路径全在本技能 `references/` 者事实层:席内达档复核过落地前检三条即转正式入队。
28+
- 其余为规则层:四件套留 draft 等人批,⛔ 不翻正式不入队;获授权批准后认领席落地。
29+
- ⛔ 两层不由席位批准;清标即落地同受此闸,漏判会被队列守卫在 merge group 里拒收。
2930
- 再读 `mergeable_state`:`dirty` ⇒ 先 merge `origin/main` 再挂;生成物在面上按 A 的固定序。
3031
- ready + 全绿 ≠ 已入队:队列从不主动拉 PR,入队是显式动作。
3132
- 零 `enqueued` 事件按序查三条:① `mergeable_state` 是否 `dirty`。
3233
- ② enable-auto-merge 调用根本没落地,重发与效果验证序列见 `platform-readings.md`。
3334
- ③ PR 碰 `.github/workflows/**` 而 token 缺 workflows 权限。
34-
- 确认 MERGED 的同一动作里给 `Part of` 卡收口,`Fixes` 卡代关但标也须摘。
35+
- 确认 MERGED 同一动作里给 `Part of` 卡收口、`Fixes` 卡代关但标也须摘,⛔ 不拆到下轮巡检。
3536
- `Part of` 卡开着不摘 `pm:dispatched`,无在飞物的卡就永远算进 `label:pm:dispatched is:open`。
3637
- 摘标换回 `pm:queue` 或按剩余物定级,加一条写清交付了什么、剩下归谁的评论。
37-
- 摘标与 MERGED 确认是一个动作,⛔ 不拆到下轮巡检。
3838
- 同刻读相关卡 `closed_by_pull_requests`,确认没有卡被正文闭合关键词误关。
3939
- 每次合并后重拉一次车道盘点与预期状态对账,⛔ 不留给下轮巡检。
4040
- 取 open 卡清单对照预期 diff:预期之外从 open 消失的,就是被闭合关键词静默误关的卡。

‎.claude/skills/pm-dispatch/references/lanes/ui.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
- 构建产物经 `pnpm objectui:refresh` 回流,见 SKILL.md 〈多仓协调〉。
2121
- 回流后的 console pin bump 是 objectstack 的单张卡,⛔ 永不做本仓 PR 的 rider。
22-
- 凡触 `packages/spec` 一律转 `domain:spec` 座位,见 SKILL.md 〈多仓协调〉。
22+
- 新 `packages/spec` 工作恒归 `domain:spec` 席,已派发卡 ⛔ 不因此转席,见 SKILL.md 〈多仓协调〉。
2323
- `scripts/pm/**` 单写手恒在 objectstack 侧 ⇒ 本席的工具需求走上游卡带 `Blocked-by:` 回链。
2424
- 受管面五项含仓根发布 `skills/**`,清单、判据与禁令见 objectui AGENTS.md §9 受管面段。
2525
- ⇒ 命中即停 draft;⛔ 未获授权批准不 ready 不入队不自合、永不批准,获批后认领席落地。

0 commit comments

Comments
 (0)