From 0d4e759696d95f56dfc3f0979e43e096b3da501d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 14:26:47 +0000 Subject: [PATCH 1/2] fix(plugin-security): withdraw the ADR-0091 D5 recertification claim from the attestation columns `last_certified_at` / `certified_by` on `sys_user_permission_set` and `sys_user_position` are the ADR-0091 D5 substrate and nothing more: a whole-tree sweep finds them only in these two declarations and the generated i18n bundles. Nothing stamps them, nothing reads them, no surface derives "never certified" or "certification stale". Their descriptions stated D5's intent as though it were the behavior. On a compliance surface (SOX / ISO 27001 access review) that reads as evidence of a control the platform does not have. ADR-0049 enforce-or-remove, settled the way `sys_capability.active` was (maintainer ruling, 2026-08-13): the claim is withdrawn in prose rather than the workflow built or the columns dropped. Co-Authored-By: Claude --- .../objects/sys-user-permission-set.object.ts | 37 ++++++++++++++++++- .../src/objects/sys-user-position.object.ts | 23 +++++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts index 684c2cef38..9b30c77187 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts @@ -98,17 +98,50 @@ export const SysUserPermissionSet = ObjectSchema.create({ 'A row with delegated_from set is not itself delegatable and not self-renewable.', }), + // [#9046] ADR-0091 D5 calls these two columns the recertification + // "substrate", and they are exactly that and nothing more. A whole-tree + // sweep (packages/, apps/, examples/, every .ts/.tsx, tests included) + // finds the pair in two kinds of place only: these declarations and the + // generated i18n bundles that carry their strings. No producer, no + // consumer - nothing stamps them, nothing reads them, and no surface + // derives "never certified" or "certification stale". Their siblings on + // this object are not like that: valid_from/valid_until are enforced by + // isGrantActive at resolution time, and reason/delegated_from are read by + // the delegated-admin gate and the security-posture lint. + // + // The old descriptions ("When this grant was last attested in a + // recertification review", "Reviewer who last attested this grant") stated + // D5's intent as though it were the behavior. Access recertification is a + // compliance control (SOX / ISO 27001 access review), so that misreading + // is the expensive kind: an admin walking these objects - or an AI agent + // authoring against this model - takes a populated "Last Certified At" as + // evidence of a review the platform never performed and never checked. + // + // ADR-0049 enforce-or-remove, settled the way sys_capability.active was + // (maintainer ruling, 2026-08-13): building the review workflow is a + // designed feature with no measured pull, and dropping shipped columns + // costs a migration over existing rows while buying nothing the prose fix + // does not - the harm here is the promise, not the storage. So the claim + // is withdrawn, and the descriptions state the inertness outright rather + // than merely omitting the promise: a reader who remembers the old wording + // has to be told it was wrong, not left to infer it. If D5 is ever + // implemented, these two descriptions are what must change with it. last_certified_at: Field.datetime({ label: 'Last Certified At', required: false, description: - '[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified.', + '[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' + + 'Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives ' + + '"never certified" or "certification stale" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed.', }), certified_by: Field.lookup('sys_user', { label: 'Certified By', required: false, - description: '[ADR-0091 D5] Reviewer who last attested this grant.', + description: + '[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' + + 'Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — ' + + 'the platform checks nothing about it and grants nothing on the strength of it.', }), created_at: Field.datetime({ diff --git a/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts b/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts index e3ec418793..6da0c51154 100644 --- a/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts +++ b/packages/plugins/plugin-security/src/objects/sys-user-position.object.ts @@ -117,17 +117,36 @@ export const SysUserPosition = ObjectSchema.create({ 'A row with delegated_from set is not itself delegatable and not self-renewable — chains are cut both ways.', }), + // [#9046] The same declared-but-inert D5 pair as on + // sys_user_permission_set, with the same disposition. The whole-tree sweep + // finds these two columns in the two declarations and the generated i18n + // bundles and nowhere else: nothing writes them, nothing reads them, no + // surface derives "never certified" or "certification stale" - while + // valid_from/valid_until (isGrantActive) and reason/delegated_from (the + // delegated-admin gate, the security-posture lint) on this same object all + // resolve to real enforcement. The old descriptions stated D5's intent as + // though it were the behavior, which on a compliance surface reads as + // evidence of an access review that never happened. ADR-0049 + // enforce-or-remove, settled as sys_capability.active was (maintainer + // ruling, 2026-08-13): the claim is withdrawn in prose rather than the + // workflow built or the columns dropped. Full rationale sits with the + // sibling declaration in sys-user-permission-set.object.ts. last_certified_at: Field.datetime({ label: 'Last Certified At', required: false, description: - '[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified.', + '[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' + + 'Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives ' + + '"never certified" or "certification stale" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed.', }), certified_by: Field.lookup('sys_user', { label: 'Certified By', required: false, - description: '[ADR-0091 D5] Reviewer who last attested this grant.', + description: + '[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' + + 'Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — ' + + 'the platform checks nothing about it and grants nothing on the strength of it.', }), created_at: Field.datetime({ From d0f90f63d288111871c009d3f659224de4ccf034 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 14:36:27 +0000 Subject: [PATCH 2/2] chore(plugin-security): regenerate en bundle and carry the D5 correction into zh-CN/ja-JP/es-ES MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `node scripts/check-i18n-bundles.mjs --write --filter=security` regenerates the default locale from the schema labels, which moves `en` only: extraction merges against the committed bundles (`mergeExisting: !--no-merge`), so a locale that already holds a hand translation is deliberately never overwritten — `--no-merge` would wipe every hand-translated string in the bundle. The three localized `help` leaves are therefore updated in place, which is the maintenance path the bundle header names ("Edit translations in place ... only the leaf string values"); keys and structure are untouched, so a re-extract reproduces them. Adds the changeset: the descriptions are user-visible, and without a version bump the correction never reaches npm. Co-Authored-By: Claude --- .../d5-recertification-claim-withdrawn.md | 39 +++++++++++++++++++ .../src/translations/en.objects.generated.ts | 8 ++-- .../translations/es-ES.objects.generated.ts | 8 ++-- .../translations/ja-JP.objects.generated.ts | 8 ++-- .../translations/zh-CN.objects.generated.ts | 8 ++-- 5 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 .changeset/d5-recertification-claim-withdrawn.md diff --git a/.changeset/d5-recertification-claim-withdrawn.md b/.changeset/d5-recertification-claim-withdrawn.md new file mode 100644 index 0000000000..674bbe23a5 --- /dev/null +++ b/.changeset/d5-recertification-claim-withdrawn.md @@ -0,0 +1,39 @@ +--- +"@objectstack/plugin-security": patch +--- + +fix(security): the ADR-0091 D5 attestation columns stop claiming a recertification review the platform does not run (#9046) + +`last_certified_at` and `certified_by` are declared on both grant tables +(`sys_user_permission_set`, `sys_user_position`) as the ADR-0091 D5 +recertification *substrate*. A whole-tree sweep over `packages/`, `apps/` and +`examples/` — every `.ts`/`.tsx`, tests included — finds the pair in exactly two +kinds of place: those two declarations and the generated i18n bundles carrying +their strings. **No producer and no consumer.** Nothing stamps either column, +nothing reads either one, and no surface derives "never certified" or +"certification stale" from them. The sweep is not blind: the sibling ADR-0091 +columns on the same objects all resolve to real enforcement — `valid_from` / +`valid_until` through `isGrantActive` at resolution time, `reason` and +`delegated_from` through the delegated-admin gate and the security-posture lint. + +Their descriptions nonetheless stated D5's intent as though it were the +behavior — *"When this grant was last attested in a recertification review. Null += never certified"* and *"Reviewer who last attested this grant."* Access +recertification is a compliance control (SOX / ISO 27001 access review), so that +misreading is the expensive kind: an admin walking `plugin-security`'s objects, +or an AI agent authoring against this model, takes a populated `Last Certified +At` as evidence of a review the platform never performed and never checked. + +ADR-0049 enforce-or-remove, settled the way `sys_capability.active` was +(maintainer ruling, 2026-08-13): **the claim is withdrawn in prose.** Building +the review workflow is a designed feature with no measured pull, and dropping +shipped columns costs a migration over existing rows while buying nothing the +prose fix does not — the harm here is the promise, not the storage, and a +description is one line to change back if D5 is ever implemented. The columns, +their types and their storage are untouched; no producer and no consumer is +added, deliberately. + +Both descriptions now state the inertness outright rather than merely omitting +the promise, so a reader who remembers the old wording is told it was wrong +instead of being left to infer it. All four locale bundles carry the corrected +text. diff --git a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts index 1911266d34..8309f8d8f6 100644 --- a/packages/plugins/plugin-security/src/translations/en.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/en.objects.generated.ts @@ -323,11 +323,11 @@ export const enObjects: NonNullable = { }, last_certified_at: { label: "Last Certified At", - help: "[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified." + help: "[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed." }, certified_by: { label: "Certified By", - help: "[ADR-0091 D5] Reviewer who last attested this grant." + help: "[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it." }, created_at: { label: "Created At" @@ -409,11 +409,11 @@ export const enObjects: NonNullable = { }, last_certified_at: { label: "Last Certified At", - help: "[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified." + help: "[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed." }, certified_by: { label: "Certified By", - help: "[ADR-0091 D5] Reviewer who last attested this grant." + help: "[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it." }, created_at: { label: "Created At" diff --git a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts index 552bb5af7c..d714cd9306 100644 --- a/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/es-ES.objects.generated.ts @@ -323,11 +323,11 @@ export const esESObjects: NonNullable = { }, last_certified_at: { label: "Última certificación", - help: "[ADR-0091 D5] Cuándo se atestiguó por última vez esta concesión en una revisión de recertificación. Nulo = nunca certificada." + help: "[ADR-0091 D5] Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado." }, certified_by: { label: "Certificado por", - help: "[ADR-0091 D5] Revisor que atestiguó por última vez esta concesión." + help: "[ADR-0091 D5] Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él." }, created_at: { label: "Creado el" @@ -409,11 +409,11 @@ export const esESObjects: NonNullable = { }, last_certified_at: { label: "Última certificación", - help: "[ADR-0091 D5] Cuándo se atestiguó por última vez esta concesión en una revisión de recertificación. Nulo = nunca certificada." + help: "[ADR-0091 D5] Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado." }, certified_by: { label: "Certificado por", - help: "[ADR-0091 D5] Revisor que atestiguó por última vez esta concesión." + help: "[ADR-0091 D5] Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él." }, created_at: { label: "Creado el" diff --git a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts index 1bc4556e15..5d4eaea6a2 100644 --- a/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/ja-JP.objects.generated.ts @@ -323,11 +323,11 @@ export const jaJPObjects: NonNullable = { }, last_certified_at: { label: "最終認証日時", - help: "[ADR-0091 D5] この付与が再認証レビューで最後に証明された日時。Null = 未認証。" + help: "[ADR-0091 D5] 将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。" }, certified_by: { label: "認証者", - help: "[ADR-0091 D5] この付与を最後に証明したレビュアー。" + help: "[ADR-0091 D5] 同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。" }, created_at: { label: "作成日時" @@ -409,11 +409,11 @@ export const jaJPObjects: NonNullable = { }, last_certified_at: { label: "最終認証日時", - help: "[ADR-0091 D5] この付与が再認証レビューで最後に証明された日時。Null = 未認証。" + help: "[ADR-0091 D5] 将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。" }, certified_by: { label: "認証者", - help: "[ADR-0091 D5] この付与を最後に証明したレビュアー。" + help: "[ADR-0091 D5] 同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。" }, created_at: { label: "作成日時" diff --git a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts index a3d94f928e..17995cc2b8 100644 --- a/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts +++ b/packages/plugins/plugin-security/src/translations/zh-CN.objects.generated.ts @@ -323,11 +323,11 @@ export const zhCNObjects: NonNullable = { }, last_certified_at: { label: "最近认证时间", - help: "[ADR-0091 D5] 此授予在重新认证复核中最近一次被认证的时间。为空 = 从未认证。" + help: "[ADR-0091 D5] 为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。" }, certified_by: { label: "认证人", - help: "[ADR-0091 D5] 最近一次认证此授予的复核人。" + help: "[ADR-0091 D5] 同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。" }, created_at: { label: "创建时间" @@ -409,11 +409,11 @@ export const zhCNObjects: NonNullable = { }, last_certified_at: { label: "最近认证时间", - help: "[ADR-0091 D5] 此授予在重新认证复核中最近一次被认证的时间。为空 = 从未认证。" + help: "[ADR-0091 D5] 为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。" }, certified_by: { label: "认证人", - help: "[ADR-0091 D5] 最近一次认证此授予的复核人。" + help: "[ADR-0091 D5] 同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。" }, created_at: { label: "创建时间"