Commit graph

12 commits

Author SHA1 Message Date
Till JS
ef96948ea0 feat(comic): Mc4 — MCP + AI-Catalog für Character-System
Persona-Runner / Claude Desktop / Web-App-Mission-Runner können jetzt
Comic-Characters bauen, iterieren und pinnen — same Auto/Propose-
Pattern wie die Story-Tools.

MCP (packages/mana-tool-registry/src/modules/comic.ts):
- comic.listCharacters (read/auto): Pull, decrypt, filter (style?,
  favoriteOnly?), liefert {id, name, style, addPrompt, source-Refs,
  variantMediaIds, pinnedVariantId, variantCount, tags, isFavorite}.
- comic.createCharacter (write/propose): legt nur die Row an —
  trennt Anlegen von Generierung damit der Agent reviewen kann
  bevor Credits fließen. Liefert characterId zurück.
- comic.generateVariant (write/propose, kostet Credits): pullt
  Character-Row, dekodiert, ruft /picture/generate-with-reference
  mit n=count (default 4) + Stil-Prefix + Identity-Anchor-Prompt,
  schreibt N picture.images mit comicCharacterId-Back-Ref, pusht
  field-level Update auf variantMediaIds + pinnedVariantId
  (auto-pin auf erste neue Variant wenn vorher null).
- comic.pinVariant (write/propose): Set-Equality-Check (variantMediaId
  muss in variantMediaIds sein), field-level Update auf
  pinnedVariantId. Snapshot-Pattern: bestehende Stories bleiben
  unverändert, nur neue Stories nutzen den neuen Pin.

AI_TOOL_CATALOG (packages/shared-ai/src/tools/schemas.ts):
- list_comic_characters (auto)
- create_comic_character (propose) — auto-resolvt face/body-refs aus
  meImages-primaries, Agent muss keine mediaIds kennen
- generate_character_variant (propose, count 1-4)
- pin_character_variant (propose)

Web-App-Executors (apps/mana/apps/web/src/lib/modules/comic/tools.ts):
- 4 ModuleTool-Einträge, die an comicCharactersStore +
  runCharacterGenerate delegieren — gleicher Code-Pfad wie die UI,
  also keine Divergenz zwischen Klick und Agent-Call.

Comic-Autor-Template (packages/shared-ai/src/agents/templates/
comic-author.ts):
- Policy bi-lingual erweitert: snake_case + dot-case Namen für
  alle 4 neuen Character-Tools.
- System-Prompt Schritt 3 ergänzt: "Wenn der User noch keinen
  passenden Comic-Character hat → list_comic_characters →
  create_comic_character → generate_character_variant → pin.
  Das ist EINMALIG — der gepinnte Character bleibt für viele
  Stories der stabile Identity-Anchor."
- Tool-Liste am Ende vom System-Prompt um den Character-Pfad
  ergänzt.

apps/mana/CLAUDE.md Tool-Coverage-Zeile für comic erweitert:
+ create_comic_character / generate_character_variant /
+ pin_character_variant (propose)
+ list_comic_characters (auto)

Tool-Count: comic 3→7. Module 23 unverändert.

107 shared-ai-Tests weiter grün. check für comic-Files clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 19:27:15 +02:00
Till JS
a1f2dccb68 feat(tool-registry): augur module — 5 server-side tools
Mirrors apps/mana/apps/web/src/lib/modules/augur/tools.ts for the
shared mana-tool-registry. Lets persona-runner / mana-mcp / mana-ai
invoke augur over stdio and HTTP without going through the web app.

Tools:
  - augur.captureSign     (write) — log a new omen / fortune / hunch
  - augur.resolveSign     (write) — fulfilled / partly / not-fulfilled
  - augur.listOpenSigns   (read)  — what's still waiting on resolution
  - augur.consultOracle   (read)  — Living Oracle reflection from history
  - augur.yearRecap       (read)  — structured year-in-review snapshot

The pure-math engines (fingerprint, matchScore, makeReflection,
yearRecap aggregation) are mirrored from the web-app lib/. Both
sides have unit tests covering the same contract — keep them in
sync. A future shared package would dedupe.

Encrypted fields declared on each spec (audit:encrypted-tools went
from 15 to 20). ModuleId extended in types.ts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:32:04 +02:00
Till JS
364522db87 feat(comic): image-model picker — OpenAI + Nano Banana wählbar
Comic nutzte bisher 'openai/gpt-image-2' hartcodiert auf drei Ebenen
(generate-panel.ts, comic.generatePanel MCP-Tool, generate_comic_panel
AI-Tool). Wardrobe hat seit dem Nano-Banana-Commit einen
TryOnModelPicker mit drei Optionen — Comic spiegelt das jetzt 1:1.

Wählbar in allen drei Editoren (PanelEditor, BatchPanelEditor,
StoryboardSuggester):
- openai/gpt-image-2 (Default) — OpenAI GPT-image Standard
- google/gemini-3-pro-image-preview — Nano Banana Pro, hohe
  Konsistenz, teurer
- google/gemini-3.1-flash-image-preview — Nano Banana 2, neuestes,
  schnell, günstig

Implementierung:
- api/generate-panel.ts: PanelModel Union + DEFAULT_PANEL_MODEL +
  model? Param auf RunPanelGenerateParams + im HTTP-Body
  weitergereicht (vorher hart 'openai/gpt-image-2').
- components/PanelModelPicker.svelte: neue Komponente, Stil/Markup
  identisch zu TryOnModelPicker für Muskel-Memory über beide Flows.
- components/PanelEditor.svelte: `let model = $state(DEFAULT_PANEL_MODEL)`
  + Picker oberhalb der Qualität-/Format-Leiste + model im
  runPanelGenerate-Call.
- components/BatchPanelEditor.svelte: gleiche Änderung — ein Model
  pro Batch (nicht pro Row) damit der Batch konsistent rendert.
- components/StoryboardSuggester.svelte: gleiches Pattern; der
  Picker landet zwischen "Panel manuell"-Button und dem
  Qualität/Format-Block.
- packages/mana-tool-registry/src/modules/comic.ts: generatePanel
  Input-Schema bekommt model mit zod.enum() + default; im Body
  wird input.model durchgereicht.
- packages/shared-ai/src/tools/schemas.ts: generate_comic_panel
  bekommt Parameter 'model' optional mit gleicher Enum-Liste.
- apps/mana/apps/web/src/lib/modules/comic/tools.ts: isValidModel
  Guard + Parameter-Validierung; model an runPanelGenerate.

Keine Story-Level-Persistierung — model bleibt lokaler State pro
Editor-Mount. Eine model-Spalte auf comicStories würde Migration
brauchen und die Wahl ist eh ad-hoc pro Panel/Batch.

Plan-Doc (§2.1) dokumentiert die Entscheidung + die drei Optionen.

107 shared-ai tests weiter grün. check + validate:all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:19:40 +02:00
Till JS
05b2209232 polish(wardrobe): garment-detail cosmetic pass + slug-cleanup on upload
Four small UI tweaks that came out of reviewing the garment-detail
screenshot against the workbench chrome:

1. Duplicate "Kleiderschrank" label — the ModuleShell header above
   DetailGarmentView already renders a back-arrow and the app title.
   The inner `<nav>` with a second arrow + text was rendering it all
   a second time. Drop the inner breadcrumb; ArrowLeft import along
   with it.

2. Raw SKU-slug as default garment name — the old
   `stripExt(file.name)` produced labels like
   `17390-gestreiftes-herren-t-shirt-aus-baumwolle-17390-2-w`. New
   `prettifyUploadName` helper:
   - drops the extension
   - replaces `-`/`_` with spaces
   - strips pure-digit tokens of length ≥ 4 (SKU shape) but keeps
     short alphanumerics like `4xl` / `w38`
   - title-cases each remaining word, rebuilding hyphens
     (`t-shirt` → `T-Shirt`, `v-neck` → `V-Neck`)
   - clamps to 80 chars on a word boundary
   GridView's ingestFiles now passes the prettified name into the
   createGarment write. User still edits on the detail page for
   anything that needs nuance.

3. Two-line CTA with Credits subtitle. The button used to read
   `Anprobieren · 10 Credits` on one line; on a narrow workbench
   card the mittelpunkt between label and cost was visually thin
   and read like a strikethrough. Split into a main label + small
   opacity-75 subtitle so the credit figure is clearly secondary
   info, not a decorated part of the CTA text. Applied to both
   GarmentTryOnButton and TryOnButton.

4. Redundant microcopy under section headers — "Einzelstück auf dir
   gerendert" under ANPROBEN and "Komposition öffnen" under IN
   OUTFITS repeated what the section title and the clickable cards
   already signalled. Remove both.

No behaviour changes, no schema, no API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:24:07 +02:00
Till JS
87b567eec9 i18n: fix IT/FR/ES parity gaps in dashboard + memoro
- dashboard: +5 Einträge pro Sprache für die beiden neuen Widgets
  activity_feed + articles_unread.
- memoro: +1 Eintrag pro Sprache für memo.load_more.

Damit sind dashboard (111) und memoro auf gleichem Stand wie DE/EN.
Verbleibende Drift (app_slider-Legacy-Keys in memoro IT/FR/ES,
common/auth-Legacy in calendar/times) ist strukturell und bleibt
einem Folge-Cleanup vorbehalten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:19:59 +02:00
Till JS
7e3f53f8a5 feat(tool-registry): wardrobe.* MCP tools — listGarments/listOutfits/createOutfit/tryOn (M5)
M5 of docs/plans/wardrobe-module.md — exposes the Wardrobe feature
through the shared tool-registry so MCP clients (Claude Desktop)
and the mana-ai mission runner can browse, compose, and try on
outfits alongside the built-in UI. Follows the pattern M5 of the
me-images plan established in packages/mana-tool-registry/src/
modules/me.ts — encrypted reads via mana-sync pull + client-side
filter on `row.spaceId === ctx.spaceId`, writes via pushInsert
with encryptRecordFields, HTTP proxy for the try-on endpoint.

Four tools in packages/mana-tool-registry/src/modules/wardrobe.ts:

- wardrobe.listGarments(category?, tags?, limit?) — read. Pulls
  wardrobeGarments from mana-sync, filters to the active space,
  decrypts name/brand/color/size/material/tags/notes, applies
  optional category + intersection-tag filters, caps at 200 rows
  (50 default). Archived + soft-deleted items excluded.

- wardrobe.listOutfits(occasion?, favoriteOnly?, limit?) — read.
  Same shape, filters by occasion (closed enum, plaintext —
  unencrypted filter) and favorite. garmentIds arrive plaintext
  so the agent can immediately resolve them via listGarments when
  it needs more than ids.

- wardrobe.createOutfit({ name, garmentIds, occasion?, tags?,
  description? }) — write. Encrypts name/description/tags, pushes
  an insert tagged with ctx.spaceId. No cross-space validation of
  the garmentIds — the calling agent is expected to have called
  listGarments first; dangling refs surface visually in the UI
  rather than as a hard server error.

- wardrobe.tryOn({ outfitId, prompt?, accessoryOnly?, quality? }) —
  write (consumes credits). Biggest tool of the set: pulls the
  outfit, its garments, and the caller's meImages in three
  separate mana-sync pulls, resolves the primary face-ref +
  body-ref, auto-detects accessoryOnly from garment categories
  (FACE_ONLY_CATEGORIES: accessory/glasses/jewelry/hat), composes
  refs respecting the 8-slot server cap, composes a default DE
  prompt from the outfit name + occasion, and proxies to
  /api/v1/picture/generate-with-reference with the user's JWT.
  Returns the resulting image's URL + mediaId + prompt + model.

  Deliberately does NOT persist a picture.images row or update
  outfit.lastTryOn from the tool — those live on the client's
  imagesStore / wardrobeOutfitsStore and doing them server-side
  would race with a user who's also looking at the outfit page.
  Agents use tryOn as a preview/inspection primitive; the user
  commits from the UI.

Types: 'wardrobe' added to the ModuleId union. registerWardrobeTools
wired into registerAllModules — mana-mcp's createMcpServerForUser
iterates the registry and exposes any user-space tool automatically.

Credit model: quality defaults to 'medium' (10 credits per render),
same tarif as text-to-image generation. The agent pays for the
generation out of the calling user's credit balance via the
standard validateCredits/consumeCredits chain on the server endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:59:31 +02:00
Till JS
66b7e08df2 feat(shared-ai): runSubAgent() primitive — Claude-Code I2A pattern (M3.1)
New packages/shared-ai/src/planner/sub-agent.ts implementing the
"one level deep, fresh messages, restricted tools, single-string
return" sub-agent contract from Claude Code's KN5/I2A launcher.

Four invariants enforced at the primitive level:

  1. FRESH messages[] — parent's history never leaks in. The sub-agent
     only sees its own system prompt + the task description. Hundreds
     of scanned files stay inside the sub-agent.
  2. RESTRICTED tool-whitelist — parent's full catalog is filtered
     per SubAgentType ('research' = auto-policy only, 'general' =
     everything, 'plan' = auto-policy + 3-round cap). Custom filter
     overrides the type default.
  3. SINGLE RETURN VALUE — sub-agent returns summary:string for
     the parent to render as task-tool-result. Individual tool calls
     stay in rawResult for debug capture but never cross the boundary.
  4. ONE LEVEL DEEP — MAX_SUB_AGENT_DEPTH = 1. parentDepth >= 1 throws
     SubAgentRecursionError; the consumer task-tool handler will
     also check, this is defense-in-depth.

Model is required (no default) — routing to a cheaper tier like the
compactor does is an explicit decision, not a sneaky default.

Belt-and-suspenders wrapper on onToolCall rejects any tool call
whose name isn't in the whitelist, even if the LLM fabricates one.

14 new tests covering recursion guard, tool filtering per type,
custom filter, whitelist rejection, fresh-messages isolation, usage
roll-up, default summary on max-rounds, type-specific system prompt,
system-prompt override, and end-to-end tool-call -> result -> summary.

93 shared-ai tests green total (was 79).

M3.2 (task tool in registry) and M3.3 (consumer wiring) follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:59:05 +02:00
Till JS
4966ca69f0 feat(tool-registry): add mood module (log/today/recent)
Third encrypted module in @mana/tool-registry, brings the registry to
16 tools across 7 modules. Lets the Anna / Sofia / Maya personas
(whose moduleMix puts mood at 20–30 %) actually exercise their
daily-tracking routine when the runner ticks.

Three tools, all encrypted per the web-app registry
(moodEntries: entry<LocalMoodEntry>(['withWhom', 'notes'])):

- mood.log
    Write a mood entry. `level` 1–10, `emotion` + `secondaryEmotions`
    from the taxonomy copied verbatim from apps/mana/.../modules/mood/
    types.ts (keep in sync if new emotions/activities get added). date
    + time default to server-clock now; personas logging
    retrospectively pass them explicitly.

- mood.today
    Return every entry for today (or `{ date }`) sorted by time.
    Multiple entries per day are normal — the web app timelines them.

- mood.recent
    Last N days (default 7), newest first. Useful for
    self-reflection turns like "how has your week been?".

Scope decisions

Calendar was on the shortlist but dropped: `events` writes couple to
`timeBlocks` (a separate table/appId), so one tool call becomes two
sync pushes with a shared transaction concern — worth a careful
session, not a drive-by. Goals dropped because `companionGoals` is
owned by the Companion Brain, not a regular module, and has no clear
mana-sync appId convention. Both candidates for a focused follow-up.

Verified

- `pnpm run validate:all` green (crypto registry 202/202, encrypted-
  tools audit 9/9 including the 3 new mood tools)
- type-check across tool-registry + mcp + runner green
- registerAllModules → 16 tools, 7 modules:
    habits: create/list/update/archive
    journal: add 🔐
    me: listReferenceImages 🔐 / generateWithReference
    mood: log 🔐 / today 🔐 / recent 🔐
    notes: create 🔐 / search 🔐
    spaces: list
    todo: create 🔐 / list 🔐 / complete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:39:00 +02:00
