From ca8a7d878c56b4b2b809619b82483d73727bed53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Mon, 19 Oct 2026 13:37:51 -0700 Subject: [PATCH] Inspect unspent file authority operation links --- .../action-boundary/AUTHORITY_CHECKPOINT.md | 2 +- .../file-revocation-handoff.mjs | 86 +++++++++++++++---- ...boundary-file-authority-inspection.test.js | 57 ++++++++++++ 3 files changed, 128 insertions(+), 17 deletions(-) diff --git a/pilots/action-boundary/AUTHORITY_CHECKPOINT.md b/pilots/action-boundary/AUTHORITY_CHECKPOINT.md index f45141d8..a1a52981 100644 --- a/pilots/action-boundary/AUTHORITY_CHECKPOINT.md +++ b/pilots/action-boundary/AUTHORITY_CHECKPOINT.md @@ -44,7 +44,7 @@ node pilots/action-boundary/inspect-file-authority.mjs \ The optional second argument is the file anchor directory. The command opens it with `create: false`; it never creates a missing anchor or authority directory. It exits zero for `observed_consistent` and 2 for a lock, mismatch, unreadable state, or other `needs_reconciliation` result. A present authority or anchor lock might belong to a live worker or a crashed one. `checkpoints.comparison` distinguishes a local/external mismatch; `committed_state.comparison` distinguishes a protocol-2 or protocol-3 state mismatch while the checkpoints still agree. `committed_state.includes_authority_mode` is true only for protocol 3. Protocol 1 reports `not_committed`, not a successful state verification. Omit the anchor only to inspect an unanchored store; an anchored local checkpoint without its anchor reports `authority_anchor_not_supplied`. -`reconciliation` lists claimed grants with no local outcome or an `outcome_unknown` record, even if the lock is gone and the checkpoints agree. Each candidate has only a grant ID and opaque operation/idempotency hashes for looking up the host's external claim and provider history; it does not include the prepared request or recipient. It flags missing or mismatched claim, grant, operation, and outcome links, including an operation whose issuer or epoch differs from its grant. An outcome must use one of the writer's valid status/reason pairs: `returned`/`callback_returned` or `outcome_unknown`/`callback_error`. An invalid pair requires reconciliation rather than counting a local return. `returned` counts callbacks that returned locally; it is not a delivery count. A missing outcome does not prove the callback ran: the external claim could have rejected or the worker could have stopped before handoff. These candidates require human review, never automatic retry. +`reconciliation` lists claimed grants with no local outcome or an `outcome_unknown` record, even if the lock is gone and the checkpoints agree. Each candidate has only a grant ID and opaque operation/idempotency hashes for looking up the host's external claim and provider history; it does not include the prepared request or recipient. It also scans all grants and operation markers, including unspent ones: an orphan operation marker can block reuse of a host intent, and a missing or mismatched marker can make an unspent grant unusable. These links, malformed grant and operation records, and missing or mismatched claim and outcome links produce `invalid_records` and require reconciliation. An outcome must use one of the writer's valid status/reason pairs: `returned`/`callback_returned` or `outcome_unknown`/`callback_error`. An invalid pair requires reconciliation rather than counting a local return. `returned` counts callbacks that returned locally; it is not a delivery count. A missing outcome does not prove the callback ran: the external claim could have rejected or the worker could have stopped before handoff. These candidates require human review, never automatic retry. The command performs no unlock, mode initialization, checkpoint advance, grant issue, clock update, provider call, or recovery. A missing mode reports `authority_mode_missing`, including in an empty directory: a never initialized store cannot be distinguished from one whose mode was deleted. Its scan is not an atomic snapshot of live workers, and `observed_consistent` cannot establish provider delivery, an independently protected anchor or external claim backend, or permission to resume an uncertain operation. Preserve the records and reconcile the provider's operation and idempotency history before any operator-controlled recovery. `inspectFileRevocationHandoff({ directory, anchor })` exposes the same observation to a host without invoking the CLI. diff --git a/pilots/action-boundary/file-revocation-handoff.mjs b/pilots/action-boundary/file-revocation-handoff.mjs index 58c4d39a..5c2d1404 100644 --- a/pilots/action-boundary/file-revocation-handoff.mjs +++ b/pilots/action-boundary/file-revocation-handoff.mjs @@ -238,22 +238,6 @@ function claimReconciliation(directory) { (grant.operation_sha256 !== undefined && !DIGEST.test(grant.operation_sha256)))) { issues.push({ grant_id: grantId, problem: "grant_missing_or_mismatch" }); } - if (grant?.operation_sha256 && DIGEST.test(grant.operation_sha256)) { - let operation, operationUnreadable = false; - try { operation = readRecord(path.join(directory, "operations", - `${grant.operation_sha256}.json`)); } - catch { issues.push({ grant_id: grantId, problem: "operation_unreadable" }); - operationUnreadable = true; } - if (!operationUnreadable && (!operation || - operation.protocol !== "aml-file-authority-operation/1" || - operation.operation_sha256 !== grant.operation_sha256 || - operation.grant_id !== grantId || - operation.binding_sha256 !== claim.binding_sha256 || - operation.issuer_sha256 !== grant.issuer_sha256 || - operation.epoch !== grant.epoch)) { - issues.push({ grant_id: grantId, problem: "operation_missing_or_mismatch" }); - } - } if (outcomeNamesSet.has(name)) { try { outcome = readRecord(path.join(outcomes, name)); } catch { issues.push({ grant_id: grantId, problem: "outcome_unreadable" }); continue; } @@ -284,6 +268,76 @@ function claimReconciliation(directory) { issues.push({ grant_id: name.slice(0, -5), problem: "outcome_without_claim" }); } } + // Claims cover only spent grants. Inspect both sides of every operation link: + // an orphan marker can block a host intent, while an unspent grant whose + // marker disappeared cannot be dispatched safely. Neither has a claim yet. + const grants = new Map(), operations = new Map(); + for (const name of fs.readdirSync(path.join(directory, "grants")).sort()) { + if (!pattern.test(name)) { + issues.push({ collection: "grants", problem: "unexpected_filename" }); + continue; + } + const grantId = name.slice(0, -5); + let grant; + try { grant = readRecord(path.join(directory, "grants", name)); } + catch { issues.push({ grant_id: grantId, problem: "grant_unreadable" }); continue; } + if (!grant || grant.protocol !== "aml-file-authority-grant/1" || + grant.grant_id !== grantId || !DIGEST.test(grant.issuer_sha256) || + !DIGEST.test(grant.binding_sha256) || + !Number.isSafeInteger(grant.epoch) || grant.epoch < 0 || + !Number.isSafeInteger(grant.issued_at_ms) || + !Number.isSafeInteger(grant.expires_at_ms) || + grant.expires_at_ms <= grant.issued_at_ms || + (grant.operation_sha256 !== undefined && + !DIGEST.test(grant.operation_sha256)) || + (grant.required_claim_protocol !== undefined && + grant.required_claim_protocol !== CLAIM_V1 && + grant.required_claim_protocol !== CLAIM_V2)) { + issues.push({ grant_id: grantId, problem: "grant_invalid" }); + continue; + } + grants.set(grantId, grant); + } + const operationPattern = /^[0-9a-f]{64}\.json$/; + for (const name of fs.readdirSync(path.join(directory, "operations")).sort()) { + if (!operationPattern.test(name)) { + issues.push({ collection: "operations", problem: "unexpected_filename" }); + continue; + } + const operationHash = name.slice(0, -5); + let operation; + try { operation = readRecord(path.join(directory, "operations", name)); } + catch { issues.push({ operation_sha256: operationHash, + problem: "operation_unreadable" }); continue; } + if (!operation || operation.protocol !== "aml-file-authority-operation/1" || + operation.operation_sha256 !== operationHash || + typeof operation.grant_id !== "string" || !ID.test(operation.grant_id) || + !DIGEST.test(operation.issuer_sha256) || + !DIGEST.test(operation.binding_sha256) || + !Number.isSafeInteger(operation.epoch) || operation.epoch < 0) { + issues.push({ operation_sha256: operationHash, problem: "operation_invalid" }); + continue; + } + operations.set(operationHash, operation); + } + const matchingLink = (grant, operation) => grant && operation && + grant.grant_id === operation.grant_id && + grant.operation_sha256 === operation.operation_sha256 && + grant.binding_sha256 === operation.binding_sha256 && + grant.issuer_sha256 === operation.issuer_sha256 && grant.epoch === operation.epoch; + for (const [grantId, grant] of grants) { + if (grant.operation_sha256 && + !matchingLink(grant, operations.get(grant.operation_sha256))) { + issues.push({ grant_id: grantId, problem: "operation_missing_or_mismatch" }); + } + } + for (const operation of operations.values()) { + if (!matchingLink(grants.get(operation.grant_id), operation)) { + issues.push({ grant_id: operation.grant_id, + operation_sha256: operation.operation_sha256, + problem: "operation_without_matching_grant" }); + } + } return { status: issues.length ? "invalid_records" : candidates.length ? "provider_review_needed" : "no_unresolved_local_claims", counts, candidates, issues }; diff --git a/test/action-boundary-file-authority-inspection.test.js b/test/action-boundary-file-authority-inspection.test.js index c22896d7..b528777a 100644 --- a/test/action-boundary-file-authority-inspection.test.js +++ b/test/action-boundary-file-authority-inspection.test.js @@ -200,6 +200,63 @@ test("unanchored inspection checks the spent operation epoch and issuer link", ( })(); }); +test("unanchored inspection reports an orphan operation marker before any claim", () => { + const parent = fs.mkdtempSync(path.join(os.tmpdir(), "aml-inspection-orphan-")); + try { + const directory = path.join(parent, "authority"); + const host = createFileRevocationHandoff({ directory }); + const grant = host.issue(input(), { operationId: "host-intent-inspect-orphan" }); + fs.rmSync(path.join(directory, "grants", `${grant.grant_id}.json`)); + const before = snapshot(parent); + const report = inspectFileRevocationHandoff({ directory }); + assert.equal(report.committed_state.comparison, "not_committed"); + assert.equal(report.status, "needs_reconciliation"); + assert.ok(report.reasons.includes("reconciliation_records_invalid")); + assert.ok(report.reconciliation.issues.some(item => + item.grant_id === grant.grant_id && + item.problem === "operation_without_matching_grant")); + assert.equal(JSON.stringify(report).includes("team@example.test"), false); + assert.deepEqual(snapshot(parent), before); + } finally { fs.rmSync(parent, { recursive: true, force: true }); } +}); + +test("unanchored inspection reports an unspent grant with a missing operation", () => { + const parent = fs.mkdtempSync(path.join(os.tmpdir(), "aml-inspection-unspent-")); + try { + const directory = path.join(parent, "authority"); + const host = createFileRevocationHandoff({ directory }); + const grant = host.issue(input(), { operationId: "host-intent-inspect-unspent" }); + const operationName = fs.readdirSync(path.join(directory, "operations"))[0]; + fs.rmSync(path.join(directory, "operations", operationName)); + const before = snapshot(parent); + const report = inspectFileRevocationHandoff({ directory }); + assert.equal(report.reconciliation.counts.claims, 0); + assert.equal(report.status, "needs_reconciliation"); + assert.ok(report.reconciliation.issues.some(item => + item.grant_id === grant.grant_id && + item.problem === "operation_missing_or_mismatch")); + assert.deepEqual(snapshot(parent), before); + } finally { fs.rmSync(parent, { recursive: true, force: true }); } +}); + +test("unanchored inspection requires the grant to point back to its operation", () => { + const parent = fs.mkdtempSync(path.join(os.tmpdir(), "aml-inspection-backlink-")); + try { + const directory = path.join(parent, "authority"); + const host = createFileRevocationHandoff({ directory }); + const grant = host.issue(input(), { operationId: "host-intent-inspect-backlink" }); + const grantFile = path.join(directory, "grants", `${grant.grant_id}.json`); + const record = JSON.parse(fs.readFileSync(grantFile, "utf8")); + delete record.operation_sha256; + fs.writeFileSync(grantFile, JSON.stringify(record) + "\n"); + const report = inspectFileRevocationHandoff({ directory }); + assert.equal(report.status, "needs_reconciliation"); + assert.ok(report.reconciliation.issues.some(item => + item.grant_id === grant.grant_id && + item.problem === "operation_without_matching_grant")); + } finally { fs.rmSync(parent, { recursive: true, force: true }); } +}); + test("an outcome with no spent claim is reported as an invalid local link", () => fixture(async ({ directory, anchor, host }) => { const grant = host.issue(input());