agent-runtime provides two vendor-neutral work-unit contracts for agents:
- A Task is bounded and atomic, with explicit acceptance represented by a versioned Task manifest.
- A Goal is complex or long-running and carries a durable, versioned journal, living acceptance checklist, ordered phases, and machine-verifiable receipts.
The Go packages can be embedded; the reference CLI is useful in scripts, contract tests, and restart-safe agent workflows.
The optional observability package derives provider-neutral lifecycle events
from those authoritative work units and delivers them through redaction-aware
sinks. It never replaces Task results or Goal journals.
Repository governance is declared by the versioned
governance/main-v1alpha1.json contract and
validated by go run ./cmd/check-governance-contract. See
docs/repository-governance-v1alpha1.md.
The initial release focuses on the boundary between a control plane and an already-installed agent client. It does not implement a model provider, tool protocol, scheduler, or operating-system sandbox.
Requires Go 1.25 or newer.
go install github.com/NDDev-it-com/agent-runtime/cmd/agent-runtime@v0.2.0
agent-runtime task validate --manifest examples/basic/agent.json --workspace examples/basic
agent-runtime task run --manifest examples/basic/agent.json --workspace examples/basicThe command receives the assembled instruction context on standard input. Its combined standard output and standard error are returned as one JSON object:
{"agent_id":"example","exit_code":0,"duration_ms":2,"output":"...","truncated":false,"timed_out":false,"cancelled":false,"accepted":true}{
"schema_version": "v1alpha1",
"id": "example",
"description": "Minimal local agent",
"instructions": ["AGENTS.md"],
"command": ["sed", "-n", "1,3p"],
"acceptance": {
"exit_codes": [0],
"output_contains": ["AGENTS.md"]
},
"workdir": ".",
"env": ["PATH"],
"timeout": "30s",
"max_output_bytes": 1048576,
"max_context_bytes": 1048576
}Acceptance is explicit and machine-evaluated: the observed exit code must be in
exit_codes, and every optional output_contains value must occur in the
bounded combined output. Task instruction order is significant. Each file is preceded by a deterministic
--- path --- boundary. Unknown fields, duplicate paths or environment names,
invalid identifiers, and unsupported versions are rejected.
Defaults are a five-minute timeout and 1 MiB each for context and captured
output. Maximums are 24 hours, 16 MiB of context, and 64 MiB of output.
max_context_bytes bounds the read itself, not only the assembled result, so an
instruction file larger than the budget is rejected from its metadata without
being loaded.
A run that ends early reports why. timed_out means the manifest timeout
elapsed; cancelled means the caller ended the run, either by cancelling its
context or by reaching its own deadline. Neither is reported for a process that
returned its own exit status, however the context behaved meanwhile: a command
that failed on its own is a failure, not a cancellation. The returned error
wraps the caller's cause so errors.Is still works.
The manifest timeout bounds the Task, not the call. A terminated run is given a
two-second grace to release its output pipes before the runtime stops waiting,
so the wall-clock ceiling for Run is the timeout plus that grace.
The canonical distributable schema is
schemas/task-manifest-v1alpha1.schema.json.
The project is released as a Go module/source product. It does not publish prebuilt platform binaries.
The current release is v0.2.0. v0.1.2 was the first published one.
v0.1.0 and v0.1.1 were tagged but never published — the first pinned a Go
toolchain below the module's own directive, the second read a repository setting
its token cannot access, and both failed before building an asset. Those tags
and their Go module proxy entries are immutable, so they are left in place
carrying no release assets and no attestations. Use v0.1.2 or later.
v0.2.0 breaks the manifest contract deliberately: a Task manifest that states
a zero timeout, output or context bound is now refused rather than silently
widened to a default. Manifests that omit a bound are unaffected.
Each tag-only release will contain one deterministic tracked-source archive, an
SPDX 2.3 JSON SBOM, canonical release notes, a release manifest, and
SHA256SUMS. The annotated signed tag identifies the exact main commit;
each dry-run or publication build also emits a versioned machine-readable build
result that binds its canonical artifact root, source and AGPL license inputs, and exact asset
path/size/digest closure.
GitHub OIDC/Sigstore attestations cover every material asset. See
docs/releasing.md for verification and rollback rules.
The repository-owned provenance contract separates
owner SSH-signed source commits, GitHub OpenPGP-signed protected-main merge
commits, and owner SSH-signed release tags. Its native verifier pins reviewed
public trust bytes and exact PR, graph, workflow and check identities on both
Linux and macOS without ambient GPG or Git trust configuration.
Every Goal progresses through exactly these phases:
orientgap_planexecutereconcileself_reviewcompleteness_omission_auditverifyclosure
Each transition requires typed evidence and a phase receipt. Closure additionally
requires an achieved outcome, cleanup record, explicit typed debt/risk list, and
canonical next-work references. All acceptance checklist items must be complete
with evidence. Passing one test or reaching execute can never complete a Goal.
agent-runtime goal init --journal goal.json --id release \
--intent 'Ship a release-ready runtime' \
--acceptance build='The binary builds' \
--non-goal 'Remote orchestration'
agent-runtime goal advance --journal goal.json --revision 1 --phase orient \
--summary 'Inspected repository and prior attempts' \
--evidence-type command --evidence-ref 'git status --short' \
--evidence-result 'clean repository'
agent-runtime goal status --journal goal.jsonMutations require the expected journal revision, use an exclusive file lock,
write a synced temporary file, and atomically replace the journal. Loading the
journal restores the full checklist and evidence after restart or compaction.
Goal identity, sealed receipts and recorded acceptance history are immutable:
the store validates the transition itself, so no mutation can rewrite what a
prior phase reported.
The canonical distributable schema is
schemas/goal-journal-v1alpha1.schema.json.
Evidence discovered later can be appended to an existing receipt with
goal evidence; receipt summaries and existing evidence remain immutable. The
--*-type, --*-ref and --*-result flags are repeatable and positional with
respect to each other, so one command can record several evidence records; an
unequal count is rejected rather than silently truncated.
The journal is designed for durable evidence, so commit it when it represents
public project work. The adjacent *.lock file is ephemeral and ignored.
memory, _ := observability.NewMemorySink("events", 100)
emitter, _ := observability.NewEmitter(
observability.Runtime{ID: "runtime-1", Version: "0.1.0"},
[]observability.Sink{memory}, observability.Options{},
)
observed := observability.TaskRunner{
Runner: agentruntime.Runner{Workspace: workspace}, Emitter: emitter,
Context: observability.Context{
CorrelationID: "request-1",
Actor: observability.Actor{Kind: observability.ActorWorker, ID: "worker-1"},
Attempt: observability.AttemptInitial,
},
}
run := observed.Run(ctx, manifest)run.Result and run.ExecutionError remain authoritative. run.Events and
run.Delivery expose immutable observations and every sink outcome. The package
also provides GoalStore, explicit handoff events, an in-memory sink, and a
durable JSONL sink. See docs/observability-v1alpha1.md
and the canonical event schema.
Task manifests and commands are trusted inputs. The runtime resolves the workspace, working directory, and instruction files through symlinks and rejects paths that leave the workspace. Child processes receive only environment variables named by the manifest. Context and captured output are bounded, and the bound holds on what a caller receives: repairing invalid UTF-8 can lengthen output, so the limit is reapplied after the repair.
On Linux and macOS the Task runs in its own process group and cancellation or a timeout terminates the group, so work the command backgrounded does not outlive the result. Elsewhere only the direct child is terminated. This bounds what the runtime started; a process that deliberately leaves its group is out of reach, and this is not a sandbox.
A bare command name is resolved against the PATH the runtime reads through
Runner.LookupEnv — the same source that supplies the values named by the
manifest allowlist — so resolution and the child environment come from one
place. An embedder that supplies LookupEnv therefore decides which executable
runs; the default reads the process environment, so the CLI still resolves
against the caller's PATH. Empty and relative PATH entries are skipped
because they resolve against the runtime's working directory rather than the
Task's. Result.executable_path records the file that ran.
These controls are not a sandbox. A trusted command can still access files,
processes, credentials, and networks available to its operating-system identity;
descendant-process cleanup is platform dependent. Run untrusted agents in a
container, VM, or OS sandbox with least-privilege credentials. See
SECURITY.md and docs/architecture.md.
Observability defaults to fail-closed redaction, driven by a vocabulary of attribute names and values rather than by scanning a value for the shape of a secret: naming an attribute honestly is the caller's job. Attribute sensitivity must be declared; confidential/secret, raw command/environment/provider content, credentials, unsafe URLs, binary values, errors/stringers, unknown structures, and oversize content are never sent to sinks. Redaction decisions report only reason/count pairs.
go test -race ./...
go run ./cmd/check-fuzz
go vet ./...
go build ./cmd/agent-runtimeThe public API is pre-1.0 and the manifest is explicitly v1alpha1. Breaking
contract changes are recorded in CHANGELOG.md.
- Bugs and scoped feature requests belong in GitHub Issues.
- Larger deferred work is kept in
ROADMAP.md, linked to an issue before implementation. - Security reports follow
SECURITY.md, not public issues. - Contributions follow
CONTRIBUTING.md.
Licensed under the GNU Affero General Public License v3.0 only
(AGPL-3.0-only).