Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,5 @@

/eslint.config.ts @lishaduck
/.oxlintrc.json @lishaduck
/vite.config.ts @lishaduck @petalcat
/renovate.json @lishaduck @petalcat
/tools/ @petalcat
/vite.config.ts @lishaduck
/renovate.json @lishaduck
58 changes: 42 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,53 @@
# PetalNet/Monorepo

The plan + canonical decisions: [tasks task-178](https://tasks.petalcat.dev/task/178).
Full spec snapshot: [docs/PLAN.md](./docs/PLAN.md).
PetalNet applications and shared libraries live in this monorepo. JavaScript and
TypeScript projects share a pnpm workspace and Vite+ toolchain; Rust and Flutter
projects keep their language-native workspaces and toolchains.

## Layout

```tree
apps/ each ships independently
packages/ shared libs (ui, utils, types)
tools/ repo-internal scripts
docs/ design / architecture / runbooks
apps/ independently shipped applications and services
packages/ shared TypeScript libraries and configuration
docs/ repository architecture and migration history
```

## Stack
### Applications

- **pnpm 11** with workspace `catalog:` for shared dep versions
- **Vite+** for build / test / lint / fmt / task running (`vp run` with `dependsOn`, `-r`, `-t`, content-addressable cache)
- **oxlint + eslint** dual lint, overlap killed by `eslint-plugin-oxlint`
- **Tailwind v4 + DaisyUI** as the runtime styling layer
- **knip + manypkg + typesync + update-ts-references** for hygiene
- **Renovate** for dep bumps
- No Turbo, no Prettier, no changesets
- Web and Node: `clarity-mcp`, `collegemap`, `console`, `grove`, and `slide`
- Rust: `box-agent`, `control-plane`, `courier`, `dispatcher`, `manager`, and the
Point server
- Flutter: the Point client under `apps/point/app`

## Status
### Shared packages

**Not live yet.** Migrations of source repos land here over the next sessions; originals stay authoritative until Parker greenlights the cutover.
- `@petalnet/better-auth-effect-qb-adapter`
- `@petalnet/console-bus-rpc`
- `@petalnet/svelte-ws`
- `@petalnet/tsconfig`, `@petalnet/types`, `@petalnet/ui`, and `@petalnet/utils`

## Toolchain

- Node 26 (see `.nvmrc` and `package.json`) and pnpm 11
- Vite+ (`vp`) for task running and formatting
- oxlint and ESLint for linting; Knip, manypkg, typesync, and
update-ts-references for workspace hygiene
- Tailwind CSS v4 for pnpm apps that use Tailwind, except `apps/slide`, which
remains on the shared Tailwind v3 legacy catalog
- Cargo and Flutter tooling for non-pnpm projects

Install dependencies with `pnpm install`. Run the root workflows through Vite+:

```sh
vp run check
vp run test
vp run build
```

Useful focused commands include `vp run lint:knip`, `vp run manypkg`, and
`vp run typesync:check`. Root script definitions are in [`package.json`](./package.json),
and workspace membership and dependency catalogs are in
[`pnpm-workspace.yaml`](./pnpm-workspace.yaml).

See [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) for repository mechanics and
[`docs/MIGRATION.md`](./docs/MIGRATION.md) for the historical migration journal.
265 changes: 0 additions & 265 deletions SOL-UNIFY-TASK.md

This file was deleted.

6 changes: 3 additions & 3 deletions apps/box-agent/DECISIONS-box-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ spawns/supervises disposable workers, advertises capacity, and emits fleet event
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| BA1 | New app `apps/box-agent`, path-dep on `apps/dispatcher` for envelope/card types + spool transport | same contract-types source of truth as control-plane (CP2) |
| BA2 | Workers are plain child processes (`std::process::Command`, argv from config with `{body}`/`{task_id}`/`{card_id}` placeholders + card fields in env) — NO tmux, NO shell string | Windows first-class is LOCKED (OS-neutral contracts); tmux is the per-agent manager's business, not the box-agent's; argv-not-shell kills injection by construction |
| BA3 | Envelope consumption dedups on envelope `id` persisted in the box-agent's own SQLite (`seen` table), and additionally on `card_id` for task.dispatch | contract D20: redials re-send the same id; receivers MUST de-duplicate — and crash-replayed cards carry the same card_id |
| BA4 | Slot accounting: `free_slots = max_workers − running`; deferred cards queue locally (FIFO) when full; honored INTERRUPT cards bypass the slot cap and spawn immediately | the interrupt classes exist precisely because they must not wait behind queued work; the cap protects the box from bulk work, not from Parker |
| BA3 | **Superseded by the durable-pending review decision below.** Envelope ids still deduplicate non-card envelopes in `seen`; accepted task cards deduplicate on `card_id` in `pending`. | acceptance must be durable before spool consumption; `pending` is the task-card source of truth |
| BA4 | **Superseded by the bounded-interrupt review decision below.** Deferred cards observe `max_workers`; honored interrupts may exceed that soft cap, but never the worker pool's absolute ceiling. | interrupts must not wait behind bulk work, but they must not permit unbounded process creation |
| BA5 | Fleet events are written per the fleet-event contract v1 (snapshot = latest event, canonical lowercase handle + '.N' host, producer never writes `offline`, `task_id` = the working card's task) | CONTRACTS §3 verbatim; the box-agent is a producer, normalization is the producer's job |
| BA6 | Worker completion emits a `response` envelope (`in_reply_to` = the task.dispatch request id) with exit status + captured output tail into the outbox spool | the dispatch request expects an answer; response correlation is the envelope contract's job — no new method needed |
| BA7 | Capacity reports (`agent.capacity` events) emit every `capacity_interval_secs` AND immediately on slot change | the control-plane's liveness derivation needs a steady cadence; slot-change pushes make the push-router's view fresh without a tight loop |
Expand Down Expand Up @@ -41,7 +41,7 @@ than clobbered (adversarial #2). Verified end-to-end and by a durability integra
Other fixes, all tested:
| Finding | Fix |
|---|---|
| adversarial #3: interrupt cap bypass = fork-bomb | interrupts bypass the SOFT slot cap but not an absolute `hard_ceiling` |
| adversarial #3: interrupt cap bypass = fork-bomb | interrupts bypass the soft `max_workers` cap but not the worker pool's internal absolute ceiling, currently `max_workers * 4 + 4` (minimum 8 because config requires `max_workers >= 1`) |
| #6: hung worker starves a slot forever | per-worker deadline (pool budget ∧ card `expires_at`); breach → kill + timeout response |
| #4: one delivery failure killed the daemon | per-item deliver-with-retry; failures logged, loop never exits on IO |
| #5: failed cards → caller hangs | malformed task.dispatch gets an `Error` envelope (in*reply_to set); transient spawn failure just retries from pending |
Expand Down
2 changes: 1 addition & 1 deletion apps/console/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Register

product
Lab Console

## Users

Expand Down
40 changes: 18 additions & 22 deletions apps/console/docs/BETTER-AUTH-OIDC.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,40 @@ provider. Do not enable Better Auth's `oidcProvider` plugin.

## Authentik provider/application

Create a confidential OAuth2/OIDC provider and application for `console.petalcat.dev`:
Create one confidential OAuth2/OIDC provider and application for the deployed console. The demo
deployment uses the unified service/application `console-demo` and Better Auth provider ID `oidc`:

- Client type: confidential; authorization-code flow.
- Redirect URI (exact): `https://console-demo.petalcat.dev/api/auth/oauth2/callback/authentik`.
- Scopes: `openid profile email groups`.
- Redirect URI (exact for demo): `https://console-demo.petalcat.dev/api/auth/oauth2/callback/oidc`.
- Scopes: `openid profile email` by default. Add custom scopes only when their claims are actually
mapped and consumed; the current runtime does not request `groups`.
- Subject mode: stable Authentik user ID. The profile must include `preferred_username`, `email`,
`name`, and a `groups` array. Only exact membership in `authentik Admins` or `admin` is inherited
from Authentik; either maps to the console `owner` tier. Other tiers are managed in Better Auth
and are not inferred from Authentik group names.
- Issuer: the Authentik application provider issuer, conventionally
`https://auth.petalcat.dev/application/o/console/`. Its discovery document must be available at
- Issuer: the Authentik application provider issuer; demo uses
`https://auth.petalcat.dev/application/o/console-demo/`. Its discovery document must be available at
`<issuer-without-trailing-slash>/.well-known/openid-configuration`.

The demo provider (`Lab Console Demo`, provider ID 69) was verified through the Authentik API on
2026-07-13. It has the `openid`, `profile`, `email`, and custom `groups` property mappings attached,
includes mapped claims in the ID token, and its `profile` mapping emits
`preferred_username: request.user.username`. No Authentik configuration change was made. The
console mapper prefers that claim, then falls back to a validated email localpart or subject so a
missing optional profile claim cannot abort user creation; the session verifier still validates the
result before resolving any ReBAC identity.

## Runtime environment

Configure both console services with the same `DATABASE_URL`, `BETTER_AUTH_URL`, and
`BETTER_AUTH_SECRET`. Store a freshly generated secret in the lab vault as
There is one unified SvelteKit service. Configure it with `DATABASE_URL`, `BETTER_AUTH_URL`,
`BETTER_AUTH_SECRET`, `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET`. Optional
`OIDC_REDIRECT_URI` overrides Better Auth's callback and `OIDC_SCOPES` is a comma-separated list
(default `openid,profile,email`). Store a freshly generated Better Auth secret in the lab vault as
`console-better-auth-secret`; never put it in Git. Configure SvelteKit with
`AUTHENTIK_OIDC_ISSUER`, `AUTHENTIK_OIDC_CLIENT_ID`, and `AUTHENTIK_OIDC_CLIENT_SECRET` from the
Authentik client. Better Auth uses host-only `__Host-console.*` cookies with `Secure`, `HttpOnly`,
the `OIDC_*` values from the Authentik client. Better Auth uses deployment-derived cookie prefixes
with `Secure` on HTTPS, `HttpOnly`,
`SameSite=Lax`, and `Path=/`; do not configure a parent cookie domain. `SameSite=Lax` allows the
short-lived OAuth state cookie on Authentik's top-level GET callback without exposing it on
cross-site subrequests; Better Auth stores the PKCE verifier in its server-side verification row.
Browser API calls go directly to console-api through same-origin `/api/v1/*`
routing, while `/api/auth/*` remains on SvelteKit. Better Auth sessions expire
Browser API calls and `/api/auth/*` are served by the same SvelteKit process. Better Auth sessions expire
after five minutes and are not extended, bounding Authentik group and `TERM_ADMIN` revocation lag.

The Better Auth CLI-generated schema is committed at `migrations/001-better-auth.sql` and the
idempotent console-api boot migration creates the same tables. Regenerate it with the Better Auth
CLI against a disposable console Postgres database after schema changes.
Authentication storage is created by the ordered app migrations:
`migrations/0001_foundation.sql` contains the Better Auth tables and
`migrations/0002_console_domain.sql` contains the console's Better-Auth principal mapping. The
normal migration runner applies both; there is no separate generated Better Auth migration.

## Browser boundary

Expand Down
13 changes: 12 additions & 1 deletion apps/console/docs/adr/UNIFIED-SVELTEKIT-CONSOLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
- **Decision owner:** Eli
- **Contracts:** [CONSOLE-CONTRACTS.md](../contracts/CONSOLE-CONTRACTS.md)

## Contract-source inventory

- Browser-safe console entities and schema conventions: `src/lib/contracts/` (aggregated by
`src/lib/server/domain/api-schema.ts`).
- Bus frames and shared op/query wire schemas: `packages/console-bus-rpc/src/schema.ts`.
- Operation catalog: `docs/contracts/ops.json`, decoded by
`src/lib/contracts/op-catalog.ts` and consumed by both UI and server.
- OpenAPI is derived from those Effect Schemas at `/api/v1/openapi.json`; deleted JSON Schema
mirrors are not contract sources.

## Context

The console UI and its TypeScript gateway were separate applications even though they shared a
Expand Down Expand Up @@ -83,7 +93,8 @@ freshness/provenance. They never invent success or derive authorization from UI

## Rewrite decisions (feat/console-rewrite, 2026-07-19)

Running log; one line per non-obvious call, appended phase by phase.
Historical implementation running log; retained for provenance. It records the rewrite as it
landed and is not a second current architecture specification.

### Phase 1 — legacy standalone server excision

Expand Down
24 changes: 13 additions & 11 deletions apps/console/docs/contracts/CONSOLE-CONTRACTS.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Lab Console — Contract Surface (P0, board round 1 applied)
# Lab Console — Contract Surface

\_Branch `feat/console-p0-contracts` · console-backend Fable, 2026-07-12 · REVIEWABLE SPEC ONLY —
no service code in this node. This is the contract the console FRONTEND builds against and the
work list the console-backend phases implement. Machine-readable schemas are Effect Schema
modules — the single source of truth (rewrite Phase 4; the former generated JSON Schema mirror
is retired):
_Originally drafted on branch `feat/console-p0-contracts` by console-backend Fable,
2026-07-12; that is historical provenance, not the current topology or status._ This document now
describes the implemented contract surface consumed by the unified `apps/console` application.
Machine-readable schemas are Effect Schema modules — the current single source of truth (rewrite
Phase 4; the former generated JSON Schema mirror is retired):

- server->client contract entities: [`src/lib/contracts/entities.ts`](../../src/lib/contracts/entities.ts),
aggregated with the REST components in
Expand All @@ -13,8 +13,9 @@ is retired):
- bus frames: [`packages/console-bus-rpc/src/schema.ts`](../../../../packages/console-bus-rpc/src/schema.ts);
- the op catalog: [`ops.json`](ops.json) (self-contained; decoded through
[`src/lib/contracts/op-catalog.ts`](../../src/lib/contracts/op-catalog.ts)).
Grounded in: the console specs (`console-fable/specs/src/`, esp. `00-foundations` §6),
WAYFINDER-DECISIONS.md, SYSTEM-MAP-as-built.md, the N0.1 contracts, GRAPHING-BACKEND-BRIEF.md.\_
Historical design sources: the console specs (`console-fable/specs/src/`, esp. `00-foundations`
§6), WAYFINDER-DECISIONS.md, SYSTEM-MAP-as-built.md, the N0.1 contracts, and
GRAPHING-BACKEND-BRIEF.md.

The console binds to **four planes**. Where the UI specs and this document disagree, the specs'
_requirements_ win and this document has a bug; where this document and an implementation
Expand Down Expand Up @@ -79,9 +80,10 @@ Library item drill reads `GET /api/v1/library/items/:itemId`, scoped typed edges
`GET /api/v1/library/items/:itemId/history`. History entries are ordered newest first and carry
`version`, `tx_from`, and the complete item envelope believed at that transaction.

One service owns the surface: **`console-api`** (`apps/console-api`) — a gateway + substrate,
not a re-implementation: commands route to their real executors, reads serve from the lake and
the sources of truth. Blast radius, stated plainly: console-api down ⇒ every console surface is
One unified SvelteKit service owns the surface: **Lab Console** (`apps/console`) — a gateway +
substrate, not a re-implementation. Its in-process console API routes commands to their real
executors and serves reads from the lake and domain sources of truth. Blast radius, stated plainly:
the console process down ⇒ every console surface is
honestly dark and Matrix remains the command floor; the assistant is never a dependency of the
emergency path, and neither is this service pretending otherwise.

Expand Down
Loading
Loading