Filed by the domain:identity seat (PM session session_01NaS1PAHJcPfAA2acnV53Tn) at ACCEPT of PR #8777, which delivers the precedence half of #8707 as Part of and stops at this boundary. Unassigned. ⛔ No domain:* applied — routing is triage's call, but the work lands in packages/spec, which is the spec seat's sole ownership.
This is a contract addition, so it is escalated rather than adjudicated. It widens the authorable surface, which is the manual floor by the mechanical boundary test.
Why it exists
#8287 gave sys_api_key an active_organization_id column, named that way deliberately so the credential table would not become org-walled and start hiding keys from their own owners. #8707 is the recorded consequence: the audit writer must stamp a row with the organization of the record it is about, and it needs to know which column carries that.
PR #8777 solved the general case by resolving the column from the registered schema, mirroring SqlDriver.computeTenantField step for step. sys_api_key is the one shipped object that route cannot reach.
Measured, on origin/main
The underlying conflation: the platform has one answer to "what is this object walled by" and no answer to "which column says who this row is about". For ordinary objects those coincide; for credential tables they deliberately do not, which is why sys_api_key had to invent a differently-named column in the first place.
Options
四棱
实际业务需求 — measured, not speculative. The concrete scenario is revoking another user's API key while active in a different organization: the row lands behind the wrong tenant's wall, where the admin who can act on it cannot see it and one with no claim to the record can. sys_api_key declares enable.trackHistory: true, so the rows exist today.
平台长远合理性 — A separates two questions the platform currently conflates. B's long-term cost is a walled credential table, i.e. re-opening a closed defect. D leaves a declared-but-unenforced shape: the writer resolves the column from schema for every object except the one that motivated the work.
避免 AI 写代码犯错 — a declared key an author sets explicitly beats a consumer-side heuristic. The tempting alternative — scan for a lookup whose reference is sys_organization — is already falsified by a shipped object: sys_organization itself declares no organization_id and exactly one such lookup, parent_organization_id, so the scan would stamp every organization's audit rows with its parent's id. No gate would catch that. PR #8777 pins the falsification in a test.
创业阶段不扩散需求 — one shipped object needs this today, which is the pull that justifies widening a surface rather than anticipating one. Scope it to that: ⛔ not a general "field roles" mechanism.
四棱同向 (A) — which is why the recommendation is confident; it is escalated because contract additions are the manual floor regardless of alignment, not because the analysis is split.
Not a duplicate of #8707
#8707 stays open and carries this half; #8777's changeset says so. This card exists because the remaining work is in another lane — #8707 is domain:identity and cannot be worked by that seat. Without a card in the spec lane, the half has no owner. Suggest Blocked-by: linkage from #8707 once this is routed.
A test in packages/plugins/plugin-audit/src/audit-writers.test.ts pins the current gap explicitly and is written to go red when this declaration lands, so the remaining half cannot be forgotten silently — but it will also need updating by whoever implements this.
Filed by the
domain:identityseat (PM sessionsession_01NaS1PAHJcPfAA2acnV53Tn) at ACCEPT of PR #8777, which delivers the precedence half of #8707 asPart ofand stops at this boundary. Unassigned. ⛔ Nodomain:*applied — routing is triage's call, but the work lands inpackages/spec, which is the spec seat's sole ownership.This is a contract addition, so it is escalated rather than adjudicated. It widens the authorable surface, which is the manual floor by the mechanical boundary test.
Why it exists
#8287 gave
sys_api_keyanactive_organization_idcolumn, named that way deliberately so the credential table would not become org-walled and start hiding keys from their own owners. #8707 is the recorded consequence: the audit writer must stamp a row with the organization of the record it is about, and it needs to know which column carries that.PR #8777 solved the general case by resolving the column from the registered schema, mirroring
SqlDriver.computeTenantFieldstep for step.sys_api_keyis the one shipped object that route cannot reach.Measured, on
origin/mainpackages/platform-objects/src/identity/sys-api-key.object.tsdeclares noorganization_idand notenancyblock at all.TenancyConfigSchema(packages/spec/src/data/object.zod.ts:516) is astrictObjectwith exactly two keys:enabledandtenantField. There is no read-neutral, stamp-only organization declaration to use. (Verified independently by the PM, not taken from the dev report.)tenancy.tenantField: 'active_organization_id'is measured-wrong, not merely inelegant:tenantFieldfeedsapplyTenantScope/injectTenantOnInsert, so it would wall the credential table on an equality that excludes NULL. Every pre-[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 key would vanish from its own owner's list — precisely the defect [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 exists to have removed. The field's owndescribetext in the schema confirms this role.The underlying conflation: the platform has one answer to "what is this object walled by" and no answer to "which column says who this row is about". For ordinary objects those coincide; for credential tables they deliberately do not, which is why
sys_api_keyhad to invent a differently-named column in the first place.Options
tenancy.organizationField, or an object-levelauditOrganizationField, consulted only by the audit writer and read by no tenant-scoping path (applyTenantScope,injectTenantOnInsert,computeTenantLayer0Filter).sys_api_keydeclaresactive_organization_idthere and stays unwalled.tenancy.tenantField. ⛔ Measured-wrong, above.|| active_organization_idin the audit writer. ⛔ Correct for exactly two objects and silently wrong for the third; excluded by Audit rows are stamped from the ACTOR's active organization in preference to the record's own — and the record-side fallback cannot seesys_api_key.active_organization_id#8707's own reasoning.sys_api_keyrevocation rows keep stamping the revoker's organization — the case that motivated Audit rows are stamped from the ACTOR's active organization in preference to the record's own — and the record-side fallback cannot seesys_api_key.active_organization_id#8707 stays broken.四棱
实际业务需求 — measured, not speculative. The concrete scenario is revoking another user's API key while active in a different organization: the row lands behind the wrong tenant's wall, where the admin who can act on it cannot see it and one with no claim to the record can.
sys_api_keydeclaresenable.trackHistory: true, so the rows exist today.平台长远合理性 — A separates two questions the platform currently conflates. B's long-term cost is a walled credential table, i.e. re-opening a closed defect. D leaves a declared-but-unenforced shape: the writer resolves the column from schema for every object except the one that motivated the work.
避免 AI 写代码犯错 — a declared key an author sets explicitly beats a consumer-side heuristic. The tempting alternative — scan for a lookup whose
referenceissys_organization— is already falsified by a shipped object:sys_organizationitself declares noorganization_idand exactly one such lookup,parent_organization_id, so the scan would stamp every organization's audit rows with its parent's id. No gate would catch that. PR #8777 pins the falsification in a test.创业阶段不扩散需求 — one shipped object needs this today, which is the pull that justifies widening a surface rather than anticipating one. Scope it to that: ⛔ not a general "field roles" mechanism.
四棱同向 (A) — which is why the recommendation is confident; it is escalated because contract additions are the manual floor regardless of alignment, not because the analysis is split.
Not a duplicate of #8707
#8707 stays open and carries this half; #8777's changeset says so. This card exists because the remaining work is in another lane — #8707 is
domain:identityand cannot be worked by that seat. Without a card in the spec lane, the half has no owner. SuggestBlocked-by:linkage from #8707 once this is routed.A test in
packages/plugins/plugin-audit/src/audit-writers.test.tspins the current gap explicitly and is written to go red when this declaration lands, so the remaining half cannot be forgotten silently — but it will also need updating by whoever implements this.