11. Shared truth across repos: distributing a schema package
Shared truth across repos: a schema package published, served and synced into a consumer
Exercises publish/sync/get/why, pkg tidy/verify/serve, three lockfile pins, integrity errors, #aon1-… inline pins
Rendered from
use-cases/11-shared-modules/README.md
in the engine repository. The models, the expected output and the
check.sh that drives the CLI over all of it are in
use-cases/11-shared-modules/.
Scenario. A platform team owns the service deployment contract for
corp.example. Application teams (here, the checkout team) validate
their service definitions against that contract from another
repository, and an agent working in the checkout repo can trust that
the contract it sees is the contract the platform team approved. The
case covers module identity (corp.example/schemas/service), the two
ways a package reaches a consumer (a copy by hand into
aontu_meta/vendor/, and aontu sync from a repository the platform
team publishes into), the three lock pins, aontu pkg keygen,
aontu publish and its compatibility gate, aontu pkg serve, the
cooldown, and a package that has moved.
Everything below was produced by the real CLI; check.sh re-runs all
of it (54 assertions) and exits 0. All output shown is verbatim with
ANSI codes stripped.
Layout
platform/service/ the package as published (1.4.2)
platform/service-next-compat/ 1.4.3 candidate (compatible widening)
platform/service-next-breaking/ 1.5.0 candidate (required key added)
platform/service2/ the same breaking change as a new package path
consumer/ the checkout repo: pkg.aon, main.aon, gate.aon,
plus the committed aontu_meta/pkg-lock.aon and
aontu_meta/vendor/ tree exactly as `aontu sync`
left them
data/ agent-emitted service candidates (JSON)
probes/refactor/ byte-different, meaning-identical module refactor
probes/transitive/ a package that depends on another package
probes/nested-ref/ a module with an internal $.-reference
expected/ goldens (consumer output, tidy report, manifest)
Language features carrying the model: close() so consumers cannot
invent fields the platform does not operate; constraint atoms
(re, min, max) for field vocabularies; *pref defaults so a
two-line service definition renders a full deployment record; k?:
for the compatible-evolution probe; hide() to keep the imported
schema out of rendered output; module imports
@"corp.example/schemas/service" and the inline #aon1-… pinned form.
The module is written self-contained, with no $.-references between
its own top-level keys: $ is the root of the importing document, so
a module imported at a nested consumer key cannot reach its own keys
that way (probes/nested-ref/ shows the refusal).
The model tree
consumer/main.aon is a consumer repository’s entry: it imports the
platform team’s module and writes its own services against it. lib is
what the module brought (the deployment spec, with its defaults and
bounds) and srv is what this repository owns.
$
├── lib
│ └── spec
│ ├── healthcheck (2)
│ ├── name re("^[a-z][a-z0-9-]{2,39}$")
│ ├── owner re("^[a-z0-9.-]+@corp[.]examp...
│ ├── port *8080|integer&min(1024)&max(6...
│ ├── replicas *2|integer&min(1)&max(64)
│ ├── telemetry (2)
│ └── tier "critical"|"standard"|*"inter...
└── srv
├── checkout
│ ├── healthcheck (2)
│ ├── name "checkout"
│ ├── owner "payments-core@corp.example"
│ ├── port 9091
│ ├── replicas 6
│ ├── telemetry (2)
│ └── tier "critical"
└── gift-cards
├── healthcheck (2)
├── name "gift-cards"
├── owner "promo@corp.example"
├── port *8080|integer&min(1024)&max(6...
├── replicas *2|integer&min(1)&max(64)
├── telemetry (2)
└── tier "critical"|"standard"|*"inter...
aontu view doc --depth 3 consumer/main.aon draws it, and check.sh pins it
with --out --check. A key with (n) after it is a container the
depth bound stopped at, and n is how many keys are not drawn; a
leaf carries its canon, which is the kind of thing it is rather
than its value.
The distribution flow
1. Cold start. The consumer declares
dep: "corp.example/schemas/service": v: "1.4.2" and has received
nothing. aontu pkg tidy consumer/ exits 1:
verdict: missing
corp.example/schemas/service: not fetched (run: aontu sync)
and writes no lockfile rather than a partial one.
2. Distribution is a copy, or a sync. With no repository to
fetch from, the platform tree is copied by hand into the store layout
the resolver expects, consumer/aontu_meta/vendor/corp.example/schemas/service/:
path segments as directories, no major suffix. The layout is documented
in how-to/vendor-by-hand.md.
With a repository (below), aontu sync does the copy itself,
and checks the publisher’s proof, the archive’s bytes and the module’s
meaning before it does.
3. aontu sync then pins. Exit 0, and aontu_meta/pkg-lock.aon is
written as one canonical, diffable, JSON-parseable line:
# pkg-lock.aon (generated by `aontu sync`; do not edit)
{"lock":{"corp.example/schemas/service":{"archive":"sha256:5b299a1256d890fbbe4062926ca8a9b4686c2907c75fb27963dca481809b7c1f","canon":"aon1-zFHnyVa1fA--g8hTx8lUUhaKzzRUNI--2nDheIMsSFs","v":"1.4.2"}}}
The canon pin equals aontu hash platform/service/service.aon to
the byte; archive is the digest of the tree’s canonical archive, which
aontu pkg manifest prints. A package acquired from a repository
carries a third pin, manifest, the digest of what its publisher
signed; a hand-vendored one has no manifest, so it has no such pin.
4. Evaluation resolves from aontu_meta/vendor/, defaults fill, the
hidden schema stays out of the output, and two consecutive runs (JSON
and --canon) are byte-identical.
What check.sh proves
Hand distribution.
-
Cold start, verify: on a consumer that has declared the dependency and received nothing,
aontu pkg verifyexits 1 and names the repair:verdict: unlocked corp.example/schemas/service: not in the lockfile (run: aontu sync)A project with no lockfile is not a verified one.
-
Cold start, tidy:
aontu pkg tidyon the same consumer isverdict: missing, exit 1, names the package and the fix (not fetched (run: aontu sync)), and writes no lockfile. -
After the platform tree is copied into
aontu_meta/vendor/,syncisverdict: ok, fetches nothing, and the lockfile it writes is byte-identical to the committedconsumer/aontu_meta/pkg-lock.aon. -
pkg tidy --format jsonmatchesexpected/tidy.json(canon and archive pins,v), the CLI version line aside. -
pkg vendorleaves the already-vendored package in place,verdict: ok.
Evaluation, hermetic.
aontu main.aonmatchesexpected/consumer.json: the consumer evaluates throughaontu_meta/vendor/,gift-cardsrenders with the module’s defaults (port 8080, replicas 2, tierinternal, the healthcheck and telemetry blocks), and the hidden schema does not render.- Hermetic: two runs of the same inputs are byte-identical, as JSON
and as
--canon. aontu hash platform/service/service.aonequals the lockfile’s canon pin.hash --formshows the hashed text includes theclose({…})wrapper: closedness, invisible in plain canon, is inside the pin.
Integrity.
-
Tamper: flipping the vendored default
*8080to*9090fails evaluation (exit 1) with both hashes named:module integrity: corp.example/schemas/service expected aon1-zFHnyVa1fA--g8hTx8lUUhaKzzRUNI--2nDheIMsSFs got aon1-NHmNT6r-Lhy8di9BgGNRfgwNFT3r5PgCZxCYnJ4F0Ws -
pkg verifyagainst the tampered store checks bytes before meaning, so it is the archive pin that speaks; it writes nothing, andaontu_meta/pkg-lock.aonis byte-identical afterwards:$ aontu pkg verify verdict: mismatch corp.example/schemas/service: pinned archive sha256:5b299a… but the store holds sha256:… $ echo $? 1This is the check a CI job runs before it evaluates.
-
sync --frozenis the same gate with the fetch step: against the tampered store it isverdict: frozen, nameslockfile would change: corp.example/schemas/service: repinned, and leaves the lockfile untouched. -
pkg tidyrun after the same tamper reportsverdict: okand re-pins the lockfile to the tampered hash.tidytrusts the store, and rewriting the lockfile is its job, which is why CI runsverifyorsync --frozenand the committed lockfile’s diffs are read like code. -
Refactor: replacing the vendored
service.aonwith the two files inprobes/refactor/(an entry delegating to a reordered, re-commentedschema.aon) gives new bytes and a new file count, and after re-sync the canon pin is back toaon1-zFHnyVa1…with the rendered output unchanged, while the archive pin moves. The canon pin hashes the module’s meaning; the archive pin, its bytes.
The inline pin.
- Inline pin: a single file with
@"corp.example/schemas/service#aon1-zFHn…", nopkg.aonand no lockfile, resolves and verifies:model get '$.svc.spec.port'answers8080. - A mangled inline pin is refused with the same
module integrity:error, exit 1. - A bare file reference,
@"config.json", is refused with the repair:local files need a ./ prefix. Nothing routes it anywhere.
Cache resolution against a trust root.
- User cache: with the package present only in the user cache under
its
(canon-hash, package path)key, and a lockfile but no vendor tree, default trust evaluates to the same output as the vendored run. --trust root:<projectdir>on that project fails withmodule not fetched: corp.example/schemas/service: the cache is outside the confinement boundary.pkg vendorcopies cache →aontu_meta/vendor/, and the confined evaluation then passes with the same output.- The cache is consulted only once a pin is known: with the cache
seeded but no lockfile,
pkg tidyis stillverdict: missing.syncfetches;tidydoes not. sync --trust rootrefuses with exit 2: the verb reads and writes the user cache, which a confinement root does not reach.
The publish boundary.
-
pkg manifest platform/servicematchesexpected/manifest-142.txt: the signed manifest a publish sends (the archive digest and size, the module’s entry and canon-hash, every file with its digest).aontu_meta/is not part of the archive. -
Publish gate, compatible:
pkg manifest --against platform/service platform/service-next-compat(1.4.3: replicas ceiling widened, optionalrunbook?:added) isverdict: ok. -
Publish gate, breaking:
platform/service-next-breaking(a “minor” 1.5.0 that adds a requiredoncallfield) exits 1 and names the key it refuses on; no version number lifts the gate:verdict: breaking ... $.spec.oncall: the general value requires this key; the specific value admits instances without it -
The identical breaking schema as
platform/service2, a new package path at 1.0.0, isverdict: ok: a new path has no predecessor to gate against, and no consumer of the old path sees it unasked.
A repository, served and synced.
pkg keygenmints an Ed25519 key once and prints its signer id (signer: ed25519:…); a secondkeygenat the same file refuses withwritten once, exit 2.publish --key … --to repo platform/servicewithout--yesisverdict: dry-run: every check runs, the signer is named, the report endsdry run: nothing sent (add --yes), and the repository directory is untouched.publish --yes … --to repowrites the read-path layout:pkg/corp.example/schemas/service/@v/{1.4.2.zip,1.4.2.manifest,1.4.2.sig,list},@latest, andadvisory/corp.example/schemas/service.aon.- Publishing 1.4.3 reports
against: corp.example/schemas/service 1.4.2: the gate runs against the newest version the repository holds. - Publishing 1.5.0 is
verdict: breaking, exit 1, and writes nothing into the repository. - Publishing 1.4.3 again is
refused: version_exists: a version is never reusable. pkg serve --listen 127.0.0.1:0 reposerves the directory on a loopback address and printsserving <dir> at http://127.0.0.1:<port>.- A consumer whose
pkg.aonnames that base and the signer (repo: { base: [...], trust: { "corp.example/*": { signer: "ed25519:…", inclusion: none } } }) syncs from a fresh cache:fetched: corp.example/schemas/service 1.4.2, the lock entry carries amanifestpin, the vendored tree keepsaontu_meta/manifest.aonandaontu_meta/proof.aonbeside it, and the acquired package evaluates byte-identically to the hand-vendored one. - A second
syncfetches nothing, andsync --frozenholds. why corp.example/schemas/serviceanswerscorp.example/checkout -> corp.example/schemas/service.pkg outdatedreports the packagecurrentand acooldown_pending:event: 1.4.3 was published minutes ago and is not selectable by default for seventy-two hours.get corp.example/schemas/service@1.4.3reportschange: raised corp.example/schemas/service 1.4.2 -> 1.4.3, fetches it, and editspkg.aon: a version named explicitly skips the cooldown.get corp.example/schemas/servicewithout a version isrefused: cooldown_pending, exit 1, naming when the hold lifts.add corp.example/schemas/serviceon a project that already declares it refuses with exit 2,already a dependency, and namesget.remove corp.example/schemas/servicereportschange: removed …and drops the dependency, its lock entry and its vendor tree.- A consumer whose trust entry names a different signer is
refused: proof_signer_untrustedbefore a byte of the archive is read; nothing is vendored. - A byte appended to the served
1.4.2.zipisrefused: archive_digest_mismatch: the digest is checked before the archive is parsed. - A move: 1.4.4 of the old path declares
moved: "corp.example/schemas/service2"; its manifest carriesmoved:; a fresh sync of the old path isrefused: module_movedwithmoved to corp.example/schemas/service2; import that instead; and a further publish to the old path isrefused: path_moved.
Vetting agent candidates through the module.
vet --at spec gate.aon data/checkout-good.jsonisverdict: valid.data/rogue-sidecar.jsonisverdict: invalid, with located[aontu/constraint]findings for the bad name and the non-corporate owner and an[aontu/closed]finding for the inventedsidecarkey.
Transitive dependencies.
-
Minimum version selection (
probes/transitive/): the consumer askscommonat 1.0.0,serviceasks 1.2.0, andsyncselects 1.2.0, the highest of the declared minima. Held packages are read before anything is fetched, so the vendoredcommonat 1.2.0 satisfies the raised bid and no request goes out. -
The flat tree
syncwrites is the layout a nested import reads: resolution tries every enclosingpkg.aonroot, nearest first, socommonvendored besideserviceis found and the consumer evaluates. -
why corp.example/schemas/commonlists every chain that reaches it:corp.example/checkout -> corp.example/schemas/commonandcorp.example/checkout -> corp.example/schemas/service -> corp.example/schemas/common. -
The pin
synclocks for the dep-bearing module equals whataontu hashcomputes for the same file. -
A module that does not evaluate on its own is not pinned. With
servicevendored butcommonabsent,tidyrefuses and writes no lockfile:$ aontu pkg tidy verdict: error corp.example/schemas/service: does not evaluate on its own; nothing to pin corp.example/schemas/common: not fetched (run: aontu sync) $ echo $? 4 -
Module-internal references:
probes/nested-ref/(spec: { port: $.defaults.port }) hashes standalone, and imported at a nested consumer key it is refused with[aontu/no_path], because$is the root of the importing document.
Version bookkeeping.
- A vendored tree whose own
pkg.aonsays1.4.2does not satisfy a declared9.9.9:syncreaches for the repository and, with none configured, isrefused: fetch_failed. The version a tree declares is the version it is taken at.
The model tree.
view doc --depth 3 consumer/main.aondraws the figure at the head of this page, and--out --checkpins it, as text and as SVG.
Run
./check.sh # 54 assertions, exits 0
check.sh uses a private XDG_CACHE_HOME, never touches the real
user cache, starts pkg serve on a port the OS chooses and stops it
when done, and runs from any cwd. AONTU may be overridden to point
at another build.