Skip to content

Latest commit

 

History

History
112 lines (91 loc) · 5.96 KB

observability-v1alpha1.md

File metadata and controls

112 lines (91 loc) · 5.96 KB

Observability contract v1alpha1

This contract implements issue #3. It is provider-neutral and deliberately separate from authoritative Task results and Goal journals.

Model

An Envelope is an immutable observation produced from a validated Draft. It contains a stable content-derived event ID, subject/correlation/causation identity, per-subject sequence, UTC timestamp, actor/runtime identity, attempt classification, outcome, typed lifecycle payload, safe attributes, and a redaction summary.

Sequences create a total order only within one (subject kind, subject id) stream. Correlation and causation express relationships across streams without inventing a global order. A Goal subject includes its authoritative journal revision. Retrying delivery reuses the same envelope and event ID. Restart uses JSONL replay checkpoints to seed each stream's next sequence.

Canonical event kinds are:

  • task.validated, task.started, task.completed, task.failed, task.blocked, task.cancelled;
  • goal.created, goal.checklist_added, goal.checklist_completed, goal.receipt_evidence_added, goal.phase_transitioned, goal.completed, goal.blocked;
  • handoff.dispatched, handoff.accepted, handoff.completed, handoff.failed, handoff.blocked.

A Task ended by its caller is task.cancelled with outcome cancelled, not task.failed. The distinction is load-bearing: the work did not go wrong, it was stopped, and retry classification and operator metrics read it differently. It requires cancellation error evidence, an unaccepted result and no blocking evidence, so the outcome cannot be claimed without the evidence that justifies it. A Task that exceeds its own timeout remains task.failed with timeout error evidence, because that is the Task failing its own contract.

The adapters derive these events from existing Task results and before/after Goal journals. They do not own another state machine.

Redaction boundary

Raw attributes exist only in Draft. Emitter.Emit applies Policy before it constructs an Envelope; every Sink receives only an envelope. An envelope may carry only public and internal attributes, so NewEmitter refuses any higher maximum sensitivity rather than letting redaction pass an attribute that envelope validation then rejects, which would discard the whole observation. The policy denies confidential, secret, unknown sensitivity, binary data, invalid/cyclic or unsupported structures, errors, formatters/stringers, raw commands, environment/provider content, credentials, tokens, and URLs.

Redaction is driven by that vocabulary, not by inspecting a value for the shape of a secret. An attribute named private_key, secret, token, password or credential is redacted whatever it holds; an attribute named note or payload is published whatever it holds. An OpenSSH private-key PEM header carried under a neutral name reaches the sink verbatim, with an empty redactions list.

Naming the attribute and declaring its sensitivity honestly is therefore the caller's responsibility, and SECURITY.md says to treat every attribute as potentially secret for that reason. The emitter is not a content scanner and does not attempt entropy or pattern detection, which would be both incomplete and a source of false positives on ordinary output.

The word list applies to attribute names, nested keys and values. It is not applied to identities. A runtime, sink, correlation, subject or actor identifier is validated against the identity grammar only, because an identifier carries no value and filtering it there rejected Task identities the manifest contract accepts.

Nested map keys are reclassified independently. Strings, depth, collection length, attribute count, and total canonical envelope bytes are bounded. Redaction summaries expose only deterministic reason/count pairs—never paths, lengths, hashes, or redacted values.

Sink and delivery semantics

Sink is synchronous and safe for concurrent calls. Synchronous delivery is the backpressure mechanism: a caller controls the deadline with context. Write is idempotent by event ID and returns an explicit duplicate result. Typed sink errors contain a stable code and retryability, never a raw underlying message. Emitter retries only retryable failures, with a bounded attempt count, and returns a delivery report for every sink. Delivery failure cannot alter the Task result or Goal journal and cannot disappear from the caller.

MemorySink is bounded and intended for tests or short-lived collection. JSONLSink appends canonical envelopes under a lock, optionally syncs every record, detects duplicates on restart, rejects unsupported/corrupt/partial records, becomes poisoned after an uncertain partial write, and has explicit Flush/idempotent Close behavior. Opening the sink and replaying a file share one definition of a valid history — canonical single-line envelopes, a newline-terminated final record, unique event identity, and a strictly increasing sequence within each subject stream — so a file can never be acceptable to append to yet impossible to replay. JSONL is an observability derivative, not a journal or recovery authority.

Compatibility and rollback

The package and schema are additive v1alpha1. Existing Task/Goal APIs and schemas are unchanged. Callers opt in by constructing an emitter and lifecycle adapter. Rollback removes emission/sinks without journal migration. Existing JSONL remains independently readable by schema version.

Sources