Publishing Trusted Layers¶
This guide is for the author of a domain trusted layer: a
tools/<domain>/trusted/ package that lives in its own repository, possibly
private, and is pulled into consumer projects with clauz3 install. The
bundled standard library is deliberately small;
richer domains with their own release cadence, a larger surface area, or
proprietary vocabularies (instrument schemas, vendor APIs) belong in a separate
repository. The first such repository is
clauz3-tools-autolabs;
the plan behind it is
agent-deal issue #41.
The consumer side (clauz3 install, gh: sources, the sha-keyed cache, the
.clauz3/ project layout) is covered in
Installing trusted layers, and the reasons for shipping a
layer with its own tests are in
Distributed trusted layers.
This page covers what the layer repository must look like, how to name it so it
cannot collide with another layer, and what its maintainer is promising.
Repository layout¶
Use the stdlib shape. It is what clauz3 install and clauz3 test resolve:
clauz3-tools-<umbrella>/
clauz3.yaml # optional kind: package; shared prerequisites
README.md # what the layer is for; install line; API at a glance
AGENTS.md # how an agent should use the layer
LICENSE
tools/
<org>/<domain>/
clauz3.yaml # domain description, dependencies, setup
trusted/
effects.py # @clauz3.trusted effect functions with preconditions
contracts.py # @contract helpers over those effects
policy.py # optional: POLICY = domain_policy(...) for coverage
tests/
Justfile # lint + every case; run by `clauz3 test`
cases/
<contract>_pass.py # must prove
<contract>_fail.py # must not prove
One repository may hold several domains side by side (tools/<org>/lims/,
tools/<org>/madsci/). clauz3 install copies every domain it finds, so split
domains across repositories when consumers should be able to install them
independently, and keep them together when they are always used as a set.
The source-only YAML registry stores locations, not descriptions. Put each domain's title, summary, description, dependencies, and effect-specific setup in its manifest. Optionally use the root package manifest for shared Python packages, external tools, account configuration, or other prerequisites. See Library manifests and setup for the LinkML schema, examples, and additive merge rules.
Installation copies each whole domain directory, keeps its manifest unchanged,
and snapshots shared package metadata into .clauz3-package.yaml beside it.
Do not author that generated file. Tests and root-level guides stay in the
source tree. Neither manifest changes domain discovery, imports, or trust.
For new releases, ship a valid manifest for every domain. Legacy sources without manifests still install with a warning in the CLI and installation review, but lack manifest descriptions and setup guidance. Omitting the root package manifest is normal and produces no metadata warning. Declaring a package while omitting any domain manifest is an error, as is malformed or conflicting metadata. Setup instructions are displayed, not executed during installation.
A domain is found by one rule, shared by install and by every command that
discovers trusted roots on its own (clauz3 run, clauz3 tools,
clauz3 policy-check, clauz3 start):
- it is a
trusted/directory attools/<domain>/trusted/ortools/<org>/<domain>/trusted/; anything deeper is skipped with a warning; - every directory name on the way is a Python identifier, because it becomes
part of the import name.
tools/acme-labs/lims/trusted/cannot be imported, so it is skipped with a warning (and an install whose only domains are skipped fails). Useacme_labs, notacme-labs, even when the GitHub org has a hyphen; - it is not a symlink and is not reached through one;
- a
trusted/directory nested inside another domain'strusted/is part of that domain, not a second one.
Choose a name that cannot collide¶
This is the decision to make first, because it appears in every import line.
The prover imports a trusted layer through Python's import system under the
dotted name of its directory: .clauz3/tools/lims/trusted/ becomes
tools.lims.trusted, whether it was installed under .clauz3/tools/ or
authored under the project's own tools/. Two different layers with the same
dotted name therefore collide. Within one project, install refuses to
overwrite an existing .clauz3/tools/<domain>/ without --force, and refuses a
domain the project already authors. Across projects in one process (a
long-running approval service, a test suite proving against several projects)
the loader refuses a module already loaded from a different file rather than
proving against the wrong one
(agent-deal issue #111).
Either way the collision is an error, not a silent swap, but it is still an
error your consumers hit.
A layer author has two options:
- Namespace the domain under your org (recommended):
tools/<org>/<domain>/trusted/, for exampletools/normalform/lims/trusted/. Consumers then writefrom tools.normalform.lims.trusted import contracts. Nested domains are supported end to end:installcopies them to.clauz3/tools/<org>/<domain>/,--skillswrites.clauz3/skills/<org>/<domain>/SKILL.md,prove --trusted-rootproves against them, andrun,tools,policy-checkandstartdiscover them without an explicit--trusted-root.clauz3 toolslists each effect by its path belowtools/(normalform/lims/trusted/effects.py:pipette), so a namespaced domain and a flat one of the same name are distinguishable. - Use a flat name that is provably unique. Never reuse a bundled stdlib
name (see Standard library tools) or an
examples/domain name. A flat name is shorter to import but you own the uniqueness check.
Whichever you choose, the name is part of the layer's public interface: renaming it later breaks every consumer's imports and generated skills.
Effect names are global across layers¶
Org namespacing separates modules, not effects. A contract addresses an
effect by its bare function name (effect("pipette")), and so does a coverage
policy's when_used. Every trusted root in a proof therefore shares one effect
namespace: if tools/acme/lims/trusted/effects.py and
tools/other/lims/trusted/effects.py both define pipette, proving with both
roots is refused with "two trusted effects are called 'pipette'" (and
clauz3 run, which discovers every installed root, refuses too). A consumer
then has to choose between your layer and the other one.
So treat effect function names as interface:
- prefix effect names with the domain when they are generic
(
lims_pipette, notpipette;madsci_move, notmove); - keep helper functions that are not effects out of
@clauz3.trusted, so they do not occupy the shared namespace; - if you add
@deal.has(...)markers for deal's runtime guard andclauz3 lint, keep them honest about the worst case the body can do. Markers no longer address anything in a contract or policy, butlintchecks the body against them.
Packaging: a plain git tree, not a wheel¶
Ship the layer as a git repository, not as a Python package on an index.
- The installable artifact is a directory.
installcopies it into the consumer's.clauz3/tools/, rejects symlinks in it, and refuses to clobber an existing domain. A wheel would bypass all three checks. - The prover imports the layer as
tools.<...>.trustedrelative to the consumer's import roots, and the untrusted-import allowlist admits only that prefix. A wheel would introduce a second import name (import acme_lims) that agent programs may not use. - Git already gives versioning (tags), pinning (shas), and access control (private repositories with your existing SSH or credential-helper auth). The provenance manifest and signing that would make a pinned install attestable are tracked in agent-deal issue #47; this guide does not define a competing scheme.
The one Python package in the picture is clauz3 itself. State the minimum
clauz3 version your layer's contracts need in README.md; the layer
repository needs no pyproject.toml unless its tests/ use extra tooling.
What the maintainer guarantees¶
The effect bodies are the security boundary: the prover never executes them,
and clauz3 run executes them after a program is proved and approved. A
consumer who installs your layer is trusting these statements about it:
- Every effect does only what its name, docstring and preconditions say, and
any
@deal.hasmarkers name everything the body can actually do. - Preconditions (
@clauz3.requires) fail closed: a call the prover cannot show satisfies them does not prove. - Effects that reach the outside world (mail, orders, actuators) default to a
dry run,
*, dry_run: bool = True, with a contract a policy can demand. The bundled email library demonstrates previews versus real mailbox writes;tests/test_dry_run_kwarg.pychecks the default-argument proof behavior. - The tree contains no symlinks, and every directory on the way to
trusted/is a Python identifier. - Every contract has at least one
_passand one_failcase, andclauz3 testis green at every tagged release. - Tags are never moved. Consumers pin shas; tags are for humans.
- For a private layer: the README states who may install it and how access is
granted, because
clauz3itself has no notion of access beyond git's.
Tests¶
Start from a stdlib tests/Justfile (for example
src/clauz3/stdlib/filesystem/tests/Justfile) and edit the paths. A minimal
template:
# `clauz3 test` overrides CLAUZ3 so the suite runs from an installed wheel
# as well as from a source checkout.
clauz3 := env_var_or_default("CLAUZ3", "uv run clauz3")
trusted := "../tools/acme/lims/trusted"
prove := clauz3 + " prove --trusted-root " + trusted + " --import-root .."
test: lint cases
lint:
{{ clauz3 }} lint {{ trusted }}/effects.py
cases: only-plate-pass only-plate-fail
only-plate-pass:
{{ prove }} cases/only_plate_pass.py
only-plate-fail:
if {{ prove }} cases/only_plate_fail.py; then exit 1; fi
Conventions that make the suite meaningful:
_pass.pycases must prove and_fail.pycases must be refuted, one pair per contract, plus a<domain>_no_guarantees_pass.pythat shows the null contract proves.- A
_failrecipe written asif {{ prove }} ...; then exit 1; fialso passes whenproveexits non-zero for the wrong reason, such as a configuration error. Where a refutation is load-bearing, assert the conclusion in a pytest test instead. - Proof tests never run effect bodies, so they are safe for real
side-effecting tools. If you add a runtime smoke recipe, keep it separate and
make it generate its own fixtures, as
stdlib:dbdoes. - For a repository with several domains, use one
tests/Justfilewith a recipe per domain and maketestdepend on all of them;clauz3 test <source> --recipe <domain>runs one.
Run the suite the way a consumer will:
clauz3 test . # from the layer repo
clauz3 test gh:<org>/clauz3-tools-<umbrella>@<sha> # from anywhere, before trusting it
Consuming a published layer¶
# Look before you trust: runs the layer's own suite from the sha-keyed cache.
clauz3 test gh:<org>/clauz3-tools-<umbrella>@<sha>
# Install, pinned to a sha, with a generated SKILL.md per domain.
clauz3 install gh:<org>/clauz3-tools-<umbrella>@<sha> --skills
# prove takes its trusted roots explicitly; it does not discover them.
clauz3 prove --trusted-root .clauz3/tools/<org>/<domain>/trusted plan.py
# run, tools, policy-check and start discover installed domains, nested or flat.
clauz3 tools
clauz3 run plan.py
Commit the installed .clauz3/tools/ and .clauz3/skills/ into the consumer
repository; the install is a copy, not a link, so the consumer's proofs do not
change when the upstream moves. Upgrade deliberately with --force after
re-running clauz3 test against the new sha. Each installed domain records its
source and resolved git revision in .clauz3-origin.json; local paths have no
git revision recorded. This is provenance, not a signature or attestation.
Keep the pinned install line in the consumer's README or Justfile for a
reproducible upgrade workflow.
Publishing checklist¶
- Name:
clauz3-tools-<umbrella>for the repository,tools/<org>/<domain>/inside, with identifier-only directory names. - Effect function names are prefixed where they are generic.
clauz3 test .is green, includingclauz3 lint.clauz3 install . --into "$(mktemp -d)" --skillscopies exactly the domains you intend, prints no missing-manifest or "skipping trusted layer" warning, validates shared package setup if present, and the generatedSKILL.mdreads sensibly.README.mdstates the install line with a sha, the minimumclauz3version, and (for a private layer) how access is granted.- Tag the release; never move the tag.
- Optionally add a row to the Registry by pull request; private layers are listed as "available on request".