CI / quality (push) Canceled after 0s
Adds claim-bound online evidence records and citation verification scripts, node removal transport/adapter, retrieval benchmark and owner-review tooling, GraphCanvas/SourceDetail knowledge browser UI, and the ingest handoff helper. Bumps the agents/gewu pack to the online-verification branch.
166 lines
28 KiB
Markdown
166 lines
28 KiB
Markdown
# Acceptance, authority and evidence contract
|
|
|
|
Status: proposed v0.2 after [independent review](../reviews/ARCHITECTURE_SENIOR_REVIEW.md). Drafted 12 October, revised 14 October 2026. Implements the boundaries in the [architecture](ARCHITECTURE.md). Initial record/API schemas and executable tests exist; the full PRD contract audit remains open.
|
|
|
|
## Record separation
|
|
|
|
A source capture records bytes. An extraction records a parser's reading of those bytes. An assessment records an agent or human judgment about a particular assertion. A decision records the authority and checks that permitted a change. Keep separate identities and immutable revisions for each. Repeated claims do not manufacture independent evidence.
|
|
|
|
All public records carry `schema_version`; all stored timestamps are timezone-aware UTC. Domain IDs are globally unique, bounded and never reused. Typed models reject extra fields, non-finite numbers, unsafe paths and oversized collections. The following shapes name the required boundaries; they are not executable JSON Schemas yet.
|
|
|
|
| Record | Required bindings |
|
|
| --- | --- |
|
|
| Source revision | Stable source ID, artifact SHA-256, media type, byte length, capture time, original URL/version where known, license/retention metadata, archive availability. |
|
|
| Extraction | Source revision, parser/version/config digest, extraction hash, ordered blocks, page/timestamp metadata, normalization version, warnings/status. |
|
|
| Assertion evidence | Node/field path, assertion digest, source revision, extraction ID, locator, exact quote and normalization profile. |
|
|
| Support assessment | Immutable assessment ID, assertion digest, evidence digests, assessor principal/run, method/rubric, supported/unsupported/unavailable, rationale. |
|
|
| Proposal revision | Proposal ID, monotonic revision, initiating project, payload digest, base commit, touched node revisions, affected policy domains, operations, evidence/assessment references, authenticated proposer and run. |
|
|
| Validation result | Proposal revision/digest, validator version, evaluated policy versions, dependency revisions, check results and actionable findings. |
|
|
| Owner decision | Decision/challenge IDs, proposal/revision/digest, human principal and owner-session ID, affected domains, policy and grant versions, decision, authorization/expiry times, decision digest. No secrets. Immutable decision snapshot; lifecycle tracked separately. |
|
|
| Acceptance manifest | Proposal/revision/digest, before-revision, changed paths/content hashes, accepted node revisions, actor/agent/key IDs, decision snapshot/ID/digest when required, policy/validator versions, decision time. |
|
|
| Receipt | Immutable proposal/revision/digest, outcome, commit ID when accepted, node revisions, acceptance-manifest path/hash and findings. Current index revision/status appears in the response envelope, separately from this immutable receipt. |
|
|
|
|
Proposer identity is derived from authentication. A self-reported model or harness label is useful provenance but grants no capability. Assessment authorship is likewise authenticated; the agent cannot submit a record pretending to be a human reviewer.
|
|
|
|
## Public transport contract
|
|
|
|
One versioned operation catalog owns request/response schemas and capabilities. Each content request includes `api_version`, `request_id`, explicit `scope`, and operation-specific fields; reads add `budget_tokens`, bounded `limit` and optional opaque `cursor` where applicable. A cursor binds the normalized query, scope and index revision; a mismatched or retired snapshot returns `CURSOR_EXPIRED`, never silently mixes pages.
|
|
|
|
Search keeps its backward-compatible full-citation default. Callers can set `include_evidence=false` to receive compact claim summaries with per-claim evidence counts; they must read the canonical node before citing or inspecting source passages. `budget_tokens` currently counts canonical-JSON Unicode codepoints rather than model tokens.
|
|
|
|
Successful reads return `api_version`, `request_id`, `resolved_scope`, `data`, complete `citations`, `warnings`, `omissions`, `canonical_revision`, `index_revision`, `retrieval_mode`, `budget_profile`, `tokens_used` and `next_cursor` where applicable. Fields irrelevant to a direct canonical read are explicitly null with a reason. Error envelopes use `code`, safe `message`, `findings` and `retryable` plus request identity. HTTP status and MCP error representation map to the same domain result. Query `data` contains only accepted records; draft status belongs to proposal operations.
|
|
|
|
| Surface | Authentication | Allowed capabilities |
|
|
| --- | --- | --- |
|
|
| Read HTTP API | Read-only browser session or agent bearer key | Scoped search, node/source/neighbor/history reads; agent responses have the same budgets and citations as MCP. No proposals or owner decisions. |
|
|
| Agent MCP | Provisioned agent key through the controlled connector profile | Permitted reads, upload/staging/run operations and typed knowledge proposals; no human owner decision. |
|
|
| Owner MCP | Dedicated short-lived human owner session | Read the challenge/diff and authorize/reject that bound protected proposal within current owner grants. No unconditional merge. |
|
|
| Authentication/control plane | Human login or restricted operator channel | Session establishment and credential/grant management only; no knowledge-authoring bypass. |
|
|
|
|
Named initial operations are `search`, `get_node`, `neighbors`, `list_projects`, `source_upload_begin/chunk/finalize`, `proposal_submit/get/revise/withdraw/evaluate`, `run_get/checkpoint` and `flag_stale` (a proposal). Exact schema files and HTTP path mappings must be generated and tested at M0 before adapters are implemented. The content/authority semantics above are fixed design recommendations, not left for each adapter to invent.
|
|
|
|
## Scope and policy domains
|
|
|
|
Reads resolve a single project's `default_includes` plus shared when `include_projects` is omitted. An explicit (possibly empty) `include_projects` list replaces those defaults. Multiple explicit project contexts and an `all` request do not inherit per-project defaults; all-project access remains explicit. Every project in the expanded scope and shared library must be authorized. Direct IDs, redirects, source citations, graph neighbors and suggestions honor the resolved scope. When a target is outside scope, return an omission reason and a way to request wider scope. Never silently follow a link into another project.
|
|
|
|
## Project registry lifecycle
|
|
|
|
Project IDs are immutable lowercase hyphen-separated slugs. The canonical project entry is `projects/<project_id>/project.md` with a strict YAML frontmatter `ProjectRecord`; its initial `AutoAcceptPolicy` is published in the same Git commit under `policies/<sha256(policy_id)>.yaml`. Project creation requires a named owner, a matching policy ID/project ID, supported-assessment enforcement, and default includes that resolve to existing projects or shared. A duplicate project or policy ID conflicts; project IDs are never reused.
|
|
|
|
Only an authenticated non-agent administrator can list full project records, create a project, or archive one. These HTTP control-plane operations are separate from agent/MCP knowledge contribution. Browser-session writes require same-origin and CSRF checks. The project-record and policy publication uses the canonical writer lock and expected-head commit so proposal validation cannot race an archive. Archive is idempotent, writes a new canonical revision, retains all previous records and citations, and cannot be reversed through this API. `active` projects accept eligible proposals; `paused` and `archived` projects remain readable but reject new accepted changes. The reserved shared scope cannot be created or archived as a project. Admin authority does not substitute for the protected owner challenge required for owner-only intent, policy-exception, merge, delete or promotion decisions.
|
|
|
|
Mutation authority follows every changed record. A project owner does not gain control of shared records or another project's records through a read include. Shared has an explicit owner grant and policy.
|
|
|
|
Proposed bundle rule: all-or-nothing acceptance in one Git commit under every affected policy. A source in shared and a project note may travel together only when both domains authorize their changes. Otherwise retain the entire bundle and return domain-specific findings. The agent may submit a shared-source proposal first, then a dependent project-note proposal; each receives its own receipt. Never report an incomplete bundle as accepted.
|
|
|
|
## Proposal lifecycle
|
|
|
|
| Persisted state | Entered by | Allowed next step |
|
|
| --- | --- | --- |
|
|
| `staged` | New proposal or corrected immutable revision | Evaluate; withdraw. |
|
|
| `validating` | Service records the evaluation attempt and input bindings | Accept, return to staged with findings, hold for review, or reject. A crashed attempt is recovered and revalidated. |
|
|
| `needs-review` | Missing protected decision, policy exception or unresolved ambiguity | Append assessment/decision and revalidate; submit corrected revision; reject or withdraw. |
|
|
| `accepted` | Canonical commit published and receipt reconciled | Terminal for this proposal. Corrections use a new linked proposal. |
|
|
| `rejected` | Authorized explicit rejection with rationale | Terminal. A new linked proposal may address the rejection. |
|
|
| `withdrawn` | Authenticated proposer/authorized owner withdraws before acceptance | Terminal. An accepted proposal cannot be withdrawn. |
|
|
|
|
`validation-failed`, `conflict`, `busy`, `dependency-unavailable` and `index-pending` are outcomes, not competing lifecycle states. Invalid or conflicting proposals return to `staged` with an immutable failed-attempt record. `index-pending` accompanies accepted state and does not request a second acceptance.
|
|
|
|
Correction appends a proposal revision and invalidates previous validation. Changed assertions or evidence invalidate their support assessments. An unchanged payload cannot inherit a validation result under a changed policy, schema or dependency revision. Withdrawal and acceptance serialize through the same writer; whichever completes first determines the permitted next action.
|
|
|
|
## Exact replay and conflict behavior
|
|
|
|
The client supplies `proposal_id`, revision and payload digest. The service recomputes the digest over canonical serialization. Exact retries return the original durable receipt, with current index status in a separate envelope. Reusing the same ID/revision with different content returns `IDEMPOTENCY_CONFLICT`; correction requires a new revision. Retrying an already accepted proposal never creates a second commit.
|
|
|
|
Proposal edits use expected revision checks. In the initial implementation, a changed canonical base commit returns a conflict, even if only an unrelated record changed. This conservative rule avoids implicit rebases. The client can fetch the new base and submit a new proposal revision, reusing only assessments whose assertion/evidence bindings remain identical. Record conflict frequency; move to dependency-level optimistic concurrency later if measurements justify it.
|
|
|
|
Authenticated status reads are allowed after key rotation through the same stable agent identity and current read capability. Revocation blocks further use of the old key; it does not erase its historical attribution.
|
|
|
|
## Publication and crash recovery
|
|
|
|
The acceptance commit itself contains the acceptance manifest at the deterministic path `acceptance/<proposal-storage-key>/<revision>.yaml`. It cannot contain its own Git commit ID. The external receipt resolves that ID from the committed manifest. The manifest path is unique and immutable; its first introducing commit on the authoritative ref's ancestry supplies the accepted commit ID. An orphan commit or another branch cannot establish acceptance.
|
|
|
|
Publication progress is a separate durable journal field, not a second proposal lifecycle: `evaluated`, `publishing` (expected head, manifest path/hash, prepared commit ID), `published-unreconciled` (published commit), then `reconciled` (receipt path/hash). The ref may advance before the journal records `published-unreconciled`; recovery always checks Git rather than trusting a stale journal field.
|
|
|
|
1. Persist the proposal revision and evaluation intent using temporary-file write, flush and atomic replace. Journal events are separate immutable files with a recoverable sequence; an interrupted tail is detectable.
|
|
2. Validate outside the writer's critical section where possible. Prepare safe content-addressed artifacts and bounded file changes. Quarantine uploads cannot become canonical references before integrity checks succeed.
|
|
3. Acquire exclusive writer access. Recheck current authentication/grants, policy versions, base commit, dependency hashes and protected decision bindings. Serialize credential/grant changes with the final authorization check and publication so revocation cannot race that check. Do no parsing, model inference or external network I/O while holding this boundary.
|
|
4. Build the exact Git tree using a private temporary index and argument-vector Git commands. Include only changed canonical records and the acceptance manifest. Generate the commit, then publish the canonical branch with `git update-ref <ref> <new> <expected-old>` while holding the process-lifetime writer lock. Only a successful ref update makes the change accepted; orphan commit objects do not.
|
|
5. Persist the immutable external receipt with the published commit ID; reconcile stage state and any consumed owner decision in the control store. If a required reconciliation write fails, freeze all new canonical publications and return an indeterminate/recovering result until reconciliation succeeds. Do not call the committed mutation rejected.
|
|
6. Schedule derived indexing from the published Git revision. The response envelope reports `index-pending`, `indexed` or `index-failed` alongside the receipt. Direct accepted-revision reads remain available.
|
|
|
|
Canonical readers use a committed tree, never partially modified worktree files. A service-owned checkout may be materialized for inspection after publication. Manual edits to that checkout are unsupported; detect drift and do not import it automatically.
|
|
|
|
| Failure point | Recovery requirement |
|
|
| --- | --- |
|
|
| Before proposal durable write | Return failure; retry can submit again. |
|
|
| Artifact stored, no accepted reference | Keep quarantined/unreferenced until retention cleanup; no accepted knowledge exists. |
|
|
| Prepared tree or orphan commit, ref unchanged | Ignore for acceptance; retry against current head. |
|
|
| Ref updated, receipt absent | Locate the committed manifest, verify digest/authority bindings, recreate receipt and reconcile stage state. Never reapply the bundle. |
|
|
| Receipt exists, index behind | Replay index updates or rebuild; status remains accepted. |
|
|
| Index generation corrupt | Serve explicit unavailable/degraded results or canonical ID reads; rebuild from authoritative records and artifact maps. |
|
|
| Unknown/manual canonical mutation | Hold writes for operator diagnosis. Do not manufacture a receipt or silently reset history. |
|
|
|
|
Startup reconciles manifests and workflow records before accepting new writes. Every proposal retry and status lookup also checks its deterministic manifest path against a captured authoritative ref when the receipt is absent or inconsistent. If present, verify proposal/digest bindings and the introducing commit, reconstruct the identical receipt and consume the bound decision idempotently under the global publication/recovery gate. If required persistence still fails, return `RECOVERY_REQUIRED` and keep new publications frozen. If absent and the writer has no active attempt, resume/revalidate staged work. This applies before restart as well as afterward, including requests from a different principal.
|
|
|
|
Fault injection must kill the process at each boundary, including filesystem write failures and receipt failures. Test live retries, unrelated queued proposals and challenge replay during the recovery window. Process-crash tests do not establish power-loss durability; storage flush behavior and backup recovery require separate acceptance on the intended filesystem. Network/shared filesystems and concurrent writers are outside this baseline.
|
|
|
|
## Human ownership without per-note approval
|
|
|
|
Routine auto-acceptance is controlled by explicit production policy. Proposed initial candidates are additive source captures and factual notes/datapoints with valid locations and supported assessments. Same-agent assessment is permitted for routine factual notes, as confirmed in the PRD. This candidate list is awaiting owner selection; a confidence threshold alone never authorizes acceptance.
|
|
|
|
Policy separately enumerates shared-source metadata, entities, typed links, interpretations and syntheses. Missing rules hold affected changes. Destructive merges, promotion, protected project intent and autonomy policy need attributable exceptional authority. Link correctness must not be inferred merely from both endpoint nodes existing.
|
|
|
|
Proposed protected-action flow:
|
|
|
|
1. An agent stages a proposal and receives a public challenge ID and exact digest. The challenge is not a bearer approval.
|
|
2. The human launches the owner client outside the agent's execution account. It obtains the dedicated session defined below, retrieves the diff directly from MCP, and displays affected projects, policy effects, hashes and expiration.
|
|
3. The human confirms there. The owner client submits a decision through MCP; the service checks project/shared ownership and records a single-use authorization bound to proposal revision, digest, policy versions and expiry.
|
|
4. The service revalidates and accepts when every affected domain permits it. Changed content, changed authority/policy, expiration or withdrawal invalidates the decision. The agent receives status only and never receives owner credentials.
|
|
|
|
An admin bootstraps accounts, key issuance and owner grants through a separate operator control plane. This authority alone cannot bypass a protected knowledge decision. Owner decisions and their audit references survive index deletion. The local `gewu-owner` command provisions an owner interactively, writes only an Argon2id password hash and encrypted TOTP seed, records a security event, and prints the enrollment URI once to a TTY. It is a local operator primitive; no owner accounts or grants are provisioned in this workspace.
|
|
|
|
Isolation is a deployment fact. If the agent can execute as the owner and read the owner's secrets, this mechanism cannot prove human-only authorization. Use a separate OS principal/device or a protected interactive authenticator before claiming that gate passed. Do not invent human approval from conversational text supplied by the agent.
|
|
|
|
### Proposed owner authentication profile
|
|
|
|
Use the controlled GEWU owner CLI over HTTPS to the same service's Streamable HTTP MCP endpoint. Before MCP initialization, the CLI calls a separate `POST /auth/owner-session` control endpoint with human account ID, password, TOTP and challenge ID. Password/TOTP are read directly from the human terminal without echo, never arguments/environment variables, and discarded after use. The server verifies an Argon2id password hash and TOTP with `argon2-cffi` and `pyotp`, limits failed attempts, prevents reuse of an accepted TOTP timestep, and binds login to an unexpired issued challenge. Enrollment records a security event; reset and recovery workflows remain unimplemented. TOTP seeds are encrypted with Fernet under `GEWU_OWNER_ENCRYPTION_KEY`, kept outside the database and ordinary data-backup set. Maintain that key in a separately protected, encrypted, access-controlled recovery backup. A restore verifies the key before enabling owner operations; if unavailable, owner login stays disabled until an attributable operator reset and human re-enrollment complete. Test both recovery paths before team release.
|
|
|
|
On success, generate a random opaque token, store its hash in the private control database, and return it once with a five-minute expiry. Store the human principal, current grant version, service audience and allowed challenge ID. The token is rejected by ordinary browser sessions and agent-key validation paths. The dedicated `/owner-mcp` surface exposes challenge metadata, the complete digest-checked diff, and an immutable authorize/reject operation. Reauthentication is required after expiry. Network operations verify TLS and the configured service identity. This custom profile is explicitly limited to GEWU's controlled clients and makes no general OAuth-interoperability claim.
|
|
|
|
MCP operations: `owner_challenge_get(challenge_id)` returns proposal revision/digest, affected domains, expiry and status; `owner_diff_get(challenge_id)` returns the complete bounded diff and its digest; `owner_decide(challenge_id, expected_proposal_revision, expected_payload_digest, expected_diff_digest, decision, rationale)` records an attributable authorize/reject decision after rechecking current grants. The current implementation caps a challenge diff at 512 KiB and refuses to create or return a larger diff; it never silently truncates one. Pagination remains future work if real pilot diffs exceed this bound.
|
|
|
|
Owner challenge bindings are persisted with the proposal revision/digest, diff digest, affected domains, grant version and expiry. Challenge issue retries are idempotent and changed content conflicts; expired, out-of-grant and decided challenges cannot create new owner sessions. Policy-only proposals generate a challenge per named owner and affected domain set. Acceptance revalidation checks that authorized challenges still match the current proposal, diff, policy versions and owner grants, and that their combined domains cover the bundle. The canonical manifest records immutable decision references; an accepted proposal's manifest and receipt prevent decision replay after publication. Explicit `consumed`/`invalidated` lifecycle rows, crash-injection across control-store reconciliation, reset/recovery and OS-level owner isolation remain open.
|
|
|
|
Contract tests must cover owner session used as an agent token, browser/agent token used for owner operations, expired/wrong-audience token, revoked grant, TOTP reuse, altered diff, duplicate decision and crash-before-consumed-state. The profile remains proposed until implemented and reviewed on the actual client/service deployment.
|
|
|
|
Library basis checked 13 October 2026: [argon2-cffi password verification](https://argon2-cffi.readthedocs.io/en/stable/howto.html) and [PyOTP](https://pyauth.github.io/pyotp/). PyOTP documents replay rejection, secret protection and throttling as application responsibilities. Neither library establishes human/agent OS isolation or makes this profile phishing-resistant; a hardware-backed authenticator is a possible later replacement for the owner login adapter.
|
|
|
|
## Source transfer and evidence locations
|
|
|
|
The stdio connector reads only an explicitly selected path within configured roots. Reject devices, directories and reparse/symlink escapes; revalidate the opened handle/file identity to avoid path races. It transfers bytes to service MCP upload-session/chunk/finalize operations. Never echo source bytes or credentials into model-visible upload responses.
|
|
|
|
Proposed initial limits for probing: 50 MiB per artifact, 512 KiB binary chunks, two concurrent uploads per credential, ten-minute idle upload expiry, 100 operations per proposal and 1 MiB proposal JSON. The JSON wire limit accounts for base64 expansion. Advertise effective limits in diagnostics; tune only from measured fixtures and operational limits. These values do not constitute owner-approved archive capacity.
|
|
|
|
Chunk retries bind session/offset/content hash. A duplicate matching chunk succeeds idempotently; changed content at the same offset fails. Finalization verifies full length and SHA-256 before creating an immutable artifact. Upload completion is staging, not source acceptance. Resume reports verified offsets; expired sessions fail explicitly.
|
|
|
|
Locators use zero-based Unicode code-point offsets and half-open spans into a specified immutable extraction block. PDF page numbers are one-based. Text/HTML locators name a block ID; timestamped transcripts additionally name the time interval. A quote must match at that locator after the declared normalization profile. Normalization may standardize line endings and Unicode form, but cannot silently rewrite numbers, units or qualifiers. Record the normalized text alongside the original map.
|
|
|
|
The server derives or verifies extraction maps from captured bytes with its pinned parser. Agent-supplied extracted text cannot substitute for an archived PDF's location map. Encrypted, scanned, multi-column or mathematical sources may require a specialized parser/OCR adapter; return an explicit unsupported/unavailable extraction state until that path is verified. Preserve originals and every cited extraction version when parsers change.
|
|
|
|
### Immutable extraction format and rebuild procedure
|
|
|
|
Store one canonical UTF-8 JSON extraction artifact with `schema_version`, source artifact hash, parser name/version/config digest, normalization profile, extraction status/warnings, and ordered `blocks`. Each block has `block_id`, ordinal, original extracted text, normalized text, page number or time range, and a normalized-to-original code-point range map. This map relates text representations; it does not claim byte offsets into compressed PDF bytes. PDF page/block association is mandatory; geometry is optional and explicitly absent when unavailable.
|
|
|
|
Within one extraction artifact, derive `block_id` from the block ordinal and hash of original extracted text. IDs are stable only within that immutable extraction version. Hash canonical serialization of the complete artifact excluding its self-ID/hash field to obtain `extraction_id`; store under that hash. Record hashing/serialization version and test vectors. A citation resolves `(source_revision, extraction_id, block_id, start, end)` against these stored bytes. Normalization maps must resolve every quoted normalized span to original-text ranges. Parser upgrades create new artifacts and never reinterpret old locators.
|
|
|
|
A full rebuild captures a canonical Git head `H`, enumerates only its accepted record manifests and referenced source/extraction IDs, validates schemas and hashes, and builds an isolated index generation. Read exact stored extraction artifacts; never rerun a parser to resolve an existing citation. Record `H`, index schema, extraction profiles, embedding artifact/profile and corpus inventory hash as the generation watermark. Missing/corrupt authoritative artifacts fail the rebuild visibly rather than dropping affected nodes. After integrity/retrieval checks, publish the generation pointer under the service's index-publication gate, then apply complete commits after `H` in order. A newly published generation may report lag, but must never claim a later head before those commits are indexed. Crash recovery uses the generation watermark and replays unapplied commits; incomplete generations are never selected.
|
|
|
|
Reference-only sources remain discoverable with availability warnings. Their unsupported assertions stay outside the accepted evidence-backed corpus. A quote-location pass establishes occurrence; semantic support, source reliability, confidence and freshness remain separate fields.
|
|
|
|
## Contract errors and acceptance fixtures
|
|
|
|
Errors carry a stable code, safe message, request/proposal identity, field findings and retry guidance. Initial codes include `SCOPE_REQUIRED`, `OUT_OF_SCOPE`, `UNAUTHORIZED`, `OWNER_DECISION_REQUIRED`, `POLICY_MISSING`, `EVIDENCE_MISMATCH`, `SUPPORT_UNAVAILABLE`, `BASE_CONFLICT`, `IDEMPOTENCY_CONFLICT`, `BUDGET_TOO_SMALL`, `UPLOAD_EXPIRED`, `BUSY` and `RECOVERY_REQUIRED`. Internal paths, raw SQL and secrets remain in neither client errors nor routine logs.
|
|
|
|
M0 must implement adversarial fixtures for wrong-location matching quotes; wrong numeric units with valid quotes; changed assertions with old assessments; cross-policy bundles; stale owner approval; revoked key at publication; changed payload replay; scope escape through redirects; crash-after-ref-before-receipt; index lag; and whole-group token budgets. These fixtures are required work, not tests claimed to exist.
|