Skip to content
Merged
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
144 changes: 144 additions & 0 deletions .agent-runtime/goals/no-unenforced-ceremony.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
{
"schema_version": "v1alpha1",
"revision": 12,
"goal": {
"id": "no-unenforced-ceremony",
"intent": "Every retained contract field buys enforcement and trust tooling resolves without ambient paths",
"acceptance": [
{
"id": "no-dead-field",
"acceptance": "The upstream_go_mod field is deleted or verified against real upstream content",
"status": "complete",
"evidence": [
{
"type": "file",
"reference": "security-tools.json; internal/cicontract/contract.go",
"result": "The field is deleted from the contract file and its Go type; nothing referenced it."
}
]
},
{
"id": "resolved-binaries",
"acceptance": "Trust binaries are resolved and identity-checked rather than hardcoded, with one source for the fact",
"status": "complete",
"evidence": [
{
"type": "file",
"reference": "internal/trustedexec/resolve.go",
"result": "One resolver, fixed absolute search, never PATH, regular non-symlink and not group or world writable, shared by signatureverify and provenance."
}
]
},
{
"id": "anchor-truth",
"acceptance": "The GDS anchor declares semver compatibility and version-tag pinning, matching how the control plane actually consumes the module",
"status": "complete",
"evidence": [
{
"type": "file",
"reference": ".gds/repository.yaml",
"result": "compatibility semver and pin_policy version-tag, with the two axes explained and the false no-semver-tag justification removed."
}
]
}
],
"non_goals": [
"Weaken any fail-closed signature or provenance check"
],
"state": "active",
"current_phase": "closure",
"receipts": {
"completeness_omission_audit": {
"phase": "completeness_omission_audit",
"summary": "Nix and Guix hosts still cannot verify, because their tools are outside every absolute directory the resolver will search and any mechanism covering them is an ambient input into the trust path. Stated in the security model with the reason and the failure message that names what was searched.",
"evidence": [
{
"type": "test",
"reference": "internal/trustedexec TestSearchNamesWhatItLookedAt",
"result": "A missing tool fails naming every directory searched and stating that PATH is never read."
},
{
"type": "test",
"reference": "https://github.com/NDDev-it-com/agent-runtime/actions/runs/31852939515",
"result": "CI rejected the first attempt: the writable-tool fixture used WriteFile(0o777), which the umask masks, so it landed at 0755 under the 022 umask CI uses and proved nothing. Modes are now set with Chmod and asserted before use, and the suite is run under umask 022, 002 and 077."
}
],
"recorded_at": "2026-08-15T00:12:44.466737374Z"
},
"execute": {
"phase": "execute",
"summary": "Removed upstream_go_mod from the contract file and its type, routed signatureverify and provenance through trustedexec, and set the anchor to semver and version-tag.",
"evidence": [
{
"type": "command",
"reference": "grep for /usr/bin across internal/signatureverify and internal/provenance",
"result": "No occurrences outside the resolver; the isolated environment has one definition."
}
],
"recorded_at": "2026-08-15T00:12:44.344133143Z"
},
"gap_plan": {
"phase": "gap_plan",
"summary": "Delete the field rather than make it executable, because nothing needs it. Give the trust tools one resolver that never reads PATH. State both axes in the anchor: what the module promises and how this estate takes it.",
"evidence": [
{
"type": "file",
"reference": "internal/trustedexec",
"result": "A fixed ordered search over absolute directories keeps the property the hardcoded path had, and covers a Homebrew macOS host that the single path did not."
}
],
"recorded_at": "2026-08-15T00:12:44.308001589Z"
},
"orient": {
"phase": "orient",
"summary": "Three statements bought nothing. upstream_go_mod was required non-empty and read by nothing. The git executable was written as a constant in signatureverify and as a raw literal in provenance, with the isolated environment spelled out twice. The GDS anchor justified commit-contract pinning with a comment saying no semver tag existed, which stopped being true at v0.1.2.",
"evidence": [
{
"type": "file",
"reference": "security-tools.json; internal/provenance/verify.go; .gds/repository.yaml",
"result": "Either upstream URL could be replaced with arbitrary text with every check still green; one fact about the trust path lived in two places; the anchor contradicted four live tags and two published releases."
}
],
"recorded_at": "2026-08-15T00:12:44.266651931Z"
},
"reconcile": {
"phase": "reconcile",
"summary": "Composing the two environments naively duplicated every git configuration setting, so the shared half now lives in trustedexec and the signature verifier adds only what is specific to running git interactively.",
"evidence": [
{
"type": "test",
"reference": "internal/signatureverify TestIsolatedGitEnvironmentRejectsAmbientOverrideChannels",
"result": "The isolated environment is asserted against the resolver's own search path rather than a hardcoded string, so the two cannot drift."
}
],
"recorded_at": "2026-08-15T00:12:44.383787796Z"
},
"self_review": {
"phase": "self_review",
"summary": "The resolver keeps the property that made the hardcoded path defensible in the first place: no environment input decides which program rules on a signature. It is tested for that directly rather than assumed.",
"evidence": [
{
"type": "test",
"reference": "internal/trustedexec TestNeverReadsThePathEnvironment",
"result": "With PATH pointing at a planted git, resolution still returns the fixed location."
}
],
"recorded_at": "2026-08-15T00:12:44.423642378Z"
},
"verify": {
"phase": "verify",
"summary": "gofmt, go vet, staticcheck, full tests under umask 002, race, and all seven contract checkers.",
"evidence": [
{
"type": "test",
"reference": "local gate transcript 2026-08-15",
"result": "All green."
}
],
"recorded_at": "2026-08-15T00:12:44.50916971Z"
}
},
"created_at": "2026-08-15T00:12:44.224755101Z",
"updated_at": "2026-08-15T00:18:10.11806904Z"
}
}
66 changes: 59 additions & 7 deletions .agent-runtime/goals/self-verified-journals.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,89 @@
{
"schema_version": "v1alpha1",
"revision": 8,
"revision": 13,
"goal": {
"id": "self-verified-journals",
"intent": "This repository's tracked Goal journals are proven valid by an executable checker",
"acceptance": [
{
"id": "journal-checker",
"acceptance": "A required CI checker validates every tracked journal against the Go contract and the published schema",
"status": "pending"
"status": "complete",
"evidence": [
{
"type": "file",
"reference": "cmd/check-goal-journals; internal/journalverify; .github/workflows/ci.yml; .gds/repository.yaml",
"result": "A required CI step holds every tracked journal to the Go contract and the published schema, and the CI contract requires the step so it cannot be dropped silently."
}
]
},
{
"id": "journals-valid",
"acceptance": "goal status succeeds on every tracked journal",
"status": "pending"
"status": "complete",
"evidence": [
{
"type": "command",
"reference": "go run ./cmd/check-goal-journals",
"result": "goal journals valid: 9 tracked, accepted by the Goal contract and the published schema."
}
]
},
{
"id": "first-release-true",
"acceptance": "The first-release journal states the published outcome and closes on its evidence",
"status": "pending"
"status": "complete",
"evidence": [
{
"type": "file",
"reference": ".agent-runtime/goals/first-v0-release.json",
"result": "State completed at closure with all seven criteria met, recording that the release shipped as v0.1.2 then v0.1.3, and keeping the unpublished v0.1.0 and v0.1.1 tags as declared debt."
}
]
},
{
"id": "no-shipped-invalid",
"acceptance": "A built source archive contains no artifact the product rejects",
"status": "pending"
"status": "complete",
"evidence": [
{
"type": "command",
"reference": "source archive built from the commit, every journal extracted and fed to agent-runtime goal status",
"result": "All accepted; the same archive built from the previous commit still rejects first-v0-release.json."
}
]
}
],
"non_goals": [
"Widen the phase vocabulary to accommodate a malformed journal"
],
"state": "active",
"state": "completed",
"current_phase": "closure",
"receipts": {
"closure": {
"phase": "closure",
"summary": "Merged as PR #58 with all four required checks green.",
"evidence": [
{
"type": "link",
"reference": "https://github.com/NDDev-it-com/agent-runtime/pull/58",
"result": "Merged to main."
}
],
"recorded_at": "2026-08-15T00:07:29.602650529Z",
"closure": {
"achieved_outcome": "The Goal contract has the executable checker every other contract here already had, and the module no longer ships an artifact it rejects.",
"cleanup": "Task branch deleted locally and on origin.",
"remaining": [],
"next_work": [
{
"type": "issue",
"reference": "no unenforced ceremony",
"result": "Delete the contract field nothing verifies, give the trust binaries one definition, and make the GDS anchor state how the module is actually consumed."
}
]
}
},
"completeness_omission_audit": {
"phase": "completeness_omission_audit",
"summary": "Registered the checker in the CI contract as well as the workflow, so it cannot be dropped from ci.yml while the contract stays green -- the same failure class the workflow model was built to close.",
Expand Down Expand Up @@ -118,6 +170,6 @@
}
},
"created_at": "2026-08-15T00:02:27.292800011Z",
"updated_at": "2026-08-15T00:02:27.487402016Z"
"updated_at": "2026-08-15T00:07:29.602650529Z"
}
}
11 changes: 7 additions & 4 deletions .gds/repository.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,13 @@ module:
# Pinned as a gitlink by the control plane. Not vendored, not published to
# a package registry, and not executed from its own repository.
- "git-submodule"
# The published surface is `v1alpha1` and carries no semver tag, so the
# consumer contract is the exact pinned commit until a tagged line exists.
compatibility: "commit-contract"
pin_policy: "default-branch-commit"
# The module publishes semver releases with attested assets, and the control
# plane pins by release tag. Both halves are stated because they are separate
# axes: the first is what this repository promises a consumer, the second is
# how this estate chooses to take it. The earlier reading — no semver tag, so
# the contract is the pinned commit — stopped being true at v0.1.2.
compatibility: "semver"
pin_policy: "version-tag"
publication:
registry: "none"
github_release: "required"
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ contract.

