From 59c287ad2441e437879ff442190c9d8970eddf4c Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 14:29:24 -0400 Subject: [PATCH 1/5] Clarify Comic Manifest approval expiry boundaries --- CHANGELOG.md | 5 ++ .../comic-manifest-v1-approval-times.json | 50 +++++++++++++++++++ fixtures/comic-manifest-v1.md | 5 +- rfcs/0007-comic-manifest-v1.md | 7 +++ specs/manifests/comic-manifest-v1.md | 12 ++++- tests/comic-manifest.test.cjs | 14 ++++++ tests/support/comic-manifest-oracle.cjs | 1 + 7 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 fixtures/comic-manifest-v1-approval-times.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3939e0d..f838ce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ compatible, or editorial. ## Unreleased +- Clarified the unreleased Comic Manifest 1.0.0 candidate's (#95 prerequisite) + detached approval time bounds: decision time is inclusive, expiry is exclusive + at both action and scoped publication time. Added synthetic boundary fixtures + and focused conformance coverage; no released contract bytes change. + - Extended the unreleased Comic Manifest 1.0.0 candidate for Studio #94 with immutable versioned output profiles, per-output limits, exact requirement/result/ release metadata matching and explicit rights/accessibility/publication boundaries. diff --git a/fixtures/comic-manifest-v1-approval-times.json b/fixtures/comic-manifest-v1-approval-times.json new file mode 100644 index 0000000..18461d7 --- /dev/null +++ b/fixtures/comic-manifest-v1-approval-times.json @@ -0,0 +1,50 @@ +[ + { + "name": "approval remains valid at decision time", + "action_time": "2026-09-15T12:02:00Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:11:00Z", + "valid": true + }, + { + "name": "action just before expiry remains valid", + "action_time": "2026-09-15T12:10:00.000Z", + "publication_time": "2026-09-15T12:00:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00.001Z", + "valid": true + }, + { + "name": "publication just before expiry remains valid", + "action_time": "2026-09-15T12:05:00Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00.001Z", + "valid": true + }, + { + "name": "action at expiry is expired", + "action_time": "2026-09-15T12:10:00Z", + "publication_time": "2026-09-15T12:09:59.999Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00Z", + "valid": false + }, + { + "name": "publication at expiry is expired", + "action_time": "2026-09-15T12:09:59.999Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00Z", + "valid": false + }, + { + "name": "decision after action time is not yet valid", + "action_time": "2026-09-15T12:10:00Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:10:00.001Z", + "expires_at": "2026-09-15T12:11:00Z", + "valid": false + } +] diff --git a/fixtures/comic-manifest-v1.md b/fixtures/comic-manifest-v1.md index d75d5cf..b7e2f87 100644 --- a/fixtures/comic-manifest-v1.md +++ b/fixtures/comic-manifest-v1.md @@ -21,7 +21,10 @@ correction, partial/failed evidence, deterministic identity, exact versioned out profiles, inclusive pixel/byte boundaries, profile/media incompatibility, missing, duplicate and undeclared outputs, accessibility/rights metadata mismatch, unknown versions/fields, ordered identities, immutable references, package bindings, -stale/revoked/forged approvals, public gates and private-context projection. Tests +stale/revoked/forged approvals, approval-time boundaries, public gates and +private-context projection. The [approval-time fixture matrix](comic-manifest-v1-approval-times.json) +covers inclusive decision equality, valid action/publication just before expiry, +expiry equality at either time, and a future decision. Tests compose non-rendered image/PDF placeholder bytes to exercise declared profile relationships; they deliberately do not claim media signatures or rendering. Output text bytes and public dependency bytes are explicit fixture inputs and diff --git a/rfcs/0007-comic-manifest-v1.md b/rfcs/0007-comic-manifest-v1.md index 4e7cf17..e378847 100644 --- a/rfcs/0007-comic-manifest-v1.md +++ b/rfcs/0007-comic-manifest-v1.md @@ -15,6 +15,13 @@ approval bindings avoid circular hashes and preserve exact scope without grantin authority from schema validity. Public private-context lineage uses approved random attestations and protected mappings, not protected record IDs or hashes. +Trusted approval-time evaluation uses an inclusive decision-time bound and an +exclusive expiry bound: `decided_at <= action_time < expires_at`. The publication +time named in the exact scope must also be strictly earlier than `expires_at`. +Equality with `decided_at` is valid; equality with `expires_at` at either the +action or scoped publication time is expired. This records the existing +test-oracle relationship as an explicit contract rule. + Codex owns normative meaning. Platform validates and supplies a test-only consumer proof against the existing released SDK/Builder output. Universe owns public publication and canon; private source owners retain access/disclosure authority. diff --git a/specs/manifests/comic-manifest-v1.md b/specs/manifests/comic-manifest-v1.md index 538d465..880d94d 100644 --- a/specs/manifests/comic-manifest-v1.md +++ b/specs/manifests/comic-manifest-v1.md @@ -266,8 +266,16 @@ raw artifact digests (empty for production review), exact release `scope`, revie role/actor, decision UUID, UTC decision/expiry and external authority reference. A binding is necessary evidence, not an authenticated grant. Trusted verification MUST bind the actual reviewer to the role, subject, scope and decision, check -revocation and current time, and fail closed on missing evidence. It MUST be valid -at the actual action and authorized publication time. Never trust an allow boolean. +revocation and current time, and fail closed on missing evidence. Let `action_time` +be the trusted time at which the authorization-dependent action is evaluated. The +time check MUST satisfy `decided_at <= action_time < expires_at`; it MUST also +satisfy `scope.publication_time < expires_at` for the publication time named in the +exact scope. Expiry is exclusive: an action or scoped publication at exactly +`expires_at` is expired, while an action at exactly `decided_at` is valid. A +decision later than `action_time` is not yet valid. The publication-time check +continues to use the existing scope relationship; this clarification adds no +separate ordering rule between `decided_at` and `scope.publication_time`. Never +trust an allow boolean. The production-reviewer approves the exact production intent for its declared scope. Publisher and canon-editor decisions bind the exact public release payload diff --git a/tests/comic-manifest.test.cjs b/tests/comic-manifest.test.cjs index 6d0358d..d034fd9 100644 --- a/tests/comic-manifest.test.cjs +++ b/tests/comic-manifest.test.cjs @@ -10,6 +10,7 @@ const schema = read('schemas/json/comic-manifest/v1/comic-manifest.schema.json') const ajv = new Ajv({ strict: true, strictRequired: false, strictTypes: false, allErrors: true }); addFormats(ajv); const validate = ajv.compile(schema), approval = ajv.compile({ $ref: schema.$id + '#/$defs/approval' }); const base = read('fixtures/comic-manifest-v1.json'), foundation = read('fixtures/context-builder-v1.json'); +const approvalTimeCases = read('fixtures/comic-manifest-v1-approval-times.json'); function schemaValid(s) { for (const k of ['production','result','release']) assert.equal(validate(s[k]), true, `${k}: ${JSON.stringify(validate.errors)}`); for (const a of s.approvals) assert.equal(approval(a), true, JSON.stringify(approval.errors)); @@ -73,6 +74,19 @@ test('all detached approval fields are closed and exactly versioned', () => { assert.equal(approval({...base.approvals[0],allow:true}),false); assert.equal(approval({...base.approvals[0],spec_version:'2.0.0'}),false); }); +for (const c of approvalTimeCases) test(c.name, () => { + const s = structuredClone(base); + s.at = c.action_time; + s.release.scope.publication_time = c.publication_time; + for (const a of s.approvals) { + a.decided_at = c.decided_at; + a.expires_at = c.expires_at; + } + rebind(s); + schemaValid(s); + if (c.valid) verifyScenario(s, foundation); + else assert.throws(() => verifyScenario(s, foundation), /APPROVAL_TIME/); +}); for (const c of read('fixtures/comic-manifest-v1-cases.json')) test(c.name, () => { const s = structuredClone(base); for (const edit of c.edits) { diff --git a/tests/support/comic-manifest-oracle.cjs b/tests/support/comic-manifest-oracle.cjs index 910c459..8cc371f 100644 --- a/tests/support/comic-manifest-oracle.cjs +++ b/tests/support/comic-manifest-oracle.cjs @@ -97,6 +97,7 @@ function verifyApproval(a, subject, artifacts, scope, s) { eq(a.artifact_digests, artifacts.map(x => x.artifact.sha256).sort(), 'APPROVAL_ARTIFACTS'); eq(a.scope, scope, 'APPROVAL_SCOPE'); const at = Date.parse(s.at); + // Decision time is inclusive; expiry is exclusive at both action and scoped publication time. check(Date.parse(a.decided_at) <= at && at < Date.parse(a.expires_at) && Date.parse(scope.publication_time) < Date.parse(a.expires_at), 'APPROVAL_TIME'); // A test harness supplies trusted decisions OUTSIDE untrusted record bytes. const trusted = s.trust.approvals.find(x => x.decision_id === a.decision_id); From 6546b0e0399de1caadb5f76c4a1bfe4192c7b9b1 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 14:43:19 -0400 Subject: [PATCH 2/5] Preserve precision in approval time checks --- CHANGELOG.md | 6 ++- .../comic-manifest-v1-approval-times.json | 48 +++++++++++++++++++ fixtures/comic-manifest-v1.md | 5 +- tests/comic-manifest.test.cjs | 2 + tests/support/comic-manifest-oracle.cjs | 15 ++++-- 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f838ce8..4dd8b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,10 @@ compatible, or editorial. - Clarified the unreleased Comic Manifest 1.0.0 candidate's (#95 prerequisite) detached approval time bounds: decision time is inclusive, expiry is exclusive - at both action and scoped publication time. Added synthetic boundary fixtures - and focused conformance coverage; no released contract bytes change. + at both action and scoped publication time. The test-only oracle preserves + submillisecond precision across variable fractional widths. Synthetic boundary + fixtures cover equal instants, expiry and future decisions; no released contract + bytes change. - Extended the unreleased Comic Manifest 1.0.0 candidate for Studio #94 with immutable versioned output profiles, per-output limits, exact requirement/result/ diff --git a/fixtures/comic-manifest-v1-approval-times.json b/fixtures/comic-manifest-v1-approval-times.json index 18461d7..14848ec 100644 --- a/fixtures/comic-manifest-v1-approval-times.json +++ b/fixtures/comic-manifest-v1-approval-times.json @@ -46,5 +46,53 @@ "decided_at": "2026-09-15T12:10:00.001Z", "expires_at": "2026-09-15T12:11:00Z", "valid": false + }, + { + "name": "submillisecond action just before expiry remains valid", + "action_time": "2026-09-15T12:10:00.0000Z", + "publication_time": "2026-09-15T12:10:00.0000Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00.0001Z", + "valid": true + }, + { + "name": "submillisecond publication just before expiry remains valid", + "action_time": "2026-09-15T12:05:00Z", + "publication_time": "2026-09-15T12:10:00.00010Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00.00011Z", + "valid": true + }, + { + "name": "equivalent fractional widths preserve inclusive decision equality", + "action_time": "2026-09-15T12:02:00.0001000Z", + "publication_time": "2026-09-15T12:10:00.00010Z", + "decided_at": "2026-09-15T12:02:00.0001Z", + "expires_at": "2026-09-15T12:10:00.00011Z", + "valid": true + }, + { + "name": "action at expiry is expired across fractional widths", + "action_time": "2026-09-15T12:10:00.0001000Z", + "publication_time": "2026-09-15T12:09:59.999Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00.0001Z", + "valid": false + }, + { + "name": "publication at expiry is expired across fractional widths", + "action_time": "2026-09-15T12:09:59.999999Z", + "publication_time": "2026-09-15T12:10:00.0001000Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15T12:10:00.0001Z", + "valid": false + }, + { + "name": "future submillisecond decision is rejected", + "action_time": "2026-09-15T12:10:00.0001Z", + "publication_time": "2026-09-15T12:10:00.0009Z", + "decided_at": "2026-09-15T12:10:00.0009Z", + "expires_at": "2026-09-15T12:10:00.0010Z", + "valid": false } ] diff --git a/fixtures/comic-manifest-v1.md b/fixtures/comic-manifest-v1.md index b7e2f87..8392367 100644 --- a/fixtures/comic-manifest-v1.md +++ b/fixtures/comic-manifest-v1.md @@ -23,8 +23,9 @@ duplicate and undeclared outputs, accessibility/rights metadata mismatch, unknow versions/fields, ordered identities, immutable references, package bindings, stale/revoked/forged approvals, approval-time boundaries, public gates and private-context projection. The [approval-time fixture matrix](comic-manifest-v1-approval-times.json) -covers inclusive decision equality, valid action/publication just before expiry, -expiry equality at either time, and a future decision. Tests +covers inclusive decision equality, just-before and equal expiry at both action +and publication time, future decisions, submillisecond boundaries, and equal +instants with different fractional widths. Tests compose non-rendered image/PDF placeholder bytes to exercise declared profile relationships; they deliberately do not claim media signatures or rendering. Output text bytes and public dependency bytes are explicit fixture inputs and diff --git a/tests/comic-manifest.test.cjs b/tests/comic-manifest.test.cjs index d034fd9..79925ec 100644 --- a/tests/comic-manifest.test.cjs +++ b/tests/comic-manifest.test.cjs @@ -82,6 +82,8 @@ for (const c of approvalTimeCases) test(c.name, () => { a.decided_at = c.decided_at; a.expires_at = c.expires_at; } + s.release.approvers = s.approvals.filter(a => a.role !== 'production-reviewer') + .map(({ decision_id, role, actor, decided_at }) => ({ decision_id, role, actor, decided_at })); rebind(s); schemaValid(s); if (c.valid) verifyScenario(s, foundation); diff --git a/tests/support/comic-manifest-oracle.cjs b/tests/support/comic-manifest-oracle.cjs index 8cc371f..154354a 100644 --- a/tests/support/comic-manifest-oracle.cjs +++ b/tests/support/comic-manifest-oracle.cjs @@ -7,6 +7,15 @@ const check = (v, code) => assert.ok(v, code); const unique = (xs, code) => eq(new Set(xs).size, xs.length, code); const rank = c => ['public', 'internal', 'confidential', 'restricted'].indexOf(c); const gates = ['editorial', 'canon-continuity', 'visual-text', 'integrity', 'provenance', 'security-privacy', 'rights', 'accessibility', 'packaging']; +function compareUtcInstants(left, right) { + const parse = value => /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/.exec(value); + const a = parse(left), b = parse(right); + check(a && b, 'APPROVAL_TIME'); + if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1; + const width = Math.max(a[2]?.length ?? 0, b[2]?.length ?? 0); + const af = (a[2] ?? '').padEnd(width, '0'), bf = (b[2] ?? '').padEnd(width, '0'); + return af < bf ? -1 : af > bf ? 1 : 0; +} const renditionProfiles = { 'comic-page-image@1.0.0': { media: ['image/png', 'image/jpeg', 'image/webp'], maxBytes: 50_000_000, image: true }, 'comic-portable-document@1.0.0': { media: ['application/pdf'], maxBytes: 50_000_000, image: false }, @@ -96,9 +105,9 @@ function verifyApproval(a, subject, artifacts, scope, s) { eq(a.subject, identity(subject), 'APPROVAL_SUBJECT'); eq(a.artifact_digests, artifacts.map(x => x.artifact.sha256).sort(), 'APPROVAL_ARTIFACTS'); eq(a.scope, scope, 'APPROVAL_SCOPE'); - const at = Date.parse(s.at); - // Decision time is inclusive; expiry is exclusive at both action and scoped publication time. - check(Date.parse(a.decided_at) <= at && at < Date.parse(a.expires_at) && Date.parse(scope.publication_time) < Date.parse(a.expires_at), 'APPROVAL_TIME'); + // Preserve arbitrary schema-valid fractional precision at each approval boundary. + check(compareUtcInstants(a.decided_at, s.at) <= 0 && compareUtcInstants(s.at, a.expires_at) < 0 && + compareUtcInstants(scope.publication_time, a.expires_at) < 0, 'APPROVAL_TIME'); // A test harness supplies trusted decisions OUTSIDE untrusted record bytes. const trusted = s.trust.approvals.find(x => x.decision_id === a.decision_id); check(trusted && trusted.sha256 === identity(a).sha256 && !s.trust.revoked.includes(a.decision_id), 'APPROVAL_TRUST'); From eb4065c8e6cef6e6d9bd01ecc5fcbea296cd1dfb Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 14:55:23 -0400 Subject: [PATCH 3/5] Accept schema timestamp separators in oracle --- .../comic-manifest-v1-approval-times.json | 24 +++++++++++++++++++ fixtures/comic-manifest-v1.md | 7 +++--- tests/support/comic-manifest-oracle.cjs | 9 +++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/fixtures/comic-manifest-v1-approval-times.json b/fixtures/comic-manifest-v1-approval-times.json index 14848ec..fd23ee8 100644 --- a/fixtures/comic-manifest-v1-approval-times.json +++ b/fixtures/comic-manifest-v1-approval-times.json @@ -94,5 +94,29 @@ "decided_at": "2026-09-15T12:10:00.0009Z", "expires_at": "2026-09-15T12:10:00.0010Z", "valid": false + }, + { + "name": "equivalent instants across accepted timestamp separators remain valid", + "action_time": "2026-09-15t12:10:00.0001000Z", + "publication_time": "2026-09-15\t12:10:00.00010Z", + "decided_at": "2026-09-15 12:10:00.0001Z", + "expires_at": "2026-09-15T12:10:00.00011Z", + "valid": true + }, + { + "name": "action at expiry is expired across accepted timestamp separators", + "action_time": "2026-09-15 12:10:00.0001000Z", + "publication_time": "2026-09-15T12:09:59.999Z", + "decided_at": "2026-09-15t12:02:00Z", + "expires_at": "2026-09-15t12:10:00.0001Z", + "valid": false + }, + { + "name": "publication at expiry is expired across accepted timestamp separators", + "action_time": "2026-09-15T12:09:59.999999Z", + "publication_time": "2026-09-15t12:10:00.0001000Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-15 12:10:00.0001Z", + "valid": false } ] diff --git a/fixtures/comic-manifest-v1.md b/fixtures/comic-manifest-v1.md index 8392367..3e173cf 100644 --- a/fixtures/comic-manifest-v1.md +++ b/fixtures/comic-manifest-v1.md @@ -24,9 +24,10 @@ versions/fields, ordered identities, immutable references, package bindings, stale/revoked/forged approvals, approval-time boundaries, public gates and private-context projection. The [approval-time fixture matrix](comic-manifest-v1-approval-times.json) covers inclusive decision equality, just-before and equal expiry at both action -and publication time, future decisions, submillisecond boundaries, and equal -instants with different fractional widths. Tests -compose non-rendered image/PDF placeholder bytes to exercise declared profile +and publication time, future decisions, submillisecond boundaries, equal +instants with different fractional widths, and equivalent instants using the +uppercase `T`, lowercase `t`, or a single whitespace separator accepted by the schema. +The tests compose non-rendered image/PDF placeholder bytes to exercise declared profile relationships; they deliberately do not claim media signatures or rendering. Output text bytes and public dependency bytes are explicit fixture inputs and independently hashed. Tests never create an approval from a production runtime or diff --git a/tests/support/comic-manifest-oracle.cjs b/tests/support/comic-manifest-oracle.cjs index 154354a..516e35c 100644 --- a/tests/support/comic-manifest-oracle.cjs +++ b/tests/support/comic-manifest-oracle.cjs @@ -8,12 +8,13 @@ const unique = (xs, code) => eq(new Set(xs).size, xs.length, code); const rank = c => ['public', 'internal', 'confidential', 'restricted'].indexOf(c); const gates = ['editorial', 'canon-continuity', 'visual-text', 'integrity', 'provenance', 'security-privacy', 'rights', 'accessibility', 'packaging']; function compareUtcInstants(left, right) { - const parse = value => /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/.exec(value); + const parse = value => /^(\d{4}-\d{2}-\d{2})[Tt\s](\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/.exec(value); const a = parse(left), b = parse(right); check(a && b, 'APPROVAL_TIME'); - if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1; - const width = Math.max(a[2]?.length ?? 0, b[2]?.length ?? 0); - const af = (a[2] ?? '').padEnd(width, '0'), bf = (b[2] ?? '').padEnd(width, '0'); + const aWholeSecond = `${a[1]}T${a[2]}`, bWholeSecond = `${b[1]}T${b[2]}`; + if (aWholeSecond !== bWholeSecond) return aWholeSecond < bWholeSecond ? -1 : 1; + const width = Math.max(a[3]?.length ?? 0, b[3]?.length ?? 0); + const af = (a[3] ?? '').padEnd(width, '0'), bf = (b[3] ?? '').padEnd(width, '0'); return af < bf ? -1 : af > bf ? 1 : 0; } const renditionProfiles = { From b6ec6080aa8cb4644144d6ee0d4e65c2ab1aec3d Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 15:42:52 -0400 Subject: [PATCH 4/5] Validate trusted approval action timestamps --- CHANGELOG.md | 7 ++-- .../comic-manifest-v1-approval-times.json | 40 +++++++++++++++++++ fixtures/comic-manifest-v1.md | 4 +- rfcs/0007-comic-manifest-v1.md | 8 +++- specs/manifests/comic-manifest-v1.md | 6 ++- tests/support/comic-manifest-oracle.cjs | 29 ++++++++++---- 6 files changed, 80 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd8b53..987d569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,10 @@ compatible, or editorial. - Clarified the unreleased Comic Manifest 1.0.0 candidate's (#95 prerequisite) detached approval time bounds: decision time is inclusive, expiry is exclusive at both action and scoped publication time. The test-only oracle preserves - submillisecond precision across variable fractional widths. Synthetic boundary - fixtures cover equal instants, expiry and future decisions; no released contract - bytes change. + submillisecond precision across variable fractional widths, accepts the full + schema timestamp separator forms, and validates trusted action calendar/clock + fields before comparison. Synthetic fixtures cover equal instants, expiry, + future decisions and malformed action times; no released contract bytes change. - Extended the unreleased Comic Manifest 1.0.0 candidate for Studio #94 with immutable versioned output profiles, per-output limits, exact requirement/result/ diff --git a/fixtures/comic-manifest-v1-approval-times.json b/fixtures/comic-manifest-v1-approval-times.json index fd23ee8..50557d4 100644 --- a/fixtures/comic-manifest-v1-approval-times.json +++ b/fixtures/comic-manifest-v1-approval-times.json @@ -103,6 +103,14 @@ "expires_at": "2026-09-15T12:10:00.00011Z", "valid": true }, + { + "name": "valid leap-day action time remains valid", + "action_time": "2024-02-29t12:10:00Z", + "publication_time": "2024-02-29 12:10:00Z", + "decided_at": "2024-02-29T12:02:00Z", + "expires_at": "2024-02-29T12:11:00Z", + "valid": true + }, { "name": "action at expiry is expired across accepted timestamp separators", "action_time": "2026-09-15 12:10:00.0001000Z", @@ -118,5 +126,37 @@ "decided_at": "2026-09-15T12:02:00Z", "expires_at": "2026-09-15 12:10:00.0001Z", "valid": false + }, + { + "name": "invalid trusted action hour is rejected", + "action_time": "2026-09-15T24:00:00Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-16T00:00:00Z", + "valid": false + }, + { + "name": "invalid trusted action minute is rejected", + "action_time": "2026-09-15T12:60:00Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-16T00:00:00Z", + "valid": false + }, + { + "name": "invalid trusted action second is rejected", + "action_time": "2026-09-15T12:03:99Z", + "publication_time": "2026-09-15T12:10:00Z", + "decided_at": "2026-09-15T12:02:00Z", + "expires_at": "2026-09-16T00:00:00Z", + "valid": false + }, + { + "name": "invalid trusted action calendar date is rejected", + "action_time": "2026-02-30T12:10:00Z", + "publication_time": "2026-02-28T12:10:00Z", + "decided_at": "2026-02-28T12:02:00Z", + "expires_at": "2026-03-01T00:00:00Z", + "valid": false } ] diff --git a/fixtures/comic-manifest-v1.md b/fixtures/comic-manifest-v1.md index 3e173cf..7444a8f 100644 --- a/fixtures/comic-manifest-v1.md +++ b/fixtures/comic-manifest-v1.md @@ -26,7 +26,9 @@ private-context projection. The [approval-time fixture matrix](comic-manifest-v1 covers inclusive decision equality, just-before and equal expiry at both action and publication time, future decisions, submillisecond boundaries, equal instants with different fractional widths, and equivalent instants using the -uppercase `T`, lowercase `t`, or a single whitespace separator accepted by the schema. +uppercase `T`, lowercase `t`, or a single whitespace separator accepted by the +schema. Trusted action-time cases cover invalid hours, minutes, seconds and +calendar dates, plus a valid leap day. The tests compose non-rendered image/PDF placeholder bytes to exercise declared profile relationships; they deliberately do not claim media signatures or rendering. Output text bytes and public dependency bytes are explicit fixture inputs and diff --git a/rfcs/0007-comic-manifest-v1.md b/rfcs/0007-comic-manifest-v1.md index e378847..e667028 100644 --- a/rfcs/0007-comic-manifest-v1.md +++ b/rfcs/0007-comic-manifest-v1.md @@ -19,8 +19,12 @@ Trusted approval-time evaluation uses an inclusive decision-time bound and an exclusive expiry bound: `decided_at <= action_time < expires_at`. The publication time named in the exact scope must also be strictly earlier than `expires_at`. Equality with `decided_at` is valid; equality with `expires_at` at either the -action or scoped publication time is expired. This records the existing -test-oracle relationship as an explicit contract rule. +action or scoped publication time is expired. Before comparison, a verifier must +validate trusted `action_time` against the candidate schema's `$defs/time` +constraints, including calendar and clock ranges; invalid values must fail closed. +Valid `T`, `t`, or single-whitespace separators and fractional seconds remain +accepted as defined by that schema. This records the existing test-oracle +relationship as an explicit contract rule. Codex owns normative meaning. Platform validates and supplies a test-only consumer proof against the existing released SDK/Builder output. Universe owns public diff --git a/specs/manifests/comic-manifest-v1.md b/specs/manifests/comic-manifest-v1.md index 880d94d..62d4dae 100644 --- a/specs/manifests/comic-manifest-v1.md +++ b/specs/manifests/comic-manifest-v1.md @@ -268,7 +268,11 @@ A binding is necessary evidence, not an authenticated grant. Trusted verificatio MUST bind the actual reviewer to the role, subject, scope and decision, check revocation and current time, and fail closed on missing evidence. Let `action_time` be the trusted time at which the authorization-dependent action is evaluated. The -time check MUST satisfy `decided_at <= action_time < expires_at`; it MUST also +verifier MUST validate `action_time` against the candidate schema's `$defs/time` +constraints, including calendar and clock ranges, before comparison. Invalid +calendar dates and out-of-range clock fields MUST fail closed. Valid `T`, `t`, or +single-whitespace separators and fractional seconds remain accepted as defined by +that schema. The time check MUST satisfy `decided_at <= action_time < expires_at`; it MUST also satisfy `scope.publication_time < expires_at` for the publication time named in the exact scope. Expiry is exclusive: an action or scoped publication at exactly `expires_at` is expired, while an action at exactly `decided_at` is valid. A diff --git a/tests/support/comic-manifest-oracle.cjs b/tests/support/comic-manifest-oracle.cjs index 516e35c..d11c11e 100644 --- a/tests/support/comic-manifest-oracle.cjs +++ b/tests/support/comic-manifest-oracle.cjs @@ -7,14 +7,29 @@ const check = (v, code) => assert.ok(v, code); const unique = (xs, code) => eq(new Set(xs).size, xs.length, code); const rank = c => ['public', 'internal', 'confidential', 'restricted'].indexOf(c); const gates = ['editorial', 'canon-continuity', 'visual-text', 'integrity', 'provenance', 'security-privacy', 'rights', 'accessibility', 'packaging']; +function parseUtcInstant(value) { + if (typeof value !== 'string' || value.length > 32) return null; + const match = /^(\d{4})-(\d{2})-(\d{2})[Tt\s](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?Z$/.exec(value); + if (!match) return null; + const [, yearText, monthText, dayText, hourText, minuteText, secondText, fraction] = match; + const year = Number(yearText), month = Number(monthText), day = Number(dayText); + const hour = Number(hourText), minute = Number(minuteText); + const second = Number(`${secondText}${fraction ? `.${fraction}` : ''}`); + const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); + const days = [0, 31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + if (month < 1 || month > 12 || day < 1 || day > days[month]) return null; + if (hour > 23 || minute > 59 || !(second < 60 || (hour === 23 && minute === 59 && second < 61))) return null; + return { + wholeSecond: `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}`, + fraction: fraction ?? '' + }; +} function compareUtcInstants(left, right) { - const parse = value => /^(\d{4}-\d{2}-\d{2})[Tt\s](\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/.exec(value); - const a = parse(left), b = parse(right); + const a = parseUtcInstant(left), b = parseUtcInstant(right); check(a && b, 'APPROVAL_TIME'); - const aWholeSecond = `${a[1]}T${a[2]}`, bWholeSecond = `${b[1]}T${b[2]}`; - if (aWholeSecond !== bWholeSecond) return aWholeSecond < bWholeSecond ? -1 : 1; - const width = Math.max(a[3]?.length ?? 0, b[3]?.length ?? 0); - const af = (a[3] ?? '').padEnd(width, '0'), bf = (b[3] ?? '').padEnd(width, '0'); + if (a.wholeSecond !== b.wholeSecond) return a.wholeSecond < b.wholeSecond ? -1 : 1; + const width = Math.max(a.fraction.length, b.fraction.length); + const af = a.fraction.padEnd(width, '0'), bf = b.fraction.padEnd(width, '0'); return af < bf ? -1 : af > bf ? 1 : 0; } const renditionProfiles = { @@ -106,7 +121,7 @@ function verifyApproval(a, subject, artifacts, scope, s) { eq(a.subject, identity(subject), 'APPROVAL_SUBJECT'); eq(a.artifact_digests, artifacts.map(x => x.artifact.sha256).sort(), 'APPROVAL_ARTIFACTS'); eq(a.scope, scope, 'APPROVAL_SCOPE'); - // Preserve arbitrary schema-valid fractional precision at each approval boundary. + // Validate trusted action time before comparing arbitrary schema-valid fractions. check(compareUtcInstants(a.decided_at, s.at) <= 0 && compareUtcInstants(s.at, a.expires_at) < 0 && compareUtcInstants(scope.publication_time, a.expires_at) < 0, 'APPROVAL_TIME'); // A test harness supplies trusted decisions OUTSIDE untrusted record bytes. From ebbff35e46fe2bb21e32f0284a481b42e58f4d50 Mon Sep 17 00:00:00 2001 From: Andrew Peris Date: Tue, 6 Oct 2026 15:55:41 -0400 Subject: [PATCH 5/5] Fail closed on unsupported leap seconds --- CHANGELOG.md | 5 +- .../comic-manifest-v1-approval-times.json | 53 +++++++++++++++++++ fixtures/comic-manifest-v1.md | 4 +- rfcs/0007-comic-manifest-v1.md | 3 ++ specs/manifests/comic-manifest-v1.md | 7 ++- tests/comic-manifest.test.cjs | 2 + tests/support/comic-manifest-oracle.cjs | 5 +- 7 files changed, 71 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 987d569..99e9b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,9 @@ compatible, or editorial. at both action and scoped publication time. The test-only oracle preserves submillisecond precision across variable fractional widths, accepts the full schema timestamp separator forms, and validates trusted action calendar/clock - fields before comparison. Synthetic fixtures cover equal instants, expiry, - future decisions and malformed action times; no released contract bytes change. + fields before comparison and fails closed on unsupported leap seconds. Synthetic + fixtures cover equal instants, expiry, future decisions and malformed action + times; no released contract bytes change. - Extended the unreleased Comic Manifest 1.0.0 candidate for Studio #94 with immutable versioned output profiles, per-output limits, exact requirement/result/ diff --git a/fixtures/comic-manifest-v1-approval-times.json b/fixtures/comic-manifest-v1-approval-times.json index 50557d4..22aced5 100644 --- a/fixtures/comic-manifest-v1-approval-times.json +++ b/fixtures/comic-manifest-v1-approval-times.json @@ -98,6 +98,7 @@ { "name": "equivalent instants across accepted timestamp separators remain valid", "action_time": "2026-09-15t12:10:00.0001000Z", + "schema_valid_action_time": true, "publication_time": "2026-09-15\t12:10:00.00010Z", "decided_at": "2026-09-15 12:10:00.0001Z", "expires_at": "2026-09-15T12:10:00.00011Z", @@ -106,14 +107,25 @@ { "name": "valid leap-day action time remains valid", "action_time": "2024-02-29t12:10:00Z", + "schema_valid_action_time": true, "publication_time": "2024-02-29 12:10:00Z", "decided_at": "2024-02-29T12:02:00Z", "expires_at": "2024-02-29T12:11:00Z", "valid": true }, + { + "name": "last ordinary second with maximum schema precision remains valid", + "action_time": "2026-09-15T23:59:59.99999999999Z", + "schema_valid_action_time": true, + "publication_time": "2026-09-15 23:59:59.99999999999Z", + "decided_at": "2026-09-15T23:59:59.99999999998Z", + "expires_at": "2026-09-16T00:00:00Z", + "valid": true + }, { "name": "action at expiry is expired across accepted timestamp separators", "action_time": "2026-09-15 12:10:00.0001000Z", + "schema_valid_action_time": true, "publication_time": "2026-09-15T12:09:59.999Z", "decided_at": "2026-09-15t12:02:00Z", "expires_at": "2026-09-15t12:10:00.0001Z", @@ -158,5 +170,46 @@ "decided_at": "2026-02-28T12:02:00Z", "expires_at": "2026-03-01T00:00:00Z", "valid": false + }, + { + "name": "invalid non-leap-year February 29 action date is rejected", + "action_time": "2025-02-29T12:10:00Z", + "publication_time": "2025-02-28T12:10:00Z", + "decided_at": "2025-02-28T12:02:00Z", + "expires_at": "2025-03-01T00:00:00Z", + "valid": false + }, + { + "name": "schema-valid midmonth leap-second action time is unsupported", + "action_time": "2026-09-15T23:59:60Z", + "schema_valid_action_time": true, + "publication_time": "2026-09-15T23:59:59Z", + "decided_at": "2026-09-15T23:59:58Z", + "expires_at": "2026-09-16T00:00:00Z", + "valid": false + }, + { + "name": "schema-valid leap-second decision cannot authorize", + "action_time": "2026-09-16T00:00:00Z", + "publication_time": "2026-09-15T23:59:59Z", + "decided_at": "2026-09-15T23:59:60Z", + "expires_at": "2026-09-16T00:01:00Z", + "valid": false + }, + { + "name": "schema-valid leap-second expiry cannot grant authority", + "action_time": "2026-09-15T23:59:59Z", + "publication_time": "2026-09-15T23:59:59Z", + "decided_at": "2026-09-15T23:59:58Z", + "expires_at": "2026-09-15T23:59:60Z", + "valid": false + }, + { + "name": "schema-valid leap-second publication time cannot authorize", + "action_time": "2026-09-15T23:59:59Z", + "publication_time": "2026-09-15T23:59:60Z", + "decided_at": "2026-09-15T23:59:58Z", + "expires_at": "2026-09-16T00:01:00Z", + "valid": false } ] diff --git a/fixtures/comic-manifest-v1.md b/fixtures/comic-manifest-v1.md index 7444a8f..15a3771 100644 --- a/fixtures/comic-manifest-v1.md +++ b/fixtures/comic-manifest-v1.md @@ -28,7 +28,9 @@ and publication time, future decisions, submillisecond boundaries, equal instants with different fractional widths, and equivalent instants using the uppercase `T`, lowercase `t`, or a single whitespace separator accepted by the schema. Trusted action-time cases cover invalid hours, minutes, seconds and -calendar dates, plus a valid leap day. +calendar dates, valid leap-day and maximum-precision ordinary-second controls, +and fail-closed behavior for schema-valid second-60 timestamps in action, +decision, expiry and publication fields. The tests compose non-rendered image/PDF placeholder bytes to exercise declared profile relationships; they deliberately do not claim media signatures or rendering. Output text bytes and public dependency bytes are explicit fixture inputs and diff --git a/rfcs/0007-comic-manifest-v1.md b/rfcs/0007-comic-manifest-v1.md index e667028..bf2d9d0 100644 --- a/rfcs/0007-comic-manifest-v1.md +++ b/rfcs/0007-comic-manifest-v1.md @@ -22,6 +22,9 @@ Equality with `decided_at` is valid; equality with `expires_at` at either the action or scoped publication time is expired. Before comparison, a verifier must validate trusted `action_time` against the candidate schema's `$defs/time` constraints, including calendar and clock ranges; invalid values must fail closed. +Approval-time comparisons support seconds `00` through `59`; a timestamp with +second `60` must fail closed for this check, even if its shape passes JSON Schema +date-time format validation. This deliberately does not infer leap-second dates. Valid `T`, `t`, or single-whitespace separators and fractional seconds remain accepted as defined by that schema. This records the existing test-oracle relationship as an explicit contract rule. diff --git a/specs/manifests/comic-manifest-v1.md b/specs/manifests/comic-manifest-v1.md index 62d4dae..24eae8d 100644 --- a/specs/manifests/comic-manifest-v1.md +++ b/specs/manifests/comic-manifest-v1.md @@ -270,9 +270,12 @@ revocation and current time, and fail closed on missing evidence. Let `action_ti be the trusted time at which the authorization-dependent action is evaluated. The verifier MUST validate `action_time` against the candidate schema's `$defs/time` constraints, including calendar and clock ranges, before comparison. Invalid -calendar dates and out-of-range clock fields MUST fail closed. Valid `T`, `t`, or +calendar dates and out-of-range clock fields MUST fail closed. Approval-time +comparisons support seconds `00` through `59`; a timestamp with second `60` MUST +fail closed for this check, even if its shape passes JSON Schema date-time format +validation. This deliberately does not infer leap-second dates. Valid `T`, `t`, or single-whitespace separators and fractional seconds remain accepted as defined by -that schema. The time check MUST satisfy `decided_at <= action_time < expires_at`; it MUST also +the schema. The time check MUST satisfy `decided_at <= action_time < expires_at`; it MUST also satisfy `scope.publication_time < expires_at` for the publication time named in the exact scope. Expiry is exclusive: an action or scoped publication at exactly `expires_at` is expired, while an action at exactly `decided_at` is valid. A diff --git a/tests/comic-manifest.test.cjs b/tests/comic-manifest.test.cjs index 79925ec..6bf8520 100644 --- a/tests/comic-manifest.test.cjs +++ b/tests/comic-manifest.test.cjs @@ -9,6 +9,7 @@ const read = path => JSON.parse(readFileSync(resolve(__dirname, '..', path))); const schema = read('schemas/json/comic-manifest/v1/comic-manifest.schema.json'); const ajv = new Ajv({ strict: true, strictRequired: false, strictTypes: false, allErrors: true }); addFormats(ajv); const validate = ajv.compile(schema), approval = ajv.compile({ $ref: schema.$id + '#/$defs/approval' }); +const timestamp = ajv.compile(schema.$defs.time); const base = read('fixtures/comic-manifest-v1.json'), foundation = read('fixtures/context-builder-v1.json'); const approvalTimeCases = read('fixtures/comic-manifest-v1-approval-times.json'); function schemaValid(s) { @@ -77,6 +78,7 @@ test('all detached approval fields are closed and exactly versioned', () => { for (const c of approvalTimeCases) test(c.name, () => { const s = structuredClone(base); s.at = c.action_time; + if (c.schema_valid_action_time !== undefined) assert.equal(timestamp(s.at), c.schema_valid_action_time); s.release.scope.publication_time = c.publication_time; for (const a of s.approvals) { a.decided_at = c.decided_at; diff --git a/tests/support/comic-manifest-oracle.cjs b/tests/support/comic-manifest-oracle.cjs index d11c11e..f02f738 100644 --- a/tests/support/comic-manifest-oracle.cjs +++ b/tests/support/comic-manifest-oracle.cjs @@ -13,12 +13,11 @@ function parseUtcInstant(value) { if (!match) return null; const [, yearText, monthText, dayText, hourText, minuteText, secondText, fraction] = match; const year = Number(yearText), month = Number(monthText), day = Number(dayText); - const hour = Number(hourText), minute = Number(minuteText); - const second = Number(`${secondText}${fraction ? `.${fraction}` : ''}`); + const hour = Number(hourText), minute = Number(minuteText), second = Number(secondText); const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); const days = [0, 31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; if (month < 1 || month > 12 || day < 1 || day > days[month]) return null; - if (hour > 23 || minute > 59 || !(second < 60 || (hour === 23 && minute === 59 && second < 61))) return null; + if (hour > 23 || minute > 59 || second > 59) return null; return { wholeSecond: `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}`, fraction: fraction ?? ''