First implementation slice: integration contract
Status: historical integration contract for the first implementation slice. See implementation status for the complete current service.
This file coordinated parallel implementation, not a change to product scope. Root owns workspace manifests/core types, server coordination.rs, migration 0002, mutation.rs, integration tests, and final integration. Authentication work owns server lib.rs/main.rs/state.rs/error.rs/auth.rs, migration 0001, auth tests and systemd/proxy examples. CLI work owns client/cli crates. UI owns web assets.
Shared Rust interface
state::AppState: Clone, publicpool: sqlx::SqlitePool,config: Config,clock: Arc<dyn Clock>;now() -> i64milliseconds;open(Config) -> anyhow::Result<Self>. Clock: Send + Sync +fn now_ms(&self) -> i64.- Config fields:
database_path: PathBuf,listen: SocketAddr,public_origin: String,allow_insecure_loopback: bool. Default creates only loopback bindings; development HTTP requires explicit opt-in. auth::Auth: axumFromRequestParts<AppState>, publicactor: Actor;verify(&self, &mut SqliteConnection, now: i64) -> Result<Actor, AppError>revalidates credential/session revocation inside mutation transaction.- Actor: Clone, public string
id,name,kind,role, optional stringcredential_id,session_id. kind human/agent; role admin/operator/agent. Valid supplied agent session headers populate session_id only with correct proof and matching active issuing credential. Humans have browser session ID there. Agent without session can read but not own an attempt. error::AppError:new(status: StatusCode, code: &str, message: &str),bad_request(&str),conflict(code:&str,message:&str),forbidden(&str),not_found(),auth_required(),with_details(Value);From<sqlx::Error>,serde_json::Error;IntoResponseJSON envelope. No secret Debug.response(data: Value) -> axum::Json<Value>produces{data,request_id,server_time}. UTC dates RFC3339. Root owns mutation.rs:Mutation::begin(&AppState,&Auth,&HeaderMap,operation:&str,&impl Serialize); fieldstx: Transaction<'static, Sqlite>,actor: Actor,now: i64, andreplay: Option<Value>;finish(...) -> Result<Value, AppError>returns data after commit. All writes use BEGIN IMMEDIATE, Auth::verify after lock, mandatory idempotency key, receipt and event in same tx. Replays still authenticate. Issued token receipt must be redacted; first response can attach secret after commit.- lib router
pub fn router(state: AppState)->axum::Routermerges coordination::routes(), mounts public/assets/auth routes. Auth private extractors must run before decoding private bodies/project IDs; any global middleware needed for unknown private routes belongs in lib/auth.
Database access foundation (migration 0001)
principals(id TEXT PK,name TEXT UNIQUE,kind TEXT,role TEXT,password_hash TEXT NULL,disabled_at INTEGER NULL,created_at INTEGER);
credentials(id TEXT PK,principal_id TEXT FK,token_hash TEXT UNIQUE, created_at INTEGER,revoked_at INTEGER NULL,expires_at INTEGER NULL);
browser_sessions(id TEXT PK,principal_id TEXT FK,token_hash TEXT UNIQUE, expires_at INTEGER,revoked_at INTEGER NULL);
agent_sessions(id TEXT PK,principal_id TEXT FK,credential_id TEXT FK, workstation_id TEXT,proof_hash TEXT,closed_at INTEGER NULL,created_at INTEGER, capabilities TEXT JSON,harness TEXT);
events(seq INTEGER PK AUTOINCREMENT,project_id TEXT NULL,actor_id TEXT, kind TEXT,record_id TEXT,data_json TEXT,created_at INTEGER);
mutation_receipts(principal_id TEXT,operation TEXT,key TEXT,fingerprint TEXT, result_json TEXT,created_at INTEGER,PRIMARY KEY(principal_id,operation,key)).
Additional checks/indexes allowed. Secret hashing: SHA256 hex via shared
auth::digest(&str)->String; random token auth::secret()->String.
No agent API can mint full replacement tokens. Password hashing runs off async
threads. Embedded SQLx migrations include 0001 and root’s later 0002.
Routes and payloads (all success data is inside envelope.data)
Public GET /healthz, /api/v1/info, /api/v1/help/authentication.
GET /, /app.js, /style.css assets embedded by server.
POST /api/v1/auth/login {username,password} -> {actor,csrf_token} + HttpOnly
same-site cookie. GET /api/v1/me same response for browser restore. POST
/api/v1/auth/logout {}. Cookie writes need X-CSRF-Token and expected Origin.
Admin: GET /api/v1/admin/credentials -> {items:[{id,name,revoked_at,class,access,...}]};
POST /api/v1/admin/agents {name,class?,access?} -> {principal_id,credential_id,token,class,access};
POST /api/v1/admin/credentials/{id}/revoke {};
POST /api/v1/admin/credentials/{id}/delete {} hides a revoked credential from the list
(409 credential_not_revoked otherwise; the row is kept for history).
Agent session: POST /api/v1/sessions {session_id,workstation_id,harness, capabilities:[]} with X-Coordinator-Session-Proof (no session header yet);
GET /api/v1/sessions/{id}; POST /api/v1/sessions/{id}/close {}.
Existing session requests send X-Coordinator-Session and
X-Coordinator-Session-Proof in addition to Authorization Bearer token.
Root implements:
GET/POST /api/v1/projects (create {name,repository_url,target_branch})
-> project {id,name,repository_url,target_branch,policy_revision:1, review_mode:"agent",lease_seconds:600,recovery_mode:"agent"};
GET/POST /api/v1/projects/{id}/tasks (create {title,description, acceptance_criteria:[string],kind:"code"|"general",priority:0..3, depends_on:[task_id],planned:bool}; default normal=2)
-> task {id,project_id,title,description,acceptance_criteria,kind,priority, lifecycle,revision,work_status,current_attempt_id,created_at};
GET /api/v1/projects/{id}/tasks/{task_id} -> task + attempts/checkpoints;
GET /api/v1/projects/{id}/orientation -> current project, instruction_version
“1”,policy_revision,required_sections:[“coordination-v1”], instructions text,
candidates,active_attempts, instructions_complete:true;
POST /api/v1/sessions/{id}/instruction-acknowledgments
{project_id,policy_revision:1,instruction_version:"1",sections:["coordination-v1"]};
POST /api/v1/projects/{id}/claims {task_id?,expected_task_revision?,mode: "work"|"recovery",policy_revision:1,instruction_version:"1"}; absent task_id
means next eligible; -> {claim: {task,attempt,lease_remaining_ms},...} or
{claim:null,reasons:[...]}. attempt includes id,task_id,owner_id,session_id,
generation,state,expires_at (RFC3339),last_heartbeat_at;
POST /api/v1/projects/{id}/attempts/{aid}/renew {generation};
POST …/checkpoints {generation,summary,current_action,next_step,blockers:[]};
POST …/release {generation,summary,blocked:bool}. Checkpoints/release don’t
imply completion. Further workflow endpoints remain visibly unavailable in this
first slice; never provide an unrestricted status=done operation.
GET /api/v1/projects/{id}/events -> {items:[...]}.
Additional implemented routes:
- PATCH
/api/v1/projects/{id}/policy: full replacement of editable policy fields withexpected_revision,review_mode,recovery_mode,lease_seconds,rules,agent_rule_editing, andautomatic_integration. Delegated agents cannot change the last two permission grants. The review/integration execution workflows remain unavailable in this foundation. - PATCH
/api/v1/projects/{id}/tasks/{task_id}:expected_revision, title, description, acceptance_criteria, priority, depends_on, and planned. Only an unowned open/planned task can be edited. There is no generic status edit. - POST
.../tasks/{task_id}/unblock: human-only{expected_revision,reason}. - POST
.../tasks/{task_id}/complete-external: human-only completion of a task delivered outside the coordinator; see the completion contract. - POST
.../tasks/{task_id}/{cancel,archive,restore}and DELETE.../tasks/{task_id}, each with{expected_revision,reason}(cancelalso takes an optionalreplacement_task_id; a cancel also soft-deletes the task, or archives it when it has history or a replacement, and reportsdeletedorarchived). Humans may use all four. An agent session may use all four on a task whose storedoriginisagentorservice(for example e2e canary tasks), without project delegation; the caller cannot supply or change the origin. On a task whose origin ishumanan agent may onlycancel, and only when the project setsagent_rule_editing; otherwise archive, restore, delete and that cancel are thetask_<action>human gate. A revert task follows the same rule by its own origin (its creator’s, orservicefor an automatic revert), except that an agent that is not independent of the reverted change (contributor or shared launch, as for a reviewer of it) is refused cancel and archive withrevert_author_conflict. The same guards apply to every actor:task_revision_changed,task_attempt_protected,task_workflow_protected(active review/integration work and the review/integration activity tasks themselves),task_archived(restore first),task_lifecycle_invalid, andtask_history_protectedfor a delete of a task with attempts, dependencies, objective links or workflow links (archive it instead). The lifecycle event and the task’s revision bump are attributed to the acting principal. The existing endpoints express this directly, so none was added. Thetask.canceled,task.archived,task.restoredandtask.deletedevents store{reason, actor_id}(andreplacement_task_idfor cancel) in their data, whichtasks history --kind eventsreturns. The task view carrieslifecycle_events(newest first, up to 20:seq,action,actor_id,actor_name,reason,replacement_task_id,created_at) andreplaces(tasks canceled with this one asreplacement_task_id:task_id,title,actor_id,actor_name,reason,canceled_at). Migration 36 copied the reason and replacement into events recorded earlier only where their original mutation receipt had not been compacted; other earlier events keep empty data and a nullreason. A canceled workflow activity records why: the activity view carriescancel_reason(code,detail,contextwithsubmission_base_revision,candidate_revision,target_branch,target_revisionand anyevidence, andcanceled_at; null for an activity canceled before migration 40 or one not canceled). Codes:target_advancedandmerge_conflict(an integratorconflictrevise with and withoutmoved_by_result_id),check_failed,manual(a reopen without a code), other revise codes as given,revert_not_mechanical,released,publication_reconciled,changes_requestedandreview_not_needed. Canceling an integration activity also records aworkflow.activity_canceledevent on the subject task, shown in itslifecycle_eventsas actionactivity_canceledwithreason_code,reason(the detail),contextandactivity_id; a subject inrevision_neededadds the reason to its workflownext_actions. - GET
.../attempts/{aid}: attempt, task, currentauthority_valid, currentlease_remaining_ms, and optional registered checkout metadata. - POST
.../attempts/{aid}/checkout:{generation,workstation_id,identity,path, branch,base_revision,clean}. Registers a client-reported separate clean checkout; the service does not execute Git or independently inspect the workstation. - POST
.../attempts/{aid}/recovery-resolution:{generation,disposition: "resume"|"restart",summary,saved_work_checked:true,running_jobs_checked:true}. Requires a recovery-mode attempt; only after this inspection may it resume normal work or release back to the queue. Incomplete inspections release blocked. When the expired attempt’s latest checkpoint recorded arevision, sendfetched_revisioninstead of the two attestations. The service cannot read Git: it only checks thatfetched_revisionequals the recorded SHA (plus the expired predecessor, the generation bump and no live jobs), and then answersevidence: "service_verified". The resolution’s own checkpoint records no revision, so a second recovery falls back to the attestations.
On a claim replay, current_authority reports whether the historical grant still
holds. The original claim is a receipt, not a fresh countdown. Renewal replay
returns current remaining time and requires a still-valid owned attempt. Checkpoint,
checkout, and recovery-resolution replays also require current ownership. Release
is the intentional exception: its historical receipt remains replayable after
release, because that operation ends ownership. Every replay reauthenticates.
All of these observations can become stale; ownership-dependent mutations still
enforce the current authority transactionally.
All lists {items:[],next_cursor:null|opaque}. Errors use stable error envelope.
The CLI must persist idempotency keys/requests before mutations, never retry an
uncertain mutation under a new key. New automatic claims are never connect’s
side effect. Missing credentials can call public help. All routes not listed as
implemented are future milestones, not advertised working features.