## [Unreleased]

### Removed

- `upstream_go_mod` is gone from `security-tools.json` and its Go type. It was
required to be non-empty and nothing read, fetched, hashed, parsed or compared
it, so it bought no evidence while creating another value to keep in step by
hand. Either URL could have been replaced with arbitrary text and every check
stayed green.

### Added

- `cmd/check-goal-journals` holds every tracked Goal journal to the Go contract
Expand All @@ -19,6 +27,20 @@ contract.

### Fixed

- `git` and `ssh-keygen` have one definition. `internal/provenance` repeated
`/usr/bin/git` as a raw literal beside `internal/signatureverify`'s constant,
and the isolated environment was written out twice — one fact in two places
that could drift apart in the code path where drift matters most. Both now
resolve through `internal/trustedexec`, which searches a fixed ordered list of
absolute directories, never `PATH`, and requires the result to be a regular,
non-symlink file that is not group or world writable. This also lets a
Homebrew macOS host verify, which the single hardcoded path did not.
- `.gds/repository.yaml` states how the module is actually consumed. Its module
block declared `commit-contract` and `default-branch-commit`, justified by a
comment saying no semver tag existed — which stopped being true at `v0.1.2`.
It now declares `semver` and `version-tag`, and says why those are separate
axes: what this repository promises a consumer, and how this estate takes it.

