From 572e5f11ac3231920b9ba48ea21dc60b73ec424d Mon Sep 17 00:00:00 2001 From: FlowmemoryAI <283694809+FlowmemoryAI@users.noreply.github.com> Date: Wed, 13 May 2026 16:02:17 -0500 Subject: [PATCH] Add FlowChain control plane API --- .../public/data/flowmemory-dashboard-v0.json | 16 +- docs/FLOWCHAIN_CONTROL_PLANE_API.md | 622 ++++++ docs/INDEXER_VERIFIER_MVP.md | 15 + .../dashboard/flowmemory-dashboard-v0.json | 16 +- .../devnet/control-plane-handoff.json | 331 +++ .../generated/devnet/dashboard-state.json | 146 +- .../generated/devnet/genesis-config.json | 15 + .../generated/devnet/indexer-handoff.json | 172 +- .../devnet/operator-key-references.json | 17 + .../launch-core/generated/devnet/state.json | 189 +- .../generated/devnet/verifier-handoff.json | 98 +- package-lock.json | 10 +- package.json | 11 +- services/control-plane/README.md | 75 + services/control-plane/package.json | 11 + services/control-plane/src/demo.ts | 28 + services/control-plane/src/errors.ts | 60 + services/control-plane/src/fixture-state.ts | 185 ++ services/control-plane/src/index.ts | 5 + services/control-plane/src/json-rpc.ts | 88 + services/control-plane/src/methods.ts | 1830 +++++++++++++++++ services/control-plane/src/server.ts | 110 + services/control-plane/src/smoke.ts | 101 + services/control-plane/src/types.ts | 118 ++ .../control-plane/test/control-plane.test.ts | 172 ++ .../flowmemory/src/generate-launch-core.ts | 2 +- 26 files changed, 4400 insertions(+), 43 deletions(-) create mode 100644 docs/FLOWCHAIN_CONTROL_PLANE_API.md create mode 100644 fixtures/launch-core/generated/devnet/control-plane-handoff.json create mode 100644 fixtures/launch-core/generated/devnet/genesis-config.json create mode 100644 fixtures/launch-core/generated/devnet/operator-key-references.json create mode 100644 services/control-plane/README.md create mode 100644 services/control-plane/package.json create mode 100644 services/control-plane/src/demo.ts create mode 100644 services/control-plane/src/errors.ts create mode 100644 services/control-plane/src/fixture-state.ts create mode 100644 services/control-plane/src/index.ts create mode 100644 services/control-plane/src/json-rpc.ts create mode 100644 services/control-plane/src/methods.ts create mode 100644 services/control-plane/src/server.ts create mode 100644 services/control-plane/src/smoke.ts create mode 100644 services/control-plane/src/types.ts create mode 100644 services/control-plane/test/control-plane.test.ts diff --git a/apps/dashboard/public/data/flowmemory-dashboard-v0.json b/apps/dashboard/public/data/flowmemory-dashboard-v0.json index 23277afe..213f3220 100644 --- a/apps/dashboard/public/data/flowmemory-dashboard-v0.json +++ b/apps/dashboard/public/data/flowmemory-dashboard-v0.json @@ -1993,12 +1993,12 @@ ], "devnetBlocks": [ { - "id": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "id": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "blockNumber": 1, - "blockHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "blockHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", - "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", - "receiptsRoot": "0x6962bd6dbf28c2361c1337c1d33d678a815cc4b961e0e50db5ccb401cc0fe076", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", + "receiptsRoot": "0x6393961b24d5db9f2984a39a98e827850b771f05c7f18005addb9a530af5a9b7", "timestamp": "2026-05-13T16:00:00.000Z", "observationCount": 8, "reportCount": 8, @@ -2015,11 +2015,11 @@ } }, { - "id": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", + "id": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", "blockNumber": 2, - "blockHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", - "parentHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "blockHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", + "parentHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", "receiptsRoot": "0xa0407b9a8a55106d549e0f19b92fceaa7f7a25697e94ebf8a1fa74af7b9168f4", "timestamp": "2026-05-13T16:00:01.000Z", "observationCount": 8, diff --git a/docs/FLOWCHAIN_CONTROL_PLANE_API.md b/docs/FLOWCHAIN_CONTROL_PLANE_API.md new file mode 100644 index 00000000..251a43e1 --- /dev/null +++ b/docs/FLOWCHAIN_CONTROL_PLANE_API.md @@ -0,0 +1,622 @@ +# FlowChain Local Control Plane API + +Status: local fixture-backed V0 contract. + +This document defines the local JSON-RPC 2.0 API for the FlowChain / FlowMemory control-plane. It gives dashboard, agent, verifier, and devnet tooling one deterministic read surface for FlowMemory objects. + +It is not a production RPC endpoint, public L1 API, hosted service, wallet API, bridge API, token API, or verifier economics surface. + +## Runtime Boundary + +The V0 service is implemented in: + +```text +services/control-plane/ +``` + +Commands: + +```powershell +npm run control-plane:test +npm run control-plane:demo +npm run control-plane:smoke +npm run control-plane:serve -- --host 127.0.0.1 --port 8675 +``` + +The service uses deterministic local files only. It does not require secrets, wallets, RPC URLs, private keys, API keys, or production services. + +Primary data sources: + +```text +fixtures/launch-core/flowmemory-launch-v0.json +fixtures/launch-core/generated/devnet/state.json +fixtures/launch-core/generated/devnet/indexer-handoff.json +fixtures/launch-core/generated/devnet/verifier-handoff.json +fixtures/launch-core/generated/devnet/control-plane-handoff.json +services/indexer/out/indexer-state.json +services/verifier/out/reports.json +services/verifier/fixtures/artifacts.json +fixtures/handoff/sample-txs.json +``` + +If the generated launch-core fixture is missing, the service rebuilds the in-memory view from indexer/verifier outputs or raw fixture receipts and artifact fixtures. This recovery path is local and read-only from the API caller perspective. + +## JSON-RPC Envelope + +Request: + +```json +{ + "jsonrpc": "2.0", + "id": "1", + "method": "chain_status", + "params": {} +} +``` + +Success: + +```json +{ + "jsonrpc": "2.0", + "id": "1", + "result": { + "schema": "flowmemory.control_plane.chain_status.v0" + } +} +``` + +Error: + +```json +{ + "jsonrpc": "2.0", + "id": "1", + "error": { + "code": -32602, + "message": "rootfield_get requires one of: rootfieldId", + "data": { + "schema": "flowmemory.control_plane.error.v0", + "reasonCode": "params.invalid", + "localOnly": true + } + } +} +``` + +Error codes: + +| Code | Meaning | +| --- | --- | +| `-32700` | Parse error in HTTP server payload. | +| `-32600` | Invalid JSON-RPC request. | +| `-32601` | Unknown method. | +| `-32602` | Missing or invalid params. | +| `-32603` | Internal local control-plane error. | +| `-32004` | Requested local object was not found. | + +## Methods + +### `health` + +Params: none. + +Returns local service readiness, source health, core object counters, and `localOnly: true`. + +HTTP health is also available: + +```text +GET /health +``` + +### `chain_status` + +Params: none. + +Returns local stack status, fixture source status, block counters, object counters, capabilities, and limitations. + +Key result fields: + +```json +{ + "schema": "flowmemory.control_plane.chain_status.v0", + "chainId": "flowmemory-local-alpha", + "environment": "local-devnet-fixture", + "source": "fixture", + "currentBlock": "123461", + "finalizedBlock": "123457", + "localOnly": true +} +``` + +### `devnet_state` + +Params: + +```json +{ + "includeBlocks": false +} +``` + +Returns local no-value devnet state, handoff summaries, rootfield counts, work receipt counts, report counts, and optional block data. + +### `block_list` + +Params: + +```json +{ + "source": "local-devnet", + "includeTransactions": false, + "limit": 50 +} +``` + +All params are optional. Returns local devnet blocks and indexer-observed FlowPulse block groups. + +### `block_get` + +Params: one of: + +```json +{ "blockNumber": "1", "includeTransactions": true } +``` + +```json +{ "blockHash": "0x..." } +``` + +### `transaction_list` + +Params: + +```json +{ + "blockNumber": "1", + "rootfieldId": "0x...", + "status": "finalized", + "source": "flowpulse-indexer", + "limit": 50 +} +``` + +All params are optional. Returns local devnet transactions plus indexer transaction groups derived from `txHash`. + +### `transaction_get` + +Params: one of: + +```json +{ "txId": "0x..." } +``` + +```json +{ "txHash": "0x..." } +``` + +### `rootfield_get` + +Params: + +```json +{ + "rootfieldId": "0x..." +} +``` + +Returns the Flow Memory `RootfieldBundle`, matching local devnet rootfield when present, memory cell id, agent view id, and provenance. + +### `rootfield_list` + +Params: + +```json +{ + "status": "verified", + "limit": 50 +} +``` + +All params are optional. Returns launch-core and local devnet rootfield rows. + +### `artifact_get` + +Params: one of: + +```json +{ "uri": "fixture://root-commit-valid" } +``` + +```json +{ "artifactId": "artifact:demo:001" } +``` + +```json +{ "commitment": "0x..." } +``` + +Returns fixture artifact resolver data or local devnet artifact commitment records. V0 does not fetch arbitrary HTTP or IPFS content. + +### `artifact_availability_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "available_fixture", + "limit": 50 +} +``` + +All params are optional. Returns local artifact commitments, native availability proofs when handoff files contain them, and verifier fixture availability rows. + +### `artifact_availability_get` + +Params: one of: + +```json +{ "availabilityId": "0x..." } +``` + +```json +{ "artifactId": "artifact:demo:001" } +``` + +```json +{ "commitment": "0x..." } +``` + +```json +{ "uri": "fixture://root-commit-valid" } +``` + +### `receipt_get` + +Params: one of: + +```json +{ "receiptId": "0x..." } +``` + +```json +{ "observationId": "0x..." } +``` + +```json +{ "reportId": "0x..." } +``` + +Returns a `MemoryReceipt` plus linked signal, transition, verifier report, and provenance when available. It also supports local devnet `workReceipts`. + +### `receipt_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "verified", + "limit": 50 +} +``` + +All params are optional. `limit` must be between `1` and `100`. + +Returns deterministic local receipt rows. + +### `work_receipt_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "verified", + "limit": 50 +} +``` + +All params are optional. Returns local devnet work receipts and launch-core MemoryReceipt compatibility rows. + +### `work_receipt_get` + +Params: one of: + +```json +{ "workReceiptId": "receipt:demo:001" } +``` + +```json +{ "receiptId": "0x..." } +``` + +```json +{ "observationId": "0x..." } +``` + +```json +{ "reportId": "0x..." } +``` + +### `verifier_module_list` + +Params: + +```json +{ + "status": "available_fixture", + "limit": 50 +} +``` + +All params are optional. Returns native verifier modules if present, plus stable projected modules from verifier report resolver policy and local devnet verifier ids. + +### `verifier_module_get` + +Params: one of: + +```json +{ "moduleId": "0x..." } +``` + +```json +{ "verifierId": "verifier:local-demo" } +``` + +```json +{ "resolverPolicyId": "flowmemory.resolver.policy.v0.fixture" } +``` + +### `verifier_report_get` + +Params: one of: + +```json +{ "reportId": "0x..." } +``` + +```json +{ "observationId": "0x..." } +``` + +Returns the deterministic verifier report, linked memory receipt, and provenance. It also supports local devnet verifier reports. + +### `verifier_report_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "valid", + "limit": 50 +} +``` + +All params are optional. `limit` must be between `1` and `100`. + +### `memory_cell_get` + +Params: one of: + +```json +{ "rootfieldId": "0x..." } +``` + +```json +{ "memoryCellId": "0x..." } +``` + +When local devnet handoff files contain `memoryCells`, this method returns that record. Otherwise it returns a stable projected memory-cell shape built from `RootfieldBundle` and `AgentMemoryView` fixtures, with an explicit `extensionPoint` field. + +### `memory_cell_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "verified", + "limit": 50 +} +``` + +All params are optional. + +### `agent_get` + +Params: one of: + +```json +{ "rootfieldId": "0x..." } +``` + +```json +{ "viewId": "0x..." } +``` + +```json +{ "agentId": "0x..." } +``` + +Returns an `AgentMemoryView` and linked `RootfieldBundle`. + +### `agent_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "verified", + "limit": 50 +} +``` + +All params are optional. + +### `model_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "local-placeholder", + "limit": 50 +} +``` + +Returns native model passports when local devnet handoff files contain them, plus explicit projected rows from launch-core agent memory views so the workbench model API remains stable. + +### `model_get` + +Params: one of: + +```json +{ "modelId": "0x..." } +``` + +```json +{ "rootfieldId": "0x..." } +``` + +### `challenge_get` + +Params: one of: + +```json +{ "targetId": "0x..." } +``` + +```json +{ "receiptId": "0x..." } +``` + +```json +{ "reportId": "0x..." } +``` + +When local devnet handoff files contain `challenges`, this method returns that record. Otherwise it returns a stable placeholder object with `status: "not_opened"` for known targets, preserving the future challenge API shape without implying a live challenge system. + +### `challenge_list` + +Params: + +```json +{ + "status": "open", + "limit": 50 +} +``` + +All params are optional. Returns native challenge handoff rows when present. + +### `finality_get` + +Params: one of: + +```json +{ "objectId": "0x..." } +``` + +```json +{ "rootfieldId": "0x..." } +``` + +```json +{ "receiptId": "0x..." } +``` + +```json +{ "reportId": "0x..." } +``` + +Returns local fixture finality only. When local devnet handoff files contain `finalityReceipts`, the result links that record. Result statuses include: + +- `local-finalized` +- `local-pending` +- `local-rejected` +- `local-unsupported` +- `reorged` + +### `finality_list` + +Params: + +```json +{ + "rootfieldId": "0x...", + "status": "local-finalized", + "limit": 50 +} +``` + +All params are optional. Returns native finality receipts when present and projected local finality rows for launch-core receipts. + +### `provenance_get` + +Params: one of: + +```json +{ "objectId": "0x..." } +``` + +```json +{ "receiptId": "0x..." } +``` + +```json +{ "reportId": "0x..." } +``` + +```json +{ "rootfieldId": "0x..." } +``` + +```json +{ "uri": "fixture://root-commit-valid" } +``` + +Returns local source files and linked IDs for receipts, reports, memory signals, transitions, rootfields, agent views, and artifacts. + +### `raw_json_get` + +Params: + +```json +{ + "source": "launchCore" +} +``` + +Allowed `source` values: + +- `launchCore` +- `indexer` +- `verifier` +- `artifacts` +- `devnet` +- `devnetIndexerHandoff` +- `devnetVerifierHandoff` +- `devnetControlPlaneHandoff` +- `txFixtures` + +Returns the raw loaded local JSON object for dashboard/workbench debug views. It does not accept arbitrary filesystem paths. + +## Dashboard Consumption Notes + +Dashboard agents should prefer: + +1. `health` and `chain_status` for source health and global counters. +2. `block_list` and `transaction_list` for chain/devnet tables. +3. `rootfield_list` and `rootfield_get` for Rootfield detail. +4. `work_receipt_list`, `receipt_list`, `verifier_module_list`, and `verifier_report_list` for lifecycle tables. +5. `receipt_get`, `work_receipt_get`, `verifier_report_get`, and `provenance_get` for detail drawers. +6. `artifact_availability_list`, `memory_cell_list`, `agent_list`, and `model_list` for dashboard/workbench panels. +7. `challenge_get`, `challenge_list`, `finality_get`, and `finality_list` for local fixture challenge/finality labels. +8. `raw_json_get` for raw JSON inspection. + +The API is intentionally read-only for V0. Submit, challenge, wallet, live indexing, and production settlement methods require separate scoped work. diff --git a/docs/INDEXER_VERIFIER_MVP.md b/docs/INDEXER_VERIFIER_MVP.md index e8005562..57027931 100644 --- a/docs/INDEXER_VERIFIER_MVP.md +++ b/docs/INDEXER_VERIFIER_MVP.md @@ -268,6 +268,21 @@ Those are future protocol decisions, not part of this local package. - Receipt fixtures: `services/indexer/fixtures/flowpulse-receipts.json` - Artifact fixtures: `services/verifier/fixtures/artifacts.json` +## Local Control Plane + +`services/control-plane` exposes the fixture-backed FlowChain / FlowMemory JSON-RPC 2.0 read API documented in `docs/FLOWCHAIN_CONTROL_PLANE_API.md`. + +Run: + +```powershell +npm run control-plane:test +npm run control-plane:demo +npm run control-plane:smoke +npm run control-plane:serve -- --host 127.0.0.1 --port 8675 +``` + +The control-plane reads committed launch-core, indexer, verifier, artifact, transaction fixture, and local devnet handoff files first. If the generated launch-core fixture is missing, it rebuilds an in-memory view from deterministic indexer/verifier fixtures. It exposes read methods for health, chain status, blocks, transactions, rootfields, agents, models, work receipts, artifact availability, verifier modules, verifier reports, memory cells, challenges, finality, provenance, and raw JSON. It does not fetch production RPC data, store secrets, or make production API claims. + ## Open Questions - What exact artifact canonicalization format should produce `artifactCommitment`? diff --git a/fixtures/dashboard/flowmemory-dashboard-v0.json b/fixtures/dashboard/flowmemory-dashboard-v0.json index 23277afe..213f3220 100644 --- a/fixtures/dashboard/flowmemory-dashboard-v0.json +++ b/fixtures/dashboard/flowmemory-dashboard-v0.json @@ -1993,12 +1993,12 @@ ], "devnetBlocks": [ { - "id": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "id": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "blockNumber": 1, - "blockHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "blockHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", - "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", - "receiptsRoot": "0x6962bd6dbf28c2361c1337c1d33d678a815cc4b961e0e50db5ccb401cc0fe076", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", + "receiptsRoot": "0x6393961b24d5db9f2984a39a98e827850b771f05c7f18005addb9a530af5a9b7", "timestamp": "2026-05-13T16:00:00.000Z", "observationCount": 8, "reportCount": 8, @@ -2015,11 +2015,11 @@ } }, { - "id": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", + "id": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", "blockNumber": 2, - "blockHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", - "parentHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "blockHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", + "parentHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", "receiptsRoot": "0xa0407b9a8a55106d549e0f19b92fceaa7f7a25697e94ebf8a1fa74af7b9168f4", "timestamp": "2026-05-13T16:00:01.000Z", "observationCount": 8, diff --git a/fixtures/launch-core/generated/devnet/control-plane-handoff.json b/fixtures/launch-core/generated/devnet/control-plane-handoff.json new file mode 100644 index 00000000..cf32711c --- /dev/null +++ b/fixtures/launch-core/generated/devnet/control-plane-handoff.json @@ -0,0 +1,331 @@ +{ + "blocks": [ + { + "blockHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", + "blockNumber": 1, + "logicalTime": 1778688000, + "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "receipts": [ + { + "error": null, + "status": "applied", + "txId": "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189" + }, + { + "error": null, + "status": "applied", + "txId": "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25" + }, + { + "error": null, + "status": "applied", + "txId": "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f" + }, + { + "error": null, + "status": "applied", + "txId": "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359" + }, + { + "error": null, + "status": "applied", + "txId": "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2" + }, + { + "error": null, + "status": "applied", + "txId": "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d" + }, + { + "error": null, + "status": "applied", + "txId": "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2" + }, + { + "error": null, + "status": "applied", + "txId": "0x73b81134901c2ce13e575f161d82a404c6f7cd1ef2e8ee17beb6697062175c46" + }, + { + "error": null, + "status": "applied", + "txId": "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa" + }, + { + "error": null, + "status": "applied", + "txId": "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b" + }, + { + "error": null, + "status": "applied", + "txId": "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad" + }, + { + "error": null, + "status": "applied", + "txId": "0xf32e17d973089ae20766e2c6ec07d1511ddecd3f79803f6146a90df971ca814f" + }, + { + "error": null, + "status": "applied", + "txId": "0x514f9ef68c09de5a4dc80611d661d07a4ea3a4fae6000a43a25864489f354a81" + } + ], + "schema": "flowmemory.local_devnet.block.v0", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", + "txIds": [ + "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189", + "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25", + "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f", + "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359", + "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", + "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d", + "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2", + "0x73b81134901c2ce13e575f161d82a404c6f7cd1ef2e8ee17beb6697062175c46", + "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa", + "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b", + "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad", + "0xf32e17d973089ae20766e2c6ec07d1511ddecd3f79803f6146a90df971ca814f", + "0x514f9ef68c09de5a4dc80611d661d07a4ea3a4fae6000a43a25864489f354a81" + ] + }, + { + "blockHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", + "blockNumber": 2, + "logicalTime": 1778688001, + "parentHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", + "receipts": [ + { + "error": null, + "status": "applied", + "txId": "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" + } + ], + "schema": "flowmemory.local_devnet.block.v0", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", + "txIds": [ + "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" + ] + } + ], + "chainId": "flowmemory-local-devnet-v0", + "genesisConfig": { + "blockTimeSeconds": 1, + "chainId": "flowmemory-local-devnet-v0", + "consensus": "single-process deterministic local block production", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "genesisLogicalTime": 1778688000, + "networkId": "flowmemory-private-local", + "noValue": true, + "operatorKeyReferenceId": "operator-key:local-devnet:alpha", + "schema": "flowmemory.local_devnet.config.v0" + }, + "latestBlock": { + "blockHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", + "blockNumber": 2, + "logicalTime": 1778688001, + "parentHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", + "receipts": [ + { + "error": null, + "status": "applied", + "txId": "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" + } + ], + "schema": "flowmemory.local_devnet.block.v0", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", + "txIds": [ + "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" + ] + }, + "mapRoots": { + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "baseAnchorRoot": "0xb3d8ee8eed1f4c01dfe6e20c13a080e43c4fb9ff3703b1401ecd265fb326fd9e", + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", + "importedObservationRoot": "0x99cb1b939d5a09f800f72e4c5a2b92988571126e1f6f93549f4893b3f7de7880", + "importedVerifierReportRoot": "0x6070b1015f000dd509c7b276d2ad68d8a9d188ef1a961c2f573346eb75ea5ad7", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" + }, + "objects": { + "agentAccounts": { + "agent:demo:alpha": { + "active": true, + "agentId": "agent:demo:alpha", + "controller": "operator:local-demo", + "memoryRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "metadataHash": "0x62348f90982ee812382bb97c0c6930e0e80583d0eeb40b4ae3c3395ca44cec1c", + "modelPassportId": "model:demo:local-alpha" + } + }, + "artifactAvailabilityProofs": { + "availability:demo:001": { + "artifactId": "artifact:demo:001", + "checkedAtBlock": 1, + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "proofDigest": "0xf5826d75505fd87288a6219067d4febfd25226cf6e70c85288317770b02d6527", + "proofId": "availability:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "status": "available", + "storageBackend": "fixture-local" + } + }, + "artifactCommitments": { + "artifact:demo:001": { + "artifactId": "artifact:demo:001", + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "rootfieldId": "rootfield:demo:alpha", + "uriHint": "fixture://artifact/demo/001" + } + }, + "baseAnchors": { + "0x632509a4e0eb4812f0817795eae3fb1f3465f61c08035d1eea767df078f31bf1": { + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "anchorId": "0x632509a4e0eb4812f0817795eae3fb1f3465f61c08035d1eea767df078f31bf1", + "appchainChainId": "flowmemory-local-devnet-v0", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "blockRangeEnd": 1, + "blockRangeStart": 1, + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", + "finalityStatus": "local-placeholder", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", + "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" + } + }, + "challenges": { + "challenge:demo:001": { + "challengeId": "challenge:demo:001", + "challenger": "reviewer:local-demo", + "evidenceHash": "0xcc0312e21517151c7422f9b7c0c2ec611388eadfd89a07f6199adf617c4b461c", + "openedAtBlock": 1, + "reasonCode": "local-review", + "receiptId": "receipt:demo:001", + "resolution": "dismissed", + "resolvedAtBlock": 1, + "status": "resolved" + } + }, + "finalityReceipts": { + "finality:demo:001": { + "challengeCount": 1, + "finalityReceiptId": "finality:demo:001", + "finalityStatus": "finalized", + "finalizedAtBlock": 1, + "finalizedBy": "operator:local-demo", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "stateRoot": "0xd1aa623e1b368f3bdf9bd45ae97eef8e35ba1a4cb5b6ac9fbc6c1ab29570cdcc" + } + }, + "memoryCells": { + "memory:demo:agent-alpha:core": { + "agentId": "agent:demo:alpha", + "currentRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "memoryCellId": "memory:demo:agent-alpha:core", + "memoryDeltaRoot": "0x6243c81a1bc56c93f5cebf081d9b69a8d7839bcd33384fe1d00c275d50f78de6", + "parentRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "rootfieldId": "rootfield:demo:alpha", + "sourceReceiptId": "receipt:demo:001", + "status": "active", + "updateCount": 1, + "updatedAtBlock": 1 + } + }, + "modelPassports": { + "model:demo:local-alpha": { + "active": true, + "issuer": "operator:local-demo", + "metadataHash": "0x18416ecbdfcad838d0008b3692e1505886a84e9c45fc6e029b29a3456befc234", + "modelFamily": "local-alpha-fixture-model", + "modelHash": "0xdeffd2a3fbb28dcc4dbb34172d082586bd0d63e1c76ab32296911946e6a7d0eb", + "modelPassportId": "model:demo:local-alpha" + } + }, + "rootfields": { + "rootfield:demo:alpha": { + "active": true, + "latestRoot": "0xbdb66f777635a2426a834652f8efee40db4f3e0b9ddd2af15f15fd065a7fe4f4", + "metadataHash": "0x514006e494877d3d6a69848ed6264b152ebe6b73b1112d8ff1b9b48860509a2f", + "owner": "operator:local-demo", + "pulseCount": 2, + "rootCount": 1, + "rootfieldId": "rootfield:demo:alpha", + "schemaHash": "0x5909a6dc30ffe1fcd89eebc118f6d2096c4d4c3ccdcc851dc0e4386fe997c6d7" + } + }, + "verifierModules": { + "verifier:local-demo": { + "active": true, + "metadataHash": "0x965eb2a0dfe118a94ded4ef151bf0fd970cd77350cd88d1b758b23fe0ffe2d14", + "moduleHash": "0x91830f85b951a7e7a1d99f48270faa05769561bd1f7ae34b783d759a35e833be", + "operator": "operator:local-demo", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0", + "verifierId": "verifier:local-demo" + } + }, + "verifierReports": { + "report:demo:001": { + "reasonCodes": [], + "receiptId": "receipt:demo:001", + "reportDigest": "0xe75619ea62e7a6d9593debe0123d366ae0f0104cff86d9a69391fb5c1e074f4c", + "reportId": "report:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "status": "verified", + "verifierId": "verifier:local-demo" + } + }, + "workReceipts": { + "receipt:demo:001": { + "artifactCommitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "inputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "outputRoot": "0xbdb66f777635a2426a834652f8efee40db4f3e0b9ddd2af15f15fd065a7fe4f4", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0", + "workerId": "worker:local-demo" + } + } + }, + "operatorKeyReferences": { + "operator-key:local-devnet:alpha": { + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "keyReferenceId": "operator-key:local-devnet:alpha", + "operatorId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "publicKeyHint": "local fixture boundary; no public key registry is implemented", + "schema": "flowmemory.local_devnet.operator_key_reference.v0", + "secretMaterialBoundary": "no signing secret material is stored in devnet state or handoff output", + "signatureScheme": "eip712-secp256k1-fixture-digest-only", + "verifierKeyId": "0xeaead587bf631e8926cf1a88ea5404f2211a339b77be7b9ffc08be420ce85551", + "verifierSetRoot": "0xbecddfb2cac22961206303e4f1255f58786e62503fbd54d875be915b68cc9635", + "workerKeyId": "0xc4cc0a4e778d201e59a442e969596ef8758fa62eb72c7ae4cb468c5493fc924d" + } + }, + "pendingTxs": [], + "schema": "flowmemory.control_plane_handoff.local_devnet.v0", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9" +} diff --git a/fixtures/launch-core/generated/devnet/dashboard-state.json b/fixtures/launch-core/generated/devnet/dashboard-state.json index ef04135f..e472c355 100644 --- a/fixtures/launch-core/generated/devnet/dashboard-state.json +++ b/fixtures/launch-core/generated/devnet/dashboard-state.json @@ -1,4 +1,26 @@ { + "agentAccounts": { + "agent:demo:alpha": { + "active": true, + "agentId": "agent:demo:alpha", + "controller": "operator:local-demo", + "memoryRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "metadataHash": "0x62348f90982ee812382bb97c0c6930e0e80583d0eeb40b4ae3c3395ca44cec1c", + "modelPassportId": "model:demo:local-alpha" + } + }, + "artifactAvailabilityProofs": { + "availability:demo:001": { + "artifactId": "artifact:demo:001", + "checkedAtBlock": 1, + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "proofDigest": "0xf5826d75505fd87288a6219067d4febfd25226cf6e70c85288317770b02d6527", + "proofId": "availability:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "status": "available", + "storageBackend": "fixture-local" + } + }, "artifactCommitments": { "artifact:demo:001": { "artifactId": "artifact:demo:001", @@ -8,21 +30,127 @@ } }, "baseAnchors": { - "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7": { - "anchorId": "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7", + "0x632509a4e0eb4812f0817795eae3fb1f3465f61c08035d1eea767df078f31bf1": { + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "anchorId": "0x632509a4e0eb4812f0817795eae3fb1f3465f61c08035d1eea767df078f31bf1", "appchainChainId": "flowmemory-local-devnet-v0", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", "blockRangeEnd": 1, "blockRangeStart": 1, + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", "finalityStatus": "local-placeholder", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", - "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" } }, "blockHeight": 2, + "challenges": { + "challenge:demo:001": { + "challengeId": "challenge:demo:001", + "challenger": "reviewer:local-demo", + "evidenceHash": "0xcc0312e21517151c7422f9b7c0c2ec611388eadfd89a07f6199adf617c4b461c", + "openedAtBlock": 1, + "reasonCode": "local-review", + "receiptId": "receipt:demo:001", + "resolution": "dismissed", + "resolvedAtBlock": 1, + "status": "resolved" + } + }, + "finalityReceipts": { + "finality:demo:001": { + "challengeCount": 1, + "finalityReceiptId": "finality:demo:001", + "finalityStatus": "finalized", + "finalizedAtBlock": 1, + "finalizedBy": "operator:local-demo", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "stateRoot": "0xd1aa623e1b368f3bdf9bd45ae97eef8e35ba1a4cb5b6ac9fbc6c1ab29570cdcc" + } + }, + "genesisConfig": { + "blockTimeSeconds": 1, + "chainId": "flowmemory-local-devnet-v0", + "consensus": "single-process deterministic local block production", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "genesisLogicalTime": 1778688000, + "networkId": "flowmemory-private-local", + "noValue": true, + "operatorKeyReferenceId": "operator-key:local-devnet:alpha", + "schema": "flowmemory.local_devnet.config.v0" + }, + "mapRoots": { + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "baseAnchorRoot": "0xb3d8ee8eed1f4c01dfe6e20c13a080e43c4fb9ff3703b1401ecd265fb326fd9e", + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", + "importedObservationRoot": "0x99cb1b939d5a09f800f72e4c5a2b92988571126e1f6f93549f4893b3f7de7880", + "importedVerifierReportRoot": "0x6070b1015f000dd509c7b276d2ad68d8a9d188ef1a961c2f573346eb75ea5ad7", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" + }, + "memoryCells": { + "memory:demo:agent-alpha:core": { + "agentId": "agent:demo:alpha", + "currentRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "memoryCellId": "memory:demo:agent-alpha:core", + "memoryDeltaRoot": "0x6243c81a1bc56c93f5cebf081d9b69a8d7839bcd33384fe1d00c275d50f78de6", + "parentRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "rootfieldId": "rootfield:demo:alpha", + "sourceReceiptId": "receipt:demo:001", + "status": "active", + "updateCount": 1, + "updatedAtBlock": 1 + } + }, + "modelPassports": { + "model:demo:local-alpha": { + "active": true, + "issuer": "operator:local-demo", + "metadataHash": "0x18416ecbdfcad838d0008b3692e1505886a84e9c45fc6e029b29a3456befc234", + "modelFamily": "local-alpha-fixture-model", + "modelHash": "0xdeffd2a3fbb28dcc4dbb34172d082586bd0d63e1c76ab32296911946e6a7d0eb", + "modelPassportId": "model:demo:local-alpha" + } + }, + "operatorKeyReferences": { + "operator-key:local-devnet:alpha": { + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "keyReferenceId": "operator-key:local-devnet:alpha", + "operatorId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "publicKeyHint": "local fixture boundary; no public key registry is implemented", + "schema": "flowmemory.local_devnet.operator_key_reference.v0", + "secretMaterialBoundary": "no signing secret material is stored in devnet state or handoff output", + "signatureScheme": "eip712-secp256k1-fixture-digest-only", + "verifierKeyId": "0xeaead587bf631e8926cf1a88ea5404f2211a339b77be7b9ffc08be420ce85551", + "verifierSetRoot": "0xbecddfb2cac22961206303e4f1255f58786e62503fbd54d875be915b68cc9635", + "workerKeyId": "0xc4cc0a4e778d201e59a442e969596ef8758fa62eb72c7ae4cb468c5493fc924d" + } + }, "rootfields": { "rootfield:demo:alpha": { "active": true, @@ -36,7 +164,17 @@ } }, "schema": "flowmemory.dashboard_state.local_devnet.v0", - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", + "verifierModules": { + "verifier:local-demo": { + "active": true, + "metadataHash": "0x965eb2a0dfe118a94ded4ef151bf0fd970cd77350cd88d1b758b23fe0ffe2d14", + "moduleHash": "0x91830f85b951a7e7a1d99f48270faa05769561bd1f7ae34b783d759a35e833be", + "operator": "operator:local-demo", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0", + "verifierId": "verifier:local-demo" + } + }, "verifierReports": { "report:demo:001": { "reasonCodes": [], diff --git a/fixtures/launch-core/generated/devnet/genesis-config.json b/fixtures/launch-core/generated/devnet/genesis-config.json new file mode 100644 index 00000000..f702b065 --- /dev/null +++ b/fixtures/launch-core/generated/devnet/genesis-config.json @@ -0,0 +1,15 @@ +{ + "schema": "flowmemory.local_devnet.config.v0", + "chainId": "flowmemory-local-devnet-v0", + "networkId": "flowmemory-private-local", + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "genesisLogicalTime": 1778688000, + "blockTimeSeconds": 1, + "operatorKeyReferenceId": "operator-key:local-devnet:alpha", + "noValue": true, + "consensus": "single-process deterministic local block production", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ] +} diff --git a/fixtures/launch-core/generated/devnet/indexer-handoff.json b/fixtures/launch-core/generated/devnet/indexer-handoff.json index e8a78cfd..4c8b40c0 100644 --- a/fixtures/launch-core/generated/devnet/indexer-handoff.json +++ b/fixtures/launch-core/generated/devnet/indexer-handoff.json @@ -1,7 +1,29 @@ { + "agentAccounts": { + "agent:demo:alpha": { + "active": true, + "agentId": "agent:demo:alpha", + "controller": "operator:local-demo", + "memoryRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "metadataHash": "0x62348f90982ee812382bb97c0c6930e0e80583d0eeb40b4ae3c3395ca44cec1c", + "modelPassportId": "model:demo:local-alpha" + } + }, + "artifactAvailabilityProofs": { + "availability:demo:001": { + "artifactId": "artifact:demo:001", + "checkedAtBlock": 1, + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "proofDigest": "0xf5826d75505fd87288a6219067d4febfd25226cf6e70c85288317770b02d6527", + "proofId": "availability:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "status": "available", + "storageBackend": "fixture-local" + } + }, "blocks": [ { - "blockHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "blockHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "blockNumber": 1, "logicalTime": 1778688000, "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", @@ -11,11 +33,31 @@ "status": "applied", "txId": "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189" }, + { + "error": null, + "status": "applied", + "txId": "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25" + }, + { + "error": null, + "status": "applied", + "txId": "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f" + }, + { + "error": null, + "status": "applied", + "txId": "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359" + }, { "error": null, "status": "applied", "txId": "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2" }, + { + "error": null, + "status": "applied", + "txId": "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d" + }, { "error": null, "status": "applied", @@ -30,23 +72,51 @@ "error": null, "status": "applied", "txId": "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa" + }, + { + "error": null, + "status": "applied", + "txId": "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b" + }, + { + "error": null, + "status": "applied", + "txId": "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad" + }, + { + "error": null, + "status": "applied", + "txId": "0xf32e17d973089ae20766e2c6ec07d1511ddecd3f79803f6146a90df971ca814f" + }, + { + "error": null, + "status": "applied", + "txId": "0x514f9ef68c09de5a4dc80611d661d07a4ea3a4fae6000a43a25864489f354a81" } ], "schema": "flowmemory.local_devnet.block.v0", - "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", "txIds": [ "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189", + "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25", + "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f", + "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359", "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", + "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d", "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2", "0x73b81134901c2ce13e575f161d82a404c6f7cd1ef2e8ee17beb6697062175c46", - "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa" + "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa", + "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b", + "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad", + "0xf32e17d973089ae20766e2c6ec07d1511ddecd3f79803f6146a90df971ca814f", + "0x514f9ef68c09de5a4dc80611d661d07a4ea3a4fae6000a43a25864489f354a81" ] }, { - "blockHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", + "blockHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", "blockNumber": 2, "logicalTime": 1778688001, - "parentHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "parentHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "receipts": [ { "error": null, @@ -55,13 +125,101 @@ } ], "schema": "flowmemory.local_devnet.block.v0", - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", "txIds": [ "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" ] } ], + "challenges": { + "challenge:demo:001": { + "challengeId": "challenge:demo:001", + "challenger": "reviewer:local-demo", + "evidenceHash": "0xcc0312e21517151c7422f9b7c0c2ec611388eadfd89a07f6199adf617c4b461c", + "openedAtBlock": 1, + "reasonCode": "local-review", + "receiptId": "receipt:demo:001", + "resolution": "dismissed", + "resolvedAtBlock": 1, + "status": "resolved" + } + }, + "finalityReceipts": { + "finality:demo:001": { + "challengeCount": 1, + "finalityReceiptId": "finality:demo:001", + "finalityStatus": "finalized", + "finalizedAtBlock": 1, + "finalizedBy": "operator:local-demo", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "stateRoot": "0xd1aa623e1b368f3bdf9bd45ae97eef8e35ba1a4cb5b6ac9fbc6c1ab29570cdcc" + } + }, + "genesisConfig": { + "blockTimeSeconds": 1, + "chainId": "flowmemory-local-devnet-v0", + "consensus": "single-process deterministic local block production", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "genesisLogicalTime": 1778688000, + "networkId": "flowmemory-private-local", + "noValue": true, + "operatorKeyReferenceId": "operator-key:local-devnet:alpha", + "schema": "flowmemory.local_devnet.config.v0" + }, "importedObservations": {}, + "mapRoots": { + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "baseAnchorRoot": "0xb3d8ee8eed1f4c01dfe6e20c13a080e43c4fb9ff3703b1401ecd265fb326fd9e", + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", + "importedObservationRoot": "0x99cb1b939d5a09f800f72e4c5a2b92988571126e1f6f93549f4893b3f7de7880", + "importedVerifierReportRoot": "0x6070b1015f000dd509c7b276d2ad68d8a9d188ef1a961c2f573346eb75ea5ad7", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" + }, + "memoryCells": { + "memory:demo:agent-alpha:core": { + "agentId": "agent:demo:alpha", + "currentRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "memoryCellId": "memory:demo:agent-alpha:core", + "memoryDeltaRoot": "0x6243c81a1bc56c93f5cebf081d9b69a8d7839bcd33384fe1d00c275d50f78de6", + "parentRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "rootfieldId": "rootfield:demo:alpha", + "sourceReceiptId": "receipt:demo:001", + "status": "active", + "updateCount": 1, + "updatedAtBlock": 1 + } + }, + "operatorKeyReferences": { + "operator-key:local-devnet:alpha": { + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "keyReferenceId": "operator-key:local-devnet:alpha", + "operatorId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "publicKeyHint": "local fixture boundary; no public key registry is implemented", + "schema": "flowmemory.local_devnet.operator_key_reference.v0", + "secretMaterialBoundary": "no signing secret material is stored in devnet state or handoff output", + "signatureScheme": "eip712-secp256k1-fixture-digest-only", + "verifierKeyId": "0xeaead587bf631e8926cf1a88ea5404f2211a339b77be7b9ffc08be420ce85551", + "verifierSetRoot": "0xbecddfb2cac22961206303e4f1255f58786e62503fbd54d875be915b68cc9635", + "workerKeyId": "0xc4cc0a4e778d201e59a442e969596ef8758fa62eb72c7ae4cb468c5493fc924d" + } + }, "schema": "flowmemory.indexer_handoff.local_devnet.v0", - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9" + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9" } diff --git a/fixtures/launch-core/generated/devnet/operator-key-references.json b/fixtures/launch-core/generated/devnet/operator-key-references.json new file mode 100644 index 00000000..6a981ca3 --- /dev/null +++ b/fixtures/launch-core/generated/devnet/operator-key-references.json @@ -0,0 +1,17 @@ +{ + "operator-key:local-devnet:alpha": { + "schema": "flowmemory.local_devnet.operator_key_reference.v0", + "keyReferenceId": "operator-key:local-devnet:alpha", + "operatorId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "workerKeyId": "0xc4cc0a4e778d201e59a442e969596ef8758fa62eb72c7ae4cb468c5493fc924d", + "verifierKeyId": "0xeaead587bf631e8926cf1a88ea5404f2211a339b77be7b9ffc08be420ce85551", + "verifierSetRoot": "0xbecddfb2cac22961206303e4f1255f58786e62503fbd54d875be915b68cc9635", + "signatureScheme": "eip712-secp256k1-fixture-digest-only", + "publicKeyHint": "local fixture boundary; no public key registry is implemented", + "secretMaterialBoundary": "no signing secret material is stored in devnet state or handoff output", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ] + } +} diff --git a/fixtures/launch-core/generated/devnet/state.json b/fixtures/launch-core/generated/devnet/state.json index 56b211b6..549a8d37 100644 --- a/fixtures/launch-core/generated/devnet/state.json +++ b/fixtures/launch-core/generated/devnet/state.json @@ -1,10 +1,42 @@ { "schema": "flowmemory.local_devnet.state.v0", + "config": { + "schema": "flowmemory.local_devnet.config.v0", + "chainId": "flowmemory-local-devnet-v0", + "networkId": "flowmemory-private-local", + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "genesisLogicalTime": 1778688000, + "blockTimeSeconds": 1, + "operatorKeyReferenceId": "operator-key:local-devnet:alpha", + "noValue": true, + "consensus": "single-process deterministic local block production", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ] + }, "chainId": "flowmemory-local-devnet-v0", "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", "nextBlockNumber": 3, "logicalTime": 1778688002, - "parentHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", + "parentHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9", + "operatorKeyReferences": { + "operator-key:local-devnet:alpha": { + "schema": "flowmemory.local_devnet.operator_key_reference.v0", + "keyReferenceId": "operator-key:local-devnet:alpha", + "operatorId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "workerKeyId": "0xc4cc0a4e778d201e59a442e969596ef8758fa62eb72c7ae4cb468c5493fc924d", + "verifierKeyId": "0xeaead587bf631e8926cf1a88ea5404f2211a339b77be7b9ffc08be420ce85551", + "verifierSetRoot": "0xbecddfb2cac22961206303e4f1255f58786e62503fbd54d875be915b68cc9635", + "signatureScheme": "eip712-secp256k1-fixture-digest-only", + "publicKeyHint": "local fixture boundary; no public key registry is implemented", + "secretMaterialBoundary": "no signing secret material is stored in devnet state or handoff output", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ] + } + }, "rootfields": { "rootfield:demo:alpha": { "rootfieldId": "rootfield:demo:alpha", @@ -17,6 +49,65 @@ "active": true } }, + "agentAccounts": { + "agent:demo:alpha": { + "agentId": "agent:demo:alpha", + "controller": "operator:local-demo", + "modelPassportId": "model:demo:local-alpha", + "metadataHash": "0x62348f90982ee812382bb97c0c6930e0e80583d0eeb40b4ae3c3395ca44cec1c", + "memoryRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "active": true + } + }, + "modelPassports": { + "model:demo:local-alpha": { + "modelPassportId": "model:demo:local-alpha", + "issuer": "operator:local-demo", + "modelFamily": "local-alpha-fixture-model", + "modelHash": "0xdeffd2a3fbb28dcc4dbb34172d082586bd0d63e1c76ab32296911946e6a7d0eb", + "metadataHash": "0x18416ecbdfcad838d0008b3692e1505886a84e9c45fc6e029b29a3456befc234", + "active": true + } + }, + "memoryCells": { + "memory:demo:agent-alpha:core": { + "memoryCellId": "memory:demo:agent-alpha:core", + "agentId": "agent:demo:alpha", + "rootfieldId": "rootfield:demo:alpha", + "currentRoot": "0xf82cda830e2aa175d9813982009a59ec8a288b691a55205a22d7007fcc3a41ae", + "parentRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "sourceReceiptId": "receipt:demo:001", + "memoryDeltaRoot": "0x6243c81a1bc56c93f5cebf081d9b69a8d7839bcd33384fe1d00c275d50f78de6", + "status": "active", + "updateCount": 1, + "updatedAtBlock": 1 + } + }, + "challenges": { + "challenge:demo:001": { + "challengeId": "challenge:demo:001", + "receiptId": "receipt:demo:001", + "challenger": "reviewer:local-demo", + "evidenceHash": "0xcc0312e21517151c7422f9b7c0c2ec611388eadfd89a07f6199adf617c4b461c", + "reasonCode": "local-review", + "status": "resolved", + "resolution": "dismissed", + "openedAtBlock": 1, + "resolvedAtBlock": 1 + } + }, + "finalityReceipts": { + "finality:demo:001": { + "finalityReceiptId": "finality:demo:001", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "finalizedBy": "operator:local-demo", + "finalityStatus": "finalized", + "challengeCount": 1, + "finalizedAtBlock": 1, + "stateRoot": "0xd1aa623e1b368f3bdf9bd45ae97eef8e35ba1a4cb5b6ac9fbc6c1ab29570cdcc" + } + }, "artifactCommitments": { "artifact:demo:001": { "artifactId": "artifact:demo:001", @@ -25,6 +116,28 @@ "uriHint": "fixture://artifact/demo/001" } }, + "artifactAvailabilityProofs": { + "availability:demo:001": { + "proofId": "availability:demo:001", + "artifactId": "artifact:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "proofDigest": "0xf5826d75505fd87288a6219067d4febfd25226cf6e70c85288317770b02d6527", + "storageBackend": "fixture-local", + "status": "available", + "checkedAtBlock": 1 + } + }, + "verifierModules": { + "verifier:local-demo": { + "verifierId": "verifier:local-demo", + "operator": "operator:local-demo", + "moduleHash": "0x91830f85b951a7e7a1d99f48270faa05769561bd1f7ae34b783d759a35e833be", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0", + "metadataHash": "0x965eb2a0dfe118a94ded4ef151bf0fd970cd77350cd88d1b758b23fe0ffe2d14", + "active": true + } + }, "workReceipts": { "receipt:demo:001": { "receiptId": "receipt:demo:001", @@ -50,16 +163,24 @@ "importedObservations": {}, "importedVerifierReports": {}, "baseAnchors": { - "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7": { - "anchorId": "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7", + "0x632509a4e0eb4812f0817795eae3fb1f3465f61c08035d1eea767df078f31bf1": { + "anchorId": "0x632509a4e0eb4812f0817795eae3fb1f3465f61c08035d1eea767df078f31bf1", "appchainChainId": "flowmemory-local-devnet-v0", "blockRangeStart": 1, "blockRangeEnd": 1, - "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023", "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", "finalityStatus": "local-placeholder" } @@ -72,10 +193,18 @@ "logicalTime": 1778688000, "txIds": [ "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189", + "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25", + "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f", + "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359", "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", + "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d", "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2", "0x73b81134901c2ce13e575f161d82a404c6f7cd1ef2e8ee17beb6697062175c46", - "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa" + "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa", + "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b", + "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad", + "0xf32e17d973089ae20766e2c6ec07d1511ddecd3f79803f6146a90df971ca814f", + "0x514f9ef68c09de5a4dc80611d661d07a4ea3a4fae6000a43a25864489f354a81" ], "receipts": [ { @@ -83,11 +212,31 @@ "status": "applied", "error": null }, + { + "txId": "0x75e63a0257621b8ef7412c6455a19d848996905e21f5ba79ccb0870d6e82eb25", + "status": "applied", + "error": null + }, + { + "txId": "0x6f55c155425b968de01092be7d276f0c24430a2994910881938bc13c72f8892f", + "status": "applied", + "error": null + }, + { + "txId": "0x05abb39c720d8ee1cd9253e32efaa595f5d5b2fcef4a908f61ab4a6bfa315359", + "status": "applied", + "error": null + }, { "txId": "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", "status": "applied", "error": null }, + { + "txId": "0x27aeba6c55c764222964764cb2bfbb69fb6fa56cb84714d6e98240ceb6e9d01d", + "status": "applied", + "error": null + }, { "txId": "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2", "status": "applied", @@ -102,15 +251,35 @@ "txId": "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa", "status": "applied", "error": null + }, + { + "txId": "0xda9d2574a0d4bec158e13623499c6efe6dddb76f838c5f06c3e4dc8457b67d3b", + "status": "applied", + "error": null + }, + { + "txId": "0xa0729982b58cc701aba6af0bc29ca993190db4e8e1489af918dbe293c0c03bad", + "status": "applied", + "error": null + }, + { + "txId": "0xf32e17d973089ae20766e2c6ec07d1511ddecd3f79803f6146a90df971ca814f", + "status": "applied", + "error": null + }, + { + "txId": "0x514f9ef68c09de5a4dc80611d661d07a4ea3a4fae6000a43a25864489f354a81", + "status": "applied", + "error": null } ], - "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", - "blockHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235" + "stateRoot": "0xfb51bb3269aa022e5d4e8271d4776d27f02cca6cabea24daddd555b97190d01b", + "blockHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449" }, { "schema": "flowmemory.local_devnet.block.v0", "blockNumber": 2, - "parentHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "parentHash": "0xbcf4f0971717e7ee790bfef9ebb0c9e9a94fbd0bb0a05b938119629fd417c449", "logicalTime": 1778688001, "txIds": [ "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" @@ -122,8 +291,8 @@ "error": null } ], - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", - "blockHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6" + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", + "blockHash": "0x3b7b4834931980ed28931449ce97e63c69f485715a12598fd8e914738acbbff9" } ], "pendingTxs": [] diff --git a/fixtures/launch-core/generated/devnet/verifier-handoff.json b/fixtures/launch-core/generated/devnet/verifier-handoff.json index 04220953..9f80224a 100644 --- a/fixtures/launch-core/generated/devnet/verifier-handoff.json +++ b/fixtures/launch-core/generated/devnet/verifier-handoff.json @@ -1,7 +1,103 @@ { + "artifactAvailabilityProofs": { + "availability:demo:001": { + "artifactId": "artifact:demo:001", + "checkedAtBlock": 1, + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "proofDigest": "0xf5826d75505fd87288a6219067d4febfd25226cf6e70c85288317770b02d6527", + "proofId": "availability:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "status": "available", + "storageBackend": "fixture-local" + } + }, + "challenges": { + "challenge:demo:001": { + "challengeId": "challenge:demo:001", + "challenger": "reviewer:local-demo", + "evidenceHash": "0xcc0312e21517151c7422f9b7c0c2ec611388eadfd89a07f6199adf617c4b461c", + "openedAtBlock": 1, + "reasonCode": "local-review", + "receiptId": "receipt:demo:001", + "resolution": "dismissed", + "resolvedAtBlock": 1, + "status": "resolved" + } + }, + "finalityReceipts": { + "finality:demo:001": { + "challengeCount": 1, + "finalityReceiptId": "finality:demo:001", + "finalityStatus": "finalized", + "finalizedAtBlock": 1, + "finalizedBy": "operator:local-demo", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "stateRoot": "0xd1aa623e1b368f3bdf9bd45ae97eef8e35ba1a4cb5b6ac9fbc6c1ab29570cdcc" + } + }, + "genesisConfig": { + "blockTimeSeconds": 1, + "chainId": "flowmemory-local-devnet-v0", + "consensus": "single-process deterministic local block production", + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "genesisLogicalTime": 1778688000, + "networkId": "flowmemory-private-local", + "noValue": true, + "operatorKeyReferenceId": "operator-key:local-devnet:alpha", + "schema": "flowmemory.local_devnet.config.v0" + }, "importedVerifierReports": {}, + "mapRoots": { + "agentAccountRoot": "0xcf31230bfff347f79e19a55f4d1ff5fa486b0b1ad4754ce22b93de4b259a3ca7", + "artifactAvailabilityProofRoot": "0xfb4b693c45014aae0947f35696e9d864e7b26ac6fd39c1df5edb3e0dcf9bd928", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "baseAnchorRoot": "0xb3d8ee8eed1f4c01dfe6e20c13a080e43c4fb9ff3703b1401ecd265fb326fd9e", + "challengeRoot": "0x16da3d2bf2dcd801bc5deb3987dc01342cb957031ad01408ea77bf5d1583656f", + "finalityReceiptRoot": "0x4f232719218263bad1968e456f2bdcfdb174821c5c92eba60353cfd9261191e8", + "importedObservationRoot": "0x99cb1b939d5a09f800f72e4c5a2b92988571126e1f6f93549f4893b3f7de7880", + "importedVerifierReportRoot": "0x6070b1015f000dd509c7b276d2ad68d8a9d188ef1a961c2f573346eb75ea5ad7", + "memoryCellRoot": "0x1b4e91099dd8d867201bd880437197ae6c031e538341aaa3cd2046e5706a2c25", + "modelPassportRoot": "0x326aa6b0b372d29d24d747fe0879adfd7aaea206373b24ae2ab77d56357e9529", + "operatorKeyReferenceRoot": "0x2bf060a51a0f896b3dc709d8dffe471ee30b8ec178ba4db3d4f8c2f1c9621f18", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "verifierModuleRoot": "0xd6ddd8a2d0f5812d64679656c69983a2e0aecd36bd36199d900245658ae4626c", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" + }, + "operatorKeyReferences": { + "operator-key:local-devnet:alpha": { + "cryptoSchemaRefs": [ + "crypto/FLOWMEMORY_CRYPTO_SPEC.md#signature-boundaries", + "crypto/ATTESTATIONS.md#local-test-keys-only" + ], + "keyReferenceId": "operator-key:local-devnet:alpha", + "operatorId": "0x06739c78255ec573518e97ffa9d2c5e11f49d49e0c65217c77d710a558a57f21", + "publicKeyHint": "local fixture boundary; no public key registry is implemented", + "schema": "flowmemory.local_devnet.operator_key_reference.v0", + "secretMaterialBoundary": "no signing secret material is stored in devnet state or handoff output", + "signatureScheme": "eip712-secp256k1-fixture-digest-only", + "verifierKeyId": "0xeaead587bf631e8926cf1a88ea5404f2211a339b77be7b9ffc08be420ce85551", + "verifierSetRoot": "0xbecddfb2cac22961206303e4f1255f58786e62503fbd54d875be915b68cc9635", + "workerKeyId": "0xc4cc0a4e778d201e59a442e969596ef8758fa62eb72c7ae4cb468c5493fc924d" + } + }, "schema": "flowmemory.verifier_handoff.local_devnet.v0", - "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "stateRoot": "0xe1c51bcfc1049256a956ca3f714ea313ddce07b59adb0070fc1cacb368f18be9", + "verifierModules": { + "verifier:local-demo": { + "active": true, + "metadataHash": "0x965eb2a0dfe118a94ded4ef151bf0fd970cd77350cd88d1b758b23fe0ffe2d14", + "moduleHash": "0x91830f85b951a7e7a1d99f48270faa05769561bd1f7ae34b783d759a35e833be", + "operator": "operator:local-demo", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0", + "verifierId": "verifier:local-demo" + } + }, "verifierReports": { "report:demo:001": { "reasonCodes": [], diff --git a/package-lock.json b/package-lock.json index 55b2c206..4346f28f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,18 @@ "services/shared", "services/indexer", "services/verifier", - "services/flowmemory" + "services/flowmemory", + "services/control-plane" ], "devDependencies": { "ajv": "^8.20.0", "ajv-formats": "^3.0.1" } }, + "node_modules/@flowmemory/control-plane-v0": { + "resolved": "services/control-plane", + "link": true + }, "node_modules/@flowmemory/indexer-v0": { "resolved": "services/indexer", "link": true @@ -108,6 +113,9 @@ "node": ">=0.10.0" } }, + "services/control-plane": { + "name": "@flowmemory/control-plane-v0" + }, "services/flowmemory": { "name": "@flowmemory/launch-core-v0" }, diff --git a/package.json b/package.json index 2e194da5..de3d2103 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,11 @@ "services/shared", "services/indexer", "services/verifier", - "services/flowmemory" + "services/flowmemory", + "services/control-plane" ], "scripts": { - "test": "npm test --prefix services/shared && npm test --prefix services/indexer && npm test --prefix services/verifier && npm test --prefix services/flowmemory", + "test": "npm test --prefix services/shared && npm test --prefix services/indexer && npm test --prefix services/verifier && npm test --prefix services/flowmemory && npm test --prefix services/control-plane", "contracts:hardening": "node infra/scripts/run-contract-hardening.mjs", "contracts:hardening:slither": "node infra/scripts/run-contract-hardening.mjs --require-slither", "index:base-canary": "npm run index:base-canary --prefix services/indexer", @@ -28,7 +29,11 @@ "build:production": "npm run launch:candidate && npm run build --prefix apps/dashboard", "e2e": "npm run index:fixtures && npm run verify:fixtures && npm run flowmemory:generate", "demo:indexer": "npm run demo --prefix services/indexer", - "demo:verifier": "npm run demo --prefix services/verifier" + "demo:verifier": "npm run demo --prefix services/verifier", + "control-plane:test": "npm test --prefix services/control-plane", + "control-plane:demo": "npm run demo --prefix services/control-plane", + "control-plane:smoke": "npm run smoke --prefix services/control-plane", + "control-plane:serve": "npm run serve --prefix services/control-plane" }, "devDependencies": { "ajv": "^8.20.0", diff --git a/services/control-plane/README.md b/services/control-plane/README.md new file mode 100644 index 00000000..6f110e07 --- /dev/null +++ b/services/control-plane/README.md @@ -0,0 +1,75 @@ +# FlowChain Control Plane V0 + +This package exposes a local JSON-RPC 2.0 control-plane for FlowMemory and FlowChain fixture data. It is fixture-first, deterministic, and read-only. + +It is not a production RPC endpoint, hosted service, wallet, sequencer, verifier network, token system, or production chain API. + +## Commands + +From the repository root: + +```powershell +npm run control-plane:demo +npm run control-plane:test +npm run control-plane:smoke +npm run control-plane:serve -- --host 127.0.0.1 --port 8675 +``` + +The demo and tests require no secrets, RPC URLs, wallets, or production services. + +## Methods + +The dispatcher supports: + +- `health` +- `chain_status` +- `devnet_state` +- `block_get` +- `block_list` +- `transaction_get` +- `transaction_list` +- `rootfield_get` +- `rootfield_list` +- `artifact_get` +- `artifact_availability_get` +- `artifact_availability_list` +- `receipt_get` +- `receipt_list` +- `work_receipt_get` +- `work_receipt_list` +- `verifier_module_get` +- `verifier_module_list` +- `verifier_report_get` +- `verifier_report_list` +- `memory_cell_get` +- `memory_cell_list` +- `agent_get` +- `agent_list` +- `model_get` +- `model_list` +- `challenge_get` +- `challenge_list` +- `finality_get` +- `finality_list` +- `provenance_get` +- `raw_json_get` + +The API contract is documented in [docs/FLOWCHAIN_CONTROL_PLANE_API.md](../../docs/FLOWCHAIN_CONTROL_PLANE_API.md). + +## Data Sources + +The loader reads committed deterministic outputs first: + +- `fixtures/launch-core/flowmemory-launch-v0.json` +- `fixtures/launch-core/generated/devnet/state.json` +- `fixtures/launch-core/generated/devnet/indexer-handoff.json` +- `fixtures/launch-core/generated/devnet/verifier-handoff.json` +- `fixtures/launch-core/generated/devnet/control-plane-handoff.json` +- `services/indexer/out/indexer-state.json` +- `services/verifier/out/reports.json` +- `services/verifier/fixtures/artifacts.json` +- `fixtures/handoff/sample-txs.json` + +If the launch-core fixture is missing, the loader rebuilds the in-memory view from indexer/verifier outputs or the raw fixture receipts and artifact resolver. It does not fetch from live RPC or write production state. + +`npm run control-plane:smoke` runs an in-process JSON-RPC client over the complete local lifecycle surface: health, chain status, blocks, transactions, rootfields, agents, models, work receipts, artifact availability, verifier modules, verifier reports, memory cells, challenges, finality, provenance, and raw JSON. diff --git a/services/control-plane/package.json b/services/control-plane/package.json new file mode 100644 index 00000000..2714f8da --- /dev/null +++ b/services/control-plane/package.json @@ -0,0 +1,11 @@ +{ + "name": "@flowmemory/control-plane-v0", + "private": true, + "type": "module", + "scripts": { + "demo": "node src/demo.ts", + "serve": "node src/server.ts", + "smoke": "node src/smoke.ts", + "test": "node --test test/*.test.ts" + } +} diff --git a/services/control-plane/src/demo.ts b/services/control-plane/src/demo.ts new file mode 100644 index 00000000..6e159253 --- /dev/null +++ b/services/control-plane/src/demo.ts @@ -0,0 +1,28 @@ +import { fileURLToPath } from "node:url"; + +import { dispatchJsonRpc } from "./json-rpc.ts"; +import { loadControlPlaneState } from "./fixture-state.ts"; + +export function runDemo(): unknown { + const state = loadControlPlaneState(); + const rootfieldId = state.launchCore.rootfieldBundles[0]?.rootfieldId; + const receiptId = state.launchCore.memoryReceipts[0]?.receiptId; + const reportId = state.launchCore.memoryReceipts[0]?.reportId; + const artifactUri = state.launchCore.memoryReceipts[0]?.evidenceRefs[0]?.uri; + + return dispatchJsonRpc([ + { jsonrpc: "2.0", id: 1, method: "health" }, + { jsonrpc: "2.0", id: 2, method: "chain_status" }, + { jsonrpc: "2.0", id: 3, method: "block_list", params: { limit: 3 } }, + { jsonrpc: "2.0", id: 4, method: "transaction_list", params: { limit: 3 } }, + { jsonrpc: "2.0", id: 5, method: "rootfield_get", params: { rootfieldId } }, + { jsonrpc: "2.0", id: 6, method: "receipt_get", params: { receiptId } }, + { jsonrpc: "2.0", id: 7, method: "verifier_report_get", params: { reportId } }, + { jsonrpc: "2.0", id: 8, method: "artifact_availability_get", params: { uri: artifactUri } }, + { jsonrpc: "2.0", id: 9, method: "provenance_get", params: { receiptId } }, + ], { state }); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + console.log(JSON.stringify(runDemo(), null, 2)); +} diff --git a/services/control-plane/src/errors.ts b/services/control-plane/src/errors.ts new file mode 100644 index 00000000..e9b60231 --- /dev/null +++ b/services/control-plane/src/errors.ts @@ -0,0 +1,60 @@ +import type { JsonValue, RpcErrorObject } from "./types.ts"; + +export const JSON_RPC_ERROR_CODES = { + invalidRequest: -32600, + methodNotFound: -32601, + invalidParams: -32602, + internalError: -32603, + objectNotFound: -32004, +} as const; + +export class ControlPlaneError extends Error { + readonly code: number; + readonly reasonCode: string; + readonly details?: JsonValue; + + constructor(code: number, message: string, reasonCode: string, details?: JsonValue) { + super(message); + this.name = "ControlPlaneError"; + this.code = code; + this.reasonCode = reasonCode; + this.details = details; + } +} + +export function invalidParams(message: string, details?: JsonValue): ControlPlaneError { + return new ControlPlaneError(JSON_RPC_ERROR_CODES.invalidParams, message, "params.invalid", details); +} + +export function objectNotFound(message: string, details?: JsonValue): ControlPlaneError { + return new ControlPlaneError(JSON_RPC_ERROR_CODES.objectNotFound, message, "object.not_found", details); +} + +export function methodNotFound(message: string, details?: JsonValue): ControlPlaneError { + return new ControlPlaneError(JSON_RPC_ERROR_CODES.methodNotFound, message, "method.not_found", details); +} + +export function rpcError(error: unknown): RpcErrorObject { + if (error instanceof ControlPlaneError) { + return { + code: error.code, + message: error.message, + data: { + schema: "flowmemory.control_plane.error.v0", + reasonCode: error.reasonCode, + details: error.details, + localOnly: true, + }, + }; + } + + return { + code: JSON_RPC_ERROR_CODES.internalError, + message: error instanceof Error ? error.message : "internal control-plane error", + data: { + schema: "flowmemory.control_plane.error.v0", + reasonCode: "internal.error", + localOnly: true, + }, + }; +} diff --git a/services/control-plane/src/fixture-state.ts b/services/control-plane/src/fixture-state.ts new file mode 100644 index 00000000..751f0669 --- /dev/null +++ b/services/control-plane/src/fixture-state.ts @@ -0,0 +1,185 @@ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, isAbsolute, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { buildLaunchCore, type LaunchCorePaths } from "../../flowmemory/src/generate-launch-core.ts"; +import type { LaunchCoreOutput } from "../../flowmemory/src/types.ts"; +import { + indexFlowPulseReceipts, + loadIndexerFixtureReceipts, + persistedIndexerState, + type PersistedIndexerState, +} from "../../indexer/src/index.ts"; +import { + loadVerifierArtifactFixture, + persistedVerifierReports, + verifyObservations, + type ArtifactResolverFixture, + type PersistedVerifierReports, +} from "../../verifier/src/index.ts"; +import type { + ControlPlanePaths, + DataSourceRecord, + JsonObject, + LoadedControlPlaneState, +} from "./types.ts"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); + +export const DEFAULT_CONTROL_PLANE_PATHS: ControlPlanePaths = { + launchCorePath: "fixtures/launch-core/flowmemory-launch-v0.json", + indexerPath: "services/indexer/out/indexer-state.json", + verifierPath: "services/verifier/out/reports.json", + artifactsPath: "services/verifier/fixtures/artifacts.json", + devnetPath: "fixtures/launch-core/generated/devnet/state.json", + devnetIndexerHandoffPath: "fixtures/launch-core/generated/devnet/indexer-handoff.json", + devnetVerifierHandoffPath: "fixtures/launch-core/generated/devnet/verifier-handoff.json", + devnetControlPlaneHandoffPath: "fixtures/launch-core/generated/devnet/control-plane-handoff.json", + txFixturesPath: "fixtures/handoff/sample-txs.json", +}; + +function resolveRepoPath(path: string): string { + return isAbsolute(path) ? path : resolve(REPO_ROOT, path); +} + +function readJson(path: string): T { + return JSON.parse(readFileSync(resolveRepoPath(path), "utf8")) as T; +} + +function sourceRecord( + name: string, + path: string, + status: DataSourceRecord["status"], + recovery?: string, +): DataSourceRecord { + return { + schema: "flowmemory.control_plane.data_source.v0", + name, + path, + status, + recovery, + }; +} + +function maybeReadJson(path: string): JsonObject | null { + if (!existsSync(resolveRepoPath(path))) { + return null; + } + return readJson(path); +} + +function loadOrBuildIndexer(path: string, sources: Record): PersistedIndexerState { + if (existsSync(resolveRepoPath(path))) { + sources.indexer = sourceRecord("indexer", path, "loaded"); + return readJson(path); + } + + const state = indexFlowPulseReceipts(loadIndexerFixtureReceipts(), { + finalizedBlockNumber: "123458", + }); + sources.indexer = sourceRecord("indexer", path, "recovered", "built in memory from services/indexer/fixtures/flowpulse-receipts.json"); + return persistedIndexerState(state); +} + +function loadOrBuildVerifier( + path: string, + indexer: PersistedIndexerState, + resolver: ArtifactResolverFixture, + sources: Record, +): PersistedVerifierReports { + if (existsSync(resolveRepoPath(path))) { + sources.verifier = sourceRecord("verifier", path, "loaded"); + return readJson(path); + } + + const reports = verifyObservations(indexer.state.observations, resolver); + sources.verifier = sourceRecord("verifier", path, "recovered", "built in memory from indexer observations and artifact fixtures"); + return persistedVerifierReports(reports); +} + +function loadArtifacts(path: string, sources: Record): ArtifactResolverFixture { + if (existsSync(resolveRepoPath(path))) { + sources.artifacts = sourceRecord("artifacts", path, "loaded"); + return readJson(path); + } + + sources.artifacts = sourceRecord("artifacts", path, "recovered", "loaded default services/verifier artifact fixture"); + return loadVerifierArtifactFixture(); +} + +function launchPaths(paths: ControlPlanePaths): LaunchCorePaths { + return { + indexerPath: paths.indexerPath, + verifierPath: paths.verifierPath, + devnetPath: paths.devnetPath, + hardwarePath: "hardware/fixtures/flowrouter_sample_seed42.json", + launchOutPath: paths.launchCorePath, + transitionsOutPath: "fixtures/launch-core/rootflow-transitions.json", + dashboardOutPath: "fixtures/dashboard/flowmemory-dashboard-v0.json", + dashboardRuntimePath: "apps/dashboard/public/data/flowmemory-dashboard-v0.json", + }; +} + +function loadOrBuildLaunchCore( + paths: ControlPlanePaths, + indexer: PersistedIndexerState, + verifier: PersistedVerifierReports, + sources: Record, +): LaunchCoreOutput { + if (existsSync(resolveRepoPath(paths.launchCorePath))) { + sources.launchCore = sourceRecord("launchCore", paths.launchCorePath, "loaded"); + return readJson(paths.launchCorePath); + } + + sources.launchCore = sourceRecord("launchCore", paths.launchCorePath, "recovered", "built in memory from indexer and verifier state"); + return buildLaunchCore(indexer, verifier, launchPaths(paths)); +} + +function loadOptionalSource( + name: string, + path: string, + sources: Record, +): JsonObject | null { + const value = maybeReadJson(path); + sources[name] = sourceRecord(name, path, value === null ? "missing" : "loaded"); + return value; +} + +export function controlPlanePaths(overrides: Partial = {}): ControlPlanePaths { + return { + ...DEFAULT_CONTROL_PLANE_PATHS, + ...overrides, + }; +} + +export function loadControlPlaneState(overrides: Partial = {}): LoadedControlPlaneState { + const paths = controlPlanePaths(overrides); + const sources: Record = {}; + const artifacts = loadArtifacts(paths.artifactsPath, sources); + const indexer = loadOrBuildIndexer(paths.indexerPath, sources); + const verifier = loadOrBuildVerifier(paths.verifierPath, indexer, artifacts, sources); + const launchCore = loadOrBuildLaunchCore(paths, indexer, verifier, sources); + const devnet = loadOptionalSource("devnet", paths.devnetPath, sources); + const devnetIndexerHandoff = loadOptionalSource("devnetIndexerHandoff", paths.devnetIndexerHandoffPath, sources); + const devnetVerifierHandoff = loadOptionalSource("devnetVerifierHandoff", paths.devnetVerifierHandoffPath, sources); + const devnetControlPlaneHandoff = loadOptionalSource("devnetControlPlaneHandoff", paths.devnetControlPlaneHandoffPath, sources); + const txFixtures = loadOptionalSource("txFixtures", paths.txFixturesPath, sources); + + return { + schema: "flowmemory.control_plane.state.v0", + launchCore, + indexer, + verifier, + artifacts, + devnet, + devnetIndexerHandoff, + devnetVerifierHandoff, + devnetControlPlaneHandoff, + txFixtures, + sources, + }; +} + +export function repoRoot(): string { + return REPO_ROOT; +} diff --git a/services/control-plane/src/index.ts b/services/control-plane/src/index.ts new file mode 100644 index 00000000..6321783a --- /dev/null +++ b/services/control-plane/src/index.ts @@ -0,0 +1,5 @@ +export * from "./errors.ts"; +export * from "./fixture-state.ts"; +export * from "./json-rpc.ts"; +export * from "./methods.ts"; +export * from "./types.ts"; diff --git a/services/control-plane/src/json-rpc.ts b/services/control-plane/src/json-rpc.ts new file mode 100644 index 00000000..5d3c53c7 --- /dev/null +++ b/services/control-plane/src/json-rpc.ts @@ -0,0 +1,88 @@ +import { CONTROL_PLANE_METHODS, callControlPlaneMethod } from "./methods.ts"; +import { JSON_RPC_ERROR_CODES, ControlPlaneError, methodNotFound, rpcError } from "./errors.ts"; +import type { + ControlPlaneContext, + JsonValue, + RpcRequest, + RpcResponse, +} from "./types.ts"; + +function isObject(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function invalidRequest(id: string | number | null, message: string): RpcResponse { + return { + jsonrpc: "2.0", + id, + error: rpcError(new ControlPlaneError( + JSON_RPC_ERROR_CODES.invalidRequest, + message, + "request.invalid", + )), + }; +} + +function requestId(value: Record): string | number | null { + const id = value.id; + return typeof id === "string" || typeof id === "number" || id === null ? id : null; +} + +export function dispatchJsonRpc( + request: unknown, + context: ControlPlaneContext = {}, +): RpcResponse | RpcResponse[] | undefined { + if (Array.isArray(request)) { + const responses = request + .map((entry) => dispatchJsonRpc(entry, context)) + .filter((entry): entry is RpcResponse => entry !== undefined && !Array.isArray(entry)); + return responses.length === 0 ? undefined : responses; + } + + if (!isObject(request)) { + return invalidRequest(null, "JSON-RPC request must be an object"); + } + + const id = requestId(request); + if (request.jsonrpc !== "2.0" || typeof request.method !== "string") { + return invalidRequest(id, "JSON-RPC request requires jsonrpc \"2.0\" and a string method"); + } + + if (!(request.method in CONTROL_PLANE_METHODS)) { + return { + jsonrpc: "2.0", + id, + error: rpcError(methodNotFound(`control-plane method not found: ${request.method}`, { method: request.method })), + }; + } + + try { + const result = callControlPlaneMethod(request.method, request.params as JsonValue | undefined, context); + if (!("id" in request)) { + return undefined; + } + return { + jsonrpc: "2.0", + id, + result, + }; + } catch (error) { + if (!("id" in request)) { + return undefined; + } + return { + jsonrpc: "2.0", + id, + error: rpcError(error), + }; + } +} + +export function parseJsonRpcPayload(payload: string): unknown { + return JSON.parse(payload) as unknown; +} + +export function dispatchJsonRpcString(payload: string, context: ControlPlaneContext = {}): string | undefined { + const response = dispatchJsonRpc(parseJsonRpcPayload(payload), context); + return response === undefined ? undefined : `${JSON.stringify(response)}\n`; +} diff --git a/services/control-plane/src/methods.ts b/services/control-plane/src/methods.ts new file mode 100644 index 00000000..dc19c3d8 --- /dev/null +++ b/services/control-plane/src/methods.ts @@ -0,0 +1,1830 @@ +import { canonicalJson, keccak256Hex } from "../../shared/src/index.ts"; +import { invalidParams, methodNotFound, objectNotFound } from "./errors.ts"; +import { loadControlPlaneState } from "./fixture-state.ts"; +import type { + ControlPlaneContext, + ControlPlaneMethod, + JsonObject, + JsonValue, + LoadedControlPlaneState, +} from "./types.ts"; + +const ZERO_ROOT = "0x0000000000000000000000000000000000000000000000000000000000000000"; + +type MethodHandler = (params: JsonValue | undefined, context: ControlPlaneContext) => JsonValue; + +function stateFor(context: ControlPlaneContext): LoadedControlPlaneState { + return context.state ?? loadControlPlaneState(context.paths); +} + +function asObjectParams(params: JsonValue | undefined, method: string): JsonObject { + if (params === undefined) { + return {}; + } + if (params === null || typeof params !== "object" || Array.isArray(params)) { + throw invalidParams(`${method} params must be an object`); + } + return params as JsonObject; +} + +function requiredString(params: JsonObject, names: string[], method: string): string { + for (const name of names) { + const value = params[name]; + if (typeof value === "string" && value.length > 0) { + return value; + } + } + throw invalidParams(`${method} requires one of: ${names.join(", ")}`, { required: names }); +} + +function optionalString(params: JsonObject, name: string): string | undefined { + const value = params[name]; + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function pageLimit(params: JsonObject): number { + const value = params.limit; + if (value === undefined) { + return 50; + } + if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 100) { + throw invalidParams("limit must be an integer from 1 to 100"); + } + return value; +} + +function optionalBoolean(params: JsonObject, name: string): boolean { + const value = params[name]; + if (value === undefined) { + return false; + } + if (typeof value !== "boolean") { + throw invalidParams(`${name} must be a boolean`); + } + return value; +} + +function asJsonObject(value: JsonValue | undefined): JsonObject | null { + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonObject : null; +} + +function asJsonArray(value: JsonValue | undefined): JsonValue[] { + return Array.isArray(value) ? value : []; +} + +function stringValue(value: JsonValue | undefined): string | null { + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return null; +} + +function stringList(value: JsonValue | undefined): string[] { + return asJsonArray(value) + .map((entry) => stringValue(entry)) + .filter((entry): entry is string => entry !== null); +} + +function compareStringNumbers(left: string, right: string): number { + if (/^\d+$/.test(left) && /^\d+$/.test(right)) { + const diff = BigInt(left) - BigInt(right); + return diff < 0n ? -1 : diff > 0n ? 1 : 0; + } + return left.localeCompare(right); +} + +function stableId(schema: string, value: JsonValue): string { + return keccak256Hex(new TextEncoder().encode(canonicalJson({ schema, value }))); +} + +function latestBlock(state: LoadedControlPlaneState): { blockNumber: string; blockHash: string } { + const latest = [...state.indexer.state.observations].sort((left, right) => { + const block = BigInt(right.blockNumber) - BigInt(left.blockNumber); + if (block !== 0n) { + return block < 0n ? -1 : 1; + } + const log = BigInt(right.logIndex) - BigInt(left.logIndex); + if (log !== 0n) { + return log < 0n ? -1 : 1; + } + return right.observationId.localeCompare(left.observationId); + })[0]; + + return { + blockNumber: latest?.blockNumber ?? "0", + blockHash: latest?.blockHash ?? ZERO_ROOT, + }; +} + +function finalizedBlock(state: LoadedControlPlaneState): string { + const finalized = state.indexer.state.observations + .filter((observation) => observation.lifecycleState === "finalized") + .map((observation) => BigInt(observation.blockNumber)); + if (finalized.length === 0) { + return "0"; + } + return finalized.reduce((max, block) => block > max ? block : max, 0n).toString(); +} + +function provenanceSource(subsystem: string, path: string, schema: string, note?: string): JsonObject { + return { + schema: "flowmemory.control_plane.provenance_source.v0", + subsystem, + path, + source: "local-fixture", + objectSchema: schema, + note, + }; +} + +function reportByIdOrObservation(state: LoadedControlPlaneState, key: string) { + return state.verifier.reports.find((report) => { + return report.reportId === key || report.reportDigest === key || report.reportCore.observationId === key; + }); +} + +function receiptByAnyId(state: LoadedControlPlaneState, key: string) { + return state.launchCore.memoryReceipts.find((receipt) => { + return receipt.receiptId === key + || receipt.observationId === key + || receipt.reportId === key + || receipt.reportDigest === key; + }); +} + +function signalByObservation(state: LoadedControlPlaneState, observationId: string) { + return state.launchCore.memorySignals.find((signal) => signal.observationId === observationId); +} + +function transitionByAnyId(state: LoadedControlPlaneState, key: string) { + return state.launchCore.rootflowTransitions.find((transition) => { + return transition.transitionId === key + || transition.observationId === key + || transition.memoryReceiptId === key + || transition.memorySignalId === key + || transition.reportId === key; + }); +} + +function devnetRootfields(state: LoadedControlPlaneState): Record { + return devnetMap(state, "rootfields"); +} + +function devnetMap(state: LoadedControlPlaneState, key: string): Record { + const controlPlaneObjects = asJsonObject(state.devnetControlPlaneHandoff?.objects); + const value = state.devnet?.[key] + ?? controlPlaneObjects?.[key] + ?? state.devnetControlPlaneHandoff?.[key] + ?? state.devnetVerifierHandoff?.[key] + ?? state.devnetIndexerHandoff?.[key]; + return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} + +function firstDevnetMap(state: LoadedControlPlaneState, keys: string[]): Record { + for (const key of keys) { + const value = devnetMap(state, key); + if (Object.keys(value).length > 0) { + return value; + } + } + return {}; +} + +function devnetBlocksArray(state: LoadedControlPlaneState): JsonObject[] { + const candidate = asJsonArray(state.devnet?.blocks).length > 0 + ? asJsonArray(state.devnet?.blocks) + : asJsonArray(state.devnetControlPlaneHandoff?.blocks).length > 0 + ? asJsonArray(state.devnetControlPlaneHandoff?.blocks) + : asJsonArray(state.devnetIndexerHandoff?.blocks); + return candidate + .map((entry) => asJsonObject(entry)) + .filter((entry): entry is JsonObject => entry !== null); +} + +function txFixtureRows(state: LoadedControlPlaneState): JsonObject[] { + return asJsonArray(state.txFixtures?.txs) + .map((entry) => asJsonObject(entry)) + .filter((entry): entry is JsonObject => entry !== null); +} + +function devnetWorkReceipts(state: LoadedControlPlaneState): Record { + return devnetMap(state, "workReceipts"); +} + +function devnetReports(state: LoadedControlPlaneState): Record { + return devnetMap(state, "verifierReports"); +} + +function devnetArtifacts(state: LoadedControlPlaneState): Record { + return devnetMap(state, "artifactCommitments"); +} + +function devnetAgentAccounts(state: LoadedControlPlaneState): Record { + return devnetMap(state, "agentAccounts"); +} + +function devnetModels(state: LoadedControlPlaneState): Record { + return firstDevnetMap(state, ["modelPassports", "models"]); +} + +function devnetVerifierModules(state: LoadedControlPlaneState): Record { + return firstDevnetMap(state, ["verifierModules", "modules"]); +} + +function devnetArtifactAvailability(state: LoadedControlPlaneState): Record { + return firstDevnetMap(state, ["artifactAvailabilityProofs", "artifactAvailability"]); +} + +function devnetMemoryCells(state: LoadedControlPlaneState): Record { + return devnetMap(state, "memoryCells"); +} + +function devnetChallenges(state: LoadedControlPlaneState): Record { + return devnetMap(state, "challenges"); +} + +function devnetFinalityReceipts(state: LoadedControlPlaneState): Record { + return devnetMap(state, "finalityReceipts"); +} + +function transactionRows(state: LoadedControlPlaneState): JsonObject[] { + const rows: JsonObject[] = []; + const txFixtures = txFixtureRows(state); + let fixtureIndex = 0; + + for (const block of devnetBlocksArray(state)) { + const blockNumber = stringValue(block.blockNumber) ?? "0"; + const blockHash = stringValue(block.blockHash) ?? ZERO_ROOT; + const receipts = asJsonArray(block.receipts) + .map((entry) => asJsonObject(entry)) + .filter((entry): entry is JsonObject => entry !== null); + + stringList(block.txIds).forEach((txId, transactionIndex) => { + const receipt = receipts.find((entry) => stringValue(entry.txId) === txId) ?? null; + const payload = txFixtures[fixtureIndex] ?? null; + fixtureIndex += 1; + rows.push({ + schema: "flowmemory.control_plane.transaction.v0", + transactionId: txId, + txHash: txId, + blockNumber, + blockHash, + transactionIndex: String(transactionIndex), + status: stringValue(receipt?.status) ?? "unknown", + type: stringValue(payload?.type) ?? "unknown", + payload, + receipt, + source: "local-devnet", + localOnly: true, + }); + }); + } + + const byHash = new Map(); + for (const observation of state.indexer.state.observations) { + const existing = byHash.get(observation.txHash) ?? { + schema: "flowmemory.control_plane.transaction.v0", + transactionId: observation.txHash, + txHash: observation.txHash, + chainId: observation.chainId, + blockNumber: observation.blockNumber, + blockHash: observation.blockHash, + transactionIndex: observation.transactionIndex, + status: observation.receiptStatus, + type: "FlowPulse", + observationIds: [], + pulseIds: [], + rootfieldIds: [], + logCount: 0, + source: "flowpulse-indexer", + localOnly: true, + }; + (existing.observationIds as JsonValue[]).push(observation.observationId); + (existing.pulseIds as JsonValue[]).push(observation.pulseId); + (existing.rootfieldIds as JsonValue[]).push(observation.rootfieldId); + existing.logCount = Number(existing.logCount ?? 0) + 1; + existing.status = observation.lifecycleState; + byHash.set(observation.txHash, existing); + } + + for (const rejected of state.indexer.state.rejectedLogs) { + const existing = byHash.get(rejected.txHash) ?? { + schema: "flowmemory.control_plane.transaction.v0", + transactionId: rejected.txHash, + txHash: rejected.txHash, + chainId: rejected.chainId, + blockNumber: rejected.blockNumber, + blockHash: rejected.blockHash, + transactionIndex: rejected.transactionIndex, + status: "rejected", + type: "FlowPulseRejectedLog", + rejectedLogs: [], + source: "flowpulse-indexer", + localOnly: true, + }; + const rejectedLogs = Array.isArray(existing.rejectedLogs) ? existing.rejectedLogs : []; + rejectedLogs.push({ + reasonCode: rejected.reasonCode, + message: rejected.message, + logIndex: rejected.logIndex, + }); + existing.rejectedLogs = rejectedLogs; + existing.status = "rejected"; + byHash.set(rejected.txHash, existing); + } + + rows.push(...byHash.values()); + return rows.sort((left, right) => { + const byBlock = compareStringNumbers(stringValue(left.blockNumber) ?? "0", stringValue(right.blockNumber) ?? "0"); + if (byBlock !== 0) { + return byBlock; + } + return compareStringNumbers(stringValue(left.transactionIndex) ?? "0", stringValue(right.transactionIndex) ?? "0"); + }); +} + +function blockRows(state: LoadedControlPlaneState, includeTransactions = false): JsonObject[] { + const txs = includeTransactions ? transactionRows(state) : []; + const rows = devnetBlocksArray(state).map((block) => { + const blockNumber = stringValue(block.blockNumber) ?? "0"; + const blockHash = stringValue(block.blockHash) ?? ZERO_ROOT; + return { + schema: "flowmemory.control_plane.block.v0", + blockNumber, + blockHash, + parentHash: stringValue(block.parentHash) ?? null, + logicalTime: block.logicalTime ?? null, + stateRoot: stringValue(block.stateRoot) ?? null, + txIds: stringList(block.txIds), + receiptCount: asJsonArray(block.receipts).length, + receipts: asJsonArray(block.receipts), + transactions: includeTransactions + ? txs.filter((tx) => tx.source === "local-devnet" && tx.blockHash === blockHash && tx.blockNumber === blockNumber) + : undefined, + source: "local-devnet", + localOnly: true, + }; + }); + + const indexerBlocks = new Map(); + for (const observation of state.indexer.state.observations) { + const key = `${observation.chainId}:${observation.blockHash}:${observation.blockNumber}`; + const existing = indexerBlocks.get(key) ?? { + schema: "flowmemory.control_plane.block.v0", + chainId: observation.chainId, + blockNumber: observation.blockNumber, + blockHash: observation.blockHash, + txIds: [], + observationIds: [], + rejectedLogCount: 0, + source: "flowpulse-indexer", + localOnly: true, + }; + const txIds = existing.txIds as JsonValue[]; + if (!txIds.includes(observation.txHash)) { + txIds.push(observation.txHash); + } + (existing.observationIds as JsonValue[]).push(observation.observationId); + existing.observationCount = Number(existing.observationCount ?? 0) + 1; + indexerBlocks.set(key, existing); + } + for (const rejected of state.indexer.state.rejectedLogs) { + const key = `${rejected.chainId}:${rejected.blockHash}:${rejected.blockNumber}`; + const existing = indexerBlocks.get(key) ?? { + schema: "flowmemory.control_plane.block.v0", + chainId: rejected.chainId, + blockNumber: rejected.blockNumber, + blockHash: rejected.blockHash, + txIds: [], + observationIds: [], + observationCount: 0, + source: "flowpulse-indexer", + localOnly: true, + }; + const txIds = existing.txIds as JsonValue[]; + if (!txIds.includes(rejected.txHash)) { + txIds.push(rejected.txHash); + } + existing.rejectedLogCount = Number(existing.rejectedLogCount ?? 0) + 1; + indexerBlocks.set(key, existing); + } + + rows.push(...[...indexerBlocks.values()].map((block) => ({ + ...block, + transactions: includeTransactions + ? txs.filter((tx) => tx.source === "flowpulse-indexer" && tx.blockHash === block.blockHash && tx.blockNumber === block.blockNumber) + : undefined, + }))); + + return rows.sort((left, right) => compareStringNumbers(stringValue(left.blockNumber) ?? "0", stringValue(right.blockNumber) ?? "0")); +} + +function rootfieldRows(state: LoadedControlPlaneState): JsonObject[] { + const rows = new Map(); + for (const bundle of state.launchCore.rootfieldBundles) { + rows.set(bundle.rootfieldId, { + schema: "flowmemory.control_plane.rootfield_row.v0", + rootfieldId: bundle.rootfieldId, + status: bundle.status, + latestRoot: bundle.latestRoot, + bundle, + devnetRootfield: null, + source: "launch-core", + localOnly: true, + }); + } + for (const [rootfieldId, value] of Object.entries(devnetRootfields(state))) { + const devnetRootfield = asJsonObject(value) ?? {}; + const existing = rows.get(rootfieldId); + rows.set(rootfieldId, { + schema: "flowmemory.control_plane.rootfield_row.v0", + rootfieldId, + status: stringValue(devnetRootfield.active) === "false" ? "inactive" : stringValue(devnetRootfield.status) ?? existing?.status ?? "active", + latestRoot: stringValue(devnetRootfield.latestRoot) ?? stringValue(existing?.latestRoot) ?? ZERO_ROOT, + bundle: existing?.bundle ?? null, + devnetRootfield, + source: existing === undefined ? "local-devnet" : "launch-core+local-devnet", + localOnly: true, + }); + } + return [...rows.values()].sort((left, right) => String(left.rootfieldId).localeCompare(String(right.rootfieldId))); +} + +function agentRows(state: LoadedControlPlaneState): JsonObject[] { + const rows: JsonObject[] = []; + for (const [agentId, value] of Object.entries(devnetAgentAccounts(state))) { + const agent = asJsonObject(value) ?? {}; + rows.push({ + schema: "flowmemory.control_plane.agent_row.v0", + agentId, + rootfieldId: stringValue(agent.rootfieldId) ?? null, + status: stringValue(agent.status) ?? "local", + agentAccount: agent, + agentMemoryView: null, + source: "local-devnet", + localOnly: true, + }); + } + for (const view of state.launchCore.agentMemoryViews) { + rows.push({ + schema: "flowmemory.control_plane.agent_row.v0", + agentId: view.viewId, + rootfieldId: view.rootfieldId, + status: view.status, + agentAccount: null, + agentMemoryView: view, + source: "launch-core", + localOnly: true, + }); + } + return rows.sort((left, right) => String(left.agentId).localeCompare(String(right.agentId))); +} + +function modelRows(state: LoadedControlPlaneState): JsonObject[] { + const nativeModels = Object.entries(devnetModels(state)).map(([modelId, value]) => { + const model = asJsonObject(value) ?? {}; + return { + schema: "flowmemory.control_plane.model.v0", + modelId, + rootfieldId: stringValue(model.rootfieldId) ?? null, + status: stringValue(model.status) ?? "local", + modelPassport: model, + source: "local-devnet", + localOnly: true, + }; + }); + const projectedModels = state.launchCore.agentMemoryViews.map((view) => ({ + schema: "flowmemory.control_plane.model.v0", + modelId: stableId("flowmemory.control_plane.model.projected.v0", view.rootfieldId), + rootfieldId: view.rootfieldId, + status: "local-placeholder", + modelPassport: null, + capabilities: ["read_agent_memory_view", "cite_local_fixture_provenance"], + extensionPoint: "No ModelPassport handoff fixture exists yet; this projected row keeps the dashboard/workbench model API stable.", + source: "projection", + localOnly: true, + })); + return [...nativeModels, ...projectedModels].sort((left, right) => String(left.modelId).localeCompare(String(right.modelId))); +} + +function workReceiptRows(state: LoadedControlPlaneState): JsonObject[] { + const rows: JsonObject[] = []; + for (const [receiptId, value] of Object.entries(devnetWorkReceipts(state))) { + const receipt = asJsonObject(value) ?? {}; + const linkedReport = Object.values(devnetReports(state)) + .map((entry) => asJsonObject(entry)) + .find((report) => report?.receiptId === receiptId) ?? null; + rows.push({ + schema: "flowmemory.control_plane.work_receipt_row.v0", + workReceiptId: receiptId, + receiptId, + rootfieldId: stringValue(receipt.rootfieldId) ?? null, + status: stringValue(linkedReport?.status) ?? "submitted", + workReceipt: receipt, + verifierReport: linkedReport, + source: "local-devnet", + localOnly: true, + }); + } + for (const receipt of state.launchCore.memoryReceipts) { + rows.push({ + schema: "flowmemory.control_plane.work_receipt_row.v0", + workReceiptId: receipt.receiptId, + receiptId: receipt.receiptId, + rootfieldId: receipt.rootfieldId, + status: receipt.flowMemoryStatus, + memoryReceipt: receipt, + verifierReport: reportByIdOrObservation(state, receipt.reportId) ?? null, + source: "launch-core-memory-receipt", + localOnly: true, + }); + } + return rows.sort((left, right) => String(left.receiptId).localeCompare(String(right.receiptId))); +} + +function artifactAvailabilityRows(state: LoadedControlPlaneState): JsonObject[] { + const rows: JsonObject[] = []; + for (const [id, value] of Object.entries(devnetArtifactAvailability(state))) { + rows.push({ + schema: "flowmemory.control_plane.artifact_availability.v0", + availabilityId: id, + status: stringValue(asJsonObject(value)?.status) ?? "local", + proof: asJsonObject(value) ?? {}, + source: "local-devnet", + localOnly: true, + }); + } + for (const [artifactId, value] of Object.entries(devnetArtifacts(state))) { + const artifact = asJsonObject(value) ?? {}; + rows.push({ + schema: "flowmemory.control_plane.artifact_availability.v0", + availabilityId: artifactId, + artifactId, + rootfieldId: stringValue(artifact.rootfieldId) ?? null, + commitment: stringValue(artifact.commitment) ?? null, + uri: stringValue(artifact.uriHint) ?? null, + status: "committed_local", + proof: artifact, + source: "local-devnet-artifact-commitment", + localOnly: true, + }); + } + for (const [uri, artifact] of Object.entries(state.artifacts.artifactsByUri)) { + const artifactObject = artifact as JsonObject; + rows.push({ + schema: "flowmemory.control_plane.artifact_availability.v0", + availabilityId: stableId("flowmemory.control_plane.artifact_availability.fixture.v0", uri), + artifactId: stableId("flowmemory.control_plane.artifact.fixture.v0", uri), + uri, + commitment: stringValue(artifactObject.artifactCommitment) ?? stringValue(artifactObject.commitment) ?? null, + status: "available_fixture", + resolverPolicyId: state.artifacts.resolverPolicyId, + artifact, + source: "verifier-fixture", + localOnly: true, + }); + } + return rows.sort((left, right) => String(left.availabilityId).localeCompare(String(right.availabilityId))); +} + +function verifierModuleRows(state: LoadedControlPlaneState): JsonObject[] { + const nativeModules = Object.entries(devnetVerifierModules(state)).map(([moduleId, value]) => ({ + schema: "flowmemory.control_plane.verifier_module.v0", + moduleId, + verifierModule: asJsonObject(value) ?? {}, + status: stringValue(asJsonObject(value)?.status) ?? "local", + source: "local-devnet", + localOnly: true, + })); + const projected = new Map(); + for (const report of state.verifier.reports) { + const key = `${report.reportCore.verifierSpecVersion}:${report.reportCore.resolverPolicyId}`; + if (!projected.has(key)) { + projected.set(key, { + schema: "flowmemory.control_plane.verifier_module.v0", + moduleId: stableId("flowmemory.control_plane.verifier_module.projected.v0", key), + verifierSpecVersion: report.reportCore.verifierSpecVersion, + resolverPolicyId: report.reportCore.resolverPolicyId, + supportedStatuses: ["valid", "invalid", "unresolved", "unsupported", "reorged"], + status: "available_fixture", + source: "verifier-report-projection", + localOnly: true, + }); + } + } + for (const report of Object.values(devnetReports(state)).map((entry) => asJsonObject(entry)).filter((entry): entry is JsonObject => entry !== null)) { + const verifierId = stringValue(report.verifierId); + if (verifierId !== null && !projected.has(verifierId)) { + projected.set(verifierId, { + schema: "flowmemory.control_plane.verifier_module.v0", + moduleId: verifierId, + verifierId, + status: "available_fixture", + source: "local-devnet-report-projection", + localOnly: true, + }); + } + } + return [...nativeModules, ...projected.values()].sort((left, right) => String(left.moduleId).localeCompare(String(right.moduleId))); +} + +function memoryCellRows(state: LoadedControlPlaneState): JsonObject[] { + const nativeCells = Object.entries(devnetMemoryCells(state)).map(([memoryCellId, value]) => { + const cell = asJsonObject(value) ?? {}; + return { + schema: "flowmemory.control_plane.memory_cell_row.v0", + memoryCellId, + rootfieldId: stringValue(cell.rootfieldId) ?? null, + status: stringValue(cell.status) ?? "local", + memoryCell: cell, + source: "local-devnet", + localOnly: true, + }; + }); + if (nativeCells.length > 0) { + return nativeCells; + } + return state.launchCore.rootfieldBundles.map((bundle) => ({ + schema: "flowmemory.control_plane.memory_cell_row.v0", + memoryCellId: bundle.rootfieldId, + rootfieldId: bundle.rootfieldId, + status: bundle.status, + latestRoot: bundle.latestRoot, + rootfieldBundle: bundle, + source: "launch-core-projection", + localOnly: true, + })); +} + +function challengeRows(state: LoadedControlPlaneState): JsonObject[] { + return Object.entries(devnetChallenges(state)).map(([challengeId, value]) => { + const challenge = asJsonObject(value) ?? {}; + return { + schema: "flowmemory.control_plane.challenge_row.v0", + challengeId, + targetId: stringValue(challenge.targetId) ?? stringValue(challenge.receiptId) ?? null, + status: stringValue(challenge.status) ?? "unknown", + challenge, + source: "local-devnet", + localOnly: true, + }; + }).sort((left, right) => String(left.challengeId).localeCompare(String(right.challengeId))); +} + +function finalityRows(state: LoadedControlPlaneState): JsonObject[] { + const rows = Object.entries(devnetFinalityReceipts(state)).map(([finalityReceiptId, value]) => { + const finality = asJsonObject(value) ?? {}; + const sourceStatus = stringValue(finality.finalityStatus) ?? stringValue(finality.status); + return { + schema: "flowmemory.control_plane.finality_row.v0", + finalityReceiptId, + objectId: stringValue(finality.receiptId) ?? stringValue(finality.rootfieldId) ?? finalityReceiptId, + status: finalityStatusFor(sourceStatus), + sourceStatus, + finalityReceipt: finality, + source: "local-devnet", + localOnly: true, + }; + }); + for (const receipt of state.launchCore.memoryReceipts) { + rows.push({ + schema: "flowmemory.control_plane.finality_row.v0", + finalityReceiptId: stableId("flowmemory.control_plane.finality.projected.v0", receipt.receiptId), + objectId: receipt.receiptId, + rootfieldId: receipt.rootfieldId, + status: finalityStatusFor(receipt.flowMemoryStatus), + sourceStatus: receipt.flowMemoryStatus, + source: "launch-core-projection", + localOnly: true, + }); + } + return rows.sort((left, right) => String(left.objectId).localeCompare(String(right.objectId))); +} + +function health(_params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const missing = Object.values(state.sources).filter((source) => source.status === "missing").map((source) => source.name); + return { + schema: "flowmemory.control_plane.health.v0", + service: "flowmemory-control-plane-v0", + status: missing.length === 0 ? "ok" : "degraded", + localOnly: true, + checks: { + launchCore: state.sources.launchCore.status, + indexer: state.sources.indexer.status, + verifier: state.sources.verifier.status, + artifacts: state.sources.artifacts.status, + devnet: state.sources.devnet.status, + devnetControlPlaneHandoff: state.sources.devnetControlPlaneHandoff.status, + txFixtures: state.sources.txFixtures.status, + }, + counts: { + observations: state.indexer.state.observations.length, + verifierReports: state.verifier.reports.length, + rootfields: rootfieldRows(state).length, + blocks: blockRows(state).length, + transactions: transactionRows(state).length, + }, + missingOptionalSources: missing, + }; +} + +function chainStatus(_params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const latest = latestBlock(state); + + return { + schema: "flowmemory.control_plane.chain_status.v0", + chainId: "flowmemory-local-alpha", + settlementContext: "local fixture stack over FlowPulse and local no-value devnet handoff", + environment: "local-devnet-fixture", + source: "fixture", + currentBlock: latest.blockNumber, + currentBlockHash: latest.blockHash, + finalizedBlock: finalizedBlock(state), + generatedAt: state.launchCore.generatedAt, + localOnly: true, + counts: { + observations: state.indexer.state.observations.length, + rejectedLogs: state.indexer.state.rejectedLogs.length, + duplicates: state.indexer.state.duplicates.length, + memorySignals: state.launchCore.memorySignals.length, + memoryReceipts: state.launchCore.memoryReceipts.length, + verifierReports: state.verifier.reports.length, + rootfields: rootfieldRows(state).length, + agents: agentRows(state).length, + models: modelRows(state).length, + workReceipts: workReceiptRows(state).length, + artifactAvailability: artifactAvailabilityRows(state).length, + verifierModules: verifierModuleRows(state).length, + memoryCells: memoryCellRows(state).length, + challenges: challengeRows(state).length, + finalityRows: finalityRows(state).length, + blocks: blockRows(state).length, + transactions: transactionRows(state).length, + devnetBlocks: devnetBlocksArray(state).length, + }, + capabilities: [ + "health_reads", + "fixture_status_reads", + "block_reads", + "transaction_reads", + "receipt_lookup", + "verifier_report_lookup", + "memory_lineage_lookup", + "artifact_fixture_lookup", + "devnet_handoff_reads", + "raw_json_reads", + ], + limitations: [ + "No production RPC URLs, wallets, or hosted services are used.", + "No production L1, bridge, tokenomics, or verifier economics are implied.", + "Challenge and finality methods expose local V0 fixture state only.", + ], + dataSources: state.sources, + }; +} + +function devnetState(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "devnet_state"); + const includeBlocks = optionalBoolean(objectParams, "includeBlocks"); + const blocks = devnetBlocksArray(state); + + return { + schema: "flowmemory.control_plane.devnet_state.v0", + available: state.devnet !== null, + chainId: typeof state.devnet?.chainId === "string" ? state.devnet.chainId : "flowmemory-local-devnet-v0", + genesisHash: typeof state.devnet?.genesisHash === "string" ? state.devnet.genesisHash : null, + latestBlockNumber: blocks.length > 0 ? blocks[blocks.length - 1]?.blockNumber ?? null : null, + latestBlockHash: blocks.length > 0 ? blocks[blocks.length - 1]?.blockHash ?? null : null, + stateRoot: typeof state.devnetControlPlaneHandoff?.stateRoot === "string" + ? state.devnetControlPlaneHandoff.stateRoot + : typeof state.devnetIndexerHandoff?.stateRoot === "string" + ? state.devnetIndexerHandoff.stateRoot + : state.devnet?.parentHash ?? null, + rootfieldCount: Object.keys(devnetRootfields(state)).length, + workReceiptCount: Object.keys(devnetWorkReceipts(state)).length, + verifierReportCount: Object.keys(devnetReports(state)).length, + agentAccountCount: Object.keys(devnetAgentAccounts(state)).length, + modelCount: Object.keys(devnetModels(state)).length, + verifierModuleCount: Object.keys(devnetVerifierModules(state)).length, + artifactAvailabilityCount: Object.keys(devnetArtifactAvailability(state)).length, + memoryCellCount: Object.keys(devnetMemoryCells(state)).length, + challengeCount: Object.keys(devnetChallenges(state)).length, + finalityReceiptCount: Object.keys(devnetFinalityReceipts(state)).length, + baseAnchorCount: state.devnet?.baseAnchors && typeof state.devnet.baseAnchors === "object" && !Array.isArray(state.devnet.baseAnchors) + ? Object.keys(state.devnet.baseAnchors).length + : 0, + blocks: includeBlocks ? blocks : undefined, + source: state.sources.devnet, + indexerHandoff: state.devnetIndexerHandoff === null ? null : { + schema: state.devnetIndexerHandoff.schema, + stateRoot: state.devnetIndexerHandoff.stateRoot, + blockCount: Array.isArray(state.devnetIndexerHandoff.blocks) ? state.devnetIndexerHandoff.blocks.length : 0, + }, + verifierHandoff: state.devnetVerifierHandoff === null ? null : { + schema: state.devnetVerifierHandoff.schema, + stateRoot: state.devnetVerifierHandoff.stateRoot, + workReceiptCount: Object.keys(devnetWorkReceipts(state)).length, + verifierReportCount: Object.keys(devnetReports(state)).length, + }, + controlPlaneHandoff: state.devnetControlPlaneHandoff === null ? null : { + schema: state.devnetControlPlaneHandoff.schema, + stateRoot: state.devnetControlPlaneHandoff.stateRoot, + blockCount: asJsonArray(state.devnetControlPlaneHandoff.blocks).length, + objectGroups: Object.keys(asJsonObject(state.devnetControlPlaneHandoff.objects) ?? {}).length, + }, + localOnly: true, + }; +} + +function blockList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "block_list"); + const limit = pageLimit(objectParams); + const source = optionalString(objectParams, "source"); + const includeTransactions = optionalBoolean(objectParams, "includeTransactions"); + const rows = blockRows(state, includeTransactions) + .filter((block) => source === undefined || block.source === source) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.block_list.v0", + count: rows.length, + nextCursor: null, + blocks: rows, + localOnly: true, + }; +} + +function blockGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "block_get"); + const key = requiredString(objectParams, ["blockHash", "blockNumber"], "block_get"); + const includeTransactions = optionalBoolean(objectParams, "includeTransactions"); + const block = blockRows(state, includeTransactions).find((candidate) => { + return candidate.blockHash === key || String(candidate.blockNumber) === key; + }); + if (block === undefined) { + throw objectNotFound(`block not found: ${key}`, { id: key }); + } + return { + schema: "flowmemory.control_plane.block_detail.v0", + block, + provenance: { + sources: [ + block.source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.block.v0") + : provenanceSource("indexer", "services/indexer/out/indexer-state.json", "flowmemory.indexer.persistence.v0"), + ], + }, + localOnly: true, + }; +} + +function transactionList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "transaction_list"); + const limit = pageLimit(objectParams); + const blockNumber = optionalString(objectParams, "blockNumber"); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const source = optionalString(objectParams, "source"); + const rows = transactionRows(state) + .filter((tx) => blockNumber === undefined || tx.blockNumber === blockNumber) + .filter((tx) => status === undefined || tx.status === status) + .filter((tx) => source === undefined || tx.source === source) + .filter((tx) => rootfieldId === undefined || stringList(tx.rootfieldIds).includes(rootfieldId) || tx.rootfieldId === rootfieldId) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.transaction_list.v0", + count: rows.length, + nextCursor: null, + transactions: rows, + localOnly: true, + }; +} + +function transactionGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "transaction_get"); + const key = requiredString(objectParams, ["txId", "txHash", "transactionId"], "transaction_get"); + const transaction = transactionRows(state).find((candidate) => { + return candidate.transactionId === key || candidate.txHash === key; + }); + if (transaction === undefined) { + throw objectNotFound(`transaction not found: ${key}`, { id: key }); + } + return { + schema: "flowmemory.control_plane.transaction_detail.v0", + transaction, + provenance: { + sources: [ + transaction.source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.block.v0") + : provenanceSource("indexer", "services/indexer/out/indexer-state.json", "flowmemory.indexer.persistence.v0"), + ], + }, + localOnly: true, + }; +} + +function rootfieldGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "rootfield_get"); + const rootfieldId = requiredString(objectParams, ["rootfieldId"], "rootfield_get"); + const bundle = state.launchCore.rootfieldBundles.find((candidate) => candidate.rootfieldId === rootfieldId); + const devnetRootfield = devnetRootfields(state)[rootfieldId] ?? null; + + if (bundle === undefined && devnetRootfield === null) { + throw objectNotFound(`rootfield not found: ${rootfieldId}`, { rootfieldId }); + } + + return { + schema: "flowmemory.control_plane.rootfield.v0", + rootfieldId, + bundle: bundle ?? null, + devnetRootfield, + memoryCellId: rootfieldId, + agentViewId: state.launchCore.agentMemoryViews.find((view) => view.rootfieldId === rootfieldId)?.viewId ?? null, + provenance: { + sources: [ + bundle ? provenanceSource("flowmemory", "fixtures/launch-core/flowmemory-launch-v0.json", bundle.schema) : null, + devnetRootfield ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.rootfield.v0") : null, + ].filter((entry): entry is JsonObject => entry !== null), + }, + localOnly: true, + }; +} + +function rootfieldList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "rootfield_list"); + const limit = pageLimit(objectParams); + const status = optionalString(objectParams, "status"); + const rows = rootfieldRows(state) + .filter((rootfield) => status === undefined || rootfield.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.rootfield_list.v0", + count: rows.length, + nextCursor: null, + rootfields: rows, + localOnly: true, + }; +} + +function artifactGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "artifact_get"); + const uri = optionalString(objectParams, "uri"); + const artifactId = optionalString(objectParams, "artifactId"); + const commitment = optionalString(objectParams, "commitment"); + + if (uri === undefined && artifactId === undefined && commitment === undefined) { + throw invalidParams("artifact_get requires uri, artifactId, or commitment"); + } + + if (uri !== undefined) { + const artifact = state.artifacts.artifactsByUri[uri]; + if (artifact !== undefined) { + return { + schema: "flowmemory.control_plane.artifact.v0", + artifactId: stableId("flowmemory.control_plane.artifact.fixture.v0", uri), + uri, + artifact, + resolverPolicyId: state.artifacts.resolverPolicyId, + provenance: { + sources: [provenanceSource("verifier", "services/verifier/fixtures/artifacts.json", "flowmemory.verifier.artifact_fixture.v0")], + }, + localOnly: true, + }; + } + } + + for (const [id, value] of Object.entries(devnetArtifacts(state))) { + const entry = value as JsonObject; + if (artifactId === id || artifactId === entry.artifactId || commitment === entry.commitment || uri === entry.uriHint) { + return { + schema: "flowmemory.control_plane.artifact.v0", + artifactId: id, + uri: entry.uriHint ?? null, + artifact: entry, + resolverPolicyId: "flowmemory.local_devnet.artifact_commitment.v0", + provenance: { + sources: [provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.artifact_commitment.v0")], + }, + localOnly: true, + }; + } + } + + throw objectNotFound("artifact not found", { uri, artifactId, commitment }); +} + +function artifactAvailabilityList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "artifact_availability_list"); + const limit = pageLimit(objectParams); + const status = optionalString(objectParams, "status"); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const rows = artifactAvailabilityRows(state) + .filter((artifact) => status === undefined || artifact.status === status) + .filter((artifact) => rootfieldId === undefined || artifact.rootfieldId === rootfieldId) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.artifact_availability_list.v0", + count: rows.length, + nextCursor: null, + artifacts: rows, + localOnly: true, + }; +} + +function artifactAvailabilityGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "artifact_availability_get"); + const key = requiredString(objectParams, ["availabilityId", "artifactId", "commitment", "uri"], "artifact_availability_get"); + const artifact = artifactAvailabilityRows(state).find((candidate) => { + return candidate.availabilityId === key + || candidate.artifactId === key + || candidate.commitment === key + || candidate.uri === key; + }); + if (artifact === undefined) { + throw objectNotFound(`artifact availability not found: ${key}`, { id: key }); + } + return { + schema: "flowmemory.control_plane.artifact_availability_detail.v0", + artifactAvailability: artifact, + provenance: artifact.source === "verifier-fixture" + ? { + sources: [provenanceSource("verifier", "services/verifier/fixtures/artifacts.json", "flowmemory.verifier.artifact_fixture.v0")], + } + : { + sources: [provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.artifact_commitment.v0")], + }, + localOnly: true, + }; +} + +function receiptGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "receipt_get"); + const key = requiredString(objectParams, ["receiptId", "observationId", "reportId"], "receipt_get"); + const receipt = receiptByAnyId(state, key); + + if (receipt !== undefined) { + const signal = signalByObservation(state, receipt.observationId) ?? null; + const transition = transitionByAnyId(state, receipt.receiptId) ?? null; + return { + schema: "flowmemory.control_plane.receipt.v0", + receipt, + signal, + transition, + verifierReport: reportByIdOrObservation(state, receipt.reportId) ?? null, + provenance: provenanceForObject(state, receipt.receiptId), + localOnly: true, + }; + } + + const devnetReceipt = devnetWorkReceipts(state)[key]; + if (devnetReceipt !== undefined) { + return { + schema: "flowmemory.control_plane.receipt.v0", + receipt: devnetReceipt, + signal: null, + transition: null, + verifierReport: null, + provenance: { + sources: [provenanceSource("devnet", "fixtures/launch-core/generated/devnet/verifier-handoff.json", "flowmemory.local_devnet.work_receipt.v0")], + }, + localOnly: true, + }; + } + + throw objectNotFound(`receipt not found: ${key}`, { id: key }); +} + +function receiptList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "receipt_list"); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const limit = pageLimit(objectParams); + const receipts = state.launchCore.memoryReceipts + .filter((receipt) => rootfieldId === undefined || receipt.rootfieldId === rootfieldId) + .filter((receipt) => status === undefined || receipt.flowMemoryStatus === status || receipt.verifierStatus === status) + .slice(0, limit); + + return { + schema: "flowmemory.control_plane.receipt_list.v0", + count: receipts.length, + nextCursor: null, + receipts, + localOnly: true, + }; +} + +function workReceiptList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "work_receipt_list"); + const limit = pageLimit(objectParams); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const rows = workReceiptRows(state) + .filter((receipt) => rootfieldId === undefined || receipt.rootfieldId === rootfieldId) + .filter((receipt) => status === undefined || receipt.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.work_receipt_list.v0", + count: rows.length, + nextCursor: null, + workReceipts: rows, + localOnly: true, + }; +} + +function workReceiptGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "work_receipt_get"); + const key = requiredString(objectParams, ["workReceiptId", "receiptId", "observationId", "reportId"], "work_receipt_get"); + const row = workReceiptRows(state).find((receipt) => { + return receipt.workReceiptId === key + || receipt.receiptId === key + || asJsonObject(receipt.memoryReceipt)?.observationId === key + || asJsonObject(receipt.memoryReceipt)?.reportId === key; + }); + if (row === undefined) { + throw objectNotFound(`work receipt not found: ${key}`, { id: key }); + } + return { + schema: "flowmemory.control_plane.work_receipt.v0", + workReceipt: row, + provenance: provenanceForObject(state, stringValue(row.receiptId) ?? key), + localOnly: true, + }; +} + +function verifierModuleList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "verifier_module_list"); + const limit = pageLimit(objectParams); + const status = optionalString(objectParams, "status"); + const rows = verifierModuleRows(state) + .filter((module) => status === undefined || module.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.verifier_module_list.v0", + count: rows.length, + nextCursor: null, + verifierModules: rows, + localOnly: true, + }; +} + +function verifierModuleGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "verifier_module_get"); + const key = requiredString(objectParams, ["moduleId", "verifierId", "resolverPolicyId"], "verifier_module_get"); + const verifierModule = verifierModuleRows(state).find((candidate) => { + return candidate.moduleId === key + || candidate.verifierId === key + || candidate.resolverPolicyId === key; + }); + if (verifierModule === undefined) { + throw objectNotFound(`verifier module not found: ${key}`, { id: key }); + } + return { + schema: "flowmemory.control_plane.verifier_module_detail.v0", + verifierModule, + provenance: { + sources: [ + verifierModule.source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.verifier_module.v0") + : provenanceSource("verifier", "services/verifier/out/reports.json", "flowmemory.verifier.persistence.v0"), + ], + }, + localOnly: true, + }; +} + +function verifierReportGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "verifier_report_get"); + const key = requiredString(objectParams, ["reportId", "observationId"], "verifier_report_get"); + const report = reportByIdOrObservation(state, key); + + if (report !== undefined) { + return { + schema: "flowmemory.control_plane.verifier_report.v0", + report, + memoryReceipt: receiptByAnyId(state, report.reportId) ?? null, + provenance: provenanceForObject(state, report.reportId), + localOnly: true, + }; + } + + const devnetReport = devnetReports(state)[key]; + if (devnetReport !== undefined) { + return { + schema: "flowmemory.control_plane.verifier_report.v0", + report: devnetReport, + memoryReceipt: null, + provenance: { + sources: [provenanceSource("devnet", "fixtures/launch-core/generated/devnet/verifier-handoff.json", "flowmemory.local_devnet.verifier_report.v0")], + }, + localOnly: true, + }; + } + + throw objectNotFound(`verifier report not found: ${key}`, { id: key }); +} + +function verifierReportList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "verifier_report_list"); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const limit = pageLimit(objectParams); + const reports = state.verifier.reports + .filter((report) => rootfieldId === undefined || report.reportCore.observation.rootfieldId === rootfieldId) + .filter((report) => status === undefined || report.reportCore.status === status) + .slice(0, limit); + + return { + schema: "flowmemory.control_plane.verifier_report_list.v0", + count: reports.length, + nextCursor: null, + reports, + localOnly: true, + }; +} + +function memoryCellGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "memory_cell_get"); + const key = requiredString(objectParams, ["memoryCellId", "rootfieldId"], "memory_cell_get"); + const rootfieldId = key.startsWith("memory:") ? key.slice("memory:".length) : key; + const devnetCellEntry = Object.entries(devnetMemoryCells(state)).find(([id, value]) => { + const cell = value as JsonObject; + return id === key || cell.memoryCellId === key || cell.rootfieldId === key || cell.rootfieldId === rootfieldId; + }); + const bundle = state.launchCore.rootfieldBundles.find((candidate) => candidate.rootfieldId === rootfieldId); + const agentView = state.launchCore.agentMemoryViews.find((view) => view.rootfieldId === rootfieldId) ?? null; + + if (bundle === undefined && agentView === null && devnetCellEntry === undefined) { + throw objectNotFound(`memory cell not found: ${key}`, { id: key }); + } + + const devnetCell = devnetCellEntry?.[1] as JsonObject | undefined; + return { + schema: "flowmemory.control_plane.memory_cell.v0", + memoryCellId: typeof devnetCell?.memoryCellId === "string" ? devnetCell.memoryCellId : rootfieldId, + rootfieldId: typeof devnetCell?.rootfieldId === "string" ? devnetCell.rootfieldId : rootfieldId, + status: typeof devnetCell?.status === "string" ? devnetCell.status : bundle?.status ?? agentView?.status ?? "observed", + latestRoot: typeof devnetCell?.currentRoot === "string" ? devnetCell.currentRoot : bundle?.latestRoot ?? agentView?.latestRoot ?? ZERO_ROOT, + devnetMemoryCell: devnetCell ?? null, + rootfieldBundle: bundle ?? null, + agentMemoryView: agentView, + extensionPoint: devnetCell === undefined + ? "Native MemoryCell handoff files are not emitted yet; this V0 object is projected from RootfieldBundle and AgentMemoryView fixtures." + : "Loaded from local devnet memoryCells handoff.", + provenance: provenanceForObject(state, rootfieldId), + localOnly: true, + }; +} + +function memoryCellList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "memory_cell_list"); + const limit = pageLimit(objectParams); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const rows = memoryCellRows(state) + .filter((cell) => rootfieldId === undefined || cell.rootfieldId === rootfieldId) + .filter((cell) => status === undefined || cell.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.memory_cell_list.v0", + count: rows.length, + nextCursor: null, + memoryCells: rows, + localOnly: true, + }; +} + +function agentGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "agent_get"); + const key = requiredString(objectParams, ["agentId", "viewId", "rootfieldId"], "agent_get"); + const devnetAgentEntry = Object.entries(devnetAgentAccounts(state)).find(([id, value]) => { + const agent = value as JsonObject; + return id === key || agent.agentId === key || agent.memoryRoot === key; + }); + const view = state.launchCore.agentMemoryViews.find((candidate) => { + return candidate.viewId === key || candidate.rootfieldId === key; + }); + + if (view === undefined && devnetAgentEntry === undefined) { + throw objectNotFound(`agent memory view not found: ${key}`, { id: key }); + } + + const devnetAgent = devnetAgentEntry?.[1] as JsonObject | undefined; + return { + schema: "flowmemory.control_plane.agent.v0", + agentId: typeof devnetAgent?.agentId === "string" ? devnetAgent.agentId : view?.viewId ?? key, + agentAccount: devnetAgent ?? null, + agentMemoryView: view ?? null, + rootfieldBundle: view === undefined ? null : state.launchCore.rootfieldBundles.find((bundle) => bundle.rootfieldId === view.rootfieldId) ?? null, + provenance: provenanceForObject(state, view?.viewId ?? key), + localOnly: true, + }; +} + +function agentList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "agent_list"); + const limit = pageLimit(objectParams); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const rows = agentRows(state) + .filter((agent) => rootfieldId === undefined || agent.rootfieldId === rootfieldId) + .filter((agent) => status === undefined || agent.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.agent_list.v0", + count: rows.length, + nextCursor: null, + agents: rows, + localOnly: true, + }; +} + +function modelList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "model_list"); + const limit = pageLimit(objectParams); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const status = optionalString(objectParams, "status"); + const rows = modelRows(state) + .filter((model) => rootfieldId === undefined || model.rootfieldId === rootfieldId) + .filter((model) => status === undefined || model.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.model_list.v0", + count: rows.length, + nextCursor: null, + models: rows, + localOnly: true, + }; +} + +function modelGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "model_get"); + const key = requiredString(objectParams, ["modelId", "rootfieldId"], "model_get"); + const model = modelRows(state).find((candidate) => candidate.modelId === key || candidate.rootfieldId === key); + if (model === undefined) { + throw objectNotFound(`model not found: ${key}`, { id: key }); + } + return { + schema: "flowmemory.control_plane.model_detail.v0", + model, + provenance: { + sources: [ + model.source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.model_passport.v0") + : provenanceSource("flowmemory", "fixtures/launch-core/flowmemory-launch-v0.json", "flowmemory.agent_memory_view.v0", "Projected model row; no ModelPassport fixture exists yet."), + ], + }, + localOnly: true, + }; +} + +function challengeGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "challenge_get"); + const targetId = requiredString(objectParams, ["challengeId", "targetId", "receiptId", "reportId", "rootfieldId"], "challenge_get"); + const challengeEntry = Object.entries(devnetChallenges(state)).find(([id, value]) => { + const challenge = value as JsonObject; + return id === targetId || challenge.challengeId === targetId || challenge.receiptId === targetId; + }); + if (challengeEntry !== undefined) { + return { + schema: "flowmemory.control_plane.challenge.v0", + challengeId: challengeEntry[0], + challenge: challengeEntry[1] as JsonObject, + status: (challengeEntry[1] as JsonObject).status ?? "unknown", + provenance: provenanceForObject(state, challengeEntry[0]), + localOnly: true, + }; + } + + const target = findObject(state, targetId); + + return { + schema: "flowmemory.control_plane.challenge.v0", + challengeId: stableId("flowmemory.control_plane.challenge.placeholder.v0", targetId), + targetId, + targetType: target.type, + status: "not_opened", + reasonCodes: [], + openedAt: null, + closesAt: null, + extensionPoint: "No challenge handoff fixture exists in V0. This method reserves the stable read shape for later challenge state.", + localOnly: true, + }; +} + +function challengeList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "challenge_list"); + const limit = pageLimit(objectParams); + const status = optionalString(objectParams, "status"); + const rows = challengeRows(state) + .filter((challenge) => status === undefined || challenge.status === status) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.challenge_list.v0", + count: rows.length, + nextCursor: null, + challenges: rows, + extensionPoint: rows.length === 0 + ? "No challenge handoff fixture exists in V0; challenge_get can still return a stable not_opened placeholder for known local objects." + : undefined, + localOnly: true, + }; +} + +function finalityStatusFor(status: string | null): string { + if (status === "verified" || status === "valid" || status === "finalized" || status === "local-placeholder") { + return "local-finalized"; + } + if (status === "failed" || status === "invalid") { + return "local-rejected"; + } + if (status === "reorged" || status === "removed") { + return "reorged"; + } + if (status === "unsupported") { + return "local-unsupported"; + } + return "local-pending"; +} + +function finalityGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "finality_get"); + const key = requiredString(objectParams, ["objectId", "rootfieldId", "receiptId", "reportId", "transitionId"], "finality_get"); + const finalityEntry = Object.entries(devnetFinalityReceipts(state)).find(([id, value]) => { + const finality = value as JsonObject; + return id === key || finality.finalityReceiptId === key || finality.receiptId === key || finality.rootfieldId === key; + }); + if (finalityEntry !== undefined) { + const finality = finalityEntry[1] as JsonObject; + const sourceStatus = typeof finality.finalityStatus === "string" ? finality.finalityStatus : null; + return { + schema: "flowmemory.control_plane.finality.v0", + objectId: key, + objectType: "devnet_finality_receipt", + finalityReceipt: finality, + status: finalityStatusFor(sourceStatus), + sourceStatus, + challengeWindow: null, + settlement: "local-devnet-fixture", + limitations: [ + "This is fixture/devnet finality only.", + "No production consensus, bridge finality, verifier economics, or challenge market is implied.", + ], + localOnly: true, + }; + } + + const target = findObject(state, key); + const status = typeof target.object.status === "string" + ? target.object.status + : typeof target.object.flowMemoryStatus === "string" + ? target.object.flowMemoryStatus + : typeof target.object.verifierStatus === "string" + ? target.object.verifierStatus + : typeof target.object.finalityStatus === "string" + ? target.object.finalityStatus + : null; + + return { + schema: "flowmemory.control_plane.finality.v0", + objectId: key, + objectType: target.type, + status: finalityStatusFor(status), + sourceStatus: status, + challengeWindow: null, + settlement: "local-fixture", + limitations: [ + "This is fixture/devnet finality only.", + "No production consensus, bridge finality, verifier economics, or challenge market is implied.", + ], + localOnly: true, + }; +} + +function finalityList(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "finality_list"); + const limit = pageLimit(objectParams); + const status = optionalString(objectParams, "status"); + const rootfieldId = optionalString(objectParams, "rootfieldId"); + const rows = finalityRows(state) + .filter((finality) => status === undefined || finality.status === status) + .filter((finality) => rootfieldId === undefined || finality.rootfieldId === rootfieldId) + .slice(0, limit); + return { + schema: "flowmemory.control_plane.finality_list.v0", + count: rows.length, + nextCursor: null, + finality: rows, + localOnly: true, + }; +} + +function findObject(state: LoadedControlPlaneState, key: string): { type: string; object: JsonObject } { + const receipt = receiptByAnyId(state, key); + if (receipt !== undefined) { + return { type: "memory_receipt", object: receipt as unknown as JsonObject }; + } + const report = reportByIdOrObservation(state, key); + if (report !== undefined) { + return { type: "verifier_report", object: report.reportCore as unknown as JsonObject }; + } + const signal = state.launchCore.memorySignals.find((candidate) => candidate.signalId === key || candidate.observationId === key || candidate.pulseId === key); + if (signal !== undefined) { + return { type: "memory_signal", object: signal as unknown as JsonObject }; + } + const transition = transitionByAnyId(state, key); + if (transition !== undefined) { + return { type: "rootflow_transition", object: transition as unknown as JsonObject }; + } + const bundle = state.launchCore.rootfieldBundles.find((candidate) => candidate.bundleId === key || candidate.rootfieldId === key); + if (bundle !== undefined) { + return { type: "rootfield_bundle", object: bundle as unknown as JsonObject }; + } + const view = state.launchCore.agentMemoryViews.find((candidate) => candidate.viewId === key || candidate.rootfieldId === key); + if (view !== undefined) { + return { type: "agent_memory_view", object: view as unknown as JsonObject }; + } + const block = blockRows(state).find((candidate) => candidate.blockHash === key || candidate.blockNumber === key); + if (block !== undefined) { + return { type: "block", object: block }; + } + const transaction = transactionRows(state).find((candidate) => candidate.transactionId === key || candidate.txHash === key); + if (transaction !== undefined) { + return { type: "transaction", object: transaction }; + } + const model = modelRows(state).find((candidate) => candidate.modelId === key || candidate.rootfieldId === key); + if (model !== undefined) { + return { type: "model", object: model }; + } + const verifierModule = verifierModuleRows(state).find((candidate) => candidate.moduleId === key || candidate.verifierId === key || candidate.resolverPolicyId === key); + if (verifierModule !== undefined) { + return { type: "verifier_module", object: verifierModule }; + } + const artifactAvailability = artifactAvailabilityRows(state).find((candidate) => candidate.availabilityId === key || candidate.artifactId === key || candidate.uri === key || candidate.commitment === key); + if (artifactAvailability !== undefined) { + return { type: "artifact_availability", object: artifactAvailability }; + } + const devnetReceipt = devnetWorkReceipts(state)[key]; + if (devnetReceipt !== undefined) { + return { type: "devnet_work_receipt", object: devnetReceipt as JsonObject }; + } + const devnetReport = devnetReports(state)[key]; + if (devnetReport !== undefined) { + return { type: "devnet_verifier_report", object: devnetReport as JsonObject }; + } + const devnetRootfield = devnetRootfields(state)[key]; + if (devnetRootfield !== undefined) { + return { type: "devnet_rootfield", object: devnetRootfield as JsonObject }; + } + const devnetAgent = devnetAgentAccounts(state)[key]; + if (devnetAgent !== undefined) { + return { type: "devnet_agent_account", object: devnetAgent as JsonObject }; + } + const devnetMemoryCell = devnetMemoryCells(state)[key]; + if (devnetMemoryCell !== undefined) { + return { type: "devnet_memory_cell", object: devnetMemoryCell as JsonObject }; + } + const devnetChallenge = devnetChallenges(state)[key]; + if (devnetChallenge !== undefined) { + return { type: "devnet_challenge", object: devnetChallenge as JsonObject }; + } + const devnetFinality = devnetFinalityReceipts(state)[key]; + if (devnetFinality !== undefined) { + return { type: "devnet_finality_receipt", object: devnetFinality as JsonObject }; + } + + throw objectNotFound(`object not found: ${key}`, { id: key }); +} + +function provenanceForObject(state: LoadedControlPlaneState, key: string): JsonObject { + const sources: JsonObject[] = []; + const links: JsonObject = {}; + const receipt = receiptByAnyId(state, key); + const report = reportByIdOrObservation(state, key); + const transition = transitionByAnyId(state, key); + const bundle = state.launchCore.rootfieldBundles.find((candidate) => candidate.bundleId === key || candidate.rootfieldId === key); + const view = state.launchCore.agentMemoryViews.find((candidate) => candidate.viewId === key || candidate.rootfieldId === key); + const signal = state.launchCore.memorySignals.find((candidate) => candidate.signalId === key || candidate.observationId === key || candidate.pulseId === key); + const block = blockRows(state).find((candidate) => candidate.blockHash === key || candidate.blockNumber === key); + const transaction = transactionRows(state).find((candidate) => candidate.transactionId === key || candidate.txHash === key); + const model = modelRows(state).find((candidate) => candidate.modelId === key || candidate.rootfieldId === key); + const verifierModule = verifierModuleRows(state).find((candidate) => candidate.moduleId === key || candidate.verifierId === key || candidate.resolverPolicyId === key); + const artifactAvailability = artifactAvailabilityRows(state).find((candidate) => candidate.availabilityId === key || candidate.artifactId === key || candidate.uri === key || candidate.commitment === key); + const selectedReceipt = receipt ?? (report ? receiptByAnyId(state, report.reportId) : undefined); + const selectedSignal = signal ?? (selectedReceipt ? signalByObservation(state, selectedReceipt.observationId) : undefined); + const selectedTransition = transition ?? (selectedReceipt ? transitionByAnyId(state, selectedReceipt.receiptId) : undefined); + + if (selectedSignal !== undefined || selectedTransition !== undefined || bundle !== undefined || view !== undefined || selectedReceipt !== undefined) { + sources.push(provenanceSource("flowmemory", "fixtures/launch-core/flowmemory-launch-v0.json", "flowmemory.launch_core.v0")); + } + if (selectedReceipt !== undefined || report !== undefined) { + sources.push(provenanceSource("verifier", "services/verifier/out/reports.json", "flowmemory.verifier.persistence.v0")); + } + if (selectedSignal !== undefined) { + sources.push(provenanceSource("indexer", "services/indexer/out/indexer-state.json", "flowmemory.indexer.persistence.v0")); + } + if (block !== undefined || transaction !== undefined) { + const source = block?.source ?? transaction?.source; + sources.push(source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.state.v0") + : provenanceSource("indexer", "services/indexer/out/indexer-state.json", "flowmemory.indexer.persistence.v0")); + } + if (model !== undefined) { + sources.push(model.source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.model_passport.v0") + : provenanceSource("flowmemory", "fixtures/launch-core/flowmemory-launch-v0.json", "flowmemory.agent_memory_view.v0", "Projected model row.")); + } + if (verifierModule !== undefined) { + sources.push(verifierModule.source === "local-devnet" + ? provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.verifier_module.v0") + : provenanceSource("verifier", "services/verifier/out/reports.json", "flowmemory.verifier.persistence.v0")); + } + if (artifactAvailability !== undefined) { + sources.push(artifactAvailability.source === "verifier-fixture" + ? provenanceSource("verifier", "services/verifier/fixtures/artifacts.json", "flowmemory.verifier.artifact_fixture.v0") + : provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.artifact_commitment.v0")); + } + + links.receiptId = selectedReceipt?.receiptId; + links.reportId = selectedReceipt?.reportId ?? report?.reportId; + links.observationId = selectedReceipt?.observationId ?? report?.reportCore.observationId ?? selectedSignal?.observationId; + links.signalId = selectedSignal?.signalId; + links.transitionId = selectedTransition?.transitionId; + links.rootfieldId = selectedReceipt?.rootfieldId ?? selectedSignal?.rootfieldId ?? bundle?.rootfieldId ?? view?.rootfieldId; + links.blockHash = block?.blockHash ?? transaction?.blockHash; + links.blockNumber = block?.blockNumber ?? transaction?.blockNumber; + links.txHash = transaction?.txHash; + links.modelId = model?.modelId; + links.verifierModuleId = verifierModule?.moduleId; + links.artifactAvailabilityId = artifactAvailability?.availabilityId; + links.artifactUris = report?.reportCore.evidenceRefs.map((entry) => entry.uri).filter((value): value is string => typeof value === "string") + ?? (selectedReceipt?.evidenceRefs.map((entry) => entry.uri).filter((value): value is string => typeof value === "string") ?? []); + + if (sources.length === 0) { + const devnetTarget = devnetWorkReceipts(state)[key] + ?? devnetReports(state)[key] + ?? devnetRootfields(state)[key] + ?? devnetArtifacts(state)[key] + ?? devnetAgentAccounts(state)[key] + ?? devnetMemoryCells(state)[key] + ?? devnetChallenges(state)[key] + ?? devnetFinalityReceipts(state)[key]; + if (devnetTarget !== undefined) { + sources.push(provenanceSource("devnet", "fixtures/launch-core/generated/devnet/state.json", "flowmemory.local_devnet.state.v0")); + } + } + + return { + schema: "flowmemory.control_plane.provenance.v0", + objectId: key, + sources, + links, + localOnly: true, + }; +} + +function provenanceGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "provenance_get"); + const key = requiredString(objectParams, ["objectId", "uri", "receiptId", "reportId", "rootfieldId"], "provenance_get"); + + if (optionalString(objectParams, "uri") !== undefined) { + const uri = key; + const artifact = state.artifacts.artifactsByUri[uri]; + if (artifact === undefined) { + throw objectNotFound(`artifact provenance not found: ${uri}`, { uri }); + } + return { + schema: "flowmemory.control_plane.provenance.v0", + objectId: uri, + sources: [provenanceSource("verifier", "services/verifier/fixtures/artifacts.json", "flowmemory.verifier.artifact_fixture.v0")], + links: { + artifactUri: uri, + receiptIds: state.launchCore.memoryReceipts + .filter((receipt) => receipt.evidenceRefs.some((ref) => ref.uri === uri)) + .map((receipt) => receipt.receiptId), + }, + localOnly: true, + }; + } + + const provenance = provenanceForObject(state, key); + if ((provenance.sources as JsonObject[]).length === 0) { + throw objectNotFound(`provenance not found: ${key}`, { id: key }); + } + return provenance; +} + +function rawJsonGet(params: JsonValue | undefined, context: ControlPlaneContext): JsonValue { + const state = stateFor(context); + const objectParams = asObjectParams(params, "raw_json_get"); + const source = requiredString(objectParams, ["source"], "raw_json_get"); + const allowed: Record = { + launchCore: state.launchCore as unknown as JsonValue, + indexer: state.indexer as unknown as JsonValue, + verifier: state.verifier as unknown as JsonValue, + artifacts: state.artifacts as unknown as JsonValue, + devnet: state.devnet, + devnetIndexerHandoff: state.devnetIndexerHandoff, + devnetVerifierHandoff: state.devnetVerifierHandoff, + devnetControlPlaneHandoff: state.devnetControlPlaneHandoff, + txFixtures: state.txFixtures, + }; + + if (!Object.prototype.hasOwnProperty.call(allowed, source)) { + throw invalidParams("raw_json_get source is not allowed", { + source, + allowedSources: Object.keys(allowed), + }); + } + const raw = allowed[source]; + if (raw === null) { + throw objectNotFound(`raw JSON source not loaded: ${source}`, { source }); + } + + return { + schema: "flowmemory.control_plane.raw_json.v0", + source, + dataSource: state.sources[source], + raw, + localOnly: true, + }; +} + +export const CONTROL_PLANE_METHODS: Record = { + health, + chain_status: chainStatus, + devnet_state: devnetState, + block_get: blockGet, + block_list: blockList, + transaction_get: transactionGet, + transaction_list: transactionList, + rootfield_get: rootfieldGet, + rootfield_list: rootfieldList, + artifact_get: artifactGet, + artifact_availability_get: artifactAvailabilityGet, + artifact_availability_list: artifactAvailabilityList, + receipt_get: receiptGet, + receipt_list: receiptList, + work_receipt_get: workReceiptGet, + work_receipt_list: workReceiptList, + verifier_module_get: verifierModuleGet, + verifier_module_list: verifierModuleList, + verifier_report_get: verifierReportGet, + verifier_report_list: verifierReportList, + memory_cell_get: memoryCellGet, + memory_cell_list: memoryCellList, + agent_get: agentGet, + agent_list: agentList, + model_get: modelGet, + model_list: modelList, + challenge_get: challengeGet, + challenge_list: challengeList, + finality_get: finalityGet, + finality_list: finalityList, + provenance_get: provenanceGet, + raw_json_get: rawJsonGet, +}; + +export function callControlPlaneMethod( + method: string, + params: JsonValue | undefined, + context: ControlPlaneContext = {}, +): JsonValue { + const handler = CONTROL_PLANE_METHODS[method as ControlPlaneMethod]; + if (handler === undefined) { + throw methodNotFound(`control-plane method not found: ${method}`, { method }); + } + return handler(params, context); +} diff --git a/services/control-plane/src/server.ts b/services/control-plane/src/server.ts new file mode 100644 index 00000000..5ca64028 --- /dev/null +++ b/services/control-plane/src/server.ts @@ -0,0 +1,110 @@ +import { createServer } from "node:http"; +import { fileURLToPath } from "node:url"; + +import { dispatchJsonRpc } from "./json-rpc.ts"; +import { loadControlPlaneState } from "./fixture-state.ts"; + +interface ServerOptions { + host: string; + port: number; +} + +function parseArgs(args: string[]): ServerOptions { + const options: ServerOptions = { + host: "127.0.0.1", + port: 8675, + }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--host") { + const value = args[index + 1]; + if (value === undefined) { + throw new Error("--host requires a value"); + } + options.host = value; + index += 1; + continue; + } + if (arg === "--port") { + const value = args[index + 1]; + if (value === undefined || !/^\d+$/.test(value)) { + throw new Error("--port requires a numeric value"); + } + options.port = Number(value); + index += 1; + continue; + } + throw new Error(`unknown option: ${arg}`); + } + + return options; +} + +export function startControlPlaneServer(options: ServerOptions): ReturnType { + const state = loadControlPlaneState(); + const server = createServer((req, res) => { + if (req.method === "GET" && req.url === "/health") { + const response = dispatchJsonRpc({ jsonrpc: "2.0", id: "health", method: "health" }, { state }); + res.writeHead(200, { "content-type": "application/json" }); + res.end(`${JSON.stringify(Array.isArray(response) ? response : response?.result ?? response)}\n`); + return; + } + + if (req.method !== "POST" || req.url !== "/rpc") { + res.writeHead(404, { "content-type": "application/json" }); + res.end(JSON.stringify({ error: "not found" })); + return; + } + + let body = ""; + req.setEncoding("utf8"); + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + try { + const payload = JSON.parse(body) as unknown; + const response = dispatchJsonRpc(payload, { state }); + if (response === undefined) { + res.writeHead(204); + res.end(); + return; + } + res.writeHead(200, { "content-type": "application/json" }); + res.end(`${JSON.stringify(response)}\n`); + } catch (error) { + res.writeHead(400, { "content-type": "application/json" }); + res.end(JSON.stringify({ + jsonrpc: "2.0", + id: null, + error: { + code: -32700, + message: error instanceof Error ? error.message : "parse error", + data: { + schema: "flowmemory.control_plane.error.v0", + reasonCode: "parse.error", + localOnly: true, + }, + }, + })); + } + }); + }); + + server.listen(options.port, options.host, () => { + const address = server.address(); + const port = typeof address === "object" && address !== null ? address.port : options.port; + const host = typeof address === "object" && address !== null ? address.address : options.host; + console.log(JSON.stringify({ + service: "flowmemory-control-plane-v0", + url: `http://${host}:${port}/rpc`, + localOnly: true, + }, null, 2)); + }); + return server; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + startControlPlaneServer(parseArgs(process.argv.slice(2))); +} diff --git a/services/control-plane/src/smoke.ts b/services/control-plane/src/smoke.ts new file mode 100644 index 00000000..482a23e9 --- /dev/null +++ b/services/control-plane/src/smoke.ts @@ -0,0 +1,101 @@ +import { fileURLToPath } from "node:url"; + +import { dispatchJsonRpc } from "./json-rpc.ts"; +import { loadControlPlaneState } from "./fixture-state.ts"; +import type { JsonObject, RpcErrorResponse, RpcSuccessResponse } from "./types.ts"; + +function firstDevnetBlock(state: ReturnType): JsonObject { + const blocks = Array.isArray(state.devnet?.blocks) ? state.devnet.blocks : []; + const block = blocks[0]; + if (block === null || typeof block !== "object" || Array.isArray(block)) { + throw new Error("control-plane smoke requires at least one local devnet block"); + } + return block as JsonObject; +} + +function stringField(value: unknown, name: string): string { + if (typeof value !== "string" && typeof value !== "number") { + throw new Error(`control-plane smoke missing ${name}`); + } + return String(value); +} + +export function runControlPlaneSmoke(): JsonObject { + const state = loadControlPlaneState(); + const rootfieldId = state.launchCore.rootfieldBundles[0]?.rootfieldId; + const receipt = state.launchCore.memoryReceipts[0]; + const reportId = receipt?.reportId; + const artifactUri = receipt?.evidenceRefs[0]?.uri; + const block = firstDevnetBlock(state); + const txIds = Array.isArray(block.txIds) ? block.txIds : []; + const txId = stringField(txIds[0], "devnet txId"); + + if (rootfieldId === undefined || receipt === undefined || reportId === undefined || artifactUri === undefined) { + throw new Error("control-plane smoke requires launch-core rootfield, receipt, report, and artifact fixture data"); + } + + const requests = [ + { jsonrpc: "2.0", id: "health", method: "health" }, + { jsonrpc: "2.0", id: "chain", method: "chain_status" }, + { jsonrpc: "2.0", id: "devnet", method: "devnet_state", params: { includeBlocks: true } }, + { jsonrpc: "2.0", id: "blocks", method: "block_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "block", method: "block_get", params: { blockNumber: stringField(block.blockNumber, "blockNumber"), includeTransactions: true } }, + { jsonrpc: "2.0", id: "transactions", method: "transaction_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "transaction", method: "transaction_get", params: { txId } }, + { jsonrpc: "2.0", id: "rootfields", method: "rootfield_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "rootfield", method: "rootfield_get", params: { rootfieldId } }, + { jsonrpc: "2.0", id: "agents", method: "agent_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "agent", method: "agent_get", params: { rootfieldId } }, + { jsonrpc: "2.0", id: "models", method: "model_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "model", method: "model_get", params: { rootfieldId } }, + { jsonrpc: "2.0", id: "workReceipts", method: "work_receipt_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "workReceipt", method: "work_receipt_get", params: { receiptId: receipt.receiptId } }, + { jsonrpc: "2.0", id: "artifactAvailability", method: "artifact_availability_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "artifact", method: "artifact_availability_get", params: { uri: artifactUri } }, + { jsonrpc: "2.0", id: "modules", method: "verifier_module_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "module", method: "verifier_module_get", params: { resolverPolicyId: receipt.resolverPolicyId } }, + { jsonrpc: "2.0", id: "reports", method: "verifier_report_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "report", method: "verifier_report_get", params: { reportId } }, + { jsonrpc: "2.0", id: "receipts", method: "receipt_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "receipt", method: "receipt_get", params: { receiptId: receipt.receiptId } }, + { jsonrpc: "2.0", id: "memoryCells", method: "memory_cell_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "memoryCell", method: "memory_cell_get", params: { rootfieldId } }, + { jsonrpc: "2.0", id: "challenges", method: "challenge_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "challenge", method: "challenge_get", params: { receiptId: receipt.receiptId } }, + { jsonrpc: "2.0", id: "finalityList", method: "finality_list", params: { limit: 10 } }, + { jsonrpc: "2.0", id: "finality", method: "finality_get", params: { receiptId: receipt.receiptId } }, + { jsonrpc: "2.0", id: "provenance", method: "provenance_get", params: { receiptId: receipt.receiptId } }, + { jsonrpc: "2.0", id: "raw", method: "raw_json_get", params: { source: "launchCore" } }, + ] as const; + + const response = dispatchJsonRpc([...requests], { state }); + if (!Array.isArray(response)) { + throw new Error("control-plane smoke expected batch JSON-RPC response"); + } + + const errors = response.filter((entry): entry is RpcErrorResponse => "error" in entry); + if (errors.length > 0) { + throw new Error(`control-plane smoke failed: ${JSON.stringify(errors, null, 2)}`); + } + + const successes = response as RpcSuccessResponse[]; + return { + schema: "flowmemory.control_plane.smoke.v0", + ok: true, + methodCount: requests.length, + responseSchemas: successes.map((entry) => (entry.result as JsonObject).schema), + queried: { + rootfieldId, + receiptId: receipt.receiptId, + reportId, + artifactUri, + blockNumber: stringField(block.blockNumber, "blockNumber"), + txId, + }, + localOnly: true, + }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + console.log(JSON.stringify(runControlPlaneSmoke(), null, 2)); +} diff --git a/services/control-plane/src/types.ts b/services/control-plane/src/types.ts new file mode 100644 index 00000000..d0a0b948 --- /dev/null +++ b/services/control-plane/src/types.ts @@ -0,0 +1,118 @@ +import type { LaunchCoreOutput } from "../../flowmemory/src/types.ts"; +import type { PersistedIndexerState } from "../../indexer/src/index.ts"; +import type { PersistedVerifierReports, ArtifactResolverFixture } from "../../verifier/src/index.ts"; + +export type JsonValue = + | null + | boolean + | number + | string + | JsonValue[] + | { [key: string]: JsonValue | undefined }; + +export type JsonObject = { [key: string]: JsonValue | undefined }; + +export type ControlPlaneMethod = + | "health" + | "chain_status" + | "devnet_state" + | "block_get" + | "block_list" + | "transaction_get" + | "transaction_list" + | "rootfield_get" + | "rootfield_list" + | "artifact_get" + | "artifact_availability_get" + | "artifact_availability_list" + | "receipt_get" + | "receipt_list" + | "work_receipt_get" + | "work_receipt_list" + | "verifier_module_get" + | "verifier_module_list" + | "verifier_report_get" + | "verifier_report_list" + | "memory_cell_get" + | "memory_cell_list" + | "agent_get" + | "agent_list" + | "model_get" + | "model_list" + | "challenge_get" + | "challenge_list" + | "finality_get" + | "finality_list" + | "provenance_get" + | "raw_json_get"; + +export interface ControlPlanePaths { + launchCorePath: string; + indexerPath: string; + verifierPath: string; + artifactsPath: string; + devnetPath: string; + devnetIndexerHandoffPath: string; + devnetVerifierHandoffPath: string; + devnetControlPlaneHandoffPath: string; + txFixturesPath: string; +} + +export interface DataSourceRecord { + schema: "flowmemory.control_plane.data_source.v0"; + name: string; + path: string; + status: "loaded" | "missing" | "recovered"; + recovery?: string; +} + +export interface LoadedControlPlaneState { + schema: "flowmemory.control_plane.state.v0"; + launchCore: LaunchCoreOutput; + indexer: PersistedIndexerState; + verifier: PersistedVerifierReports; + artifacts: ArtifactResolverFixture; + devnet: JsonObject | null; + devnetIndexerHandoff: JsonObject | null; + devnetVerifierHandoff: JsonObject | null; + devnetControlPlaneHandoff: JsonObject | null; + txFixtures: JsonObject | null; + sources: Record; +} + +export interface ControlPlaneContext { + state?: LoadedControlPlaneState; + paths?: Partial; +} + +export interface RpcRequest { + jsonrpc: "2.0"; + id?: string | number | null; + method: string; + params?: JsonValue; +} + +export interface RpcErrorObject { + code: number; + message: string; + data: { + schema: "flowmemory.control_plane.error.v0"; + reasonCode: string; + details?: JsonValue; + localOnly: true; + }; +} + +export interface RpcSuccessResponse { + jsonrpc: "2.0"; + id: string | number | null; + result: JsonValue; +} + +export interface RpcErrorResponse { + jsonrpc: "2.0"; + id: string | number | null; + error: RpcErrorObject; +} + +export type RpcResponse = RpcSuccessResponse | RpcErrorResponse; diff --git a/services/control-plane/test/control-plane.test.ts b/services/control-plane/test/control-plane.test.ts new file mode 100644 index 00000000..71304d32 --- /dev/null +++ b/services/control-plane/test/control-plane.test.ts @@ -0,0 +1,172 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; + +import { canonicalJson } from "../../shared/src/index.ts"; +import { + dispatchJsonRpc, + loadControlPlaneState, + type RpcErrorResponse, + type RpcSuccessResponse, +} from "../src/index.ts"; +import { runControlPlaneSmoke } from "../src/smoke.ts"; + +test("dispatches JSON-RPC methods against local fixture state", () => { + const response = dispatchJsonRpc({ jsonrpc: "2.0", id: "status", method: "chain_status" }) as RpcSuccessResponse; + + assert.equal(response.jsonrpc, "2.0"); + assert.equal(response.id, "status"); + assert.equal(response.result.schema, "flowmemory.control_plane.chain_status.v0"); + assert.equal(response.result.localOnly, true); +}); + +test("returns stable invalid params errors for missing required params", () => { + const response = dispatchJsonRpc({ jsonrpc: "2.0", id: 1, method: "rootfield_get" }) as RpcErrorResponse; + + assert.equal(response.error.code, -32602); + assert.equal(response.error.data.reasonCode, "params.invalid"); + assert.equal(response.error.data.localOnly, true); +}); + +test("returns standard unknown method errors", () => { + const response = dispatchJsonRpc({ jsonrpc: "2.0", id: 1, method: "flow_sendTransaction" }) as RpcErrorResponse; + + assert.equal(response.error.code, -32601); + assert.equal(response.error.data.reasonCode, "method.not_found"); +}); + +test("validates malformed requests and bad params with stable codes", () => { + const invalidRequest = dispatchJsonRpc({ jsonrpc: "2.0", id: 1 }) as RpcErrorResponse; + const badLimit = dispatchJsonRpc({ jsonrpc: "2.0", id: 2, method: "receipt_list", params: { limit: 0 } }) as RpcErrorResponse; + const badRawSource = dispatchJsonRpc({ jsonrpc: "2.0", id: 3, method: "raw_json_get", params: { source: "E:/secrets" } }) as RpcErrorResponse; + + assert.equal(invalidRequest.error.code, -32600); + assert.equal(invalidRequest.error.data.reasonCode, "request.invalid"); + assert.equal(badLimit.error.code, -32602); + assert.equal(badLimit.error.data.reasonCode, "params.invalid"); + assert.equal(badRawSource.error.code, -32602); + assert.equal(badRawSource.error.data.reasonCode, "params.invalid"); +}); + +test("keeps deterministic chain status response snapshots", () => { + const first = dispatchJsonRpc({ jsonrpc: "2.0", id: 1, method: "chain_status" }) as RpcSuccessResponse; + const second = dispatchJsonRpc({ jsonrpc: "2.0", id: 2, method: "chain_status" }) as RpcSuccessResponse; + const snapshot = (response: RpcSuccessResponse) => { + const result = response.result; + return canonicalJson({ + schema: result.schema, + chainId: result.chainId, + counts: result.counts, + capabilities: result.capabilities, + }); + }; + + assert.equal(snapshot(first), snapshot(second)); + assert.equal( + snapshot(first), + "{\"capabilities\":[\"health_reads\",\"fixture_status_reads\",\"block_reads\",\"transaction_reads\",\"receipt_lookup\",\"verifier_report_lookup\",\"memory_lineage_lookup\",\"artifact_fixture_lookup\",\"devnet_handoff_reads\",\"raw_json_reads\"],\"chainId\":\"flowmemory-local-alpha\",\"counts\":{\"agents\":2,\"artifactAvailability\":5,\"blocks\":11,\"challenges\":1,\"devnetBlocks\":2,\"duplicates\":1,\"finalityRows\":9,\"memoryCells\":1,\"memoryReceipts\":8,\"memorySignals\":8,\"models\":2,\"observations\":8,\"rejectedLogs\":2,\"rootfields\":2,\"transactions\":23,\"verifierModules\":3,\"verifierReports\":8,\"workReceipts\":9},\"schema\":\"flowmemory.control_plane.chain_status.v0\"}", + ); +}); + +test("recovers when generated launch/indexer/verifier fixtures are missing", () => { + const dir = mkdtempSync(join(tmpdir(), "flowmemory-control-plane-")); + try { + const state = loadControlPlaneState({ + launchCorePath: join(dir, "missing-launch.json"), + indexerPath: join(dir, "missing-indexer.json"), + verifierPath: join(dir, "missing-reports.json"), + }); + const response = dispatchJsonRpc({ jsonrpc: "2.0", id: 1, method: "chain_status" }, { state }) as RpcSuccessResponse; + + assert.equal(state.sources.launchCore.status, "recovered"); + assert.equal(state.sources.indexer.status, "recovered"); + assert.equal(state.sources.verifier.status, "recovered"); + assert.equal(response.result.counts.observations, 8); + assert.equal(response.result.counts.verifierReports, 8); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + +test("looks up receipt, report, and memory provenance", () => { + const state = loadControlPlaneState(); + const receipt = state.launchCore.memoryReceipts[0]; + const reportId = receipt.reportId; + const rootfieldId = receipt.rootfieldId; + + const receiptProvenance = dispatchJsonRpc( + { jsonrpc: "2.0", id: 1, method: "provenance_get", params: { receiptId: receipt.receiptId } }, + { state }, + ) as RpcSuccessResponse; + const reportProvenance = dispatchJsonRpc( + { jsonrpc: "2.0", id: 2, method: "provenance_get", params: { reportId } }, + { state }, + ) as RpcSuccessResponse; + const memoryCell = dispatchJsonRpc( + { jsonrpc: "2.0", id: 3, method: "memory_cell_get", params: { rootfieldId } }, + { state }, + ) as RpcSuccessResponse; + + assert.equal(receiptProvenance.result.links.receiptId, receipt.receiptId); + assert.equal(receiptProvenance.result.links.reportId, reportId); + assert.equal(reportProvenance.result.links.reportId, reportId); + assert.equal(memoryCell.result.schema, "flowmemory.control_plane.memory_cell.v0"); + assert.equal(memoryCell.result.rootfieldId, rootfieldId); + assert.match(String(memoryCell.result.extensionPoint), /projected from RootfieldBundle/); +}); + +test("supports receipt and report object lookup by provenance-linked ids", () => { + const state = loadControlPlaneState(); + const receipt = state.launchCore.memoryReceipts[0]; + const receiptResponse = dispatchJsonRpc( + { jsonrpc: "2.0", id: 1, method: "receipt_get", params: { observationId: receipt.observationId } }, + { state }, + ) as RpcSuccessResponse; + const reportResponse = dispatchJsonRpc( + { jsonrpc: "2.0", id: 2, method: "verifier_report_get", params: { reportId: receipt.reportId } }, + { state }, + ) as RpcSuccessResponse; + + assert.equal(receiptResponse.result.receipt.receiptId, receipt.receiptId); + assert.equal(reportResponse.result.report.reportId, receipt.reportId); +}); + +test("exposes artifact, devnet, challenge, and finality read methods", () => { + const state = loadControlPlaneState(); + const receipt = state.launchCore.memoryReceipts[0]; + const artifactUri = receipt.evidenceRefs[0]?.uri; + assert.equal(typeof artifactUri, "string"); + + const artifact = dispatchJsonRpc( + { jsonrpc: "2.0", id: 1, method: "artifact_get", params: { uri: artifactUri } }, + { state }, + ) as RpcSuccessResponse; + const devnet = dispatchJsonRpc( + { jsonrpc: "2.0", id: 2, method: "devnet_state" }, + { state }, + ) as RpcSuccessResponse; + const challenge = dispatchJsonRpc( + { jsonrpc: "2.0", id: 3, method: "challenge_get", params: { receiptId: receipt.receiptId } }, + { state }, + ) as RpcSuccessResponse; + const finality = dispatchJsonRpc( + { jsonrpc: "2.0", id: 4, method: "finality_get", params: { receiptId: receipt.receiptId } }, + { state }, + ) as RpcSuccessResponse; + + assert.equal(artifact.result.resolverPolicyId, "flowmemory.resolver.policy.v0.fixture"); + assert.equal(devnet.result.schema, "flowmemory.control_plane.devnet_state.v0"); + assert.equal(challenge.result.status, "not_opened"); + assert.equal(finality.result.status, "local-finalized"); +}); + +test("smoke client queries the complete local lifecycle surface", () => { + const smoke = runControlPlaneSmoke(); + + assert.equal(smoke.schema, "flowmemory.control_plane.smoke.v0"); + assert.equal(smoke.ok, true); + assert.equal(smoke.methodCount, 31); + assert.ok((smoke.responseSchemas as string[]).includes("flowmemory.control_plane.raw_json.v0")); +}); diff --git a/services/flowmemory/src/generate-launch-core.ts b/services/flowmemory/src/generate-launch-core.ts index 48408559..614d6c48 100644 --- a/services/flowmemory/src/generate-launch-core.ts +++ b/services/flowmemory/src/generate-launch-core.ts @@ -313,7 +313,7 @@ function buildMemoryReceipt(report: VerifierReport): MemoryReceipt { }; } -function buildLaunchCore(indexer: IndexerPersistence, verifier: VerifierPersistence, paths: LaunchCorePaths): LaunchCoreOutput { +export function buildLaunchCore(indexer: IndexerPersistence, verifier: VerifierPersistence, paths: LaunchCorePaths): LaunchCoreOutput { const sortedObservations = sortObservations(indexer.state.observations); const reportByObservation = new Map(verifier.reports.map((report) => [report.reportCore.observationId, report])); const receiptByObservation = new Map();