Skip to content

feat(mem): auto-curadoria do cérebro via assinatura Claude do usuário#154

Merged
BrunooMoniz merged 8 commits into
mainfrom
feat/mem-curador-assinatura
Jun 19, 2026
Merged

feat(mem): auto-curadoria do cérebro via assinatura Claude do usuário#154
BrunooMoniz merged 8 commits into
mainfrom
feat/mem-curador-assinatura

Conversation

@BrunooMoniz

Copy link
Copy Markdown
Owner

O quê

O cérebro se mantém sozinho: para cada conta que conecta a própria assinatura Claude, um agente server-side (cron) usa a conexão dela para extrair fatos das memórias de conversa → vira sinal na escada de confiança do perfil curado. Custo na assinatura da pessoa, não numa chave central. Porta o mecanismo OAuth-de-assinatura do Odysseus. Spec (revisado adversarialmente): docs/superpowers/specs/2026-06-19-curadoria-assinatura-claude-design.md.

Componentes

  • Provider (claude-subscription.ts puro + claude-subscription-store.ts): parse da credencial colada (3 formatos + heurística ms/s), headers OAuth (Bearer + oauth-2025-04-20, nunca x-api-key), spoof de Claude Code (system blocks), refresh com lock cross-process (pg_advisory_xact_lock), callSubscription(accountId,...) (accountId explícito, nunca getAccountId; effort gated por modelo; token nunca logado). Vault account_secrets kind claude_oauth.
  • Rotas /portal/claude/{connect,disconnect,status}: account de res.locals (nunca input), teto de 16 KB, token nunca exposto.
  • Extrator (memory-extractor.ts + tick gated MEMORY_EXTRACT_ENABLED): loop por conta com assinatura, requestContext.run por conta (isolado), só LÊ brain_chunks (fora do eval gate), guarda de segredo (looksLikeSecret/stripSecrets), memory_audit trigger llm com evidence_ref = ponteiro, cursor por conta.
  • UI (Next.js web/app/(app)/perfil/): card "Conexão Claude" (connect via paste do claude setup-token, status com expiry, disconnect).

Segurança

  • Per-account opt-in (conecta → entra no ciclo). Isolamento por conta (credencial e fatos sempre account_id-scoped). Token nunca logado/exposto. Fora do eval gate F8 (não muta brain_chunks/search.ts).
  • 1417 testes engine + 249 web verdes; build limpo; NEXT_PUBLIC_BASE_PATH=/app bun run build OK.

Deploy (após merge)

Engine (rotina) + build do Next na VPS. Enable: MEMORY_EXTRACT_ENABLED=true (kill-switch global; cada conta entra ao conectar a assinatura).

🤖 Generated with Claude Code

BrunooMoniz and others added 8 commits June 19, 2026 10:54
… (porta mecanismo do Odysseus)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… lock cross-process, isolamento por conta, PII em claro, expiry ms/s + expires_in, bare-token TTL)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fresh)

Porta o mecanismo do Odysseus para o engine (D1 do spec
2026-06-19-curadoria-assinatura-claude): módulo PURO src/claude-subscription.ts,
sem DB e sem rede real, usando `fetch` nativo (não o SDK Anthropic, que usa
x-api-key). Constantes fixas e centralizadas (CLIENT_ID, TOKEN_URL/MESSAGES_URL,
anthropic-version, oauth-beta, TTL default, spoof de Claude Code).

- parsePastedCredential: 3 formatos (bare sk-ant-oat01, JSON claudeAiOauth, flat),
  heurística epoch ms-vs-s (>1e12), TTL default sem expiry, lixo -> erro claro.
- oauthHeaders: Authorization Bearer + anthropic-beta oauth-2025-04-20, NUNCA
  x-api-key.
- buildSubscriptionSystemBlocks: identidade + guidance SEMPRE antes do system do
  usuário (sem isso, 429 da Anthropic em Opus/Sonnet).
- needsRefresh: skew 300s; sem refresh_token (bare token) nunca tenta.
- supportsEffort: Opus >=4.5, Sonnet >=4.6, Fable/Mythos (boundary octopus).
- refreshSubscription: POST grant_type=refresh_token + client_id; novo expiry de
  expires_in (delta s); preserva refresh_token antigo quando a resposta nao traz.

AC 1-4 verdes (29 testes). Token nunca logado.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + callSubscription)

D2/D3 do spec 2026-06-19-curadoria-assinatura-claude:
src/claude-subscription-store.ts — persiste a credencial no vault existente
(account_secrets kind claude_oauth, JSON {access_token,refresh_token,
expires_at ISO,auth_mode} criptografado) e expõe a chamada central.

- loadCredential/saveCredential/deleteCredential via set/get/deleteAccountSecret.
- Erros tipados: SubscriptionNotConnected, SubscriptionReauthRequired,
  SubscriptionRateLimited.
- resolveAccessToken(accountId, deps): accountId EXPLÍCITO (nunca getAccountId,
  que cairia no fallback 'bruno'). Refresh sob lock CROSS-PROCESS
  (pg_advisory_xact_lock(hashtext('claude_refresh:'||accountId)) numa transação),
  atrás do seam injetável deps.withAccountLock pra testar a serialização sem
  Postgres real. Re-lê + re-checa sob o lock, refresca preservando o refresh_token
  antigo quando ausente, grava. Sem credencial -> NotConnected; 401 -> Reauth.
- callSubscription: resolve -> headers OAuth (Bearer, nunca x-api-key) + payload
  com spoof de Claude Code + max_tokens; effort/thinking só em supportsEffort e aí
  OMITE temperature; URL FIXA /v1/messages; 401->Reauth, 429->RateLimited; token
  NUNCA logado (catch loga só status/mensagem).

AC 4b/5/9 verdes. Build (tsc) limpo; suite completa 1391/1391, zero regressão.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r conta

Adiciona POST /portal/claude/connect, POST /portal/claude/disconnect e
GET /portal/claude/status em src/portal/routes.ts (spec D7/AC9). Account
sempre de res.locals.accountId (sessão, nunca body/query). connect valida
string não-vazia + teto de 16 KB antes de parsear, chama parsePastedCredential
→ saveParsedCredential e devolve só {ok, expires_at}; parse inválido → 400
invalid_credential. status devolve {connected, expires_at?, needs_reauth?} sem
nunca expor access/refresh token. disconnect chama deleteCredential da conta da
sessão. Testes de rota com pool em memória (sem Postgres).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inatura/cursor

Plumbing account-scoped para o extrator de memória (spec
2026-06-19-curadoria-assinatura-claude-design, D4/D6):

- storage.ts: listConversationMemoriesSince(accountId, since, limit) — SELECT
  read-only de brain_chunks source_type='conversation' NEWER que o cursor,
  oldest-first, account_id como único filtro de tenant. NUNCA muta brain_chunks
  nem o índice de busca (fica fora do eval gate).
- claude-subscription-store.ts: listAccountsWithSubscription() (DISTINCT
  account_id em account_secrets kind='claude_oauth' — quem tem assinatura, NÃO
  quem tem fatos) + load/saveMemoryExtractCursor() (cursor por conta em
  account_secrets kind='memory_extract_cursor').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… isolado)

Agente de auto-curadoria powered pela assinatura da própria pessoa (spec
2026-06-19-curadoria-assinatura-claude-design, D4/D5/D6; AC 6/7/8):

- memory-extractor.ts: runMemoryExtraction(deps) processa CADA conta com
  assinatura DENTRO de requestContext.run({authType:'bearer',scopes:[],accountId,
  isOperator:false}) — accountId SEMPRE explícito, nunca getAccountId(). Lê
  memórias de conversa desde o cursor (orçamento 50/conta), chama callSubscription
  (opus-4-8, maxTokens 1024), parseia array JSON (tolera ```json). Cada fato:
  looksLikeSecret descarta, stripSecrets redige → upsertProfileFact status=signal
  source=llm + insertMemoryAudit trigger=llm com evidence_ref=PONTEIRO (source_id,
  nunca conteúdo/token). Cursor avança; erros por conta (parse/401/429) pulam a
  conta sem derrubar as outras. A conta A só lê a própria credencial e só escreve
  fatos com o próprio account_id.
- memory-extraction-tick.ts: tickMemoryExtraction(label, run?) atrás do gate
  MEMORY_EXTRACT_ENABLED (kill-switch DENTRO do tick), lazy-import das deps reais,
  recordRun(source='memory-extraction'). Espelha memory-curation-tick.
- index-classifier.ts: cron noturno (default 04:45, após a curadoria 04:15).
- Testes (deps injetadas, sem rede/DB): sinais, audit com ponteiro, isolamento
  A/B (AC7), guarda de segredo, cursor, gating off (AC8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adiciona ao Perfil logado (Next.js) um card onde a pessoa conecta a
assinatura Claude dela colando a credencial do `claude setup-token`, vê
o status (conectado / desconectado / reconectar) com o expiry, e
desconecta com confirmação inline. Consome o contrato já no backend:
GET /portal/claude/status, POST /portal/claude/{connect,disconnect}.

- web/hooks/claude.ts: useClaudeStatus + connect/disconnect (invalidam
  no sucesso), no padrão de hooks/perfil.ts.
- web/components/profile/ClaudeConnectionCard.tsx: estados
  loading/erro/desconectado/conectado/needs_reauth; trata 400
  invalid_credential com mensagem clara; nunca exibe a credencial depois
  de enviada (limpa no sucesso).
- contracts: ClaudeStatusResponse / ClaudeConnect{Request,Response}.
- plugado no Perfil após o BudgetMeter.
- 5 testes de componente (vitest); typecheck + build + build basePath /app verdes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@BrunooMoniz BrunooMoniz merged commit 19aa788 into main Jun 19, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant