Skip to content

feat(playbot-lane): onboard Playbot 0.94.0 native lane via fused threads:launch contract - #9

Merged
jokim1 merged 5 commits into
lila-mainfrom
fm/fm-playbot-094-lane-onboard
Aug 20, 2026
Merged

feat(playbot-lane): onboard Playbot 0.94.0 native lane via fused threads:launch contract#9
jokim1 merged 5 commits into
lila-mainfrom
fm/fm-playbot-094-lane-onboard

Conversation

@jokim1

@jokim1 jokim1 commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Intent

Onboard Playbot 0.94.0 to the native lane so bin/fm-playbot-lanes.mjs doctor reaches native-enabled and native dispatch is un-gated, matching the 0.93.1 onboard precedent (commit 66edbb5).

0.94.0 removed two IPC channels the lane depended on, so this is a release-aware adaptation, not just a manifest seed entry. Captain approved ADAPT (over downgrading or staying courier-primary); firstmate then chose to extend the adapt to the fused create+open contract and to harden the static IPC check in the same PR.

Deliberate decisions a diff reviewer would not otherwise know:

  1. threads:openThread and db:workspaceThreads:open are gone in 0.94.0; opening a thread now flows through threads:launch (existing-workspace), which the app mints the id for and returns { workspace, thread, ... }. The abstract operation and evidence key deliberately STAY threads:openThread; only the wire channel and result contract are release-aware. This is intentional to keep the frozen NATIVE_REQUIRED_OPERATIONS / evidence-integrity / overlay machinery stable and low-risk; the evidence body records the real wire channel.
  2. workspace:create is also gone in 0.94.0 (the old substring scan false-positived it on the workspace:created event). Workspace creation is fused into threads:launch(new-workspace), which creates the workspace AND opens its first thread in one call. Firstmate's requirement: model this as the single fused operation but keep BOTH former guarantees (workspace created + thread opened) provable in evidence. So the fused create returns the workspace (reconstructed from the authoritative DB row) plus the fused thread id, the smoke ADOPTS that thread rather than opening a second one, and both evidence keys are recorded from the one launch, annotated with the real wire channel and the fusion.
  3. The fused launch provisions its worktree root asynchronously after returning, so waitForWorkspaceProvisioned polls until workspace_roots lands before asserting identity/branch. This is expected behavior of the fusion, not a workaround.
  4. Approved in the same PR: harden scanFileForNeedles with an exactToken mode so an event string can never satisfy a command-channel needle by substring; the doctor's ipc_channel_strings_static uses it, the preload-bridge substring scan is unchanged.

Everything was verified read-only against the live 0.94.0 app.asar and then proven end to end by the Phase-1 disposable smoke (operator-run, targets only the registered disposable project, never MAIN): doctor reports native-enabled and the smoke recorded signed evidence for every 0.94.0 mutation plus write-denial confinement, preserving 0.92.0/0.93.1 evidence. Tests cover the workspace-create and thread-open contracts, the fused launch result validation, and the exact-token accept/reject behavior. The committed evidence files under docs/verification/playbot-mutation-evidence/ are smoke-produced signed artifacts, not hand-written.

What Changed

  • Onboarded Playbot 0.94.0 to the native lane in bin/fm-playbot-lanes.mjs with a release-aware adaptation: 0.94.0 removed the threads:openThread, db:workspaceThreads:open, and workspace:create IPC channels, so thread-open now routes through threads:launch (existing-workspace, app-minted thread id) and workspace creation is fused into threads:launch (new-workspace), which creates the workspace and opens its first thread in one call. The abstract operations and evidence keys are unchanged; evidence bodies record the real wire channel and fusion annotations, and waitForWorkspaceProvisioned polls for the asynchronously provisioned worktree root.
  • Hardened scanFileForNeedles with an exactToken mode used by the doctor's ipc_channel_strings_static check, so an event string (e.g. workspace:created) can no longer satisfy a command-channel needle by substring; the preload-bridge substring scan is unchanged.
  • Committed the signed Phase-1 smoke evidence publication (2026-08-20T16-17-04-891Z) covering every 0.94.0 mutation operation plus write-denial confinement while preserving 0.92.0/0.93.1 evidence, and extended the hermetic test suite with the 0.94.0 thread-open/workspace-create contract, fused-launch result validation, and exact-token accept/reject cases. Three known cleanups (explicit --thread-id rejection on the launch path, non-empty provisioned-path check, launch-payload dedupe) were intentionally reverted to keep the script byte-identical to the smoke-attested version and are deferred to a follow-up that re-runs the smoke, as noted in the pipeline review.

Risk Assessment

