Blog

Ansina: Password Login, Throttling, and Browser CORS

Password login and browser CORS landed; the auth stack stopped leaking by accident.

We replaced the fragile “some auth paths enforce policy, some do not” model with one boundary that actually holds. The fix was simple and boring: enforce weak-password checks before writes, lock brute-force attempts by username and IP, then make browser preflights respect the same auth rules.

⚡ Password Policy and Self-Service Change

  • Password policy — auth/password_policy.py enforces a min-length floor of 8, casefolded username checks, and a bundled common-password reject list before any user row exists.
  • Self-service reset — PUT /auth/me/password proves possession with the current password, then checks the new one, so a stolen token cannot probe policy or silently overwrite.
  • Everything in scope — the same policy runs on create-user and admin password changes, which closes the bypass that would otherwise exist on the one request path that writes a password.

🛡️ Throttling and Failure Signal Control

  • Username and IP locks — LoginThrottle holds separate buckets per username and per IP, records unknown usernames the same as real ones, and returns the longer remaining lock in Retry-After.
  • Failure timing — the route now verifies a dummy argon2 hash on unknown-user and wrong-password failures, so reconnaissance does not leak an easy time signal.
  • Success clears state — a successful login clears both buckets; repeated failures keep the lock, and the 429 path stays consistent instead of becoming an enumeration oracle.

🔒 Browser CORS and Auth Boundary

  • Origin allowlists — CorsSettings accepts an explicit list, strips trailing slashes, and fails fast on empty or wildcard configs before uvicorn binds a port.
  • Preflight ordering — CORS is registered outermost, so browser OPTIONS requests never hit BearerAuthMiddleware and a valid preflight returns 200 instead of a confusing 401.
  • Client visibility — Access-Control-Expose-Headers includes X-Request-ID and Retry-After, so JavaScript can correlate auth failures and surface throttle state without hidden network errors.

The auth stack is finally boring in the right way. 💥

#AIEngineering #SoftwareEngineering #Authentication #WebSecurity

Fail-Closed Access: Custom Roles and Federated Identity

ai-engineerai-agentView source PR ↗

Ansina’s identity layer moved from static role assumptions to fail-closed, federated access control.

M3 shipped custom roles, TOTP step-up, credential expiry, role-mapping provenance, and an OAuth 2.0/OIDC login exchange. The common thread is structural enforcement: permissions must match served routes, sensitive mutations require live step-up, and federated claims cannot silently override local assignments.

⚡ Fail-Closed Authorization

  • Sudo gate — SudoRequiredError now applies to every non-admin role on sensitive resources. A custom role granting auth.* cannot bypass step-up by avoiding the old maintain slug.
  • Factor registry — StepUpRegistry returns every enrolled verifier, while GET /auth/me exposes step_up_factors. Zero factors, unknown factors, and ambiguous requests fail with 403 without consuming lockout attempts.
  • Grant fidelity — Resource catalogs now persist served verbs and policy class. Builtin grants are intersected with resource.verbs, so inert permissions such as system.version:DELETE are pruned on boot.

🛡️ Roles, Credentials, and TOTP

  • Custom role lifecycle — POST, PATCH, and DELETE /auth/roles validate catalogued, served, grantable permissions before self-escalation checks. Builtin roles and assigned custom roles remain immutable or undeletable.
  • Credential expiry — Authentication filters credentials.expires_at in SQL and uses one injected clock instant for expiry and staleness. issue_token can now mint tokens with an explicit TTL.
  • Encrypted step-up — RFC 6238 TOTP uses an AES-GCM v1 envelope, an env-only 32-byte key, drift handling, and replay prevention. Enrollment is intentionally ungated; disabling it requires sudo.

🔄 Federated Login Without Rewriting Auth

  • Login exchange — POST /auth/oidc/login and GET /auth/oidc/callback implement authorization code plus PKCE. ID tokens reject bad signatures, issuers, audiences, expiry, nonces, and unsupported algorithms before provisioning.
  • Claims reconciliation — Each login refreshes role_mappings through sync_mapped_roles. Provider-owned assignments change with claims; local assignments survive. JIT provisioning links matching local usernames, with explicit refusals and warnings.
  • Deliberate boundary — OIDC is a login exchange, not a per-request authenticator. Authenticator, bearer middleware, resolve_principal, and the TUI stayed untouched; ansina-tui remains an external API client.

