171 lines
15 KiB
Markdown
171 lines
15 KiB
Markdown
---
|
||
phase: 06-turn-by-turn-guidance-live-driving
|
||
plan: 03
|
||
subsystem: bot
|
||
tags: [kv, ttl-only, guidance-sessions, envelope, api-endpoints, bearer-claim, privacy, gate-repin, tdd, workspace-dep]
|
||
|
||
# Dependency graph
|
||
requires:
|
||
- phase: 05-routing-route-display
|
||
plan: 03
|
||
provides: the FROZEN GuidanceSessionEnvelope/GuidanceRouteSnapshot shape + GUIDANCE_SESSION_TTL_SECONDS + guidance:<id> key scheme (this plan implements, never redefines)
|
||
- phase: 05-routing-route-display
|
||
plan: 01
|
||
provides: decodePolyline6 — the pure codec validating POST body geometry server-side
|
||
- phase: 03-mini-app-shell-geolocation
|
||
plan: 01
|
||
provides: the Bearer-claim precedent (handleLocation) and getClaimByToken — the only identity source for /api/guidance
|
||
provides:
|
||
- "mintGuidanceSession/getGuidanceSession/deleteGuidanceSession — the TTL-only envelope service over the unchanged frozen shape (GE1/GE2/GE3 hold)"
|
||
- "POST/GET/DELETE /api/guidance — chat identity from the Bearer claim ONLY; body chat_id structurally ignored (T-06-02); cross-chat read/delete impossible (T-06-02b)"
|
||
- "MAX_GUIDANCE_BODY_CHARS = 8192 with the WR-01 pre/post-read double check"
|
||
- "RP2 re-pinned as an explicit allowlist: exactly @araharah/routing/src/polyline.ts may enter the Worker"
|
||
- "PG1 storageModules now covers kv/guidance-sessions.ts (same-line TTL law on the new write path)"
|
||
affects: [phase-6 guidance turn-card cron (plan 07 reads envelopes), GUID-05 web POST-on-minimize, GUID-06 resume, GUID-06 trip-end DELETE]
|
||
|
||
# Tech tracking
|
||
tech-stack:
|
||
added:
|
||
- "@araharah/routing:* in bot/package.json (workspace dep; only the pure polyline6 codec subpath is imported)"
|
||
patterns:
|
||
- "Types-frozen + service-beneath: behavior added BELOW an interface whose bytes never changed (GE2 runtime twin = exact-key checks in the defensive read)"
|
||
- "Gate re-pins ship with the invalidating change: GE4/GE7 (service live / consumers scoped) in 0cfa0bd, RP2 (codec-only allowlist) in 7875ed4"
|
||
- "Resolve-then-OWN endpoint posture: getGuidanceSession → envelope.chat_id === claim.chat_id, else an indistinguishable 404 (no existence oracle)"
|
||
|
||
key-files:
|
||
created: [] # zero new files — behavior landed inside existing modules per the frozen-contract plan
|
||
modified:
|
||
- bot/src/kv/guidance-sessions.ts
|
||
- bot/src/webhook.ts
|
||
- bot/package.json
|
||
- package-lock.json
|
||
- bot/test/guidance-envelope.test.mjs
|
||
- bot/test/webhook.test.mjs
|
||
- bot/test/privacy.test.mjs
|
||
- bot/test/route-privacy.test.mjs
|
||
|
||
key-decisions:
|
||
- "RP2 re-pin is an ALLOWLIST, not a lifted ban: every @araharah/routing specifier in bot/src must be EXACTLY the pure codec subpath — the routing stack (graph/A*/fixtures) stays web-side; only charset/decode validation crosses the boundary"
|
||
- "The envelope service REUSES the chat_sessions namespace under guidance:<id> keys — no second kv_namespaces entry (GE5 stands), no new Env field (GE6 stands); the binding is handed in structurally (the sessions.ts/location-pin.ts KvLike seam)"
|
||
- "validateSnapshot builds the snapshot field-by-field, so a body chat_id (or any extra property) is structurally dropped before mint — T-06-02 by construction, proven by the GW4 injection probe"
|
||
- "404 for absent, malformed, AND foreign ids — indistinguishable by design (T-06-02b, no existence oracle); GW7/GW10 additionally prove a foreign GET/DELETE never deletes"
|
||
|
||
patterns-established:
|
||
- "Guidance endpoint test block (GW1–GW11): claims minted through the REAL /api/session path (mintSession) against the Map-fake KV, write-count assertions made relative to the claim-mint baseline"
|
||
|
||
requirements-completed: [GUID-05] # server surface of GUID-05 (envelope mint/read/delete over HTTP); REQUIREMENTS.md tick left to the orchestrator
|
||
|
||
# Metrics
|
||
duration: ~8min (commits 05:02:28Z–05:06:25Z; resumed once — see Deviations)
|
||
completed: 2026-10-24
|
||
---
|
||
|
||
# Phase 6 Plan 3: Guidance Envelope Service + /api/guidance Endpoints Summary
|
||
|
||
**The Phase 5-frozen `GuidanceSessionEnvelope` goes LIVE (D-11's precondition): a TTL-only mint/get/delete service beneath the byte-identical 4-field shape, plus Bearer-claim-gated POST/GET/DELETE `/api/guidance` — chat identity from the claim only, every write TTL'd at 7200 s, corrupt records read as null, and the RP2 routing ban re-pinned to a codec-only allowlist.**
|
||
|
||
## Performance
|
||
|
||
- **Duration:** ~8 min (task commits 05:02:28Z → 05:06:25Z; session resumed once — see Deviations)
|
||
- **Started:** 2026-10-24T05:02:28Z (first task commit)
|
||
- **Completed:** 2026-10-24T05:06:25Z (last task commit); continuation verification + summary ~05:08Z
|
||
- **Tasks:** 2/2 (both TDD RED→GREEN)
|
||
- **Files modified:** 8 (zero created — the plan's whole point was to add behavior inside existing modules)
|
||
- **Suites:** bot 74 → 90 tests (5 GS behavioral + 11 GW endpoint), 90/90 green; `bot:typecheck` exit 0
|
||
|
||
## Accomplishments
|
||
|
||
- **The envelope service exists under the frozen shape** (task 1): `mintGuidanceSession` (crypto.randomUUID id, claim-derived chat_id, ONE put at `guidance:<id>` with same-line `expirationTtl: GUIDANCE_SESSION_TTL_SECONDS`), `getGuidanceSession` (UUID-shape guard BEFORE any KV touch — V5; try/catch parse; exact-key 4+6 field check + per-field type/finite/range validation mirroring the location-pin defensive read), `deleteGuidanceSession` (active trip-end sweep, D-14's server half; malformed id is a silent no-op). The envelope/snapshot interfaces are byte-identical to Phase 5 — GE1/GE2/GE3 pass unchanged.
|
||
- **The boundary gates re-pinned WITH the change** (0cfa0bd): GE4 flipped from the types-only behavior-token ban to "the service trio is exported"; GE7 flipped from a total consumer ban to "named imports of the service surface only" (mint/get/delete, the TTL constant, the two types — no default/namespace/side-effect end-run). GE5 (exactly ONE kv namespace) and GE6 (no Env guidance field) stand untouched because the service reuses `chat_sessions`.
|
||
- **`/api/guidance` is live over HTTP** (task 2): POST (401 → 413 → 400 → 201 classes; hand-rolled snapshot validation — slug `destinationId` per the search workspace's ENTRY_ID_PATTERN law, bounded `destinationName`, finite non-negative metrics, safe-integer `startedAtSec`, geometry that `decodePolyline6` decodes to ≥ 2 points) → `mintGuidanceSession(env.chat_sessions, claim.chat_id, snapshot)` → 201 `{id}`. GET returns the envelope only to the OWNING chat; DELETE removes the record actively. Non-POST/GET/DELETE → 405.
|
||
- **The threat register's mitigations are all code, all gate-proven**: T-06-02 (body `chat_id: 999` with claim chat 101 stores 101 — GW4 injection probe; snapshot built field-by-field so extras are structurally dropped), T-06-02b (foreign/absent → identical 404 `{error:"not_found"}`; the record SURVIVES a foreign GET/DELETE — GW7/GW10), T-06-04 (every log line is a literal category — `[guidance] rejected: unauthorized|malformed`; no ids, no coordinates, no payloads; TTL 7200 s on every write — PG1+GE4+GS1), DoS (MAX_GUIDANCE_BODY_CHARS 8192 with the pre/post-read double check — GW3 proves the lying-Content-Length backstop).
|
||
- **PG1 grew with the write path**: `kv/guidance-sessions.ts` joined `storageModules` in the same commit that added the `.put(` (the PATTERNS caution honored mechanically).
|
||
|
||
## Task Commits
|
||
|
||
1. **task 1: envelope service + GE4–GE7 re-pins + PG1 (TDD)**
|
||
- `0cfa0bd` test(06-03): re-pin GE4/GE7 to the live service + behavioral envelope suite + PG1 storage list (RED)
|
||
- `c87dc7d` feat(06-03): guidance envelope service — TTL-only mint/get/delete beneath the frozen shape (GREEN)
|
||
2. **task 2: /api/guidance POST + GET + DELETE (TDD)**
|
||
- `212aa57` test(06-03): guidance endpoint suite GW1-GW11 — 401/413/400 classes, claim-owned chat (T-06-02 injection probe), own-vs-foreign GET/DELETE, 405 (RED)
|
||
- `7875ed4` feat(06-03): /api/guidance POST/GET/DELETE — Bearer-claim gated, TTL-only service beneath (GREEN) — includes the RP2 re-pin + `@araharah/routing` workspace dep
|
||
|
||
## TDD Gate Compliance
|
||
|
||
- Task 1: RED `0cfa0bd` (GE4/GS1–GS5 fail on the absent service; GE1/GE2/GE3 pass unchanged; GE5–GE7 green-by-construction boundary guards, documented) → GREEN `c87dc7d` (90-suite predecessor green).
|
||
- Task 2: RED `212aa57` (GW1–GW7, GW9–GW11 fail on the missing route arm; GW8 green-by-construction — the catch-all already 404s; noted in the RED commit message) → GREEN `7875ed4`.
|
||
- Both RED→GREEN pairs verified in git log; no gate skipped.
|
||
|
||
## Files Created/Modified
|
||
|
||
- `bot/src/kv/guidance-sessions.ts` — frozen types untouched; the service added below them: `GuidanceSessionKv` seam, `ENVELOPE_ID_PATTERN` + exact-key tables (`ENVELOPE_KEYS`/`SNAPSHOT_KEYS`), mint/get/delete with the defensive-read field law (`isEnvelopeValid`). Module header now states the live-service law: shape frozen (GE2); the service owns every write; every write carries `expirationTtl: GUIDANCE_SESSION_TTL_SECONDS`.
|
||
- `bot/src/webhook.ts` — `/api/guidance` route arm + `handleGuidance` (method gate → Bearer claim → POST cap/validate/mint | GET/DELETE resolve-then-own), `MAX_GUIDANCE_BODY_CHARS = 8192`, `DESTINATION_ID_PATTERN` (mirrors `search/src/source.ts`), `MAX_DESTINATION_NAME_CHARS = 200`, `validateSnapshot` (field-by-field construction), category-only response helpers; header route table documents the new surface.
|
||
- `bot/test/guidance-envelope.test.mjs` — GE1–GE3 unchanged in meaning; GE4/GE7 re-pinned; GS1–GS5 behavioral (TTL'd mint, round-trip, 7-class defensive null read, UUID key-guard with a KV-untouched counter, active delete).
|
||
- `bot/test/webhook.test.mjs` — GW1–GW11 offline undici block; claims minted through the real `/api/session` path; injection probe; log-literal assertions.
|
||
- `bot/test/privacy.test.mjs` — PG1 `storageModules` + `kv/guidance-sessions.ts`.
|
||
- `bot/test/route-privacy.test.mjs` — RP2 re-pinned to the codec-only allowlist (header documents the 06-03 re-pin and why); RP1 unchanged.
|
||
- `bot/package.json` + root `package-lock.json` — `"@araharah/routing": "*"` workspace dep.
|
||
|
||
## Decisions Made
|
||
|
||
- **RP2 as an allowlist, not a lifted ban** — the plan required `import { decodePolyline6 } from "@araharah/routing/src/polyline.ts"` in webhook.ts, which the old RP2 (zero `@araharah/routing` tokens in bot/src) forbids. Per the gate re-pin law (PATTERNS: "the plan adding the routing import re-pins RP2, listed explicitly"), the GREEN commit narrows RP2 to: every routing specifier in bot/src must be EXACTLY the pure codec subpath. The routing stack (router/graph/A*/fixtures) still cannot enter the Worker; RP1 (no `/api/route` proxy) untouched.
|
||
- **Envelope storage reuses `chat_sessions`** — the frozen key scheme (`guidance:<id>`) lives in the existing namespace, so GE5/GE6 stay green without edits; no second namespace was created (R2 later is a different binding class).
|
||
- **`deleteGuidanceSession` UUID-guards like the read** — a malformed id constructs no KV key at all (silent no-op), closing arbitrary-key deletion symmetrically with the V5 read guard.
|
||
|
||
## Deviations from Plan
|
||
|
||
### Auto-fixed Issues
|
||
|
||
**1. [Rule 3 - Blocking] RP2 gate re-pin (file not in `files_modified`)**
|
||
- **Found during:** task 2
|
||
- **Issue:** the plan's required codec import in `bot/src/webhook.ts` makes the existing RP2 gate (zero `@araharah/routing` specifiers in bot/src) permanently red — task 2's own verify (`npm test -w bot` green) is impossible without touching `bot/test/route-privacy.test.mjs`, which the plan's file list omitted.
|
||
- **Fix:** re-pinned RP2 to the codec-only allowlist in the same GREEN commit as the import, per the repo's explicit gate-re-pin law (PATTERNS "Gate re-pins ship WITH the change that invalidates them", which names RP2 and THIS kind of plan as the owner).
|
||
- **Files modified:** bot/test/route-privacy.test.mjs
|
||
- **Commit:** 7875ed4
|
||
|
||
**2. [Rule 1 - Bug] GW test write-count baselines corrected at GREEN**
|
||
- **Found during:** task 2 GREEN
|
||
- **Issue:** the RED fixtures minted claims via the real `mintSession` (2 KV writes) but several GW cases asserted absolute `ttlLog.length` (0 or 1) — a fixture-counting bug that would have made honest implementations fail.
|
||
- **Fix:** counts made relative to a `writesBefore` baseline captured after the claim mint; assertion strength unchanged (the guidance write is still proven singular, TTL'd, and correctly keyed).
|
||
- **Files modified:** bot/test/webhook.test.mjs
|
||
- **Commit:** 7875ed4
|
||
|
||
### Session Continuation Note
|
||
|
||
This plan executed across two executor sessions: the first landed all four task commits but its final working-tree writes flushed late (observed by the continuation agent as a tree that settled between checks). The continuation agent verified every gate post-flush (all four commits present, 90/90 green, typecheck clean, no untracked residue, no deletions in any plan commit) rather than re-executing. No work was redone or lost.
|
||
|
||
## Known Stubs
|
||
|
||
None — the service and all three endpoints are fully wired to the real KV binding (`env.chat_sessions`); nothing is mocked, placeholdered, or deferred. Plan 07's cron is the designed next consumer, not a missing piece of this plan.
|
||
|
||
## Threat Surface Scan
|
||
|
||
No surface beyond the plan's `<threat_model>`: one new endpoint family (`/api/guidance`, Bearer-gated — covered), one new KV write path (TTL-only — covered by PG1/GE4), no new bindings, no file access, no schema at a trust boundary. All four STRIDE mitigations are implemented and gate-proven (see Accomplishments). No threat flags.
|
||
|
||
## Issues Encountered
|
||
|
||
- Node lives under nvm in this environment — verification commands source `~/.nvm/nvm.sh` first (the 05-01/05-03 precedent). No project impact.
|
||
- STATE.md / ROADMAP.md / REQUIREMENTS.md intentionally untouched — orchestrator-owned writes after the wave completes (GUID-05 completion is recorded above for the orchestrator to tick).
|
||
|
||
## User Setup Required
|
||
|
||
None — no new secrets, no new wrangler bindings, no external services. The workspace dep links locally via npm workspaces; deploy needs nothing beyond the existing `chat_sessions` namespace.
|
||
|
||
## Next Phase Readiness
|
||
|
||
- **Plan 07 (cron turn-cards)**: reads envelopes through `getGuidanceSession`'s defensive validation (corrupt → null, never pushed); iterates `guidance:` keys; pushes to `envelope.chat_id` only.
|
||
- **Web plans (GUID-05/06 flows)**: POST on minimize (envelope in → `{id}` out), DELETE on trip end (D-14's client half already has its server twin), GET on resume.
|
||
- **Verifier hooks**: GE1–GE7, PG1, RP1/RP2, GW1–GW11, GS1–GS5 all pin the surface mechanically; the injection probe (GW4) is the regression canary for the claim-only identity law.
|
||
|
||
---
|
||
*Phase: 06-turn-by-turn-guidance-live-driving*
|
||
*Completed: 2026-10-24*
|
||
|
||
## Self-Check: PASSED
|
||
|
||
- All 8 modified files + the SUMMARY verified present on disk.
|
||
- All four plan commits verified in git log (0cfa0bd RED, c87dc7d GREEN, 212aa57 RED, 7875ed4 GREEN) — correct RED→GREEN interleaving per task.
|
||
- `npm test -w bot`: 90 passed, 0 failed (GE1–GE7 re-pinned + GS1–GS5 + GW1–GW11 + all 74 pre-existing). `npm run bot:typecheck`: exit 0.
|
||
- Acceptance greps re-run post-flush: `expirationTtl: GUIDANCE_SESSION_TTL_SECONDS` ×3 in the service module; `MAX_GUIDANCE_BODY_CHARS = 8192` at webhook.ts:225; all `[guidance]` log lines are literal categories (no interpolation).
|
||
- No deletions in any plan commit; no untracked non-planning files; STATE.md / ROADMAP.md / REQUIREMENTS.md untouched.
|