From afe547432bf24623d237ee12b556bf41ad6000a8 Mon Sep 17 00:00:00 2001 From: Jason Cruz Falzon Date: Wed, 7 Oct 2026 06:44:46 +0100 Subject: [PATCH] docs: correct the fourth review's twenty-two statements, check stated counts in npm test, fix the font licence (DR-187, task 1.1) Every statement the fourth review found the artefacts contradicting, re-verified against the artefact first: the upgrading note's compatibility direction, the details key filter described as an approximation with its differences listed, 58 of 110 allow-list entries, seven schemas carrying schema_version, the pattern dialect (ECMA-262 compiling under RE2, not I-Regexp), reasonCode MUST NOT, /1.x/ serving main, the 1.0.x tagged commits, DR-182 Approved, review summaries in place of private links, 651 declared properties, six validators, three extensions, the CI and push-baseline wording, the two provenance populations, three NOT CHECKED package lines, enums in full on the site reference, versions page tag links for 1.0.x, the harness's empty lease journal, three :root blocks, the SPEC header and ten actor.runtime values, and the CI narrowing comment. conformance/claims-check.mjs recomputes 56 stated counts and lists in npm test, NOT CHECKED where the source is not in the package or the tags are absent; the conformance job checks out full history. The Source Sans 3 and JetBrains Mono licence texts are the upstream ones (the bundled Source Sans 3 text named Google Inc.). Description text only in the schemas; the guard reports no finding. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Jason Cruz Falzon --- .github/workflows/contract.yml | 16 +- CHANGELOG.md | 95 ++++- CONTRIBUTING.md | 13 +- GOVERNANCE.md | 4 +- README.md | 75 +++- SECURITY.md | 8 +- SPEC-agent-lease-manifest.md | 18 +- conformance/claims-check.mjs | 375 ++++++++++++++++++ conformance/run.mjs | 15 +- conformance/suite-export.mjs | 2 +- .../suite/draft7/agent-lease-manifest.json | 8 +- conformance/suite/draft7/agent-lease.json | 8 +- conformance/suite/draft7/audit-event.json | 6 +- conformance/suite/draft7/cdi-assessment.json | 2 +- conformance/suite/draft7/cdi-signal.json | 6 +- conformance/suite/draft7/config-core.json | 2 +- conformance/suite/draft7/lease-record.json | 16 +- conformance/suite/draft7/plugin-manifest.json | 248 ++++++------ .../suite/draft7/plugin-marketplace.json | 8 +- conformance/suite/draft7/provenance.json | 2 +- docs/conformance/cdf-harness.md | 5 +- docs/decisions.md | 21 +- docs/implementing.md | 11 +- docs/proposals/2026-10-05-identity-rules.md | 4 +- .../2026-10-05-tool-args-declaration.md | 3 +- docs/proposals/TEMPLATE.md | 5 +- docs/upgrading-1.0-to-1.2.md | 27 +- generate-schemas-lock.mjs | 2 +- schemas/agent-lease-manifest.schema.json | 6 +- schemas/agent-lease.schema.json | 6 +- schemas/audit-event.schema.json | 4 +- schemas/cdi-signal.schema.json | 4 +- schemas/lease-record.schema.json | 14 +- schemas/plugin-manifest.schema.json | 6 +- schemas/plugin-marketplace.schema.json | 6 +- tooling/build-site.mjs | 44 +- tooling/regex-portability/main.go | 8 +- tooling/site-reference.mjs | 24 +- tooling/site/BRAND-NOTICE.md | 9 + tooling/site/fonts/OFL-jetbrains-mono.txt | 6 +- tooling/site/fonts/OFL-source-sans-3.txt | 8 +- 41 files changed, 859 insertions(+), 291 deletions(-) create mode 100644 conformance/claims-check.mjs diff --git a/.github/workflows/contract.yml b/.github/workflows/contract.yml index 4e98b0e..c416de0 100644 --- a/.github/workflows/contract.yml +++ b/.github/workflows/contract.yml @@ -21,7 +21,11 @@ jobs: matrix: node: ['20', '22'] steps: + # Full history with tags: the claims check recomputes the guard's counts between two + # release tags, and reports them NOT CHECKED in a clone without the tags. - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-node@v4 with: @@ -32,11 +36,13 @@ jobs: # Every valid fixture accepted, every invalid one rejected with a written reason and at the # path its .expect.json names, an unknown field surviving a round trip (top level and nested), - # the canonical, narrowing and signature vectors, the lease rules, the guard's own scenarios, - # every schema compiling under Ajv strict mode and validating against the meta-schema, every - # inlined copy identical to its source, schemas/index.json valid and current, the README's - # Layout block, the SPEC traceability map, the exported suite current, and the version - # stated consistently everywhere. + # the canonical and signature vectors, the narrowing vectors checked for shape only (the + # reference adapter offers no `narrow`, so their behaviour is NOT CHECKED here; the reference + # implementation runs them), the lease rules, the guard's own scenarios, every schema + # compiling under Ajv strict mode and validating against the meta-schema, every inlined copy + # identical to its source, schemas/index.json valid and current, the README's Layout block, + # the site build, the SPEC traceability map, the exported suite current, the stated counts + # (the claims check), and the version stated consistently everywhere. - run: npm test package: diff --git a/CHANGELOG.md b/CHANGELOG.md index 478ef1b..e64aca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ tooling in this repository. self-test defect is attributed to the release candidate (`main` at e91afb0) rather than the published tarball; "eleven words" is ten in the CHANGELOG and in the `details` descriptions of `audit-event` and `cdi-signal` (description text only; the suite export regenerated, the lock and - the index unchanged); `allow.tool_args` is described as a `` of `stability: development`; + the index unchanged); `allow.tool_args` is described as a `$comment` of `stability: development`; the 1.1.0 section carries one `Added`, one `Changed` and one `Fixed`; SPEC §9 names schema set 1.2 and rule L3 (clauses 9-1 and 9-3 re-affirmed in `conformance/traceability.json`, a §13 editorial row); CONTRIBUTING lists everything `npm test` checks and which guard findings need which lock @@ -48,7 +48,7 @@ tooling in this repository. `check-additive.mjs`, `generate-schemas-lock.mjs`, `generate-contract-types.mjs` and the two workflows say what the code does today. - **The site carries the cognitivedelivery.co.uk brand** (harness DR-186): the website's design tokens - in `tooling/site.css` (one `:root` palette, no literal colour outside it), Source Sans 3 and + in `tooling/site.css` (three `:root` blocks: the website's palette, the hoisted header and footer values, and the dark scheme; no literal colour outside them), Source Sans 3 and JetBrains Mono self-hosted as variable woff2 files under the SIL Open Font Licence with its text beside them, the teal and white logos and the three favicons under `tooling/site/`, every page's header and footer carrying the logo, and `tooling/site/BRAND-NOTICE.md`, rendered at @@ -57,11 +57,67 @@ tooling in this repository. `icon` or `apple-touch-icon` only), an ``, a `` and a stylesheet `url()` only when site-internal and resolving to a written file, the one eyebrow-mark data URI by exact match, refuses `@import` and any other host, and reports the distinct targets as `assets`. +- **`npm test` checks the numbers the documents state** (harness DR-187, the fourth review of 6 + October 2026). `conformance/claims-check.mjs` holds a declarative list of claims, each a file, a + pattern capturing the stated value and a function computing the true one from the repository, + and fails on a difference or on a statement that is no longer there. It covers a new "corpus in + numbers" paragraph in the README (fixtures valid, invalid and by rule; suite tests and cases; + canonical, RFC 8785, narrowing and signature vectors; SPEC clauses and exclusions; patterns; + declared properties and `properties` keys; allow-list entries), the schemas carrying + `schema_version` in the README and the SPEC, the vector and event counts in the README, the SPEC + and `docs/`, the guard's counts between v1.1.0 and v1.2.0 in CONTRIBUTING and this file, the + 1.2.0 and 1.2.1 property and clause counts, and the tagged commits in `docs/decisions.md`. A claim + whose source is not in the package, or that needs the release tags, is reported NOT CHECKED; CI's + conformance job now checks out full history so the tags are there. `run.mjs` prints + `Claims: N checked, F failure(s).`; the README's Layout block and CONTRIBUTING describe it. +- **The site reference shows every enumeration in full**: past ten values the count is followed by + every value inside a `
` element (no script), where it used to show three examples + (`tooling/site-reference.mjs`, with self-test assertions that every hook event and every + `cdi-signal` event type is on the page). The versions page links a 1.0.x release to its tag, + labelled "no GitHub Release", because GitHub Releases start at 1.1.0 (a constant in + `tooling/build-site.mjs` read from this file's 1.1.0 entry, since the build makes no network + request); the front and versions pages say `/1.x/` serves `main`. + +### Fixed + +- **Twenty-two statements the fourth review found the artefacts contradicting** (harness DR-187), + each re-verified against the artefact before it was changed. The upgrading note had the + compatibility direction backwards (a 1.2 reader reads what a conformant 1.0 writer produced; a + 1.0 reader may reject 1.2 output, and the 1.0.2 schemas reject two current valid fixtures). The + `details` key filter is described as approximating the reference writer's rule, not mirroring it, + with the differences listed (case-sensitive; a digit ends a camelCase segment) in the README, + SECURITY, the 1.1.0 entry below and the `propertyNames` descriptions of `audit-event` and + `cdi-signal`; a later task tightens it. "110 entries cover the 115 tightenings" is 58 of the 110. + `schema_version` is carried by seven schemas, not every one (README, SPEC §6.2, the upgrading + note). The pattern dialect is an ECMA-262 subset that compiles under RE2, applied as an + unanchored search, not RFC 9485 I-Regexp (SPEC §4.4, the host, path and credential descriptions, + two proposals, the 1.1.0 entry and two code comments). The `reasonCode` description says an + issuer MUST NOT mint R7 and above, which the pattern admits. `/1.x/` serves `main`, not the + 1.2.1 tag (the 1.2.1 entry, the site's front and versions pages, the README, the suite export's + labels). `docs/decisions.md` gives the 1.0.x tagged commits rather than the annotated tag objects, + DR-182 as Approved, and a summary of each review in place of links to private documents. 666 + `properties` keys are 651 declared properties and 15 condition keys. The corpus runs under "six + validators", Ajv among them (the CI job keeps its required-check name). Three CDF extensions, not + two. The CI paragraph says which job runs on which Node version and when. The push baseline is + the pushed-from commit. The two real-record counts are two provenance populations (front-matter + blocks and journal lines). From the installed package three lines report a repository-only + check, not two. The site reference shows every enumeration. The versions page labels the 1.0.x + links as tags. The harness's real-artefact test passes an empty lease journal. The site + stylesheet has three `:root` blocks. The SPEC header dates its editorial revisions (a §13 row) + and the journal carries ten distinct `actor.runtime` values, not eleven. The CI comment says the + narrowing vectors are checked for shape only. An empty code span in the entry above is `$comment` + again (lost to shell interpolation). Description text only in the schemas: the guard reports no + finding, the lock is unchanged, the suite export and the inlined copies regenerated. +- **The Source Sans 3 licence named the wrong copyright holder.** The bundled OFL text said "Google + Inc."; the font is Adobe's, with the Reserved Font Name "Source". Both font licence texts are now + the upstream projects' own (`adobe-fonts/source-sans` `LICENSE.md`, `JetBrains/JetBrainsMono` + `OFL.txt`), and `tooling/site/BRAND-NOTICE.md` records where each came from and what differed. ## [1.2.1] — 2026-10-06 -Tooling only; the SPEC's schema set stays 1.2 (no normative change) and `/1.x/` serves the 1.2.1 -schemas, additive over 1.2.0 by one field; `schemaSetVersion` and `index.json`'s `schema_set` +Tooling only; the SPEC's schema set stays 1.2 (no normative change) and `/1.x/` served the 1.2.1 +schemas when it was tagged, additive over 1.2.0 by one field (`/1.x/` serves the schemas on `main`, +which may carry description-only changes ahead of the next tag; `/1.2.1/` is the frozen copy); `schemaSetVersion` and `index.json`'s `schema_set` follow the package version, of which only the major is load-bearing. Found by pinning the reference implementation to 1.2.0: the type generator, the Pages deploy on a tag, and the narrowing vectors being the reference's own bytes. One additive field, `capabilities.tool_args` on the plugin @@ -89,13 +145,14 @@ schemas, so `capabilities` stays the lease `allow` shape property for property. ## [1.2.0] — 2026-10-05 Schema set 1.2 (CDF spec `contract-batch-two-implement-all`, DR-183): the second review's fourteen -improvements, from a package that could not run its own test to a corpus that runs under six other -validators. **Additive within 1.x by the contract's own rule, mechanically checked**: against 1.1.0 -the guard reports 115 allow-listed tightenings, covered by 110 entries (an entry at a definition's -path covers every property in its schema that refers to it), each naming the exact value it +improvements, from a package that could not run its own test to a corpus that runs under six +validators, Ajv among them. **Additive within 1.x by the contract's own rule, mechanically checked**: against 1.1.0 +the guard reports 115 allow-listed tightenings, covered by 58 of the allow-list's 110 entries (an +entry at a definition's path covers every property in its schema that refers to it; the other 52 +entries are 1.1.0's), each naming the exact value it admits and the fixture or recorded check that proves no conformant writer ever produced what it now refuses, and every real journal in the reference deployment validates with zero rejections (33,608 audit -records, 19,231 CDI signals, 995 provenance records, 2 assessments, the six-line lease journal, the +records, 19,231 CDI signals, 995 provenance front-matter blocks, 2 assessments, the six-line lease journal, the workspace config). No byte of any existing hash or signature changes. One new schema, `lease-record`; one field at development stability, `allow.tool_args`; everything else stable. @@ -157,8 +214,8 @@ workspace config). No byte of any existing hash or signature changes. One new sc with the reference writer's first segments reserved and a vendor name for anyone else; `summary` and `reasoning` refuse any control character; a `details` string value is at most 200 characters; `actor.runtime_agent` (optional, the closed vocabulary) is added while `actor.runtime` stays open, - because the real journal spells it eleven ways; `schema_version` is `major.minor` on every schema - (the lease schemas widen from the literal `1.0`); `provenance.spec` is a slug; a CDI assessment has + because the real journal carries ten distinct values of it; `schema_version` is `major.minor` on + every schema that carries it (the lease schemas widen from the literal `1.0`); `provenance.spec` is a slug; a CDI assessment has exactly six dimensions, each id once, with integer scores; a sealed config path is dotted lower-case and the runner checks it names a field the fixture carries. Ten invalid fixtures, one valid. Every real audit record, signal, provenance record and assessment in the reference deployment validates. @@ -177,7 +234,7 @@ workspace config). No byte of any existing hash or signature changes. One new sc added where a clause had nothing to point at: a manifest with a bare-major `schema_version`, a `runtime_agent` outside the vocabulary, a model without `family`, an `allow` or `deny` missing a list, an attestation with an unknown issuer, and a valid home-relative deny path. -- **The corpus runs under six other validators.** `conformance/suite/draft7/` is the corpus in the official +- **The corpus runs under six validators.** `conformance/suite/draft7/` is the corpus in the official JSON-Schema-Test-Suite format (one file per schema, every fixture a test, written by `npm run lock` and checked current by `npm test`), and CI runs it through Bowtie against `go-jsonschema`, `rust-jsonschema`, `python-jsonschema`, `java-json-schema`, @@ -186,8 +243,9 @@ workspace config). No byte of any existing hash or signature changes. One new sc its reason in the workflow). Two orphan `componentSource` definitions the typed hook and MCP shapes had left behind are removed, and the marketplace's empty `relevance.signals` schema gained a description, both found by that lint. -- **Every property says what it promises.** All 664 declared properties (666 at 1.2.1, with - `capabilities.tool_args` on both plugin schemas) carry a `$comment` of `stability: stable` or +- **Every property says what it promises.** All 664 `properties` keys (666 at 1.2.1, with + `capabilities.tool_args` on both plugin schemas), which are 649 declared properties (651 at 1.2.1) + and 15 keys inside `if` and `contains` conditions, carry a `$comment` of `stability: stable` or `stability: development` (only `allow.tool_args` is development), with `; deprecated: ` for retiring a field. `conformance/metaschema.json` holds that and the other conventions (dialect, `$id`, title, description, no `format`) and `npm test` validates @@ -314,7 +372,7 @@ refuses, and every real journal in the reference deployment (33,557 audit record - **The README no longer claims the plugin schemas contain every key Claude Code has.** The sentence was true at 1.0.0 and stopped being true as Claude Code grew; a standing superlative about a moving target is the kind of sentence this contract exists to refuse. The README now says - what is modelled and as of which date, names the two CDF extensions (`cdf`; the `local` source + what is modelled and as of which date, names the three CDF extensions (`cdf`; the `local` source form; plugin-level `category`) so nobody mistakes them for Claude Code's, and the old wording is a banned claim in the reference implementation's documentation gate. @@ -332,8 +390,9 @@ refuses, and every real journal in the reference deployment (33,557 audit record `steering_hash` and `content_hash` require a 64-character lower-case hex digest; `details` on the audit event and the CDI signal refuses by `propertyNames` any key whose segment is one of the reference writer's ten words (`authorization`, `content`, `file`, `password`, `path`, `payload`, - `prompt`, `request`, `secret`, `token`), split on non-alphanumerics and camelCase boundaries - exactly as the writer does; `summary` is capped at 300 characters and `reasoning` at 500, the + `prompt`, `request`, `secret`, `token`), split on non-alphanumerics and camelCase boundaries, + which approximates the writer's rule (the schema is case-sensitive where the writer is not; see + the `propertyNames` descriptions for the differences); `summary` is capped at 300 characters and `reasoning` at 500, the writer's own caps. Before this a raw prompt in `request_hash` validated, which SECURITY.md itself calls a security issue. Each tightening is allow-listed with its fixture; the check against the reference deployment's journals (33,538 audit records, 5,634 signals, 4,535 provenance lines) rejects @@ -350,7 +409,7 @@ refuses, and every real journal in the reference deployment (33,557 audit record - **Path rules are written without lookahead, and refuse three forms they used to accept.** Every `read_paths`, `write_paths`, `deny.paths`, plugin `worker`, `docPacks` and `git-subdir` `path` - rule is now `allOf` of `not`/`pattern` clauses in the RFC 9485 I-Regexp subset. Go's `regexp` + rule is now `allOf` of `not`/`pattern` clauses written without lookahead in the ECMA-262 subset RE2 also compiles. Go's `regexp` (RE2) and the validators built on it could not load the old `(?!…)` rule at all, so the README's "an implementation in another language needs nothing else" was false for Go; the new `regex portability (RE2)` CI job now passes and is required. An `allow` path additionally refuses diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9323735..3f0c921 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,15 @@ SPEC traceability map (`conformance/traceability-check.mjs`), the currency of th (`conformance/suite/`), and the version statement: `package.json` (twice), the README, the CHANGELOG (a section with a body and a link definition), `schemas.lock.json`, every `$id` and the SPEC header agree, and nothing still names the old `$id` host. The version check was added -because the lock silently kept the previous version through a release. +because the lock silently kept the previous version through a release. Last, the claims check +(`conformance/claims-check.mjs`): every count and list the documents state (the corpus in numbers +in the README, the schemas that carry `schema_version`, the vector counts in the SPEC, the guard's +counts between two release tags, the tagged commits in `docs/decisions.md`) is recomputed from the +repository and compared. A statement you add with a number in it gets a claim beside it, with the +file, a pattern capturing the value and a function computing it; a number you edit is checked on +the next run; and a statement you remove fails the run until its claim goes too. +`node conformance/claims-check.mjs` lists the claims that are NOT CHECKED and why, and the file's +`EXCLUDED` list names the stated numbers it deliberately leaves alone. `npm run check:additive -- --baseline-ref origin/main` compares the schema set against the shape it had at that ref, digesting the baseline's schemas with the current generator; in CI the baseline @@ -109,7 +117,8 @@ reached through a local `$ref` is recorded with `via`, the place it lives in the (`#allow.read_paths[]` for `allow.read_paths[]`, as `schemas.lock.json` records it), and the guard accepts an entry whose `path` is either the property path or that `via`, in the same `schema`. So one entry at a definition's path covers every property in that schema that refers to it, which -is why 110 entries cover the 115 tightenings the guard reports against v1.1.0. It does not reach +is why, at v1.2.0, 58 of the allow-list's 110 entries covered the 115 tightenings the guard reports +between v1.1.0 and v1.2.0 (the other 52 entries are 1.1.0's own). It does not reach across files: the copies inlined in `agent-lease` and `lease-record` are separate schemas and carry their own entries. diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 1e0a595..9722d34 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -36,7 +36,9 @@ description, a tooling fix, a dependency bump. It goes straight to a pull reques one that is not. 2. The checks the README names, all required: the corpus on two Node versions, the additive guard against `main`, the published-package self-test, the RE2 compile, line endings, the corpus under - six other validators, and Sourcemeta's meta-schema and lint. + six validators (Ajv among them; the job keeps its required-check name, "corpus under six other + validators (Bowtie)", because renaming it would change a required check), and Sourcemeta's + meta-schema and lint. 3. The pull-request template's boxes: CHANGELOG entry, SPEC §13 row where the SPEC changes, README current, every tightening allow-listed with its evidence. 4. Review by the maintainer. A pull request from the maintainer is reviewed by the checks and the diff --git a/README.md b/README.md index 0f41883..0e309c8 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ keep about a moving target: that every key Claude Code would ever define was alr wording stopped being true as Claude Code grew, and it is now a banned claim in the reference implementation's documentation gate. -**The contract has, Claude Code lacks.** Two extensions, both named here so no one mistakes them +**The contract has, Claude Code lacks.** Three extensions, all named here so no one mistakes them for Claude Code's: - **`cdf`** on a manifest and on a marketplace entry: what the harness alone understands @@ -125,11 +125,15 @@ version change. The comparison is against the shape *before* the change, not against the lock sitting beside the schemas: a lock regenerated in the same commit agrees with whatever broke it. On a pull request -the baseline is the branch being merged into; on a push it is the previous commit. +the baseline is the branch being merged into; on a push to `main` it is the commit the push started +from (`github.event.before`), so every commit of a multi-commit push is inside the comparison. -Every artefact carries `schema_version`, and the schemas require it: a conformant writer always -writes it. A reader meeting a pre-contract artefact without it may read it as `1.0`; it must not -emit one. +Seven schemas carry `schema_version` and require it (`agent-lease-manifest`, `agent-lease`, +`audit-event`, `cdi-assessment`, `cdi-signal`, `lease-record` and `provenance`, in its inner record), +so a conformant writer of those artefacts always writes it. `config-core`, `plugin-manifest` and +`plugin-marketplace` do not carry it: the first is a section of a product's config file and the +plugin files are Claude Code's format. A reader meeting a pre-contract artefact without it may read +it as `1.0`; it must not emit one. Since 1.2 every declared property says which promise it is under: a `$comment` of `stability: stable` (held to the additive rule) or `stability: development` (may change within the @@ -156,8 +160,9 @@ You can also use it straight from this repository, as a submodule or a clone pin `schemas/` and `fixtures/` are plain files and an implementation in another language needs nothing else. Each schema is also served at its `$id` (`https://cognitive-delivery.github.io/contract/1.x/`), so a validator that resolves -identifiers finds the current 1.x schema there; the version a document was written against is its -own `schema_version` field. +identifiers finds the 1.x schema there as it is on `main`, which may carry description-only changes +ahead of the next tag; the version a document was written against is its own `schema_version` +field. Each release is also served frozen at `https://cognitive-delivery.github.io/contract//` (`/1.1.0/`, `/1.2.0/`, `/1.2.1/`, …), byte for byte as tagged and never rewritten, so a reader that pinned a version can fetch exactly what it shipped; and `schemas/index.json`, served beside both, lists every @@ -228,13 +233,16 @@ npm ci npm test # the corpus (valid, invalid at the named path, round trip), the canonical, # narrowing and signature vectors, the lease rules, the guard's own scenarios, # the schema and meta-schema checks, the Layout block, traceability, suite and - # index currency and the version statement, against the reference adapter + # index currency, the version statement and the stated counts (claims), + # against the reference adapter npm run check:additive # the lock is current, and this change is additive ``` -Both run in CI on every push and pull request, against Node 20 and 22, beside a job that compiles -every pattern under RE2. The point of a corpus is that a third party can check the claim, so the -check has to be runnable by someone who has never seen the product. +In CI, on every pull request and every push to `main`, `npm test` runs on Node 20 and 22 and the +additive guard runs on Node 22 (against the base branch of a pull request, or the commit a push +started from), beside a job that compiles every pattern under RE2. The point of a corpus is that a +third party can check the claim, so the check has to be runnable by someone who has never seen the +product. Since 1.2 the claim that "a reader in any language needs nothing else" is checked rather than made. `conformance/suite/draft7/` carries the corpus in the official @@ -257,6 +265,19 @@ through `exports`, so `@cognitive-delivery/contract/conformance/narrowing-vector From the tarball the additive guard's scenarios are reported as not present rather than passed, because the guard ships with the repository. +### The corpus in numbers + +At this commit the corpus holds **40** valid fixtures, **98** invalid fixtures and **4** by-rule +fixtures, which the exported suite carries as **142** tests in **11** cases. Beside them are **9** +canonical-bytes vectors with **2** values that must fail to serialise, RFC 8785's own **6**, **28** +narrowing vectors and **3** signature vectors. The SPEC has **87** normative clauses, **16** of them +excluded from the traceability map with a reason. The schemas carry **232** patterns, every one +compiled under RE2 in CI, and **651** declared properties; counted as `properties` keys there are +**666**, because **15** keys sit inside `if` and `contains` conditions, and every one of the 666 +carries a stability comment. `compat-allowlist.json` holds **110** entries. These are checked, not +remembered: `conformance/claims-check.mjs` recomputes every number in this paragraph, and the other +counts the documents state, from the repository in `npm test`, and fails when one is stale. + ### Signature vectors and rejection places `conformance/signature-vectors.json` names lease fixtures signed with the published **test key** @@ -351,22 +372,30 @@ anything that reaches it is effectively permanent. exactly those two shapes. - The actor records a **kind** — human, agent or system — and the model. Never a name, email or git identity. -- `details` is sanitised before write, and the schema enforces the same rule: a key whose segment - (split on non-alphanumerics and camelCase boundaries) is `authorization`, `content`, `file`, - `password`, `path`, `payload`, `prompt`, `request`, `secret` or `token` is refused. Values are - flat scalars. `summary` is capped at 300 characters and `reasoning` at 500. Renaming a sensitive +- `details` is sanitised before write, and the schema approximates the writer's rule: a key whose + lower-case segment (split on non-alphanumerics) or camelCase segment is `authorization`, + `content`, `file`, `password`, `path`, `payload`, `prompt`, `request`, `secret` or `token` is + refused. It is an approximation, not a mirror: the writer lower-cases a key before splitting it and + the schema does not, so the schema admits `Authorization`, `API_TOKEN`, `TokenCount` and `x-Token`, + which the writer drops; and the schema ends a camelCase segment at a digit, so it refuses + `apiToken2`, which the writer keeps. The `propertyNames` description lists the differences. Values + are flat scalars. `summary` is capped at 300 characters and `reasoning` at 500. Renaming a sensitive field to evade the list defeats the control. Since 1.2 the evidence schemas also hold the hygiene the reference writer already kept: an audit `event_type` is lower-case dotted segments with the writer's first segments reserved and a vendor name for anyone else; `summary` and `reasoning` carry no control character; a `details` value is at -most 200 characters; `schema_version` is `major.minor` everywhere and compared on the major; a +most 200 characters; `schema_version` is `major.minor` wherever it is carried and compared on the +major; a provenance `spec` is a slug; an assessment has exactly six integer-scored dimensions; a sealed config path is well-formed and names a field the file carries. `actor.runtime` stays open, because the real -journal spells it eleven ways, and `actor.runtime_agent` carries the closed identity. Every one of -the reference deployment's records validates under these rules: 33,608 audit records, 19,231 signals, -995 provenance records and 2 assessments at the 5 October check (`fixtures/evidence/`), and 33,665, -19,287, 4,588 and 2 at the 1.2.1 pin on 6 October (`docs/conformance/cdf-harness.md`). +journal carries ten distinct values of it (and some records omit it), and `actor.runtime_agent` +carries the closed identity. Every one of the reference deployment's records validates under these +rules, counted twice over two different provenance populations: at the 5 October check +(`fixtures/evidence/`), 33,608 audit records, 19,231 signals, 995 provenance front-matter blocks (the +`cdf` block of every Markdown file under `.cdf/specs/`) and 2 assessments; at the 1.2.1 pin on 6 +October (`docs/conformance/cdf-harness.md`), 33,665 audit records, 19,287 signals, 4,588 provenance +journal lines (every line of every `.cdf/specs/*/provenance.jsonl`) and 2 assessments. ## The site @@ -377,7 +406,8 @@ upgrade a writer); the schema reference, one page per schema walked from the sch property's description, constraints and stability; the SPEC, this README, GOVERNANCE, CONTRIBUTING, SECURITY and the CHANGELOG as pages; the implementing guide, the upgrading note and the decision index; the conformance page with the claim rule, the implementation reports and the recorded portability -results; the proposals; and the versions, each frozen copy linked to its release. The schema bytes at +results; the proposals; and the versions, each frozen copy linked to its GitHub Release, or to its +tag for the 1.0.x releases, which were cut before the release workflow created GitHub Releases. The schema bytes at every `$id` and under each `//` are laid out by the workflow and never touched by the generator. No page carries a script or reaches another host. `conformance/site-check.mjs` builds the site in `npm test` and fails on a broken link, a changed schema byte, a script tag, a resource from @@ -418,9 +448,10 @@ conformance/ the reference runner and the vectors; takes an adapt spec-clauses.mjs extracts every normative clause of the SPEC with a stable id and a drift key traceability.json every clause mapped to the fixture, vector, check or rule that tests it, or excluded with a reason traceability-check.mjs fails `npm test` on an unmapped, stale or edited clause, or a mapping that names nothing + claims-check.mjs every count and list the documents state, recomputed from the repository; NOT CHECKED per claim when its source is not in the package suite-export.mjs builds the corpus in the official JSON-Schema-Test-Suite format, and says when the export is stale schema-index.mjs builds schemas/index.json (file, $id, title, dialect, fileMatch) and says when it is stale - suite/ that export under `draft7/` (Bowtie reads the dialect from the directory name); what six other validators run + suite/ that export under `draft7/` (Bowtie reads the dialect from the directory name); what the six validators, Ajv among them, run changelog.mjs one release's CHANGELOG section, which the GitHub Release's notes come from guard-tests.mjs the additive guard's own scenarios; reported absent in the published package canonical-vectors.json the canonical-bytes vectors of SPEC section 7, in pure ASCII diff --git a/SECURITY.md b/SECURITY.md index 41604f7..d3c9b34 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,9 +18,11 @@ the contract itself permits something it should not: and retained indefinitely, so anything that reaches it is effectively permanent. Prompts are never recorded; `prompt_hash`, `request_hash`, `steering_hash` and `content_hash` are SHA-256 and the schemas refuse any other shape; `workspace_id` is a hash of the git remote URL or a - per-checkout UUID; `details` keys whose segment is one of `authorization`, `content`, `file`, - `password`, `path`, `payload`, `prompt`, `request`, `secret` or `token` are refused by the - schema; `summary` and `reasoning` are capped. The actor records a kind and a model, never a + per-checkout UUID; `details` keys whose lower-case or camelCase segment is one of + `authorization`, `content`, `file`, `password`, `path`, `payload`, `prompt`, `request`, `secret` + or `token` are refused by the schema, which approximates the reference writer's case-insensitive + rule rather than mirroring it (the schema admits `Authorization` and `API_TOKEN`; the + `propertyNames` description lists every difference); `summary` and `reasoning` are capped. The actor records a kind and a model, never a name, email or git identity. A shape that lets identifying data into a permanent record is a security issue, not a style one. diff --git a/SPEC-agent-lease-manifest.md b/SPEC-agent-lease-manifest.md index 7bd0fcd..42c9631 100644 --- a/SPEC-agent-lease-manifest.md +++ b/SPEC-agent-lease-manifest.md @@ -1,6 +1,6 @@ # The Agent Lease Manifest -**Version 1.2 · Schema set 1.2 · 2026-10-05** +**Version 1.2 · Schema set 1.2 · 2026-10-05, with editorial revisions of 2026-10-06 (§13)** A portable way for a software agent to declare what it needs, for a host to answer with a signed grant that is never wider than what it was given, and for both halves to be recorded so a third @@ -128,8 +128,9 @@ that has any of these five forms, and the published schemas reject them too: | a drive-letter prefix | `C:/Users/x` | absolute on Windows, invisible to a slash-only check | | any backslash | `..\x`, `C:\Users\x` | never a separator in a POSIX glob; refused rather than normalised | -The schema expresses the rule as `allOf` of `not`/`pattern` clauses, each in the RFC 9485 I-Regexp -subset, with no lookahead. That is deliberate: Go's `regexp` and every validator built on it reject +The schema expresses the rule as `allOf` of `not`/`pattern` clauses, each an ECMA-262 regular +expression in the subset that also compiles under RE2 (no lookaround, no backreferences), applied as +an unanchored search as JSON Schema specifies. That is deliberate: Go's `regexp` and every validator built on it reject lookahead, and a schema set a Go implementation cannot load is not portable whatever its README says. The contract's CI compiles every pattern under RE2. A `.` segment (`./x`, `src/./x`) and a percent-encoded sequence (`%2e%2e/x`) are **not** refused by pattern: a glob is not a URL, and the @@ -145,7 +146,7 @@ can carry a conforming relative path outside the root. **Identity of a host, a command and a tool** (schema set 1.2). Two conforming implementations **MUST NOT** disagree about what an entry names, so each list has one identity rule, enforced by -the schemas as a pattern in the I-Regexp subset: +the schemas as a pattern in that same subset (ECMA-262, compiling under RE2, an unanchored search): | List | An entry is | Refused | |---|---|---| @@ -335,7 +336,9 @@ to check, so a reader with one record and no journal cannot check it and **MUST ### 6.2 Reading evidence -Every schema in the set carries `schema_version` as `major.minor` (schema set 1.2). A reader +Seven schemas in the set carry `schema_version` as `major.minor` (schema set 1.2): `agent-lease-manifest`, +`agent-lease`, `audit-event`, `cdi-assessment`, `cdi-signal`, `lease-record` and `provenance` (in its +inner record); `config-core`, `plugin-manifest` and `plugin-marketplace` do not carry it. A reader **MUST** compare the major only: within a major every change is additive, so a minor the reader has not met is a record with fields it may ignore, never one it may refuse. @@ -347,8 +350,8 @@ first segments are reserved for its vocabulary (`agent`, `architecture`, `artefa `tool`, `ux`, `work`, `workbench`, `workspace`); any other writer **MUST** use its vendor name as the first segment, so two writers in one journal cannot collide. An audit event **MAY** carry `actor.runtime_agent` from the closed vocabulary of §4.2; `actor.runtime` is a label and stays open, -because the real journal carries eleven spellings of it and a reader keys identity on -`runtime_agent`. +because the reference deployment's journal carries ten distinct values of it (and some records omit +it) and a reader keys identity on `runtime_agent`. ## 7. Canonical bytes @@ -566,6 +569,7 @@ is an additive change; removing or redefining one is not. | Version | Date | Change | |---|---|---| +| 1.2 | 2026-10-06 | Editorial, no normative change: §4.4 names the pattern dialect as ECMA-262 compiling under RE2 and applied as an unanchored search, not RFC 9485 I-Regexp, whose implicitly anchored matching the schemas' `^`/`$` patterns do not follow; §6.2 names the seven schemas that carry `schema_version` and counts the reference journal's `actor.runtime` values as ten; the header dates the editorial revisions | | 1.2 | 2026-10-06 | Editorial, no normative change: §9 names schema set 1.2 (any 1.x set under §10) rather than 1.0, and names rule L3 of §6.1 beside L1 and L2 with its journal condition and the `rules(lease, context)` adapter hook | | 1.2 | 2026-10-05 | §4.4 adds `allow.tool_args`, a per-tool argument-schema declaration with development stability that an issuer MAY omit from the grant and MUST NOT treat as authority; the vector `tool-args-dropped` shows the reference dropping it; §11 cites Progent | | 1.2 | 2026-10-05 | §6.2 states that `schema_version` is `major.minor` everywhere and compared on the major only; reserves the reference writer's audit `event_type` first segments and gives other writers the vendor-name rule; adds `actor.runtime_agent` to the audit event while `actor.runtime` stays open | diff --git a/conformance/claims-check.mjs b/conformance/claims-check.mjs new file mode 100644 index 0000000..87833f3 --- /dev/null +++ b/conformance/claims-check.mjs @@ -0,0 +1,375 @@ +/** + * The claims check: every count and list the documents state, recomputed from the repository. + * + * The fourth review of 6 October 2026 found twenty-two statements the artefacts contradicted, and + * most were numbers or lists that had been true once: "110 entries cover the 115 tightenings" (58 + * do), "every schema carries `schema_version`" (seven of ten), "eleven spellings" (ten values). + * A number in prose is a fact with a short half-life unless something recomputes it, so this file + * holds a declarative list of claims, each `{ id, file, pattern, compute }`: the file the statement + * lives in, a regular expression whose one capture group is the stated value, and a function that + * computes the true value from the repository. `npm test` fails when the two differ, and when the + * pattern no longer finds its statement, because a claim that silently stopped matching is a check + * that silently stopped running. + * + * Stated values may be digits (`1,234`), number words (`seven`, `thirty-three`) or a list of + * backticked names; the comparison is on the parsed value. A claim whose source file or inputs are + * not present (the published package ships no CONTRIBUTING, `docs/`, allow-list, guard or git + * history) is reported NOT CHECKED with its reason rather than passed. Claims about a release are + * computed from that release's tag, so they stay true as `main` moves on, and need a clone with + * tags (CI's conformance job checks out full history for this). + * + * `EXCLUDED` lists the stated numbers deliberately not checked here, each with its reason. + */ + +import { execFileSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { extractClauses } from './spec-clauses.mjs'; + +const here = dirname(fileURLToPath(import.meta.url)); + +/** A claim that cannot be computed where it runs; reported NOT CHECKED with the reason. */ +class NotAvailable extends Error {} + +// --------------------------------------------------------------------------------------------- +// Parsing a stated value. +// --------------------------------------------------------------------------------------------- + +const UNITS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', + 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen']; +const TENS = { twenty: 20, thirty: 30, forty: 40, fifty: 50, sixty: 60, seventy: 70, eighty: 80, ninety: 90 }; + +/** `1,234`, `seven`, `Seven` or `thirty-three` as a number; null when it is none of those. */ +export function parseCount(text) { + const t = String(text).trim().toLowerCase(); + if (/^\d{1,3}(,\d{3})*$|^\d+$/.test(t)) return Number(t.replace(/,/g, '')); + if (UNITS.includes(t)) return UNITS.indexOf(t); + if (Object.hasOwn(TENS, t)) return TENS[t]; + const m = /^([a-z]+)-([a-z]+)$/.exec(t); + if (m && Object.hasOwn(TENS, m[1]) && UNITS.indexOf(m[2]) > 0 && UNITS.indexOf(m[2]) < 10) return TENS[m[1]] + UNITS.indexOf(m[2]); + return null; +} + +/** The backticked names in a stated list, sorted: "`a`, `b` and `c`" is ['a', 'b', 'c']. */ +export function parseNames(text) { + return [...String(text).matchAll(/`([^`]+)`/g)].map((m) => m[1]).sort(); +} + +// --------------------------------------------------------------------------------------------- +// Computing a true value. +// --------------------------------------------------------------------------------------------- + +function makeContext(root) { + const cache = new Map(); + const memo = (key, fn) => { + if (!cache.has(key)) cache.set(key, fn()); + return cache.get(key); + }; + + const need = (path) => { + if (!existsSync(join(root, path))) throw new NotAvailable(`${path} is not present (the published package does not ship it)`); + }; + const text = (path) => { need(path); return readFile(join(root, path), 'utf8'); }; + const json = async (path) => JSON.parse(await text(path)); + + // Git, only when `root` is itself the top of a clone: from an installed package inside someone + // else's repository, `git` would answer about that repository instead. + const gitRoot = memo('gitRoot', () => { + try { + const top = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); + return resolve(top) === resolve(root); + } catch { return false; } + }); + const git = (args) => { + if (!gitRoot) throw new NotAvailable('not a git clone of this repository'); + return execFileSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] }); + }; + const needRef = (ref) => { + try { git(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]); } catch (error) { + if (error instanceof NotAvailable) throw error; + throw new NotAvailable(`${ref} is not in this clone (a shallow checkout has no tags)`); + } + }; + const showAt = (ref, path) => { needRef(ref); return git(['show', `${ref}:${path}`]); }; + + /** `{ file: schema }` for every `*.schema.json` at a ref, or in the working tree. */ + const schemasAt = (ref) => memo(`schemas:${ref}`, async () => { + const out = {}; + if (ref === null) { + for (const f of (await readdir(join(root, 'schemas'))).filter((n) => n.endsWith('.schema.json')).sort()) { + out[f] = JSON.parse(await readFile(join(root, 'schemas', f), 'utf8')); + } + return out; + } + needRef(ref); + for (const f of git(['ls-tree', '--name-only', `${ref}:schemas`]).split('\n').filter((n) => n.endsWith('.schema.json')).sort()) { + out[f] = JSON.parse(showAt(ref, `schemas/${f}`)); + } + return out; + }); + + /** The guard's comparison between two release tags, with the allow-list as it was at `current`. */ + const guardBetween = (baselineRef, currentRef) => memo(`guard:${baselineRef}:${currentRef}`, async () => { + need('check-additive.mjs'); + need('generate-schemas-lock.mjs'); + const { compare, applyAllowlist } = await import(pathToFileURL(join(root, 'check-additive.mjs')).href); + const { buildLock } = await import(pathToFileURL(join(root, 'generate-schemas-lock.mjs')).href); + const lockAt = async (ref) => { + const dir = await mkdtemp(join(tmpdir(), 'cdf-claims-')); + try { + needRef(ref); + for (const f of git(['ls-tree', '--name-only', `${ref}:schemas`]).split('\n').filter((n) => n.endsWith('.json'))) { + await writeFile(join(dir, f), showAt(ref, `schemas/${f}`), 'utf8'); + } + return await buildLock({ schemaDir: dir, version: ref }); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }; + const allowlist = JSON.parse(showAt(currentRef, 'compat-allowlist.json')); + const { findings } = compare(await lockAt(baselineRef), await lockAt(currentRef)); + const { accepted, unlisted } = applyAllowlist(findings, allowlist); + return { + findings: findings.length, + accepted: accepted.length, + unlisted: unlisted.length, + entriesUsed: new Set(accepted.map((f) => f.entry)).size, + entries: allowlist.entries.length, + }; + }); + + return { root, text, json, git, needRef, showAt, schemasAt, guardBetween }; +} + +/** Files under a fixtures directory that are fixtures, not their `.expect.json` or `.reason`. */ +async function fixtureCount(root, dir) { + return (await readdir(join(root, 'fixtures', dir))).filter((f) => f.endsWith('.json') && !f.endsWith('.expect.json')).length; +} + +/** Every string-valued `pattern` keyword, counted as `tooling/regex-portability` counts them. */ +function countPatterns(node) { + if (Array.isArray(node)) return node.reduce((n, item) => n + countPatterns(item), 0); + if (!node || typeof node !== 'object') return 0; + let n = typeof node.pattern === 'string' ? 1 : 0; + for (const [k, v] of Object.entries(node)) if (k !== 'pattern') n += countPatterns(v); + return n; +} + +/** + * `properties` keys: all of them, and the declared ones, which are those outside `if`, `contains`, + * `not` and `propertyNames` bodies (conditions, not declarations; conformance/metaschema.json and + * the site reference count the same way, independently of this). + */ +export function countPropertyKeys(node, inCondition = false) { + if (Array.isArray(node)) { + return node.reduce((acc, item) => { + const c = countPropertyKeys(item, inCondition); + return { all: acc.all + c.all, declared: acc.declared + c.declared }; + }, { all: 0, declared: 0 }); + } + if (!node || typeof node !== 'object') return { all: 0, declared: 0 }; + let all = 0; + let declared = 0; + for (const [k, v] of Object.entries(node)) { + if (k === 'properties' && v && typeof v === 'object' && !Array.isArray(v)) { + const n = Object.keys(v).length; + all += n; + if (!inCondition) declared += n; + for (const child of Object.values(v)) { + const c = countPropertyKeys(child, inCondition); + all += c.all; + declared += c.declared; + } + continue; + } + const c = countPropertyKeys(v, inCondition || ['if', 'contains', 'not', 'propertyNames'].includes(k)); + all += c.all; + declared += c.declared; + } + return { all, declared }; +} + +function sumKeys(schemas) { + return Object.values(schemas).reduce((acc, s) => { + const c = countPropertyKeys(s); + return { all: acc.all + c.all, declared: acc.declared + c.declared }; + }, { all: 0, declared: 0 }); +} + +/** The schemas that declare a `schema_version` property anywhere, by name without `.schema.json`. */ +function schemasWithSchemaVersion(schemas) { + const declares = (node) => { + if (Array.isArray(node)) return node.some(declares); + if (!node || typeof node !== 'object') return false; + if (node.properties && typeof node.properties === 'object' && Object.hasOwn(node.properties, 'schema_version')) return true; + return Object.values(node).some(declares); + }; + return Object.entries(schemas).filter(([, s]) => declares(s)).map(([f]) => f.replace(/\.schema\.json$/, '')).sort(); +} + +async function clausesAt(ctx, ref) { + const spec = ref === null ? await ctx.text('SPEC-agent-lease-manifest.md') : ctx.showAt(ref, 'SPEC-agent-lease-manifest.md'); + const map = ref === null + ? JSON.parse(await readFile(join(here, 'traceability.json'), 'utf8')) + : JSON.parse(ctx.showAt(ref, 'conformance/traceability.json')); + const clauses = extractClauses(spec); + const excluded = clauses.filter((c) => typeof map.clauses?.[c.id]?.excluded === 'string').length; + return { clauses: clauses.length, excluded }; +} + +async function suiteCounts(root) { + const dir = join(root, 'conformance', 'suite', 'draft7'); + let cases = 0; + let tests = 0; + for (const f of (await readdir(dir)).filter((n) => n.endsWith('.json'))) { + const list = JSON.parse(await readFile(join(dir, f), 'utf8')); + cases += list.length; + for (const c of list) tests += c.tests.length; + } + return { cases, tests }; +} + +const shortCommit = (ctx, tag) => { ctx.needRef(tag); return ctx.git(['rev-parse', '--short=7', `${tag}^{commit}`]).trim(); }; +const shortObject = (ctx, tag) => { ctx.needRef(tag); return ctx.git(['rev-parse', '--short=7', tag]).trim(); }; + +// --------------------------------------------------------------------------------------------- +// The claims. `pattern` has one capture group: the stated value. `kind` is 'count' (the default), +// 'names' (a backticked list, compared as a sorted set) or 'text' (compared verbatim). +// --------------------------------------------------------------------------------------------- + +const vectorsLength = (file, key) => async (ctx) => (await ctx.json(`conformance/${file}`))[key].length; +const enumLength = (file, pick) => async (ctx) => pick((await ctx.schemasAt(null))[file]).length; + +export const CLAIMS = [ + // README, "The corpus in numbers": the current figures, all of them recomputed. + { id: 'readme-valid-fixtures', file: 'README.md', pattern: /holds \*\*(\d[\d,]*)\*\* valid fixtures/, compute: (ctx) => fixtureCount(ctx.root, 'valid') }, + { id: 'readme-invalid-fixtures', file: 'README.md', pattern: /\*\*(\d[\d,]*)\*\* invalid fixtures/, compute: (ctx) => fixtureCount(ctx.root, 'invalid') }, + { id: 'readme-by-rule-fixtures', file: 'README.md', pattern: /\*\*(\d[\d,]*)\*\* by-rule\s+fixtures/, compute: (ctx) => fixtureCount(ctx.root, 'invalid-by-rule') }, + { id: 'readme-suite-tests', file: 'README.md', pattern: /suite carries as \*\*(\d[\d,]*)\*\* tests/, compute: async (ctx) => (await suiteCounts(ctx.root)).tests }, + { id: 'readme-suite-cases', file: 'README.md', pattern: /tests in \*\*(\d[\d,]*)\*\* cases/, compute: async (ctx) => (await suiteCounts(ctx.root)).cases }, + { id: 'readme-canonical-vectors', file: 'README.md', pattern: /Beside them are \*\*(\d+)\*\*\s+canonical-bytes vectors/, compute: vectorsLength('canonical-vectors.json', 'vectors') }, + { id: 'readme-canonical-must-fail', file: 'README.md', pattern: /with \*\*(\d+)\*\* values that must fail to serialise/, compute: vectorsLength('canonical-vectors.json', 'must_fail') }, + { id: 'readme-jcs-vectors', file: 'README.md', pattern: /RFC 8785's own \*\*(\d+)\*\*/, compute: async (ctx) => (await readdir(join(ctx.root, 'conformance', 'jcs', 'input'))).filter((f) => f.endsWith('.json')).length }, + { id: 'readme-narrowing-vectors', file: 'README.md', pattern: /\*\*(\d+)\*\*\s+narrowing vectors/, compute: vectorsLength('narrowing-vectors.json', 'vectors') }, + { id: 'readme-signature-vectors', file: 'README.md', pattern: /\*\*(\d+)\*\* signature vectors/, compute: vectorsLength('signature-vectors.json', 'vectors') }, + { id: 'readme-spec-clauses', file: 'README.md', pattern: /The SPEC has \*\*(\d+)\*\* normative clauses/, compute: async (ctx) => (await clausesAt(ctx, null)).clauses }, + { id: 'readme-spec-excluded', file: 'README.md', pattern: /\*\*(\d+)\*\* of them\s+excluded from the traceability map/, compute: async (ctx) => (await clausesAt(ctx, null)).excluded }, + { id: 'readme-patterns', file: 'README.md', pattern: /The schemas carry \*\*(\d+)\*\* patterns/, compute: async (ctx) => Object.values(await ctx.schemasAt(null)).reduce((n, s) => n + countPatterns(s), 0) }, + { id: 'readme-declared-properties', file: 'README.md', pattern: /\*\*(\d+)\*\* declared properties; counted/, compute: async (ctx) => sumKeys(await ctx.schemasAt(null)).declared }, + { id: 'readme-properties-keys', file: 'README.md', pattern: /`properties` keys there are\s+\*\*(\d+)\*\*/, compute: async (ctx) => sumKeys(await ctx.schemasAt(null)).all }, + { id: 'readme-condition-keys', file: 'README.md', pattern: /because \*\*(\d+)\*\* keys sit inside/, compute: async (ctx) => { const c = sumKeys(await ctx.schemasAt(null)); return c.all - c.declared; } }, + { id: 'readme-allowlist-entries', file: 'README.md', pattern: /`compat-allowlist\.json` holds \*\*(\d+)\*\* entries/, compute: async (ctx) => (await ctx.json('compat-allowlist.json')).entries.length }, + + // README, elsewhere. + { id: 'readme-schema-version-count', file: 'README.md', pattern: /(\w+) schemas carry `schema_version` and require it/, compute: async (ctx) => schemasWithSchemaVersion(await ctx.schemasAt(null)).length }, + { id: 'readme-schema-version-list', file: 'README.md', kind: 'names', pattern: /carry `schema_version` and require it \(([^)]*)\)/, compute: async (ctx) => schemasWithSchemaVersion(await ctx.schemasAt(null)) }, + { id: 'readme-event-type-enum', file: 'README.md', pattern: /closed enum of (\d+) values/, compute: enumLength('cdi-signal.schema.json', (s) => s.properties.event_type.enum) }, + { id: 'readme-hook-events', file: 'README.md', pattern: /\(the ([\w-]+) events the hooks reference lists/, compute: enumLength('plugin-manifest.schema.json', (s) => s.definitions.hooksMap.propertyNames.enum) }, + { id: 'readme-canonical-more', file: 'README.md', pattern: /`conformance\/canonical-vectors\.json` (\w+) more/, compute: vectorsLength('canonical-vectors.json', 'vectors') }, + { id: 'readme-jcs-own', file: 'README.md', pattern: /carries RFC 8785's own (\w+) reference vectors/, compute: async (ctx) => (await readdir(join(ctx.root, 'conformance', 'jcs', 'input'))).filter((f) => f.endsWith('.json')).length }, + { id: 'readme-canonical-fail', file: 'README.md', pattern: /plus (\w+) values that must \*fail\*/, compute: vectorsLength('canonical-vectors.json', 'must_fail') }, + { id: 'readme-canonical-all', file: 'README.md', pattern: /the runner checks all (\w+);/, compute: async (ctx) => (await ctx.json('conformance/canonical-vectors.json')).vectors.length + (await readdir(join(ctx.root, 'conformance', 'jcs', 'input'))).filter((f) => f.endsWith('.json')).length }, + + // The SPEC. + { id: 'spec-schema-version-count', file: 'SPEC-agent-lease-manifest.md', pattern: /(\w+) schemas in the set carry `schema_version`/, compute: async (ctx) => schemasWithSchemaVersion(await ctx.schemasAt(null)).length }, + { id: 'spec-schema-version-list', file: 'SPEC-agent-lease-manifest.md', kind: 'names', pattern: /carry `schema_version` as `major\.minor` \(schema set [\d.]+\): ([\s\S]*?); `config-core`/, compute: async (ctx) => schemasWithSchemaVersion(await ctx.schemasAt(null)) }, + { id: 'spec-canonical-vectors', file: 'SPEC-agent-lease-manifest.md', pattern: /`conformance\/canonical-vectors\.json` carries (\w+) vectors/, compute: vectorsLength('canonical-vectors.json', 'vectors') }, + { id: 'spec-canonical-fail', file: 'SPEC-agent-lease-manifest.md', pattern: /plus (\w+) cases that \*\*MUST\*\* fail to serialise/, compute: vectorsLength('canonical-vectors.json', 'must_fail') }, + { id: 'spec-jcs', file: 'SPEC-agent-lease-manifest.md', pattern: /carries RFC 8785's own (\w+) reference vectors/, compute: async (ctx) => (await readdir(join(ctx.root, 'conformance', 'jcs', 'input'))).filter((f) => f.endsWith('.json')).length }, + { id: 'spec-lease-events', file: 'SPEC-agent-lease-manifest.md', pattern: /(\w+) events, with what each \*\*MUST\*\*/, compute: enumLength('lease-record.schema.json', (s) => s.properties.event.enum) }, + + // docs/ (repository only). + { id: 'implementing-canonical', file: 'docs/implementing.md', pattern: /each of the (\w+) vectors in `canonical-vectors\.json`/, compute: vectorsLength('canonical-vectors.json', 'vectors') }, + { id: 'implementing-jcs', file: 'docs/implementing.md', pattern: /the (\w+) inputs under `jcs\/input\/`/, compute: async (ctx) => (await readdir(join(ctx.root, 'conformance', 'jcs', 'input'))).filter((f) => f.endsWith('.json')).length }, + { id: 'implementing-must-fail', file: 'docs/implementing.md', pattern: /and (\w+) values that must fail \(`NaN`/, compute: vectorsLength('canonical-vectors.json', 'must_fail') }, + { id: 'upgrading-lease-events', file: 'docs/upgrading-1.0-to-1.2.md', pattern: /one line per decision, (\w+) events/, compute: enumLength('lease-record.schema.json', (s) => s.properties.event.enum) }, + { id: 'decisions-1.0.x-commits', file: 'docs/decisions.md', kind: 'text', pattern: /\| 1\.0\.0, 1\.0\.1 and 1\.0\.2 \| [\d-]+ \| ([0-9a-f]{7}, [0-9a-f]{7}, [0-9a-f]{7}) \(annotated/, compute: (ctx) => ['v1.0.0', 'v1.0.1', 'v1.0.2'].map((t) => shortCommit(ctx, t)).join(', ') }, + { id: 'decisions-1.0.x-tag-objects', file: 'docs/decisions.md', kind: 'text', pattern: /\(annotated tags ([0-9a-f]{7}, [0-9a-f]{7}, [0-9a-f]{7})\)/, compute: (ctx) => ['v1.0.0', 'v1.0.1', 'v1.0.2'].map((t) => shortObject(ctx, t)).join(', ') }, + { id: 'decisions-1.1.0-commit', file: 'docs/decisions.md', kind: 'text', pattern: /\| 1\.1\.0 \| [\d-]+ \| ([0-9a-f]{7}) \|/, compute: (ctx) => shortCommit(ctx, 'v1.1.0') }, + { id: 'decisions-1.2.0-commit', file: 'docs/decisions.md', kind: 'text', pattern: /\| 1\.2\.0 \| [\d-]+ \| ([0-9a-f]{7}) \|/, compute: (ctx) => shortCommit(ctx, 'v1.2.0') }, + { id: 'decisions-1.2.1-commit', file: 'docs/decisions.md', kind: 'text', pattern: /\| 1\.2\.1 \| [\d-]+ \| ([0-9a-f]{7}) \|/, compute: (ctx) => shortCommit(ctx, 'v1.2.1') }, + { id: 'proposal-tool-args-keys', file: 'docs/proposals/2026-10-05-tool-args-declaration.md', pattern: /at 1\.2\.1, (\d+) `properties` keys carry one/, compute: async (ctx) => sumKeys(await ctx.schemasAt('v1.2.1')).all }, + { id: 'proposal-tool-args-declared', file: 'docs/proposals/2026-10-05-tool-args-declaration.md', pattern: /the (\d+)\s+declared properties the site reference lists/, compute: async (ctx) => sumKeys(await ctx.schemasAt('v1.2.1')).declared }, + + // The guard between two release tags (repository with tags only). + { id: 'contributing-entries-used', file: 'CONTRIBUTING.md', pattern: /at v1\.2\.0, (\d+) of the allow-list's/, compute: async (ctx) => (await ctx.guardBetween('v1.1.0', 'v1.2.0')).entriesUsed }, + { id: 'contributing-entries', file: 'CONTRIBUTING.md', pattern: /of the allow-list's (\d+) entries covered/, compute: async (ctx) => (await ctx.guardBetween('v1.1.0', 'v1.2.0')).entries }, + { id: 'contributing-tightenings', file: 'CONTRIBUTING.md', pattern: /covered the (\d+) tightenings the guard reports/, compute: async (ctx) => (await ctx.guardBetween('v1.1.0', 'v1.2.0')).accepted }, + { id: 'contributing-other-entries', file: 'CONTRIBUTING.md', pattern: /the other (\d+) entries are 1\.1\.0's/, compute: async (ctx) => { const g = await ctx.guardBetween('v1.1.0', 'v1.2.0'); return g.entries - g.entriesUsed; } }, + { id: 'changelog-1.2.0-tightenings', file: 'CHANGELOG.md', pattern: /against 1\.1\.0\s+the guard reports (\d+) allow-listed tightenings/, compute: async (ctx) => (await ctx.guardBetween('v1.1.0', 'v1.2.0')).accepted }, + { id: 'changelog-1.2.0-entries-used', file: 'CHANGELOG.md', pattern: /covered by (\d+) of the allow-list's/, compute: async (ctx) => (await ctx.guardBetween('v1.1.0', 'v1.2.0')).entriesUsed }, + { id: 'changelog-1.2.0-entries', file: 'CHANGELOG.md', pattern: /of the allow-list's (\d+) entries \(an/, compute: async (ctx) => (await ctx.guardBetween('v1.1.0', 'v1.2.0')).entries }, + { id: 'changelog-1.2.0-other-entries', file: 'CHANGELOG.md', pattern: /the other (\d+)\s+entries are 1\.1\.0's/, compute: async (ctx) => { const g = await ctx.guardBetween('v1.1.0', 'v1.2.0'); return g.entries - g.entriesUsed; } }, + { id: 'changelog-1.2.0-clauses', file: 'CHANGELOG.md', pattern: /extracts the (\d+) clauses with stable ids/, compute: async (ctx) => (await clausesAt(ctx, 'v1.2.0')).clauses }, + { id: 'changelog-1.2.0-excluded', file: 'CHANGELOG.md', pattern: /excluded with a reason \((\d+) are:/, compute: async (ctx) => (await clausesAt(ctx, 'v1.2.0')).excluded }, + { id: 'changelog-1.2.0-keys', file: 'CHANGELOG.md', pattern: /All (\d+) `properties` keys \(/, compute: async (ctx) => sumKeys(await ctx.schemasAt('v1.2.0')).all }, + { id: 'changelog-1.2.1-keys', file: 'CHANGELOG.md', pattern: /`properties` keys \((\d+) at 1\.2\.1/, compute: async (ctx) => sumKeys(await ctx.schemasAt('v1.2.1')).all }, + { id: 'changelog-1.2.0-declared', file: 'CHANGELOG.md', pattern: /which are (\d+) declared properties \(/, compute: async (ctx) => sumKeys(await ctx.schemasAt('v1.2.0')).declared }, + { id: 'changelog-1.2.1-declared', file: 'CHANGELOG.md', pattern: /declared properties \((\d+) at 1\.2\.1\)/, compute: async (ctx) => sumKeys(await ctx.schemasAt('v1.2.1')).declared }, +]; + +/** Stated numbers deliberately not checked here, and why. Kept so a reader sees the edge. */ +export const EXCLUDED = [ + { where: 'CHANGELOG 1.1.0', statement: 'against 1.0.2 the guard reports 46 allow-listed tightenings', reason: "1.1.0's allow-list entries predate `after` and its guard used lock format 3, so today's guard cannot reproduce the count; checked by hand on 6 October 2026 with v1.1.0's own check-additive.mjs (46)" }, + { where: 'README, CHANGELOG, the SPEC, fixtures/evidence/, docs/conformance/', statement: 'counts of the reference deployment\'s real records (audit records, signals, provenance, lease records) and its ten distinct actor.runtime values', reason: 'the journals live in the reference implementation\'s repository; each count is dated and its evidence file names how it was taken' }, + { where: 'docs/conformance/cdf-harness.md', statement: 'the recorded run output (40 valid and 98 invalid fixtures, 6 tests)', reason: 'a verbatim record of one run at a named pin, true of that run whatever the corpus holds later' }, + { where: 'fixtures/evidence/*.md', statement: 'pattern counts at a check (139, 211, 226 patterns)', reason: 'dated records of the check that an allow-list entry cites; the current count is the README claim above' }, + { where: 'CHANGELOG release sections', statement: 'what an entry added ("Eighteen invalid fixtures", "27 scenarios", "Eleven valid fixtures")', reason: 'a count of what that change added, which later changes do not make false' }, + { where: 'docs/implementing.md', statement: '"Three further lines from the installed package"', reason: 'a count of lines run.mjs prints, not of repository content; the lines themselves are quoted beside it' }, +]; + +// --------------------------------------------------------------------------------------------- +// Running them. +// --------------------------------------------------------------------------------------------- + +function sameValue(kind, stated, actual) { + if (kind === 'names') { + const a = parseNames(stated); + return { ok: JSON.stringify(a) === JSON.stringify([...actual].sort()), shown: a.join(', '), truth: [...actual].sort().join(', ') }; + } + if (kind === 'text') return { ok: stated.trim() === String(actual), shown: stated.trim(), truth: String(actual) }; + const n = parseCount(stated); + return { ok: n !== null && n === actual, shown: stated, truth: String(actual) }; +} + +/** `{ checked, notChecked: [{ id, reason }], failures: [string], total }`. */ +export async function runClaimsCheck(root = resolve(here, '..')) { + const ctx = makeContext(root); + const failures = []; + const notChecked = []; + let checked = 0; + const files = new Map(); + for (const claim of CLAIMS) { + try { + if (!files.has(claim.file)) files.set(claim.file, existsSync(join(root, claim.file)) ? await readFile(join(root, claim.file), 'utf8') : null); + const source = files.get(claim.file); + if (source === null) throw new NotAvailable(`${claim.file} is not present (the published package does not ship it)`); + const match = claim.pattern.exec(source); + if (!match) { + failures.push(`claims: ${claim.id}: the statement is no longer in ${claim.file} (pattern ${claim.pattern}); update the claim with the text, or remove it with the statement`); + continue; + } + const actual = await claim.compute(ctx); + const { ok, shown, truth } = sameValue(claim.kind ?? 'count', match[1], actual); + checked += 1; + if (!ok) failures.push(`claims: ${claim.id}: ${claim.file} states "${shown}", the repository says ${truth}`); + } catch (error) { + if (error instanceof NotAvailable) notChecked.push({ id: claim.id, reason: error.message }); + else failures.push(`claims: ${claim.id}: could not compute: ${error.message.split('\n')[0]}`); + } + } + return { checked, notChecked, failures, total: CLAIMS.length }; +} + +const invokedDirectly = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (invokedDirectly) { + const r = await runClaimsCheck(); + for (const n of r.notChecked) console.log(`NOT CHECKED ${n.id}: ${n.reason}`); + for (const f of r.failures) console.error(f); + console.log(`Claims: ${r.checked} checked, ${r.failures.length} failure(s).`); + process.exit(r.failures.length > 0 ? 1 : 0); +} diff --git a/conformance/run.mjs b/conformance/run.mjs index 09855e0..941279d 100644 --- a/conformance/run.mjs +++ b/conformance/run.mjs @@ -20,6 +20,7 @@ import { idFor } from './ids.mjs'; import { changelogSection } from './changelog.mjs'; import { runTraceabilityCheck } from './traceability-check.mjs'; import { staleSuiteFiles } from './suite-export.mjs'; +import { runClaimsCheck } from './claims-check.mjs'; const here = dirname(fileURLToPath(import.meta.url)); @@ -157,7 +158,19 @@ console.log(stale.length === 0 : `Suite export: ${stale.length} stale file(s) — run \`npm run lock\`.`); const suiteFailures = stale.map((f) => `suite: conformance/suite/${f} is stale; run \`npm run lock\` and commit it.`); -const failures = [...report.failures, ...currency.problems, ...guard.failures, ...schemaChecks.failures, ...layout.failures, ...site.failures, ...trace.failures, ...suiteFailures]; +// Every count and list the documents state, recomputed from the repository: a number in prose is +// a fact with a short half-life unless something checks it (the fourth review found twenty-two +// that had gone stale). A claim whose source is not in the package, or that needs the release tags, +// is listed NOT CHECKED rather than passed. +const claims = await runClaimsCheck(); +console.log(`Claims: ${claims.checked} checked, ${claims.failures.length} failure(s).`); +if (claims.notChecked.length > 0) { + const byReason = new Map(); + for (const n of claims.notChecked) byReason.set(n.reason, [...(byReason.get(n.reason) ?? []), n.id]); + console.log(`Claims: ${claims.notChecked.length} NOT CHECKED (\`node conformance/claims-check.mjs\` lists each): ${[...byReason].map(([reason, ids]) => `${ids.length} because ${reason}`).join('; ')}.`); +} + +const failures = [...report.failures, ...currency.problems, ...guard.failures, ...schemaChecks.failures, ...layout.failures, ...site.failures, ...trace.failures, ...suiteFailures, ...claims.failures]; if (failures.length > 0) { console.error(`\n${failures.length} failure(s):\n`); diff --git a/conformance/suite-export.mjs b/conformance/suite-export.mjs index b6721a0..170c47a 100644 --- a/conformance/suite-export.mjs +++ b/conformance/suite-export.mjs @@ -64,7 +64,7 @@ export async function buildSuite() { // carrying the full schema; a schema alone above the budget still gets one test per case, // which is the best that can be done without changing what is tested. const cases = []; - const title = `${schema.title ?? shape} (${file}, @cognitive-delivery/contract ${pkg.version})`; + const title = `${schema.title ?? shape} (${file}, exported from a tree at @cognitive-delivery/contract ${pkg.version}; on main its descriptions may be ahead of that tag)`; let chunk = []; const lineLength = (list, index) => JSON.stringify({ description: `${title}: fixtures ${index}`, schema, tests: list }).length; for (const test of tests) { diff --git a/conformance/suite/draft7/agent-lease-manifest.json b/conformance/suite/draft7/agent-lease-manifest.json index 181804c..462a091 100644 --- a/conformance/suite/draft7/agent-lease-manifest.json +++ b/conformance/suite/draft7/agent-lease-manifest.json @@ -1,6 +1,6 @@ [ { - "description": "Agent Lease Manifest (agent-lease-manifest.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "Agent Lease Manifest (agent-lease-manifest.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/agent-lease-manifest.schema.json", @@ -140,7 +140,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -218,7 +218,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -302,7 +302,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } diff --git a/conformance/suite/draft7/agent-lease.json b/conformance/suite/draft7/agent-lease.json index 088e148..79da8f7 100644 --- a/conformance/suite/draft7/agent-lease.json +++ b/conformance/suite/draft7/agent-lease.json @@ -1,6 +1,6 @@ [ { - "description": "Agent Lease (agent-lease.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "Agent Lease (agent-lease.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/agent-lease.schema.json", @@ -164,7 +164,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -242,7 +242,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -327,7 +327,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } diff --git a/conformance/suite/draft7/audit-event.json b/conformance/suite/draft7/audit-event.json index d6c8b63..2c0ab44 100644 --- a/conformance/suite/draft7/audit-event.json +++ b/conformance/suite/draft7/audit-event.json @@ -1,6 +1,6 @@ [ { - "description": "CDF governance audit event (audit-event.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "CDF governance audit event (audit-event.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/audit-event.schema.json", @@ -152,7 +152,7 @@ }, "details": { "type": "object", - "description": "Flat scalars only, and keys are filtered by name: a key whose segment is authorization, content, file, password, path, payload, prompt, request, secret or token is refused by the schema, mirroring the reference writer, which drops it before write. Do not defeat this by renaming a sensitive field.", + "description": "Flat scalars only, and keys are filtered by name: a key with a lower-case or camelCase segment that is authorization, content, file, password, path, payload, prompt, request, secret or token is refused by the schema. This approximates the reference writer's rule, which drops such a key before write; the schema is case-sensitive where the writer is not, and `propertyNames` lists the differences. Do not defeat this by renaming a sensitive field.", "additionalProperties": { "type": [ "string", @@ -163,7 +163,7 @@ "description": "A flat scalar; a string value is at most 200 characters, the reference writer's own cap, because `details` reaches a permanent record." }, "propertyNames": { - "description": "Mirrors the reference writer's rule, which splits a key on non-alphanumerics and camelCase boundaries and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", + "description": "Approximates the reference writer's rule, which splits a key at camelCase boundaries, lower-cases it, splits it on non-alphanumerics and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). The differences: the schema is case-sensitive, so it admits `Authorization`, `API_TOKEN`, `Token`, `TokenCount`, `PATH` and `x-Token`, all of which the writer drops; and it ends a camelCase segment at a digit, so it refuses `apiToken2` and `myFile2`, which the writer keeps. `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", "allOf": [ { "not": { diff --git a/conformance/suite/draft7/cdi-assessment.json b/conformance/suite/draft7/cdi-assessment.json index 9e78bde..3995180 100644 --- a/conformance/suite/draft7/cdi-assessment.json +++ b/conformance/suite/draft7/cdi-assessment.json @@ -1,6 +1,6 @@ [ { - "description": "CDF CDI assessment record (cdi-assessment.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "CDF CDI assessment record (cdi-assessment.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/cdi-assessment.schema.json", diff --git a/conformance/suite/draft7/cdi-signal.json b/conformance/suite/draft7/cdi-signal.json index 6ac3b04..6128af6 100644 --- a/conformance/suite/draft7/cdi-signal.json +++ b/conformance/suite/draft7/cdi-signal.json @@ -1,6 +1,6 @@ [ { - "description": "CDF CDI signal (cdi-signal.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "CDF CDI signal (cdi-signal.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/cdi-signal.schema.json", @@ -227,7 +227,7 @@ }, "details": { "type": "object", - "description": "Flat scalars only, and keys are filtered by name exactly as on the audit event: a key whose segment is authorization, content, file, password, path, payload, prompt, request, secret or token is refused. Must never contain secrets, personal data or file contents.", + "description": "Flat scalars only, and keys are filtered by name as on the audit event: a key with a lower-case or camelCase segment that is authorization, content, file, password, path, payload, prompt, request, secret or token is refused, which approximates the reference writer's rule (`propertyNames` lists the differences). Must never contain secrets, personal data or file contents.", "additionalProperties": { "type": [ "string", @@ -236,7 +236,7 @@ ] }, "propertyNames": { - "description": "Mirrors the reference writer's rule, which splits a key on non-alphanumerics and camelCase boundaries and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", + "description": "Approximates the reference writer's rule, which splits a key at camelCase boundaries, lower-cases it, splits it on non-alphanumerics and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). The differences: the schema is case-sensitive, so it admits `Authorization`, `API_TOKEN`, `Token`, `TokenCount`, `PATH` and `x-Token`, all of which the writer drops; and it ends a camelCase segment at a digit, so it refuses `apiToken2` and `myFile2`, which the writer keeps. `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", "allOf": [ { "not": { diff --git a/conformance/suite/draft7/config-core.json b/conformance/suite/draft7/config-core.json index 4f36bc1..4e8ac33 100644 --- a/conformance/suite/draft7/config-core.json +++ b/conformance/suite/draft7/config-core.json @@ -1,6 +1,6 @@ [ { - "description": "CDF config, shared core (config-core.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "CDF config, shared core (config-core.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/config-core.schema.json", diff --git a/conformance/suite/draft7/lease-record.json b/conformance/suite/draft7/lease-record.json index c127a1a..c67739f 100644 --- a/conformance/suite/draft7/lease-record.json +++ b/conformance/suite/draft7/lease-record.json @@ -1,6 +1,6 @@ [ { - "description": "Agent lease record (lease-record.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "Agent lease record (lease-record.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/lease-record.schema.json", @@ -164,7 +164,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -242,7 +242,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -327,7 +327,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } @@ -616,7 +616,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -694,7 +694,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -779,7 +779,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } @@ -940,7 +940,7 @@ }, "reasonCode": { "type": "string", - "description": "A refusal reason in one of three reserved forms (SPEC §5.2). `R`: the specification's own conditions, R1 to R6 today; R7 and above are reserved for the specification to assign, so an issuer cannot mint one. `runtime_error:`: the issuer could not decide (no signing key, parent unknown, schema invalid), named by the issuer. `vendor::`: anything else, namespaced by whoever defines it. Prose goes in `reason`, never here, so two implementations can compare refusals by code.", + "description": "A refusal reason in one of three reserved forms (SPEC §5.2). `R`: the specification's own conditions, R1 to R6 today; R7 and above are reserved for the specification to assign, and an issuer MUST NOT mint one, although the pattern admits R7 to R99. `runtime_error:`: the issuer could not decide (no signing key, parent unknown, schema invalid), named by the issuer. `vendor::`: anything else, namespaced by whoever defines it. Prose goes in `reason`, never here, so two implementations can compare refusals by code.", "anyOf": [ { "pattern": "^R[1-9][0-9]?$" diff --git a/conformance/suite/draft7/plugin-manifest.json b/conformance/suite/draft7/plugin-manifest.json index 7490d0f..3daf90d 100644 --- a/conformance/suite/draft7/plugin-manifest.json +++ b/conformance/suite/draft7/plugin-manifest.json @@ -1,6 +1,6 @@ [ { - "description": "CDF Plugin Manifest (plugin-manifest.schema.json, @cognitive-delivery/contract 1.2.1): fixtures, part 1 of 2", + "description": "CDF Plugin Manifest (plugin-manifest.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): fixtures, part 1 of 2", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/plugin-manifest.schema.json", @@ -88,7 +88,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -166,7 +166,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -902,7 +902,7 @@ }, "credentialFree": { "type": "string", - "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses — a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT — each as an I-Regexp without lookahead or word boundaries. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", + "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses (a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT), each a pattern without lookahead or word boundaries, which RE2 compiles. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", "allOf": [ { "not": { @@ -3033,127 +3033,11 @@ } }, "valid": false - }, - { - "description": "plugin-manifest.mcp-remote-without-url is refused", - "data": { - "name": "cdf-data-checks", - "displayName": "CDF Data Checks", - "version": "1.2.0", - "description": "Native data-quality checks, a tracker strategy and a model provider, contributed to the harness as data.", - "author": { - "name": "Cognitive Delivery", - "email": "contact@example.com", - "url": "https://example.com" - }, - "homepage": "https://example.com/plugins/cdf-data-checks", - "repository": "https://example.com/plugins/cdf-data-checks.git", - "license": "PolyForm-Noncommercial-1.0.0", - "keywords": [ - "cdf", - "data", - "quality" - ], - "category": "Productivity", - "metadata": { - "supportChannel": "https://example.com/support" - }, - "defaultEnabled": false, - "skills": "./skills/", - "commands": [ - "./commands/", - "./extra-commands/" - ], - "agents": "./agents/", - "hooks": "./hooks/hooks.json", - "mcpServers": { - "remote": { - "type": "http", - "headersHelper": "./auth.sh" - } - }, - "lspServers": { - "example-lsp": { - "command": "example-lsp", - "args": [ - "--stdio" - ], - "extensionToLanguage": { - ".example": "example" - } - } - }, - "cdf": { - "schemaVersion": "1.0", - "contributes": { - "providers": [ - { - "id": "example-inference", - "kind": "openai-compatible", - "baseUrl": "https://api.example.com/v1", - "model": "example-medium", - "prices": { - "input_per_million": 0.5, - "output_per_million": 1.5 - }, - "extraHosts": [ - "cdn.example.com" - ] - } - ], - "trackers": [ - { - "id": "examplecorp", - "label": "ExampleCorp Work Items", - "readAction": "examplecorp.readItemStatus", - "commentAction": "examplecorp.commentCdfStatus", - "transitionAction": "examplecorp.transitionItem", - "createAction": "examplecorp.createItemForSpec", - "externalIdLabel": "item id", - "externalIdPlaceholder": "e.g. 1234" - } - ], - "docPacks": [ - "./docs/data-quality-pack.json" - ], - "checks": [ - { - "id": "null-rate", - "worker": "./workers/null-rate.mjs" - } - ] - }, - "capabilities": { - "tools": [ - "cdf_lint_artefacts" - ], - "read_paths": [ - "models/**", - ".cdf/specs/**" - ], - "write_paths": [ - ".cdf/checks/**" - ], - "hosts": [ - "api.example.com" - ], - "commands": [ - "node" - ] - }, - "attestation": { - "issuer": "plugin", - "signature": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", - "key_fingerprint": "ab12cd34ef56" - } - } - }, - "valid": false } ] }, { - "description": "CDF Plugin Manifest (plugin-manifest.schema.json, @cognitive-delivery/contract 1.2.1): fixtures, part 2 of 2", + "description": "CDF Plugin Manifest (plugin-manifest.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): fixtures, part 2 of 2", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/plugin-manifest.schema.json", @@ -3241,7 +3125,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -3319,7 +3203,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -4055,7 +3939,7 @@ }, "credentialFree": { "type": "string", - "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses — a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT — each as an I-Regexp without lookahead or word boundaries. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", + "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses (a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT), each a pattern without lookahead or word boundaries, which RE2 compiles. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", "allOf": [ { "not": { @@ -4855,6 +4739,122 @@ "additionalProperties": true }, "tests": [ + { + "description": "plugin-manifest.mcp-remote-without-url is refused", + "data": { + "name": "cdf-data-checks", + "displayName": "CDF Data Checks", + "version": "1.2.0", + "description": "Native data-quality checks, a tracker strategy and a model provider, contributed to the harness as data.", + "author": { + "name": "Cognitive Delivery", + "email": "contact@example.com", + "url": "https://example.com" + }, + "homepage": "https://example.com/plugins/cdf-data-checks", + "repository": "https://example.com/plugins/cdf-data-checks.git", + "license": "PolyForm-Noncommercial-1.0.0", + "keywords": [ + "cdf", + "data", + "quality" + ], + "category": "Productivity", + "metadata": { + "supportChannel": "https://example.com/support" + }, + "defaultEnabled": false, + "skills": "./skills/", + "commands": [ + "./commands/", + "./extra-commands/" + ], + "agents": "./agents/", + "hooks": "./hooks/hooks.json", + "mcpServers": { + "remote": { + "type": "http", + "headersHelper": "./auth.sh" + } + }, + "lspServers": { + "example-lsp": { + "command": "example-lsp", + "args": [ + "--stdio" + ], + "extensionToLanguage": { + ".example": "example" + } + } + }, + "cdf": { + "schemaVersion": "1.0", + "contributes": { + "providers": [ + { + "id": "example-inference", + "kind": "openai-compatible", + "baseUrl": "https://api.example.com/v1", + "model": "example-medium", + "prices": { + "input_per_million": 0.5, + "output_per_million": 1.5 + }, + "extraHosts": [ + "cdn.example.com" + ] + } + ], + "trackers": [ + { + "id": "examplecorp", + "label": "ExampleCorp Work Items", + "readAction": "examplecorp.readItemStatus", + "commentAction": "examplecorp.commentCdfStatus", + "transitionAction": "examplecorp.transitionItem", + "createAction": "examplecorp.createItemForSpec", + "externalIdLabel": "item id", + "externalIdPlaceholder": "e.g. 1234" + } + ], + "docPacks": [ + "./docs/data-quality-pack.json" + ], + "checks": [ + { + "id": "null-rate", + "worker": "./workers/null-rate.mjs" + } + ] + }, + "capabilities": { + "tools": [ + "cdf_lint_artefacts" + ], + "read_paths": [ + "models/**", + ".cdf/specs/**" + ], + "write_paths": [ + ".cdf/checks/**" + ], + "hosts": [ + "api.example.com" + ], + "commands": [ + "node" + ] + }, + "attestation": { + "issuer": "plugin", + "signature": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", + "key_fingerprint": "ab12cd34ef56" + } + } + }, + "valid": false + }, { "description": "plugin-manifest.mcp-stdio-without-command is refused", "data": { diff --git a/conformance/suite/draft7/plugin-marketplace.json b/conformance/suite/draft7/plugin-marketplace.json index 0b70d92..afbcdda 100644 --- a/conformance/suite/draft7/plugin-marketplace.json +++ b/conformance/suite/draft7/plugin-marketplace.json @@ -1,6 +1,6 @@ [ { - "description": "CDF Plugin Marketplace (plugin-marketplace.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "CDF Plugin Marketplace (plugin-marketplace.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/plugin-marketplace.schema.json", @@ -55,7 +55,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -133,7 +133,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -1182,7 +1182,7 @@ }, "credentialFree": { "type": "string", - "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses — a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT — each as an I-Regexp without lookahead or word boundaries. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", + "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses (a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT), each a pattern without lookahead or word boundaries, which RE2 compiles. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", "allOf": [ { "not": { diff --git a/conformance/suite/draft7/provenance.json b/conformance/suite/draft7/provenance.json index ac8bc65..e4c42b2 100644 --- a/conformance/suite/draft7/provenance.json +++ b/conformance/suite/draft7/provenance.json @@ -1,6 +1,6 @@ [ { - "description": "CDF provenance (provenance.schema.json, @cognitive-delivery/contract 1.2.1): every fixture of the conformance corpus", + "description": "CDF provenance (provenance.schema.json, exported from a tree at @cognitive-delivery/contract 1.2.1; on main its descriptions may be ahead of that tag): every fixture of the conformance corpus", "schema": { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://cognitive-delivery.github.io/contract/1.x/provenance.schema.json", diff --git a/docs/conformance/cdf-harness.md b/docs/conformance/cdf-harness.md index 4470355..1310486 100644 --- a/docs/conformance/cdf-harness.md +++ b/docs/conformance/cdf-harness.md @@ -46,8 +46,9 @@ the adapter reaches no editor code. ## Real artefacts -`test/unit/contractRealArtefacts.test.ts` validates the harness's own journals, and a zero count -fails. At the pin, 2026-10-06: 33,665 audit events, 19,287 CDI signals, 4,588 provenance records, +`test/unit/contractRealArtefacts.test.ts` validates the harness's own journals. A zero count fails +for the audit events, CDI signals, provenance records and assessments; an empty or absent lease +journal passes, because a workspace that has never issued a lease has none. At the pin, 2026-10-06: 33,665 audit events, 19,287 CDI signals, 4,588 provenance records, 2 assessments, 6 lease records and 1 config; zero rejected. ## Narrowing vectors diff --git a/docs/decisions.md b/docs/decisions.md index 918602e..49564c3 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -3,16 +3,17 @@ The contract is Apache-2.0 and lives in `Cognitive-Delivery/contract`. The decisions that shaped it were taken in the reference implementation's repository, `Cognitive-Delivery/cdf-harness` (PolyForm Noncommercial 1.0.0), as decision records under `docs/governance/decisions/`, and the three reviews -that drove 1.1 and 1.2 are Claude documents. A reader here cannot follow a CHANGELOG entry back to +that drove 1.1 and 1.2 are the maintainer's private working documents, which a reader here cannot +open; this page summarises what each found instead of linking to it. A reader here cannot follow a CHANGELOG entry back to its decision without crossing that licence boundary, so this page is the bridge: every release from 1.0.0 to 1.2.1, the records and reviews behind it, and one line on what each decided. Proposals accepted or rejected since 1.2.0 are recorded in [`docs/proposals/`](proposals/). ## Releases -| Release | Date | Tag commit | Shaped by | What was decided | +| Release | Date | Tagged commit | Shaped by | What was decided | |---|---|---|---|---| -| 1.0.0, 1.0.1 and 1.0.2 | 2026-09-19 | 3ad8832, 3150336, e26142f | DR-146, DR-156 | The schema set extracted from the harness under Apache-2.0 with the additive rule and the lock (first); the SPEC and the canonical-bytes vectors (second); the lock's version checked after the second tarball shipped it stale (third) | +| 1.0.0, 1.0.1 and 1.0.2 | 2026-09-19 | f575139, df0b1f8, 54c134b (annotated tags 3ad8832, 3150336, e26142f) | DR-146, DR-156 | The schema set extracted from the harness under Apache-2.0 with the additive rule and the lock (first); the SPEC and the canonical-bytes vectors (second); the lock's version checked after the second tarball shipped it stale (third) | | 1.1.0 | 2026-10-05 | c04f53c | DR-182; the first review | Five of the first review's ten improvements: the guard sees tightenings, the allow-list, narrowing and signature vectors, `.expect.json` paths, RFC 8785 named, every `$id` served, the privacy properties enforced by shape | | 1.2.0 | 2026-10-05 | 8cd038d | DR-183; the second review | All fourteen of the second review's improvements: identity rules, lease rules and `lease-record`, evidence hygiene, typed plugin components, `tool_args`, traceability, the suite under six validators, stability markers, governance written down, every release frozen at its URL | | 1.2.1 | 2026-10-06 | 3088e44 | DR-183 (its closing tasks); the third review; DR-184 | Found by pinning the reference implementation to 1.2.0: the type generator, the Pages deploy on a tag, the narrowing vectors as the reference's own bytes, and `capabilities.tool_args`. The third review's findings are parked for batch three; DR-184 re-based every document on 1.2.1 | @@ -23,14 +24,18 @@ accepted or rejected since 1.2.0 are recorded in [`docs/proposals/`](proposals/) |---|---|---| | DR-146, "The governance contract has a second home, and phases are not in it" | 2026-07-25 | The contract is extracted into its own repository under Apache-2.0; within a major, changes are additive only; lifecycle phases are an open string the contract does not define | | DR-156, "The harness consumes the contract as a submodule, not from npm" | 2026-09-19 | The reference implementation pins the contract as a git submodule at `contract/` and runs the published runner against itself | -| DR-182, "Contract fix batch one, and what the evidence changed" | 2026-10-04 | The five improvements taken from the first review into 1.1.0, and where real journals changed the design (its header still reads Proposed, owner review pending) | +| DR-182, "Contract fix batch one, and what the evidence changed" | 2026-10-04 | The five improvements taken from the first review into 1.1.0, and where real journals changed the design (Approved) | | DR-183, "Contract batch two, and where the evidence overruled the review" | 2026-10-05 | All fourteen of the second review's improvements into 1.2.0, with eight decisions D1 to D8 where the evidence overruled the review: among them D2 (IP literals admitted as hosts; no port form in 1.x), D5 (refusal reasons as codes), D6 (`tool_args` a declaration) and D7 (lease cross-field rules in the runner, not the schema) | | DR-184, "Every document that describes the contract reflects 1.2.1" | 2026-10-06 | Every document is re-based on one fact sheet at 1.2.1, with a gate that fails on a stale statement; this page and the proposals directory come from it | ## The reviews -| Review | Date | Document | Outcome | +Each review is a private working document of the maintainer's; the summaries below are the record +here. + +| Review | Date | What it found | Outcome | |---|---|---|---| -| First | 2026-10-04 | | Ten improvements; five taken in 1.1.0 (DR-182) | -| Second | 2026-10-05 | | Fourteen improvements, all taken in 1.2.0 (DR-183) | -| Third | 2026-10-06 | | Twenty-four findings, none pre-empted by the 1.2.1 fact sheet; parked for batch three (Linear COGDEV-271) | +| First | 2026-10-04 | The design sound and four headline promises false: privacy enforced by shape, a reader in any language needing nothing else, a strict superset of Claude Code's plugin format, and the Index computed from the closed signal vocabulary; also path forms the SPEC refuses accepted by the schema, no narrowing or signature vectors, and an additive guard blind to a tightened pattern | Ten improvements; five taken in 1.1.0 (DR-182) | +| Second | 2026-10-05 | At `main` e91afb0: the published package unable to run its own test, the lease vocabulary under-specified, the decision side of the lease unspecified, and the repository short of the practices an implementing organisation looks for first | Fourteen improvements, all taken in 1.2.0 (DR-183) | +| Third | 2026-10-06 | At 1.2.1: further keywords the lock does not record, pattern semantics that differ between regular-expression engines, and statements in the documents to make precise | Twenty-four findings, none pre-empted by the 1.2.1 fact sheet; parked for batch three (Linear COGDEV-271) | +| Fourth | 2026-10-06 | Twenty-two statements the artefacts contradicted, a font licence naming the wrong copyright holder, the reference implementation signing different bytes from the ones the contract verifies, a lease journal any governed agent can append a revocation to, and attestations re-signed without verification | Six recommendations, all taken in batch three (DR-187); the statements and the licence corrected first | diff --git a/docs/implementing.md b/docs/implementing.md index af3009d..b797b61 100644 --- a/docs/implementing.md +++ b/docs/implementing.md @@ -79,10 +79,13 @@ field behind it and the absence that produces it. | `rejection paths NOT CHECKED (adapter reports no errors)` | `errorPathsChecked: false` | `lastErrors` is not an array after a rejection | An absent `roundTrip` prints nothing and has no field: the planted fixture is still validated, but -nothing checks that the field survives. Two further lines from the installed package are about the -tarball, not the adapter: `Additive guard: NOT PRESENT in this package` and `Layout: NOT CHECKED -(published package; the repository check runs from a clone)`, because `check-additive.mjs` and the -repository layout ship with the repository only. +nothing checks that the field survives. Three further lines from the installed package are about +the tarball, not the adapter: `Additive guard: NOT PRESENT in this package`, `Layout: NOT CHECKED +(published package; the repository check runs from a clone)` and `Site: NOT CHECKED (tooling/ is not +in the package)`, because `check-additive.mjs`, the repository layout and the site generator ship +with the repository only. The claims line, `Claims: N checked, F failure(s).`, is printed from the +package too, with each claim whose source is not in the package (CONTRIBUTING, `docs/`, the +allow-list, the git history) listed as NOT CHECKED rather than passed. The reference adapter (`conformance/ajv-adapter.mjs`) offers every hook but `narrow`, so the contract's own CI prints `behaviour NOT CHECKED` for narrowing; the reference implementation runs diff --git a/docs/proposals/2026-10-05-identity-rules.md b/docs/proposals/2026-10-05-identity-rules.md index b1146b0..b5f4780 100644 --- a/docs/proposals/2026-10-05-identity-rules.md +++ b/docs/proposals/2026-10-05-identity-rules.md @@ -29,8 +29,8 @@ of at most 253 characters, which admits IPv4 literals and `localhost`; a command `^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$`; a tool or approval is `^[A-Za-z0-9_][A-Za-z0-9_.:/-]{0,127}$`, never `*`. Applied to `allow` and `deny`, to the inlined copies in `agent-lease` and `lease-record`, and to the plugin schemas' `capabilities`. `agent.name` is capped at 120 and described as never a -person's name; `intent.purpose` at 500. Every pattern is in the I-Regexp subset and compiles under -RE2 in CI. +person's name; `intent.purpose` at 500. Every pattern is an ECMA-262 regular expression without +lookaround or backreferences, applied as an unanchored search, and compiles under RE2 in CI. ## Backward compatibility diff --git a/docs/proposals/2026-10-05-tool-args-declaration.md b/docs/proposals/2026-10-05-tool-args-declaration.md index 6c4e2e2..ce6a725 100644 --- a/docs/proposals/2026-10-05-tool-args-declaration.md +++ b/docs/proposals/2026-10-05-tool-args-declaration.md @@ -54,5 +54,6 @@ nowhere to live and a later rule has no history. A custom keyword for stability: Accepted, 2026-10-05 (DR-183 D6). Shipped: SPEC §4.4, §11 and §13; the manifest schema and its inlined copies; the vector; the two fixtures; `conformance/metaschema.json`, which requires a -stability comment on every declared property (666 at 1.2.1). At its 1.2.1 pin the harness's +stability comment on every declared property (at 1.2.1, 666 `properties` keys carry one: the 651 +declared properties the site reference lists and 15 keys inside `if` and `contains` conditions). At its 1.2.1 pin the harness's `CapabilityLists` excludes `tool_args`: a declaration, never a grant. diff --git a/docs/proposals/TEMPLATE.md b/docs/proposals/TEMPLATE.md index 3c99b45..d12cf02 100644 --- a/docs/proposals/TEMPLATE.md +++ b/docs/proposals/TEMPLATE.md @@ -27,8 +27,9 @@ would be nice. The shape, rule or check, concretely: a field with its type and constraints; a rule with its code and the fixture that would refuse it; a vector with the grant or refusal it must produce. -Patterns are RFC 9485 I-Regexp without lookahead, and must read the same under Python's `re`, RE2 -and ECMAScript (the corpus runs under six validators). +Patterns are ECMA-262 regular expressions in the subset that compiles under RE2 (no lookaround, no +backreferences), applied as an unanchored search per JSON Schema, and must read the same under +Python's `re`, RE2 and ECMAScript (the corpus runs under six validators). ## Backward compatibility diff --git a/docs/upgrading-1.0-to-1.2.md b/docs/upgrading-1.0-to-1.2.md index 9e32cc7..931085a 100644 --- a/docs/upgrading-1.0-to-1.2.md +++ b/docs/upgrading-1.0-to-1.2.md @@ -2,11 +2,18 @@ For an implementation built against schema set 1.0 (contract 1.0.0, 1.0.1 and 1.0.2, all of 2026-09-19). Every change from 1.0.2 to 1.2.1 is additive within 1.x by the contract's own rule, and -mechanically checked: a 1.0 reader keeps reading every artefact a 1.2 writer produces. What did -change is the set of values the schemas refuse. Each tightening is allow-listed in -`compat-allowlist.json` with the fixture or recorded check proving no conformant writer produced -what it refuses, and the reference deployment's journals validate under 1.2 with zero rejections. -Each item below cites its CHANGELOG entry. +mechanically checked in one direction: a 1.2 reader reads every artefact a conformant 1.0 writer +produced. The other direction does not hold. A 1.0 reader validating with the 1.0.2 schemas may +reject what a 1.2 writer produces: a `lease-record` line (no 1.0 schema describes it), a +`schema_version` of `"1.2"` on the lease schemas (1.0 required the literal `1.0`), and plugin forms +1.0 did not admit, such as `commands` as an object map in a plugin manifest (1.0 typed it as a path +or a list of paths) or a marketplace source form 1.0 did not list. The corpus shows it: the 1.0.2 +schemas reject the valid fixtures `plugin-manifest.claude-code-reference` (at `/commands`) and +`plugin-marketplace.populated` (at `/plugins/3/source`). Beyond those additions, what changed is +the set of values the schemas refuse. Each tightening is allow-listed in `compat-allowlist.json` +with the fixture or recorded check proving no conformant writer produced what it refuses, and the +reference deployment's journals validate under 1.2 with zero rejections. Each item below cites its +CHANGELOG entry. ## Part A: what the schemas now refuse @@ -21,10 +28,10 @@ Each item below cites its CHANGELOG entry. | `agent.name` over 120 characters; `intent.purpose` over 500 | the manifest and its inlined copies | | same | | A lease timestamp in any form but UTC with milliseconds and `Z` | `agent-lease` `issued_at` and `expires_at` | `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$` | 1.2.0 "Lease rules L1 and L2, and one timestamp form inside the signed bytes" | | `budget.depth` above 16; `budget.fan_out` above 256; any integer above 2^53 - 1 | the manifest and its inlined copies; every integer field | | 1.2.0, the same entry; 1.1.0 "Canonical bytes are declared to be RFC 8785" for the 2^53 bound | -| A `schema_version` that is not `major.minor` | every schema; the lease schemas widen from the literal `1.0` | `^\d+\.\d+$` | 1.2.0 "Evidence hygiene" | +| A `schema_version` that is not `major.minor` | the seven schemas that carry it (all but `config-core` and the two plugin schemas); the lease schemas widen from the literal `1.0` | `^\d+\.\d+$` | 1.2.0 "Evidence hygiene" | | A control character in `summary` or `reasoning` | `audit-event` | | same | | A `details` string value over 200 characters; `summary` over 300; `reasoning` over 500 | `audit-event` | | 1.2.0 "Evidence hygiene" for the value cap; 1.1.0 "The privacy properties are enforced by shape, not prose" for the two caps | -| A `details` key with a sensitive segment | `audit-event.details`, `cdi-signal.details` | by `propertyNames`: a key is refused when any segment, split on non-alphanumerics and camelCase boundaries, is `authorization`, `content`, `file`, `password`, `path`, `payload`, `prompt`, `request`, `secret` or `token` | 1.1.0 "The privacy properties are enforced by shape, not prose" | +| A `details` key with a sensitive segment | `audit-event.details`, `cdi-signal.details` | by `propertyNames`, approximating the writer's rule: a key is refused when a lower-case segment (split on non-alphanumerics) or a camelCase segment is `authorization`, `content`, `file`, `password`, `path`, `payload`, `prompt`, `request`, `secret` or `token` | 1.1.0 "The privacy properties are enforced by shape, not prose" | | An `event_type` that is not two or more lower-case dotted segments, or over 120 characters | `audit-event` | `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$`; the reference writer's first segments are reserved and any other writer leads with its vendor name (SPEC §6.2) | 1.2.0 "Evidence hygiene" | | A `spec` that is not a slug of at most 80 characters; an assessment without exactly six integer-scored dimensions; a `sealed` path that is not dotted lower-case | `provenance`, `cdi-assessment`, `config-core` | | same | | A plugin name outside Claude Code's rule; a `command` source `mode` other than `copy` or `link`, or a `timeout` outside 1 to 600; a credential-shaped value in a hook header, an MCP `env` or MCP `headers`; an `authorization` key in a marketplace entry's `headers`; a provider at plain `http://` off loopback | `plugin-manifest`, `plugin-marketplace` | | 1.1.0 "Every key the Claude Code plugin and marketplace references document"; 1.2.0 "Inline plugin hooks and MCP servers are typed, and a manifest cannot carry a credential" | @@ -46,7 +53,7 @@ Each item below cites its CHANGELOG entry. `gemini-code-assist`, `github-copilot`, `cdf-native`, `unknown`), beside `actor.runtime`, which stays an open label. Optional in the schema; a 1.2 writer records it. CHANGELOG 1.2.0 "Evidence hygiene". -- **`schema_version` as `major.minor`** everywhere, compared on the major. The same entry. +- **`schema_version` as `major.minor`** wherever it is carried, compared on the major. The same entry. - **One timestamp form** in `issued_at` and `expires_at`, and L1 and L2 held before signing. CHANGELOG 1.2.0 "Lease rules L1 and L2, and one timestamp form inside the signed bytes". - **`allow.tool_args`** may be declared; an issuer may omit it from the grant and must not treat it @@ -69,6 +76,6 @@ Each item below cites its CHANGELOG entry. declared to be RFC 8785". - **The closed vocabularies**: `cdi-signal.event_type` and the six dimension ids. `phase` stays open. - **`$id`.** The 1.0 identifiers never resolved; since 1.1.0 each is - `https://cognitive-delivery.github.io/contract/1.x/`, and every release from 1.1.0 is also - frozen at `//`. A reader that resolved by `$id` lost nothing it had. CHANGELOG 1.1.0 + `https://cognitive-delivery.github.io/contract/1.x/`, and every `v1.*` tag, 1.0.0 included, + is also frozen at `//`. A reader that resolved by `$id` lost nothing it had. CHANGELOG 1.1.0 "Every `$id` resolves". diff --git a/generate-schemas-lock.mjs b/generate-schemas-lock.mjs index fd7b1ef..1417977 100644 --- a/generate-schemas-lock.mjs +++ b/generate-schemas-lock.mjs @@ -58,7 +58,7 @@ function compareJson(a, b) { /** * The patterns a value must NOT match, expressed as `allOf: [{ not: { pattern } }, …]`. - * This is how a path rule is written without lookahead (RFC 9485 I-Regexp has none), so the + * This is how a path rule is written without lookahead (RE2 has none), so the * lock has to see it as the constraint it is: adding one tightens, removing one loosens. */ function notPatternsOf(node) { diff --git a/schemas/agent-lease-manifest.schema.json b/schemas/agent-lease-manifest.schema.json index 548de7a..bb18efc 100644 --- a/schemas/agent-lease-manifest.schema.json +++ b/schemas/agent-lease-manifest.schema.json @@ -137,7 +137,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -215,7 +215,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -299,7 +299,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } diff --git a/schemas/agent-lease.schema.json b/schemas/agent-lease.schema.json index 18df023..f08d2d4 100644 --- a/schemas/agent-lease.schema.json +++ b/schemas/agent-lease.schema.json @@ -161,7 +161,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -239,7 +239,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -324,7 +324,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } diff --git a/schemas/audit-event.schema.json b/schemas/audit-event.schema.json index 05ccab7..da242cb 100644 --- a/schemas/audit-event.schema.json +++ b/schemas/audit-event.schema.json @@ -149,7 +149,7 @@ }, "details": { "type": "object", - "description": "Flat scalars only, and keys are filtered by name: a key whose segment is authorization, content, file, password, path, payload, prompt, request, secret or token is refused by the schema, mirroring the reference writer, which drops it before write. Do not defeat this by renaming a sensitive field.", + "description": "Flat scalars only, and keys are filtered by name: a key with a lower-case or camelCase segment that is authorization, content, file, password, path, payload, prompt, request, secret or token is refused by the schema. This approximates the reference writer's rule, which drops such a key before write; the schema is case-sensitive where the writer is not, and `propertyNames` lists the differences. Do not defeat this by renaming a sensitive field.", "additionalProperties": { "type": [ "string", @@ -160,7 +160,7 @@ "description": "A flat scalar; a string value is at most 200 characters, the reference writer's own cap, because `details` reaches a permanent record." }, "propertyNames": { - "description": "Mirrors the reference writer's rule, which splits a key on non-alphanumerics and camelCase boundaries and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", + "description": "Approximates the reference writer's rule, which splits a key at camelCase boundaries, lower-cases it, splits it on non-alphanumerics and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). The differences: the schema is case-sensitive, so it admits `Authorization`, `API_TOKEN`, `Token`, `TokenCount`, `PATH` and `x-Token`, all of which the writer drops; and it ends a camelCase segment at a digit, so it refuses `apiToken2` and `myFile2`, which the writer keeps. `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", "allOf": [ { "not": { diff --git a/schemas/cdi-signal.schema.json b/schemas/cdi-signal.schema.json index f4f5e4d..6ce8aa1 100644 --- a/schemas/cdi-signal.schema.json +++ b/schemas/cdi-signal.schema.json @@ -224,7 +224,7 @@ }, "details": { "type": "object", - "description": "Flat scalars only, and keys are filtered by name exactly as on the audit event: a key whose segment is authorization, content, file, password, path, payload, prompt, request, secret or token is refused. Must never contain secrets, personal data or file contents.", + "description": "Flat scalars only, and keys are filtered by name as on the audit event: a key with a lower-case or camelCase segment that is authorization, content, file, password, path, payload, prompt, request, secret or token is refused, which approximates the reference writer's rule (`propertyNames` lists the differences). Must never contain secrets, personal data or file contents.", "additionalProperties": { "type": [ "string", @@ -233,7 +233,7 @@ ] }, "propertyNames": { - "description": "Mirrors the reference writer's rule, which splits a key on non-alphanumerics and camelCase boundaries and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", + "description": "Approximates the reference writer's rule, which splits a key at camelCase boundaries, lower-cases it, splits it on non-alphanumerics and drops it when any segment is one of ten words: authorization, content, file, password, path, payload, prompt, request, secret, token. The three clauses refuse the lower-case segment form (`api_token`, `file-path`), the camelCase interior form (`filePath`, `apiToken`) and the camelCase leading form (`tokenCount`). The differences: the schema is case-sensitive, so it admits `Authorization`, `API_TOKEN`, `Token`, `TokenCount`, `PATH` and `x-Token`, all of which the writer drops; and it ends a camelCase segment at a digit, so it refuses `apiToken2` and `myFile2`, which the writer keeps. `estimated_files_touched` is legal: `files` is not `file`. No lookahead, so RE2 validators load it.", "allOf": [ { "not": { diff --git a/schemas/lease-record.schema.json b/schemas/lease-record.schema.json index c18fcb7..e373c3e 100644 --- a/schemas/lease-record.schema.json +++ b/schemas/lease-record.schema.json @@ -161,7 +161,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -239,7 +239,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -324,7 +324,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } @@ -613,7 +613,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -691,7 +691,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the agent may reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -776,7 +776,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host refused outright. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" } @@ -937,7 +937,7 @@ }, "reasonCode": { "type": "string", - "description": "A refusal reason in one of three reserved forms (SPEC §5.2). `R`: the specification's own conditions, R1 to R6 today; R7 and above are reserved for the specification to assign, so an issuer cannot mint one. `runtime_error:`: the issuer could not decide (no signing key, parent unknown, schema invalid), named by the issuer. `vendor::`: anything else, namespaced by whoever defines it. Prose goes in `reason`, never here, so two implementations can compare refusals by code.", + "description": "A refusal reason in one of three reserved forms (SPEC §5.2). `R`: the specification's own conditions, R1 to R6 today; R7 and above are reserved for the specification to assign, and an issuer MUST NOT mint one, although the pattern admits R7 to R99. `runtime_error:`: the issuer could not decide (no signing key, parent unknown, schema invalid), named by the issuer. `vendor::`: anything else, namespaced by whoever defines it. Prose goes in `reason`, never here, so two implementations can compare refusals by code.", "anyOf": [ { "pattern": "^R[1-9][0-9]?$" diff --git a/schemas/plugin-manifest.schema.json b/schemas/plugin-manifest.schema.json index a3038ef..5d141cb 100644 --- a/schemas/plugin-manifest.schema.json +++ b/schemas/plugin-manifest.schema.json @@ -85,7 +85,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -163,7 +163,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -899,7 +899,7 @@ }, "credentialFree": { "type": "string", - "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses — a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT — each as an I-Regexp without lookahead or word boundaries. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", + "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses (a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT), each a pattern without lookahead or word boundaries, which RE2 compiles. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", "allOf": [ { "not": { diff --git a/schemas/plugin-marketplace.schema.json b/schemas/plugin-marketplace.schema.json index 7d1dcac..b575eee 100644 --- a/schemas/plugin-marketplace.schema.json +++ b/schemas/plugin-marketplace.schema.json @@ -52,7 +52,7 @@ }, "read_paths": { "type": "array", - "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2 or I-Regexp validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", + "description": "Workspace-relative POSIX globs the agent may read. Refused here, without lookahead so any RE2-based validator can load the rule: a leading `/`, any `..` segment, a leading `~`, a drive-letter prefix and any backslash. The registry re-checks the resolved path against the workspace root.", "items": { "type": "string", "minLength": 1, @@ -130,7 +130,7 @@ "minLength": 1, "maxLength": 253, "pattern": "^(\\*|(\\*\\.)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*)$", - "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which is RFC 9485 I-Regexp with no lookahead: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." + "description": "A host the plugin asks to reach. A lower-case DNS name of one or more labels, an IPv4 literal or `localhost` (both are DNS-name shaped), with an optional single leading `*.` label, or the bare `*`. Refused by the pattern, which uses no lookahead or backreference and compiles under RE2: a scheme, a port, a path, whitespace, upper case, a trailing dot and a wildcard anywhere but the first label. A port is not a host: the egress proxy matches hostnames, and a rule nothing enforces is a claim. IP literals are admitted because a local model provider (Ollama, LM Studio) lives at 127.0.0.1 and the root policy derives its hosts from provider URLs." }, "$comment": "stability: stable" }, @@ -1179,7 +1179,7 @@ }, "credentialFree": { "type": "string", - "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses — a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT — each as an I-Regexp without lookahead or word boundaries. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", + "description": "A string that is not a credential. Refuses the seven shapes the reference evidence sanitiser refuses (a private key block, an AWS access key id, a GitHub token, an OpenAI key, a Slack token, a literal bearer token and a JWT), each a pattern without lookahead or word boundaries, which RE2 compiles. A detector, not a guarantee: it catches these shapes and nothing else, and the documented route for a secret remains the host's own environment (`$VAR` interpolation) or `headersHelper`.", "allOf": [ { "not": { diff --git a/tooling/build-site.mjs b/tooling/build-site.mjs index 220494c..68a5997 100644 --- a/tooling/build-site.mjs +++ b/tooling/build-site.mjs @@ -454,7 +454,7 @@ async function frontPage(ctx) { ${ways.map(([label, path, blurb]) => `
  • ${label} ${blurb}
  • `).join('\n')}
    -
    Current version
    ${esc(ctx.version)}
    +
    Current release
    ${esc(ctx.version)}
    Specification
    ${link(site('/spec/'), 'The Agent Lease Manifest')}: ${specInline(specHeader)}
    Licence
    ${esc(ctx.licence)}
    Repository
    ${link(REPO_URL, 'github.com/Cognitive-Delivery/contract')}
    @@ -462,13 +462,13 @@ ${ways.map(([label, path, blurb]) => `
  • = 0; +} + +/** A release's GitHub Release, or its tag when it has none, labelled as what it is. */ +function releaseLink(version, { long = false } = {}) { + return hasGithubRelease(version) + ? link(`${REPO_URL}/releases/tag/v${version}`, long ? `Release v${version}` : `release v${version}`) + : link(`${REPO_URL}/tree/v${version}`, long ? `Tag v${version} (no GitHub Release)` : `tag v${version} (no GitHub Release)`); +} + +/** `/versions/`: every frozen directory, its GitHub Release or tag and its changelog section. */ function versionsPage(ctx) { const rows = ctx.frozen.map((v) => `${esc(v.version)}` + `${link(site(`/${v.version}/`), `/${v.version}/`)}` - + `${link(`${REPO_URL}/releases/tag/v${v.version}`, `v${v.version}`)}` + + `${releaseLink(v.version)}` + `${link(`${site('/changelog/')}#${v.version}`, `CHANGELOG ${v.version}`)}`); const body = `

    Versions

    -

    The current schema set is ${esc(ctx.version)}, served at each schema's $id under /${esc(ctx.major)}.x/ and listed on the ${link(site('/'), 'front page')}. Each release is also served frozen under its own directory, byte for byte as tagged and never rewritten.

    -${rows.length === 0 ? '

    No frozen copies were laid out in this build.

    ' : `\n\n\n${rows.join('\n')}\n\n
    VersionFrozen copyReleaseChangelog
    `} +

    The current release is ${esc(ctx.version)}. Each schema's $id under /${esc(ctx.major)}.x/ serves the schema as it is on main, listed on the ${link(site('/'), 'front page')}; main may carry description-only changes ahead of the next tag. Each release is also served frozen under its own directory, byte for byte as tagged and never rewritten. Releases from ${esc(FIRST_GITHUB_RELEASE)} have a GitHub Release; the 1.0.x releases were tagged before the release workflow created GitHub Releases, so they link to their tags.

    +${rows.length === 0 ? '

    No frozen copies were laid out in this build.

    ' : `\n\n\n${rows.join('\n')}\n\n
    VersionFrozen copyGitHub Release or tagChangelog
    `} `; return { path: '/versions/', title: `Versions · ${SITE_NAME}`, body }; } @@ -629,7 +648,7 @@ function versionPage(ctx, v) {
      ${files}
    -

    ${link(`${REPO_URL}/releases/tag/v${v.version}`, `Release v${v.version}`)} and its ${link(`${site('/changelog/')}#${v.version}`, 'changelog section')}.

    +

    ${releaseLink(v.version, { long: true })} and its ${link(`${site('/changelog/')}#${v.version}`, 'changelog section')}.

    `; return { path: `/${v.version}/`, title: `contract ${v.version} · ${SITE_NAME}`, body }; } @@ -757,6 +776,15 @@ function selfTest() { try { siteHref('docs/missing.md', 'README.md', ctx); } catch (error) { refused = error instanceof SiteError; } check(refused, true, 'a link to a path not in the tree is refused'); check(pageFor(BRAND_NOTICE), '/brand-notice/', 'the brand notice has its page'); + + // A release before the first GitHub Release links to its tag and says so; later ones to the release. + check(hasGithubRelease('1.0.2'), false, '1.0.2 has no GitHub Release'); + check(hasGithubRelease('1.1.0'), true, '1.1.0 has a GitHub Release'); + check(hasGithubRelease('1.10.0'), true, 'versions compare numerically'); + const versions = versionsPage({ version: '1.2.1', major: '1', frozen: [{ version: '1.0.2', files: [] }, { version: '1.2.1', files: [] }] }).body; + check(versions.includes(`
    tag v1.0.2 (no GitHub Release)`), true, 'a 1.0.x row links its tag, labelled as one'); + check(versions.includes(`release v1.2.1`), true, 'a later row links its GitHub Release'); + check(versions.includes('releases/tag/v1.0.2'), false, 'no 1.0.x row claims a GitHub Release'); console.log(`build-site self-test: ${count} assertions passed`); } diff --git a/tooling/regex-portability/main.go b/tooling/regex-portability/main.go index 7559423..cf0057f 100644 --- a/tooling/regex-portability/main.go +++ b/tooling/regex-portability/main.go @@ -4,9 +4,11 @@ // load, and a schema set that carries one is not "usable from another language with // nothing else", whatever the README says. // -// RE2 syntax is a superset of RFC 9485 I-Regexp, which is the portability target the -// specification names, so passing here is necessary and not quite sufficient; it catches -// the construct implementers actually hit. +// The specification's portability target (SPEC section 4.4) is an ECMA-262 regular +// expression in the subset that also compiles under RE2, applied as an unanchored search. +// Passing here is necessary and not quite sufficient: it catches lookaround and +// backreferences, the constructs implementers actually hit, but not a construct both +// engines accept with different meanings. // // Usage, from this directory: go run . ../../schemas package main diff --git a/tooling/site-reference.mjs b/tooling/site-reference.mjs index 9ffb655..32d9887 100644 --- a/tooling/site-reference.mjs +++ b/tooling/site-reference.mjs @@ -127,13 +127,16 @@ function refName(node) { return node.$ref.slice('#/definitions/'.length); } -/** `enum` in full up to ten values; beyond that a count and three examples. */ +/** + * `enum` in full, always. Up to ten values inline; beyond that the count, then every value inside + * a `
    ` element, which the browser opens without a script. An earlier version showed three + * examples past ten, which dropped a constraint the index page says is shown. + */ function enumText(values, what = 'one of') { - const shown = values.length <= ENUM_IN_FULL ? values : values.slice(0, 3); - const list = shown.map((v) => json(v)).join(', '); + const list = values.map((v) => json(v)).join(', '); return values.length <= ENUM_IN_FULL ? `${what} ${list}` - : `${what} ${values.length} values, e.g. ${list}`; + : `${what} ${values.length} values
    all ${values.length}${list}
    `; } // --------------------------------------------------------------------------------------------- @@ -644,7 +647,7 @@ function indexPage(walkedAll, { base, major, siteName, stabilityLegend }) { }); const total = walkedAll.reduce((n, w) => n + w.propertyCount, 0); const body = `

    Schema reference

    -

    One page per schema of set ${esc(major)}.x, read from the schema files themselves: every declared property with its path, type, requirement, description, constraints and stability, and every definition a property refers to. The pages are generated from schemas/*.schema.json at every deploy, so they say what the schemas say and nothing else; the JSON each page links to is the file served at the schema's $id.

    +

    One page per schema of set ${esc(major)}.x, read from the schema files themselves: every declared property with its path, type, requirement, description, constraints and stability, and every definition a property refers to. An enumeration longer than ten values is shown in full under a disclosure; inside the one-line summary of a union branch, a deeper union or allOf is counted rather than listed, and the schema's JSON, linked from each page, is authoritative. The pages are generated from schemas/*.schema.json at every deploy, so they say what the schemas say and nothing else; the JSON each page links to is the file served at the schema's $id.

    @@ -837,9 +840,16 @@ export async function selfTestReference() { const reasonCode = leaseRecord.definitions.find((d) => d.name === 'reasonCode'); includes(reasonCode.rows[0].constraints.join('\n'), 'any of: (1) pattern ^R[1-9][0-9]?$; (2) pattern', 'a pattern-only branch is summarised without a type'); - // Enum collapse past ten values: the hooks event map's key list. + // An enum past ten values is rendered in full inside
    : the hooks event map's key list. const hooksMap = byName['plugin-manifest'].definitions.find((d) => d.name === 'hooksMap'); - includes(hooksMap.rows[0].constraints.join('\n'), 'keys one of 33 values, e.g.', 'an enum past ten values collapses to a count'); + const hooksKeys = hooksMap.rows[0].constraints.join('\n'); + includes(hooksKeys, 'keys one of 33 values
    all 33', 'an enum past ten values is counted and disclosed'); + const hookEvents = inputs.schemas.find((x) => x.file === 'plugin-manifest.schema.json').schema.definitions.hooksMap.propertyNames.enum; + check(hookEvents.length, 33, 'the hooks event map lists thirty-three events'); + for (const event of hookEvents) includes(hooksKeys, json(event), `every hook event is shown, ${event} among them`); + const eventTypes = byName['cdi-signal'].rows.find((r) => r.path === 'event_type'); + const eventTypeEnum = inputs.schemas.find((x) => x.file === 'cdi-signal.schema.json').schema.properties.event_type.enum; + for (const value of eventTypeEnum) includes(eventTypes.constraints.join('\n'), json(value), 'every cdi-signal event_type value is shown'); const dimensions = byName['cdi-assessment'].rows.find((r) => r.path === 'dimensions'); includes(dimensions.constraints.join('\n'), 'must contain an item: object, with id = "provenance-integrity", requires id', 'contains is rendered as a constraint'); diff --git a/tooling/site/BRAND-NOTICE.md b/tooling/site/BRAND-NOTICE.md index 12010ec..e9de246 100644 --- a/tooling/site/BRAND-NOTICE.md +++ b/tooling/site/BRAND-NOTICE.md @@ -11,5 +11,14 @@ the font files: Source Sans 3 under [OFL-source-sans-3.txt](fonts/OFL-source-san JetBrains Mono under [OFL-jetbrains-mono.txt](fonts/OFL-jetbrains-mono.txt). That licence permits redistribution with its text alongside, and that is how they are carried here. +Both texts are the upstream projects' own, copied on 6 October 2026: Source Sans 3's from +`adobe-fonts/source-sans` (`LICENSE.md` on its `release` branch) and JetBrains Mono's from +`JetBrains/JetBrainsMono` (`OFL.txt` on `master`). The Source Sans 3 text first carried here named +"Google Inc." as the copyright holder; the font is Adobe's, with the Reserved Font Name "Source", +and the upstream text says so. The JetBrains Mono text differed only in a duplicated copyright +phrase and the older licence URL, and was replaced with the upstream text for exactness. A modified +Source Sans 3 file may not carry the name "Source" (condition 3 of the licence); the site serves +the files as obtained and does not modify them. + Everything else on this site is under the repository's Apache-2.0 licence: the schemas, the fixtures, the conformance runner, the specification and the pages themselves. diff --git a/tooling/site/fonts/OFL-jetbrains-mono.txt b/tooling/site/fonts/OFL-jetbrains-mono.txt index 8f7ed67..5ceee00 100644 --- a/tooling/site/fonts/OFL-jetbrains-mono.txt +++ b/tooling/site/fonts/OFL-jetbrains-mono.txt @@ -1,8 +1,8 @@ -Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) JetBrainsMono-Italic[wght].ttf: Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL +https://openfontlicense.org ----------------------------------------------------------- @@ -18,7 +18,7 @@ with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, +fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The diff --git a/tooling/site/fonts/OFL-source-sans-3.txt b/tooling/site/fonts/OFL-source-sans-3.txt index 046fc66..22c601b 100644 --- a/tooling/site/fonts/OFL-source-sans-3.txt +++ b/tooling/site/fonts/OFL-source-sans-3.txt @@ -1,8 +1,8 @@ -Google Inc. +Copyright 2010-2024 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- @@ -18,7 +18,7 @@ with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, +fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The
    SchemaTitleDescriptionPropertiesRaw