diff --git a/.changeset/environment-artifact-checksum-coverage-describe.md b/.changeset/environment-artifact-checksum-coverage-describe.md new file mode 100644 index 0000000000..24aa57019f --- /dev/null +++ b/.changeset/environment-artifact-checksum-coverage-describe.md @@ -0,0 +1,10 @@ +--- +"@objectstack/spec": patch +--- + +The environment artifact's `checksum` now states its own coverage boundary, and `grantedPermissions` states that it sits outside the digest by design. + +Describe text only — no key, value schema or accept-set change on `EnvironmentArtifactSchema`, and the digest itself is computed and verified by the control plane, not here. + +- **`checksum`** carried the shared `Sha256DigestSchema` describe ("SHA-256 digest (64 hex chars)"), which says what the value *is* and nothing about what it *covers*. It now has its own field-level describe: the SHA-256 digest of the canonical JSON serialization of the `metadata` block (stable key ordering), computed by the control plane when assembling the GET response — and coverage stops there, so no other key on the envelope is under the digest. The shared `Sha256DigestSchema` describe is unchanged, so every other digest field still inherits it. +- **`grantedPermissions`** gains one sentence group at the end of its describe: it sits beside `metadata`, outside the digest, and integrity of the granted consent set rests on the carrier — the artifact is environment-local and control-plane served (ADR-0003 / cloud ADR-0007) — an accepted boundary of this envelope rather than an oversight. Its five existing clauses (the manifest-`id` keying, the `sys_package_installation` source, the enforcer consumer, absent ≠ `{}`) are unchanged. diff --git a/packages/spec/src/system/environment-artifact.test.ts b/packages/spec/src/system/environment-artifact.test.ts index bac79afd93..d395a8ec6f 100644 --- a/packages/spec/src/system/environment-artifact.test.ts +++ b/packages/spec/src/system/environment-artifact.test.ts @@ -205,6 +205,47 @@ describe('EnvironmentArtifactSchema (wire shape)', () => { }); }); + // ─── Digest coverage is stated on the surface, not only in a docblock ──── + // + // There is no digest MECHANISM here to pin: `checksum` is computed by the + // control plane and this envelope only carries it. What can silently rot is + // the sentence that states what the digest covers — and the shape that + // carries it. `checksum` used to inherit the SHARED `Sha256DigestSchema` + // describe, which says what the value IS and nothing about what it COVERS; + // the coverage sentence therefore belongs on the KEY, and putting it back on + // the shared schema would relabel every other digest field in the repo. + describe('the checksum describe states its coverage boundary, on the key and not on the shared digest', () => { + const checksumDescription = () => EnvironmentArtifactSchema.shape.checksum.description ?? ''; + + it('names the `metadata` block as what the digest covers', () => { + expect(checksumDescription()).toMatch(/canonical JSON serialization of the `metadata` block/); + }); + + it('states what it does NOT cover — nothing else on the envelope, `grantedPermissions` included', () => { + expect(checksumDescription()).toMatch(/nothing else on the envelope/); + expect(checksumDescription()).toMatch(/`grantedPermissions` included/); + }); + + it('leaves the SHARED Sha256DigestSchema describe untouched — the key-level text did not leak onto it', () => { + expect(Sha256DigestSchema.description).toBe('SHA-256 digest (64 hex chars)'); + expect(EnvironmentArtifactSchema.shape.checksum).not.toBe(Sha256DigestSchema); + }); + + it('validates exactly as the shared digest still does — describing the key changed no rule', () => { + expect(EnvironmentArtifactSchema.shape.checksum.safeParse(WIRE_CHECKSUM).success).toBe(true); + expect(EnvironmentArtifactSchema.shape.checksum.safeParse(WIRE_CHECKSUM.toUpperCase()).success).toBe(false); + expect(EnvironmentArtifactSchema.shape.checksum.safeParse(WIRE_CHECKSUM.slice(0, 63)).success).toBe(false); + }); + + // `check:doc-authoring`: describe prose projects into + // content/docs/references/** and the generated skill artifacts, where an + // internal issue id is a citation-shaped token resolving to nothing. The + // card anchor lives in a `//` comment beside the key instead. + it('carries no internal issue id — this text is read by customers', () => { + expect(checksumDescription()).not.toMatch(/#\d{3,}/); + }); + }); + // The v0 keys are tombstoned, not silently stripped: authoring one raises // the prescription itself (retiredKey, #3855), and `tsc` types the key // `never` at the authoring site. @@ -383,6 +424,16 @@ describe('grantedPermissions — install-time granted set per plugin manifest `i expect(description).toMatch(/`\{\}` = consent-bearing and consented to nothing/); }); + it('the key description also states the digest boundary and where integrity of the granted set rests', () => { + const description = EnvironmentArtifactSchema.shape.grantedPermissions.description ?? ''; + expect(description).toMatch(/outside the `checksum` digest/); + expect(description).toMatch(/covers the `metadata` block/); + expect(description).toMatch(/rests on the carrier/); + expect(description).toMatch(/ADR-0003 \/ cloud ADR-0007/); + // Same `check:doc-authoring` rule as the checksum pin above. + expect(description).not.toMatch(/#\d{3,}/); + }); + it('any string key parses — which IS the residual risk: a `package_id`-shaped key is accepted and would simply never be looked up', () => { const parsed = EnvironmentArtifactSchema.parse({ ...wireMinimal, diff --git a/packages/spec/src/system/environment-artifact.zod.ts b/packages/spec/src/system/environment-artifact.zod.ts index 09ce094cf1..cea5caa47d 100644 --- a/packages/spec/src/system/environment-artifact.zod.ts +++ b/packages/spec/src/system/environment-artifact.zod.ts @@ -109,7 +109,12 @@ export const EnvironmentArtifactSchema = lazySchema(() => z.object({ * a type change invisible to key-level gates, #4666; pinned by the * parse tests next to this file). */ - checksum: Sha256DigestSchema, + checksum: Sha256DigestSchema.describe( + 'SHA-256 digest of the canonical JSON serialization of the `metadata` block (stable key ' + + 'ordering), computed by the control plane when assembling the GET response. Coverage stops ' + + 'at that block: nothing else on the envelope — `grantedPermissions` included — is under ' + + 'this digest, so a matching checksum attests the compiled metadata and nothing more.', + ), /** Build timestamp (ISO 8601). */ builtAt: z.string().datetime().optional(), @@ -170,13 +175,19 @@ export const EnvironmentArtifactSchema = lazySchema(() => z.object({ * silently. Sits beside `metadata`, outside the `checksum` digest (which * covers the `metadata` block only). */ + // Internal anchor for the digest-coverage boundary stated in the describe below: it was + // recorded as accepted on the premise that the carrier is trusted end-to-end (#14993). grantedPermissions: z.record(z.string(), PluginPermissionsSchema).optional() .describe( 'Install-time GRANTED permission set per plugin, keyed by the plugin manifest `id` ' + '(not the control-plane `package_id`). Written by the cloud control plane at consent-compile ' + 'time from `sys_package_installation.granted_permissions`; consumed by the materialize-time ' + 'loader via `PluginPermissionEnforcer.registerGrantedPermissions`. Absent = no consent record; ' - + '`{}` = consent-bearing and consented to nothing — the two are never collapsed.', + + '`{}` = consent-bearing and consented to nothing — the two are never collapsed. ' + + 'Sits beside `metadata`, outside the `checksum` digest, which covers the `metadata` block ' + + 'only: integrity of the granted set rests on the carrier — the artifact is ' + + 'environment-local and control-plane served (ADR-0003 / cloud ADR-0007) — an accepted ' + + 'boundary of this envelope, not an oversight.', ), // ── Retired v0 keys (#4740, ADR-0049) ──────────────────────────────