✅ Low: The final tree is byte-identical to the round-1 reviewed head whose evidence chain I verified end-to-end (record hashes match overlay pointers, overlay matches the signed receipt, and the receipt's lanesSha256 again matches the committed script, restoring the doctor's path to native-enabled), with the three minor code findings explicitly deferred by the user to a follow-up smoke-backed PR.

Testing

Ran the full hermetic lanes suite (green, including the new 0.94.0 contract and exact-token tests), then demonstrated the intent live: the target-commit doctor and ready commands against the installed Playbot 0.94.0 report native-enabled with mutations un-gated while the base-commit doctor fails closed on the same app; verified the live app.asar wire-channel claims via exact-token vs substring scans (workspace:create exists only inside workspace:created); and independently verified the committed smoke evidence chain — receipt signature, overlay hash, and all 21 record hashes across 0.92.0/0.93.1/0.94.0 — plus the fused-launch and write-denial annotations in the 0.94.0 records. No visual artifacts because the surface is a CLI; JSON transcripts are the end-user surface. No live mutation/smoke was run (operator-only, disposable-project-only by design).

Evidence: Live doctor on Playbot 0.94.0 (target commit): native-enabled

"appVersion": "0.94.0", "operatingState": "native-enabled", "readOnlyReady": true, "ready": true, "mutationsEnabled": true; mutation_evidence_integrity verified all 21 entries across 0.92.0/0.93.1/0.94.0 with zero refusals

{
  "command": "doctor",
  "appVersion": "0.94.0",
  "operatingState": "native-enabled",
  "readOnlyReady": true,
  "ready": true,
  "mutationsEnabled": true,
  "dimensions": [
    {
      "name": "release_compatibility",
      "status": "pass",
      "appVersion": "0.94.0",
      "manifestVersion": 2,
      "reason": null
    },
    {
      "name": "mutation_evidence_integrity",
      "status": "pass",
      "overlayPresent": true,
      "verified": [
        "0.92.0/workspace:create",
        "0.92.0/threads:openThread",
        "0.92.0/threads:send",
        "0.92.0/threads:stop",
        "0.92.0/threads:archiveThread",
        "0.92.0/workspace:delete",
        "0.92.0/confinement",
        "0.93.1/workspace:create",
        "0.93.1/threads:openThread",
        "0.93.1/threads:send",
        "0.93.1/threads:stop",
        "0.93.1/threads:archiveThread",
        "0.93.1/workspace:delete",
        "0.93.1/confinement",
        "0.94.0/workspace:create",
        "0.94.0/threads:openThread",
        "0.94.0/threads:send",
        "0.94.0/threads:stop",
        "0.94.0/threads:archiveThread",
        "0.94.0/workspace:delete",
        "0.94.0/confinement"
      ],
      "refused": []
    },
    {
      "name": "mutation_evidence_attestation_tool",
      "status": "pass",
      "tool": "ssh-keygen"
    },
    {
      "name": "app_reachability_and_run_identity",
      "status": "pass",
      "appRun": {
        "appRunId": "473bd287-8b0b-4b3e-8d1f-68eb9f21a340",
        "startedAt": "2026-08-20T04:48:31.593Z",
        "clean": false
      },
      "cdp": {
        "ok": true,
        "port": 51181,
        "browser": "Chrome/150.0.7871.212",
        "protocolVersion": "1.3",
        "targetCount": 6
      }
    },
    {
      "name": "application_database_schema",
      "status": "pass",
      "ok": true,
      "checks": [
        {
          "check": "user_version",
          "expected": 0,
          "actual": 0,
          "ok": true
        },
        {
          "check": "table:projects",
          "ok": true,
          "missingColumns": []
        },
        {
          "check": "table:repositories",
          "ok": true,
          "missingColumns": []
        },
        {
          "check": "table:project_roots",
          "ok": true,
          "missingColumns": []
        },
        {
          "check": "table:workspaces",
          "ok": true,
          "missingColumns": []
        },
        {
          "check": "table:workspace_roots",
          "ok": true,
          "missingColumns": []
        },
        {
          "check": "table:workspace_threads",
          "ok": true,
          "missingColumns": []
        }
      ]
    },
    {
      "name": "codex_database_schema",
      "status": "pass",
      "ok": true,
      "checks": [
        {
          "check": "user_version",
          "expected": 0,
          "actual": 0,
          "ok": true
        },
        {
          "check": "table:threads",
          "ok": true,
          "missingColumns": []
        }
      ]
    },
    {
      "name": "ipc_channel_strings_static",
      "status": "pass",
      "ok": true,
      "checks": [
        {
          "needle": "threads:launch",
          "ok": true
        },
        {
          "needle": "threads:setActiveThread",
          "ok": true
        },
        {
          "needle": "threads:send",
          "ok": true
        },
        {
          "needle": "threads:stop",
          "ok": true
        },
        {
          "needle": "threads:archiveThread",
          "ok": true
        },
        {
          "needle": "workspace:archive",
          "ok": true
        },
        {
          "needle": "workspace:delete",
          "ok": true
        }
      ]
    },
    {
      "name": "generic_preload_invoke_static",
      "status": "pass",
      "ok": true,
      "checks": [
        {
          "needle": "electronAPI",
          "ok": true
        },
        {
          "needle": "ipcRenderer.invoke",
          "ok": true
        }
      ]
    },
    {
      "name": "rollout_schema_and_exact_mapping",
      "status": "not_checked",
      "reason": "pass an exact --thread-id to validate one rollout"
    },
    {
      "name": "controller_lease_and_call_correlation",
      "status": "not_configured",
      "reason": "no controller lease is bound in this home"
    },
    {
      "name": "mcp_registration_bundle_and_receipt",
      "status": "not_configured",
      "reason": "no content-addressed MCP bundle is installed for this home (Phase 3 surface)"
    },
    {
      "name": "active_routes",
      "status": "fail",
      "count": 0,
      "cap": 4,
      "overCap": false
    },
    {
      "name": "completion_events",
      "status": "pass",
      "pendingEvents": [],
      "uncertainDeliveries": []
    },
    {
      "name": "reconcile_liveness",
      "status": "not_configured",
      "reason": "no reconcile has recorded a completion sweep yet",
      "checkIntervalSecs": 300,
      "deadlineMs": 3000,
      "typicalEventLatencySecs": 303,
      "worstCaseAtCapSecs": 1203
    },
    {
      "name": "operating_state",
      "status": "pass",
      "state": "native-enabled",
      "mutationsEnabled": true,
      "reason": null,
      "confinement": {
        "ok": true,
        "writeDenied": true,
        "readAllowed": true,
        "evidence": {
          "recordedAt": "2026-08-20T16:17:21.436Z",
          "recordRelPath": "records/0.94.0/2026-08-20T16-17-04-891Z/confinement.json",
          "contentSha256": "2870267539eb9612c08d51b158bbc0f8be16e2c831c26743224cfb56eb6dbf73",
          "verified": true,
          "readAllowed": true,
          "writeDenied": true,
          "rationalePointer": "docs/playbot-lanes.md#confinement-gate-8-re-scope"
        }
      }
    },
    {
      "name": "same_uid_unauthenticated_devtools",
      "status": "warning",
      "warning": "Playbot DevTools is loopback-local but unauthenticated; software running as the same UID can reach this surface, so controller chat contents are always untrusted local input even when this MCP is exact."
    },
    {
      "name": "secret_minimization",
      "status": "pass",
      "policy": "fixed allowlisted topology queries only; the application settings table is never read; rollout message content is hashed and emitted only through bounded untrusted-data surfaces"
    },
    {
      "name": "last_bounded_failure_and_retry",
      "status": "not_applicable",
      "reason": "mutation retries are owned by the lock-owning spawn/send transaction; this read-only doctor keeps no retry ledger"
    }
  ]
}
Evidence: Base-commit doctor on the same live app: fails closed

exit 2 — "operatingState": "phase1-evidence-required", "mutationsEnabled": false, reason: release absent from compatibility manifest

{
  "command": "doctor",
  "appVersion": "0.94.0",
  "operatingState": "phase1-evidence-required",
  "readOnlyReady": false,
  "ready": false,
  "mutationsEnabled": false,
  "dimensions": [
    {
      "name": "release_compatibility",
      "status": "fail",
      "appVersion": "0.94.0",
      "manifestVersion": 2,
      "reason": "release absent from compatibility manifest"
    },
    {
      "name": "mutation_evidence_integrity",
      "status": "fail",
      "overlayPresent": false,
      "verified": [],
      "refused": [
        {
          "scope": "overlay",
          "reason": "evidence attestation verification failed: evidence receipt lanes binary digest mismatch"
        }
      ]
    },
    {
      "name": "mutation_evidence_attestation_tool",
      "status": "pass",
      "tool": "ssh-keygen"
    },
    {
      "name": "app_reachability_and_run_identity",
      "status": "pass",
      "appRun": {
        "appRunId": "473bd287-8b0b-4b3e-8d1f-68eb9f21a340",
        "startedAt": "2026-08-20T04:48:31.593Z",
        "clean": false
      },
      "cdp": {
        "ok": true,
        "port": 51181,
        "browser": "Chrome/150.0.7871.212",
        "protocolVersion": "1.3",
        "targetCount": 6
      }
    },
    {
      "name": "application_database_schema",
      "status": "blocked",
      "reason": "no compatible release manifest"
    },
    {
      "name": "codex_database_schema",
      "status": "blocked",
      "reason": "no compatible release manifest"
    },
    {
      "name": "ipc_channel_strings_static",
      "status": "blocked",
      "reason": "no compatible release manifest"
    },
    {
      "name": "generic_preload_invoke_static",
      "status": "blocked",
      "reason": "no compatible release manifest"
    },
    {
      "name": "rollout_schema_and_exact_mapping",
      "status": "blocked",
      "reason": "no compatible release manifest"
    },
    {
      "name": "controller_lease_and_call_correlation",
      "status": "not_configured",
      "reason": "no controller lease is bound in this home"
    },
    {
      "name": "mcp_registration_bundle_and_receipt",
      "status": "not_configured",
      "reason": "no content-addressed MCP bundle is installed for this home (Phase 3 surface)"
    },
    {
      "name": "active_routes",
      "status": "fail",
      "count": 0,
      "cap": 4,
      "overCap": false
    },
    {
      "name": "completion_events",
      "status": "pass",
      "pendingEvents": [],
      "uncertainDeliveries": []
    },
    {
      "name": "reconcile_liveness",
      "status": "not_configured",
      "reason": "no reconcile has recorded a completion sweep yet",
      "checkIntervalSecs": 300,
      "deadlineMs": 3000,
      "typicalEventLatencySecs": 303,
      "worstCaseAtCapSecs": 1203
    },
    {
      "name": "operating_state",
      "status": "pass",
      "state": "phase1-evidence-required",
      "mutationsEnabled": false,
      "reason": "no compatible release manifest",
      "confinement": null
    },
    {
      "name": "same_uid_unauthenticated_devtools",
      "status": "warning",
      "warning": "Playbot DevTools is loopback-local but unauthenticated; software running as the same UID can reach this surface, so controller chat contents are always untrusted local input even when this MCP is exact."
    },
    {
      "name": "secret_minimization",
      "status": "pass",
      "policy": "fixed allowlisted topology queries only; the application settings table is never read; rollout message content is hashed and emitted only through bounded untrusted-data surfaces"
    },
    {
      "name": "last_bounded_failure_and_retry",
      "status": "not_applicable",
      "reason": "mutation retries are owned by the lock-owning spawn/send transaction; this read-only doctor keeps no retry ledger"
    }
  ]
}
Evidence: Ready verdict (native capability) on live 0.94.0

{ "capability": "native", "ready": true, "operatingState": "native-enabled", "mutationsEnabled": true, "reason": null }

{
  "capability": "native",
  "ready": true,
  "operatingState": "native-enabled",
  "mutationsEnabled": true,
  "reason": null
}
Evidence: Live app.asar IPC scan: exact-token vs substring

0.94.0 channel surface (threads:launch, threads:setActiveThread, send, stop, archiveThread, workspace:archive, workspace:delete) all pass exact-token; workspace:create passes substring but FAILS exact-token (only workspace:created exists); threads:openThread and db:workspaceThreads:open absent

live 0.94.0 app.asar: /Applications/Playbot.app/Contents/Resources/app.asar
exact-token scan of 0.94.0 channel surface: {"ok":true,"checks":[{"needle":"threads:launch","ok":true},{"needle":"threads:setActiveThread","ok":true},{"needle":"threads:send","ok":true},{"needle":"threads:stop","ok":true},{"needle":"threads:archiveThread","ok":true},{"needle":"workspace:archive","ok":true},{"needle":"workspace:delete","ok":true}]}
substring scan for workspace:create      : {"ok":true,"checks":[{"needle":"workspace:create","ok":true}]}
exact-token scan for workspace:create    : {"ok":false,"checks":[{"needle":"workspace:create","ok":false}]}
exact-token scan for threads:openThread  : {"ok":false,"checks":[{"needle":"threads:openThread","ok":false}]}
exact-token scan for db:workspaceThreads:open: {"ok":false,"checks":[{"needle":"db:workspaceThreads:open","ok":false}]}
Evidence: Evidence signature + hash verification (all releases)

ssh-keygen: Good "firstmate-playbot-smoke" signature for jokim1 with ED25519 key; receipt overlaySha256 matches overlay; all 21 record contentSha256 values verify (0.92.0, 0.93.1, 0.94.0 incl. confinement)

0.92.0/workspace:create sha256 OK
0.92.0/threads:openThread sha256 OK
0.92.0/threads:send sha256 OK
0.92.0/threads:stop sha256 OK
0.92.0/threads:archiveThread sha256 OK
0.92.0/workspace:delete sha256 OK
0.92.0/confinement sha256 OK
0.93.1/workspace:create sha256 OK
0.93.1/threads:openThread sha256 OK
0.93.1/threads:send sha256 OK
0.93.1/threads:stop sha256 OK
0.93.1/threads:archiveThread sha256 OK
0.93.1/workspace:delete sha256 OK
0.93.1/confinement sha256 OK
0.94.0/workspace:create sha256 OK
0.94.0/threads:openThread sha256 OK
0.94.0/threads:send sha256 OK
0.94.0/threads:stop sha256 OK
0.94.0/threads:archiveThread sha256 OK
0.94.0/workspace:delete sha256 OK
0.94.0/confinement sha256 OK
Evidence: Hermetic lanes test suite transcript

fm-playbot-lanes: all tests passed (incl. 0.94.0 release-aware contracts, fused-launch validation, exact-token accept/reject)

ok - doctor is read-only green on the compatible 0.90 fixture with mutations disabled
ok - unknown Playbot release fails closed
ok - malformed application schema fails closed
ok - exact resolution works and multi-root ambiguity refuses
ok - strict JSONL parsing accepts a real completion and rejects the forged worker-text fixture
ok - composer/busy/agent-state vocabularies hold, with Playbot absence never guessed dead
ok - ready/mutation gates refuse with PHASE1-EVIDENCE-REQUIRED and conservative defaults
ok - well-formed meta + bound route + live DB conjunction validates
ok - tampered generation and wrong-mode route records fail closed
ok - bind-project binds exactly once and refuses duplicates
ok - bind-controller mints generation-bound mode-0600 leases and refuses without the lock
ok - MCP serves health only, denies task-data tools without proven identity, and exposes no mutation surface
ok - connect refusal reports not-ok with an error
ok - malformed version response rejected
ok - missing websocket identity rejected
ok - malformed target enumeration rejected
ok - dead target skipped, live second target selected
ok - bounded Runtime.evaluate round trip returns the renderer value
ok - socket close after open rejects the pending command
ok - stalled socket is bounded by its own command timer
ok - stall rejected fast, not at the 5s default
ok - invoke bridge JSON-serializes hostile payload bytes into inert data
ok - malformed IPC channel refused before evaluation
cdp-transport: all scenarios passed
ok - CDP transport rejects on close/timeout, skips dead targets, and serializes payloads safely
ok - mutation evidence records, hash integrity, shape parsers, and write-denial confinement gate hold offline
fm-playbot-lanes: all tests passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-playbot-lanes.mjs:1770 - On 0.94.0, mutationOpenThreadLaunch silently ignores request.id: the payload only carries destination/thread/activate, so open-thread --thread-id <id> (bin/fm-playbot-lanes.mjs:4019 passes args['thread-id']) accepts an explicit caller-minted id it cannot honor and returns a different app-minted id without any error. The launch path should throw when request.id is provided, since the 0.94.0 contract makes a caller-chosen id unsatisfiable.
  • ℹ️ bin/fm-playbot-lanes.mjs:1661 - The fused 0.94.0 create asserts only the workspace:create evidence gate (gatedInvoke('workspace:create', ..., wireChannel: 'threads:launch')), yet the same wire call also performs the thread-open operation. If threads:openThread evidence were individually refused (e.g. one corrupt record) while workspace:create verified, CLI create would still open a thread without that gate. Edge case: both records come from the same smoke, and native dispatch separately requires all operations, so impact is marginal. This matches the intent's single-fused-operation modeling; flagging the gating asymmetry for awareness.
  • ℹ️ bin/fm-playbot-lanes.mjs:1562 - waitForWorkspaceProvisioned returns once project_root_id, archive_state, and branch match, but never checks that workspace_roots.path is non-empty, and the smoke immediately consumes workspaceRow.path as worktreePath for the confinement probe. If the app ever committed the root row with a null path (LEFT JOIN would surface it), downstream would proceed with a null worktree path. Adding a non-empty path condition to the poll would make the 'fully provisioned' guarantee complete.
  • ℹ️ bin/fm-playbot-lanes.mjs:1772 - The thread/activate payload block ({ title, approvalMode, planMode, ephemeral } plus activate: request.activate !== false) is duplicated between mutationWorkspaceCreateFusedLaunch (bin/fm-playbot-lanes.mjs:1651-1657) and mutationOpenThreadLaunch (bin/fm-playbot-lanes.mjs:1772-1778). Extracting a small shared launchThreadPayload helper would prevent the two launch payload shapes from drifting apart in future release adaptations.

🔧 Fix: reject explicit launch thread id, require provisioned path, dedupe payload
1 error still open:

  • 🚨 docs/verification/playbot-mutation-evidence/publications/2026-08-20T16-17-04-891Z/overlay.v1.json.receipt.json:16 - The review-fix commit d1303fc edited bin/fm-playbot-lanes.mjs after the Phase-1 smoke, invalidating the committed 0.94.0 evidence attestation: the receipt pins lanesSha256=30975d99... (the a4809d4 script), but the file now hashes to ee3f73b4..., and verifyEvidenceAttestation (bin/fm-playbot-lanes.mjs:345-348) enforces that binding in every default loadCompatibilityManifest call. At this head the overlay is refused ('evidence receipt lanes binary digest mismatch'), all mutation evidence reverts to PHASE1-EVIDENCE-REQUIRED, doctor reports phase1-evidence-required, and native dispatch is re-gated — contradicting the intent's required outcome: 'doctor reaches native-enabled and native dispatch is un-gated'. The hermetic tests will NOT catch this because fixture receipts are minted at test time with the current file hash. Resolution requires operator action, since evidence must be smoke-produced and signed (intent: 'smoke-produced signed artifacts, not hand-written'): re-run the Phase-1 disposable smoke against the fixed script to publish a fresh receipt (verifyPriorEvidenceForSmoke uses ignoreLanesSha256, so 0.92.0/0.93.1 evidence is preserved), or drop the fix commit and ship the exact smoked script.

🔧 Fix: restore smoke-attested lanes script, defer fixes to follow-up
1 info still open:

  • ℹ️ bin/fm-playbot-lanes.mjs:1766 - Deliberate deferral on record: commit 28007c6 restores bin/fm-playbot-lanes.mjs byte-identical to the smoke-attested a4809d4 version (sha256 30975d99... matches the receipt's lanesSha256, so evidence attestation verifies and doctor can reach native-enabled). As a consequence, the three round-1 findings (mutationOpenThreadLaunch silently ignoring an explicit --thread-id on 0.94.0, waitForWorkspaceProvisioned not requiring a non-empty workspace_roots.path, and the duplicated launch thread/activate payload block) are intentionally back in the code, per the user's explicit instruction to defer them to a follow-up PR that re-runs the Phase-1 smoke and regenerates evidence bound to the fixed script. No action in this PR.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-playbot-lanes.test.sh (hermetic suite incl. new 0.94.0 thread-open/workspace-create contract tests, validateThreadLaunchResult accept/reject, exactToken scan accept/reject) — all pass
  • node bin/fm-playbot-lanes.mjs doctor --json read-only against live Playbot 0.94.0 → operatingState native-enabled, mutationsEnabled true, exit 0
  • base-commit (66edbb5) doctor against the same live app → fails closed with 'release absent from compatibility manifest', mutationsEnabled false, exit 2
  • node bin/fm-playbot-lanes.mjs ready --json --capability native → ready true, native-enabled
  • exact-token vs substring scanFileForNeedles against the live /Applications/Playbot.app app.asar: 0.94.0 channel surface all present; workspace:create matches only by substring (workspace:created event) and is rejected exact-token; threads:openThread and db:workspaceThreads:open absent
  • ssh-keygen -Y verify of the 2026-08-20T16-17-04-891Z publication receipt against allowed_signers (good firstmate-playbot-smoke signature) plus receipt overlaySha256 match
  • independent sha256 verification of all 21 evidence records (0.92.0, 0.93.1, 0.94.0 mutation ops + confinement) against the published overlay
  • manual inspection of 0.94.0 workspace:create/threads:openThread/confinement evidence records for the fused-launch annotations (wireChannel, fused, fusedThreadId, fusedWith, idSource, write-denial)
⚠️ **Document** - 1 info
  • ℹ️ bin/fm-playbot-lanes.mjs:3745 - bin/fm-playbot-lanes.mjs --help (the designated owner of exact CLI flags) still lists open-thread [--thread-id <native-id>], which is silently ignored on 0.94.0; fixing it requires editing the smoke-attested script (breaking the evidence receipt's lanesSha256) and re-running the operator smoke, so it should land with the follow-up already deferred by commit 28007c6 (which also restores d1303fc's explicit rejection of a caller-chosen launch thread id). The operator doc now carries the correct behavior in the meantime.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jokim1 added 5 commits August 20, 2026 08:46
…94.0

Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open
IPC channels. "Open a thread" was restructured: the operation now flows
through threads:launch, which the app mints the thread id for and returns
the persisted thread ({workspace, thread, activate, createdWorkspace}),
with activation split into threads:setActiveThread. This inverts every
assumption the legacy lane relied on (caller-minted chat-N-N id, undefined
result, single-channel open).

Make the thread-open wire contract release-aware while keeping the abstract
operation and evidence key stable at threads:openThread:

- releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a
  threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and
  the app-minted-id, non-undefined-result contract.
- MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract
  operation keys so threads:launch is invokable; gatedInvoke takes an explicit
  wireChannel so the evidence gate still keys off threads:openThread.
- mutationOpenThread dispatches to the legacy path (unchanged) or a new launch
  path that consumes the app-minted result.thread.id via validateThreadLaunchResult
  and re-asserts the persisted row.
- The Phase-1 smoke records the real wire channel and app-minted id in the
  evidence body so the 0.94.0 overlay honestly reflects the launch contract.

The static IPC surface and payload shapes were verified read-only against the
live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility,
both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled
still awaits the Phase-1 disposable smoke as designed.

Tests and the fixture bundle extend to the 0.94.0 launch contract.
…+ record evidence

0.94.0 removed the standalone workspace:create channel and fused workspace
creation into threads:launch with a new-workspace destination (one call creates
the workspace and opens its first thread). The prior static IPC scan had
false-positived workspace:create by matching it as a substring of the
workspace:created event.

Extend the release-aware adapt to the fused create+open contract:

- Add a release-aware workspaceCreate descriptor (legacy standalone
  workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen.
  mutationWorkspaceCreate dispatches to the legacy or fused-launch path.
- The fused path creates the workspace via threads:launch(new-workspace),
  reconstructs the workspace result from the authoritative DB row (unchanged
  downstream contract), and returns the fused thread id. The abstract evidence
  keys stay workspace:create and threads:openThread; the smoke adopts the fused
  thread rather than opening a second one and records BOTH guarantees (workspace
  created + thread opened) from the single launch, each body annotated with the
  real wire channel and the fusion.
- The fused launch provisions its worktree root asynchronously after returning,
  so add waitForWorkspaceProvisioned to poll until workspace_roots lands before
  asserting workspace identity and branch.

Harden the static IPC surface check against substring false-positives:

- scanFileForNeedles gains an exactToken mode requiring each needle to be
  bounded by non-channel-token bytes, so an event string can never satisfy a
  command-channel needle. The doctor's ipc_channel_strings_static uses it; the
  preload-bridge substring scan is unchanged. Streaming boundary handling keeps
  cross-chunk matches correct.

The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were
verified read-only against the live app.asar and then proven end to end by the
Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded
signed evidence for every 0.94.0 mutation plus write-denial confinement
(0.92.0/0.93.1 evidence preserved). The smoke targets only the registered
disposable project, never MAIN.

Tests cover the workspace-create contract, the fused launch result validation,
and the exact-token accept/reject behavior.
@jokim1
jokim1 merged commit c344c72 into lila-main Aug 20, 2026
13 checks passed
jokim1 added a commit that referenced this pull request Aug 25, 2026
…ads:launch contract (#9)

* feat(playbot-lane): release-aware thread-open contract for Playbot 0.94.0

Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open
IPC channels. "Open a thread" was restructured: the operation now flows
through threads:launch, which the app mints the thread id for and returns
the persisted thread ({workspace, thread, activate, createdWorkspace}),
with activation split into threads:setActiveThread. This inverts every
assumption the legacy lane relied on (caller-minted chat-N-N id, undefined
result, single-channel open).

Make the thread-open wire contract release-aware while keeping the abstract
operation and evidence key stable at threads:openThread:

- releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a
  threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and
  the app-minted-id, non-undefined-result contract.
- MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract
  operation keys so threads:launch is invokable; gatedInvoke takes an explicit
  wireChannel so the evidence gate still keys off threads:openThread.
- mutationOpenThread dispatches to the legacy path (unchanged) or a new launch
  path that consumes the app-minted result.thread.id via validateThreadLaunchResult
  and re-asserts the persisted row.
- The Phase-1 smoke records the real wire channel and app-minted id in the
  evidence body so the 0.94.0 overlay honestly reflects the launch contract.

The static IPC surface and payload shapes were verified read-only against the
live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility,
both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled
still awaits the Phase-1 disposable smoke as designed.

Tests and the fixture bundle extend to the 0.94.0 launch contract.

* feat(playbot-lane): fuse 0.94.0 workspace-create into threads:launch + record evidence

0.94.0 removed the standalone workspace:create channel and fused workspace
creation into threads:launch with a new-workspace destination (one call creates
the workspace and opens its first thread). The prior static IPC scan had
false-positived workspace:create by matching it as a substring of the
workspace:created event.

Extend the release-aware adapt to the fused create+open contract:

- Add a release-aware workspaceCreate descriptor (legacy standalone
  workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen.
  mutationWorkspaceCreate dispatches to the legacy or fused-launch path.
- The fused path creates the workspace via threads:launch(new-workspace),
  reconstructs the workspace result from the authoritative DB row (unchanged
  downstream contract), and returns the fused thread id. The abstract evidence
  keys stay workspace:create and threads:openThread; the smoke adopts the fused
  thread rather than opening a second one and records BOTH guarantees (workspace
  created + thread opened) from the single launch, each body annotated with the
  real wire channel and the fusion.
- The fused launch provisions its worktree root asynchronously after returning,
  so add waitForWorkspaceProvisioned to poll until workspace_roots lands before
  asserting workspace identity and branch.

Harden the static IPC surface check against substring false-positives:

- scanFileForNeedles gains an exactToken mode requiring each needle to be
  bounded by non-channel-token bytes, so an event string can never satisfy a
  command-channel needle. The doctor's ipc_channel_strings_static uses it; the
  preload-bridge substring scan is unchanged. Streaming boundary handling keeps
  cross-chunk matches correct.

The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were
verified read-only against the live app.asar and then proven end to end by the
Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded
signed evidence for every 0.94.0 mutation plus write-denial confinement
(0.92.0/0.93.1 evidence preserved). The smoke targets only the registered
disposable project, never MAIN.

Tests cover the workspace-create contract, the fused launch result validation,
and the exact-token accept/reject behavior.

* no-mistakes(review): reject explicit launch thread id, require provisioned path, dedupe payload

* no-mistakes(review): restore smoke-attested lanes script, defer fixes to follow-up

* no-mistakes(document): document 0.94.0 fused threads:launch contract and smoke evidence
jokim1 added a commit that referenced this pull request Aug 25, 2026
…ads:launch contract (#9)

* feat(playbot-lane): release-aware thread-open contract for Playbot 0.94.0

Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open
IPC channels. "Open a thread" was restructured: the operation now flows
through threads:launch, which the app mints the thread id for and returns
the persisted thread ({workspace, thread, activate, createdWorkspace}),
with activation split into threads:setActiveThread. This inverts every
assumption the legacy lane relied on (caller-minted chat-N-N id, undefined
result, single-channel open).

Make the thread-open wire contract release-aware while keeping the abstract
operation and evidence key stable at threads:openThread:

- releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a
  threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and
  the app-minted-id, non-undefined-result contract.
- MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract
  operation keys so threads:launch is invokable; gatedInvoke takes an explicit
  wireChannel so the evidence gate still keys off threads:openThread.
- mutationOpenThread dispatches to the legacy path (unchanged) or a new launch
  path that consumes the app-minted result.thread.id via validateThreadLaunchResult
  and re-asserts the persisted row.
- The Phase-1 smoke records the real wire channel and app-minted id in the
  evidence body so the 0.94.0 overlay honestly reflects the launch contract.

The static IPC surface and payload shapes were verified read-only against the
live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility,
both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled
still awaits the Phase-1 disposable smoke as designed.

Tests and the fixture bundle extend to the 0.94.0 launch contract.

* feat(playbot-lane): fuse 0.94.0 workspace-create into threads:launch + record evidence

0.94.0 removed the standalone workspace:create channel and fused workspace
creation into threads:launch with a new-workspace destination (one call creates
the workspace and opens its first thread). The prior static IPC scan had
false-positived workspace:create by matching it as a substring of the
workspace:created event.

Extend the release-aware adapt to the fused create+open contract:

- Add a release-aware workspaceCreate descriptor (legacy standalone
  workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen.
  mutationWorkspaceCreate dispatches to the legacy or fused-launch path.
- The fused path creates the workspace via threads:launch(new-workspace),
  reconstructs the workspace result from the authoritative DB row (unchanged
  downstream contract), and returns the fused thread id. The abstract evidence
  keys stay workspace:create and threads:openThread; the smoke adopts the fused
  thread rather than opening a second one and records BOTH guarantees (workspace
  created + thread opened) from the single launch, each body annotated with the
  real wire channel and the fusion.
- The fused launch provisions its worktree root asynchronously after returning,
  so add waitForWorkspaceProvisioned to poll until workspace_roots lands before
  asserting workspace identity and branch.

Harden the static IPC surface check against substring false-positives:

- scanFileForNeedles gains an exactToken mode requiring each needle to be
  bounded by non-channel-token bytes, so an event string can never satisfy a
  command-channel needle. The doctor's ipc_channel_strings_static uses it; the
  preload-bridge substring scan is unchanged. Streaming boundary handling keeps
  cross-chunk matches correct.

The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were
verified read-only against the live app.asar and then proven end to end by the
Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded
signed evidence for every 0.94.0 mutation plus write-denial confinement
(0.92.0/0.93.1 evidence preserved). The smoke targets only the registered
disposable project, never MAIN.

Tests cover the workspace-create contract, the fused launch result validation,
and the exact-token accept/reject behavior.

* no-mistakes(review): reject explicit launch thread id, require provisioned path, dedupe payload

* no-mistakes(review): restore smoke-attested lanes script, defer fixes to follow-up

* no-mistakes(document): document 0.94.0 fused threads:launch contract and smoke evidence
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant