Skip to content

security explain: the fls layer's three field-mask states are expressible only in the detail free-text string — no structured per-field slot on ExplainLayer #9329

Description

@os-project-manager

Observation

Found while implementing #9127 (out of its scope — that card's ask was that the layer stop misreporting, which it now does).

#9127 gives the fls layer three states — hidden / partially masked / readable — but ExplainLayerSchema (packages/spec/src/security/explain.zod.ts) has no structured slot for per-field results. The layer carries layer, kernelTier, verdict, detail (a plain string), contributors and the record-grained record. So the three states, and the maskingRule that produced each partial mask, exist only inside the prose of detail:

1 field(s) masked from responses: [ssn]; 2 field(s) PARTIALLY masked — the key is
still served, its value replaced: [phone (phone), bank (bank_account)]

A consumer that wants to know which fields are in which state has to string-parse that sentence.

Why it may matter

ADR-0090 D6 names two uses for the engine, and the second one is machine-facing — the module header states it directly: "the AI-safety story gets its audit substrate — a publish gate can show the SEMANTIC impact of a grant change instead of a JSON diff." A publish gate diffing two explain reports to show that a grant change moved phone from readable to partially masked can only do it by diffing prose today. contributors and record.rules[] exist precisely so the other layers do not have this problem; the field-mask axis has no equivalent.

Why this is filed as an observation, not a defect

Measured on main at e4e5c6e — nothing is broken and nothing is waiting on it:

  • no in-repo consumer reads the fls layer's detail, or matches on layer === 'fls' outside plugin-security itself (the only hits are the spec enum, the spec's own test, and an unrelated id: 'fls' row in the dogfood authz matrix);
  • packages/rest's explain route passes the whole decision through untouched;
  • no publish gate consuming explain reports exists yet.

So this is a speculative capability surface until something actually needs to read it. Per the startup-focus principle, the honest disposition is to wait for a real consumer and let that consumer's needs shape the field, rather than mint a reporting shape now and guess at it — a declared-but-unread structured field would be its own declared ≠ enforced liability on a security surface. Filing it so the reasoning is on record when a consumer does show up.

If one does, the shape worth considering is a per-field array on the layer (field name, state, and the applicable rule for the partial case), which keeps detail as the human rendering rather than the data.

Backlink: #9127 · PR #9328

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions