Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ updates:
schedule:
interval: weekly
open-pull-requests-limit: 5
# Updates move the locks and never a floor (#1028). `increase-if-necessary`
# leaves a range in pyproject.toml alone while it already admits the new
# release, and still rewrites an exact `==` pin in constraints/. Unset, the
# choice was Dependabot's: it raised floors (`increase`) whenever PyPI's
# summary for agents-shipgate differed from pyproject.toml's description,
# and widened ranges otherwise. `lockfile-only` is not the fix despite its
# name: Dependabot reads constraints/*.txt as requirement files, not as
# lockfiles, so it would stop proposing their updates, security fixes
# included. When a floor may move is CONTRIBUTING.md § Changing dependencies.
versioning-strategy: increase-if-necessary
ignore:
# `# via cyclonedx-bom`, which requires chardet!=6.*,<8,>=5.1 since 7.5.0
# (7.3.1 capped it at <6.0, which is why #407 was closed). chardet 7.x is
Expand Down
105 changes: 105 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
name: CI

on:
Expand Down Expand Up @@ -444,6 +444,111 @@
- name: The installed command answers over stdio with five read-only tools
run: python scripts/mcp_extra_requirement.py --stdio-smoke

dependency-floors:
# #1028: the runtime floors in pyproject.toml are a contract with every
# environment Agents Shipgate is installed into. While they tracked the
# newest upstream release, `pip install agents-shipgate` beside an agent
# project's ordinary pins quietly installed 0.15.0. This job holds the two
# facts that keep that from coming back:
#
# - pip installs this version beside those pins (`coinstall`: one
# `pip install --dry-run --report` per scenario); and
# - the engine works at its floors. `uv pip compile --resolution
# lowest-direct` puts every runtime dependency at exactly its floor and
# everything else at the newest release, and the checks below run there.
#
# It resolves from the index on purpose: beside an adopter's floors, their
# resolver picks the newest of everything else, and that moves. What is
# installed is still hash-checked against what this job compiled, with the
# uv that constraints/release-publish.txt pins.
#
# The tests are the ones that exercise each runtime dependency's API: the
# CLI and doctor (typer), manifests, reports, scan and verify (pydantic,
# PyYAML), manifest patches (ruamel.yaml), signed human authorization
# (cryptography) and the safety canaries, plus every sample golden byte for
# byte. `scripts/generate_schemas.py --check` is not among them: the
# committed JSON schemas are rendered by the locked pydantic, and an older
# one renders some differently (#1088), which says nothing about what the
# engine emits or accepts.
#
# Its first run took under two minutes: 19 s for the three dry runs and 55 s
# for the tests. The dry runs are the one step whose time the index sets
# (CrewAI's resolves about 140 distributions), so the cap is the usual one.
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
with:
python-version: "3.12"

- name: Install the resolver and the scripts' dependencies, apart from the floors
# Their own environment, because `packaging` is one of the floors under
# test: the copy the scripts import must not be the one the tests run
# on. `uv` comes from the publication lock and `packaging` from the
# build-backend lock, both hash-checked.
run: |
python -m venv "${RUNNER_TEMP}/tools"
"${RUNNER_TEMP}/tools/bin/python" -m pip install --require-hashes --requirement constraints/release-publish.txt
"${RUNNER_TEMP}/tools/bin/python" -m pip install --require-hashes --requirement constraints/build-backend.txt

- name: pip installs this version beside the pins agent projects carry
run: '"${RUNNER_TEMP}/tools/bin/python" scripts/dependency_floors.py coinstall'

- name: Resolve every runtime dependency at exactly its floor
run: >-
"${RUNNER_TEMP}/tools/bin/python" scripts/dependency_floors.py compile
--uv "${RUNNER_TEMP}/tools/bin/uv" --output "${RUNNER_TEMP}/floors.txt"

- name: Install that closure, and nothing else
# Not the project itself: pytest's `pythonpath` and the scripts put
# `src` on the path, and an editable install would need a build backend
# whose own `packaging` would move a floor under test.
run: |
python -m pip install --require-hashes --requirement "${RUNNER_TEMP}/floors.txt"
python -m pip check
python scripts/dependency_floors.py installed

- name: The engine's dependency-heavy tests at the floors
# About 290 seconds of test time. The whole suite passed at these
# floors when they were set (#1028); here it would need the six shards
# `suite` uses.
run: >-
python -m pytest -n auto -m "not perf and not slow"
tests/test_cli.py
tests/test_environment.py
tests/test_config.py
tests/test_scan.py
tests/test_verify.py
tests/test_apply_patches.py
tests/test_declaration_authoring.py
tests/test_authorization_cli.py
tests/test_authorization_execution.py
tests/test_human_review_decision.py
tests/test_p0_safety_canaries.py
tests/test_p0_binding_canaries.py
tests/test_agent_boundary.py
tests/test_dependency_floors.py

- name: Every sample golden, byte for byte, at the floors
run: python scripts/regenerate_goldens.py --check

- name: doctor finds every dependency at or above its floor
# The floors differ from the lock the other jobs install, so the only
# dependency code doctor may report here is the lock-drift warning.
run: |
./shipgate doctor --config samples/support_refund_agent/shipgate.yaml --json > doctor.json
python - <<'PY'
import json, pathlib
environment = json.loads(pathlib.Path("doctor.json").read_text())[0]["environment"]
codes = {entry["code"] for entry in environment["mismatches"]}
assert codes <= {"dependency_differs_from_lock"}, environment["mismatches"]
PY

clean-checkout-launcher:
# `./shipgate` has to work on a checkout where Agents Shipgate itself is
# not installed: no console script on `PATH`, no editable install, no
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ Every `doctor --json` payload carries an `environment` block, and so does every
| `source_tree` | The enclosing Agents Shipgate checkout, if any: `root`, `version`, `launcher`, and `contains_import`. |
| `mismatches[]` | `code`, `severity` (`error` / `warning`), `detail`, and — when one exists — a runnable `command` spelled for this invocation. Empty is the normal state. |

`mismatches[]` codes: `interpreter_unsupported`, `import_outside_source_tree`, `source_tree_version_differs`, `installed_version_differs`, `console_script_interpreter_missing`, `console_script_runs_other_interpreter`. Nothing here executes an interpreter or a console script to find out — a stale wrapper is identified by reading it, because a wrapper that cannot start is exactly the one that cannot report on itself.
`mismatches[]` codes: `interpreter_unsupported`, `import_outside_source_tree`, `source_tree_version_differs`, `installed_version_differs`, `console_script_interpreter_missing`, `console_script_runs_other_interpreter`, `dependency_below_declared_floor` (a runtime dependency below the floor the running build declares), `dependency_differs_from_lock` (a source checkout's dependency off `constraints/dev.txt`, so CI may not reproduce the result). Nothing here executes an interpreter or a console script to find out — a stale wrapper is identified by reading it, because a wrapper that cannot start is exactly the one that cannot report on itself.

### One control vocabulary across the commands

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
- **The published verifier `0.21` and agent-boundary-result `v3` schemas now declare a capability row closed.** The locked development closure moves to pydantic 2.14, which reflects an enclosing model's `extra="forbid"` in the JSON Schema of a standard-library dataclass it contains; `CapabilityDiffRow` therefore gains `additionalProperties: false` in both files. Validation is unchanged: the same model already rejected an unknown row key under pydantic 2.13, and no emitted artifact, schema version or contract version moves. A consumer that validates a row against these schemas and relied on the object being open now rejects an unknown key, as the producing model always did.
- Move the published-release pins, examples and adoption prompts to `v1.2.0` (contract 41) now that it is published, re-capture the README and quickstart `diff` answers from the published `1.2.0`, and re-measure the pilot ledger's Route H dry run on it. No schema or contract change. (#778)

### Packaging

- **`pip install agents-shipgate` installs this release beside the older pins agent projects carry, instead of quietly installing 0.15.0.** The runtime floors had tracked the newest upstream release (`cryptography>=50.0.1,<51`, `packaging>=26.3,<27`, `pydantic>=2.13.4`, `pyyaml>=6.0.3`, `ruamel.yaml>=0.19.1`, `typer>=0.27.0`). An environment that already held `cryptography==46.0.7` or `packaging==24.2` therefore got 0.15.0 from pip with no warning, an engine without the verifier and `diff` this documentation describes, and one with CrewAI 1.15 (`pydantic<2.13`) got an older release or `ResolutionImpossible`. Each floor is now the oldest version the engine is tested on: `cryptography>=44.0.3`, `packaging>=24.2`, `pydantic>=2.12.5,<3`, `pyyaml>=6.0.2,<7`, `ruamel.yaml>=0.18.10,<1` and `typer>=0.20.0,<1`. The `cryptography` and `packaging` caps are gone: neither has a known break, and each new major would have repeated the fallback. The lower `cryptography` floor admits releases with published advisories; none of them is in the Ed25519 signature verification that is all Agents Shipgate uses it for, and a fresh install still resolves the newest release. A new CI job, `dependency-floors`, resolves every runtime dependency at exactly its floor (`uv pip compile --resolution lowest-direct`), runs the dependency-heavy tests and every sample golden there, and checks with `pip install --dry-run --report` that this version installs beside those three pins. Dependabot now moves the locks and never a floor (`versioning-strategy: increase-if-necessary`), and `CONTRIBUTING.md` § Changing dependencies says when a floor may move. An environment that already resolved a 1.x release is unaffected. (#1028)
- **`doctor --json` reports a runtime dependency below its floor.** The `environment` block compared Shipgate's own versions but never its dependencies, so a virtualenv below the declared floors (typer 0.25.1 under a 0.27.0 floor) reported `mismatches: []`. Two codes are added to `mismatches[]`: `dependency_below_declared_floor` (`error`), when a runtime dependency is installed below the floor the running build declares, with a reinstall `command`; and `dependency_differs_from_lock` (`warning`), in a source checkout only, when one differs from `constraints/dev.txt`, the closure CI tests the tree against. Versions are read from installed metadata; nothing is executed. The block's fields are unchanged, and a consumer that ignores unknown codes is unaffected. (#1028)

## 1.2.0 - 2026-09-30

An advisory-channel minor release. It adds `diff --application`, which compares
Expand Down
56 changes: 53 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,61 @@ stale fails there by name rather than as a puzzling import error. See
[`docs/release-runbook.md`](docs/release-runbook.md#the-environment-is-locked-and-it-is-cis)
for which job installs which lock.

### Runtime floors

The `>=` floor of each `[project].dependencies` entry is the oldest version
Agents Shipgate is tested on, not the newest one released. It is a contract
with every environment the package is installed into. While the floors tracked
the newest upstream release, `pip install agents-shipgate` in an agent project
that already pinned `cryptography==46.0.7` or `packaging==24.2` quietly
installed 0.15.0, and beside CrewAI (`pydantic<2.13`) it installed an older
release or failed (#1028).

A floor moves only when:

- the code starts to need an API or a fix that the floor version lacks; or
- a version at or above the floor is known to break Agents Shipgate. A
vulnerability counts when it is reachable through what Agents Shipgate calls.
An advisory in code it never calls does not: the adopter's resolver decides
whether a vulnerable release is installed, and a higher floor only takes the
newer Agents Shipgate away. Every `cryptography` release before 50.0.0 carries
advisories, and none of them is in the raw Ed25519 verification, which is all
this package uses `cryptography` for.

A Dependabot pull request, a lock refresh or a new upstream release is never a
reason to move a floor. An upper cap is declared only where a break is known.
`tests/test_dependency_floors.py` lists the caps in force and the break each
one stands for.

CI's `dependency-floors` job proves the floors on every pull request. It
resolves the runtime requirements with `uv pip compile --resolution
lowest-direct` and fails unless each one lands exactly on its floor. It then
runs the tests that exercise those dependencies, and every sample golden, in
that closure, and checks with `pip install --dry-run --report` that pip still
installs this version beside the pins in `scripts/dependency_floors.py`. To
check a floor change before pushing:

```bash
python scripts/dependency_floors.py coinstall
python scripts/dependency_floors.py compile --uv "$(command -v uv)" --output floors.txt
```

`doctor --json` reports `dependency_below_declared_floor` when the interpreter it
runs in has a runtime dependency below its floor. In a source checkout it also
reports `dependency_differs_from_lock` when one differs from
`constraints/dev.txt`, the closure CI tests the tree against.

### Dependabot

Dependabot groups each week's minor and patch version updates into one pull
request per ecosystem. Major bumps, security updates, `uv` and the harness's
`claude-agent-sdk` still arrive individually. Dependabot edits the hash locks
as plain requirement files, so a grouped pip PR is a request, never merged
as-is. To act on it, raise any floor it changes in `pyproject.toml` or a
`claude-agent-sdk` still arrive individually. Its pip updates use
`versioning-strategy: increase-if-necessary`, so a range in `pyproject.toml`
that already admits the new release is left alone: its pull requests move the
locks, never a floor, and the harness's `claude-agent-sdk` range is proposed
for change only when a release falls outside it. Dependabot edits the hash
locks as plain requirement files, so a grouped pip PR is a request, never
merged as-is. To act on it, move any exact toolchain pin it changes in a
`constraints/*.in`, then recompile only the locks it touches, for example
`python scripts/update_locks.py constraints/dev.txt`, with the `uv` version
`constraints/release-publish.in` pinned before the PR. A recompile refreshes
Expand Down
12 changes: 6 additions & 6 deletions constraints/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,22 @@
# changed extra, marker, URL or range cannot slip past without recompiling.
#
# declares: build<2,>=1.6.0
# declares: cryptography<51,>=50.0.1
# declares: cryptography>=44.0.3
# declares: cyclonedx-bom<8,>=7.3.1
# declares: hypothesis<7,>=6.165.10
# declares: jsonschema<5,>=4.26.0
# declares: packaging<27,>=26.3
# declares: packaging>=24.2
# declares: pip-audit<3,>=2.10.1
# declares: pydantic<3,>=2.13.4
# declares: pydantic<3,>=2.12.5
# declares: pytest-cov<8,>=7.1.0
# declares: pytest-xdist<4,>=3.8
# declares: pytest<10,>=9.1.1
# declares: pyyaml<7,>=6.0.3
# declares: ruamel-yaml<1,>=0.19.1
# declares: pyyaml<7,>=6.0.2
# declares: ruamel-yaml<1,>=0.18.10
# declares: ruff<1,>=0.16.1
# declares: sigstore<5,>=4.5.0
# declares: twine<8,>=7.0.0
# declares: typer<1,>=0.27.0
# declares: typer<1,>=0.20.0
#
annotated-doc==0.0.5 \
--hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \
Expand Down
7 changes: 7 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,13 @@ dies before a line of Shipgate runs.
| `installed_version_differs` | warning | Two *installed* copies shadow each other, so which one answers depends on path order. Not raised when a source checkout out-votes an install: that is intended, and an editable install's metadata lags every version bump by design. |
| `console_script_interpreter_missing` | error | A wrapper on `PATH` names an interpreter that no longer exists, so it fails before Shipgate starts. |
| `console_script_runs_other_interpreter` | warning | A bare `agents-shipgate` would execute a different installation than this command did. |
| `dependency_below_declared_floor` | error | A runtime dependency is installed below the floor the running build declares: its `pyproject.toml` in a source checkout, its `Requires-Dist` when installed. Nothing tests that version. The `command` reinstalls: the checkout's `constraints/dev.txt` with hashes, or else the declared requirement. |
| `dependency_differs_from_lock` | warning | Source checkout only. A runtime dependency differs from the version `constraints/dev.txt` pins, the closure CI tests the tree against, so a result here may not reproduce in CI. |

Dependency versions are what `importlib.metadata` records for the running
interpreter. A dependency with no recorded metadata, a requirement behind an
environment marker, and a version the standard-library comparison cannot read
(an epoch) are left unjudged rather than guessed at.

Nothing here runs an interpreter or executes a console script to find out.
That is the trust-model invariant (`tests/test_adapter_static_only.py` bans
Expand Down
2 changes: 1 addition & 1 deletion llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ Every `doctor --json` payload carries an `environment` block, and so does every
| `source_tree` | The enclosing Agents Shipgate checkout, if any: `root`, `version`, `launcher`, and `contains_import`. |
| `mismatches[]` | `code`, `severity` (`error` / `warning`), `detail`, and — when one exists — a runnable `command` spelled for this invocation. Empty is the normal state. |

`mismatches[]` codes: `interpreter_unsupported`, `import_outside_source_tree`, `source_tree_version_differs`, `installed_version_differs`, `console_script_interpreter_missing`, `console_script_runs_other_interpreter`. Nothing here executes an interpreter or a console script to find out — a stale wrapper is identified by reading it, because a wrapper that cannot start is exactly the one that cannot report on itself.
`mismatches[]` codes: `interpreter_unsupported`, `import_outside_source_tree`, `source_tree_version_differs`, `installed_version_differs`, `console_script_interpreter_missing`, `console_script_runs_other_interpreter`, `dependency_below_declared_floor` (a runtime dependency below the floor the running build declares), `dependency_differs_from_lock` (a source checkout's dependency off `constraints/dev.txt`, so CI may not reproduce the result). Nothing here executes an interpreter or a console script to find out — a stale wrapper is identified by reading it, because a wrapper that cannot start is exactly the one that cannot report on itself.

### One control vocabulary across the commands

Expand Down
Loading
Loading