Align resume failure and contract validation with guard transitions
2 files changed,
+57
−18
implementation/maintenance-protocol/proposal.json+11 −8implementation/maintenance-protocol/schemas.json+46 −10
CI passed — run details
diff --git a/implementation/maintenance-protocol/proposal.json b/implementation/maintenance-protocol/proposal.json| index 8925b9e79..7989abafa 100644 |
| --- a/implementation/maintenance-protocol/proposal.json |
| +++ b/implementation/maintenance-protocol/proposal.json |
| @@ -191,9 +191,10 @@ |
| "approval_forms": "application/x-www-form-urlencoded; exact fields in approval_form, one value each; reject unknown/duplicate fields. csrf uses existing console field and is never copied into durable approval metadata. Browser Origin/CSRF checks must match normal console/router contract; any stronger Origin rule needs explicit integration tests.", |
| "metadata_digest": "New proposal: deterministic Go encoding/json serialization of the declared version-1 typed object in schemas.json property order, no indentation or appended newline, normal Go HTML escaping; sorted unique required-contract arrays and sorted unique environment entry names. Compare exact stored bytes for reference digests; no float64 intermediates.", |
| "approval_endpoint": "The single review/approve path uses the NEW request_id for operation, retry, or resume. Server immutable request kind determines the required operation or compatibility/resume checks. Predecessor is metadata, never substituted into the approval path. Missing/mismatched kind/pins refuses.", |
| - "browser_scope": "Use existing accountApp sqid routing and resolve sqid to exact account before comparing stored scope; sqid is not an account slug. Approval redirects 303 to the same new request review URL /a/{sqid}/apps/{app}/instances/{host}/maintenance/{request_id}/review. Owner and fresh-auth gate order is unchanged.", |
| + "browser_scope": "Use existing accountApp sqid routing and resolve sqid to exact account before comparing stored scope; sqid is not an account slug. Approval redirects 303 to the same new request review URL /a/{sqid}/apps/{app}/instances/{host}/maintenance/{request_id}/review. Owner and fresh-auth gate order is unchanged. Existing resolver accepts an unambiguous account name as well as sqid; emitted URLs use canonical sqid. accountApp alone is member-readable and insufficient: maintenance handlers must perform the explicit owner gate with identical missing/nonowner HTTP404 before fresh-auth, including missing-app response normalization.", |
| "predecessor_agreement": "Initial POST /requests requires predecessor=null. Retry and resume require non-null predecessor; predecessor.id equals path request_id and its digest equals that exact predecessor request reference (not an outcome ID). Server resolves that request to its immutable authoritative outcome and current lineage head; every body selector must agree. Missing outcome, stale predecessor or mismatch refuses. New body request_id is distinct from predecessor/path request_id.", |
| - "result_semantics": "GET /result resolves status.outcome from authoritative current request metadata. Pending resume, including compatibility-checked/resume-ready, returns409 result-unavailable; it never returns the intermediate checker receipt as resumed. Before a final outcome status.result_state=pending and result_envelope_digest=null; historical compatibility/evidence refs may be non-null. For final resume outcome, return that final outcome/ref and exact retained checker envelope whose digest is copied unchanged from compatibility_outcome; server verifies both immutable receipts and linkage under the same request/attempt/scope/recipient. No checker output yet or lost output yields409 unavailable; expired private payload yields410. Final failed/ambiguous resume may expose an existing validated checker result but state remains failed/ambiguous and CLI must not report resumed. Operation result semantics remain exact command ciphertext under its final outcome. Result envelope alone never establishes outcome or admission." |
| + "result_semantics": "GET /result resolves status.outcome from authoritative current request metadata. Pending resume, including compatibility-checked/resume-ready, returns409 result-unavailable; it never returns the intermediate checker receipt as resumed. Before a final outcome status.result_state=pending and result_envelope_digest=null; historical compatibility/evidence refs may be non-null. For final resume outcome, return that final outcome/ref and exact retained checker envelope whose digest is copied unchanged from compatibility_outcome; server verifies both immutable receipts and linkage under the same request/attempt/scope/recipient. No checker output yet or lost output yields409 unavailable; expired private payload yields410. Final failed/ambiguous resume may expose an existing validated checker result but state remains failed/ambiguous and CLI must not report resumed. Operation result semantics remain exact command ciphertext under its final outcome. Result envelope alone never establishes outcome or admission. After final outcome status.result_state=available with non-null digest while payload exists; expiry sets expired and preserves immutable digest/ref, loss sets unavailable without inventing payload. Schema does not force available forever for resumed because private ciphertext expires after24h; resumed admission history persists.", |
| + "required_contracts": "Resume requires1..16 sorted unique names, each matching maintenanceguard.ValidContract lowercase grammar; reject unsorted input without silently normalizing signed request bytes. Require a superset of retained guard contracts before preparing/approving or starting a process. Empty/invalid/duplicate/unsorted/floor-removing sets refuse before any spawn." |
| }, |
| "cli": [ |
| { |
| @@ -262,7 +263,7 @@ |
| "execution_disconnect": "HTTP client disconnect does not cancel durable worker ownership. Worker timeout/lease loss invokes checked stop and publishes failed/ambiguous when possible; never automatically retries.", |
| "retry": "New platform request linked to immutable predecessor receipt. Same exact app stdin/argv required: client validates retained private hash; worker independently compares decrypted predecessor input with new input while private predecessor payload exists. Expired/missing predecessor payload refuses retry; operator may prepare a separately reviewed recovery request preserving Home idempotency, never rewrite old authority. No fresh Home signature is generated by carlos.", |
| "concurrency": "One exact active request per retained lifecycle ownership. Concurrent retries may prepare but only approved matching claim can execute; no last-writer-wins head. Existing incomplete lifecycle claim must be recovered explicitly rather than stolen.", |
| - "resume": "Original operation may have succeeded, failed, or become ambiguous; resume eligibility requires checked stop/containment and a NEW independently successful app-specific non-serving compatibility-check command from the pinned serving release, under the retained guard/lifecycle/lease. Private checker argv/stdin use unchanged CMI1+DirectionInput; stdout uses CaptureResult+SealResult to the newly durable per-request CLI key. Approval pins exact checker ciphertext, artifact/manifest, environment snapshot, record identity/revision, predecessor and required contracts. Validate checker exit plus app-specific compatibility result, then integrity and final checkpoint/restore evidence. Publish immutable resume-ready evidence while guard remains in the existing ResumeRequested phase; do not invent a new guard enum. Only then start exact serving artifact and hand lease to replication, verify health/version, and CAS matching resumed guard+projection. Failure keeps serving admission closed. Migration success is not compatibility proof; checker cannot rerun the original mutation.", |
| + "resume": "Original operation may have succeeded, failed, or become ambiguous; resume eligibility requires checked stop/containment and a NEW independently successful app-specific non-serving compatibility-check command from the pinned serving release, under the retained guard/lifecycle/lease. Private checker argv/stdin use unchanged CMI1+DirectionInput; stdout uses CaptureResult+SealResult to the newly durable per-request CLI key. Approval pins exact checker ciphertext, artifact/manifest, environment snapshot, record identity/revision, predecessor and required contracts. Validate checker exit plus app-specific compatibility result, then integrity and final checkpoint/restore evidence. Publish immutable resume-ready evidence while guard remains in the existing ResumeRequested phase; do not invent a new guard enum. Only then start exact serving artifact and hand lease to replication, verify health/version, and CAS matching resumed guard+projection. Failure keeps serving admission closed. Migration success is not compatibility proof; checker cannot rerun the original mutation. On final checker/handoff failure use existing ResumeRequested -> Failed/Ambiguous transition with same request/attempt/pins; only a new explicitly approved attempt may then resume from that phase.", |
| "expiry": "Server immutable payload expiry24h from first accepted upload; idempotent resend never extends it. Proposed preflight review expiry5min; expiry requires fresh preflight/new candidate and explicit review, never silently renew old consent. Execution must begin before candidate expiry; child and operation deadlines independently bounded." |
| }, |
| "Home_integration": { |
| @@ -316,7 +317,9 @@ |
| "Browser sqid resolves through accountApp; mismatched stored account refuses before fresh-auth and 303 returns exact review route.", |
| "Changing lifecycle OperationID, expected head revision or predecessor outcome invalidates candidate_digest and held-POST approval; no latest-head substitution.", |
| "Retry null predecessor and retry/resume predecessor differing from path reject before draft creation; predecessor digest means request reference, not outcome reference.", |
| - "Resume-ready has no final outcome and GET result returns409; final resumed outcome links immutable checker outcome/evidence and returns unchanged checker envelope. Failed handoff never reports resumed or clears guard." |
| + "Resume-ready has no final outcome and GET result returns409; final resumed outcome links immutable checker outcome/evidence and returns unchanged checker envelope. Failed handoff never reports resumed or clears guard.", |
| + "Failed checker publishes final failed/ambiguous outcome with null compatibility/evidence references and transitions ResumeRequested to Failed/Ambiguous; fresh new resume from that phase succeeds only with all new approval/execution checks.", |
| + "Empty/invalid/duplicate/unsorted required contracts and attempts to remove retained contracts refuse before any process starts." |
| ], |
| "lifecycle_lineage": { |
| "stable_identity": "Server derives lifecycle_operation_id from the retained trusted initial request anchor (proposed equal to initial platform request_id) and existing Guard.OperationID/lifecycle claim. New retry/resume request_id and attempt_id never create a new app claim or replace OperationID before Resumed. Caller has no lifecycle-operation-ID write field.", |
| @@ -328,18 +331,18 @@ |
| "input": "Exactly maintenance.Input{Arguments:CHECK_ARG,Stdin:private JSON}, EncodeInput/SealInput to approved deployment key with new request/attempt and DirectionInput. Worker supplies executable and database path; no caller environment or fd/path authority. Generic limits remain 32args/8192 aggregate/65536 stdin.", |
| "output": "Exact <=65536 bytes on dedicated stdout, captured to EOF and encrypted with existing SealResult/DirectionResult to new request recipient. App-specific checker JSON/schema and compatibility validation belong to reviewed serving artifact contract. Platform does not accept an arbitrary compatible=true body or a caller-uploaded checker receipt.", |
| "authority": "Approved serving artifact must actually expose the intended non-serving checker; no guessed Home command or unmounted dispatcher is treated as implemented. Worker executes/checks it under the same fence/lease and authenticated provenance as mutations; independent integrity/checkpoint follows success.", |
| - "publication": "Checker success publishes immutable compatibility-checked outcome with execution_kind=compatibility-check and private checker result. status.compatibility_outcome retains that reference; status.outcome stays null while final resume is pending. Resume-ready evidence links that exact outcome, immutable approval candidate, final checkpoint/restore and exact serving pins. status.resume_ready_evidence is set only after validated publication, with guard still ResumeRequested. After handoff, exact health/version and matched resumed guard/projection, publish a NEW immutable outcome state=resumed, execution_kind=resume, referencing compatibility_outcome and resume_ready_evidence; CAS status.outcome to it. No receipt is overwritten. Final result_envelope_digest is the SAME checker ciphertext digest, encrypted under the same resume request/attempt/key; no second command/output or re-encryption. Historical compatibility receipt stays reachable from final outcome. Failure after checker success may publish final failed/ambiguous resume outcome with existing compatibility/evidence links but never resumed; serving stays fenced.", |
| + "publication": "Checker success publishes immutable compatibility-checked outcome with execution_kind=compatibility-check and private checker result. status.compatibility_outcome retains that reference; status.outcome stays null while final resume is pending. Resume-ready evidence links that exact outcome, immutable approval candidate, final checkpoint/restore and exact serving pins. status.resume_ready_evidence is set only after validated publication, with guard still ResumeRequested. After handoff, exact health/version and matched resumed guard/projection, publish a NEW immutable outcome state=resumed, execution_kind=resume, referencing compatibility_outcome and resume_ready_evidence; CAS status.outcome to it. No receipt is overwritten. Final result_envelope_digest is the SAME checker ciphertext digest, encrypted under the same resume request/attempt/key; no second command/output or re-encryption. Historical compatibility receipt stays reachable from final outcome. Failure after checker success may publish final failed/ambiguous resume outcome with existing compatibility/evidence links but never resumed; serving stays fenced. A checker failure publishes final failed/ambiguous execution_kind=compatibility-check outcome in status.outcome, with compatibility_outcome=null and resume_ready_evidence=null; only successful checker validation produces compatibility_outcome. If bounded output was captured and authenticated it may be fetched with the failure outcome; absent output is unavailable, never fabricated. Either checker or later handoff failure advances the matching ResumeRequested guard to Failed/Ambiguous with unchanged request/attempt/pins and next generation, preserving denial and allowing a separately freshly approved new resume. Failed CAS/publication leaves recovery-required uncertainty, never automatic execution or serving.", |
| "expiry": "Fresh approval and exact checker input expiry are checked independently. No retry/resume extends prior Home authorization, payload expiry, worker lease or operation timeout." |
| }, |
| - "revision_note": "R2 addresses Fable seven blockers on bae085e; now based on merged11a3039 with exact primitive sources included. Interface-only proposal; no runtime implementation, worker authority or deployment approval.", |
| + "revision_note": "R3 addresses two new R2 blockers against guard.validAdvance/ValidContracts, plus routing/reference syntax clarifications. R1 seven blockers remain resolved. No implementation or deployment approval.", |
| "status_and_guard_mapping": { |
| "authority": "Status is per request workflow metadata, never serving admission authority. Guard phase plus matching projection remains authoritative. Draft retry/resume does not change the predecessor guard. The mapping below distinguishes workflow overlays and guard phases; do not infer absence or clear a guard from draft/refusal/expiry.", |
| "awaiting-preflight, awaiting-approval": "No new guard transition. Initial request has no fence; retry/resume retains the current predecessor guard and denial.", |
| "approved": "Owner gesture accepted, before durable requested fence (initial/retry) or ResumeRequested transition (resume); no command may launch from this status. Existing fence remains intact.", |
| - "requested, quiescing, fenced, executing, succeeded, failed, ambiguous": "Operation workflow states map to identical guard phases after matching durable references. Resume checker execution/failure is instead a substate under ResumeRequested: guard must not move backward to Executing/Failed. State alone never authorizes serving.", |
| + "requested, quiescing, fenced, executing, succeeded, failed, ambiguous": "Operation workflow states map to identical guard phases after matching durable references. Resume checker execution is a substate under ResumeRequested, not a backward move to Executing. Final failed/ambiguous checker or handoff advances ResumeRequested to Failed/Ambiguous with the same request/attempt/pins and next generation, exactly guard.validAdvance. These phases remain denied; subsequent fresh-owner resume uses new request/attempt from Failed/Ambiguous with stable OperationID.", |
| "resume-requested": "Guard ResumeRequested after fresh resume approval and required CAS; all checker, validation and serving handoff steps remain denied.", |
| "resume-ready": "Workflow substate only; guard remains ResumeRequested. Immutable checker outcome and resume-ready evidence exist but no resumed claim.", |
| "resumed": "Only after exact serving health/version, lease handoff, and both matching guard Resumed and record projection. No premature success pointer.", |
| - "refused, expired": "Pre-execution workflow refusal/expiry creates no fence and clears none. A previous active guard remains unchanged. After fence acceptance approval expiry cannot undo it; command/lease failures map failed/ambiguous for operations or stay ResumeRequested for resume." |
| + "refused, expired": "Pre-execution refusal/expiry creates no fence and clears none. Previous active guard remains unchanged. After fence acceptance expiry cannot undo it. Command/lease/checker/handoff failure publishes failed/ambiguous with checked stop evidence and CASes the matching guard to Failed/Ambiguous; uncertainty keeps admission closed. No timeout takeover or automatic retry." |
| } |
| } |
diff --git a/implementation/maintenance-protocol/schemas.json b/implementation/maintenance-protocol/schemas.json| index d7b2e62d3..a2af21654 100644 |
| --- a/implementation/maintenance-protocol/schemas.json |
| +++ b/implementation/maintenance-protocol/schemas.json |
| @@ -105,7 +105,11 @@ |
| "revision": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 256 |
| + "maxLength": 256, |
| + "pattern": "^[!-~]+$", |
| + "not": { |
| + "pattern": "\\*" |
| + } |
| } |
| } |
| }, |
| @@ -139,12 +143,23 @@ |
| "record_revision": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 256 |
| + "maxLength": 256, |
| + "pattern": "^[!-~]+$", |
| + "not": { |
| + "pattern": "\\*" |
| + } |
| }, |
| "artifact_version": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 64 |
| + "maxLength": 64, |
| + "pattern": "^[a-zA-Z0-9._-]{1,64}$", |
| + "not": { |
| + "enum": [ |
| + ".", |
| + ".." |
| + ] |
| + } |
| }, |
| "artifact_digest": { |
| "$ref": "#/$defs/digest" |
| @@ -317,12 +332,23 @@ |
| "record_revision": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 256 |
| + "maxLength": 256, |
| + "pattern": "^[!-~]+$", |
| + "not": { |
| + "pattern": "\\*" |
| + } |
| }, |
| "artifact_version": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 64 |
| + "maxLength": 64, |
| + "pattern": "^[a-zA-Z0-9._-]{1,64}$", |
| + "not": { |
| + "enum": [ |
| + ".", |
| + ".." |
| + ] |
| + } |
| }, |
| "artifact_digest": { |
| "$ref": "#/$defs/digest" |
| @@ -529,7 +555,8 @@ |
| { |
| "type": "null" |
| } |
| - ] |
| + ], |
| + "description": "Opaque server-internal immutable reference; exact stored encoding and authenticated producer validation belong to future coordinator; CLI comparison is not provenance proof." |
| } |
| }, |
| "allOf": [ |
| @@ -642,7 +669,11 @@ |
| "record_revision": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 256 |
| + "maxLength": 256, |
| + "pattern": "^[!-~]+$", |
| + "not": { |
| + "pattern": "\\*" |
| + } |
| }, |
| "artifact_digest": { |
| "$ref": "#/$defs/digest" |
| @@ -718,7 +749,8 @@ |
| { |
| "type": "null" |
| } |
| - ] |
| + ], |
| + "description": "Opaque server-internal immutable reference; exact stored encoding and authenticated producer validation belong to future coordinator; CLI comparison is not provenance proof." |
| } |
| }, |
| "allOf": [ |
| @@ -869,8 +901,12 @@ |
| "items": { |
| "type": "string", |
| "minLength": 1, |
| - "maxLength": 64 |
| - } |
| + "maxLength": 64, |
| + "pattern": "^[a-z][a-z0-9]*([.-][a-z0-9]+)*$" |
| + }, |
| + "minItems": 1, |
| + "uniqueItems": true, |
| + "x-validation": "maintenanceguard.ValidContracts; strict ascending byte order and superset of retained contracts before approval/spawn." |
| }, |
| "result_recipient": { |
| "$ref": "#/$defs/recipient" |