Skip to content

fix(contract): align wire spec_version with spec/VERSION before first publish - #41

Merged
imran-siddique merged 1 commit into
mainfrom
fix/contract-version-drift
Sep 2, 2026
Merged

fix(contract): align wire spec_version with spec/VERSION before first publish#41
imran-siddique merged 1 commit into
mainfrom
fix/contract-version-drift

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

The problem

spec/VERSION, pyproject.toml and packages/typescript/package.json all declare 0.1.0-alpha.1. The wire contract declared 0.1.0-dev, and envelope.schema.json pinned it as a const:

"spec_version": {"const": "0.1.0-dev"}

So an adopter who read the release version and emitted it as spec_version would have been rejected by our own validator. The only correct value was a string that appears nowhere in the packaging.

tools/check_versions.py did not catch this, because it derives the Python and npm versions from spec/VERSION and never looks at the wire const.

The change

0.1.0-dev to 0.1.0-alpha.1 across 31 files: the schema const, all three schema copies (spec/schema, src/agentrust_telemetry/schemas, packages/typescript/schemas), the 13 conformance fixtures, the golden compatibility events, both SDKs, the examples, and four docs.

On the two golden digests that moved

spec_version is inside the RFC 8785 canonical bytes that get hashed, so the evidence-chain and tool-transcript goldens change. These were regenerated from the code, not hand-edited, and the change was verified attributable to nothing else:

  • Recomputing chain entry 0 with the version string reverted reproduces the previous digest f31be297... exactly.
  • Python and TypeScript independently produce the same new tool-transcript hash sha256:3ef76a71..., which is the cross-language parity these goldens exist to protect.

Verification

Gate Result
check_versions / check_schemas / check_typescript_schemas / check_otel_compatibility pass
Conformance fixtures 13/13
Python tests 108 pass, 2 skipped
TypeScript tests 41 pass
examples/governed_workflow.py, examples/manual_governance.py pass
Residual 0.1.0-dev none

Nothing is published on npm or PyPI yet, so this costs no adopter a migration. That is also why it is worth landing before the first publish rather than after.

Not in this PR

All eight schemas carry $id: https://agentrust.io/telemetry/v0.1/schema/.... That domain is not ours; it serves a parked-domain lander and returns HTTP 200 for every path, so a resolver gets HTML with a success code. The real site is agentrust-io.com. This is the same class of defect trace-spec corrected in its v0.1 to v0.2 cutover. Raised separately because choosing the replacement identifier is a design decision, not a fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn

… publish

spec/VERSION, pyproject and package.json all say 0.1.0-alpha.1, while the wire
contract said 0.1.0-dev. envelope.schema.json pinned it as a const, so every
event had to carry a version string that appears nowhere in the packaging. An
adopter reading the release version and emitting it as spec_version would have
been rejected by the validator.

Replaces 0.1.0-dev with 0.1.0-alpha.1 across the schema const, all three schema
copies, the 13 conformance fixtures, the golden compatibility events, both SDKs,
the examples and four docs.

The two golden digests move because spec_version is inside the RFC 8785 bytes
that are hashed. Verified the change is attributable to nothing else: recomputing
entry 0 with the version reverted reproduces the previous digest exactly. Python
and TypeScript independently produce the same new tool-transcript hash, which is
the cross-language parity the goldens exist to protect.

Nothing is published on npm or PyPI yet, so this costs no adopter a migration.

Gates: check_versions, check_schemas, check_typescript_schemas,
check_otel_compatibility, 13/13 conformance fixtures, 108 Python tests,
41 TypeScript tests, both examples.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn
@imran-siddique
imran-siddique merged commit 64530b7 into main Sep 2, 2026
8 checks passed
@imran-siddique
imran-siddique deleted the fix/contract-version-drift branch September 2, 2026 20:09
imran-siddique added a commit that referenced this pull request Sep 2, 2026
## The problem

All eight schemas, in all three copies, identify themselves under
`https://agentrust.io`:

```json
"$id": "https://agentrust.io/telemetry/v0.1/schema/envelope.schema.json"
```

