20. Aegis Product-Input Contract Acceptance¶
Date: 2026-09-15
Status¶
Accepted. Amended by ADR-0028.
Context¶
The connected-stack contract in cordanaLLM/Aegis-OS (docs/integration/stack.md) requires every crossing of the Aegis boundary to carry a schema, a correlation identifier, an exact revision, bounded retries, and a recorded result. It asks cordanaLLM/imago for three consumer proofs: the Aegis configuration is accepted, the pinned kernel is consumed, and an output digest with boot evidence is returned. Aegis milestone M18 shipped the product-input manifest build/product-input.json under the schema aegis.p01.product-input.v1 (commit 5148ab27bb4c9d32759e8739b97c05d9b423b912): correlation id, exact revision, the pinned Arch snapshot (Aegis D18), the four configuration references (repart, sysupdate, mkosi, kernel-requirement), the package set, the boot kernel identity (Aegis D07), and a retry budget. The Aegis-side crate validates every field while decoding, so an ill-formed field is a refusal rather than a value checked later or not at all.
ADR-0019 left this contract as a planned milestone flagged as needing an external contract. The planning graph tracks it as step-aegis-schema-pin under req-aegis-contract, whose acceptance criteria are: the Aegis fixture is accepted, a manifest without a correlation id is rejected, and a retry count at the bound is accepted while one above it is refused.
pkg/builder.Request is the Packer flavor dispatcher: it names one of the 44 catalog flavors and a CI backend. The Aegis manifest describes an mkosi/UKI image of a pinned distribution snapshot with an externally produced kernel; no flavor or provisioner in this repository produces that output today.
Decision¶
- Acceptance boundary, not executor.
pkg/aegisaccepts the manifest and stops. A typedProductInputmirrors the fixture field for field with the hyphenated Aegis wire keys. Decoding usesencoding/jsonwithDisallowUnknownFields, rejects trailing content after the first JSON value, and bounds the input toMaxInputBytes(1 MiB) throughio.LimitReader, refusing anything longer before parsing. The fixture is vendored byte-for-byte underpkg/aegis/testdata/product-input.jsonand is the positive test. - Documented bounds as exported constants.
schemamust equalSchema.correlation-idis 1..MaxCorrelationIDLen(128) characters of[A-Za-z0-9._-].revisionis exactlyRevisionLen(40) lowercase hexadecimal characters.distribution.idis a 1..MaxTokenLen(128) token of[A-Za-z0-9._-];distribution.snapshotis a 1..128 token of[A-Za-z0-9./_:-]. Eachdefinitions.*path is required, at mostMaxPathLen(256) characters, relative, forward-slash separated, free of control characters, backslashes, empty segments, and..segments.packagesholds 1..MaxPackages(256) unique names of[A-Za-z0-9._+@-]starting alphanumeric.kernel.sourceis a bounded token (not an enumeration; consuming the pinned kernel is the nucleus contract tracked asstep-nucleus-contract), andkernel.default-packagemust be one ofpackages.retries.max-attemptsis 1..MaxRetryAttempts= 10 andretries.backoff-secondsis 0..MaxBackoffSeconds= 3600. Validation walks bytes against fixed character sets; no regular expression is used, so it is linear in the already bounded input with no backtracking hazard. - Correlated errors. Every rejection is an
*aegis.Error{CorrelationID, Field, Reason, Err}whose text readsaegis product-input <correlation-id>: <field>: <reason>. When the manifest carries no correlation id the placeholder is(missing); when it carries one that is not a valid token the placeholder is(invalid), so an unbounded or unprintable value never reaches a log line. The correlation id is probed leniently from the first JSON value before strict decoding, so unknown-field and trailing-content refusals are correlated regardless of key order. Sentinel causes (ErrInputTooLarge,ErrTrailingContent,ErrDecode,ErrSchema,ErrMissingField,ErrInvalidField,ErrOutOfBounds) supporterrors.Is; theencoding/jsonerror is wrapped soerrors.Asreaches it. - Mapping to an imago-side request.
ProductInput.BuildRequest()validates and returnsaegis.BuildRequest(snake_case keys:correlation_id,revision,distribution,definitions,packages,kernel_source,kernel_package,retry{attempts, backoff}with the backoff as atime.Duration). It is deliberately notpkg/builder.Request: that type dispatches Packer flavors and has no mkosi/UKI flavor, and forcing the manifest into it would fabricate a flavor that does not exist. Binding the accepted request to an executor (mkosi/UKI synthesis consuming the nucleus kernel artifact) is the next planning step. - Symmetric result.
aegis.Resultpins the recorded outcome the stack contract requires:schema(imago.p01.product-result.v1, proposed to Aegis alongside the request/result pair in M09),correlation-id,image-digest(sha256:plus 64 lowercase hexadecimal characters),signature-ref, andboot-evidence-ref(each at mostMaxReferenceLen= 512 characters, no control characters).Result.Validate()returns the same correlated error. Nothing produces a result yet. - CLI.
imago aegis validate <path>loads, validates, and prints the mapped request (--jsonprints theBuildRequest); a rejection exits non-zero with the correlated error.
Consequences¶
- Positive: Aegis M09 can test one request/result pair against a real consumer, and every malformed manifest yields one deterministic, correlated refusal that names the field and the rule.
- Positive: the bounds are named constants recorded here; changing any of them is a contract change that requires a superseding ADR, not a silent edit.
- Negative: the input is parsed twice (lenient correlation probe, then strict decode). Both parses are bounded by 1 MiB and happen once per request at the acceptance boundary, so the cost is accepted for deterministic correlation.
- Negative:
imago aegis validateaccepts but does not build. The executor binding and the Aegis-owned request/result pairing (step-aegis-pair, which needs hardware) remain open in the planning graph. - Neutral: the result schema identifier is proposed by this repository; if Aegis records a different name in M09, a superseding ADR renames the constant.