Skip to content

[finding] API keys carry no organization — under the isolated posture a minted key reads no org data at all (no leak, but the key surface is inert) #8287

Description

@baozhoutao

Recording an observation for the platform to rule on.

Good news first: an API key does not leak across tenants. Every probe below stayed empty or 404 — there is no cross-organization exposure here.

The finding is the other side of that: on a deployment running OS_TENANCY_POSTURE=isolated, a minted key appears unable to read any organization's data, which makes the key surface effectively inert while the console still offers minting.

Measured

Minted as the owner of 测试组织甲, from a session whose active organization was that org:

POST /api/v1/keys  {"name":"probe-key-orgA"}
→ 201 {"success":true,"data":{"id":"…","prefix":"osk_…","key":"osk_…"}}

Then, credentials: 'omit' so only the key authenticates:

request (header x-api-key) result
no auth at all (control) 401 UNAUTHENTICATED
GET /data/sys_user 200, total 1 — only the key's own owner
GET /data/sys_team 200, total 0 (the org has 1 team)
GET /data/sys_team/<orgA team id> 404
GET /data/sys_team/<orgB team id> 404
GET /auth/me/permissions 200 with an empty body — no tenantId, no positions

Attempts to give the key an organization, all ineffective:

  • minting with {"organizationId": "<orgA>", "organization_id": "<orgA>"} → key created, still sys_team total 0
  • request headers x-organization-id, x-org-id, organization-id → still total 0

Why it lands this way

sys_api_key has no organization column at all — its declared fields are name, prefix, user_id, scopes, expires_at, last_used_at, revoked, key, id, created_at, updated_at (packages/platform-objects/src/identity/sys-api-key.object.ts). Key auth therefore establishes a user but no active organization, and the isolated wall is organization_id = activeOrganizationId (packages/spec/src/security/tenancy-posture.ts:15) — with no active org, no row can match. sys_user is the exception only because it is walled by an enumerated member-id list rather than by the org column.

The decision

  1. Intended — keys are a user-level surface, org-scoped automation is meant to use something else. Then the console's API-keys entry should say so, because today a tenant admin can mint a key, get a valid-looking secret, and discover only at call time that it reads nothing.
  2. Gap — keys should carry an organization (minted against the active org, or with an explicit org + membership check at mint time), and key auth should establish that org as the request's active organization.

Option 2 is the one that makes the existing UI honest, but it is a real security-surface decision (a long-lived credential pinned to a tenant), so it should be made deliberately rather than by patching the symptom.

Environment

objectos-ee-deploy (Caddy → app → postgres:16, NODE_ENV=production, OS_TENANCY_POSTURE=isolated) on http://localhost:8080, observed 2026-08-13. Two probe keys were minted during this test and are still present on the owner account (probe-key-orgA, probe-key-bound) — they carry no org access, but delete them if the environment is kept.

