Repository navigation
Commit 35fc4ac
Fixes #18335
Clause-②: no
⛔ **Governed surface — this PR parks as a draft by design.**
`docs/adr/**` is Tier H (Prime Directive #14). No ready-flip, no
enqueue, no auto-merge, and no approval by any agent seat. An authorized
human approval is owed before this lands; a draft with the work done is
the complete deliverable.
## What this lands
ADR-0090 D10 rule 4 read 「every write records `performed_by` (agent) +
`on_behalf_of` (user) + run id」. No door has ever read it that widely,
and the gap had never been written down. Per the ruling on the card
(comment 5690859150, batch #139 item 1, letter **A**, maintainer 「同意」
2026-09-16, reaffirmed at 5731818788), this PR closes it documentarily:
1. **Rule 4 is narrowed** to *every write by an agent principal*, and it
now says what an agent principal IS — a caller a door resolves to
`principalKind: 'agent'`, today the OAuth / MCP client door alone — so a
later reader can classify a new caller type without re-litigating. It
also states the positive reading of the absence: a missing
`performed_by` is the record that the principal acted for itself.
2. **A dated note** at the end of D10 (`Note (2026-09-16, #18335)`)
records that an API key is its owner's **credential**, not an agent
principal, and *why* — a credential is how a principal acted, never a
second who — plus what was refused (API keys as a principal category of
their own) and on what basis.
Documentary only. **No code**: one file, +47 / -2.
## The ruling's premise, measured rather than inherited
The ruling asserts that API-key writes audit as the owner today. A note
that misdescribed the enforcement would recreate the very defect this
card closes, so the assertion was measured first. All readings against
`origin/main` at base `e3b3cdd`, taken 2026-09-20T11:05–11:15Z.
| reading | result |
|---|---|
| the one seam that produces an agent principal |
`packages/core/src/security/assemble-execution-context.ts#entryFields` —
`const agent = !anonymous && oauth?.clientId ? oauth : undefined`,
consumed by `principalKind`, `onBehalfOf` and `performedBy` |
| `principalKind` / `onBehalfOf` / `performedBy` in
`packages/core/src/security/api-key.ts` | **0 / 0 / 0** — lit control:
`userId` reads **6** in the same file |
| the same three in
`packages/core/src/security/resolve-authz-context.ts` | **0 / 0 / 0** —
lit control: `userId` reads **48** in the same file |
| the same three in `packages/runtime/src/security/api-key.ts` | **0 / 0
/ 0** (a 25-line re-export module) |
| which doors honour an API key, and what each hands the assembler |
**all three** that run `resolve-authz-context.ts#resolveAuthzContext`.
REST and MCP **stdio** pass `oauth: undefined` **by construction**
(`rest-server.ts`, `mcp/src/plugin.ts#resolveStdioExecutionContext`);
the runtime / MCP **HTTP** dispatcher excludes keys with a **guard**,
`resolve-execution-context.ts#extractJwtBearer`, refusing an
`osk_`-prefixed or non-JWT bearer |
| repo-wide non-test writers of `performedBy` | the MCP/OAuth seam, the
spec + hook declarations of the field, and the audit writer that reads
it. No API-key path |
⇒ an API-key caller falls through `agent ? 'agent' : anonymous ? 'guest'
: 'human'` to `human`, carries no `onBehalfOf` and no `performedBy`, and
its `sys_audit_log` row is the owner's own. **The measurement agrees
with the ruling's premise**, so the narrowing describes the enforcement
rather than changing it.
## Was there a dangling D6 sentence?
No. The clause 「explain (D6) reports both sides of the intersection」
lives *inside* rule 4, so the narrowing carries it. Measured on the ADR:
`attribution` occurs once in the whole file (rule 4) and `both sides`
once (its second line). The companion `docs/design/permission-model.md`
does not restate the rule at all — `performed_by` / `performedBy` /
`attribution` / `every write` read **0** there, against a lit control of
**14** for `agent`. The generated `content/docs/references/**` tables
carry the field's own `.describe()` text, which already says 「Set only
at the /mcp OAuth door … absent everywhere else」 — already narrow,
nothing to correct.
## Gates
Derived in this worktree from the real change set and reconciled with
the run log:
```
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack # 18 commands
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_FILE
Run reconciliation — 18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN.
EXIT CODES — all 18 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
```
All 18 exit 0, each captured to disk **before** any pipe.
`check-adr-links`, `check-adr-symbol-anchors` (+ both self-tests),
`check:adr-anchors`, `check:doc-authoring`, `check:nul-bytes`,
`check:pm-governed-merges`, `check:pm-prior-rulings`,
`check:comment-mask-corpus`, `check:cross-package-test-inputs`,
`check:driver-memory-census`, `check:refd-timer-probe`,
`check:watch-hint-literal`, `check:ci-filter-parity`,
`check:closing-keyword-parity` (+ self-test) are among them.
`check:doc-formula-expressions` first exited **3** — `PREREQUISITE NOT
MET`, the gate's own "nothing was measured" code — and was re-run to
exit 0 after `turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint`, taken under the shared verify lock.
The three new symbol anchors in the note (`#entryFields`,
`#resolveApiKeyAdmission`, `#resolveAuthzContext`) resolve as
`declaration` class; no line-number anchor was introduced.
## 维护者速读(草稿)
**改了什么。** 只动一份决策记录(ADR-0090),一个文件,47 行增、2
行删。一句原本写着「**每一次**写入都要记下『谁代谁干的』」的规则,被收窄成「**代理主体**的每一次写入」;同一节末尾新增一段带日期的说明,写明
**API key 不在这条规则里,以及为什么**。⛔ 没有改任何代码,平台行为一行都没变。
**为什么改。** 平台里只有一个地方会把调用方判成「AI 代理」:MCP 的 OAuth 门。**三个门都认 API
key**,但没有一个会把它变成代理:REST 与 MCP stdio 结构上就不传 OAuth 凭据,MCP HTTP
门则靠一道两行的**守卫**挡住 `osk_` 开头的
bearer。三条路都只认出**钥匙的主人**,所以审计日志记的就是主人本人——这一点本轮在源码上实测过,与裁定的前提一致。于是规则写的是「每一次」,实现做的是「只有代理那一次」,这就是**声明面与执行面对不上**。维护者已裁
A(API key 是主人的**凭证**,不是第二个「谁」),裁定里同时明写:只维持现状而不改那句话,等于新留一条「说的和做的不一致」。这份
PR 就是把那句话改对,并把理由记在案。
**风险与代价(含回滚)。** 风险低:纯文档,无运行时、无 API、无数据结构变化,不发布任何 npm
包。真正的代价是**语义上的**:这条规则从此明确**不**覆盖 API
key。将来若出现合规要求「要分得清是人按的还是钥匙跑的」,那是一张新卡、新决策,而不是重读这一条——这一点也写进了 Note 里。回滚是一次
`git revert`,零迁移、零数据影响。
**席位意见。**
**你要做的(一个动作)。** 读一遍 D10 rule 4 那六行和它后面那段带日期的
Note,回「同意」或指出要改的措辞。这是受管面(`docs/adr/**`,Tier H),在你点头之前它会一直停在 draft。
## Acceptance notes
- **`skip-changeset` is owed and was deliberately NOT applied.** This
diff publishes nothing (`docs/adr/**`), so `Check Changeset` needs that
label; the dispatch forbids this executor from touching any label on a
governed-surface PR, so the label is left to the seat. Until it is
applied, `Check Changeset` is expected red and that red is not a finding
about this diff.
- **Noted, not filed — D10's 2026-07 Status blockquote still lists 「the
agent audit-provenance gap」 as an open follow-up.** That gap is the one
#17022 covered, and that card is `closed`/`completed`
(2026-09-16T06:41Z). This is a stale pointer in prose, not a
reproducible defect, not a contract violation and not a
metadata-authoring trap, so it is not one of the three filing classes.
Carrier: **#18374**, which already owns the residual close-out in the
same D10 area.
- **Seat assumption 2 re-measured wider than dispatched.** The dispatch
named four PRs; all **36** open PRs in the repo were read at
2026-09-20T11:10Z (`GET /pulls/{n}/files`). Exactly one touches
`docs/adr/**` — #18985, on ADR-0089 and ADR-0137, disjoint files. Zero
open PRs hold ADR-0090. One caveat recorded rather than hidden: PR
#17076 (`chore: version packages`) has more than 200 files; pages 1 and
2 were read (200 files, zero `docs/adr/` hits) and the tail was not
enumerated.
- **A small line drift in the dispatch's measurement table, reported as
instructed.** The card body cites the agent channel at `:293` consumed
at `:316`/`:317`; on `e3b3cdd` those are `:294`, `:317`, `:318` — the
file gained the `performedBy` line from #18371. The rule 4 sentence
itself was anchored on its text and matched verbatim, at line 391 as the
seat read it.
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5cdb0db commit 35fc4ac
1 file changed
Lines changed: 47 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
388 | 388 | | |
389 | 389 | | |
390 | 390 | | |
391 | | - | |
392 | | - | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
393 | 397 | | |
394 | 398 | | |
395 | 399 | | |
| |||
398 | 402 | | |
399 | 403 | | |
400 | 404 | | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
401 | 446 | | |
402 | 447 | | |
403 | 448 | | |
| |||
0 commit comments