From d7264b843df6cd82e0af3901d1d4f00bcde991ca Mon Sep 17 00:00:00 2001 From: mptyl Date: Sun, 9 Aug 2026 20:47:30 +0200 Subject: [PATCH] fix: complete evidence documentation contract --- docs/contracts/workspace-evidence-v3.md | 22 +++++++---- scripts/test-verify-workspace-install-docs.sh | 37 +++++++++++++++++-- scripts/verify-workspace-install-docs.sh | 23 ++++++++++-- 3 files changed, 69 insertions(+), 13 deletions(-) diff --git a/docs/contracts/workspace-evidence-v3.md b/docs/contracts/workspace-evidence-v3.md index 03b35aea..68e642cf 100644 --- a/docs/contracts/workspace-evidence-v3.md +++ b/docs/contracts/workspace-evidence-v3.md @@ -37,7 +37,9 @@ have no whitespace, control character, backslash, userinfo, query, or fragment. `authentication: none`; signed mode uses `authentication: signed_urls_file`. Defaults are `authentication: none`, `connect_timeout_ms: 5000`, `read_timeout_ms: 30000`, `max_bytes: 10485760`, `max_redirects: 5`, `allow_private_hosts: false`, and -`max_cache_bytes: 67108864`. +`max_cache_bytes: 67108864`. Public HTTP (`authentication: none`) uses the declared query-free +URIs directly and requires no Evidence credential file. Signed HTTP uses the installation file +contract below and preserves a mandatory one-to-one provenance mapping in declared-URI order. ### Example: http @@ -72,6 +74,8 @@ Defaults are `credentials: ambient`, `trusted_endpoint: false`, `allow_private_e `allow_insecure_endpoint: false`, `max_bytes: 10485760`, `max_objects: 10000`, `max_pages: 100`, and `page_size: 1000` (and page size cannot exceed 1000). A custom endpoint requires `trusted_endpoint: true`; an HTTP endpoint additionally requires `allow_insecure_endpoint: true`. +Endpoint-policy flags cannot be enabled without `endpoint_url`. Ambient S3 +(`credentials: ambient`) uses the runtime provider chain and requires no Evidence credential file. Static mode uses `credentials: static_files`, requires access-key and secret-key files together, and permits an optional session-token file. @@ -99,10 +103,14 @@ Safe: `s3://example-evidence/curated/`. Unsafe identities include `s3://Invalid_Bucket/evidence` and `s3://example-evidence/evidence#section`. S3 userinfo and query identities are rejected in prose and implementation; no credential-bearing example is published. -## Policy +## Policy and numeric domains The strict policy defaults to `max_chunk_chars: 4000` and -`retain_published_generations: 3`. +`retain_published_generations: 3`. Filesystem `max_bytes`; HTTP `connect_timeout_ms`, +`read_timeout_ms`, `max_bytes`, and `max_cache_bytes`; S3 `max_bytes`, `max_objects`, `max_pages`, +and `page_size`; and both policy values must be positive safe integers from 1 through +9007199254740991. HTTP `max_redirects` must be a nonnegative safe integer from 0 through +9007199254740991. S3 `page_size` has the stricter maximum of 1000. ## Installation files @@ -111,12 +119,12 @@ hold file paths, never credential or signed-URL values. | Mode | Variable | File contract | | --- | --- | --- | -| Signed HTTP | `THT_WS__EVIDENCE_SIGNED_URLS_FILE` | Required for `signed_urls_file`; nonempty JSON string array in declared-URI order; query-stripped identities must match `uris`. | -| Static S3 pair | `THT_WS__EVIDENCE_ACCESS_KEY_FILE` and `THT_WS__EVIDENCE_SECRET_KEY_FILE` | Required together for `static_files`. | -| Static S3 session | `THT_WS__EVIDENCE_SESSION_TOKEN_FILE` | Optional, and valid only with the required access/secret pair. | +| Signed HTTP | `THT_WS__EVIDENCE_SIGNED_URLS_FILE` | Required for `signed_urls_file`; at most 1048576 bytes; nonempty UTF-8 JSON string array in declared-URI order; query-stripped identities must match `uris` one-to-one. | +| Static S3 pair | `THT_WS__EVIDENCE_ACCESS_KEY_FILE` and `THT_WS__EVIDENCE_SECRET_KEY_FILE` | Required together for `static_files`; each file is at most 65536 bytes. | +| Static S3 session | `THT_WS__EVIDENCE_SESSION_TOKEN_FILE` | Optional, valid only with the required access/secret pair, and at most 65536 bytes. | Every variable is an absolute path to a readable regular file whose resolved target is strictly below one of the roots configured by `THT_WORKSPACE_SECRET_ROOTS`. Scalar S3 files are nonempty -UTF-8 tokens without whitespace. Public docs, exports, and rendered YAML never expose file contents. `changeme`, `replace-me`, `YOUR_SECRET`, ``, access-key-looking strings, and any +UTF-8 tokens without whitespace or NUL. Public docs, exports, and rendered YAML never expose file contents. `changeme`, `replace-me`, `YOUR_SECRET`, ``, access-key-looking strings, and any credential-bearing or query-bearing URI are forbidden as public placeholder values. ## One shared registry repository diff --git a/scripts/test-verify-workspace-install-docs.sh b/scripts/test-verify-workspace-install-docs.sh index 6c586bf0..df5f6701 100755 --- a/scripts/test-verify-workspace-install-docs.sh +++ b/scripts/test-verify-workspace-install-docs.sh @@ -486,21 +486,40 @@ elif mutation == "wrong-docs-directory": "workspaces/.env.example\nworkspaces/.md", 1, ) +elif mutation == "public-http-mode-omitted": + changed = original.replace( + "Public HTTP (`authentication: none`) uses the declared query-free\nURIs directly and requires no Evidence credential file.", + "", + 1, + ) +elif mutation == "ambient-s3-mode-omitted": + changed = original.replace( + "Ambient S3\n(`credentials: ambient`) uses the runtime provider chain and requires no Evidence credential file.", + "", + 1, + ) +elif mutation == "numeric-domains-omitted": + changed = original.replace("positive safe integers", "positive integers", 1) + changed = changed.replace("nonnegative safe integer", "nonnegative integer", 1) +elif mutation == "endpoint-without-url-invariant-omitted": + changed = original.replace( + "Endpoint-policy flags cannot be enabled without `endpoint_url`.", "", 1 + ) elif mutation == "http-file-boundary-omitted": changed = original.replace( - "| Signed HTTP | `THT_WS__EVIDENCE_SIGNED_URLS_FILE` | Required for `signed_urls_file`; nonempty JSON string array in declared-URI order; query-stripped identities must match `uris`. |\n", + "| Signed HTTP | `THT_WS__EVIDENCE_SIGNED_URLS_FILE` | Required for `signed_urls_file`; at most 1048576 bytes; nonempty UTF-8 JSON string array in declared-URI order; query-stripped identities must match `uris` one-to-one. |\n", "", 1, ) elif mutation == "s3-pair-boundary-omitted": changed = original.replace( - "| Static S3 pair | `THT_WS__EVIDENCE_ACCESS_KEY_FILE` and `THT_WS__EVIDENCE_SECRET_KEY_FILE` | Required together for `static_files`. |\n", + "| Static S3 pair | `THT_WS__EVIDENCE_ACCESS_KEY_FILE` and `THT_WS__EVIDENCE_SECRET_KEY_FILE` | Required together for `static_files`; each file is at most 65536 bytes. |\n", "", 1, ) elif mutation == "s3-token-boundary-omitted": changed = original.replace( - "| Static S3 session | `THT_WS__EVIDENCE_SESSION_TOKEN_FILE` | Optional, and valid only with the required access/secret pair. |\n", + "| Static S3 session | `THT_WS__EVIDENCE_SESSION_TOKEN_FILE` | Optional, valid only with the required access/secret pair, and at most 65536 bytes. |\n", "", 1, ) @@ -920,6 +939,18 @@ expect_evidence_fixture_rejected \ expect_evidence_fixture_rejected \ "generated docs in wrong directory" docs/contracts/workspace-evidence-v3.md wrong-docs-directory \ "generated docs path invalid" +expect_evidence_fixture_rejected \ + "public HTTP mode omitted" docs/contracts/workspace-evidence-v3.md public-http-mode-omitted \ + "missing public HTTP mode" +expect_evidence_fixture_rejected \ + "ambient S3 mode omitted" docs/contracts/workspace-evidence-v3.md ambient-s3-mode-omitted \ + "missing ambient S3 mode" +expect_evidence_fixture_rejected \ + "strict Evidence numeric domains omitted" docs/contracts/workspace-evidence-v3.md numeric-domains-omitted \ + "missing strict Evidence numeric domains" +expect_evidence_fixture_rejected \ + "S3 endpoint policy without endpoint invariant omitted" docs/contracts/workspace-evidence-v3.md endpoint-without-url-invariant-omitted \ + "missing S3 endpoint policy without endpoint invariant" expect_evidence_fixture_rejected \ "signed HTTP file boundary omitted" docs/contracts/workspace-evidence-v3.md http-file-boundary-omitted \ "missing signed HTTP file boundary" diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh index 5a13022d..819e23ab 100755 --- a/scripts/verify-workspace-install-docs.sh +++ b/scripts/verify-workspace-install-docs.sh @@ -348,6 +348,18 @@ for phrase in required_contract_phrases: if phrase not in contract: raise SystemExit(f"workspace Evidence contract lacks required rule: {phrase}") +mode_rules = { + "missing public HTTP mode": "Public HTTP (`authentication: none`) uses the declared query-free\nURIs directly and requires no Evidence credential file.", + "missing ambient S3 mode": "Ambient S3\n(`credentials: ambient`) uses the runtime provider chain and requires no Evidence credential file.", +} +for error, phrase in mode_rules.items(): + if phrase not in contract: + raise SystemExit(error) +if "positive safe integers" not in contract or "nonnegative safe integer" not in contract or "9007199254740991" not in contract: + raise SystemExit("missing strict Evidence numeric domains") +if "Endpoint-policy flags cannot be enabled without `endpoint_url`." not in contract: + raise SystemExit("missing S3 endpoint policy without endpoint invariant") + # The canonical one-repository tree is exact, including generated docs outside workspaces/. legacy_docs = re.compile(r"workspaces/(?:<[^>]+>|[^\s`/]+)\.(?:env\.example|md)") all_public = "\n".join(path.read_text() for path in paths) @@ -396,17 +408,22 @@ if no_scope not in contract or re.search(r"P1\s+(?:materializes|extracts|indexes installation_rows = {row[0]: row[1:] for row in table_for("Installation files")} http_row = " ".join(installation_rows.get("Signed HTTP", [])) if "THT_WS__EVIDENCE_SIGNED_URLS_FILE" not in http_row or not all( - token in http_row for token in ("nonempty JSON string array", "declared-URI order", "query-stripped identities") + token in http_row for token in ( + "1048576 bytes", "nonempty UTF-8 JSON string array", "declared-URI order", + "query-stripped identities", "one-to-one", + ) ): raise SystemExit("missing signed HTTP file boundary") s3_pair = " ".join(installation_rows.get("Static S3 pair", [])) if not all(token in s3_pair for token in ( "THT_WS__EVIDENCE_ACCESS_KEY_FILE", - "THT_WS__EVIDENCE_SECRET_KEY_FILE", "Required together", + "THT_WS__EVIDENCE_SECRET_KEY_FILE", "Required together", "65536 bytes", )): raise SystemExit("missing static S3 file boundary") s3_token = " ".join(installation_rows.get("Static S3 session", [])) -if "THT_WS__EVIDENCE_SESSION_TOKEN_FILE" not in s3_token or "Optional" not in s3_token: +if not all(token in s3_token for token in ( + "THT_WS__EVIDENCE_SESSION_TOKEN_FILE", "Optional", "65536 bytes", +)): raise SystemExit("missing static S3 session-token boundary") if "tht config check -c " not in contract: