Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
66498c6
refactor: Refine ADR-0004 documentation based on feedback
flyingrobots Nov 10, 2025
46ec339
docs: Clarify commit trailer format in F9-US-SEC acceptance criteria
flyingrobots Nov 10, 2025
676b6fa
docs: Clarify actor-id format in SPEC.md
flyingrobots Nov 10, 2025
7c35665
docs: Clarify size unit for OpaquePointer in SPEC.md
flyingrobots Nov 10, 2025
fce8bb1
docs: Clarify error handling for digest mismatch in SPEC.md Pointer R…
flyingrobots Nov 10, 2025
3d6bcbd
docs: Clarify PrivateStore participant as interface in TECH-SPEC.md d…
flyingrobots Nov 10, 2025
935ee36
docs: Add details for Projection Determinism test suite in TECH-SPEC.md
flyingrobots Nov 10, 2025
f643aea
docs: Update F9-US-DEV user story to BDD format in FEATURES.md
flyingrobots Nov 10, 2025
b39373c
docs: Mark gatos-compute as planned in SPEC.md system diagram
flyingrobots Nov 10, 2025
412b9b9
docs: Change PoC envelope storage requirement from SHOULD to MUST in …
flyingrobots Nov 10, 2025
c63785e
docs: Clarify purpose of gatos-kv crate in TECH-SPEC.md
flyingrobots Nov 10, 2025
1ca5ec6
docs: Mark chart data as illustrative in TECH-SPEC.md Performance Gui…
flyingrobots Nov 10, 2025
b5235c4
docs: Refine F9-US-DEV acceptance criteria to be more granular
flyingrobots Nov 10, 2025
8805e1b
docs: Simplify PrivateStore participant name in TECH-SPEC.md diagram
flyingrobots Nov 10, 2025
cc8d49e
docs: Remove redundant acceptance criteria for F9-US-DEV
flyingrobots Nov 10, 2025
f1e6f8b
docs: Add missing field description for digest in OpaquePointer
flyingrobots Nov 10, 2025
8cc6091
docs: Clarify interaction of expiration dates in Consensus Governance
flyingrobots Nov 10, 2025
f8de6f2
docs: Specify HTTP GET method for pointer resolution endpoint
flyingrobots Nov 10, 2025
e00b578
docs: Correct type casing in ADR-0004 OpaquePointer diagram
flyingrobots Nov 10, 2025
e48b7f9
docs: Further refine F9-US-DEV acceptance criteria with BDD-style gra…
flyingrobots Nov 10, 2025
b8b9504
docs: Remove redundant acceptance criteria for F9-US-DEV in FEATURES.md
flyingrobots Nov 10, 2025
5dca809
docs: Clarify sorting order for approvals in SPEC.md PoC section
flyingrobots Nov 10, 2025
a7b1a21
docs: Correct inconsistent endpoint URL in ADR-0004
flyingrobots Nov 10, 2025
d363688
docs(ADR-0004,SPEC,TECH-SPEC): switch resolver to POST + JWT; add opt…
flyingrobots Nov 10, 2025
4bbbccb
docs: harmonize DigestMismatch to 422 across SPEC/TECH-SPEC; ADR resp…
flyingrobots Nov 10, 2025
9d32a93
Merge remote-tracking branch 'origin/decisions/ADR-0004' into ADR-0005
flyingrobots Nov 10, 2025
79fde02
docs(ADR-0005, SPEC, TECH-SPEC): formalize Shiplog + shiplog-compat; …
flyingrobots Nov 10, 2025
ea6c357
docs/schemas(ADR-0005): add Shiplog ADR text, shiplog schemas and exa…
flyingrobots Nov 10, 2025
671141f
docs/schemas(ADR-0005): align envelope field ns->topic; add shiplog s…
flyingrobots Nov 10, 2025
20c5e28
docs(diagrams): regenerate after ADR-0005 edits
flyingrobots Nov 10, 2025
194d79f
docs(ADR-0005): rename envelope field ns->topic across diagrams, sche…
flyingrobots Nov 10, 2025
061d82c
docs/schemas(ADR-0005): normalize envelope key to ns; add MUST JCS wr…
flyingrobots Nov 10, 2025
563e6fc
docs(ADR-0005, SPEC, TECH-SPEC, ADR-0004): address review nits: envel…
flyingrobots Nov 10, 2025
38df747
schemas/docs: unify on topic (envelope); fix ; update trailer (repo_h…
flyingrobots Nov 10, 2025
474e137
schemas/docs: unify on topic across envelope/ADR; remove what.repo_he…
flyingrobots Nov 10, 2025
c3000ba
docs(privacy): pin AEAD to XChaCha20-Poly1305; enforce nonce uniquene…
flyingrobots Nov 10, 2025
5c4eecf
docs(ADR-0005): note that commit_oid in consumer checkpoints MUST be …
flyingrobots Nov 10, 2025
07802d4
gatos-privacy: make digest optional; add validate() + content_id_from…
flyingrobots Nov 10, 2025
1edcda4
build: fix AJV runner to use 'npx -y ajv-cli@5' (remove redundant 'aj…
flyingrobots Nov 10, 2025
66a7c2d
schemas: strict AJV fixes (types in if/then); consumer_checkpoint any…
flyingrobots Nov 10, 2025
436bd33
docs(FEATURES): add F6 Privacy Opaque Pointers (ADR‑0004) and move Sh…
flyingrobots Nov 11, 2025
c06a46d
build: use working AJV invocation (npx ajv-cli@5 ...); drop stray gat…
flyingrobots Nov 11, 2025
69784bb
build: fix truncated path to governance_min.json in schema-validate t…
flyingrobots Nov 11, 2025
bea1c90
docs/schemas: choose ns as canonical envelope field; align schema/exa…
flyingrobots Nov 11, 2025
75012bb
ADR-0005: ns/topic invariant clarifications; CLI sample cleanup; repl…
flyingrobots Nov 11, 2025
31276bd
ADR-0005: polish per review
flyingrobots Nov 11, 2025
3618a2c
docs: markdownlint fixes in ADR-0004 and ADR-0005 fences/blanks
flyingrobots Nov 11, 2025
179f2ba
tests(neg): add topic vs envelope.ns mismatch check for ADR-0005 work…
flyingrobots Nov 11, 2025
9da82ef
ADR-0004: Pin AEAD to XChaCha20-Poly1305; clarify AAD and nonce disci…
flyingrobots Nov 11, 2025
a418cb1
ADR-0004: Explicitly supersede AES-256-GCM; XChaCha20-Poly1305 is the…
flyingrobots Nov 11, 2025
ba91632
ADR-0005: Standardize on ns (namespace) over topic; fix refs, invaria…
flyingrobots Nov 11, 2025
9c08cac
ADR-0005: Fix mismatched fenced code block closers (remove ); ensure …
flyingrobots Nov 11, 2025
ebe9109
docs: mark hex-case and schema-path fixes as resolved in external rev…
flyingrobots Nov 11, 2025
4ede46d
ADR-0005: Clarify trailer structure — repo_head is top-level only (no…
flyingrobots Nov 11, 2025
f3ee31a
ADR-0005: Define per-namespace monotonic ULID algorithm and tie-break…
flyingrobots Nov 11, 2025
90fd868
ADR-0005: Disambiguate commit headers — Envelope-Schema and Trailer-S…
flyingrobots Nov 11, 2025
752eee2
ADR-0005: Clarify Hashing Law path — path is a blob inside the commit…
flyingrobots Nov 11, 2025
dfa0a6e
ADR-0005: Remove duplicate numeric-discipline text; reference §1 once
flyingrobots Nov 11, 2025
13d284d
ADR-0005: Unify error taxonomy names (TemporalOrder, AppendRejected);…
flyingrobots Nov 11, 2025
b92e9ea
ADR-0005: Specify tail() fairness (per-namespace round-robin) and wat…
flyingrobots Nov 11, 2025
e4556db
ADR-0005: Document anchor objects — when to write, consumer usage, an…
flyingrobots Nov 11, 2025
76483ff
ADR-0004: Standardize fenced code blocks — replace for proper rendering
flyingrobots Nov 11, 2025
9d11132
kill-check: add scripts for schema headers, ULID external reference, …
flyingrobots Nov 11, 2025
2a49f19
ADR-0005: Add external reference to ULID Spec §4.1 for monotonic orde…
flyingrobots Nov 11, 2025
c5efd80
ADR-0005/ADR-0004: Polish pass — stable round-robin, stronger repo_he…
flyingrobots Nov 11, 2025
2ab91ac
test(privacy): ciphertext-only opaque pointers should deserialize (sc…
flyingrobots Nov 11, 2025
a0b49cc
privacy: make OpaquePointer.digest optional; add runtime validate() i…
flyingrobots Nov 11, 2025
673ac3c
privacy: add VerifiedOpaquePointer with validate-on-deserialize; READ…
flyingrobots Nov 11, 2025
1379a29
ADR-0004: Implementation note — validate pointers on ingest (use Veri…
flyingrobots Nov 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"crates/gatos-mind",
"crates/gatos-echo",
"crates/gatos-policy",
"crates/gatos-privacy",
"crates/gatos-kv",
"crates/gatosd",
"bindings/wasm",
Expand Down
69 changes: 48 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ diagrams:
@bash -lc 'scripts/mermaid/generate_all.sh'

lint-md:
@bash -lc 'if command -v node >/dev/null 2>&1; then \
@bash -lc 'if command -v node >/dev/null 2>&1; then \
npx -y markdownlint-cli "**/*.md" --config .markdownlint.json; \
elif command -v docker >/dev/null 2>&1; then \
docker run --rm -v "$$PWD:/work" -w /work node:20 bash -lc "npx -y markdownlint-cli \"**/*.md\" --config .markdownlint.json"; \
else echo "Need Node.js or Docker" >&2; exit 1; fi'

fix-md:
@bash -lc 'if command -v node >/dev/null 2>&1; then \
@bash -lc 'if command -v node >/dev/null 2>&1; then \
npx -y markdownlint-cli "**/*.md" --fix --config .markdownlint.json; \
elif command -v docker >/dev/null 2>&1; then \
docker run --rm -v "$$PWD:/work" -w /work node:20 bash -lc "npx -y markdownlint-cli \"**/*.md\" --fix --config .markdownlint.json"; \
Expand All @@ -36,38 +36,65 @@ schema-compile:
@bash -lc 'set -euo pipefail; \
if ! command -v node >/dev/null 2>&1; then \
echo "Node.js required (or run in CI)" >&2; exit 1; fi; \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/job_manifest.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/proof_of_execution_envelope.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proposal.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/approval.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/grant.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/revocation.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proof_of_consensus_envelope.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json'
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/job_manifest.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/proof_of_execution_envelope.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proposal.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/approval.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/grant.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/revocation.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proof_of_consensus_envelope.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/privacy/opaque_pointer.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/event_envelope.schema.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/consumer_checkpoint.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/deployment_trailer.schema.json && \
npx -y ajv-cli@5 compile --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/anchor.schema.json'

schema-validate:
@bash -lc 'set -euo pipefail; \
if ! command -v node >/dev/null 2>&1; then \
echo "Node.js required (or run in CI)" >&2; exit 1; fi; \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/job_manifest.schema.json -d examples/v1/job/manifest_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/proof_of_execution_envelope.schema.json -d examples/v1/job/poe_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proposal.schema.json -d examples/v1/governance/proposal_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/approval.schema.json -d examples/v1/governance/approval_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/grant.schema.json -d examples/v1/governance/grant_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/revocation.schema.json -d examples/v1/governance/revocation_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proof_of_consensus_envelope.schema.json -d examples/v1/governance/poc_envelope_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d examples/v1/policy/governance_min.json'
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/job_manifest.schema.json -d examples/v1/job/manifest_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/job/proof_of_execution_envelope.schema.json -d examples/v1/job/poe_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proposal.schema.json -d examples/v1/governance/proposal_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/approval.schema.json -d examples/v1/governance/approval_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/grant.schema.json -d examples/v1/governance/grant_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/revocation.schema.json -d examples/v1/governance/revocation_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/governance/proof_of_consensus_envelope.schema.json -d examples/v1/governance/poc_envelope_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d examples/v1/policy/governance_min.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/privacy/opaque_pointer.schema.json -d examples/v1/privacy/opaque_pointer_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/event_envelope.schema.json -d examples/v1/shiplog/event_min.json -r schemas/v1/common/ids.schema.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/consumer_checkpoint.schema.json -d examples/v1/shiplog/checkpoint_min.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/deployment_trailer.schema.json -d examples/v1/shiplog/trailer_min.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/anchor.schema.json -d examples/v1/shiplog/anchor_min.json && \
npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/privacy/opaque_pointer.schema.json -d examples/v1/privacy/pointer_low_entropy_min.json'

schema-negative:
@bash -lc 'set -euo pipefail; \
if ! command -v node >/dev/null 2>&1; then \
echo "Node.js required (or run in CI)" >&2; exit 1; fi; \
# Negative: checkpoint requires both fields
! npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/consumer_checkpoint.schema.json -d examples/v1/shiplog/checkpoint_ulid_only_invalid.json; \
! npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/consumer_checkpoint.schema.json -d examples/v1/shiplog/checkpoint_commit_only_invalid.json; \
# Negative: low-entropy pointer must not allow plaintext digest
! npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/privacy/opaque_pointer.schema.json -d examples/v1/privacy/pointer_low_entropy_invalid.json'

.PHONY: kill-check
kill-check:
@bash -lc 'scripts/killcheck/schema_headers.sh'
@bash -lc 'scripts/killcheck/ulid_reference.sh'
@bash -lc 'scripts/killcheck/error_casing.sh'

schema-negative:
@bash -lc 'set -euo pipefail; \
if ! command -v node >/dev/null 2>&1; then \
echo "Node.js required (or run in CI)" >&2; exit 1; fi; \
echo "{\"governance\":{\"x\":{\"ttl\":\"P\"}}}" > /tmp/bad1.json; \
echo "{\"governance\":{\"x\":{\"ttl\":\"PT\"}}}" > /tmp/bad2.json; \
if npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d /tmp/bad1.json; then \
if npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d /tmp/bad1.json; then \
echo "Should have rejected ttl=P" >&2; exit 1; else echo "Rejected ttl=P as expected"; fi; \
if npx -y ajv-cli@5 ajv validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d /tmp/bad2.json; then \
if npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d /tmp/bad2.json; then \
echo "Should have rejected ttl=PT" >&2; exit 1; else echo "Rejected ttl=PT as expected"; fi'
Comment on lines +73 to 98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Duplicate schema-negative target breaks Makefile correctness.

Lines 73–81 and 89–98 both define schema-negative, causing the second definition to override the first. Make will only execute lines 89–98. The first block validates checkpoint and pointer constraints; the second validates TTL duration. Both are valid test suites—merge or rename to consolidate.

Suggested fix: Consolidate both blocks into a single schema-negative target:

schema-negative:
	@bash -lc 'set -euo pipefail; \
	 if ! command -v node >/dev/null 2>&1; then \
	   echo "Node.js required (or run in CI)" >&2; exit 1; fi; \
	 # Negative: checkpoint requires both fields
	 ! npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/consumer_checkpoint.schema.json -d examples/v1/shiplog/checkpoint_ulid_only_invalid.json; \
	 ! npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/shiplog/consumer_checkpoint.schema.json -d examples/v1/shiplog/checkpoint_commit_only_invalid.json; \
	 # Negative: low-entropy pointer must not allow plaintext digest
	 ! npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/privacy/opaque_pointer.schema.json -d examples/v1/privacy/pointer_low_entropy_invalid.json; \
	 # Negative: TTL duration validation
	 echo "{\"governance\":{\"x\":{\"ttl\":\"P\"}}}" > /tmp/bad1.json; \
	 echo "{\"governance\":{\"x\":{\"ttl\":\"PT\"}}}" > /tmp/bad2.json; \
	 if npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d /tmp/bad1.json; then \
	   echo "Should have rejected ttl=P" >&2; exit 1; else echo "Rejected ttl=P as expected"; fi; \
	 if npx -y ajv-cli@5 validate --spec=draft2020 --strict=true -c ajv-formats -s schemas/v1/policy/governance_policy.schema.json -d /tmp/bad2.json; then \
	   echo "Should have rejected ttl=PT" >&2; exit 1; else echo "Rejected ttl=PT as expected"; fi'
🤖 Prompt for AI Agents
In Makefile around lines 73–81 and 89–98 there are two separate definitions of
the schema-negative target causing the second to override the first; consolidate
both test suites into a single schema-negative target by merging the commands
from the first block (checkpoint and pointer AJV negative checks) with the
commands from the second block (TTL duration checks) into one @bash -lc 'set
-euo pipefail; ...' invocation, remove the duplicate target definition, keep or
place the .PHONY: kill-check entry unchanged, and ensure each command remains
separated by semicolons/line-continuations and preserves the existing
failure-expectation logic (leading ! for AJV-negative checks and if/then checks
for TTL).


schemas: schema-compile schema-validate schema-negative
Expand Down
12 changes: 12 additions & 0 deletions crates/gatos-privacy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "gatos-privacy"
version = "0.1.0"
edition = "2021"
Comment on lines +1 to +4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Missing package metadata.

The manifest lacks essential fields:

  • description (required for publication to crates.io)
  • license or license-file (legally required)
  • repository (helps users find source)
  • authors or readme

Add metadata:

 [package]
 name = "gatos-privacy"
 version = "0.1.0"
 edition = "2021"
+description = "GATOS privacy model: opaque pointers and low-entropy validation"
+license = "MIT OR Apache-2.0"  # or your chosen license
+repository = "https://github.com/flyingrobots/gatos"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[package]
name = "gatos-privacy"
version = "0.1.0"
edition = "2021"
[package]
name = "gatos-privacy"
version = "0.1.0"
edition = "2021"
description = "GATOS privacy model: opaque pointers and low-entropy validation"
license = "MIT OR Apache-2.0" # or your chosen license
repository = "https://github.com/flyingrobots/gatos"
🤖 Prompt for AI Agents
In crates/gatos-privacy/Cargo.toml around lines 1 to 4, the package manifest is
missing required metadata fields; add a description = "...", license = "..." (or
license-file = "LICENSE"), repository = "https://...", and either authors =
["Name <email>"] and/or readme = "README.md" (and ensure the referenced files
exist). Update the top-level [package] section to include those keys with
appropriate values and ensure the license string matches SPDX format so the
crate can be published.


[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
blake3 = { workspace = true }
hex = { workspace = true }
anyhow = { workspace = true }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Redundant error handling crates.

Both anyhow (line 11) and thiserror (line 12) are present. Typical usage:

  • thiserror for library error types (derive Error)
  • anyhow for application error handling (context, chaining)

Library crates usually only need thiserror. anyhow in a library often indicates the error API isn't well-designed (returning anyhow::Error loses type information for consumers).

Verify usage:

If anyhow is only used internally, consider removing it and using thiserror + Result<T, E> everywhere.


🏁 Script executed:

#!/bin/bash
# Check how anyhow is actually used in the crate

echo "=== anyhow usage in gatos-privacy ==="
rg -n --type=rust 'use anyhow|anyhow::' crates/gatos-privacy/src/

echo -e "\n=== thiserror usage ==="
rg -n --type=rust 'use thiserror|thiserror::' crates/gatos-privacy/src/

echo -e "\n=== Public error types ==="
rg -n --type=rust 'pub.*Error|pub enum.*Error' crates/gatos-privacy/src/

Length of output: 326


Remove unused anyhow dependency.

Script output confirms anyhow is never imported or used anywhere in the crate (crates/gatos-privacy/src/). The crate correctly uses thiserror to define PointerError (line 75) and returns properly typed Result<(), PointerError> in the public API (line 46). Dead code dependency should be purged from Cargo.toml line 11.

🤖 Prompt for AI Agents
In crates/gatos-privacy/Cargo.toml around line 11, the dependency entry "anyhow
= { workspace = true }" is unused in this crate; remove that line from
Cargo.toml to purge the dead dependency and then run cargo check/cargo build to
ensure no code depended on anyhow; if workspace-level changes require updating
lockfiles run cargo update and commit the updated Cargo.toml (and Cargo.lock if
modified).

thiserror = "1"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Loose thiserror version constraint enables breaking changes.

Using thiserror = "1" allows any version from 1.0.0 to 1.999.999, including breaking changes within the v1 major version (which happens in practice despite semver).

Pin to a specific minor version:

-thiserror = "1"
+thiserror = "1.0"

Or even better, use workspace version like the other dependencies:

-thiserror = "1"
+thiserror = { workspace = true }

Then define it in the workspace Cargo.toml for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
thiserror = "1"
thiserror = "1.0"
🤖 Prompt for AI Agents
In crates/gatos-privacy/Cargo.toml around line 12, the dependency thiserror is
specified as "1", which is too loose and can pull incompatible changes; update
the entry to pin a specific minor (for example "1.0.x" or "1.1" style) or,
better, replace it with a workspace-managed version placeholder (e.g. use
workspace = true or the same pattern used by other deps) and then add the
concrete version to the workspace Cargo.toml so all crates share a fixed
thiserror minor version; ensure the local Cargo.toml matches the workspace
convention and run cargo update to verify the lockfile.

33 changes: 33 additions & 0 deletions crates/gatos-privacy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# gatos-privacy

Opaque Pointer types and helpers for the GATOS hybrid privacy model (ADR-0004).

Key types
- `OpaquePointer`: JSON-facing struct that mirrors `schemas/v1/privacy/opaque_pointer.schema.json`.
- `digest: Option<String>` — plaintext digest (may be omitted)
- `ciphertext_digest: Option<String>` — ciphertext digest
- `extensions.class = "low-entropy"` implies `ciphertext_digest` MUST be present and `digest` MUST be absent.

- `VerifiedOpaquePointer`: wrapper that enforces invariants during deserialization.
- Use this at trust boundaries to guarantee the low-entropy rules.

Validation
- After deserializing `OpaquePointer`, call `pointer.validate()` to enforce:
- At least one of `digest` or `ciphertext_digest` is present.
- Low-entropy class requires `ciphertext_digest` and forbids `digest`.

Examples
```rust
use gatos_privacy::{OpaquePointer, VerifiedOpaquePointer};

// 1) Verified wrapper enforces invariants automatically
let v: VerifiedOpaquePointer = serde_json::from_str(json)?;

// 2) Manual validation on the plain struct
let p: OpaquePointer = serde_json::from_str(json)?;
p.validate()?;
```

Canonicalization
- When computing content IDs or digests, serialize JSON with RFC 8785 JCS (performed by higher layers).

110 changes: 110 additions & 0 deletions crates/gatos-privacy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! gatos-privacy — Opaque Pointer types and helpers
//!
//! This crate defines the JSON-facing pointer envelope used by the
//! hybrid privacy model (ADR-0004). The struct mirrors the v1 schema
//! in `schemas/v1/privacy/opaque_pointer.schema.json`.
//!
//! Canonicalization: when computing content IDs or digests, callers
//! MUST serialize JSON using RFC 8785 JCS. This crate intentionally
//! does not take a dependency on a specific JCS implementation to
//! keep the workspace lean; higher layers may provide one.

use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OpaquePointer {
pub kind: Kind,
pub algo: Algo,
#[serde(skip_serializing_if = "Option::is_none")]
pub digest: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ciphertext_digest: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
pub location: String,
pub capability: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Value>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Kind {
OpaquePointer,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Algo {
Blake3,
}

impl OpaquePointer {
/// Validate invariants beyond serde schema mapping.
pub fn validate(&self) -> Result<(), PointerError> {
let has_plain = self.digest.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
let has_cipher = self
.ciphertext_digest
.as_ref()
.map(|s| !s.is_empty())
.unwrap_or(false);
if !(has_plain || has_cipher) {
return Err(PointerError::MissingDigest);
}
let low_entropy = self
.extensions
.as_ref()
.and_then(|v| v.get("class"))
.and_then(|c| c.as_str())
.map(|s| s == "low-entropy")
.unwrap_or(false);
if low_entropy {
if !has_cipher {
return Err(PointerError::LowEntropyNeedsCiphertextDigest);
}
if has_plain {
return Err(PointerError::LowEntropyForbidsPlainDigest);
}
}
Ok(())
}
}

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum PointerError {
#[error("at least one of digest or ciphertext_digest is required")]
MissingDigest,
#[error("low-entropy class requires ciphertext_digest")]
LowEntropyNeedsCiphertextDigest,
#[error("low-entropy class forbids plaintext digest")]
LowEntropyForbidsPlainDigest,
}

/// A validated wrapper that enforces `OpaquePointer::validate()` during
/// deserialization. Prefer this type when accepting pointers from untrusted
/// inputs; it guarantees schema-level invariants at the boundary.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct VerifiedOpaquePointer(pub OpaquePointer);

impl<'de> Deserialize<'de> for VerifiedOpaquePointer {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let inner = OpaquePointer::deserialize(deserializer)?;
inner
.validate()
.map_err(serde::de::Error::custom)?;
Ok(Self(inner))
}
}

impl core::ops::Deref for VerifiedOpaquePointer {
type Target = OpaquePointer;
fn deref(&self) -> &Self::Target {
&self.0
}
}
23 changes: 23 additions & 0 deletions crates/gatos-privacy/tests/pointer_schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use gatos_privacy::OpaquePointer;

fn read_example(rel: &str) -> String {
let dir = env!("CARGO_MANIFEST_DIR");
std::fs::read_to_string(format!("{}/../../examples/v1/{}", dir, rel)).unwrap()
}
Comment on lines +3 to +6
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bare unwrap() will produce cryptic panic messages.

The read_example helper uses .unwrap() on file I/O without context. When this fails in CI, the error message will be unhelpful (just "called unwrap() on an Err value").

Apply this diff to provide actionable error context:

 fn read_example(rel: &str) -> String {
     let dir = env!("CARGO_MANIFEST_DIR");
-    std::fs::read_to_string(format!("{}/../../examples/v1/{}", dir, rel)).unwrap()
+    let path = format!("{}/../../examples/v1/{}", dir, rel);
+    std::fs::read_to_string(&path)
+        .unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn read_example(rel: &str) -> String {
let dir = env!("CARGO_MANIFEST_DIR");
std::fs::read_to_string(format!("{}/../../examples/v1/{}", dir, rel)).unwrap()
}
fn read_example(rel: &str) -> String {
let dir = env!("CARGO_MANIFEST_DIR");
let path = format!("{}/../../examples/v1/{}", dir, rel);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", path, e))
}
🤖 Prompt for AI Agents
In crates/gatos-privacy/tests/pointer_schema.rs around lines 3 to 6, the helper
read_example uses .unwrap() on read_to_string which yields unhelpful panic
messages; replace the unwrap with a fallible call that includes the file path
and the I/O error in the message (for example use .expect or .map_err with a
formatted string containing the resolved path and the error) so failures in CI
show the exact file attempted and the underlying error; ensure the path is
computed first into a variable and included in the error message.


#[test]
fn ciphertext_only_pointer_should_deserialize() {
// This example omits plaintext digest by design (low-entropy class)
let json = read_example("privacy/pointer_low_entropy_min.json");
let ptr: Result<OpaquePointer, _> = serde_json::from_str(&json);
assert!(ptr.is_ok(), "ciphertext-only opaque pointer must deserialize");
}
Comment on lines +8 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Test lacks negative assertions.

This test verifies successful deserialization but doesn't validate that the resulting pointer satisfies low-entropy constraints (e.g., digest.is_none(), extensions.class == Some("low-entropy")). A bug in the validation logic could silently accept invalid pointers.

Strengthen the test:

 #[test]
 fn ciphertext_only_pointer_should_deserialize() {
     // This example omits plaintext digest by design (low-entropy class)
     let json = read_example("privacy/pointer_low_entropy_min.json");
-    let ptr: Result<OpaquePointer, _> = serde_json::from_str(&json);
-    assert!(ptr.is_ok(), "ciphertext-only opaque pointer must deserialize");
+    let ptr: OpaquePointer = serde_json::from_str(&json)
+        .expect("ciphertext-only opaque pointer must deserialize");
+    assert!(ptr.digest.is_none(), "low-entropy pointer must omit plaintext digest");
+    assert!(ptr.ciphertext_digest.is_some(), "low-entropy pointer must include ciphertext_digest");
+    assert_eq!(
+        ptr.extensions.as_ref().and_then(|e| e.get("class")).and_then(|v| v.as_str()),
+        Some("low-entropy"),
+        "low-entropy pointer must have extensions.class='low-entropy'"
+    );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn ciphertext_only_pointer_should_deserialize() {
// This example omits plaintext digest by design (low-entropy class)
let json = read_example("privacy/pointer_low_entropy_min.json");
let ptr: Result<OpaquePointer, _> = serde_json::from_str(&json);
assert!(ptr.is_ok(), "ciphertext-only opaque pointer must deserialize");
}
#[test]
fn ciphertext_only_pointer_should_deserialize() {
// This example omits plaintext digest by design (low-entropy class)
let json = read_example("privacy/pointer_low_entropy_min.json");
let ptr: OpaquePointer = serde_json::from_str(&json)
.expect("ciphertext-only opaque pointer must deserialize");
assert!(ptr.digest.is_none(), "low-entropy pointer must omit plaintext digest");
assert!(ptr.ciphertext_digest.is_some(), "low-entropy pointer must include ciphertext_digest");
assert_eq!(
ptr.extensions.as_ref().and_then(|e| e.get("class")).and_then(|v| v.as_str()),
Some("low-entropy"),
"low-entropy pointer must have extensions.class='low-entropy'"
);
}
🤖 Prompt for AI Agents
In crates/gatos-privacy/tests/pointer_schema.rs around lines 8 to 14, the test
only asserts deserialization succeeded but does not assert that the deserialized
OpaquePointer meets low-entropy expectations; update the test to unwrap the
Result, inspect the resulting pointer, and add assertions that digest.is_none(),
that extensions.class == Some("low-entropy") (or equivalent field name in your
struct), and any other low-entropy-specific invariants (e.g., absence of
plaintext fields). Ensure you handle unwrap safely in the test (use expect with
a helpful message) and add concise assertions verifying the low-entropy
constraints.


#[test]
fn both_digests_allowed_when_not_low_entropy() {
let json = read_example("privacy/opaque_pointer_min.json");
let ptr: OpaquePointer = serde_json::from_str(&json).unwrap();
let has_digest = ptr.digest.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
assert!(has_digest);
assert!(ptr.ciphertext_digest.is_some());
}
Comment on lines +1 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Missing negative test cases.

The test suite only validates successful deserialization. Add tests for:

  • Invalid low-entropy pointer with plaintext digest present (should fail)
  • Pointer with neither digest nor ciphertext_digest (should fail)
  • Invalid ULID format (if applicable)
  • Invalid URI schemes
  • Malformed blake3 digests

Do you want me to generate comprehensive negative test cases, or open an issue to track this?

🤖 Prompt for AI Agents
In crates/gatos-privacy/tests/pointer_schema.rs around lines 1 to 23, add
negative unit tests to complement the existing positive cases: create a test
that loads the low-entropy example JSON and injects a non-empty plaintext digest
(expect serde_json::from_str to return Err), a test that removes both digest and
ciphertext_digest fields from a valid pointer JSON (expect Err), a test that
replaces the ULID field with an invalid string (expect Err or validation failure
during deserialization), a test that sets the URI scheme to an unsupported value
(expect Err), and a test that corrupts the blake3 digest string (wrong
length/characters) (expect Err); implement each by reading a base example (use
read_example), programmatically modifying the JSON string (or parsing to a
serde_json::Value and altering fields) and asserting deserialization fails.

Comment on lines +16 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Weak validation: checking !is_empty() on Option-wrapped strings.

Lines 20-21 map over ptr.digest to check if it's non-empty, but this doesn't verify the digest format, length, or prefix (blake3:). The test could pass with invalid digest values like "x".

Add format validation:

 #[test]
 fn both_digests_allowed_when_not_low_entropy() {
     let json = read_example("privacy/opaque_pointer_min.json");
     let ptr: OpaquePointer = serde_json::from_str(&json).unwrap();
-    let has_digest = ptr.digest.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
-    assert!(has_digest);
+    assert!(
+        ptr.digest.as_ref().map(|s| s.starts_with("blake3:") && s.len() == 71).unwrap_or(false),
+        "non-low-entropy pointer should have valid blake3 digest (blake3: + 64 hex chars)"
+    );
     assert!(ptr.ciphertext_digest.is_some());
+    assert!(
+        ptr.ciphertext_digest.as_ref().map(|s| s.starts_with("blake3:") && s.len() == 71).unwrap_or(false),
+        "ciphertext_digest should be valid blake3 format"
+    );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn both_digests_allowed_when_not_low_entropy() {
let json = read_example("privacy/opaque_pointer_min.json");
let ptr: OpaquePointer = serde_json::from_str(&json).unwrap();
let has_digest = ptr.digest.as_ref().map(|s| !s.is_empty()).unwrap_or(false);
assert!(has_digest);
assert!(ptr.ciphertext_digest.is_some());
}
#[test]
fn both_digests_allowed_when_not_low_entropy() {
let json = read_example("privacy/opaque_pointer_min.json");
let ptr: OpaquePointer = serde_json::from_str(&json).unwrap();
assert!(
ptr.digest.as_ref().map(|s| s.starts_with("blake3:") && s.len() == 71).unwrap_or(false),
"non-low-entropy pointer should have valid blake3 digest (blake3: + 64 hex chars)"
);
assert!(ptr.ciphertext_digest.is_some());
assert!(
ptr.ciphertext_digest.as_ref().map(|s| s.starts_with("blake3:") && s.len() == 71).unwrap_or(false),
"ciphertext_digest should be valid blake3 format"
);
}
🤖 Prompt for AI Agents
In crates/gatos-privacy/tests/pointer_schema.rs around lines 16 to 23, the test
currently only checks ptr.digest for non-empty string which allows invalid
values; replace that weak check with a validation that the Option is Some and
the string starts with the "blake3:" prefix and that the suffix is a
64-character lowercase hex string (or otherwise the expected blake3 hex length),
e.g. extract the substring after "blake3:", verify its length and that all chars
are hex (0-9a-f) — use a simple regex or explicit checks — then assert that
ciphertext_digest is Some as before.

22 changes: 22 additions & 0 deletions crates/gatos-privacy/tests/verified.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use gatos_privacy::{OpaquePointer, VerifiedOpaquePointer};

fn read_example(rel: &str) -> String {
let dir = env!("CARGO_MANIFEST_DIR");
std::fs::read_to_string(format!("{}/../../examples/v1/{}", dir, rel)).unwrap()
}

#[test]
fn verified_accepts_ciphertext_only_low_entropy() {
let json = read_example("privacy/pointer_low_entropy_min.json");
let v: VerifiedOpaquePointer = serde_json::from_str(&json).expect("verified deserialize");
assert!(v.ciphertext_digest.is_some());
assert!(v.digest.is_none());
}

#[test]
fn verified_rejects_low_entropy_with_plain_digest() {
let json = read_example("privacy/pointer_low_entropy_invalid.json");
let v: Result<VerifiedOpaquePointer, _> = serde_json::from_str(&json);
assert!(v.is_err(), "should reject invalid low-entropy pointer");
}

Loading
Loading