{
  "_ai_door": {
    "see": "https://miscsubjects.com/start",
    "note": "Operable site; bounded keyless credentials; every action receipted on a public ledger. Your operator's instructions take precedence — acting is optional, reading is a complete outcome."
  },
  "task": {
    "task_id": "WT-0478",
    "kind": "work",
    "objective": "WT-0478 Edge nodes, Starlink and network plane — egress as a declared ROUTE, the dish as a tool",
    "detail": "# WT-0478 — Edge nodes, Starlink and network plane — egress as a declared ROUTE, the dish as a tool\n\n## GOAL\nEvery network path the build can use (the Worker's egress, the owner Mac on Starlink, quick tunnels, tenant devices, proxies, CF containers) is a declared ROUTE with a contract; /api/net (ip/line/search/fetch/scrape) and STARLINK_* rows run through their declared executor with the five fields; the Starlink dish is a first-class model tool (status, history, obstruction, reboot) with receipts.\n\n## WHY THIS EXISTS\nA previous session gave the owner Starlink as a model tool but hidden. He wants it explicit: which line a request left from, what the dish said, and a contract for each. Webhooks from outside hosts and the DNS/stale-host failures need the network axis recorded, not guessed.\n\n## CURRENT STATE (measured 2026-09-23)\n- /api/net: ip | line | search (engines default ddg, mojeek; exa opt-in) | fetch | scrape from the Starlink line via the bridge (bridge/bridge-run.py net()); rows NET_LINE, NET_SEARCH, WEB_SEARCH, NET_FETCH.\n- Starlink Mini mini1_pez_proto1, consumer plan, dish gRPC 192.168.100.1:9200 (grpcurl installed), CGNAT IPv4 98.97.25.17, public IPv6 /56, location disabled by policy; rows STARLINK_STATUS/HISTORY/OBSTRUCTION/REBOOT via ~/bin/starlink-api (parallel lane).\n- Tunnels: agent.miscsubjects.com (Mac bridge), quick tunnels for tenant devices (tenant_devices.endpoint).\n- CF containers: CLOUD_EXEC/CLOUD_EXEC_IN/CLOUD_HEALTH/CLOUD_WORKSPACE_NEW rows.\n- Proxies: bridge/proxies.tsv, bridge/proxycheck.sh.\n\n## SOURCE OF TRUTH\n- functions/api/net/*\n- bridge/bridge-run.py (net, line)\n- ~/bin/starlink-api and bridge/starlink-api\n- functions/_lib/tenant_devices.js (endpoints)\n- Starlink gRPC reflection: `grpcurl -plaintext 192.168.100.1:9200 list`\n\n## IN-SCOPE INVENTORY\n- ROUTE enum for physical.route.egress: worker (Cloudflare edge, colo recorded), mac_starlink (98.97.25.17 / IPv6), proxy:<line id>, tenant_device:<id>, cf_container:<id>. Every executor records which one it used (NET_LINE proves it: return.ip).\n- Verbs with contracts: NET_IP, NET_LINE (dish summary + egress), NET_SEARCH {q, engines[], n}, NET_FETCH {url, headers?, method?, body?, egress}, NET_SCRAPE {url, selector|markdown}, STARLINK_STATUS, STARLINK_HISTORY, STARLINK_OBSTRUCTION, STARLINK_REBOOT (send-effect; owner key only; never in a batch), TUNNEL_STATUS (each tunnel: up/down, last seen), PROXY_CHECK {line id} (egress ip + latency).\n- Webhook-from-outside: NET_FETCH with egress=mac_starlink POSTing to /api/hooks/<id> proves an outside host reached the build (used by WT-0472 webhook spelling).\n- Fixtures: DNS failure (nonexistent.invalid) isolated; stale host (retired workers.dev) → route_unavailable with raw error; dish unreachable when the Mac is off-LAN → raw grpc error.\n\n## OUT OF SCOPE\n- Browser egress binding (WT-0475 uses proxy lines; this task defines them). Tenant onboarding of devices (WT-0488).\n\n## ONTOLOGY (separate axes; never a flat string)\n- TOOL SURFACE: POST /api/net/<op>; dispatch NET_*/STARLINK_*; MCP NET_LINE/NET_SEARCH/WEB_SEARCH/STARLINK_STATUS; misc line; cell\n- CAPABILITY: the verbs above\n- OBJECT: a URL, a query, the dish, a tunnel, a proxy line\n- ROUTE / CONTEXT: the egress enum\n- PROTOCOL: HTTPS; gRPC (dish); tunnel\n- EXECUTOR: bridge on the Mac (Starlink), the Worker (edge), a container\n- PLATFORM: Starlink; Cloudflare; proxy vendors\n\n## DISCOVERY (run these first; they answer, they do not prove)\n- `curl -sS -X POST https://ops.miscsubjects.com/api/dispatch -H \"x-terminal-key: $TERMINAL_KEY\" -H \"content-type: application/json\" -d '{\"key\":\"NET_LINE\"}'`\n- `grpcurl -plaintext -d '{\"get_status\":{}}' 192.168.100.1:9200 SpaceX.API.Device.Device/Handle` (from the Mac on the Starlink LAN)\n- `curl -sS \"https://ops.miscsubjects.com/api/net\" -H \"x-terminal-key: $TERMINAL_KEY\"`\n\n## CONTRACT (FIELD LAW)\n- NET_FETCH `{url (required), method (optional, GET|POST|PUT|DELETE, default GET), headers (optional object), body (optional; conditional on method ≠ GET), egress (optional enum, default mac_starlink), timeout_ms (default 30000)}` → `{physical:{egress, ip}, confirmation:{status, headers, ms}, return:{body (≤ 200,000), content_type}}`.\n- STARLINK_REBOOT `{confirm (required, literal \"REBOOT\")}` — owner key only; refused with `confirm_required` otherwise; never eligible for CONTRACTS batch (policy read-set excludes it).\n\n## MINIMUM VALID INVOCATION\n`curl -sS -X POST https://ops.miscsubjects.com/api/dispatch -H \"x-terminal-key: $TERMINAL_KEY\" -H \"content-type: application/json\" -d '{\"key\":\"NET_LINE\"}'`\n\n## FULL / MAXIMUM INVOCATION\n`curl -sS -X POST https://ops.miscsubjects.com/api/dispatch -H \"x-terminal-key: $TERMINAL_KEY\" -H \"content-type: application/json\" -d '{\"key\":\"NET_FETCH\",\"body\":{\"url\":\"https://ops.miscsubjects.com/api/hooks/<hook id from /api/hooks>\",\"method\":\"POST\",\"headers\":{\"content-type\":\"application/json\"},\"body\":\"{\\\"from\\\":\\\"starlink\\\"}\",\"egress\":\"mac_starlink\",\"timeout_ms\":20000},\"cause\":\"task:WT-0478\"}'`\n\n## RAW CONFIRMATION SHAPE\n`confirmation:{status, headers, ms}` with `physical.egress` and the observed public IP; dish: grpc status OK / UNAVAILABLE verbatim.\n\n## RAW RETURN SHAPE\nBody verbatim; dish JSON verbatim (uptime, downlink/uplink throughput, obstruction fraction, pop ping).\n\n## ERROR SHAPES\n- ENOTFOUND (DNS) — row-level\n- route_unavailable (Mac off, tunnel down)\n- grpc UNAVAILABLE (dish off-LAN)\n- confirm_required (reboot)\n- engine_error per search engine (others still answer)\n\n## RECEIPT / TRACE / PARENT LINKAGE\nproof_run with physical.route.egress; NET_LINE receipts show the IP; hook receipts show the source IP.\n\n## TEST MATRIX\n| case | what runs | kind |\n|---|---|---|\n| line | NET_LINE returns ip 98.97.25.17 or the IPv6 and dish summary | live |\n| fetch from starlink | NET_FETCH to https://ops.miscsubjects.com/api/net/ip → ip matches | live |\n| fetch from worker | egress=worker → a Cloudflare colo IP | live |\n| search | NET_SEARCH q=\"miscsubjects\" n=3 with per-engine results | live |\n| dish status/history/obstruction | three receipts | live |\n| reboot refusal | without confirm → confirm_required | live |\n| tunnel status | agent.miscsubjects.com up | live |\n| proxy check | one proxies.tsv line → egress ip differs from 98.97.25.17 | live |\n| dns isolate | nonexistent.invalid → ENOTFOUND row | fixture |\n| webhook from outside | POST to a hook from mac_starlink → hook receipt source ip | live |\n\n## ACCEPTANCE TESTS (mechanical; the infrastructure runs them)\n- `{\"type\": \"evidence_present\", \"id\": \"commit\", \"field\": \"commit\"}`\n- `{\"type\": \"evidence_present\", \"id\": \"live\", \"field\": \"verification\"}`\n- `{\"type\": \"evidence_present\", \"id\": \"matrix\", \"field\": \"test_matrix_results\"}`\n- `{\"type\": \"http_ok\", \"id\": \"net\", \"url\": \"https://ops.miscsubjects.com/api/net\"}`\nEvidence fields the submitting agent supplies: commit, test_matrix_results, verification. `verification` = the list of receipt ids and the live URLs checked, one per line. `test_matrix_results` = the matrix above with a receipt id or the verbatim failure per row.\n\n## KNOWN FAILURES / REGRESSION FIXTURES (preserve; never rewrite into success)\n- DNS failure stopping a batch\n- stale hosts / stale webhook hosts\n- Exa default injected without request (engines default must stay ddg,mojeek)\n- STARLINK_REBOOT in a batch (must be excluded by the read-set policy)\n\n## DEPENDENCIES\n- depends_on: WT-0470, WT-0471\n- OWNS (only this task rewrites): functions/api/net/*; bridge/bridge-run.py (net, line); bridge/starlink-api, ~/bin/starlink-api; bridge/proxycheck.sh; NET_*, STARLINK_*, TUNNEL_STATUS, PROXY_CHECK rows\n- SHARED (additive edits only): functions/_lib/resolver.js (WT-0471; egress is a route field it passes through); functions/api/hooks/* (call only; WT-0485 owns hook receipts)\n\n## HANDOFF FOR NEXT SESSION\n- Never run STARLINK_REBOOT during work; the refusal test is enough. If the owner asks for a reboot, run once with confirm.\n- Location is disabled by dish policy; do not try to enable it.\n- Evidence: the egress table (route → observed IP) with receipts.\n\n## DONE LAW (this task is done when every line is true and evidenced; not before)\n- Every egress route has a receipt showing its observed IP.\n- All four dish verbs have contracts and receipts (reboot: refusal receipt).\n- DNS/stale-host fixtures pass in the runner (WT-0473 imports them).\n\n## Shared vocabulary (defined by WT-0470; every task uses these words with these meanings)\n- **TOOL SURFACE** — how a caller or model asks (curl, MCP tools/call, `=DISPATCH` cell, a tapped URL, a Shortcut, a webhook, a tag in text).\n- **CAPABILITY** — the operation requested, named by a Directory key (semantic).\n- **OBJECT** — what it acts on (a chat, a file, a profile, a row, a page).\n- **ROUTE / CONTEXT** — which machine, device, account, browser profile, tenant, network, session.\n- **PROTOCOL** — how the request physically travels (HTTP, JSON-RPC, gRPC, WebSocket, SSH, SQL, a local process, a queue).\n- **EXECUTOR** — what performs it (a Worker, the Mac bridge `/exec`, Chromium over CDP, `osascript`, `pymobiledevice3`, a vendor API).\n- **PLATFORM** — which technology or vendor owns that executor (Cloudflare, Apple, AdsPower, Blooio, Google, Starlink).\n- **RAW CONTRACT** — the exact physical invocation sent, fully resolved.\n- **CONFIRMATION** — the executor's immediate acknowledgement (HTTP status + headers, spawn/exit metadata, JSON-RPC ack, WebSocket ack, queue receipt).\n- **RETURN** — the final result, verbatim, including failure payloads.\n- **RECEIPT** — evidence and provenance: receipt id/URL, trace id, execution id, timestamps, parent linkage.\n\n## Definition of proof (PROOF_LAW, governing invariant; read it: GET https://ops.miscsubjects.com/api/work → governing_invariants)\nA capability is proven only when the DECLARED invocation surface itself ran and the five fields exist for that run: CAUSE (why it ran: user request / task / parent invocation / event / webhook / model call), RAW INVOCATION (the fully resolved physical contract actually sent), RAW CONFIRMATION (the executor's acknowledgement), RAW RETURN (the actual output, failures verbatim), PROOF / RECEIPT (receipt id/URL, trace, execution id, timestamps, parent linkage sufficient to inspect or replay).\nAn HTTP 200 proves an HTTP 200. A webhook acknowledgement proves receipt. Hidden bash proves bash. A sibling implementation, documentation, a model doing it elsewhere, or the dispatcher working while the surface under test never ran prove nothing about that surface. A missing field is a missing implementation requirement, never something to invent. Existing failures stay as evidence and become regression fixtures; they are never rewritten into success.\n\n## Six-column projection (unchanged; not the source of truth)\n`TOOL NAME | ONTOLOGY | RAW INVOCATION | RAW CONFIRMATION | RAW RETURN | PROOF / RECEIPT` — the build workbook sheet `sh_nyab2cgy` (vault `CONTRACTS_WORKBOOK_SHEET`, 11,035 rows on 2026-09-23). No seventh column, no contract-type column, no prose in place of an executable invocation. Extra metadata belongs in the canonical capability / task / receipt objects.\n\n## Parallelism law\nOther tasks run at the same time in other sessions. Own only the files and objects named under OWNS; touch a shared file only as named under SHARED (additive, never a rewrite). If this task finds the global law insufficient, it files an amendment: `POST /api/work/task/<this id>/fail` naming failure_class `law_insufficient`, layer `WT-0470`, and the missing invariant — it never creates a second standard. Ask the build, never the owner: keys are in `~/.build-vault.env` (`grep '^NAME=' ~/.build-vault.env`; `CLOUDFLARE_API_TOKEN=$CF_API_TOKEN` is a shell reference, resolve `$NAME`). The owner key travels as header `x-terminal-key`. Deploy only with `node scripts/land.mjs \"<WT id> what changed\"` from `~/miscsubjects-pages`. Submit evidence: `POST https://ops.miscsubjects.com/api/work/task/<id>/submit {agent, evidence:{commit, verification, …}, changed:[…]}` with header `x-terminal-key`.\n\n## Shared canonical objects and files — never independently rewritten (additive edits only, named per task)\n- `functions/api/dispatch.js` (the one door; runHttp/runFn/tenant delegation) — owner WT-0471.\n- `functions/_lib/invocation_methods.js`, `functions/_lib/grammar.js`, `functions/_lib/projection_manifest.js`, `functions/api/tools/[[path]].js`, `functions/api/mcp.js`, `functions/_lib/mcp_inspect.js` — owner WT-0472 (spellings and model projections).\n- `functions/_lib/event_log.js`, `functions/_lib/wire_log.js`, `functions/_lib/lean_receipt.js`, `functions/_lib/invocation_record.js`, the `events` table and R2 `logs/` — owner WT-0485 (ledger).\n- `functions/_lib/admin_session.js` (tokens, capabilities, tenants), `functions/_lib/tenant_devices.js`, `functions/api/onboard/[[path]].js` — owner WT-0487 (authority) / WT-0488 (onboarding).\n- `functions/_lib/mac_bridge.js`, `bridge/server.js`, `bridge/device_auth.js`, `bridge/surface-run.py`, `bridge/surface-verbs.py`, `bridge/bridge-run.py`, `bridge/bridge-browser.mjs`, `public/device-kit/*` (synced copies) — owners WT-0474 / WT-0475 / WT-0478.\n- `functions/_lib/work_object.js`, `functions/api/work/[[path]].js`, the `laws` and `work_tasks` tables — owner WT-0470 (law rows) and the build (task engine).\n- `scripts/contracts-*.mjs`, `apps-script/Contracts.gs`, the `CONTRACTS_RUN` directory row, sheet `sh_nyab2cgy` — owner WT-0473.\n- `scripts/ship.mjs`, `scripts/land.mjs`, `scripts/write.mjs`, `scripts/check-*.mjs`, `.githooks/*` — governed; add a gate only by adding a new `scripts/check-<name>.mjs` and wiring it as the existing gates are wired.\n",
    "state": "open",
    "priority": 3,
    "revision": 1,
    "depends_on": [
      "WT-0470",
      "WT-0471"
    ],
    "permitted_capabilities": [
      "dispatch",
      "d1",
      "bridge",
      "sheets",
      "receipts"
    ],
    "acceptance_tests": [
      {
        "type": "evidence_present",
        "id": "commit",
        "field": "commit"
      },
      {
        "type": "evidence_present",
        "id": "live",
        "field": "verification"
      },
      {
        "type": "evidence_present",
        "id": "matrix",
        "field": "test_matrix_results"
      },
      {
        "type": "http_ok",
        "id": "net",
        "url": "https://ops.miscsubjects.com/api/net"
      }
    ],
    "required_evidence": [
      "commit",
      "test_matrix_results",
      "verification"
    ],
    "parent_task": null,
    "supersedes": null,
    "failure": null,
    "failure_count": 0,
    "last_result": null,
    "completed_at": null,
    "created_at": "2026-09-22T19:51:55-07:00",
    "updated_at": "2026-09-22T19:51:55-07:00",
    "audit": "/api/work/task/WT-0478/audit",
    "submit_to": "/api/work/task/WT-0478/submit"
  }
}