Action-only observation reference
Development-branch contract. Raw admission, bounded normalization, typed collections, desired comparisons, durable decisions, approval/automatic dispatch and shared REST/GraphQL/UI reads are implemented. Execution verification uses independent reads under frozen terms. Synthetic runtime/server/DNS workflows are verified; native execution, real-provider and release qualification remain separate. Unsupported persisted state is refused without conversion or deletion.
Definition contract
Define producers, subscribers, comparisons, policies and action-local finality in
actions/*.toml. Use module.toml for module identity, parameters and dependencies.
See authoring.
Producer identity is module:actions/path.toml. observe.from references a
producer directly. Producers own normalization, fixed string scope and type overrides.
Subscribers cannot override them. A producer can emit multiple named collections
without executable commands or desired graph resources.
Artifact boundary
Only raw-input HTTP admission publishes observations. Result/state-file uploads do
not normalize, reserve observation ordering, or produce observation receipts.
Artifact uploads reject X-Rescile-Verification-Read; use the raw-input endpoint for
an eligible controller-ticketed independent read. Ordinary result limits and state
bookkeeping remain unchanged.
Comparison contexts
Controller consumers use InputService::comparison_context(as_of) to pin canonical
action-selected desired values, immutable configuration and a durable observation
revision together. New contexts refuse stale configuration, recovery or backward
clock state rather than returning empty success. A previously pinned context remains
a consistent read after reload or new observations; it is not fresh authorization
for a later approval or dispatch.
origin_resource and match_on select intent using ordinary action semantics.
Matching retains all ambiguous provider keys and never fabricates desired vertices.
The action-only context does not use a rule/binding registry, persist a finding cache,
normalize data or create decisions/jobs. The public APIs below consume the same pinned
projection; policy reconciliation is a separate writer.
Offer projection
The read-only service aggregates action-owned comparisons. Any usable watched
difference yields difference; all equal yields converged; missing or incompatible
comparisons yield unknown. Unmatched, ambiguous or unavailable resources yield
not_matched, never implicit creation, deletion or adoption. Explicit approval/auto
mode does not itself grant execution authority. These projections do not create
approvals, episodes or jobs, and do not release possible work.
Raw input
POST /api/v1/observation-inputs
Required content type: application/json. Content encoding is not supported.
The active workspace is implicit.
{
"version": 1,
"producer": "cloud-servers:actions/inventory.toml",
"submission_id": "11111111-1111-4111-8111-111111111111",
"read_at": 1793692800,
"collections": {
"server": {
"coverage": "complete"
}
},
"payload": {
"instances": [
{
"id": "vm-739",
"name": "server-a",
"vcpus": 4,
"network_ids": [
"network-9"
]
}
]
}
}
| Field | Meaning |
|---|---|
version |
Exactly 1 |
producer |
Known captured producer declaration |
submission_id |
Non-nil UUID; never reuse for another input |
read_at |
Nonnegative Unix seconds when the provider read occurred |
collections |
Named coverage claims: complete, partial, or failed |
payload |
Raw JSON supplied to pure Jsonnet |
Unknown envelope fields are rejected. Scope, params, lookups and authority are not caller fields. New timestamps allow at most 60 seconds of future skew. A controller-issued verification-read header binds a separate independent execution read; ordinary raw inputs never establish terminality. Verification requires the captured producer, scope and recipe, complete target evidence, and same-read provider plus runner/root/descendant/resumable terminal assertions. Equality or exit zero is not proof. Durable finalization and journal scope closure remain separate.
A successful 202 response contains producer, submission ID, durable per-producer
sequence, disposition (accepted or replayed) and normalization status.
Statuses are pending, applied, partial, failed, late, or superseded.
Admission is durable before acknowledgement; normalization is asynchronous.
Replay, ordering and retention
Canonical interpreted envelope content detects exact replay and changed-content conflicts for 1024 receipts per producer. Exact wire JSON is retained separately. A persisted retired-read floor refuses expired unknown submissions. This is a finite replay guarantee: UUID reuse with changed time outside the window is undetectable and forbidden. Execution deduplication must never use this window.
Read time prevents late inputs replacing newer facts; controller sequence orders equal-time inputs. Complete coverage replaces membership. Partial coverage replaces whole reported rows without refreshing unreported rows. Failures retain unusable history. Missing output/coverage, type failures and duplicate keys fail collection-locally when the normalizer can isolate them.
Active raw/context retention is capped at 16 inputs per producer and 32 MiB aggregate. After 24 hours, unpinned completed raw inputs are eligible for admission-triggered cleanup. Pending and proof-pinned inputs cannot be evicted. Capacity refusal leaves durable pending inputs intact, without partially publishing results.
These limits do not erase immutable generations or backups and do not bound total historical disk usage. Normalized rows retain self-contained provenance.
Normalization and types
The result is an object mapping collection names to row arrays. Each row has a
nonblank unique string key, optional string name, and values object.
Logical types are null, boolean, integer, float, string, array,
and object. Inline overrides use observe.types.<collection>.
No string/boolean coercion occurs. Integer requires an integer JSON representation; float permits numeric JSON and retains float metadata even for an integer representation. Explicit null is distinct from absence. Non-null overrides reject null; nullable override syntax is deferred.
Jsonnet numbers must stay within ±(2^53−1). Large provider IDs must already be strings. Unsafe input is a global failure; unsafe output can fail its collection independently.
The owned worker receives only payload, frozen params, controller context
(producer_id, scope) and explicit lookups. No imports, application native functions,
network, environment or Vault acquisition. Limits: one active child per controller
process, 5-second active wall/CPU budget, 1 GiB address space, logical stack 128,
2 MiB request, 4 MiB response and JSON depth 64. Cancellation kills/reaps the child.
Unsupported worker platforms refuse. Address space is not RSS.
HTTP limits: 1 MiB body, four shared body readers, 16 pending inputs, 60 requests/minute/known producer and a bounded body-read deadline. Existing CE open access and optional EE authentication remain unchanged.
Matching and comparison
origin_resource selects desired graph vertices. observe.match selects one desired
property and one observed property. Observed name/key are reserved metadata;
other names address values. No fallback, string coercion, graph-UUID identity,
implicit adoption or deletion occurs.
Matching preserves ambiguity. Desired-only, observed-only, invalid-match, evidence-unavailable and matched states are distinct. Partial inventory cannot prove absence. Failed, stale, future-dated or configuration-invalid evidence does not produce usable differences. Each row keeps its own read time.
Comparisons have stable IDs: explicit id, otherwise property. IDs are unique
within an action; repeated properties require explicit IDs. observed_property
defaults to property; operator defaults to equal. Logical types must agree:
integer and float are not implicitly interchangeable. Same-kind floats compare numeric
values. Arrays use ordered equality; DNS sets require explicit normalization.
Equality, difference and unknown are read results, not execution authorization.
Only watched/matching desired properties are projected, not arbitrary graph fields.
Default comparison freshness is 60 seconds; action-local max_age overrides it.
Frozen material and software compatibility
Recipes freeze an evaluator ABI derived from normalization source, dependencies, compiler and target/code-generation inputs. Controller and worker must agree before results are accepted. Incompatible retained recipes refuse store reopen before cache publication/pruning; incompatible candidate state refuses before commit. A worker ABI mismatch leaves admitted input pending and prior collections unchanged. Older development state without the required stamp is also refused. These stamps check compatibility, not binary provenance or attestation; they are conservative and can change between builds. No automatic migration, replay under new semantics or reset.
Full configuration pins include captured observation material and execution-policy compatibility. Capture checks source bytes against both the immutable declaration and its file manifest, including runtime files of zero-instance declarations. Source/parameter/registry changes cannot silently substitute new policy at handoff. Execution integration uses these pins before protected delivery and independent reads.
Shared public reads and explicit decisions
REST uses the active workspace implicitly: GET /api/v1/observations/collections,
resources, differences, or decisions. GraphQL uses the stable observedState
root with kind selecting the same four views. Its context, items and page
JSON scalars have the same shape as REST. No producer-specific schema file or schema
rebuild follows an observation update.
Each envelope contains status (available or unavailable), context, items
and page (has_more, next). Context binds configuration/observation revisions,
Unix-second as_of, latest read time/age, normalization backlog, ceiling and pause
controls. GraphQL aliases share one pinned context. Unavailable is never empty success.
A declared but unreported collection is not_reported; complete empty is an available
collection with zero rows. Failed collections retain unusable history.
Typed values expose kind, JSON value, and lossless exact_json text. Use
exact_json for exact display: ordinary JavaScript JSON numbers can round large desired
integers. Null and missing stay distinct. Resource availability is not execution
eligibility; action comparisons enforce row freshness and matching. Observed-only rows,
provider keys and desired matches remain visible without creating graph vertices.
Exact filters: producer, collection, name, key, action, status.
Apply filters relevant to the selected kind; resource action selects desired matches.
first defaults to 25, bounded to 1–100; filter strings are bounded to 2048 bytes.
after binds kind/filters/configuration/observation revision and as_of, expiring in
five minutes. Changed context/cursor requires refresh (REST 400 or GraphQL error),
not merged pages. A GraphQL request has a bounded projection budget. Reads never
normalize, reconcile episodes, approve, collect, enqueue or dispatch. Raw inputs,
normalizer source and frozen lookup payloads are not public read items.
Decision items include frozen configuration/material, independent finality target,
effective mode, CAS revision, approval and durable execution status. finalized is not
a claim that the journal scope is free; scope_closure: not_asserted is explicit.
closed_and_rearmed records both closure and fresh valid convergence.
POST /api/v1/observations/decisions/{id}/approve or /reject accepts JSON with
exactly configuration, material, expected_revision copied from the displayed
record. No scope/action/input override is accepted. Approval revalidates current
material and independent finality identity. Conflicts return 409: refresh/review,
never automatically retry an uncertain acknowledgement. Approval does not bypass
current dispatch checks. Queued/possible work cannot be rejected into an unlocked state.
POST /api/v1/observations/pause accepts expected_revision, paused, optional
action; use context.control_revision. Omit action for workspace pause. Controls
cannot raise ceilings. Commands use existing CE/EE authentication and bounded JSON bodies.
The decision book retains at most 10,000 records, then refuses new decisions rather
than evicting execution history.
Policy and finality boundary
trigger.on = "difference" selects approval (default) or auto policy.
Startup auto_execute is unrelated. Approved episode rules require configuration-bound
approvals and fresh valid convergence plus closure of previous possible work before
rearming. Durable decisions deduplicate equivalent reports/restart. Rejection suppresses
the episode even as observations change. Changed material invalidates unsent approval.
Both normalization recipes are bound. A durable collection authority epoch prevents
failure/recovery between policy ticks from reviving an old approval.
Competing automatic actions for one logical scope are held.
An executable action’s [execution_finality] declares producer, collection,
settle_timeout, timeout, and max_age. Positive whole-second durations are
required, with timeout greater than settling. References resolve directly to producers.
These declarations do not replace independent proof or runtime/scope qualification.
Possible execution never unlocks from equality, exit zero, timeout, cancellation or disconnect. Independent same-read state and provider plus runner/root/descendant/ resumable terminal assertions remain mandatory. Existing records and journals are preserved; unsupported state is refused, not overwritten or automatically migrated.