Skip to content

feat(plugin-security): organization administrators hold manage_connector_connections by default (ADR-0143 D5, ruling B) - #28441

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-jtzyzv
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-jtzyzv

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Part of #28427. This PR carries the code, pins, docs and changeset. The card's item 4 (one line in ADR-0143 D5 recording ruling B) is split out so that this PR is not held as Tier H; the line is handed to the seat as a patch, and the card stays open until it lands.

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:组织管理员(organization_admin,以及由它派生的 organization_admin_no_bypass)默认拥有 manage_connector_connections,组织的 owner/admin 一开始就能为本组织连接 OAuth2 连接器(DocuSign、Salesforce、Google 等),不再需要有人手工绑定权限集。
  • 为什么改:Tesla 在决策清单第 6 项答「同意」,即选 B:「B:开一张跟进卡,在 S4 Console Connect 之前默认授予 organization_admin 与 organization_admin_no_bypass」(裁决记录 security: a dedicated org-scoped manage_connector_connections capability for connecting OAuth2 connector instances (before ADR-0143 S4's Console flow) #28327 评论 6545223261)。不改的话,S4 的 Connect 按钮上线后每个组织管理员都会被拒。
  • 风险与回滚:这是一次放宽:组织管理员现在能读、发起、完成、撤销本组织的连接;只限本组织(授予按组织生效,门只作用于当前组织),看不到任何令牌。成员、只读等其它权限集不变,其它能力也不变。代表组织管理员、带 actions:execute 授权的 AI agent 会一起获得(沿用现有派生,平台管理员的 agent 今天已是如此),见 Acceptance notes。回滚即 revert 本 PR。
  • 先看:packages/plugins/plugin-security/src/objects/default-permission-sets.ts 的 organization_admin.systemPermissions 一行;objects/default-permission-sets.test.ts 的两条持有者 pin;automation-connector-connection-door.test.ts 的"本组织放行、他组织拒绝"用例。
  • 两分钟验证:pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/objects/default-permission-sets.test.ts,预期全绿,持有者恰为 admin_full_access、organization_admin、organization_admin_no_bypass 三个。
  • 需要你做的:无,席位落地。

What changes

The grant. organization_admin's systemPermissions gains manage_connector_connections (default-permission-sets.ts). The set is declared once; deriveWallLessOrgAdmin spreads the parent's declaration and drops only the wildcard superuser bits, so organization_admin_no_bypass carries the capability from that one line. It is never written a second time. auto-org-admin-grant.ts already grants one of the two sets, per organization and by posture, to every member whose role contains owner or admin, so those members hold the capability in that organization.

Scope. The capability is org-scoped. Core's resolver applies an organization-scoped grant only in the organization the grant row names (grantAppliesInTenant in resolve-authz-context.ts), and the connection door hands the service the caller's own tenantId. So an organization admin of org_a connects org_a's instances only. In an organization where they are a plain member they hold the member baseline, which does not carry the capability, and the door refuses them.

Unchanged. member_default, viewer_readonly and every other shipped set hold the capability only by explicit grant. No other capability of any set moves. Nothing under packages/spec changes.

The agent golden. builtin-positions.boot.test.ts and grant-permission-set-name.equivalence.test.ts build the agent row from the organization admin's resolved envelope through assembleExecutionContext (packages/core/src/security/assemble-execution-context.ts). For an OAuth agent with actions:execute (delegatesActions), that function passes the human's systemPermissions through unchanged. The agent's golden gains the capability through that path only. No line names an agent.

Docs. content/docs/automation/connectors.mdx: the connect paragraph now says platform administrators and organization administrators hold the capability by default, and anyone else receives it through a permission set.

Tests

  • objects/default-permission-sets.test.ts: the holders of manage_connector_connections across the shipped sets are exactly admin_full_access, organization_admin, organization_admin_no_bypass. member_default and viewer_readonly exist and do not hold it. A second pin asserts the no-bypass variant's systemPermissions equal its parent's, so the capability rides the derivation and is not a second list.
  • automation-connector-connection-door.test.ts (runtime), for each of GET / POST / DELETE / callback: an organization admin on the shipped grant is admitted with 200, and the service member is called once with tenantId: 'org_a'. The same person active in org_b, holding the member baseline there, is refused with 403 PERMISSION_DENIED, the message names the capability, and no service member runs. The transcribed ORG_ADMIN_GRANT now carries the capability.
  • Grant goldens: resolve-authz-grant-set-by-name.golden.test.ts, grant-readers-by-name.golden.test.ts, grant-permission-set-name.equivalence.test.ts and builtin-positions.boot.test.ts. Each gains the capability on the organization admin's envelope inside the organization, and on the agent acting for them. The resolver golden's noOrganization envelope stays systemPermissions: [].
  • packages-capability-gate.test.ts: the commit-trail refusal case named "the organization admin's full grant" now lists the full grant. It is still refused, because that door asks studio.access.

Whole packages on 29c35857: plugin-security Test Files 238 passed (238), Tests 5126 passed | 45 skipped (5171). runtime Test Files 496 passed (496), Tests 8972 passed | 19 skipped (8991).

Ablations

Both mutations went through scripts/ablation-replace.mjs. The anchor landed on disk (count 1 → 0), and the file was restored with the blob equal to HEAD and git diff HEAD empty. Both suites import their subjects from src, so no dist was involved.

mutation run red
drop 'manage_connector_connections' from organization_admin.systemPermissions (default-permission-sets.ts) the five plugin-security files above 17 / 93: the holders pin, all 8 boot cases, 2 equivalence, 3 grant-readers, 3 resolver goldens
the connection gate never refuses (refuseUngrantedConnectorConnectionAccess call replaced by undefined, automation.ts) automation-connector-connection-door.test.ts -t 'refused in another' 4 / 4: expected 200 to be 403 on each verb

The derivation-equality pin stays green under the first mutation, as expected, since both variants lose the capability together. The holders pin is the one that reds there.

Acceptance notes

  • ADR-0143 D5 line, split out. CODEOWNERS makes docs/adr/** Tier H. The line is a record of ruling B and changes no code path, so it ships separately rather than holding this PR. Patch for the seat: server-seat-1/r2026-10-31/out/28427-adr-0143-d5.patch. The landing of that PR closes the card.
  • Three packages/spec comments ruling B makes false, left untouched by the seat's ruling (spec lane's line, handed over after this merges): packages/spec/src/security/capabilities.ts:103-110, packages/spec/src/identity/eval-user.zod.ts:166-169, packages/spec/src/identity/platform-admin-capabilities.test.ts:52-54. Each says the organization administrator holds the capability only by explicit grant.
  • Agents acting for an organization admin reach the door. The connection door reads only systemPermissions, and an OAuth agent with actions:execute carries its human's list. So an agent acting for an organization admin can now read, begin and revoke that organization's connection, as an agent acting for a platform admin already can on main. Ruling B did not address agents. Whether the door should refuse an agent principal is a separate question for the seat.
  • content/docs/permissions/permission-sets.mdx's summary row for organization_admin names a subset of its capabilities (it already leaves out manage_mailboxes), so it is not edited here.
  • Branch claude/project-thread-jtzyzv is the card thread's platform-assigned branch; claim 6545275282 names it.

Verification

Local, on 29c35857: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 92 families. Each was run with stdin from /dev/null, and its exit was captured by redirect before any pipe. All 92 exit 0. Three of them (check:skill-examples, check:dual-build-cjs-loads, check:i18n) first exited 3, PREREQUISITE NOT MET, and were re-run after a full pnpm build. --ran: 92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED, exit 0. Scoped lint, the root lint script's eslint over packages/plugins/plugin-security packages/runtime with --no-inline-config: exit 0. pnpm --filter @objectstack/plugin-security --filter @objectstack/runtime run typecheck: exit 0.


Generated by Claude Code

…ections by default (WIP)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxkKcPULsq3fo4JFAX
…ault holders of manage_connector_connections

The shipped-set pin names admin_full_access and both organization-admin
sets as the only holders and member_default / viewer_readonly as
withheld; the no-bypass variant's list equals its parent's. The grant
goldens gain the capability on every organization-admin envelope inside
the organization (and the agent acting on its behalf), never outside it.
The connection door admits the shipped organization-admin grant for its
own organization and refuses the same person active in another one.
Docs: connectors.mdx names organization admins as default holders.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxkKcPULsq3fo4JFAX
…mmit-trail pin carries manage_connector_connections

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibxkKcPULsq3fo4JFAX
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-security, touching 13 documentable anchor(s).

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 5517005d3a53600300f5cb8974ae20f178b63132.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 60 of 212 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 17 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 5517005d3a53600300f5cb8974ae20f178b63132 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 86aa22a26f303241465cf18f1831f8216b433d4a — the merge of head 29c358576aa95cc4463f0cf1b616609dd81a113f into base 5517005d3a53600300f5cb8974ae20f178b63132, 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 86aa22a26f303241465cf18f1831f8216b433d4a && git checkout 86aa22a26f303241465cf18f1831f8216b433d4a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5517005d3a53600300f5cb8974ae20f178b63132 29c358576aa95cc4463f0cf1b616609dd81a113f && git checkout -B drift-repro 5517005d3a53600300f5cb8974ae20f178b63132 && git merge --no-ff 29c358576aa95cc4463f0cf1b616609dd81a113f

node scripts/docs-audit/affected-docs.mjs --json 5517005d3a53600300f5cb8974ae20f178b63132

⚠️ 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 5517005d3a53600300f5cb8974ae20f178b63132 → 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

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 29c358576aa95cc4463f0cf1b616609dd81a113f · 2026-10-31T16:46Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 29c358576aa95cc4463f0cf1b616609dd81a113f
Local-runs: none
Scope: packages/plugins/plugin-security/src/objects/default-permission-sets.ts (one list entry on organization_admin.systemPermissions + comment), its holders/derivation pins (default-permission-sets.test.ts), four grant goldens (boot positions, name equivalence, grant readers, resolver), packages/runtime automation-connector-connection-door.test.ts (own-org admitted / other-org refused per verb) and packages-capability-gate.test.ts, content/docs/automation/connectors.mdx (connect paragraph), .changeset/28427-org-admin-connector-connections.md (@objectstack/plugin-security patch). 10 files, +92/−33. Part of #28427 (maintainer-direct card; claim 6545275282; dev report 6545987804); the ADR-0143 D5 line is split out as a Tier H patch for the seat.
Inputs: card #28427 body + claim + dev report, ruling record 6545223261 on #28327, the PR body, the PR diff at this head, git grep on origin/main at the merge-base 5da3a8ce (deriveWallLessOrgAdmin, refuseUngrantedConnectorConnectionAccess, grantAppliesInTenant; main's one later connectors.mdx hunk is at L415, this PR's at L516 — merge clean), check-runs (read 2026-10-31T16:49Z: all 50 latest-per-name check-runs completed, 36 success + 14 skipped, none failed, none running) — 50 checks. Nothing built or run here; the seat's numbers (plugin-security 238 files / 5126, runtime 496 / 8972, two ablations 17/93 and 4/4, 92 derived gates) are the dev report's.

① Derived judgments

  • One line, two sets, as ruled. manage_connector_connections joins organization_admin.systemPermissions; deriveWallLessOrgAdmin (main L1572-1577) spreads ...base and strips only the wildcard viewAllRecords/modifyAllRecords, so organization_admin_no_bypass carries it from the same literal — the derivation-equality pin keeps it that way. No other shipped set moves (holders pin: exactly admin_full_access, organization_admin, organization_admin_no_bypass).
  • Scope is the organization, by existing machinery. The capability is org-scoped (spec capabilities.ts, unchanged); core applies an organization-scoped grant only in the grant row's organization (grantAppliesInTenant, resolve-authz-context.ts L914); the door (automation.ts L952-958, L3101) reads systemPermissions fail-closed with only isSystem bypassing and hands the service the caller's own tenantId. The door test pins own-org 200 with tenantId: 'org_a' and other-org 403 PERMISSION_DENIED naming the capability, on GET/POST/DELETE/callback. Nothing in packages/runtime/src or packages/spec changes.
  • Declarations. Clause-②: yes (widening) — who the connection door admits widens; Metadata-protocol: none — no served key or shape. The patch bump on @objectstack/plugin-security is the seat's convention call (a default-grant change is behaviour, not surface); not a contract matter.
  • Agent reach is a consequence, not a change. assembleExecutionContext passes the human's systemPermissions to an OAuth agent with actions:execute on main already (platform admins' agents reach the door today); the goldens show the organization admin's agent gaining the capability through that one path. The PR names this and leaves "should the door refuse an agent principal" to the seat — correctly outside ruling B.
  • Docs. connectors.mdx now says platform and organization administrators hold it by default; permission-sets.mdx's summary row is left (already a subset). Three packages/spec comments that ruling B makes false (capabilities.ts L103-110, eval-user.zod.ts L166-169, platform-admin-capabilities.test.ts L52-54) are named and handed to the spec lane after merge — prose only, no behaviour.

② Rulings cited

③ Narrowing / widening

  • Widening only: holders of the two shipped sets are admitted at the door in their own organization. No refusal added; no served document changes meaning.

④ Behaviour / flags

  • Follow-ups owed, not blocking: the ADR-0143 D5 one-line PR (Tier H, closes the card); the three spec comment corrections (spec lane); the agent-principal question (seat).

Implemented-by: claude/project-thread-jtzyzv
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz
VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-tesla · session_01L9qibz3ZUbZ8Qcn1rDw9yc

维护者速读

  • 改了什么:组织管理员(包括无越权变体)默认持有「管理连接器连接」能力,只在自己的组织内有效,可以连接、重连、撤销本组织的 OAuth 连接器。改动只是一行权限列表,加上相应的测试和文档。
  • 为什么改:你在 15:10 对决裁清单第 6 项的「同意」(裁定 B):在 S4 控制台「连接」按钮上线之前,就让组织管理员能直接连接。
  • 风险与回滚:其他内置权限集不变;跨组织仍然拒绝(已有测试钉住)。回滚即 revert,回到只有平台管理员持有。
  • 先看:plugin-security/src/objects/default-permission-sets.ts 中 organization_admin 的 systemPermissions。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/domains/automation-connector-connection-door.test.ts,全部通过。
  • 需要你做的:无,席位落地。ADR-0143 D5 的记录行另开一个小 PR,需要 os-zhuang 批准。

席位意见:契约复审 PASS。代理人(agent)代组织管理员调用这扇门的问题不在裁定 B 范围内,已另交决裁清单(#435 第 1 项),不挡本 PR。可以直接落地。

席位核对

ACCEPT · seat domain:server#1 · head 29c35857 · 2026-10-31T16:51Z

  1. Shape: Part of #28427, draft, base main, 10 files, +92/−33, with a patch changeset for @objectstack/plugin-security. No packages/spec path.
  2. Ruling: B (6545223261). The grant is one entry on organization_admin.systemPermissions; organization_admin_no_bypass inherits it through deriveWallLessOrgAdmin, with a derivation-equality pin. The holders pin is exactly admin_full_access plus both org-admin sets. The door pin admits own-org and refuses other-org with 403 on all four verbs.
  3. Contract review: lane PASS 6546313449 at this head. Agent reach is noted as an existing consequence of assembleExecutionContext, outside ruling B. The seat sent it to the maintainer's decision list.
  4. Gates / check-runs:
    • Dev report 6545987804: 92/92 derived gates exit 0, ablations 17/93 and 4/4.
    • On head 29c35857 at review: 39 success, 18 skipped, 1 running, 0 failed. Auto-merge waits on the required checks.
  5. Accepted deviations:
    • Part of instead of Fixes, because the ADR-0143 D5 line is split into its own Tier H PR (patch held in project files). That PR carries Fixes #28427.
    • Three stale packages/spec comments are left for the spec lane, per the seat's ruling.
  6. Landing: check-governed-merges --pr 28441 reports NOT governed. The seat readies the PR and arms auto-merge under the maintainer's 「按 pm-dispatch 审过的pr你可以自主落地」.

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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant