Skip to content

fix(plugin-auth,plugin-audit): a SCIM-driven write names the SCIM connection that performed it, on its audit row - #30989

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-c66ai3
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-c66ai3

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

domain:server#5 · os-elon-musk · session_016WV4cyRYCwPvYJ68qTXzEP

Fixes #30960
Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:IdP 通过 SCIM 开通、停用、删除用户时,平台写的每条审计记录现在标明是哪个 SCIM 连接做的:actor 记为 svc:scim:连接ID,metadata 里带 scim_connection_id,user_id 仍为空(没有人在操作)。
  • 为什么改:以前这些记录的「谁做的」三栏全空,审计员查不出离职员工是被哪个 IdP 停用的;企业版 SCIM 离职流程(cloud#3667)和审计导出(cloud#3665)都要靠这个归属。
  • 风险与回滚:只在 SCIM 请求内生效,其他写入不变;bearer 和它的摘要永远进不了记录;两个连接并发时各记各的。这三点都有测试钉住。回滚即 revert。
  • 先看:① scim-connection-service.ts 的 ScimRequestScope.connectionId 和 recordScimRequestConnection(槽位为什么能到达每一次写入);② auth-actor-attribution.ts 中 authSystemWriteContext 开头的 SCIM 分支;③ plugin-audit audit-writers.ts 的 metadata 一段。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/scim-transaction-scope.test.ts,预期 9 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

A SCIM protocol request is performed by the identity provider's connection, which its bearer verifies as. Every sys_audit_log row such a request writes now names that connection.

@objectstack/plugin-auth

  1. The slot. scimRequestScope, the store handleRequest opens with run(...) around every /scim/v2/* request, is now typed ScimRequestScope and carries an optional connectionId. recordScimRequestConnection(verification) writes the verified connection's id into the CURRENT store (first verification wins; a no-op outside the scope and on a failed verification) and returns the verification unchanged. scimRequestConnectionId() reads it.
  2. The verifier records it. The verifyBearerToken callback in auth-manager-plugin-list.ts now returns recordScimRequestConnection(await verifyScimBearerToken(...)). verifyScimBearerToken itself is unchanged and still pure.
  3. Every write reads it. authSystemWriteContext(), the context builder the adapter's withSystemContext (and the membership reconciler) uses for every better-auth-owned write, returns { isSystem: true, actor: 'svc:scim:CONNECTION_ID' } when the request has a verified connection, and builds the context exactly as before otherwise. No attributedUserId, so user_id stays null. The deactivation's ban write (reconcileScimUserLifecycle) goes through the vendor-bound adapter, so it carries the same actor.
  4. scimServicePrincipal(connectionId) spells the label, SCIM_SERVICE_PRINCIPAL_PREFIX is svc:scim:.

@objectstack/plugin-audit

  1. New scim-connection-attribution.ts (SCIM_CONNECTION_ID_KEY, scimConnectionOf), in the shape of impersonation-attribution.ts. In audit-writers.ts, the CRUD ledger row's metadata gains scim_connection_id when the row's actor label starts with svc:scim:. The label is already recorded on actor by the existing service-principal fallback (userId ?? session.actor). Rows of every other actor are byte-identical.

Why

The card's measurement on a licensed objectos-ee serve: every SCIM-driven row had actor, user_id and metadata null. Reproduced here first, on main 256a4b72, through AuthManager.handleRequest with a real bearer, over better-sqlite3, with plugin-audit's real writer and sys_audit_log registered: a SCIM create, a deactivation, a second create and a delete wrote 21 rows, and all 21 had actor, user_id and metadata null. After the change, on this PR's head, the same sequence wrote 21 rows. 20 have actor: svc:scim:okta-30960, user_id: null and metadata: {"scim_connection_id":"okta-30960"}. The 21st is noted below. Neither the bearer nor its digest occurs in any row, before or after.

Tests

Two homes, the split the package boundary already draws. plugin-auth does not depend on plugin-audit, and auth-account-event-audit.test.ts records that the row shape is plugin-audit's to pin.

packages/plugins/plugin-auth/src/scim-transaction-scope.test.ts: a new top-level describe('[#30960] …'), in the shared harness of the SCIM request scope. Each case drives AuthManager.handleRequest() with a real bearer on a real ObjectQL over better-sqlite3, and records every engine insert / update / delete with the context it reached the RAW engine with, beneath withSystemContext, which is the context the audit hooks are built from.

  • (e) A create, a deactivation and a delete: each request's writes all carry isSystem: true and actor: 'svc:scim:okta-14522', and none carries attributedUserId, impersonatedBy or performedBy. The deactivation includes the sys_user ban write (banned set), and the delete includes the sys_scim_user delete and the sys_user ban write.
  • (f) Across all three, no write's data, query or context contains the bearer or its digest. The control: the capture holds at least 5 writes and does contain the label.
  • (g) A sign-up before a SCIM request and a sign-in after it carry no actor at all, and svc:scim: occurs nowhere in their writes.
  • (h) Isolation: a second connection (entra-30960) is minted. Both requests' credential probes are held at a barrier until BOTH have arrived, so both connections are verified before either request writes. The sys_user insert for each email carries its own request's connection. Every write the vendor stamps with a connection_id carries the actor of that same connection, and no write carries any other actor.

packages/plugins/plugin-audit/src/audit-writers.test.ts: a new top-level describe('audit writers — a SCIM connection is the actor of its writes (#30960)').

  • The create, the ban update and the delete with session.actor: 'svc:scim:okta-30960': actor is the label, user_id is null, metadata is exactly {"scim_connection_id":"okta-30960"}.
  • svc:cloud-control, svc:flow:mirror_status and a bare svc:scim: actor recorded, no metadata.
  • A user's write beside a SCIM label: the user is the actor and there is no SCIM key.

Ablations (node scripts/ablation-replace.mjs, anchor hit 1 → 0, run, restore proven blob == HEAD and git diff HEAD empty; plugin-auth's and plugin-audit's tests import the edited modules from src/, so no rebuild sits between the mutation and the run):

Leg Mutation Red
A1 the verifier does not record the connection (e) (f) (h)
A1b the verifier records with scimRequestScope.enterWith(...), the rejected design (e) (f) (h): the writes never see it
A2 the bearer recorded as the connection id (e) (f) (h)
A3 a process-wide slot instead of the per-request store (g) (h)
A5 the scim_connection_id spread deleted the three SCIM row cases
A6 any svc: label read as a SCIM connection the other-principal case

Two first attempts did not apply as written. A5's empty replacement was refused (the tool asks for --delete) and ran nothing. A3's held half was refused because its replacement contained its own anchor, so only the reading half landed and that run stayed green while testing nothing. Both were re-run with corrected anchors, and the table reports the re-runs. The ablations ran at 4cd757af, before the cookie case was dropped (see the acceptance notes); under A1, A1b and A2 that case also went red, and the cases the table names are the ones still in the file. A4 is in the acceptance notes.

Acceptance notes

  • Why the slot reaches every write. The verifier runs in a continuation the endpoint handler does not descend from: the enterWith note in scim-connection-service.ts, measured on 1.7.2, which this PR does not re-open. A field written on the store that handleRequest's run(...) opened is visible to both frames, because both run inside that one run(...). Each request has its own object, so requests do not share it. A1b is the measurement: recording through enterWith turns (e), (f) and (h) red. The template is the existing (a)–(d) measurement in the same file.
  • The actor form, svc:scim:CONNECTION_ID. It follows the incumbent svc:KIND:NAME shape (svc:flow:FLOW_NAME, ADR-0014 D2), as the triage audit rules. It uses the connection id, not a name, because the id is immutable and keys sys_scim_connection_binding, sys_scim_user and the credential row. The credential row has no connection name; its label names one credential, which a rotation replaces.
  • ADR tension, recorded and not resolved here. ADR-0118 D5 says the actor dimension is two-valued and that "which automation" is answered by an association field. ExecutionContext.performedBy's doc reads that as keeping sys_audit_log.actor to a user id or null. The incumbent svc:flow: label and sys_audit_log.actor's own description ("a user id, svc:NAME, or null") put service principals on actor. This PR follows the triage ruling (the incumbent) and also writes D5's association field, metadata.scim_connection_id.
  • Why the metadata key is read off the label. A hook receives only session.actor and the closed provenance copy list that @objectstack/objectql's buildProvenance builds. A typed carrier for the connection id would need a key on ExecutionContext and HookContext.provenance in packages/spec plus a line in objectql, and both are outside this card's file surface. So the plugin-audit half reads the one channel that already reaches it, and the column and the key cannot disagree. The cost is that the svc:scim: prefix is spelled in both packages (plugin-audit does not depend on plugin-auth). The two test files' literals hold the spellings equal. The plugin-audit file surface is used as the claim allows ("only if the row writer must read a new context field"). The writer reads no new field. It reads the existing session.actor, and its metadata block gains one key.
  • Clause-②: yes (widening), against the claim's provisional no. Re-derived from the diff, by the criterion 「本卡放宽接受集或扩大公开面吗」. authSystemWriteContext is published (export * from './auth-actor-attribution.js'), and the built packages/plugins/plugin-auth/dist/index.d.ts declares its return type with the new actor?: string member, which widens the public surface. The SCIM helpers are not on the published surface (0 hits in dist/index.d.ts), and neither is plugin-audit's new module (0 hits). Nothing is narrowed. The changeset level is patch (Metadata-protocol: none).
  • The one row that stays null. The sys_oauth_resource create is written when the first request builds the better-auth instance, before any bearer is verified. It is the platform's boot write, not a provisioning one. The pins warm the instance with a SCIM read first, for that reason.
  • sys_member. reconcile-membership.ts builds its write context with the same authSystemWriteContext(), so a SCIM create that binds a membership names the connection. It is not driven here, because the rig assigns no organization.
  • A case removed as vacuous. A SCIM request that also carries a signed-in human's session cookie was pinned to credit no human. A4, the ablation that let the human win, stayed GREEN: on a SCIM request the session resolver credits no human even with a cookie. The SCIM-first ordering in authSystemWriteContext is therefore not observable through the door, and the case was deleted rather than kept as a pin that cannot fail. The ordering stays: the bearer is the request's only principal, and it saves a session resolution per write.
  • Length bound, not measured on PostgreSQL. sys_audit_log.actor is maxLength: 255 (driver-sql makes it a varchar(255)), and so is sys_scim_connection_credential.connection_id. A connection id longer than 246 characters gives a label longer than 255. SQLite stores it. On PostgreSQL the ledger insert would be refused and the row lost through the writer's swallow-and-report path. Which connection ids are accepted is the mint door's decision (plugin-auth: no door mints a SCIM connection bearer (mintScimConnectionCredential has no caller and the object refuses create), so no IdP can connect #30958, seat 3), where a bound of 246 would close it.
  • Activity timeline. The sys_activity mirror row of these writes still shows the system (actor_id null). The card is about sys_audit_log, so this is noted and not changed.
  • Serial with plugin-auth: no door mints a SCIM connection bearer (mintScimConnectionCredential has no caller and the object refuses create), so no IdP can connect #30958. No PR existed for plugin-auth: no door mints a SCIM connection bearer (mintScimConnectionCredential has no caller and the object refuses create), so no IdP can connect #30958 before this branch's first push or at opening. origin/main was merged at 4876c8cf (it brought fix(plugin-auth)!: revoke an MCP access token through its OAuth consent #30879's plugin-auth changes, and nothing conflicted).

Verification

All at 727dacbe, the head this PR opens with (origin/main 1aeeafd9 merged in at 4876c8cf; origin/main has since moved 11 commits, none touching packages/plugins/plugin-auth or packages/plugins/plugin-audit, so it is not merged again).

  • pnpm --filter @objectstack/plugin-auth test: Test Files 229 passed (229), Tests 4151 passed | 10 skipped (4161) (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/plugin-audit test: Test Files 75 passed (75), Tests 1181 passed (1181) (lock VERDICT command-exit 0).
  • pnpm --filter @objectstack/plugin-auth typecheck and pnpm --filter @objectstack/plugin-audit typecheck: exit 0 (check:test-typecheck: OK for both).
  • The reproduction above, re-run on ad43453b (727dacbe adds one comment): 20 of 21 rows named, the bearer and its digest absent from every row.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 65 commands; all ran, plus pnpm lint, pnpm check:cli-command-ids and pnpm check:adr-symbol-anchors: all exit 0. Three first exited 3 (PREREQUISITE NOT MET): check-plugin-teardown-shape.mjs --self-test on a shallow clone, exit 0 after git fetch --unshallow origin main; check:dual-build-cjs-loads and check:i18n with no whole-tree dist/, exit 0 after pnpm exec turbo run build --concurrency=2 (lock VERDICT command-exit 0, 119/119 tasks). dispatch-gates.mjs --ran: exit 0, "65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED".
  • node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0.
  • Built packages/plugins/plugin-auth/dist/index.d.ts: authSystemWriteContext() returns a Promise of { isSystem: true; actor?: string; … }; scimRequestConnectionId, recordScimRequestConnection, scimServicePrincipal: 0 hits. Built packages/plugins/plugin-audit/dist/index.d.ts: scimConnectionOf, SCIM_CONNECTION_ID_KEY: 0 hits.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

… that performed them

The SCIM request scope handleRequest opens now carries a mutable
connectionId slot. The bearer verifier records the verified connection
into it, and authSystemWriteContext gives every write of the request
the service principal svc:scim:CONNECTION_ID on ExecutionContext.actor,
with no attributedUserId. The audit writer records the label on actor
and the connection id as metadata.scim_connection_id.

Claude-Session: https://claude.ai/code/session_016WV4cyRYCwPvYJ68qTXzEP
Co-authored-by: Claude <noreply@anthropic.com>
…ine and on the ledger row

Claude-Session: https://claude.ai/code/session_016WV4cyRYCwPvYJ68qTXzEP
Co-authored-by: Claude <noreply@anthropic.com>
…on could turn red

Claude-Session: https://claude.ai/code/session_016WV4cyRYCwPvYJ68qTXzEP
Co-authored-by: Claude <noreply@anthropic.com>
…nnection principal

Claude-Session: https://claude.ai/code/session_016WV4cyRYCwPvYJ68qTXzEP
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-audit, @objectstack/plugin-auth, touching 14 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via withSystemContext (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 000a4c0d36397398e856f0c34e9e0cdbbf3d96b0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7277f25b2598a738398070ad88dd9072252743d5 — the merge of head 727dacbee0d1fac4dbf89f1500d375f5faf2f709 into base 000a4c0d36397398e856f0c34e9e0cdbbf3d96b0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7277f25b2598a738398070ad88dd9072252743d5 && git checkout 7277f25b2598a738398070ad88dd9072252743d5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 000a4c0d36397398e856f0c34e9e0cdbbf3d96b0 727dacbee0d1fac4dbf89f1500d375f5faf2f709 && git checkout -B drift-repro 000a4c0d36397398e856f0c34e9e0cdbbf3d96b0 && git merge --no-ff 727dacbee0d1fac4dbf89f1500d375f5faf2f709

node scripts/docs-audit/affected-docs.mjs --json 000a4c0d36397398e856f0c34e9e0cdbbf3d96b0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 000a4c0d36397398e856f0c34e9e0cdbbf3d96b0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Contract-review-claim: session_016WV4cyvF7SP65frVFAYLv7 · head 727dacb · 2026-11-06T15:58Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 727dacbee0d1fac4dbf89f1500d375f5faf2f709
Local-runs: none

Owed by the Clause-②: yes (widening) line alone (check-owed-reviews face declaration). The diff touches no published schema and no governed path (check-governed-merges --pr 30989: 0 of 9 paths hit, NOT governed), and declares no narrowing. Inputs: card #30960 (body and triage audit), the seat's claims, the dev's os-dev report 6689150540, the PR body, and the net diff against the merge-base 1aeeafd9 (9 files, +508/-5), with the cited code read at head.

① Derived judgments

  1. authSystemWriteContext() (published through export * from './auth-actor-attribution.js') gains an optional actor?: string on its return type. Additive: no member is removed or narrowed, and every existing caller spreads the object into a write context. Right, and it is the only published movement. The new scim-connection-service.ts exports (ScimRequestScope, recordScimRequestConnection, scimRequestConnectionId, scimServicePrincipal, SCIM_SERVICE_PRINCIPAL_PREFIX) are not reachable from the package index, and plugin-audit's new module is internal too.
  2. Inside a SCIM protocol request whose bearer verified, every write that goes through the adapter's asAttributedSystem carries actor: 'svc:scim:CONNECTION_ID' and no attributedUserId, impersonatedBy or performedBy. The early return is right: the bearer authorized the request, so a session cookie that happens to ride along is not the actor. Outside a SCIM scope, or before verification, scimRequestConnectionId() is undefined and the function is byte-identical to main. Right.
  3. The carrier is a mutable connectionId slot on the store handleRequest opens with run(...), filled once by recordScimRequestConnection inside verifyBearerToken. The slot is per request (each run gets its own object), first-verified-wins, and a no-op on a failed verification. The dev's ablations show both rejected designs going red: enterWith (A1b) and a process-wide slot (A3), with the concurrent two-connection case (h) holding at a barrier. Right. No import cycle: scim-connection-service.ts imports only node:crypto and node:async_hooks.
  4. plugin-audit derives metadata.scim_connection_id from the same actorLabel it already writes to sys_audit_log.actor, so the two cannot disagree. Only a svc:scim: prefix with a non-empty id yields the key, and other svc: principals are unchanged (A6 pins that). user_id stays null. The metadata object is written only when one of its four keys is present, so rows of every other actor are unchanged. Right.
  5. Secrets: only verification.connection.id is stored on the scope. The bearer, its digest and the credential row never enter the write context, and test (f) asserts that no row carries either one. Right.
  6. Other readers of context.actor / session.actor at head: action-invocation-audit.ts and read-audit.ts, both attribution-only. Nothing in an authorization path reads the label (ExecutionContext.actor is attribution-only by its spec docblock). No widening of who may do what.

② Semver level

One changeset: '@objectstack/plugin-auth': patch and '@objectstack/plugin-audit': patch, with Clause-②: yes (widening) and Metadata-protocol: none line-initial in both the changeset and the PR body. patch is right by the level rule (Metadata-protocol: none ships patch, widening included). The two packages are exactly the packages the diff moves.

③ Boundary flags

No must-fix.

Implemented-by: claude/project-thread-c66ai3
Reviewed-by: session_016WV4cyvF7SP65frVFAYLv7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

席位核对 · ACCEPT

PR #30989 · head 727dacbe · 2026-11-06T16:02Z · card #30960 (p1) · claim re-posted with Branch claude/project-thread-c66ai3 · os-dev report 6689150540 · review PASS 6689251324 (TIER_DEFAULT, 727dacbe)

维护者速读(席位校正)

The body's summary lines stand as written, read against the diff.

席位核对

  • Card fit: an identity provider's SCIM writes (the sys_user create, the deactivation's ban update, the delete, and the sys_scim_* rows) now land on sys_audit_log with actor = 'svc:scim:CONNECTION_ID', metadata.scim_connection_id = CONNECTION_ID and user_id null. Reproduced on main first: 21 rows, all null. After the fix, 20 of 21 name the connection; the 21st is the boot-time sys_oauth_resource create, written before any bearer is verified.
  • Secrets: only the connection id is placed on the request scope. Test (f) pins that no row carries the bearer or its digest.
  • Isolation: the slot is per request (run(...) opens one object per request). Test (h) holds two concurrent requests on two connections at a barrier, and each attributes to its own. Sign-up and sign-in around a SCIM request carry no actor (test g).
  • Declarations: Fixes #30960. Clause-②: yes (widening) was re-derived from the claim's provisional no (the published return type of authSystemWriteContext gains actor?). Metadata-protocol: none. Both packages are patch, which is right by the level rule; the review is owed and posted.
  • Carried, no card: the label-length edge (a connection id over 246 characters would overflow sys_audit_log.actor), noted to domain:server#3 for the plugin-auth: no door mints a SCIM connection bearer (mintScimConnectionCredential has no caller and the object refuses create), so no IdP can connect #30958 mint door. The sys_activity mirror still shows the system. The svc:scim: prefix is spelled in both packages, held equal by tests.
  • Gates (report): 65 derived, all exit 0, 0 NOT-MEASURED. Lint, check:cli-command-ids and check:adr-symbol-anchors exit 0. plugin-auth 4151 passed, plugin-audit 1181 passed. Six ablations went red and were restored; one vacuous case was deleted.
  • Landing: check-governed-merges --pr 30989 says NOT governed, +508/-5. The seat lands with pr_ready + automerge_enable once every check on 727dacbe is green or expected-skip.

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 tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-auth: SCIM-driven writes leave sys_audit_log rows with actor, user_id and metadata null, so no audit row names the SCIM connection that acted

2 participants