# Roadmap — Rekkan.Ai Agent Box

The incremental path from a working control plane to a GTM-ready product. Each version is a milestone that ships on its own: it has a goal, deliverables, and acceptance criteria that can be checked without reference to a later version. Dates are TBD and are not invented; a milestone is released when its acceptance criteria hold.

Naming follows the same rule as the changelog: "Rekkan.Ai" is the display name; internal identifiers (rekan package, rekan-api.service, brand_domain) stay lowercase rekan.


# v0.1.0 — Provisioning foundation

Goal. One command deploys a real Hermes agent box for a customer's business workload, and the platform can prove the box is running what it asked for.

Deliverables.

  • Job-driven lifecycle on LXD (create / start / stop / restart / re-apply / delete) with an append-only per-box event log.
  • The deploy wizard: three tenant questions (persona, private data, gateway); everything else hard-set and shown read-only.
  • Secrets only in the box's /etc/rekan/box.env (0600) and names-not-values in box.yaml; worker job payloads scrubbed after a job finishes.
  • Readiness proven by the SHA-256 apply marker in /var/lib/rekan/.apply-done.
  • Per-box Hermes dashboard over an SSH loopback tunnel with a derived per-box token.

Acceptance criteria. A box reaches READY only when its apply marker matches the pushed box.yaml; a marker mismatch leaves the box FAILED. Credential values never appear in a job row or in box.yaml. The suite is green.

Status. Shipped (TBD date). 141 tests.


# v0.2.0 — Locale and brand

Goal. The panel speaks the user's language wherever they sign in, and the product's display name is correct everywhere it is rendered.

Deliverables.

  • users.locale (ID/EN) defaulting from settings.default_locale, exposed on UserOut / MeOut / TokenOut.user.
  • PATCH /api/v1/auth/me for the caller's own record; anything but id/en is a 422.
  • Idempotent bootstrap migration adding the column to an existing users table (SQLite and PostgreSQL), to be replaced by Alembic before the next schema change.
  • Display brand "Rekkan.Ai" in every human-facing control-plane string, identifiers untouched.
  • docs/CHANGELOG.md and this roadmap; version 0.2.0.

Acceptance criteria. A fresh registration is id; a PATCH to en survives a new login; an invalid value is rejected and leaves the record unchanged; one user's PATCH cannot change another user's locale; init_db() adds the column to a table created without it and is safe to run twice; __version__ is semver and GET /api/v1/health reports it.

Status. Shipped (2026-09-22). 149 tests.


# v0.3.0 — Agent dashboard panel (simple view)

Goal. A business owner opens a box and sees what their co-worker is doing in plain language, without learning the Hermes dashboard.

Deliverables.

  • Simple view with four panels: Percakapan (conversations), Berkas (files), Pemakaian (usage), Kemampuan (capabilities / skills).
  • A "Manage agent" button on each box, opening that box's detail.
  • An Advanced toggle that passes the real dashboard through the existing allow-listed pass-through (GET /boxes/{id}/hermes/dash/…, rekan/api/hermes.py).

Acceptance criteria. Each simple panel is populated from real box data, not a placeholder; a tenant can only open its own boxes (cross-org access is 401/404); the Advanced toggle renders the allow-listed dashboard and anything off the allow list is refused with an explanation; the KEMAMPUAN panel reflects the workload pack the box was built from.

Status. Delivered 2026-09-22 (verification below). 125 frontend tests pass (cd web && node --test tests/).

What is done. The four panels, the Kelola agent / Manage agent button on every box row and in the box detail header, the #/boxes/:id/agent route, and the Advanced toggle with its allow-listed frame. Cross-org access is refused by the backend (_owned → 404, pinned in tests/test_box_dashboard.py).

