Skip to content

spec: audit stamping needs a read-neutral organization declaration — tenancy.tenantField cannot serve sys_api_key without walling the credential table (#8707 remainder) #8778

Description

@qq9340100

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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions