Uh oh!
There was an error while loading. Please reload this page.
feat(spec,plugin-security): partial field masking — maskingRule presets + keepHead/keepTail, enforced on the FieldMasker channel (#8993) - #9128
Conversation
…ts + keepHead/keepTail, enforced in FieldMasker (#8993) Re-introduces the 2026-06-pruned maskingRule key WITH its runtime consumer in the same PR (ADR-0049 declare = enforce; maintainer ruling 2026-08-16 Option A): - spec: FieldMaskingRuleSchema — closed preset enum (phone/id_card/ bank_account/email/name) + keepHead/keepTail escape hatch; guidance tombstone flipped (the #9019 placeholder precedent); liveness row live with evidence; authoring forms; regenerated artifacts. - plugin-security: maskFieldValue (deterministic, length-preserving, idempotent); maskResults REPLACES masked-for-caller fields instead of deleting (requiredPermissions is the unmask gate — same ADR-0066 D3 evaluation; explicit permission-set denies still delete); predicate + aggregate guards refuse masked-field probes (the equality oracle); detectMaskedEchoWrites refuses masked round-trip writes with 400 VALIDATION_ERROR; getReadableFields keeps masked columns in the projection so export headers match served rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
…changeset (#8993) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
…e form strings (#8993) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01225pUjnCKWqxcc1PeqKFUq
📓 Docs Drift CheckThis PR changes 3 package(s): 109 hand-written doc(s) reference the affected code — list omitted above 15 rows. Re-derive: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31949519830 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
os-zhuang
commented
Aug 16, 2026
Queue-failure triage (lane PM, own-PR handling — steward yield protocol observed). Build 31949519830 died in Action: re-queuing ONCE (auto-merge re-armed). A second identical setup death would suggest a runner-image/corepack drift worth its own card rather than another rebuild. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8993
Implements the maintainer's 2026-08-16 Option A ruling (「主线二:批 A+A = 宣布进入监管行业市场(企业级合规包方向)」): partial field masking as a runtime-first capability, riding the existing
FieldMaskerchannel, with the declarable key landing in the same PR as its enforcement.What landed
Spec (
packages/spec)FieldSchema.maskingRule— the key pruned 2026-06 returns WITH its runtime consumer (ADR-0049 declare = enforce; thetrackHistory/auditTrailprecedent). Closed preset enumphone/id_card/bank_account/email/nameplus a{ keepHead, keepTail }escape hatch (FieldMaskingRuleSchema,FieldMaskingKeepSchema,FIELD_MASKING_PRESETS). No free-form format strings, no per-role rule matrices.placeholderon FieldSchema — Option C ruling on objectui#4676 (#9019) #9113 flippedplaceholder's: theFIELD_KEY_GUIDANCE.maskingRuleretirement entry is removed (the "no guidance entry names a key the schema now declares" test enforces the absence), and the 2026-06 prune note now records the reintroduction-with-consumer.maskingRulerow aslivewith evidence (the spec: declareplaceholderon FieldSchema — four objectui render surfaces already honour the key the producer refuses (ruled Option C on objectui#4676) #9019 precedent);state-counts.mdregenerated. Authoring forms (field.form.ts,object.form.ts) gained rows; the new form strings are translated (zh-CN / ja-JP / es-ES). ADR-0122: bare alias is the author state (z.input), isomorphism pinned asIso850.Enforcement (
packages/plugins/plugin-security) — the single channelmaskFieldValue(value, rule): deterministic, length-preserving, idempotent transforms (same input → same masked value; list rendering and grouping stay stable). Non-string scalars/objects collapse to a fixed opaque mask so no length or truthiness bit leaks from shapes a rule was never written for. An invalid stored rule fails closed to a full mask, with a warning.requiredPermissionsand the caller holds all of them — the unmask gate is the existing ADR-0066 D3 evaluation, no parallel matrix. On-behalf-of reads require BOTH principals to hold them (ADR-0090 D10). A permission set that marks the field non-readable still wins entirely: the key stays deleted — a masking rule never widens an explicit deny.maskResultsREPLACES masked-for-caller values instead of deleting the key; runs onfind/findOneand the write-echo images, so API caller and browser user are masked identically and the AI-context interceptor inherits it for free.detectMaskedEchoWrites): a payload value that is a fixed point of its own rule and carries the mask character is the placeholder a masked read served, round-tripped; writing it through would replace the stored value with138****5678. Refused loudly with400 VALIDATION_ERROR(MaskedValueWriteError; standard-catalog code, bothstatusandstatusCodespelled). Callers holding the unmask capabilities are exempt by construction, so a privileged import of literally-starred data stays possible. A short value that equals its own mask image but carries no*passes.getReadableFields/getMetadataReadableFieldskeep masked-but-served columns in the projection (mirroring the step-4 exclusion exactly), so the security 服务 getReadableFields 查询面(export 列投影的长期正解) #3547 export header matches the masked values the same caller's rows carry.Export path (
packages/rest) — measured, no bypassThe PM's mechanism assumption held:
GET /data/:object/exportstreams throughfindDatawith the caller's context, i.e. through the same engine middleware — there is no masking bypass on export. Newexport-integration.test.tssuite pins screen-vs-CSV parity (the auditors' probe): the CSV cell equals the list value byte-for-byte (写**), JSON export too, and the masked column's header survives.Packaging split (per the AI-governance precedent)
cloud'ssecurity-enterpriseAI-governance README documents the pattern: the open runtime carries the enforcement channel; Enterprise adds policy and inherits masking "by construction" through forced actor context. Applied here: the spec key, the preset tables AND the masking engine all live in the open runtime (spec + plugin-security) — a declared rule must be enforced in a community deployment too, or the key would be exactly the dead surface the 2026-06 prune removed. The enterprise compliance bundle (#8992 pairing) composes on top of this channel; nothing enterprise-only was created.Semantics worth reviewing (the judgment calls)
requiredPermissionsmasks everyone (non-system). The alternative — inert until a gate is declared — recreates a silently-dead declaration per field, the ADR-0104/ADR-0049 failure class. Declare = enforce from the first read; the unmask gate is opt-in.Verification
Full local union re-run at the final commit —
git rev-parse --short HEAD=54cda1e3b(every reading below is from that head, post-#9113-merge):pnpm --filter @objectstack/plugin-security test— 66 files / 1276 tests green (newfield-masking-rule.test.ts: preset transforms, idempotence, applicability matrix, oracle-guard refusals with code+status, echo-write refusal, readable-projection).pnpm --filter @objectstack/spec test— 406 files / 10781 tests green (declaration suite incl. closed-enum rejections; authoring-key-lint flip; Iso850 pin).pnpm --filter @objectstack/rest test— 120 files / 2003 tests green (export parity suite).check:generatedall 13 artifacts current;check:liveness,check:i18n,check:i18n-coverage,check:spec-parsed-alias, ratchet family (type-check-coverage/debt,engine-double-contract,where-matcher,query-options-erasure),check:nul-bytes, changeset gates,check:dev-prereqs— green. Example appsobjectstack validate(crm/showcase/todo) exit 0.placeholderon FieldSchema — Option C ruling on objectui#4676 (#9019) #9113 (placeholder, same-file churn) landed mid-task and is re-merged viascripts/pm/os-regen-merge.sh; artifacts regenerated on the merged tree; both keys verified present inauthorable-surface/data.jsonand the liveness ledger.Changeset: minor on
@objectstack/spec+@objectstack/plugin-security(accept-surface addition; not breaking, so no ADR-0087 marker owed).Out of ruled scope, noted: the explain engine (
security.explain) reports the field-mask layer as binary readable/masked and does not yet distinguish "partially masked" — filed as #9127.Generated by Claude Code
Generated by Claude Code