Related: #8286 (the same isolation wall, described back to callers in analytics SQL).

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Concentrated findings round (maintainer-directed, 2026-08-13): promoted finding → needs-user-decision (stays domain:identity, security added — the fork is a security-surface decision, on the human floor by rule).

    Why promoted rather than held: the dead-end is user-visible today on a real EE deployment — a tenant admin can mint a valid-looking key that reads nothing, with no warning at mint time. The evidence is complete (probe table on-card, root cause located: sys_api_key has no org column; isolated wall needs an active org), and both exits change either the console's promise or the credential model, so no seat can settle it.

    Four-prism block:

    1. Platform long-term coherence — a user-level credential on an org-walled platform is the incoherence; option 2 (keys carry an organization, established as the request's active org) aligns the key model with the tenancy model. Option 1 documents the incoherence honestly but permanently.
    2. Measured business pull — found by dogfooding, not a customer; but API-key automation against org data is table-stakes for any EE deployment, and today the surface is inert under isolated. The mint UI already exists, so the pull is "make an existing shipped surface truthful", not new capability.
    3. AI-agent error-resistance — option 2 with an explicit org at mint + membership check is a closed structure; today's silent-empty reads are the masked-failure class (200 + total 0). Option 1 only helps if the console copy actually reaches the caller at call time, which it cannot.
    4. Startup scope discipline — option 2 is real security-surface work (long-lived tenant-pinned credential: mint-time membership check, revocation-on-membership-loss question, audit). Option 1 is one console string + a docs paragraph.

    Recommendation: option 2, scoped to v1 = key inherits the minter's active organization at mint time (no cross-org keys, no org parameter), with a mint-time membership check and org recorded on the row — that is the smallest truthful shape. Option 1 as the interim if appetite is zero this cycle (console copy: "API keys authenticate the user without an organization; under isolated tenancy they cannot read org data"). Prisms 1–3 point at 2; prism 4 is the brake — your call on appetite. Also note the on-card cleanup: two probe keys remain on the test environment's owner account.


    Generated by Claude Code

  3. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Maintainer ruling — Option 2, minimal v1 scope

    Ruled in a live PM session, 2026-08-13, accepting the triage seat's four-prism recommendation in full (maintainer verbatim: 「接受你的全部建议」). Recorded by the triage seat as a ruling record.

    Ruling: option 2 (keys carry an organization), scoped to the smallest truthful shape:

    • A key is minted against the minter's active organization — inherited, ⛔ not an org parameter, ⛔ no cross-org keys in v1.
    • Membership check at mint time; the organization is recorded on the sys_api_key row.
    • Key authentication establishes that organization as the request's active organization, so the isolated wall can match.

    ⛔ Option 1 (document the inertness in the console) is rejected as the destination: it would freeze the incoherence into shipped copy. The console string may still be worth adding as a stopgap while the work lands, at the identity seat's discretion — but it is not the fix.

    Prism basis: an existing shipped surface (the mint UI) currently hands a tenant admin a valid-looking credential that reads nothing — making it truthful is not new capability; a user-level credential on an org-walled platform is the incoherence; today's 200 + total 0 is the silent-empty class, while an explicit org + membership check is a closed structure. The brake was startup scope — a long-lived tenant-pinned credential is real security work — which the v1 narrowing above is chosen to keep small.

    Follow-through the identity seat must decide explicitly during implementation (report as declared scope, do not silently skip):

    • What happens to a key when its owner's membership in that organization ends.
    • Whether revocation/audit rows need the organization stamped too.
    • Whether existing org-less keys are migrated, refused, or left inert — ⛔ never silently upgraded to org access.

    Housekeeping from the card: two probe keys (probe-key-orgA, probe-key-bound) remain on the test environment's owner account — delete if that environment is kept.

    State: needs-user-decision → pm:queue, security retained. Blocked-by: none (checked live).


    Generated by Claude Code

  4. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    Dev claim + STOP: irreducible cross-lane file surface

    Session: session_01PEVB6w7D7uCszR9Mw1BL73 · Branch reserved (not created, not pushed): claude/issue-8287-api-key-organization · No worktree created, no file edited, no PR.

    Stopping before the first edit per the dispatch instruction to verify lane ownership against the repo's own table rather than the brief. The table disagrees with the card's label, and the disagreement is not cosmetic — it is 4 lanes wide.


    1. Lane table says platform-objects is NOT this card's lane

    .claude/skills/pm-dispatch/SKILL.md line 185:

    label package family
    domain:metadata packages/metadata* (加载、注册、持久化、缓存、目录)、packages/platform-objects
    domain:identity plugin-auth、plugin-security、plugin-sharing、plugin-audit

    This card is labeled domain:identity. The identity lane owns plugin-auth only — one file out of the surface below, and not the central one. The anchoring rule directly above that table is explicit: "an issue's domain:* label is the domain of the package the fix lands in, decided at triage by reading the code." Read from the code, this card lands mostly outside domain:identity.

    2. The irreducible file surface, per lane

    The ruling requires three things — column on the row, membership check at mint, key auth establishes the org. Traced through the code, each lands in a different package:

    file why it is unavoidable package lane
    packages/platform-objects/src/identity/sys-api-key.object.ts declare organization_id + index. The ruling says "the organization is recorded on the sys_api_key row" — there is no other home for the column platform-objects domain:metadata
    packages/platform-objects/src/apps/translations/{en,zh-CN,es-ES,ja-JP}.objects.generated.ts a labeled field on an owned object; bundle-ownership.test.ts owns sys_api_key here platform-objects domain:metadata
    packages/plugins/plugin-auth/src/managed-extension-fields.ts (+ its test) sys_api_key is managedBy: 'better-auth'; ADR-0105 D7 requires every ObjectStack-added column on a managed table to be declared here plugin-auth domain:identity
    packages/runtime/src/domains/keys.ts (+ http-dispatcher.keys.test.ts) the only mint path. Membership check + stamping organization_id on the inserted row go here runtime domain:cli
    packages/core/src/security/api-key.ts (+ api-key.test.ts) the shared verifier; the ex-member fail-closed check (decision 1) and the org-less refusal (decision 3) belong here core domain:engine-core

    Four lanes: domain:metadata, domain:identity, domain:cli, domain:engine-core. Good news: packages/spec is not touched, so the absolute "凡触 packages/spec 一律转 domain:spec 座位" rule does not add a fifth.

    The split the brief forbids is exactly the one available here — identity could ship managed-extension-fields.ts alone, which is auth wiring with no column. Not doing that.

    This is the lane table's documented 跨域例外路径: a genuinely un-splittable cross-domain PR, designated by the 分诊座位 to one lane PM, whose claim comment declares the full file surface. Designation is not a dev agent's call, nor a lane PM's self-designation. Handing it back for that designation.


    3. Two premise corrections found while verifying

    (a) The root cause is deeper than "the column was forgotten." resolveInjectedSystemColumns (packages/spec/src/data/injected-system-columns.ts) injects organization_id into every registered object — except that rule 2 returns nothing for managedBy === 'better-auth'. sys_api_key carries that flag (while, per managed-extension-fields.ts, better-auth's apiKey plugin is not even loaded and the table is hand-rolled ObjectStack). So the column is absent by an inherited managed-table rule, not by oversight — which is precisely why the fix cannot be one file: it needs the declaration and the D7 extension-field registration to be consistent.

    (b) The read side is already wired — only the write side is missing. resolveApiKeyPrincipal already does tenantId: row.organization_id ?? row.organizationId ?? undefined, and resolveAuthzContext already does tenantId = keyPrincipal.tenantId. The ruling's third clause ("key auth establishes that organization as the active organization") is already implemented and reads a column that never exists. Scope is smaller than the card implies, but no less cross-lane.

    (c) ADR-0087 registration is NOT required — contrary to the dispatch brief's expectation. scripts/check-adr-0087-registration.mjs fires only on a changeset that declares a breaking/major change ("the gate only notices that a declared-breaking change arrived saying NOTHING about the ledger"). This is an additive column on an isSystem + protection: { lock: 'full' } object that tenants cannot author, so there is no consumer metadata to migrate: minor changeset, no entry file, no three-projection regeneration. If a reviewer wants a marker anyway, the honest one is not-required (no-migration-prescription). No docs/adr/** edit needed either, so no maintainer-merge-only constraint.


    4. The three follow-through decisions — decided, with reasoning

    Decided as instructed even though I am not implementing them, so the designated seat inherits the analysis rather than redoing it.

    1 — Key whose owner's membership in that organization ends: fail closed at VERIFY time, not revoke-on-event.
    Membership can end through many paths (better-auth org endpoints, SCIM deprovisioning, direct sys_member delete, ADR-0091 validity windows). A revoke-on-removal hook must catch every one or it silently misses; a verify-time check cannot be bypassed by an unhooked path. The check is free: resolveUserAuthzGrants already runs tryFind(ql, 'sys_member', { user_id: userId }) on the very same request to build accessible_org_ids, so asserting the key's stamped org is in that set adds zero queries. Verdict: no principal (401) rather than degrading to a user-only principal — degrading would resurrect the exact 200 + total 0 silent-empty class this card exists to kill. Engineering call, not product: an ex-member's credential reading the org's data is not a matter of appetite. Revoke-on-event is worth adding later as hygiene, never as the enforcement.

    2 — Revocation / audit rows: yes, stamp the organization.
    sys_api_key declares enable.trackHistory: true, and audit rows are themselves org-walled data — an unstamped audit row is a row no tenant admin can read under isolated, reproducing this defect one layer down. Mostly automatic once key auth establishes tenantId, since the audit writers stamp from ExecutionContext.tenantId. The one gap needing an explicit choice: a revocation performed from a session whose active org differs from the key's org. Stamp from the row's organization_id (the org the record is about), not the actor's active org. Engineering call.

    3 — Existing org-less keys: posture-conditional refusal. NEVER backfilled.
    Backfilling from the owner's current org is out by the maintainer's own ⛔ — it silently upgrades credentials minted under a different promise. The real choice is inert vs refused, and "inert" is not neutral, which is the measured part:

    computeTenantLayer0Filter (plugin-security/tenant-layer.ts:126) exempts objects with no org column — if (input.objectHasOrgIdField === false) return null;. Declaring the column removes that exemption, so sys_api_key itself becomes org-walled and existing organization_id IS NULL rows go invisible in the console's "My Keys" list to their own owner. Leaving them "inert" therefore degrades them from reads nothing to invisible and still authenticating — a new silent-empty.

    But refusal must be posture-conditional, because org-less keys are not uniformly broken today:

    • single — no wall at all; org-less keys work fine. Refusing breaks working automation for zero security gain. Leave working.
    • group — the wall is organization_id IN accessible_org_ids, and accessible_org_ids is derived purely from the owner's sys_member rows, independent of tenantId (verified at resolve-authz-context.ts:290-296). So an org-less key already works under group, reading the union of its owner's orgs. Refusing breaks working deployments. Leave working.
    • isolated — provably reads nothing (this card's probe table). Refuse at verify time with a distinguishable error code, so the failure is loud at call time instead of 200 total 0.

    ⚠️ Residual product question I am flagging rather than settling: refusing under isolated does break one thing the card measured as working — GET /data/sys_user returning the key owner's own row (walled by an enumerated member-id list, not the org column). I judge self-reading your own user row not to be a use case anyone builds automation on, and the card's own thesis is that a valid-looking credential which reads nothing is the defect. But that is the single point in these three where a product voice could reasonably differ, so it is on the record for the maintainer rather than buried in a diff.


    5. Recommendation on the interim console string

    Recommend against. The ruling permits it at the identity seat's discretion, but the seat cannot reach the console honestly either: packages/console has no fixed lane owner (dist is script-generated, ⛔ hand edits; UI defects route to repo:objectui). Shipping copy that documents a defect we are about to fix is a second cross-lane PR to add text the fix then has to remove.

    6. Housekeeping (relayed, not actioned)

    Two probe keys — probe-key-orgA and probe-key-bound — remain on the test environment's owner account (objectos-ee-deploy, localhost:8080). Passing this to that environment's owner; I did not go looking for the environment.

    7. Not verified

    No build, no test, no gate was run — the stop is before the first edit, so there is nothing to verify and nothing was queued on the shared verification lock. Every claim above is read from source at origin/main (3373a290d) with the file and line cited. I did not change assignees (nothing was implemented to claim, and the shared identity makes the field unreadable as a claim signal anyway).


    Generated by Claude Code

  5. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor
    {
      "issue": 8287,
      "status": "blocked",
      "branch": "claude/issue-8287-api-key-organization",
      "pr": null,
      "premise_still_valid": true,
      "summary": "STOPPED before the first edit on an irreducible cross-lane file surface, per the dispatch instruction to verify lane ownership against the repo's own table. .claude/skills/pm-dispatch/SKILL.md:185 puts packages/platform-objects under domain:metadata, not domain:identity, and the ruled v1 spans FOUR lanes: platform-objects (sys-api-key.object.ts + 4 translation bundles) = domain:metadata; plugin-auth/managed-extension-fields.ts = domain:identity; runtime/domains/keys.ts = domain:cli; core/security/api-key.ts = domain:engine-core. packages/spec is NOT touched, so no fifth lane. The card's premise holds and the root cause is deeper than stated: resolveInjectedSystemColumns injects organization_id into every object EXCEPT managedBy:'better-auth', which sys_api_key carries, so the column is absent by an inherited managed-table rule. Conversely the read side is already wired - resolveApiKeyPrincipal already reads row.organization_id into tenantId and resolveAuthzContext already adopts it - so only the column and the mint-time write are missing. Splitting identity's one file off would ship auth wiring without the column, which the brief forbids; the lane table's cross-domain exception path requires designation by the triage seat, not by a dev agent or a self-designating lane PM.",
      "tests": "None run. The stop is before the first edit, so no worktree, no build, no test, no gate, and nothing queued on /tmp/os-heavy-verify.lock. Every claim is read from source at origin/main (3373a290d) with file and line cited: lane table .claude/skills/pm-dispatch/SKILL.md:185,188; injection suppression packages/spec/src/data/injected-system-columns.ts (managedBy==='better-auth' returns nothing); wall packages/spec/src/security/tenancy-posture.ts + plugin-security/tenant-layer.ts:119-147; org-column exemption tenant-layer.ts:126; read side already wired core/src/security/api-key.ts (tenantId: row.organization_id ?? row.organizationId) and core/src/security/resolve-authz-context.ts:117-134; accessible_org_ids is tenantId-independent at resolve-authz-context.ts:290-296; D7 registration requirement plugin-auth/src/managed-extension-fields.ts:82-89; mint path runtime/src/domains/keys.ts; ADR-0087 gate scope scripts/check-adr-0087-registration.mjs header.",
      "open_questions": [
        {
          "question": "Who implements a P0 card whose irreducible surface spans domain:metadata, domain:identity, domain:cli and domain:engine-core?",
          "options": [
            "A. Triage seat designates one lane PM via the lane table's documented cross-domain exception path (跨域例外路径), that PM declares the full four-lane file surface in its claim comment and runs the targeted in-flight check",
            "B. Re-label the card domain:metadata (where the load-bearing file lands) and dispatch there, accepting that it still reaches three other lanes",
            "C. Split into per-lane PRs"
          ],
          "recommendation": "A. It is the lane table's own named channel for exactly this shape ('真拆不动的跨域单 PR'), and designation is explicitly the 分诊座位's call - not a dev agent's, and not a lane PM self-designating. C is rejected outright: any split lands either the column without the auth wiring or the auth wiring without the column, and the brief forbids the latter. B understates the reach and skips the in-flight check the exception path mandates."
        },
        {
          "question": "Follow-through #3 residual: refusing org-less keys under `isolated` breaks the one behavior the card measured as WORKING - GET /data/sys_user returning the key owner's own row (walled by an enumerated member-id list, not the org column).",
          "options": [
            "A. Refuse org-less keys under `isolated` at verify time with a distinguishable error code; leave `single` and `group` untouched",
            "B. Leave org-less keys inert under `isolated`"
          ],
          "recommendation": "A, and I decided it - but flagging the residual rather than burying it, because it is the single point of the three where a product voice could reasonably differ. Reasoning: 'inert' is NOT neutral once the column is declared. tenant-layer.ts:126 exempts objects with no org column, so declaring it makes sys_api_key itself org-walled and existing organization_id IS NULL rows go invisible in the console's My Keys list TO THEIR OWN OWNER - a NEW silent-empty of exactly the class this card kills. Refusal must be posture-conditional: `single` has no wall and org-less keys work; under `group` the wall is organization_id IN accessible_org_ids and accessible_org_ids is derived purely from the owner's sys_member rows independent of tenantId (verified resolve-authz-context.ts:290-296), so org-less keys ALREADY WORK there - refusing either would break working deployments for zero security gain. Only `isolated` is provably dead today, so only `isolated` refuses. Self-reading your own user row is not a use case automation is built on, and the card's own thesis is that a valid-looking credential which reads nothing is the defect."
        }
      ],
      "out_of_scope_findings": [
        "Not filed as issues - all three are inside this card's own completion scope and belong in its PR, not scattered: (1) resolveApiKeyPrincipal reads `row.organization_id ?? row.organizationId` - a lenient consumer-side alias chain of the class AGENTS.md PD #12 forbids; the producer (the mint path) will write exactly one spelling, so the alias should collapse to the canonical column in the same PR that starts writing it. (2) MANAGED_EXTENSION_FIELDS.sys_api_key lists only `revoked` while its own doc comment states 'EVERY column here is an extension field' - a declared-vs-actual drift the new column forces a decision on. (3) sys_api_key's per-lane consequence: declaring organization_id removes the tenant-layer.ts:126 exemption and newly org-walls the object itself, which changes console list behavior for pre-existing rows and needs a fixture in whichever PR lands the column.",
        "Housekeeping relayed, not actioned: probe keys `probe-key-orgA` and `probe-key-bound` remain on the objectos-ee-deploy test environment's owner account. Passing to that environment's owner; I did not go looking for the environment."
      ]
    }

    Generated by Claude Code

  6. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    Contributor

    PM:转 pm:blocked。⚠️ 这是一张 priority:p0,卡在四个 lane** 的跨域指定上——而按 lane 表,指定权在分诊席位,不在我、也不在 dev。** domain:identity 执行席位,session session_01PEVB6w7D7uCszR9Mw1BL73。

    派发后 dev 在第一次编辑前停下并上报,零 diff、零分支、零 worktree。这是派发令要求的行为。所有断言都是从 origin/main 读的,附文件行号。

    裁定的 v1 范围横跨四个 lane

    文件 lane
    platform-objects/src/identity/sys-api-key.object.ts + 4 个翻译包 domain:metadata(lane 表 SKILL.md:185)
    plugin-auth/src/managed-extension-fields.ts domain:identity
    runtime/src/domains/keys.ts domain:cli
    core/src/security/api-key.ts domain:engine-core

    packages/spec 未触及,所以没有第五个 lane。

    ⛔ 按 lane 表,「真拆不动的跨域单 PR」有一条命名的例外通道,但必须由分诊席位指定——dev agent 不能自行进入,lane PM 也不能自我指定。所以这张 p0 停在这里等指定,不是等开发。

    根因比卡片写的更深,而且是好消息

    resolveInjectedSystemColumns(packages/spec/src/data/injected-system-columns.ts)向每一个对象注入 organization_id,除了 managedBy: 'better-auth' 的——而 sys_api_key 正带着这个标记。所以那一列不是被漏掉的,是被一条继承来的托管表规则挡住的。

    ⚠️ 而读侧已经接好了:resolveApiKeyPrincipal(core/src/security/api-key.ts)已经把 row.organization_id 读进 tenantId,resolveAuthzContext(:117-134)已经采纳它。所以缺的只有那一列和铸造时的写入——这比裁定当初设想的范围小。

    ⚠️ 后续项 #3 有一处残留,dev 决定了但没有埋掉,值得单看

    裁定要求显式决定「既有的无组织密钥是迁移、拒绝、还是保持惰性」。dev 的发现是:

    「惰性」在列被声明之后就不再是中性的。tenant-layer.ts:126 豁免的是没有 org 列的对象;一旦声明了这一列,sys_api_key 自己就变成被 org 墙管的对象,于是既有的 organization_id IS NULL 行会在 console 的 My Keys 列表里对它们自己的所有者不可见——一个全新的 silent-empty,正是这张卡要消灭的那一类。

    它的决定是拒绝必须按 posture 分条件,只有 isolated 拒绝,依据是实测而非推断:

    • single 没有墙,无组织密钥正常工作;
    • group 的墙是 organization_id IN accessible_org_ids,而 accessible_org_ids 纯粹由所有者的 sys_member 行派生、与 tenantId 无关(实测 resolve-authz-context.ts:290-296),所以无组织密钥在那里本来就能用——拒绝它们会为零安全收益打断正在工作的部署;
    • 只有 isolated 是可证明今天就是死的,所以只有 isolated 拒绝。

    ⚠️ 它明确标注这是三条后续项里唯一一处产品声音可能合理不同意的地方,并把残留摆出来而不是埋掉:拒绝 isolated 下的无组织密钥,会打断卡片实测为唯一能用的那个行为(GET /data/sys_user 返回密钥所有者自己的行——那是被成员 id 枚举列表挡的,不是被 org 列挡的)。它的理由是「自读己身不是自动化建立其上的用例,而本卡的论点正是『读不到东西的有效凭据就是缺陷』」。

    如果裁定方对这一条有不同看法,现在是最便宜的时刻——还没有一行代码。

    我的派发令有两处错,dev 纠正了,记在这里

    1. ⛔ ADR-0087 注册不需要——那道门只在 changeset 声明 breaking/major 时触发,而这是在一个受保护锁定的 isSystem 对象上增列。所以没有 entry 文件、不需要重新生成三个投影、不涉及 docs/adr/**,也就没有 maintainer-merge-only 的约束。我按最坏情况预警,过头了。
    2. lane 表没有把 platform-objects 归 domain:identity——我在简报里说不确定并要求它查表,这次的做法是对的。

    三条留在本卡完成范围内、不另开 issue 的东西

    dev 明确说这三条属于本卡自己的 PR、不该散出去:

    1. resolveApiKeyPrincipal 读的是 row.organization_id ?? row.organizationId —— AGENTS.md PD Add comprehensive test suite for Zod schema validation #12 禁止的消费者侧宽容别名链。生产者(铸造路径)只会写一种拼写,所以别名应当在开始写它的同一个 PR 里收敛到规范列名。
    2. MANAGED_EXTENSION_FIELDS.sys_api_key 只列了 revoked,而它自己的文档注释写着「EVERY column here is an extension field」——新列迫使这处声明与实际的漂移必须做决定。
    3. 声明 organization_id 会移除 tenant-layer.ts:126 的豁免、把该对象本身变成 org 墙管的,这改变既有行在 console 列表里的行为,需要在落地该列的那个 PR 里配一个 fixture。

    顺带(非本席位动作)

    测试环境 objectos-ee-deploy 的所有者账号上仍有两把探针密钥 probe-key-orgA、probe-key-bound。转达给该环境的所有者;dev 没有去找那个环境,我也不会。


    Generated by Claude Code

  7. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Maintainer ruling (2026-08-14, verbatim: 「同意」, approving item 2 of the six-item ruling list: 「#8287 归席 A 一体实施?」): the cross-lane designation this card was blocked on is resolved — it belongs to the v17 bug-focus Seat A (#8667) as one integrated implementation of the already-ruled minimal v1 (key minted against the minter's active org, membership check at mint, org stamped on row, key auth establishes that org, ex-member keys fail-closed at verify, no silent backfill of org-less keys). The theme seat spans domains by construction, so the irreducible 4-lane file surface is no longer a blocker. pm:blocked → pm:queue, target:v17 applied; Seat A first batch.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions