Local-first personal assistant runtime built as a deterministic L2 workflow: Python code owns routing, state, permissions, idempotency, and side effects; LLM calls are bounded provider activities used only when deterministic logic needs help.
The project is intentionally not a generic autonomous agent loop. It is a single assistant harness for personal productivity workflows with tenant-scoped memory, explicit approvals, local-first observability, Telegram integration, optional audio, and optional durable Postgres storage.
The repository currently includes:
- A Python 3.11 package with domain, application, adapter, contract, and infrastructure layers.
- FastAPI runtime endpoints for health, readiness, reminders, approvals, workflow state, traces, Telegram webhooks, and local admin views.
- Telegram webhook normalization, command routing, text replies, reminder approvals, due-reminder dispatch, and outbound Telegram notification adapters.
- Optional Telegram voice/audio transcription through an OpenAI-compatible speech-to-text adapter.
- Optional MiniMax text-to-speech for Telegram audio replies through
sendAudio. - Deterministic reminder and calendar workflows with P3/P5 approval gates, idempotency keys, event-store writes, outbox records, workflow states, and trace events.
- In-memory stores by default, plus optional Postgres persistence for approvals, events, outbox, workflow state, memory, local calendar, scheduler, and traces.
- A local-only read-only admin dashboard for tenant-scoped inspection.
- MiniMax and generic Anthropic-compatible LLM adapters behind
LLMProvider. - A deny-by-default outbound egress allowlist (ADR-004 layer A): exact
scheme + hostnameentries derived from the configured provider base URLs plusapi.telegram.org, an explicitEGRESS_ALLOWED_HOSTSoverride, fail-closed startup validation, and hostname-only startup audit records. - A hardened container profile (ADR-004 layer B): multi-stage
Dockerfilewith a non-root user, plusdeploy/compose.yamlwith a read-only root filesystem, dropped capabilities, andno-new-privileges. - An automated process kill/restart recovery exercise against PostgreSQL proving exactly-once delivery and operator-reconciled ambiguous outcomes.
- Deterministic tests for tenant isolation, idempotency, permissions, prompt-injection handling, HTTP boundaries, admin visibility, Telegram delivery, audio adapters, Postgres wiring, and architecture boundaries.
Current limits:
- Notification delivery records are still adapter-local rather than persisted in Postgres.
- No production deployment hardening beyond the local container profile, external calendar sync, OAuth token storage, semantic vector memory, active MCP runtime path, or active A2A runtime path.
Telegram webhook / local runtime request
-> Channel normalization
-> Trusted principal + tenant resolution
-> Conversation command service
-> Deterministic reminder/calendar workflow
-> Ports for LLM, transcription, TTS, calendar, scheduler, events,
outbox, approvals, memory, notifications, traces
-> In-memory or Postgres adapters
-> Optional workers for due reminders and notification dispatch
MVP autonomy is L2. Deterministic code owns the path and uses bounded LLM calls only for classification, extraction, or drafting where the route allows it. WhatsApp, A2A, MCP, and external integration contracts may exist, but they are not the active internal runtime for the MVP.
The executable contract is in agents/personal_assistant/contract.md. The core
security invariant is that tenant_id comes from the authenticated Principal,
never from Telegram text, tool arguments, LLM output, request JSON bodies, or
retrieved documents.
Guardrails scan both directions at every entry point. Inputs (reminder text,
runtime tasks, document content) are scanned for prompt injection, PII, and
content-policy signals; blocking findings fail the request, and document
content is treated as untrusted data, never as instructions. Outputs
(assistant replies, notification bodies, document summaries) are scanned
against the ratified content policy in docs/policy/content-policy.md —
credential material, exfiltration instructions, hidden-instruction leaks, and
destructive-action text are blocked before they reach the user. Every scan
emits a sanitized guardrail.checked trace event (action, categories, and
rule labels only — never excerpts or user content), and the admin surface
aggregates them into hit-rate metrics at GET /admin/guardrails/metrics
(see docs/runbook/admin-dashboard.md).
Python 3.11 or newer is required.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[test]'Install optional API and Postgres dependencies when needed:
python -m pip install -e '.[api,test]'
python -m pip install -e '.[api,test,postgres]'Configuration is loaded from the process environment and, by default, an
optional local .env file. Use .env.example as the template. Set
APP_ENV_FILE=disabled for hermetic tests. Keep Telegram tokens, webhook
secrets, MiniMax keys, speech provider keys, ADMIN_TOKEN, DATABASE_URL, OAuth
tokens, and every other credential out of git. For the public webhook boundary,
local secrets, and rotation procedure, follow
docs/runbook/hardened-local-deployment.md.
The main local gate does not require Telegram, MiniMax, Groq, Postgres, or other network services:
APP_ENV_FILE=disabled PYTHONPATH=src python3 -B -m pytest -q
PYTHONPATH=src python3 -B -m compileall -q src tests
python3 -m json.tool eval/cases.json >/dev/nullFor the focused API/persistence/admin gate used during this MVP:
APP_ENV_FILE=disabled PYTHONPATH=src python3 -B -m pytest -q \
tests/test_llm_adapters.py tests/test_telegram_notifications.py tests/test_http_runtime.py \
tests/test_postgres_persistence.py tests/test_persistence_config.py \
tests/test_admin_dashboard.py tests/test_prompt_and_reply_catalogs.pyExpected properties:
- All tests pass and source/test files compile.
eval/cases.jsonparses as valid JSON.- User-supplied
tenant_id=...remains inert text. - P3/P5 side effects require approval and are idempotent.
- Duplicate Telegram webhook delivery does not duplicate calendar, reminder, event-store, workflow-state, scheduler, or outbox records.
- Cross-tenant canary data is not returned through memory or calendar paths.
Start the local runtime on loopback:
export APP_ENV_FILE=.env
export PERSISTENCE_BACKEND=memory
PYTHONPATH=src python3 -m uvicorn personal_assistant.infrastructure.http:app \
--host 127.0.0.1 \
--port 8000Check the process:
curl -sS http://127.0.0.1:8000/livez | python3 -m json.tool
curl -sS http://127.0.0.1:8000/readyz | python3 -m json.tool/healthz is a deprecated compatibility alias for /livez in
0.2.0-alpha.1.
Create a reminder through the loopback-only trusted runtime API. It requires the server's fixed authority and an admin bearer token; request identity headers do not provide authority:
# Read from ignored .env into this PowerShell process; do not print the value.
$adminLines = @(Get-Content .env | Where-Object {
$_ -match '^ADMIN_TOKEN="[^"]+"$'
})
if ($adminLines.Count -ne 1) { throw 'Expected exactly one non-empty ADMIN_TOKEN.' }
$adminLine = $adminLines[0]
$adminToken = ([regex]::Match($adminLine, '^ADMIN_TOKEN="(?<value>[^"]+)"$')).Groups['value'].Value
$headers = @{ Authorization = "Bearer $adminToken" }
$body = @{
message_id = 'telegram-message-1'
source_event_id = 'api-request-1'
conversation_id = 'telegram-chat-1'
text = 'recuerdame clase el martes a las 5'
channel = 'telegram'
recipient = 'telegram-chat-1'
now = '2026-06-20T12:00:00+00:00'
timezone = 'America/Bogota'
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri 'http://127.0.0.1:8000/v1/runtime/reminders' `
-Headers $headers -ContentType 'application/json' -Body $body |
ConvertTo-Json -Depth 10The expected first response is an approval escalation. Approve it with:
$approvalId = '<approval id>'
Invoke-RestMethod -Method Post `
-Uri "http://127.0.0.1:8000/v1/runtime/approvals/$approvalId/approve" `
-Headers $headers -ContentType 'application/json' -Body '{}' |
ConvertTo-Json -Depth 10After the local check, run Remove-Variable adminToken, headers in that
PowerShell session.
The Telegram bridge is documented in docs/runbook/telegram.md. The active
webhook path is:
POST /webhooks/telegram
Minimum local environment:
export TELEGRAM_BOT_TOKEN="<bot token>"
export TELEGRAM_WEBHOOK_SECRET="<random webhook secret>"
export TELEGRAM_ALLOWED_USER_IDS="<comma-separated Telegram user ids>"
export ASSISTANT_TENANT_ID="personal"
export ASSISTANT_TIMEZONE="America/Bogota"Run FastAPI on loopback and configure an HTTPS proxy/tunnel that forwards only
POST /webhooks/telegram; it must deny health, runtime, and admin routes. Use
the in-memory PowerShell Telegram helper in docs/runbook/telegram.md to set,
inspect, or remove the webhook. It avoids putting the bot token or webhook
secret in command arguments, process listings, or terminal output. Telegram's
Bot API requires the bot token in its request URL, but the helper constructs
that URL only inside the HTTP process; it never appears in the public webhook
URL.
The webhook resolves tenant authority from runtime configuration and requires a
constant-time match for the X-Telegram-Bot-Api-Secret-Token header. Telegram
actors come only from Telegram user from.id data; chat.id is never an actor
fallback. The user allowlist is default-deny, including when it is empty. All
three checks run before command routing or any state, provider, or notification
work. Telegram sends are owned by the notification adapter and remain
P5/idempotent side effects.
Incoming Telegram voice/audio messages can be transcribed before command routing. Configure an OpenAI-compatible transcription provider, for example Groq:
export TRANSCRIPTION_PROVIDER="openai_compatible"
export GROQ_API_KEY="<speech provider key>"
export TRANSCRIPTION_BASE_URL="https://api.groq.com/openai"
export TRANSCRIPTION_MODEL="whisper-large-v3-turbo"Optional Telegram audio replies use MiniMax TTS. Workflows still produce text; infrastructure may synthesize a short audio copy after the text reply is accepted:
export TTS_PROVIDER="minimax"
export MINIMAX_API_KEY="<minimax token plan key>"
export TTS_BASE_URL="https://api.minimax.io"
export TTS_MODEL="speech-2.8-turbo"
export TTS_VOICE_ID="male-qn-qingse"
export TTS_AUDIO_FORMAT="mp3"
export TTS_LANGUAGE_BOOST="Spanish"
export TTS_MAX_REPLY_CHARACTERS="280"
export TELEGRAM_AUDIO_REPLY_MODE="voice_only"TELEGRAM_AUDIO_REPLY_MODE=voice_only sends audio only when the incoming
message was voice/audio. Use always for every Telegram reply or disabled for
text-only behavior. See docs/runbook/minimax.md for provider notes.
The admin dashboard is a local read-only inspection surface, not a production
ops console. It is loopback-only and tenant-scoped; it always requires
Authorization: Bearer <ADMIN_TOKEN>. Query parameters only filter the display
where supported; they do not establish authority.
Generate and store ADMIN_TOKEN in ignored .env with the PowerShell procedure
in docs/runbook/hardened-local-deployment.md; do not export or print it.
Open:
http://127.0.0.1:8000/admin
Useful JSON endpoints include /admin/snapshot, /admin/health,
/admin/approvals, /admin/traces, /admin/outbox, /admin/scheduler,
/admin/agenda, /admin/reminders, /admin/errors, /admin/events,
/admin/states, /admin/memory, and /admin/guardrails/metrics. The full
guide is in docs/runbook/admin-dashboard.md.
Memory mode is the default and is disposable. Use Postgres when webhook retries, approval resumes, worker restarts, or admin inspection must survive process restarts.
Install the optional extra and configure the backend:
python -m pip install -e '.[api,test,postgres]'
export PERSISTENCE_BACKEND=postgres
export DATABASE_URL="postgresql://personal_assistant:personal_assistant@127.0.0.1:5432/personal_assistant"
PYTHONPATH=src python3 -m uvicorn personal_assistant.infrastructure.http:app \
--host 127.0.0.1 \
--port 8000PERSISTENCE_BACKEND=postgres fails startup if DATABASE_URL, psycopg, or
the database connection is unavailable. The current adapter initializes
assistant_* tables for approvals, events, outbox, workflow states, memory,
local calendar, scheduled reminders, and traces. See
docs/runbook/persistence.md for schema notes, idempotency rules, worker lease
expectations, and limitations.
Durable reminder delivery requires PostgreSQL and a configured Telegram bot; memory remains a disposable development/test backend. The outbox is canonical and the scheduler is only a mirrored operational view. Operator commands are:
uv run python -m personal_assistant.infrastructure.worker run-once
uv run python -m personal_assistant.infrastructure.worker list-uncertain
uv run python -m personal_assistant.infrastructure.worker resolve-uncertain \
--message-id <id> --resolution delivered --confirm <id>retry is also available as a resolution. It returns the message to pending
only after exact confirmation and a runtime-owned P5 approval, and is rejected
once the message has reached four attempts. Automatic retries apply only to
known outcomes; ambiguous outcomes stop at uncertain for manual reconciliation
to bias the system against duplicate sends.
Present:
normalize_telegram_webhook(payload, tenant_id=...)build_container()andpersonal_assistant.infrastructure.http:appConversationCommandService,ReminderWorkflow, andReminderWorker- Local-only admin dashboard and JSON admin endpoints
- Telegram webhook bridge and outbound Telegram notification adapter
- MiniMax LLM/TTS adapters and generic Anthropic/OpenAI-compatible provider adapters where applicable
- In-memory persistence and optional Postgres persistence for the main runtime state stores
Not present yet:
- Persisted notification delivery ledger
- Production auth/deploy hardening beyond the local container profile
- OAuth credential storage
- External calendar sync
- Active MCP or A2A execution path
Forbidden by contract:
- Tenant authority from untrusted text, request bodies, LLM output, or retrieved documents
- Direct
telegram.sendfrom the agent workflow - MVP
mcp.*ora2a.*tool calls - Third-party messaging, financial actions, destructive bulk deletion, and secret reads
docs/runbook/telegram.md- BotFather, ngrok, webhook, Telegram command, audio, and local runtime notes.docs/runbook/hardened-local-deployment.md- HTTPS webhook-only boundary, local secret handling, verification, rotation, and rollback.docs/runbook/admin-dashboard.md- local admin dashboard and JSON endpoint guide.docs/runbook/persistence.md- memory/Postgres persistence guide.docs/runbook/v0.2.0-alpha.1.md- installation, migration, startup, uncertain-delivery, rollback, and no-secret smoke procedure for this alpha.docs/runbook/minimax.md- MiniMax LLM and TTS provider notes.docs/adr/- accepted architecture decisions.docs/architecture/- architecture reviews and short design notes.docs/architecture/build-vs-frameworks.md- why the MVP uses a small local harness instead of OpenClaw, HermeAgent/Hermes Agent, or OpenHands as the core runtime.docs/development/maintainer-workflow.md- executable single-maintainer workflow forcodex/branches, worktrees, review, commits, phase PRs, rollback, gates, and Definition of Done;docs/development/hardening-log.mdis its evidence template.docs/development/github-governance.md- stable CI checks, GitHub branch protection, merge-method policy, and the read-only verification workflow.docs/public/- public written artifacts.
tests/test_contracts.py- A2A serialization, tool policy surface, and inactivemcp.*/a2a.*fail-closed behavior.tests/test_command_router.py- Telegram command routing, pending approvals, approval commands, agenda, and status.tests/test_documents_and_channels.py- Telegram/WhatsApp normalization and document prompt-injection warning behavior.tests/test_reminder_workflow.py- reminder extraction, approval gate, idempotency, workflow state reuse, outbox/event/trace writes.tests/test_permissions_and_tenant.py- tenant identity, P3/P5 approval, idempotent side effects, and cross-tenant memory/calendar isolation.tests/test_durable_state.py- terminal workflow state immutability.tests/test_events_outbox.py- event and outbox tenant/idempotency behavior.tests/test_http_runtime.py- runtime API health, readiness, tenant authority, approval resume, structured errors, and tenant-scoped queries.tests/test_admin_dashboard.py- local admin dashboard snapshots, local-only guard, tenant-scoped visibility, and error categorization.tests/test_scheduler_worker.py- reminder notification worker dispatch, P5 approval policy, bounded loop, and tenant scope.tests/test_telegram_notifications.py- Telegram dispatcher P5 approval, replay idempotency, audio sends, and conflict detection.tests/test_llm_adapters.py- MiniMax LLM/TTS and OpenAI-compatible transcription adapters.tests/test_persistence_config.pyandtests/test_postgres_persistence.py- Postgres backend selection, optional dependency behavior, schema, and DTO serialization.tests/test_architecture_boundaries.py- hexagonal import boundaries.eval/cases.json- curated golden, failure-mode, and regression fixtures mapped to contractAC-*andFM-*references.
agents/personal_assistant/contract.md- single-agent contract.src/personal_assistant/domain/- business models, policies, permissions, pure domain services, and exceptions.src/personal_assistant/application/- DTOs, use cases, service ports, and bounded runtime orchestration.src/personal_assistant/adapters/- inbound channel/API adapters, outbound provider adapters, persistence adapters, and local observability.src/personal_assistant/contracts/- A2A and future interoperability contracts that are not the internal runtime.src/personal_assistant/infrastructure/- configuration, composition root, FastAPI app, prompts, replies, admin, and worker wiring.docs/,eval/, andtests/- design records, public artifacts, runbooks, golden cases, and regression checks.