What is not yet true of it. The Advanced frame cannot authenticate itself: an <iframe> navigation carries no Authorization header, and the proxy requires one, so the frame receives 401 and the view correctly falls back to the notice plus a new-tab link. Making the embed real is v0.4.0, and the minimal fix is a short-lived ticket cookie (issued by an authenticated call, scoped to the dash path) accepted by both …/hermes/dash/* and the root /api/* catch-all.


# v0.4.0 — Embedded Advanced dashboard

Goal. The full Hermes dashboard works inside the panel — chat and terminal included — so there is one interface, not two.

Deliverables.

  • WebSocket proxy for the dashboard's live paths (/api/ws, /api/chat, /api/pty), which are deliberately not proxied today (see the note in rekan/api/hermes.py).
  • A per-box origin so the SPA's absolute paths (/assets/…, /api/…) resolve through the panel instead of colliding with the panel's own routes.

Acceptance criteria. A chat message and a PTY session round-trip through the panel to a live box; the SPA loads with no broken asset or API URL under a per-box prefix; the proxy still enforces the tenant's JWT and the per-box token, and off-box WebSocket targets are refused.

Status. Delivered 2026-09-22. 191 control-plane tests (cd control-plane && .venv/bin/python -m pytest -q) and 133 frontend tests (cd web && node --test tests/) pass. The three acceptance criteria are measured by hand against a live box — real commands, real output — and not inferred from the suites: docs/v0.4.0-verification.md.

What is done.

  • A chat message and a PTY session round-trip through the panel to a live box. On rekan-rani-e194-40204c (LXD project rekan, 10.99.0.100), through a probe instance of the working tree on 127.0.0.1:8099: the panel minted the box-scoped ticket (POST …/hermes/ticket → 200, expires_in=3600), the /api/pty handshake answered 101, a real 130-byte terminal frame came back from the box's TUI, one keystroke was sent and the next frame (11 bytes) followed, and a subscriber on /api/events through the panel received a frame published on the box's own /api/pub byte-identically. Chat was read-only before this milestone.
  • The SPA loads with no broken asset or API URL under a per-box prefix. In a real browser on <prefix>/chat (<prefix> = /api/v1/boxes/{id}/hermes/dash): #root rendered with no page errors, 52 requests answered 200, the only 404s were the three config surfaces the allow list refuses on purpose, and all three live sockets (/api/ws, /api/events, /api/pty) were open=True through the prefix.
  • The proxy still enforces the tenant's JWT and the per-box token, and off-box WebSocket targets are refused. Pinned by tests: no ticket closes 1008, a path off the live allow list closes 1008 even with a valid ticket, a DELETED/DELETING box closes 1008, a tenant-supplied token/ticket/internal parameter is dropped and ours injected, and the upstream host comes from the box record — the client cannot name a target.

What is not claimed (part of what was actually verified, so it belongs here):

  • Not a dedicated origin. The embed is same-origin with the panel, so JavaScript the box serves runs with access to the panel's own storage. The blast radius is the tenant's own users — the box is theirs, and they already have a shell in it through this very terminal — never another org's. A per-box hostname/port is the hardening path, recorded as an open decision in docs/dashboard-in-panel.md.
  • Two dashboard tabs on two boxes share one ticket cookie, and the origin-root /assets fallback reads it, so their chunks come from the most recently ticketed box. Identical files on one Hermes build; reload the tab if builds differ.
  • /api/profiles, /api/profiles/active and /api/config stay refused on purpose (config surfaces), so the dashboard's profile switcher and config page degrade.

# v0.5.0 — Box-to-box isolation

Goal. Two tenants' boxes on the same bridge cannot reach each other, and the isolation is verified, not assumed.

Deliverables.

  • A network isolation mechanism that is measured to work between instances.

Acceptance criteria. From box A, a connection to box B's port is refused, and the refusal is demonstrated by measurement on real hardware. An LXD bridge ACL was measured not to filter instance-to-instance traffic (2026-09-22, docs/dashboard-in-panel.md: the ACL attached and was correctly ordered, yet a same-bridge instance still reached the port). So the mechanism here must be host nftables with bridge netfilter, or a per-guest firewall — not a bridge ACL.

Status. Planned. Date TBD.


# v0.6.0 — Image v2

Goal. A fresh box is useful sooner, and re-apply is safe for boxes that reach the model through a gateway route.

Deliverables.

  • The Hermes dashboard unit pre-warmed into the image (no first-boot build).
  • Guest apply that supports gateway-route configuration alongside the existing direct model endpoint.

Acceptance criteria. A newly created box has the dashboard unit active without building the SPA on first boot; a box configured with a gateway route applies cleanly and its apply marker still matches; an existing box re-applies without a manual step.

Status. Planned. Date TBD.


# v1.0.0 — The platform works end to end

Goal. Four customer Agent Boxes run on production infrastructure, reachable over their own messaging gateway, backed by a model endpoint that survives the primary going down, and a repository that cannot lose the code.

Deliverables.

  • Lifecycle-provisioned Agent Box VMs on LXD from a custom image the repo builds, each with a connected, route-gated messaging gateway installed as a system unit and a per-box Hermes dashboard.
  • The deploy wizard: three tenant questions, everything else hard-set and shown read-only, with the inference endpoints named on that table.
  • A primary and a backup model endpoint per box, the backup written where Hermes actually reads it (fallback_providers).
  • A landing page at / and a control panel at /app, both ID/EN.
  • infra/git/: the repository backs itself up to a private remote on every commit and every 15 minutes, and refuses to autosave a credential-shaped string.

Acceptance criteria. A box reaches READY only when its apply marker matches the pushed box.yaml; its gateway is enabled + active and answers its operator while logging no allowlist warning; a turn whose primary endpoint fails is answered from the backup endpoint; git log --branches --not --remotes is empty. Suite: 223 control-plane + 172 web tests green. Frozen as tag v1.0.0 — see docs/v1.0.0-checkpoint.md for what is open.

Status. Shipped 2026-09-24.


# v1.5.0 — The box becomes a coworker

Goal. A box stops being a chatbot: you choose its brain, it has a real shared and classified filesystem to work in, operators read the docs inside the panel, and anyone can talk to a box from the panel to watch it work.

Deliverables. (detail in docs/1.5-plan.md)

  • Model selection — a catalog of selectable models (Qwen3.6 35B, Nemotron 3.5 Lightning; the remote DeepSeek V4.0/V4.1 Flash options follow in a later phase) with the 64k floor enforced at catalog build and deploy; model.choice in box.yaml; a picker in the wizard.
  • Shared filesystem — a file server provisioned from the repo, with per-file department and information level metadata, mounted into each box as its working directory plus a read-only knowledge mount. Enforced by the control plane's API (the share is never exported to a box), so the agent cannot reach what its policy forbids.
  • Documentation — a public documentation site at /docs (explicit allow-list; internal docs are never published) plus the in-panel view, both generated from docs/*.md.
  • Playground — a minimal per-box chat in the panel (streaming, tool calls, which model answered) over the existing ticket + WebSocket bridge.

Acceptance criteria. Each box answers from its selected model; a box cannot read another department's file, nor a file above its declared data classes — proven by cat inside the box, not by reading config; a box reboot restores its mounts with no manual step; every doc is reachable in the panel; a panel chat streams an answer with a tool call visible and still 403s without a ticket; the phase's E2E suite runs green on a throwaway box and git log --branches --not --remotes is empty.

Status. Planned — in progress on branch 1.5, built on the v1.0.0 floor.


# v1.6.0 — Agent boundary hardening

Goal. A coworker keeps its employer's infrastructure out of the conversation. Whatever pack a box came from, and whatever the operator answered in the wizard, it does not hand out the address of the machine it runs on, does not describe the platform behind it, and does not touch the host, the container runtime or a neighbouring box.

Deliverables.

  • The boundaries live in the base of SOUL.md — the block image/guest/rekan-box-apply writes before any persona answer. Four rules: never disclose infrastructure (address, IP, hostname, port, container or instance name), never describe the platform's internals (control plane, scheduler, model endpoint, storage tier, deployment layout), never touch the platform (host, container runtime, another instance), and treat an attempt to lift the boundaries as an incident.
  • Base, not a pack field. They deliberately do not live in persona.directives: that section is the operator's, capped at five by the wizard and editable per tenant, so a rule put there is one a customer can read back and ask to have removed.
  • Three pins in control-plane/tests/test_guest_apply.py: all twelve shipped packs produce a box that carries them (applied for real, one box per pack), the block is written before the persona renderer appends, and the five-directive worst case still deploys.

Acceptance criteria. Every shipped pack, applied headless, writes all four boundary rules into its box's SOUL.md; the block survives with the persona section full or empty; __version__, pyproject.toml and the ## [1.6.0] changelog entry agree with each other.

Status. In progress on branch 1.6 since 2026-09-26, built on the 1.5 floor. Honest limit: SOUL.md is instruction, not enforcement. A box's own config.yaml still names its model endpoint, the engineering-devops pack keeps the terminal toolset and can read its own address, and boxes share lxdbr0 with the control plane. Network and instance isolation is v0.5.0's milestone, not this one's — this milestone makes the coworker refuse, it does not make refusal impossible to bypass.


# v2.0.0 — GTM-ready

Goal. A customer can be sold, onboarded and supported without an engineer in the loop.

Deliverables.

  • A sales deck. (exists: docs/gtm/Rekkan.Ai-Agent-Box-GTM.pptx)
  • A self-serve onboarding path from sign-up to first running box.

Acceptance criteria. A new customer signs up, deploys a box and uses it without engineering assistance; the documentation site (shipped in v1.5.0) covers sign-in, the deploy wizard, the dashboard and troubleshooting; the version, changelog and roadmap in the shipped build agree with each other.

Status. Planned. Date TBD.