The access layer now fails closed by construction. 🔒

#AIEngineering #SoftwareEngineering #RBAC #FederatedIdentity #TOTP

Ansina CLI/TUI: A Dependency-Isolated Control Surface

ai-engineerai-agentView source PR ↗

Ansina’s REST API now has a real control surface: a dependency-isolated CLI for automation and a TUI for operators.

We built ansina-tui without importing ansina, then wired authentication, token lifecycle, raw API access, and a live Overview tab on top of HTTP. The result is a usable front door for both CI/CD and humans, with failure states designed instead of leaked as tracebacks.

⚡ A Separate Control Surface

  • Dependency isolation — tui/ owns its packaging, lockfile, virtual environment, and CI job. tests/unit/test_isolation.py pins the boundary: the CLI never imports the daemon package, while typer, httpx, textual, and rich stay on the client side.

  • One invocation, two modes — Bare ansina-tui opens the TUI only on a TTY. Any argument selects the CLI, while a non-TTY bare invocation prints help to stderr and exits 2. --refresh is validated only when an actual TUI launch occurs.

  • Operational exit codes — status checks /healthz before /readyz, then /version, and distinguishes unreachable, not-ready, and unhealthy states. Codes 6 and 7 make the command useful as a container readiness and health probe, not just a diagnostic.

🔒 Identity and Credential Boundaries

  • Self identity is first-class — GET /auth/me resolves the existing Principal without a database read. The me.* policy carve-out grants every builtin role access to me.profile, while sensitive auth.* routes remain protected.

  • Tokens are self-service — Users can mint, list, and revoke their own tokens; administrators can manage tokens on behalf of other users. Raw credentials are returned exactly once, hashes never leave the server, and revoked tokens fail on the next authenticated request.

  • Bootstrap means break-glass — The bootstrap identity is auto-generated, never overridden or rotated, and can hold exactly one token. A separate configured Admin is provisioned from ANSINA_SECURITY__ADMIN_USERNAME and ANSINA_SECURITY__API_TOKEN; the token-only configuration is no longer valid.

🧠 Automation Without Hidden Interaction

  • Raw REST coverage — ansina-tui api reaches every daemon route without a client-side allow-list. -f key=value builds simple JSON bodies, --input sends typed or raw payloads, and -H cannot override the real Authorization or X-Sudo-Token headers.

  • Sudo becomes state — A live sudo grant is attached automatically through the shared session layer. Without one, sensitive calls exit 4 with an auth sudo hint and never prompt mid-request, keeping the API command safe for CI/CD.

  • Output stays machine-safe — --json and piped output preserve the daemon body verbatim, including non-2xx problem documents. Diagnostics go to stderr, so api ... --json | jq remains predictable.

🔬 A TUI That Fails Deliberately

  • Overview is a refresh contract — fetch_overview() reads health, readiness, version, and identity into an OverviewSnapshot. Connectivity failures and insecure hosts.toml become designed states rather than exceptions from Textual workers.

  • No flicker, no duplicate widgets — An exclusive refresh worker updates existing sections in place on a timer or with r. Adding another tab is an append to TABS, not a rewrite of the application shell.

  • The honest boundary — Heart-disabled handling is explicitly out of scope because Overview calls four routes and /heart/tick is not one of them. That acceptance item is N/A, documented rather than silently implied.

M4 is shipped: a REST API with a control surface that respects both operators and automation. ⚙️

#AIEngineering #SoftwareEngineering #CommandLineTools #MultiAgentSystems #RBAC

Engineering Database-Backed RBAC with Sudo Enforcement

ai-engineerai-agentView source PR ↗

Ansina’s access control is now database-backed, route-audited, and step-up protected.

This milestone replaced static credential assumptions with a complete RBAC system spanning identity, permissions, sudo grants, and management APIs. The hard part was not adding decorators. It was making every authorization decision explicit, testable, and difficult to bypass accidentally.

⚡ The Authorization Boundary

  • Database-backed identity — Users, groups, roles, credentials, external identities, and role assignments now live in SQLite repositories. API tokens are stored only as salted hashes, passwords use argon2id, and BearerAuthMiddleware resolves active principals through an injectable authenticator chain.

  • Startup as a security gate — audit_route_coverage() walks the real FastAPI route table and fails create_app() when a non-public route lacks require(...). The old BOOTSTRAP_RESOURCES list is gone; the running application is now the resource catalog.

  • Explicit failure semantics — Unauthorized requests remain 401, while insufficient permissions return distinguishable 403 problem+json codes. The response does not reveal whether the identity, token, or permission was the failing component.

⚙️ Sudo as a Real Step-Up Flow

  • Verifier-agnostic grants — SudoService owns issuance, TTL, revocation, and lockout above a StepUpVerifier protocol. M2 uses password re-verification, but a future verifier can be registered without rewriting grant or enforcement logic.

  • Sensitive routes are structurally protected — Maintain must present a live X-Sudo-Token for sensitive resources; Admin does not. Bad, expired, or revoked grants do not become authentication failures—they simply leave the principal without sudo and produce CODE_SUDO_REQUIRED.

  • State survives restarts — Sudo grants and lockouts are persisted in sudo_grants and sudo_lockouts, not process memory. Failed attempts produce a real 429 with Retry-After, and no raw password or grant token reaches logs.

🛡️ Management Without Self-Escalation

  • The API can bootstrap its own users — Admin can create a user over HTTP, set its password, issue a token, assign Maintain, and exercise the resulting identity. This is the first end-to-end path that provisions a non-bootstrap principal without touching SQLite directly.

  • Maintain is deliberately bounded — Even with a valid sudo grant, Maintain cannot assign Admin or any role granting auth.* permissions. The checks cannot be reduced to a permission-subset comparison because Maintain and Admin share fixed grants under this policy.

  • Deletion is a tombstone — DELETE /auth/users/{id} records deleted_at and purges credentials, assignments, memberships, and live sudo grants in one transaction. The identity remains for audit attribution, but manually reactivating the row restores nothing.

🔬 Verification Against the Failure Modes

  • Boot behavior is explicit — Fresh databases auto-generate a 256-bit bootstrap token, print it once, and retain only its hash. Operator overrides are validated for entropy, dev mode requires a loopback bind, and manual runs confirmed stable restart behavior with no secrets in JSON logs.

  • The suite reached 598 unit tests — ruff, formatting, and mypy --strict stayed clean while unit coverage remained at 100%. The e2e suite reached 20 black-box tests, including the full 401 → 403 → 200 authorization chain and the RBAC management round trip.

  • The failure cases are intentional — Missing route dependencies fail startup. Inactive or tombstoned users stop authenticating. The final Admin cannot be deleted or demoted, and a sudoed Maintain cannot mint an Admin.

Deterministic access control is now part of the application boundary, not a convention.

#AIEngineering #SoftwareEngineering #RBAC #AccessControl #MultiAgentSystems

Ansina: Heart & Brain — MLX heart runtime, autonomic tick loop, and OpenAI‑compatible brain

ai-agentai-a2aai-mcpView source PR ↗

Small changes in surface, big moves under the hood: Ansina gets a Heart and a Brain.

Lead: Merged PR #23 wires together three pieces that make Ansina more autonomous and interoperable — a Heart runtime protocol (MLX), an autonomic tick loop for continuous decisioning, and a BrainProvider with an OpenAI-compatible adapter. This is about runtime contracts, graceful looping, and making the brain easier to swap and test.

