Bundle formats (v1)
The shared data formats for sharing rig bundles: the configurations a bundle offers, the before-install view of what it
will do, run records and the public status file, and registry entries. The schemas are in schemas/, with
fixtures that pass and fail them in schemas/fixtures/. Tools outside this repository read them
at a pinned commit.
A v1 format only grows: new optional properties may appear, and consumers ignore properties they don't know. Any other change is a v2. The schemas are open to new properties, with three closed on purpose: the package digest (an exact value), a run record's subject (a harness check can never carry a team's identity), and the public status file, where nothing outside the schema may appear, so a private field can't leak in. A new property in a closed part is a v2. The status schema also rejects labels that contradict their evidence: a tested label needs records and a package, tested with help needs its count, tested can't carry a count above zero, and an unreadable listing carries only Status unavailable.
Three identities, kept apart
| What | Name | Defined by |
|---|---|---|
| What someone chose | configuration ID | the mapping of every seat to a runtime |
| What was packaged | package digest | the built archive's files |
| What actually ran | execution binding | a run record (resolved resources, settings, environment, unknowns) |
None stands in for another. A package digest doesn't describe what ran, and a configuration ID doesn't describe what was packaged.
Source identity
A bundle on GitHub is named by repository (https://github.com/<owner>/<repo>, no credentials), folder (the
repo-relative path of the folder holding rig.yaml, or . for the repository root) and resolvedCommit (40 lowercase
hex).
requestedRef(the branch, tag or commit someone gave, orHEADfor a link without/tree/…) andcanonicalUrl(…/tree/<resolvedCommit>/<folder>, with each folder segment percent-encoded and no folder part for the root) explain it, and aren't part of it.- A built bundle records its source in its manifest's provenance, which is outside the package digest.
Configuration ID
Every member's pod.member=runtime, sorted by pod.member in UTF-16 code-unit order, joined with ,. There's no
whitespace, and runtimes are spelled as in rig.yaml. For example:
build.impl=claude-code,build.lead=claude-code,check.qa=codex,check.review=codex
Preset names such as recommended or all-claude are aliases shown beside the ID, never part of it.
Package digest
{ algorithm: "sha256", value, coverage: "openrig.package-digest/v1" }:
valueis SHA-256 over UTF-8 lines<quoted path>\t<sha256>\n, one for each entry of the built archive'sintegrity.files, sorted by path in UTF-16 code-unit order;<quoted path>is the path as a JSON string, exactly as JavaScript'sJSON.stringify(path)writes it, so a tab or newline in a file name can't make two different file lists hash the same. In words: wrapped in";"and\escaped with a backslash; U+0008, U+0009, U+000A, U+000C and U+000D written\b,\t,\n,\fand\r; any other character below U+0020, and any lone surrogate, written\uXXXXin lowercase hex; everything else as itself;- it's the same when the same folder is rebuilt by the same OpenRig, while the archive's own hash changes with every
build (
createdAt).
Coverage, shown wherever the digest is: the bytes of every packaged file. It doesn't cover:
bundle.yaml, so no manifest field, provenance orcreatedAt;- the names the integrity walk skips (
.DS_Store,Thumbs.db,.gitkeep); - file modes;
- anything resolved on the installing machine at launch, for example an
openrig-home:plugin, catalog skills, or a model or effort nothing declares.
A build result carries source (null unless the bundle was built from a GitHub link), configurationId,
packageDigest, archiveHash and assembler. rig bundle inspect reports the same fields for an archive, and any the
archive doesn't state are null. assembler is { openrigVersion }: the format also allows a commit, which this
OpenRig doesn't write. configurationId() and packageDigest() are exported from @openrig/daemon/bundle-identity.
Shared test vectors are in schemas/fixtures/identity-vectors.json.
The formats
| Format | Schema | Lives | Written by |
|---|---|---|---|
| Declared configurations | bundle-configurations.v1 |
configurations.yaml beside rig.yaml |
the bundle's author |
| Before-install view | bundle-behaviour.v1 (openrig.bundle-behaviour/v1) |
the registry, one file per configuration and assembler version; never inside the archive | the daemon's inspect: rig bundle inspect, under behaviour with --json |
| Run record | run-record.v1 |
beside its receipt, private | Fleet, Dev QA, and maintainers for community reports |
| Public status | bundle-status.v1 |
openrig-world status, generated |
the status generator only |
| Registry entry | registry-entry.v1 |
openrig-world/registry/<slug>.yaml |
maintainers, through review |
Declared configurations
seats gives a pod.member the runtimes it may use, and the profile each runtime uses. A member it doesn't list keeps
the runtime rig.yaml gives it. presets names full mappings, and recommended names one of them, which must be
rig.yaml as written. rig bundle create, rig bundle configurations and rig bundle check refuse a file that breaks
these rules.
--preset and --seat choose a configuration (see the rig bundle reference). Any mix of each seat's
declared runtimes can be built, not only the presets; a runtime a seat doesn't declare is refused, and nothing is built.
Without either flag, create builds rig.yaml as written. The built archive's manifest records
configuration: { id, preset } outside the package digest; preset is there only when --preset or --seat chose a
mapping that matches a declared preset.
Before-install view
It's derived from the archive's files alone. Nothing is launched, probed or fetched. It's a view, never a gate, and carries no tested status.
identity: source, configuration ID, package digest, the archive's assembler, the generator that made the view, stated compatibility, stated provenance (not verified), and integrity (self-consistency, not authorship). A source, configuration ID, package digest or assembler the archive doesn't state isnull; unstated compatibility and provenance are empty objects. None of it is filled from the inspecting OpenRig.team,posture,toldFiles,alsoRuns,writes,outsideAddresses,needsandunknownBeforeLaunch, in that order. Facts cite the archive file they come from (sourceRefs), exceptteamentries and the bundle-wide unknowns.toldFilesandalsoRunsalso say how each resolves:archive,host_at_launchorunresolved.- Posture separates what the archive declares from the product's default. Its effect on the host is
unknown, because host settings decide it. Optionalposture[].permissionPromptsisofffor declaredfull_bypass(including rig- or member-levelbuiltin:yolo) on any runtime; for Claude/Codex,onfor a declared flag-surface floor policy ordefaultwhen no policy is declared (prompts on unless the installing machine's settings turn them off); for Codex,onfor declaredauto. Omit it for Claude under auto (naming auto mode inselection), config-surface policies (builtin:locked,builtin:standard,builtin:openor an archivedsurface: configpolicy), host-resolved Codex profiles,permission_policy: none, a policy the archive can't resolve, and every runtime other than Claude and Codex (Pi, terminal) except declared full bypass.selectionnames the declared policy. An absent field means not stated, neveron. Foroff, Claude bypasses permissions, Codex runs with full access and never asks, and Pi gets full resource trust (--approve, not a permission mode). These are archive facts;nativeEffectremainsunknown. Optionalposture[].firstRunWarnings.claudeBypass: "harness_asks_once"names the first-run bypass warning for a declared full-bypass Claude seat, conditional on whether the harness has remembered acceptance.posture[].nonInterruptive: "available"says a full-bypass Claude/Codex seat can use non-interruptive launch flags. It does not select the option, read host acceptance state or prove a prompt-free launch. Both fields are omitted where inapplicable or unresolved; older v1 views remain valid without them. Optionalposture[].nonInterruptiveDefault(boolean) carries the rig's authorednon_interruptivefor those seats; an explicit launch choice overrides it. When it istrue,firstRunWarningsis omitted. Pi has no additional warning-suppression flag.nativeEffectstaysunknown. - An empty list means none are known. Something unknown goes in
unknownBeforeLaunch. - Per-seat facts carry
seat(pod.member) and human output labels them with that identity. It's required onteamandposture. Elsewhere it's optional: bundle-wide facts (services, library and project writes, setup and version needs) omit it, and older generators may not have stated it.
- Posture separates what the archive declares from the product's default. Its effect on the host is
not_generated: a combination without a generated view, or an archive the generator can't describe: schema 1, no readable pod-aware rig spec, a member missing its pod, id or agent reference, a runtime that depends on an AgentSpec the archive can't read, or an error while reading. It carries a reason and the local command that shows it, and no sections.
Declared setup preconditions
The author's bundle.yaml, beside rig.yaml, may declare setup that the user must do before installing:
preconditions:
- name: Run inside an OpenRig source clone with dependencies installed and packages built.
commands:
- git clone https://github.com/mvschwarz/openrig.git
- cd openrig
- npm ci
- npm run build
- name: Sign in to the selected coding runtimes before starting the team.
Local-folder creation and GitHub-link imports carry these declarations into the archive manifest. A generated
before-install view lists each as needs[] with kind: precondition, the author's name, optional ordered commands,
status: not_checked, and a sourceRefs entry naming bundle.yaml and preconditions[index]. Inspect, plan and apply
previews show the same declaration. A legacy schema-1 archive retains it in the manifest, but its view remains
not_generated. Missing preconditions mean none were declared, not that no setup is needed.
Each command is one plain command line, without shell operators (;, &&, pipes, command substitution, backticks or
redirection). Run them in order in one shell, then run the install command in that same shell: it inherits the
working directory left by the setup commands. There is no separate workdir field. Commands are untrusted author data;
a site building a copyable block must validate every line and bound the number of entries, omitting all setup lines
if any fails its plain-command rule. OpenRig displays these declarations; it does not execute them, verify that setup
was done, or add a prerequisite gate. The archive hash covers the manifest; packageDigest excludes bundle.yaml
and therefore does not cover these declarations.
At creation, malformed preconditions produce a warning and the whole block is omitted; other author-field validation is unchanged. Shell operators in otherwise valid command lines produce a warning, but the declarations are retained as data. An already-built archive with malformed preconditions reports them as unknown before launch.
Run records and status
A run record's subject is either a team (source, configuration ID, package digest, assembler) or a harness check (a runtime). A harness check never upgrades a team label.
A run record's environment.platform and arch are Node's process.platform (linux, darwin or win32) and
process.arch (x64 or arm64): the values a status platform key can carry, so every valid record can be labelled.
Records are private. Only outcome.publicNote (a short public cause) reaches the status file.
Relations: supersedes, withdraws and resolves. Reusing earlier evidence for a changed package is a reviewer's
decision, recorded in the registry entry's evidenceReuse.
The status file is generated from records by rule openrig.status-rule/v1:
- Labels:
known_problem,tested,tested_with_help,partly_tested,not_testedandstatus_unavailable. They're shown as Known problem, Tested by OpenRig, Tested with help (N), Partly tested, Not tested by OpenRig and Status unavailable. - A label belongs to one configuration ID. No configuration is ever shown another configuration's label.
- Empty
platformsmeans Not tested by OpenRig. So does a custom configuration, one the listing doesn't offer. - Status unavailable is only for a listed configuration whose records can't be read, or that's missing from the
status file; never Not tested by OpenRig. A listing whose records can't be read is
status_unavailableas a whole. - Platform keys are
<os>-<arch>(linux,darwinorwin32;x64orarm64). testedmeans no help was needed: it can't carry an assistance count above zero.- An assist is one action a person or the test runner took to get past a problem during the run, such as restarting a seat or re-running a command; following the bundle's own documented steps doesn't count.
- The file holds no private paths, host names, row IDs, account names or receipt text, and regenerating it gives identical bytes.
bodyDigestis SHA-256, lowercase hex, over the RFC 8785 (JCS) canonical JSON, as UTF-8, of an object holding every other top-level field. The site import recomputes it and rejects a hand-edited file.harnessCheckshas its ownstate, so an unreadable harness-check record reads Status unavailable too.
Registry entries
Each entry records:
- the source at its reviewed commit;
- each offered configuration with its package digest, assembler and behaviour-view file;
- optional
evidenceReuse; - the review date, and
listedorwithdrawn(withwithdrawnOn).
v1 has no author field: a listing shows the owner of source.repository.
A listing shows "Reviewed for listing on <date> at commit <short>. Review is not a security audit."
From docs/reference/bundle-formats.md at OpenRig v0.6.6, corrected after v0.6.6 (commit 51bd931e) to describe 2620dea8