Till JS
cb9a9bb42e refactor(profile,tool-registry): flip meImages from user-scoped to space-scoped (v40)
Flips `meImages` out of USER_LEVEL_TABLES so it lives under the same
tenancy model as every other data table (tags, scenes, tasks, …).
Precursor to the Wardrobe module, which is space-scoped across all
six space types — leaving meImages user-global would leave an
inconsistency where the Wardrobe catalog is per-space but its
reference input is cross-space, plus a latent privacy leak in shared
spaces (agents in a brand-space would see the owner's entire pool).

Plan: docs/plans/me-images-space-scope-migration.md.

Key decisions:

- Strict scope, no cross-space fallback. Switching into a brand-space
  with no uploaded face shows an empty state and links back to
  /profile/me-images; it does not quietly reach into the personal-
  space pool. Keeps the mental model clean.
- auth.users.image remains pinned to personal-space primary-avatar.
  Only a primary change inside personal space triggers the Better
  Auth sync; brand/club/family/team/practice primaries stay local.
- Single Dexie v40 upgrade: stamps `spaceId=_personal:<uid>`
  sentinel, `authorId=<uid>`, `visibility='space'` on every existing
  row and drops the legacy `userId` column. Dexie upgrades block app
  startup, so by the time the new code's scopedForModule reads run,
  every row is already space-stamped. reconcileSentinels() on the
  next active-space bootstrap rewrites `_personal:<uid>` to the real
  personal-space id, same path v28 used.
- Legacy-avatar migration (M2.5) now pins its row to
  `_personal:<uid>` explicitly — the legacy avatar is the user's
  global SSO identity and belongs in the personal space even if the
  migration happens to fire while the user is in a brand space.

Code changes:

- types.ts: LocalMeImage gains spaceId/authorId/visibility (all
  optional — stamped by hook). Public MeImage exposes spaceId for
  queries that want to branch on space type.
- database.ts: meImages out of USER_LEVEL_TABLES; new v40 upgrade
  block that stamps sentinels + drops userId in one pass.
- queries.ts: all four hooks (useAllMeImages, useMeImagesByKind,
  useReferenceImages, useImageByPrimary) read via scopedForModule.
  Scope-switch triggers automatic re-render via the existing
  scopedTable filter path.
- stores/me-images.svelte.ts: setPrimaryInTx uses scopedForModule so
  a setPrimary in Brand-space never clears Personal-space's holder.
  syncAvatarToAuth gates on activeSpace.type==='personal' so non-
  personal primary changes don't leak into Better Auth.
  createMeImage accepts optional spaceId override — the legacy-
  avatar migration uses it, regular uploads let the hook stamp the
  active space.
- migration/legacy-avatar.ts: explicitly passes
  spaceId=_personal:<uid> to pin the legacy row into personal space.
- MeImagesView.svelte: subtle badge in the intro card shows the
  active space ("Persönlich" for personal, space name otherwise) so
  users notice when the pool changes on space switch.
- packages/mana-tool-registry/src/modules/me.ts: me.listReferenceImages
  filters pulled rows by row.spaceId === ctx.spaceId. mana-sync
  returns all spaces the user belongs to; the tool only wants the
  active space's subset.

No schema/index change on meImages (non-indexed fields, pool size
small enough for in-memory scopedTable filter). If perf matters
later, adding [spaceId+kind] is a 5-minute follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:09:57 +02:00
Till JS
fc635f9830 feat(tool-registry): me.listReferenceImages + me.generateWithReference (M5)
Closes M5 of docs/plans/me-images-and-reference-generation.md —
exposes the meImages feature through the shared tool-registry so MCP
clients (Claude Desktop) and the mana-ai mission runner can drive it
alongside the built-in webapp UI.

Two tools in packages/mana-tool-registry/src/modules/me.ts:

- me.listReferenceImages(kind?) — scope: user-space, read. Pulls the
  user's meImages rows from mana-sync (app='profile'), filters to
  usage.aiReference=true and soft-live records, decrypts the `label`
  and `tags` fields with the caller's master key (same pattern as
  notes.search). Returns mediaIds + kind + primary-slot info so a
  persona can pick references intelligently. ZK users will see this
  fail at getMasterKey() — correct, because the label is truly
  unrecoverable server-side for them.

- me.generateWithReference({prompt, referenceMediaIds, quality,
  size, n}) — scope: user-space, write. Thin proxy over the M3
  endpoint POST /api/v1/picture/generate-with-reference in apps/api:
  forwards the JWT, lets apps/api re-verify ownership, and returns
  the generated images' mediaIds + URLs. Credits are consumed at
  the same 3/10/25 tarif as text-to-image, so a persona plan pass
  should gate this behind explicit budget rather than leaving it on
  auto-policy.

Registered in modules/index.ts + adds 'me' to the ModuleId union in
types.ts. No other wiring needed — mana-mcp's createMcpServerForUser
iterates the registry and exposes any user-space tool, so both tools
become available to Claude Desktop immediately on next deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 14:43:56 +02:00
Till JS
e5d230e599 feat(agent-loop): M1 — policy gate + reminder channel + parallel reads
Three Claude-Code-inspired primitives for runPlannerLoop, derived from the
reverse-engineering reports in docs/reports/:

1. **Policy gate** (@mana/tool-registry) — evaluatePolicy() gates every tool
   dispatch: denies admin-scope, denies destructive tools not in the user's
   opt-in list, rate-limits per tool (30/60s default), flags prompt-injection
   markers in freetext without blocking. Wired into mana-mcp with a
   per-user rolling invocation log and POLICY_MODE env (off|log-only|enforce,
   default log-only). mana-ai uses detectInjectionMarker only — tool dispatch
   there is plan-only, so rate-limit/destructive checks don't apply yet.

2. **Reminder channel** (packages/shared-ai/src/planner/loop.ts) — new
   reminderChannel callback in PlannerLoopInput. Called once per round with
   LoopState snapshot (round, toolCallCount, usage, lastCall); returned
   strings wrap in <reminder> tags and inject as transient system messages
   into THIS LLM request only. Never pushed to messages[] — the Claude-Code
   <system-reminder> pattern that keeps the KV-cache prefix stable.

3. **Parallel reads** (loop.ts) — isParallelSafe predicate enables
   Promise.all dispatch when every tool_call in a round is parallel-safe,
   in batches of PARALLEL_TOOL_BATCH_SIZE=10. Any non-safe call downgrades
   the whole round to sequential. messages[] always appends in source
   order, never completion order, so the debug log stays linear.
   Default-off (undefined predicate) preserves pre-M1 behaviour.

Tests: 21 new in tool-registry (policy), 9 new in shared-ai (5 parallel,
4 reminder). All 74 green, type-check clean across 4 packages.

Design/plan: docs/plans/agent-loop-improvements-m1.md
Reports: docs/reports/claude-code-architecture.md,
         docs/reports/mana-agent-improvements-from-claude-code.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:56:40 +02:00
Till JS
16c8818338 feat(mcp): M1+M1.5 MCP gateway + tool-registry + shared-crypto
Foundation for autonomous Claude-driven testing. Plan:
docs/plans/mana-mcp-and-personas.md.

New packages
- @mana/tool-registry — schema-first ToolSpec<InputSchema, OutputSchema>
  with zod generics, scope ('user-space' | 'admin') and policyHint
  ('read' | 'write' | 'destructive'). sync-client helpers speak the
  mana-sync push/pull protocol directly so RLS and field-level LWW are
  preserved. MasterKeyClient fetches per-user MKs via the existing
  mana-auth GET /api/v1/me/encryption-vault/key endpoint (JWT-gated,
  ZK-aware, already audited) — no new service-key endpoint built.
  ZeroKnowledgeUserError surfaced as a typed throw.
- @mana/shared-crypto — AES-GCM-256 primitives extracted from the web
  app's $lib/data/crypto/aes.ts so the server-side tool handlers and the
  browser produce byte-for-byte identical wire format
  (enc:1:{b64(iv)}.{b64(ct)}). Web app aes.ts now re-exports from
  shared-crypto — 5 existing importers unchanged, svelte-check stays
  green.

New service
- services/mana-mcp (:3069, Bun/Hono) — MCP Streamable HTTP gateway.
  JWKS auth against mana-auth, per-user session isolation (session-id
  belongs to the user who opened it — cross-user access returns 403),
  admin-scoped tools filtered out before registration. MasterKeyClient
  cached per process with a 5-minute TTL.

11 tools registered
- habits.{create,list,update,archive}, spaces.list (plaintext, M1)
- todo.{create,list,complete}, notes.{create,search}, journal.add
  (encrypted — field lists match
  apps/mana/apps/web/src/lib/data/crypto/registry.ts verbatim)

Infra
- Port 3069 added to docs/PORT_SCHEMA.md
- services/mana-mcp/CLAUDE.md with architecture, auth model,
  tool-authoring recipe, local smoke-test steps
- Root CLAUDE.md services list updated

Type-check green across shared-crypto, mana-tool-registry, mana-mcp.
svelte-check on apps/mana/apps/web stays at 0 errors / 0 warnings.
Boot smoke verified: /health returns registry.loaded=true, unauthed
/mcp → 401, invalid-JWT /mcp → 401 with descriptive message.

Decisions locked in for later milestones (per plan D1–D10):
- Personas will be real mana-auth users (users.kind='persona'), no
  service-key bypass (D1, D2)
- Tool-registry is the SSOT; mana-ai and the legacy
  apps/api/src/mcp/server.ts get merged into it in M4 (three current
  parallel tool catalogs collapse to one)
- Persona-runner (:3070) will be a separate service using the Claude
  Agent SDK + MCP client (D5)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:18:35 +02:00