Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
5139ff2
feat(crypto): recover the satellite, routing its core seams through /…
Arcoders Jul 15, 2026
36ccd6e
feat(ai): add tool/function calling to the AI satellite (WS-AI-11)
Arcoders Jul 16, 2026
fe33592
feat(ai): meter+audit the tool executor and publish the ./tools autho…
Arcoders Jul 16, 2026
e39c467
feat(ai): wire the tool loop live through the chat controller (WS-AI-11)
Arcoders Jul 16, 2026
01d1fbb
test(ai): fuzz the tool argument + result surfaces and pin the front …
Arcoders Jul 16, 2026
0aa4fdd
docs(ai): pin I7 with a structural guard and document the tool surfac…
Arcoders Jul 16, 2026
52aa2a0
test(ai): prove tool isolation on a real database, interleaved (WS-AI…
Arcoders Jul 16, 2026
6725508
chore(tests): drop the scaffold READMEs from slots that have specs
Arcoders Jul 17, 2026
0caa4c1
test(ai): add the fault-injection tier for the tool loop (WS-AI-11)
Arcoders Jul 17, 2026
950372f
feat(ai): make the per-tenant concurrency cap observable (WS-AI-11)
Arcoders Jul 17, 2026
873f8df
fix(ai): let only content frames reach conversation memory (WS-AI-11)
Arcoders Jul 17, 2026
734dd39
feat(ai): mint and verify action-tool confirmations (WS-AI-11 Phase 3a)
Arcoders Jul 17, 2026
9277857
feat(ai): fence confirmed actions at most once (WS-AI-11 Phase 3a)
Arcoders Jul 17, 2026
499cade
feat(ai): gate action tools on a human confirmation (WS-AI-11 Phase 3a)
Arcoders Jul 17, 2026
60bab57
build: guard that every package typechecks its own tests
Arcoders Jul 17, 2026
83edab7
test: typecheck every package's specs and fix what surfaced
Arcoders Jul 17, 2026
d85f0b7
docs: link the AI tools guide from the satellites overview
Arcoders Jul 17, 2026
c43a883
feat(ai): plan a whole tool round and challenge actions before runnin…
Arcoders Jul 17, 2026
7edc66d
feat(ai): wire the action-confirmation flow through the controller (W…
Arcoders Jul 17, 2026
76f596b
test(ai): pin the fatal/retryable taxonomy and refresh the action-too…
Arcoders Jul 17, 2026
cdfec46
docs(ai): document the human-confirmation flow for action tools (WS-A…
Arcoders Jul 17, 2026
6ba1714
test(ai): chaos spec — an action fails closed when the audit DB is do…
Arcoders Jul 17, 2026
66188a0
feat(rental): add the Karimoto example app
Arcoders Jul 17, 2026
afdb2db
feat(rental): rich-render assistant answers (markdown + stat/chart bl…
Arcoders Jul 18, 2026
3e23cf0
feat(isthmus): harden the guard-audit event (metadata + dispatch policy)
Arcoders Jul 18, 2026
f411bef
perf(isthmus): self-overhead invariant, micro-bench, O(1) satellite l…
Arcoders Jul 18, 2026
df3cf9d
feat(isolation): unbypassable ceiling, migration/warm pools, PgBounce…
Arcoders Jul 18, 2026
c8c9705
docs(isthmus): security model, decision note, connection/pool guidance
Arcoders Jul 18, 2026
b9a4b1b
feat(doctor): precise diagnosis and a non-destructive root cure
Arcoders Jul 18, 2026
269badf
feat(doctor): do-no-harm — status exhaustiveness + non-destructive le…
Arcoders Jul 19, 2026
54852d6
docs(ai): enterprise-hardening design bundle (Deliverable 0)
Arcoders Jul 19, 2026
b3d9ca9
refactor(ai): compile-force retryability, extract the stream hotspot,…
Arcoders Jul 19, 2026
981d65c
feat(ai): resilience foundation — one policy seam, a vector-store out…
Arcoders Jul 19, 2026
79a91d7
test(ai): multi-spec threat-vector matrix + fault-tier fail-open veri…
Arcoders Jul 19, 2026
ef6c9ef
feat(ai): injection defense — observable structural boundary + plugga…
Arcoders Jul 19, 2026
b9081b2
feat(ai): audit consumption pillar — reader/query, self-verifiable ex…
Arcoders Jul 19, 2026
6ab8c8e
fix(ai): unbreak the Wave-4 audit-checkpoints stub + pre-existing tes…
Arcoders Jul 20, 2026
ccd05c4
feat(ai): data-at-rest seams — per-tenant memory DEK + embeddings con…
Arcoders Jul 20, 2026
17a95db
docs(ai): document the Wave 5 data-at-rest surface
Arcoders Jul 25, 2026
d38e28e
style(ai): rewrite comments and docblocks to a human voice
Arcoders Jul 25, 2026
9cf2ddb
ci: temporarily run CI on LASAGNA-140726/recover-crypto
Arcoders Jul 25, 2026
7f50a90
build: build crypto before ai so the Wave 5 peer resolves
Arcoders Jul 25, 2026
35f4f81
feat(core): add the first-class 'ai' audit actor type
Arcoders Jul 25, 2026
e434996
style: prettier-format the doctor and isthmus wave files
Arcoders Jul 25, 2026
052dbaa
docs: document the tenant:ai:audit export and archive commands
Arcoders Jul 25, 2026
2e8d794
ci: register the apps/rental workspace with knip
Arcoders Jul 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
59 changes: 51 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: CI

on:
push:
# TEMP: run CI on this branch while it bakes. Revert before merging.
branches: [master, main, LASAGNA-090706/soft]
# TEMP: run CI on these branches while they bake. Revert before merging.
branches: [master, main, LASAGNA-090706/soft, LASAGNA-140726/recover-crypto]
pull_request:
branches: [master, main]

Expand Down Expand Up @@ -53,8 +53,8 @@ jobs:
- name: Build (core)
run: npm run build

- name: Build satellite packages (sso, billing, admin, backup, websockets, reporting, ai)
run: npm run build:sso && npm run build:billing && npm run build:admin && npm run build:backup && npm run build:websockets && npm run build:reporting && npm run build:ai
- name: Build satellite packages (crypto, sso, billing, admin, backup, websockets, reporting, ai)
run: npm run build:crypto && npm run build:sso && npm run build:billing && npm run build:admin && npm run build:backup && npm run build:websockets && npm run build:reporting && npm run build:ai

# The dev-only satellite-test-kit is imported by core's bin/test.integration.ts
# (and each satellite's), which core's tsconfig typechecks. Build it before the
Expand Down Expand Up @@ -201,6 +201,7 @@ jobs:
npm run test:coverage --workspace @adonisjs-lasagna/websockets
npm run test:coverage --workspace @adonisjs-lasagna/reporting
npm run test:coverage --workspace @adonisjs-lasagna/ai
npm run test:coverage --workspace @adonisjs-lasagna/crypto

# Satellite ABI compatibility (B5): the reference third-party satellite
# is built + tested against the freshly-built core above. Its typecheck
Expand Down Expand Up @@ -302,6 +303,14 @@ jobs:
path: coverage/.v8/ai-unit
if-no-files-found: warn

- name: Upload raw satellite unit coverage (V8) — crypto
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: c8-raw-crypto-unit
path: coverage/.v8/crypto-unit
if-no-files-found: warn

# Full report (unused exports / orphaned files / deps) stays informational.
- name: Knip (unused-code report)
run: npm run knip
Expand Down Expand Up @@ -330,7 +339,7 @@ jobs:
- name: Package contracts (publint + types resolution)
run: |
set -e
for pkg in core sso billing admin backup websockets reporting ai; do
for pkg in core sso billing admin backup websockets reporting ai crypto; do
echo "== @adonisjs-lasagna/$pkg =="
( cd "packages/$pkg" \
&& npx -y publint@0.3.21 \
Expand Down Expand Up @@ -475,11 +484,11 @@ jobs:
# with `default_transaction_read_only = on` so a write is denied by Postgres.
PLUGIN_RO_DB_USER: plugin_ro
PLUGIN_RO_DB_PASSWORD: plugin_ro
# This job HAS a real Postgres service, so the AI real-PG proofs are
# This job HAS a real Postgres service, so the crypto/AI real-PG proofs are
# mandatory here: they must EXECUTE, never self-skip. The satellite real-PG
# helpers read this flag and turn a would-be self-skip (PG unreachable, role
# lacks CREATEDB) into a hard failure — the fail-loud twin of RLS_DB_USER,
# so a broken/hardened runner can't ship those proofs green.
# so a broken/hardened runner can't ship the crypto crown-jewel proofs green.
REQUIRE_REAL_PG: '1'

steps:
Expand Down Expand Up @@ -605,7 +614,9 @@ jobs:
- name: Test (fault injection) — chaos tier
if: contains(github.event.head_commit.message, '[chaos]') || contains(github.event.pull_request.title, '[chaos]')
continue-on-error: true
run: npm run test:fault:run --workspace @adonisjs-lasagna/saas-tenancy
run: |
npm run test:fault:run --workspace @adonisjs-lasagna/saas-tenancy
npm run test:fault:run --workspace @adonisjs-lasagna/crypto

# Satellite integration tiers boot through the shared satellite-test-kit,
# proving the harness end to end on a real satellite. The step above ran
Expand Down Expand Up @@ -653,6 +664,14 @@ jobs:
- name: Test (satellite integration) + coverage — ai
run: npm run test:integration:coverage --workspace @adonisjs-lasagna/ai

# crypto's integration tier: the wrapped-DEK store + shred + WORM ledger + the
# @encrypted/@searchable decorators against real Postgres via the kit, across
# every placement (schema-pg, a real second database for database-pg, and the
# shared rowscope table with its RLS stub). The database-pg + rowscope specs
# self-skip if the CI role cannot CREATEDB / set the RLS GUC.
- name: Test (satellite integration) + coverage — crypto
run: npm run test:integration:coverage --workspace @adonisjs-lasagna/crypto

# Consumer canary: boot the shared harness from a fresh satellite (the
# reference template) against core's fixture. A kit change that breaks a
# real consumer fails here on an isolated, fast signal instead of buried in
Expand Down Expand Up @@ -743,6 +762,18 @@ jobs:
path: coverage/.v8/ai-integration
if-no-files-found: warn

# crypto integration V8 (written by test:integration:coverage to
# coverage/.v8/crypto-integration in the "— crypto" step above). The
# coverage-report job merges this with crypto's unit V8 so
# check-satellite-coverage.mjs gates a real per-satellite MERGED number.
- name: Upload raw satellite integration coverage (V8) — crypto
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: c8-raw-crypto-integration
path: coverage/.v8/crypto-integration
if-no-files-found: warn

test-e2e-demo:
name: E2E (demo app)
runs-on: ubuntu-latest
Expand Down Expand Up @@ -1223,6 +1254,12 @@ jobs:
name: c8-raw-ai-integration
path: coverage/.v8/all

- name: Download raw satellite integration coverage (V8) — crypto
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: c8-raw-crypto-integration
path: coverage/.v8/all

# websockets integration V8 is produced by the test-e2e-websockets job (a
# different job than test-integration above), now run under c8. This is why
# test-e2e-websockets is in this job's needs:.
Expand Down Expand Up @@ -1278,6 +1315,12 @@ jobs:
name: c8-raw-ai-unit
path: coverage/.v8/all

- name: Download raw satellite unit coverage (V8) — crypto
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: c8-raw-crypto-unit
path: coverage/.v8/all

- name: Aggregate coverage report (unit + integration, remapped to src)
run: npm run coverage:report

Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,16 @@ jobs:
fi
}

# Core first so satellites resolve a published peer. Only the six
# Core first so satellites resolve a published peer. Only the
# publishable workspaces belong here: `admin` and `websockets` are
# `private: true` (npm refuses them with EPRIVATE, which under
# `set -euo pipefail` would abort this script mid-release), and the
# crypto satellite was removed from the repo. `check-publish-coverage.mjs`
# asserts every non-private package appears below.
# `set -euo pipefail` would abort this script mid-release).
# `check-publish-coverage.mjs` asserts every non-private package
# appears below.
publish_pkg "packages/core"
publish_pkg "packages/sso"
publish_pkg "packages/billing"
publish_pkg "packages/backup"
publish_pkg "packages/reporting"
publish_pkg "packages/ai"
publish_pkg "packages/crypto"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ and runs the full e2e suite against it.
| **Dependency resilience** | Per-dependency fail-open/fail-closed degradation policy via `ResilienceService`. Emits `DependencyDegraded` for alerting and returns a typed 503 (`DependencyUnavailableException`) when fail-closed. |
| **Lifecycle hooks + 30 typed events** | Declarative `before` / `after` hooks wired into commands and jobs. 20 core (tenant / quota / maintenance / resilience / metrics / data-change / guard-audit lifecycle) + 10 billing. |
| **Contextual logging** | `tenantId` rides along through HTTP and queue jobs via `AsyncLocalStorage`. |
| **`tenant:doctor`** | Ten built-in checks (plus `backup_recency` and `backup_encryption` when the backup satellite is installed), `--fix` for auto-recovery, `--json` for CI, `--watch` for a live TUI. |
| **`tenant:doctor`** | Thirteen built-in checks (plus `backup_recency` and `backup_encryption` when the backup satellite is installed), `--fix` for auto-recovery (heals unmigrated/behind/failed tenants), `--json` for CI, `--watch` for a live TUI. |
| **Plans and quotas** | Declarative plans, rolling counters, snapshot usage, an `enforceQuota()` middleware that returns 429 and emits `TenantQuotaExceeded`. |
| **Scheduled backups + retention** | Tier-based intervals and `keepLast`, S3 mirror with purge awareness, idempotent cron command. |
| **Health probes + Prometheus** | `/livez`, `/readyz`, `/healthz`, `/metrics`. No `prom-client` peer dep. |
Expand Down
56 changes: 56 additions & 0 deletions apps/rental/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ─── App ──────────────────────────────────────────────────────────
NODE_ENV=development
PORT=3333
HOST=127.0.0.1
LOG_LEVEL=info
APP_KEY=karimoto-dev-app-key-please-change-32ch

# ─── Multitenancy ────────────────────────────────────────────────
# Companies are addressed as <slug>.localhost (stored as custom_domain) and
# resolved via domain-or-subdomain, with x-tenant-id (UUID) as the API fallback.
TENANT_HEADER_KEY=x-tenant-id
APP_DOMAIN=localhost
IMPERSONATION_SECRET=karimoto-dev-impersonation-secret-change-me-0123456789

# Webhook-delivery tests post to an in-process listener on loopback, so the SSRF
# guard must exempt loopback for the suite. Keep OFF in any real deployment.
WEBHOOKS_ALLOW_LOOPBACK_TARGETS=true

# ─── PostgreSQL (matches docker-compose.yml) ──────────────────────
DB_HOST=127.0.0.1
DB_PORT=55433
DB_USER=karimoto
DB_PASSWORD=karimoto
DB_DATABASE=karimoto

# ─── Redis (matches docker-compose.yml) ───────────────────────────
REDIS_HOST=127.0.0.1
REDIS_PORT=56380

QUEUE_REDIS_HOST=127.0.0.1
QUEUE_REDIS_PORT=56380
QUEUE_REDIS_DB=1

CACHE_REDIS_HOST=127.0.0.1
CACHE_REDIS_PORT=56380
CACHE_REDIS_DB=2

# ─── Seed a demo owner into every company schema at migrate time ──
DEMO_SEED_TENANT_USERS=true

# ─── Backups (optional) ──────────────────────────────────────────
BACKUP_STORAGE_PATH=./storage/backups

# ─── Billing (Stripe) — leave unset to run the offline mock ──────
# BILLING_DRIVER=stripe
# STRIPE_API_KEY=sk_test_...
# STRIPE_WEBHOOK_SECRET=whsec_...

# ─── AI (Anthropic) — leave unset to run the offline mock ────────
# ANTHROPIC_API_KEY=sk-ant-...

# ─── Mail (MailCatcher in dev, real SMTP in prod) ────────────────
MAILCATCHER_HOST=127.0.0.1
MAILCATCHER_PORT=1025
MAIL_FROM_ADDRESS=noreply@karimoto.test
MAIL_FROM_NAME=Karimoto
7 changes: 7 additions & 0 deletions apps/rental/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
build
coverage
.env
storage
tmp
*.log
87 changes: 87 additions & 0 deletions apps/rental/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Karimoto

A real car-rental SaaS built on `@adonisjs-lasagna/*`, exercising the whole
platform: two auth realms, schema-per-tenant isolation, the nine satellites
(admin, billing, ai, crypto, sso, backup, websockets, reporting) plus the core
feature set, a `telematics` plugin, and two Inertia + React consoles (the
platform operator and the rental company).

- **Operator** lives on the apex host `localhost:3333`.
- **Companies** live on a vanity host `<slug>.localhost:3333` (e.g.
`acme.localhost:3333`), stored as the tenant's `custom_domain`.

## Runtime processes

A real deployment runs three processes. In dev:

- `npm run dev` — the HTTP server (Vite is auto-started, no `--hmr` needed).
- `npm run dev:worker` — the queue worker (`queue:work`). **Required** for
tenant provisioning: company creation dispatches an `InstallTenant` job, and
the schema only exists once the worker has run it.

Infrastructure (Postgres `pgvector/pgvector:pg16` + Redis + MailCatcher) comes
up with `npm run infra:up`. Ports are 55433 / 56380 / 1025+1080, distinct from
the core demo so both run side by side.

## Setup from a clean database

Provisioning is asynchronous, so setup is two passes with the worker running in
between. From `apps/rental`:

```bash
npm run infra:up # Postgres + Redis + MailCatcher

# 1. Control plane: operator account, central car catalog, and the two demo
# companies (each dispatches InstallTenant).
npm run setup # backoffice:setup + central migrate + rental:seed

# 2. Materialise the schemas: start the worker (leave it running) so it drains
# the InstallTenant jobs. The AI provider's after('provision') hook installs
# pgvector into the `extensions` schema as each company is provisioned.
npm run dev:worker # in a second terminal; wait for the jobs to drain

# 3. Data plane: migrate each tenant schema, then fill it with demo data.
npm run setup:demo # tenant:vector:provision + migration:tenant:run + rental:seed:demo
```

`setup:demo` runs `tenant:vector:provision` first as a belt-and-suspenders step:
it is idempotent, and it guarantees the `vector` extension exists on a
pre-existing database (or one whose schemas were provisioned before the AI
provider's hook was in place) before the `ai_embeddings vector(N)` migration runs.

Then start the server:

```bash
npm run dev # http://localhost:3333
```

### Logins (dev only, refused in production)

| Realm | Host | Email | Password |
|---|---|---|---|
| Operator | `localhost:3333` | `operator@karimoto.test` | `operator-demo-password` |
| Company staff | `acme.localhost:3333` | `owner@karimoto.test` | `owner-demo-password` |
| Company staff | `sahara-cars.localhost:3333` | `owner@karimoto.test` | `owner-demo-password` |

## The two seed commands

- **`rental:seed`** (control plane) — the operator account, the shared central
car catalog, and the demo company rows (dispatching provisioning). Idempotent.
- **`rental:seed:demo`** (data plane) — fills each already-migrated company with
branches, a rate card, a fleet drawn from the catalog, renters with encrypted
PII, bookings across the lifecycle (invoices + payments for completed ones),
and a small RAG corpus of policy docs whose bodies are embedded into the tenant
vector store. Idempotent; safe to re-run to top up missing rows. Sizing follows
the company plan (`fleet`/`enterprise` get a full fleet, `starter` a smaller one
that stays under its `vehiclesPerTenant` quota).

## The fleet assistant (RAG)

The assistant streams over SSE at `POST /ai/chat`. Retrieval is opt-in
(`retrieve: true`): it embeds the query and searches the tenant's `ai_embeddings`
store, which `rental:seed:demo` populates from the policy docs. Offline (no
`ANTHROPIC_API_KEY`) the chat and embeddings run on the in-process mocks, so
retrieval returns real matches but the ranking is a deterministic hash, not
semantic relevance. Set `ANTHROPIC_API_KEY` (and a real embedding backend) in
`.env` and the same path uses the real model with no code change — re-run
`rental:seed:demo` so the corpus is re-embedded into the real vector space.
29 changes: 29 additions & 0 deletions apps/rental/ace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* AdonisJS ace entrypoint. Boots the Ignitor in a console environment so
* commands can be discovered and executed via `npm run ace -- <command>`.
*/
import 'reflect-metadata'
import { Ignitor, prettyPrintError } from '@adonisjs/core'

const APP_ROOT = new URL('./', import.meta.url)
const IMPORTER = (filePath: string) => {
if (filePath.startsWith('./') || filePath.startsWith('../')) {
return import(new URL(filePath, APP_ROOT).href)
}
return import(filePath)
}

new Ignitor(APP_ROOT, { importer: IMPORTER })
.tap((app) => {
app.booting(async () => {
await import('#start/env')
})
app.listen('SIGTERM', () => app.terminate())
app.listenIf(app.managedByPm2, 'SIGINT', () => app.terminate())
})
.ace()
.handle(process.argv.splice(2))
.catch((error) => {
process.exitCode = 1
prettyPrintError(error)
})
Loading
Loading