feat: implement GEWU service and local agent workflows
CI / quality (push) Canceled after 0s

This commit is contained in:
vasily committed 2026-10-14 19:21:35 +08:00
1 parent 4db3f45164
commit d25c04cdd2
175 files changed
+56243 -15

No files matched your search

+13 -7
View File
@@ -1,6 +1,6 @@
# Acceptance, authority and evidence contract
Status: proposed v0.2 after [independent review](../reviews/ARCHITECTURE_SENIOR_REVIEW.md). Drafted 12 October, revised 13 October 2026. Implements the boundaries in the [architecture](ARCHITECTURE.md); schemas and executable tests are still required at M0.
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
@@ -39,7 +39,13 @@ Named initial operations are `search`, `get_node`, `neighbors`, `list_projects`,
## Scope and policy domains
Reads resolve requested projects plus their declared includes and shared. An `all` request is explicit. 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.
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.
@@ -110,19 +116,19 @@ Proposed protected-action flow:
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 account/bootstrap commands are proposed tooling, not currently runnable commands.
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 a maintained Argon2id password hash and TOTP with a pinned library (proposed `argon2-cffi` and `pyotp`), rate limits failures and prevents reuse of an accepted TOTP timestep. Enrollment/reset requires restricted operator identity and records a security event. TOTP seeds are encrypted under a service 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.
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 256-bit 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, allowed challenge ID and capabilities `owner:inspect`/`owner:decide`. The CLI keeps the token in memory and presents it as the MCP bearer credential; it is rejected by ordinary browser sessions and agent-key validation paths. The token cannot create arbitrary proposals or authorize another challenge. 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.
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/policy versions, expiry and status; `owner_diff_get(challenge_id)` returns the exact bounded diff and its digest; `owner_decide(challenge_id, expected_proposal_revision, expected_payload_digest, expected_diff_digest, decision, rationale)` accepts an attributable authorize/reject decision after rechecking current grants. Oversized diffs must be fully paginated and hash-bound before confirmation; they cannot be silently truncated.
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.
Challenges live in the control database with states `issued`, `authorized`, `consumed`, `expired` or `invalidated`, plus immutable transition events. Only an unexpired `issued` challenge can acquire an immutable owner decision. Exact decision retries return the same decision; altered retries fail. Multiple owners can supply separate decisions for a bundle, each bound to its domains. An authorization is consumed by one acceptance manifest containing the full non-secret decision snapshot and digest. The global writer/recovery gate ensures the manifest is checked before any decision can be reused. A crash before the SQLite consumed flag is updated is recovered from that manifest; a missing flag cannot permit another acceptance. Rejection invalidates the challenge and records its decision without publishing the proposed knowledge.
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.