Skip to content
10 changes: 10 additions & 0 deletions .changeset/environment-artifact-checksum-coverage-describe.md
Original file line number Diff line number Diff line change
@@ -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.
51 changes: 51 additions & 0 deletions packages/spec/src/system/environment-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions packages/spec/src/system/environment-artifact.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) ──────────────────────────────
Expand Down
Loading