❤️ Heart: runtime protocol & MLX adapter

  • 🧭 Implemented a Heart runtime protocol (src/ansina/heart/runtime.py) to formalize how the agent’s “heart” signals intentions and reacts to runtime events.
  • 🔌 Added an MLX adapter (src/ansina/heart/adapters/mlx.py) so the Heart can speak an external ML execution layer — a practical runtime bridge for experiments and deployments.
  • 🛠️ Exposed heartbeat endpoints and route integration (src/ansina/api/routes/heart.py) so orchestration layers can observe and drive the Heart.
  • ✅ Tests added around models, runtime, and the MLX adapter (tests/unit/heart/* and tests/unit/heart/adapters/test_mlx.py) to keep the contract honest.

🧠 BrainProvider: port + OpenAI-compatible adapter

  • 🔁 Ported a BrainProvider abstraction (src/ansina/brain/provider.py) to centralize model calls, retries, and selection logic.
  • 🧩 Introduced an OpenAI-compatible adapter (src/ansina/brain/adapters/openai_compat.py) to make local or third-party LLMs plug into existing tooling and tests.
  • 🧪 Rich test coverage for events, retry logic, and adapter behavior (tests/unit/brain/*) to validate fallbacks and deterministic selection strategies.
  • 📦 Config surfaced in settings and examples (src/ansina/config/settings.py, ansina.example.toml) so switching brains is configuration, not code surgery.

⚙️ Autonomy: autonomic tick loop & decision snapshots

  • ⏱️ Built an autonomic tick loop (src/ansina/heart/tick/loop.py) that drives periodic decision ticks and lifecycle transitions — the agent keeps itself honest over time.
  • 🧠 Split decision logic, snapshots, and selection into focused modules (tick/decision.py, tick/snapshot.py) for easier reasoning and replay.
  • 🔄 The loop integrates with Heart runtime events and BrainProvider calls to make pacing and retries explicit and testable (many unit tests added).
  • 🪪 Snapshots and deterministic decisions make debugging and e2e validation (tests/e2e/test_server.py) reproducible.

📚 DX, docs & reliability

  • 📝 Docs and blueprints updated (README.md, docs/architecture/blueprint.md, AGENTS.md) to document the runtime contract and configuration.
  • 🧰 CI-friendly changes and a fuller pyproject + lock (pyproject.toml, uv.lock) — improved reproducibility.
  • 🧩 API and error surface tightened (src/ansina/api/* and src/ansina/errors.py) so telemetry and retries are actionable.
  • 🔍 Numerous unit tests added/extended across API, brain, heart, and logging to keep future refactors safe.

This merge moves Ansina from prototype toward a composable runtime: clear contracts for heartbeats, a pluggable brain interface, and an autonomic loop that keeps the agent moving even when things fail.

Onward — building agents with both heart and brain.

Ansina Rock‑Solid Skeleton: packaging, config, logging, REST API, auth, persistence, CI, docs

ai-agentai-mcpai-engineerView source PR ↗

Laid the foundation: core infra, API, auth, persistence, CI, tests and docs — a production-minded M0 skeleton for Ansina.

Lead A focused engineering sweep to make Ansina reviewable and operable without any model code. The PR delivers a full infra baseline (packaging, config, logging already merged via #19), then adds REST endpoints, auth, persistence primitives, CI gates, testing conventions and docs scaffolding. All checks (unit + e2e, both OS legs) are green; branch is still a draft pending final author flip to “ready for review.”

🚧 What this PR adds

  • 🧩 REST: 🛣️ API skeleton and middleware (src/ansina/api/app.py, routes/health.py, middleware.py, exception_handlers.py) to standardize request/response flows.
  • 🔐 Auth: 🪪 API authentication and public endpoints (src/ansina/api/auth.py) with tests to validate behavior.
  • 💾 Persistence: 🧰 DB foundation, migrator and initial migration (src/ansina/storage/database.py, migrator.py, migrations/0001_init.sql).
  • 📄 Docs & README: 📝 README rewrite and docs scaffolding (docs/architecture/blueprint.md) to explain architecture sections §3–§5.

⚙️ CI, tests, and quality gates

  • 🧪 Tests: ✅ Unit + E2E tests added (tests/unit/*, tests/e2e/test_server.py) and a testing strategy that defines unit conventions + E2E build validation gate.
  • 🔁 CI pipeline: 🛠️ .github/workflows/ci.yml implements unit and E2E jobs; both OS legs pass on CI.
  • 🧹 Hygiene: ✨ pre-commit, Makefile, pyproject tweaks and .gitignore updates to keep developer UX smooth.

🧭 Engineering choices & tradeoffs

  • ⚖️ Separation: Model code intentionally excluded — M0 focuses on stability, contracts, and reproducible ops before wiring inference.
  • 🧪 Validation-first: E2E gate ensures the runnable artifact builds cleanly on multiple OS runners before merging downstream model work.
  • 📚 Documented: Architecture blueprint and examples (ansina.example.toml) make onboarding and future design decisions explicit.

🔎 Files and signals worth scanning

  • 🔁 CI + tests: .github/workflows/ci.yml, tests/e2e/test_server.py
  • 🧭 API surface: src/ansina/api/app.py, auth.py, routes/health.py, readiness.py
  • 🗄️ Storage: src/ansina/storage/database.py, migrator.py, migrations/0001_init.sql
  • 📘 Docs: README.md, docs/architecture/blueprint.md

This was a foundation-first sprint: stable contracts, testable CI, and readable docs — readying Ansina for the next phase where model integration becomes a consumer of this platform.

Onward to M1 — wire the models once the infra is battle-tested.

M0 skeleton: packaging & toolchain, layered configuration, and structured logging

ai-agentai-mcpai-engineerView source PR ↗

Ship the foundations so higher-level intelligence has a stable home.

Lead: Merged a stacked change set that establishes the M0 skeleton for ansina — packaging, layered configuration, and structured logging with an error taxonomy. This is infrastructure-first: small, test-covered primitives that make future agent work reliable and observable.

🚀 Packaging & Toolchain

  • 🧰 Added reproducible packaging via pyproject.toml and uv.lock to pin deps for predictable builds.
  • 🛠️ Makefile provides simple developer ergonomics (build, lint, test) so contributors can focus on features.
  • 📦 Project layout: src/ansina, py.typed, and sensible .gitignore / .pre-commit-config.yaml to catch regressions early.
  • 🔁 CI-ready baseline: commits and scaffolding aim to make downstream PRs (like config & logging) trivial to merge.

🧭 Layered Configuration System

  • 🗂️ Introduced a layered settings model (src/ansina/config/settings.py) that merges defaults, env, and ansina.example.toml.
  • 🔁 Enables runtime overrides and environment-specific profiles while keeping a clear canonical source-of-truth.
  • 🧪 Tests added (tests/unit/config/test_settings.py) to validate precedence, parsing, and edge-cases — configuration is code, and now it’s tested.
  • 📄 Documented intent in docs/architecture/blueprint.md (sections 3–5) so config decisions are traceable.

🔍 Structured Logging & Error Taxonomy

  • 🧾 Structured logging modules (src/ansina/logging/*) introduce context, formatter, redaction, and setup helpers for consistent logs.
  • 🧩 Error taxonomy (src/ansina/errors.py) classifies failures so observability and retries can be systematic instead of ad-hoc.
  • 🔒 Redaction utilities keep sensitive fields out of telemetry; formatters standardize JSON-friendly output for downstream ingestion.
  • ✅ Coverage: unit tests (tests/unit/logging/* and tests/unit/test_errors.py) ensure shape, context propagation, and redaction behave as expected.

🛠️ Why this order mattered

  • 🧱 Packaging first: a stable toolchain made subsequent config and logging work reproducible across machines and CI.
  • 🔗 Layered config next: it lets apps behave correctly in dev, CI, and production without code changes.
  • 📡 Logging & errors last: with predictable packaging and config, logs map to real runtime semantics and tests can assert behavior.

What’s parked or next

  • ▶️ #3 (structured logging) was merged together for cohesion; CI green gates remain a priority before broader feature work.
  • 🔭 Next: wire observability into agent runtime paths and start collecting real traces during e2e experiments.

Small wins like these are quiet but multiply: fewer surprises, faster iteration, and safer experiments.

Building Ansina: A Blueprint for a Deterministic, Dual-Model AI Runtime

Designing a lean, provable AI agent runtime built from the ground up on Python ≥ 3.14: introducing the architecture blueprint for Ansina.

Currently in its blueprint phase and moving rapidly toward initial implementation, Ansina focuses on a tight, deterministic core where every architectural choice serves operational control, high signal-to-noise ratio, and zero unnecessary bloat.

Here is an inside look at what Ansina brings to the table as it prepares to launch:

📐 Pure Hexagonal Architecture

  • Streamlined REST Surface: Exposes a clean, single internal FastAPI REST API with zero channel bloat or unnecessary gateway protocol overhead.
  • Deliberate State Management: Starts with a minimal SQLite schema, growing persistence intentionally rather than accumulating hundreds of unmanaged state tables.
  • Never-Throw Streaming Contracts: Features an ApiProvider port where streaming errors are returned synchronously as terminal stream events, guaranteeing predictable error paths.
  • First-Class Redaction: Implements structured logging with redaction hardcoded into the formatting pipeline to ensure sensitive data is filtered before it hits disk.

🫀 The Dual-Model Core: Heart & Brain

Ansina separates autonomic local liveness from remote cognitive reasoning to eliminate round-trip network hops for basic decisions:

  • The Heart (In-Process Autonomic Loop):

  • Uses an embedded ≤4B parameter model running natively in-process via MLX on Apple Silicon (with a llama-cpp-python fallback).

  • Bounded strictly by an 8k context window to ensure optimal prompt performance.

  • Drives an always-on tick loop tasked exclusively with deciding idle vs. act vs. escalate without network overhead.

  • The Brain (Remote High-Reasoning):

  • Connects to 35B+ parameter models via an OpenAI-compatible BrainProvider port for complex reasoning tasks.


🎯 Hardware Target & Testing Strategy

  • Hardware First: Optimized explicitly to run as an always-on engine on local Apple Silicon hardware (M4 Mac Mini, 16GB unified memory).
  • Black-Box Verification: Uses an E2E test harness that spins up python -m ansina as an isolated subprocess, testing readiness, migrations, and auth strictly over HTTP.

The design is locked, the constraints are set, and implementation of M0 — Skeleton is about to begin.

Single-agent A2A pivot — park the tri-node, preserve the work

ai-agentai-a2aai-mcpView source PR ↗

Hook: We pivoted to a pragmatic architecture — single-agent A2A — while keeping the original tri-node design one uncomment away.

Lead: After end-to-end tests showed sub-agent → A2A is unreliable in OpenClaw, the team chose stability and observability over brittle enforcement. The result: main does cross-agent calls directly, tri-node is parked (preserved), MCP tooling added, and docs + tests updated.

🔁 Why we pivoted

  • ⚠️ Sub-agent A2A break: tests showed a sub-agent cannot reliably drive another agent over A2A; the break is the sub-agent+cross-agent combination.
  • 🔍 Practical trade: preserve the tri-node design, but avoid production fragility by routing A2A from main→main.
  • ♻️ Preserve, not delete: tri-node config, prompts, and workspaces remain in-place as JSON5 comments and IDENTITY.tri-node.md (tag v0.1.0 = revival baseline).

🛠️ What changed (concrete)

  • 🧭 Single-agent mode: both containers make main the working agent; when code is needed Researcher main curls Coder at http://coder:3000/a2a/tasks.
  • 🗂️ JSON5 preservation: sub-agent entries commented in openclaw.json so revival is one uncomment away.
  • 🧰 Local MCPs: added filesystem MCP for Coder and memory MCP for Researcher; tools surface as bundle-mcp:<server>__<tool>.
  • 🧪 Orchestration + tests: index.js remained logic-identical (comments updated); tmux vm-bridge used for live runs and probes.

✅ What we validated

  • 🔎 Spawn logs: live single-agent tasks showed spawn_count = 0 — no sub-agent spawn.
  • ↔️ A2A round-trip: Researcher→Coder→Researcher worked in live tests (Coder logged payload, returned structured JSON, Researcher folded result.output).
  • 🧰 MCP probe: openclaw mcp doctor files → files: ok; doctor memory → memory: ok; live tool calls returned expected results.
  • ⚙️ Docs & artifacts: AGENTS.md + README updated, diffs and diffstat adjusted, IDENTITY.tri-node.md preserved, tag v0.1.0 recorded.

📌 Edge cases, constraints & next steps

  • 🪪 Prompt hygiene: observed headless-output violations (narration prefixes, markdown fences) and a fallback self-write when Coder was down — these are prompt-enforcement issues to fix, not A2A plumbing failures.
  • 🔁 Operational note: to pick up MCP config changes, restart the container (sudo podman restart) — do NOT use pm2 restart openclaw-gateway (it can orphan the gateway).
  • 🛠️ Revival path: once OpenClaw fixes cross-agent delegation from sub-agents, simply uncomment JSON5 entries + swap IDENTITY.tri-node.md back to resume tri-node operation.
  • 🚦 Next: tighten prompt enforcement, add CI smoke-tests for A2A round-trip, and monitor upstream OpenClaw fixes to evaluate revival.

Final: Practical pivots win — preserved the long-term design while shipping a robust, testable PoC for cross-agent work.