That domain is not ours. It serves 114 bytes:

```html
<!DOCTYPE html><html><head><script>window.onload=function(){window.location.href="/lander"}</script></head></html>
```

A parked-domain lander. It also answers **HTTP 200 for every path**, so
anything resolving a schema `$id` gets HTML with a success code rather
than a 404 it could act on. Our real site is `agentrust-io.com`, served
from GitHub Pages.

Before this PR, `agentrust-io.com` appeared in **zero** files in the
repository.

This is the same class of defect trace-spec corrected in its v0.1 to
v0.2 cutover, where the concern was letting "a record minted under a
domain we do not own continue to pass as conformant."

## The change

The `$id` host in `spec/schema/`, `src/agentrust_telemetry/schemas/` and
`packages/typescript/schemas/`, plus the id template in `validation.ts`.
One line per file, 25 files.

Relative `$ref`s are untouched and still resolve, because swapping the
host keeps the base hierarchical.

## Why this needs no hosting change to be correct

Neither SDK dereferences `$id`. Both read the schemas from local files
and register them in their validator by `$id`, so nothing depends on the
URL being fetchable. Serving the eight files at that path is worth doing
and is not a prerequisite for this to be right. Even unserved, an
identifier on a domain we control is strictly better than one on a
parked domain that returns 200 and HTML.

## Considered and rejected: a tag URI

`tag:agentrust-io.com,2026:telemetry/...` would have matched the
identifier style trace-spec v0.2 uses. It does not work here, tested
both ways:

- **Python** (`referencing`): relative `$ref`s cannot resolve against a
non-hierarchical `tag:` base. Making all cross-schema refs absolute does
fix it, and passes 6/6 valid and 7/7 invalid fixtures.
- **Ajv**, used by the TypeScript SDK: cannot resolve `tag:` URIs at
all, even fully absolute ones. `can't resolve reference
tag:agentrust-io.com,2026:telemetry/v0.1/schema/common.schema.json#/$defs/event_id`

So a tag URI would break the TypeScript SDK. trace-spec uses one as a
claim **value** (`eat_profile`), not as a JSON Schema `$id` with refs
hanging off it, which is why the parallel does not carry.

## Verification

| Gate | Result |
|---|---|
| `check_versions` / `check_schemas` / `check_typescript_schemas` /
`check_otel_compatibility` | pass |
| Conformance fixtures | 13/13 |
| Python tests | 108 pass, 2 skipped |
| TypeScript tests | 41 pass |
| `examples/governed_workflow.py` | pass |
| Residual bare `agentrust.io` | none |

Branched from `main`, so it is independent of #41 and the two can land
in either order.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
imran-siddique added a commit that referenced this pull request Sep 2, 2026
## The problem

`release.yml` published npm with no dist-tag:

```yaml
run: npm publish npm-dist/*.tgz --access public
```

npm refuses that outright for a prerelease. Confirmed by hand during
today's bootstrap publish:

```
npm error You must specify a tag using --tag when publishing a prerelease version.
```

So `publish-npm` would have failed on any `0.x` prerelease. On a stable
version it would have succeeded and moved `latest` without anyone
deciding to.

## The change

The phase is already declared exactly once, in `spec/VERSION`. Rather
than hardcode a tag in the workflow or in `publishConfig`, where it
needs editing at every phase change, this derives it:

| `spec/VERSION` | dist-tag |
|---|---|
| `1.0.0` | `latest` |
| `0.1.0-alpha.1` | `alpha` |
| `0.2.0-beta.3` | `beta` |
| `1.0.0-rc.1` | `rc` |
| `0.1.0-dev` | `dev` |

- **`tools/npm_dist_tag.py`** prints the tag.
- **`check_versions.py` now exposes `CONTRACT_PATTERN`**, so both tools
read one grammar. A second copy of that regex would be its own drift
risk, which is exactly the class of bug #41 fixed.
- **The `build` job publishes it as an output**, so the distribution is
still built once and `publish-npm` needs no checkout of its own.
- **`RELEASING.md`** documents the derived tag and the by-hand
equivalent, `npm publish --tag "$(python tools/npm_dist_tag.py)"`.

## One npm behaviour worth recording

npm sets `latest` on a package's **first** published version regardless
of `--tag`. Today's bootstrap publish of `0.1.0-alpha.1` went out as
`--tag alpha` and the registry still shows:

```
dist-tags: { alpha: '0.1.0-alpha.1', latest: '0.1.0-alpha.1' }
```

That is expected on a first publish, npm protects `latest` from removal,
and it corrects itself when the first stable version ships. Noted in
`RELEASING.md` so it is not mistaken for a bug later.

## Verification

| Gate | Result |
|---|---|
| `check_versions` / `check_schemas` / `check_typescript_schemas` /
`check_otel_compatibility` | pass |
| `check_release_tag v0.1.0-alpha.1` | pass |
| Repository gate tests | 16 pass, including 3 new |
| `release.yml` | parses as valid YAML |
| `python tools/npm_dist_tag.py` on current `spec/VERSION` | `alpha` |

The new tests assert the mapping across all five phases, that a bad
version raises, and the invariant that matters: a prerelease never
resolves to `latest`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
imran-siddique added a commit that referenced this pull request Sep 2, 2026
Release preparation for `0.1.0-alpha.2`, following step 1 and 2 of
`RELEASING.md`.

## What is in this release

All three already merged to `main`:

| PR | |
|---|---|
| #41 | wire `spec_version` now matches `spec/VERSION` |
| #42 | schema `$id` moved off `agentrust.io`, a domain we do not own |
| #43 | npm dist-tag derived from `spec/VERSION` |

## Why a new version instead of releasing v0.1.0-alpha.1

npm already holds `0.1.0-alpha.1` from the bootstrap publish that
`RELEASING.md` explicitly sanctions:

> If npm does not expose publisher settings until the first version
exists, bootstrap only that first package ownership using npm's
interactive 2FA flow

`release.yml` publishes both registries from one release event, and
`release-assets` has `needs: [publish-pypi, publish-npm]`. Cutting
`v0.1.0-alpha.1` would publish PyPI, fail `publish-npm` on the duplicate
version, and skip attestation entirely. A fresh version lets one build
feed both registries with provenance intact, which is what the pipeline
exists to do.

PyPI has never been published, so `0.1.0-alpha.2` will be its first
version.

## On the moved digests

`spec/VERSION` is the contract version and the source both package
versions derive from, so bumping it changes the wire `spec_version`,
which sits inside the RFC 8785 bytes that get hashed. Both goldens were
regenerated from the code rather than hand-edited, and Python and
TypeScript independently agree on the new tool-transcript hash.

## A dead test case, found while bumping

`tests/test_repository_gates.py` had `"0.1.0-alpha.1"` as a dict key
**twice**:

```python
"0.1.0-alpha.1": ("0.1.0.dev0", "0.1.0-alpha.1.0"),
"0.1.0-alpha.1": ("0.1.0a1", "0.1.0-alpha.1"),
```

Python keeps the last, so the first entry was silently discarded and the
`dev` phase has never been tested. Its npm spelling was wrong as well:
`0.1.0-alpha.1.0` is not something `ecosystem_versions` can produce for
any input. Restored as a real `0.1.0-dev` case, with both alpha
spellings now asserted.

## Verification

| Gate | Result |
|---|---|
| `check_versions` | `contract=0.1.0-alpha.2 python=0.1.0a2
npm=0.1.0-alpha.2` |
| `check_release_tag v0.1.0-alpha.2` | pass |
| `npm_dist_tag` | `alpha` |
| `check_schemas` / `check_typescript_schemas` /
`check_otel_compatibility` | pass |
| Conformance fixtures | 13/13 |
| Python tests | 111 pass |
| TypeScript tests | 41 pass |

## After this merges

1. Configure the npm trusted publisher for `@agentrust-io/telemetry`,
now possible because the package exists.
2. Create the GitHub release tagged `v0.1.0-alpha.2` targeting `main`.
3. Approve the `pypi` and `npm` deployment jobs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Xyu1wLe68MPCKaqUXeEpXn

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant