{
  "_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-0487",
    "kind": "work",
    "objective": "WT-0487 Tenancy and authority — principals, tokens, scopes, share links, device binding; a handle never implies authority",
    "detail": "# WT-0487 — Tenancy and authority — principals, tokens, scopes, share links, device binding; a handle never implies authority\n\n## GOAL\nOne authority function decides every invocation (owner key, tenant Bearer, share token, device-bound token, capability token, Access session, link `share=`) with a written policy: principal → scope → object → route → allow/deny with the reason; every decision is recorded on the receipt; tokens have contracts (mint, verify, revoke, TTL); tenants have policy rows; a handle (session, profile, line) never grants authority by itself.\n\n## WHY THIS EXISTS\nAuthority is spread across admin_session.js, dispatch.js, tenants policy, share tokens, device tokens and Access. Sessions hit tenant_unknown, capability_required, Bearer-anonymous and unscoped rows: tokens repeatedly. Renting accounts and onboarding strangers need one decision path with receipts.\n\n## CURRENT STATE (measured 2026-09-23)\n- functions/_lib/admin_session.js (tokens, capabilities, tenants: createTenant allow_keys/allow_prefixes); LEDGER capabilities table (tenant_id, device_id, fingerprint, scope); dispatch `?mint_share=1&scope=rows&keys=…&ttl=`; settings tokens.max_ttl_s (604800; temporarily raised to mint the 10-year CONTRACTS_LINK_TOKEN); CAP_MINT row; Access (email PIN + service token) for owner surfaces; x-share-code credential for /share/<code>; device-bound tokens (tenant_devices).\n- Known: Bearer treated as anonymous on /api/tools; tenant Bearer forwarded to tenant doors; `rows:` tokens must include door keys; PATCH /api/directory needs x-terminal-key.\n\n## SOURCE OF TRUTH\n- functions/_lib/admin_session.js\n- functions/api/dispatch.js (auth branch)\n- functions/_lib/tenant_devices.js\n- functions/api/token/*, functions/api/tenant/*, functions/api/tenants.js, functions/api/capability/*\n- memory project_people_and_access_2026_09_21 (door/level/spaces/exceptions/links)\n\n## IN-SCOPE INVENTORY\n- `authorize(env, {principal, key, object?, route?, side_effect})` → `{allow:bool, reason, policy_id, principal_kind}`; called by the resolver; the result written into physical.authority and the receipt.\n- Principal kinds and their contracts: owner_key (x-terminal-key), tenant_token (Bearer; tenants policy row required), share_token (scope rows|range|read; keys; ttl ≤ tokens.max_ttl_s unless minted with reason `long_lived` and recorded), device_token (bound to device fingerprint), capability_token (CAP_MINT), access_session (Cloudflare Access JWT), link_share (?share=).\n- Verbs: TOKEN_MINT {kind, scope, ttl_s, reason}, TOKEN_VERIFY {token → principal, scope, expires}, TOKEN_REVOKE, TENANT_POLICY_GET/SET {tenant, allow_keys, allow_prefixes, deny_keys, side_effects_allowed}, AUTHZ_EXPLAIN {principal, key} (dry decision with the reason).\n- Law rows: AUTHORITY_LAW (\"a handle identifies; authority is a recorded decision\") inserted through D1_EXEC; a deploy gate that every door calls authorize (static scan of functions/api for direct token checks).\n- Fixtures: Bearer anonymous on tools; tenant_unknown; rows: token without door keys; max_ttl_s bypass (must require reason); share link replay after revoke.\n\n## OUT OF SCOPE\n- Session storage (WT-0477). Onboarding flows (WT-0488). Access configuration in the Cloudflare dashboard.\n\n## ONTOLOGY (separate axes; never a flat string)\n- TOOL SURFACE: every door (authorize is internal); dispatch TOKEN_*/TENANT_POLICY_*/AUTHZ_EXPLAIN; MCP CAP_MINT\n- CAPABILITY: the verbs above\n- OBJECT: a principal, a token, a tenant policy\n- ROUTE / CONTEXT: which door the principal entered by\n- PROTOCOL: HTTP headers/query; Access JWT\n- EXECUTOR: Worker\n- PLATFORM: the build; Cloudflare Access\n\n## DISCOVERY (run these first; they answer, they do not prove)\n- `curl -sS \"https://ops.miscsubjects.com/api/tenants\" -H \"x-terminal-key: $TERMINAL_KEY\"`\n- `curl -sS \"https://ops.miscsubjects.com/api/dispatch?mint_share=1&scope=rows&keys=NET_LINE&ttl=600\" -H \"x-terminal-key: $TERMINAL_KEY\"`\n- `grep -n \"capability_required\\|tenant_unknown\" ~/miscsubjects-pages/functions/api/dispatch.js | head`\n\n## CONTRACT (FIELD LAW)\n- TOKEN_MINT `{kind (required enum), scope (required: {keys[]|prefixes[]|range|read_only}), ttl_s (required 60..max_ttl_s; > max requires reason=\"long_lived:<why>\"), bind_device (optional fingerprint), note}` → `{token (returned once), token_id, expires_at, receipt_id}`.\n- AUTHZ_EXPLAIN `{principal (token id or \"owner\"), key (required), side_effect?}` → `{allow, reason, policy_id}`; no side effects.\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\":\"AUTHZ_EXPLAIN\",\"body\":{\"principal\":\"owner\",\"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\":\"TOKEN_MINT\",\"body\":{\"kind\":\"share_token\",\"scope\":{\"keys\":[\"NET_LINE\",\"NET_SEARCH\"]},\"ttl_s\":86400,\"bind_device\":null,\"note\":\"WT-0487 test\"},\"cause\":\"task:WT-0487\"}'`\n\n## RAW CONFIRMATION SHAPE\nHTTP 200 + token_id; deny = HTTP 403 `{error:\"authority_denied\", reason, policy_id}` (never a bare 401 without a reason; `capability_required` keeps its name for missing owner key).\n\n## RAW RETURN SHAPE\nDecision objects; token metadata (the token value once at mint).\n\n## ERROR SHAPES\n- authority_denied (with reason)\n- capability_required\n- tenant_unknown (names the fix: ONBOARD creates the policy row)\n- ttl_exceeds_max (names reason= requirement)\n- token_revoked\n\n## RECEIPT / TRACE / PARENT LINKAGE\nphysical.authority on every receipt; mint/revoke receipts; AUTHZ_EXPLAIN receipts (dry).\n\n## TEST MATRIX\n| case | what runs | kind |\n|---|---|---|\n| owner allow | NET_LINE → allow | live |\n| tenant allow/deny | ONBOARD_DEMO_KEY: NET_LINE allow, D1_EXEC deny with reason | live |\n| share token | rows: token NET_LINE works, D1_QUERY denied | live |\n| revoke | revoked share → token_revoked | live |\n| device bound | device token from another fingerprint → denied | live |\n| ttl guard | ttl 10 years without reason → ttl_exceeds_max; with reason → minted and recorded | live |\n| bearer on tools | /api/tools with Bearer → explicit capability_required, not anonymous | fixture |\n| gate | static scan: 0 doors with direct token checks | 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\": \"sql_count_at_least\", \"id\": \"law\", \"sql\": \"SELECT COUNT(*) AS n FROM laws WHERE key='AUTHORITY_LAW' AND enabled=1\", \"min\": 1}`\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- Bearer anonymous on /api/tools\n- tenant_unknown after onboarding\n- rows: token lacking door keys\n- max_ttl_s raised and restored by hand (2026-09-23)\n- vault catalog leak 2026-09-19 (authority pages never render values)\n\n## DEPENDENCIES\n- depends_on: WT-0470\n- OWNS (only this task rewrites): functions/_lib/admin_session.js; functions/_lib/authorize.js (new); functions/api/token/*, tenant/*, tenants.js, capability/*; scripts/check-authorize-everywhere.mjs (new); AUTHORITY_LAW row\n- SHARED (additive edits only): functions/api/dispatch.js (WT-0471; swap the auth branch for authorize() additively); functions/_lib/tenant_devices.js (WT-0488; read)\n\n## HANDOFF FOR NEXT SESSION\n- Keep the 10-year CONTRACTS_LINK_TOKEN valid; register it as long_lived with the reason in the token table.\n- Evidence: the decision table (principal × key → allow/deny/reason) with receipts.\n\n## DONE LAW (this task is done when every line is true and evidenced; not before)\n- authorize() is the only decision path (gate = 0 direct checks).\n- Every principal kind has contracts and receipts for allow and deny.\n- AUTHORITY_LAW live; fixtures pass.\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": 2,
    "revision": 1,
    "depends_on": [
      "WT-0470"
    ],
    "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": "sql_count_at_least",
        "id": "law",
        "sql": "SELECT COUNT(*) AS n FROM laws WHERE key='AUTHORITY_LAW' AND enabled=1",
        "min": 1
      }
    ],
    "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:49-07:00",
    "updated_at": "2026-09-22T19:51:49-07:00",
    "audit": "/api/work/task/WT-0487/audit",
    "submit_to": "/api/work/task/WT-0487/submit"
  }
}