Plan D Completion Notes — Deferred Backlog (2026-07-02)
Companion to 2026-07-02-monorepo-split-plan-d-backlog.md. Records what shipped,
what broke along the way, and what remains deliberately deferred. Deploy order was
D3 → D4 → D2 → D1; all 17 tasks and the final checklist are green.
Item summary
| Item | Scope | Outcome |
|---|---|---|
| D3 — Durable RAG net attach | ai-ahu-rag compose on Server 2 declares ahu-net external |
Shipped (Server 2 compose edit; backup compose.yaml.bak-plan-d). --force-recreate preserves the attach; public doc query green afterwards. No repo commit — the compose lives on Server 2. |
| D4 — Local embeddings | Env-gated TEI embedder (tei-qwen3-embed) in both agents + pgvector re-ingest |
Shipped (f39147c, 1f8c463, dde0527). Re-ingested at 2560 dims via load_knowledge --recreate; hybrid search returns non-zero scores through TEI; agent logs clean (2aacf95). |
| D2 — sql-guard enforcement | sqlglot pre-execution guard in the Python public agent, SURFACE=public gated; public stack flipped to public_dash via DASH_VARIANT=public |
Shipped (f711af6, 079988c, cf8225e, 6c7b348). 23 in-container pytest green; live forbidden pattern refused; live aggregate passes with per-year table (2024: 171.943 / 2025: 220.839 / 2026: 127.055) matching a direct DB probe; run_sql_query confirmed in session tool_calls. Staff surface untouched (data-agent + reasoning-data-agent still registered, spot-check run COMPLETED). |
| D1 — Staff server-side auth | SQLite staff_users + bcrypt, httpOnly JWT cookie (ahu_staff_session, HS256, 8h) with tv (token_version) claim, Edge-safe middleware on /admin/* + /api/admin/*, Node-side mutation guard re-checking token_version, users CRUD API + /admin/shared/users page, login rewired to the server route, mock accounts deleted |
Shipped (0171258 → 721c370). Live-verified from the internet: no-cookie 401 / login 200 / with-cookie 200; disabled user's unexpired session gets 401 "Sesi tidak berlaku lagi" on mutations while reads render until expiry; audit rows (user.create, auth.login, user.update, user.disable) confirmed on /data/audit.sqlite. |
| Cutover — Basic Auth removal | Comment out auth_basic on the staff vhost (local nginx edge) |
Shipped (499d9af). Backup at sites-available/x056.ahu-demo.chatbot-neo-staff.val.id.pre-plan-d-backup; htpasswd retained. Post-cutover internet checks: no-cookie 401, login page 200, with-cookie 200, page redirect 307. |
Bug log (what broke and how it was fixed)
- Orchestrator returned all-empty answers while the agent run was healthy (D2) —
evaluate.tssliced the data-tool response with.slice(0, 800)andcompose.ts
with.slice(0, 1200). Thepublic_dashagent streams inline reasoning before
its final answer (all as RunContent deltas), so the judge and synthesis only ever
saw reasoning, never the conclusion. Fixed via TDD to tail slices
.slice(-800)/.slice(-1200)(cf8225e). - Misdiagnosis trap en route to (1) — a POST access-log line 0.5s after request
start suggested the agent "completed instantly" (suspected guard short-circuit).
uvicorn logs access lines at response start, not completion; reconstructing the
SSE deltas showed the run had succeeded with the full table. - DataTool spoke the wrong wire format (D2) — the orchestrator's DataTool needed
AgentOS multipart form fields + RunContent event parsing to talk to the agent
(079988c). - Transient
Error during hybrid search: 'op'— benign agno filter-DSL KeyError
when the model emits an unsupported knowledge-filter; retries follow. One post-fix
live run failed on it; subsequent runs passed. Known noise, not a regression. - Turbopack dev panic —
pnpm devin internal-web ("Next.js package not found").
Workaround for all local smoke tests:./node_modules/.bin/next dev -p 3501
(no turbopack; 3501 avoids the staging-port convention on 3500). - Guard-insertion script corrupted a multi-line import — the Task 12 script that
edited all 16 admin routes inserted the guard import insideanon-limit/route.ts's
multi-lineimport { ... } from "@/lib/anon/limits". Caught immediately by
tsc --noEmit; moved below the import block. The other 15 files were clean. test:streamspointed at pre-split paths — internal-web's script referenced
scripts/test-*.tsrelative to the app (they live at repo root and import the old
rootsrc/, which moved topackages/streamsin Plan A). Repointed the script at
the migratedpackages/streamsvitest suite, 8/8 (9f4d926).- Live verification 401'd at nginx, not the app —
/tmp/staff-basic-auth.txt
holdsSTAFF_BASIC_PASS=<value>, not a bare password;$(cat …)sent the whole
line as the password. Parse withcut -d= -f2. - First Task 16 build died mid-image — the background build was killed when the
previous CLI session exited. Re-run completed from docker layer cache. - In-container better-sqlite3 audit check — plain
require('better-sqlite3')
fails in the deployed image; use the absolute pnpm path
/app/node_modules/.pnpm/better-sqlite3@11.10.0/node_modules/better-sqlite3. - No headless browser on the build box — Task 14's browser check was substituted
with API-level exercise of every page action (create / role change / disable /
reset password / last-admin guard) plus SSR-shell parity with the known-good jobs
page. Task 16 Step 5's browser flow was likewise driven with two curl cookie jars. - No passwordless sudo — the nginx cutover (Task 17 Steps 1–2) was run by the
operator via a single preparedsudo sh -c '…'command; verified from the session
afterwards.
Success criteria status (final checklist)
- ✅ Internet
/api/admin/*without cookie → 401; after login → 200 (post-cutover). - ✅ Disabled user's existing session cannot mutate — token_version verified live
(scratch adminuji@ahu.local, left disabled by design). - ✅
/admin/shared/usersCRUD works, writes audit rows; Basic Auth removed. - ✅ sql-guard pytest green in-container (23); live forbidden refused; allowed
aggregate passes. - ✅ RAG
--force-recreatepreserves the ahu-net attach; public doc query green. - ✅ Hybrid search non-zero via TEI; re-ingest via
load_knowledge --recreate. - ✅ Suites green: internal-web typecheck + vitest 10/10 + streams 8/8;
public-web 53/53 (incl. 13 sql-guard cases); shipped via build-and-ship +
deploy-staging (final tag 721c370).
Still deferred
- SSO Kemenkumham — by design, SSO replaces only the
/api/auth/loginroute;
cookie, middleware, mutation guard, andstaff_usersstay as-is. - Public DB replica flip — blocked; no replica exists. Public agent keeps
borrowing internal read-only MySQL creds (Plan C caveat) until one does.
Operational notes
- Staff console:
https://x056.ahu-demo.chatbot-neo-staff.val.id— session auth only
(no Basic Auth). Seed admin isadmin@ahu.local; password lives in git-ignored
infra/env/internal.env(STAFF_ADMIN_PASSWORD) and should be rotated via
/admin/shared/users→ Reset sandi after handoff. - Seeding only runs when
staff_usersis empty — rotating the env password later
does NOT change the DB credential; use the UI reset. - Rotating
NEXTAUTH_SECRETnow logs every staff session out (signer + verifier). - Basic Auth rollback: restore
*.pre-plan-d-backupover the vhost (or uncomment the
twoauth_basiclines) +nginx -t && systemctl reload nginx; htpasswd file kept. - Mutation revocation model: middleware checks JWT signature/expiry only (Edge-safe);
writes re-check the DB row (requireMutationSession). Read-only staleness is
bounded by the 8h cookie — accepted trade-off. uji@ahu.local(id 2) is a disabled scratch admin from live verification; user
delete is unsupported by design (audit trail), leave it disabled.- Dev-server smoke recipe: from
apps/internal-web,
STAFF_DB=/tmp/staff-dev.sqlite STAFF_ADMIN_EMAIL=admin@ahu.local STAFF_ADMIN_PASSWORD=dev-admin-123 AUDIT_DB=/tmp/audit-dev.sqlite ./node_modules/.bin/next dev -p 3501.