- `.agent-runtime/goals/first-v0-release.json` is a journal this module accepts.
It recorded each recovery cycle under an invented receipt key, which the Goal
contract does not permit, so `agent-runtime goal status` refused to load it —
Expand Down
17 changes: 17 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ Avoid placing secrets in manifests, instructions, command arguments, logs, or
issue reports. Captured command output may contain sensitive data and should be
handled accordingly by callers.

### Executables the trust path runs

Signature and provenance verification shell out to `git` and `ssh-keygen`, so
which file those names refer to decides what a verdict is worth. They are never
resolved through `PATH`: the runtime searches a fixed, ordered list of absolute
directories — `/usr/bin`, `/bin`, `/usr/local/bin`, `/opt/homebrew/bin` — and
requires the result to be a regular, non-symlink file that is not group or
world writable. Children of the trust path are given exactly those directories
as their `PATH` and nothing they inherited.

The cost is real and deliberate: a host that keeps its tools outside those
directories, which in practice means Nix and Guix, cannot run verification at
all. Every mechanism that would cover them — an environment override, a `PATH`
search — is an ambient input into the one code path that must not have any.
Verification fails with a message naming what was searched rather than an
obscure exec error.

## Release integrity

Official releases originate only from annotated signed `vMAJOR.MINOR.PATCH`
Expand Down
9 changes: 4 additions & 5 deletions internal/cicontract/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ type Contract struct {
CompatibilityGo string `json:"compatibility_go"`
}
type Tool struct {
Module string `json:"module"`
Version string `json:"version"`
MinimumGo string `json:"minimum_go"`
UpstreamGoMod string `json:"upstream_go_mod"`
Module string `json:"module"`
Version string `json:"version"`
MinimumGo string `json:"minimum_go"`
}

func Load(path string) (Contract, error) {
Expand All @@ -45,7 +44,7 @@ func Load(path string) (Contract, error) {
return Contract{}, errors.New("contract has missing or unsupported fields")
}
for name, tool := range map[string]Tool{"govulncheck": c.Govulncheck, "staticcheck": c.Staticcheck} {
if tool.Module == "" || tool.Version == "" || tool.MinimumGo == "" || tool.UpstreamGoMod == "" {
if tool.Module == "" || tool.Version == "" || tool.MinimumGo == "" {
return Contract{}, fmt.Errorf("contract tool %q has missing or unsupported fields", name)
}
}
Expand Down
10 changes: 8 additions & 2 deletions internal/provenance/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"github.com/ProtonMail/go-crypto/openpgp"

"github.com/NDDev-it-com/agent-runtime/internal/signatureverify"

"github.com/NDDev-it-com/agent-runtime/internal/trustedexec"
)

const maxAPIBytes = 8 << 20
Expand Down Expand Up @@ -564,9 +566,13 @@ func actionRunID(details string) (int64, error) {
}

func readSignedCommit(ctx context.Context, root, sha string) ([]byte, []byte, error) {
command := exec.CommandContext(ctx, "/usr/bin/git", "cat-file", "commit", sha)
git, err := trustedexec.Git()
if err != nil {
return nil, nil, err
}
command := exec.CommandContext(ctx, git, "cat-file", "commit", sha)
command.Dir = root
command.Env = []string{"PATH=/usr/bin:/bin", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=" + os.DevNull, "GIT_CONFIG_COUNT=0", "GIT_NO_REPLACE_OBJECTS=1"}
command.Env = trustedexec.Environment
raw, err := command.Output()
if err != nil {
return nil, nil, fmt.Errorf("read integration commit object: %w", err)
Expand Down
Loading