Skip to content

Releases: Yigtwxx/Maestro

Maestro v0.1.3 — free to start, harder to abuse

Choose a tag to compare

@Yigtwxx Yigtwxx released this 07 Aug 20:19
323c2bc

A security and access release. Every account is now provisioned with an active free plan, so nothing gates the product behind a payment wall that moves no money. Around that, the account surface was hardened on four axes it previously left open — where the session credential lives, how often an account can be guessed at, how many identities one mailbox can hold, and how much of the platform one account can hold at once. A self-contained watchdog means a default deploy is no longer silent when it breaks.

Every account starts free

Registration provisions an active free subscription with an unlimited token quota. POST /tasks no longer answers 402 to a new account, and the plan grid leads with free rather than presenting a paywall.

Paid plans are parked, not removed: BILLING_ENABLED (backend) and BILLING_LIVE (frontend) are both false while no real processor is integrated, so /billing/subscribe and /billing/cancel answer 403 for everyone except admins — who keep the live flow so the operator can test it — and the billing surfaces render "coming soon". The two flags flip together when a processor lands.

Migration 0018_free_plan backfills every existing account onto the new plan.

EMAIL_VERIFICATION_REQUIRED now ships false. That is one decision with EMAIL_PROVIDER=console, not two: the console sender writes verification mail to the server log, so an enforced gate locked every account on a fresh install with no way through. Nothing was removed — /verify-email, the resend endpoint and the new code flow all keep working. Enable the gate alongside a real sender.

The refresh token never reaches JavaScript

The refresh token is now set and read as an httpOnly, Secure, SameSite=Strict cookie scoped to /api/v1/auth. No endpoint accepts one in a request body, none returns one in a response body, and the access token lives in a module-level variable that is never persisted. Neither localStorage nor sessionStorage holds a credential, so an XSS foothold can spend the current access token for at most its 30-minute life and cannot exfiltrate a 7-day session — which rotation alone could never prevent, since a thief holding both halves just rotates like a normal user and trips no detection. tests/test_auth_cookie.py fails on any of the three.

SameSite is the whole CSRF control on the two cookie-authenticated routes, and it is sufficient because both are POST-only. SameSite=None is deliberately not offered and is rejected at parse time: a split-domain deployment routes the API through the frontend's BACKEND_ORIGIN rewrite rather than deleting the control.

Because memory starts empty, every document load spends one rotation, and several tabs restoring at once would replay one cookie into reuse detection and burn the session family. navigator.locks serializes rotation across the origin's documents, so a waiting tab presents the successor rather than a token already spent. That per-load rotation is also why /refresh now carries its own rate-limit tier instead of the shared auth bucket: routine page loads must not be throttled alongside credential stuffing.

REFRESH_COOKIE_SECURE / REFRESH_COOKIE_SAMESITE / REFRESH_COOKIE_DOMAIN are new. SECURE is deliberately unset rather than true — Safari has historically refused a Secure cookie over http://localhost — and resolves to "on outside development", with production refusing a boot that turns it off.

Sign-in throttling counts the right axis

A rate-limit bucket keys by caller identity, which on an unauthenticated route is the client IP — so a credential-stuffing run spread over a botnet spends one or two attempts per address against a target account and never approaches the limit, leaving guesses against any single account effectively unbounded.

utils/login_throttle counts the other axis: failures per account, whatever address they arrive from (10 per 15 minutes, and 5 for the second factor, which is only 10⁶ and sits behind an already-known password). It runs over the limiter's own buckets, so Redis, the in-memory fallback and the circuit breaker are shared rather than duplicated. Only failures are recorded and a success clears the record. The check runs before the password is verified, which is what makes it a block rather than a counter — and keeps a blocked account from costing an Argon2 hash. The subject is hashed into the bucket key, because an address is PII and a key travels through monitoring output that the address should not.

The accepted trade-off is the classic one, and it is bounded three ways: the block is temporary rather than an administrative lock, it cannot be extended past one window, and password reset stays open throughout because it is keyed by an emailed token rather than by these counters.

Signup abuse, on the two axes a limiter cannot see

  • utils/mail_budget counts sends per recipient address — the axis a caller-keyed limiter is blind to, since the endpoints that send mail all let the caller pick or trigger who receives it. POST /users/me/email is the sharpest of them: it takes an arbitrary address behind a session, so without this one account is a mail cannon aimed at anyone.
  • utils/human_check adds a honeypot, a server-signed challenge nonce and an optional CAPTCHA to the two unauthenticated endpoints only. CAPTCHA_PROVIDER defaults to none and that is not "unprotected" — the other two layers still run and no third party is contacted, keeping the zero-egress default. Production refuses turnstile with a missing key, because a provider that fails closed on every call stops registration silently.

Every rejection is silent: the endpoint answers with its normal fixed body. A 429 or a "captcha failed" would break the byte-identical response /register guarantees and tell an automated client which layer caught it. The cost is that a false positive is invisible to the user, which is why each rejection increments maestro_abuse_rejected_total{reason} — that counter is the only place the false-positive rate can be seen.

At /register an exhausted budget suppresses the mail and still creates the account. Refusing would hand back a better weapon than the one removed: keeping one address's bucket full would deny that address registration indefinitely.

One mailbox, one account

mail_budget bounds how much mail an inbox takes and human_check bounds how mechanically a form is submitted, but neither notices that you+1@gmail.com, you+2@gmail.com and y.o.u@gmail.com are one mailbox. users.canonical_email carries a unique index over the canonical form, so an account is bounded per mailbox rather than per typed string. The rule applies only to known providers — + is a legal local-part character whose meaning belongs to the receiving server, and stripping it from an unknown domain would merge unrelated accounts.

Enforcement is the index, never a SELECT before the INSERT: the collision surfaces as an IntegrityError and falls into the silent-duplicate branch /register already had, which keeps the endpoint free of both a TOCTOU window and a timing oracle. Migration 0019 grandfathers accounts that already collide as NULL rather than merging them or picking a winner, and CI now downgrades one revision, seeds a colliding pair and re-runs the backfill to assert that behaviour — the one code path whose only production run is its first real one.

Two domain-level checks sit beside it and answer visibly with a 400, unlike every other control here: a disposable-provider blocklist (vendored and curated rather than fetched, so the zero-egress default holds) and an MX check. Neither answer can be turned into an account-existence oracle, because both are properties of the submitted domain alone. Privacy relays are allowlisted ahead of the blocklist — Apple Private Relay and its peers are permanent addresses belonging to precisely the users most careful about their data. The MX check is not an abuse control and does not pretend to be one; every burner provider has good MX records. It exists so hard bounces do not damage sender reputation, and it fails open on every DNS error.

DISPOSABLE_EMAIL_BLOCK_ENABLED, DISPOSABLE_DOMAINS_EXTRA and EMAIL_MX_CHECK_ENABLED are the per-control rollbacks. All ship on, because neither widens what the platform reaches out to.

Email ownership

An account's address is now only ever set by proving the inbox is reachable. PATCH /users/me cannot touch email at all; the change goes through POST /users/me/email, which requires the current password, leaves the account on its old address, mails a link plus code to the new one and an actionable-nothing notice to the old one, and revokes other sessions when it lands. The pending address rides on the token row rather than on users, so it expires and rotates with the token and a stale link can never apply an older address.

  • Every verification and email-change mail now carries a 6-digit code beside the link. The two are not interchangeable: a 256-bit token is globally unique, which is what lets the link endpoints stay unauthenticated, while 10⁶ is guessable — so the code carries a short TTL, an attempt cap that burns the row, and a lookup scoped to one user_id. Password reset deliberately has no code: it grants account takeover and must not gain a guessable second credential.
  • Single-use tokens are claimed with one conditional UPDATE … RETURNING, never a read-then-write, so two concurrent redemptions of the same link cannot both succeed.
  • /register no longer reveals whether an address is known. A duplicate registration creates nothing, leaves the existing account's credentials untouched, and notifies the real owner instead. POST /users/me/email likewise never reports that a target address is taken — a collision surfaces only at confirm time, once the caller has proven they can read that inb...
Read more

Maestro v0.1.2 — your data, your APIs

Choose a tag to compare

@Yigtwxx Yigtwxx released this 28 Jul 11:39

A feature release. Agents can now read your own uploaded documents and conversation memory, call HTTP endpoints you register yourself, and hand each other tools mid-run. The agent creation flow was rebuilt, and the documentation moved to its own site.

Agents can read your own data

Two new tools, document_search and memory_recall, run over the user's uploaded documents and conversation memory in Qdrant. They are keyless — nothing to configure beyond DOCUMENT_SEARCH_ENABLED / MEMORY_RECALL_ENABLED — and every query is scoped by user_id at the service layer, so one account's documents cannot enter another's context. Both degrade to a "no results" note on a cold Qdrant rather than an error, so they are safe to leave on before anything is ingested. Every domain squad declares them.

The Main Agent can also run a bounded, read-only pass over exactly those two tools before it plans, so the subtask breakdown is grounded in what you actually have rather than guessed at. MAIN_AGENT_DISCOVERY_ENABLED turns it off; MAIN_AGENT_DISCOVERY_MAX_CALLS (default 2) bounds it. No external or action tool can run at the main tier.

Document upload limit is raised to 5 MB, validated client-side before the request.

Register your own APIs as agent tools

An endpoint you register becomes a custom_api__{slug} action a subagent can call. Method, path parameters, query values, static headers and one credential (bearer, header or query), plus a dry-run test call that goes through the real execution path — a check that takes a different route can pass while the real call fails. It lives in the Capabilities step of the agent wizard, so registering an endpoint and attaching it to an agent is one flow.

This is the first place in Maestro where a user supplies the host an outbound request goes to, so none of the "every base URL is a constant of ours" reasoning that covers the GitHub/X/Discord/Maps tools applies. url_guard runs twice — at registration and again on every call, because a record outlives its validation and the DNS for a host you own is yours to change. Redirects are never followed: without a hard-coded host there is nothing to bound the hop to, and following one is how an Authorization header reaches another origin. Path parameters are percent-encoded with an empty safe set. Responses are byte-capped while streaming. The credential is AES-256-GCM in Mongo and is kept out of every response by both a query projection and an explicit public field list. The tool's own name and description pass prompt_guard, because they are interpolated into the subagent's system prompt.

The residual risk is DNS rebinding, which url_guard documents as unclosed — so CUSTOM_API_TOOLS_ENABLED ships false. Turning it on is an operator saying the deployment can accept outbound requests to hosts its users choose.

Tools are assigned per subagent, and can be requested mid-run

The Main Agent can now hand each squad member a subset of the domain's tools rather than the whole set. A member that finds it needs something it was not given emits a request_tool directive; the Main Agent, acting as a gatekeeper, autonomously grants or denies it. No task pause and no human in the loop — this is distinct from the ask_user channel.

A grant can never bypass a gate: the grantable pool resolves through the same domain/switch/credential filter as any other tool, so a subagent cannot obtain something the operator disabled or you hold no key for. Escalations are capped by SUBAGENT_MAX_TOOL_GRANTS (default 2), and a grant raises tool variety, never call volume — the per-tool and total call caps still bound execution. Grant state stays local to each subtask run, so a grant to one member in a parallel wave never leaks to its siblings.

Rebuilt agent creation and registry

The single long agent form is now a five-step wizard — Identity, Behavior, Capabilities, Routing, Preview — with per-step validation. Its rules live in pure, testable logic keyed off shared limits that a backend parity test compares against the Pydantic schema, so the form cannot silently drift from what the API accepts.

The agent list is a searchable registry with distinct cards for built-in and custom agents, and per-domain hover motifs. The tool catalog is served from a live endpoint carrying each tool's capability status, instead of being duplicated in the frontend.

Documentation site

The docs now build with MkDocs Material and publish to GitHub Pages on every change: https://yigtwxx.github.io/Maestro/ — quick start, architecture, a comparison page, configuration, deployment, API reference, security policy and the contributing guide.

Security and defaults

  • CODE_EXECUTION_ENABLED now defaults to false. It is the one tool whose blast radius is the host: enabling it means mounting the Docker socket, which hands agent-authored code the ability to start privileged containers outside the sandbox. The daemon probe in front of it is an availability check, not a security boundary.
  • /health/ready no longer returns its per-dependency checks map to anonymous callers. The probe stays publicly reachable and the 200/503 status code is unchanged, so no uptime monitor needs a credential — but which backing service is down is reconnaissance. A degraded Redis, for instance, announces that rate-limit buckets just fell back to process-local counters. Set HEALTH_DETAIL_TOKEN and send it as X-Health-Token to get the map back; unset (the default) withholds it from everyone.
  • Base images are pinned by digest, and the README carries an OpenSSF Scorecard badge.

CI

Two blocking gates were added for things the existing ones structurally cannot catch:

  • Invariantssemgrep --error over hand-written rules for the architectural invariants no public ruleset knows: unscoped Qdrant queries, a redirect-following HTTP client outside the one tool allowed to follow one, a credential reaching a logger, raw SQL outside Alembic. A second step re-runs the rules against fixtures and asserts the exact per-rule hit count, so a rule that silently stops matching fails the build instead of reporting zero findings and looking clean.
  • Smoke — boots the built backend image against the four compose services, runs alembic upgrade head from inside it, polls /health/ready until Postgres, Mongo, Qdrant and Redis all answer, and asserts a public route responds. pytest runs over an ASGI transport with fixtures in place of real servers, so a missing runtime dependency, a broken migration chain or an import-time settings failure passes every other gate and would otherwise surface first on deploy.

Alembic migrations are also diffed against the models on a real Postgres, and the frontend gained a vitest unit suite wired into CI. Backend coverage is reported in the log and deliberately not gated on a threshold.

Operations

  • MongoDB's dev host port moved from 27017 to 27018. A native MongoDB service (Homebrew's mongodb-community, the Windows service) already owns 27017 on plenty of machines, and it binds 127.0.0.1 specifically while Docker's proxy binds the wildcard — a loopback-specific bind wins, so localhost:27017 silently reached the native server. Those installs default to authorization enabled, which surfaced as an unrelated-looking "Command update requires authentication" from the seed script. If your .env pins MONGODB_URL to port 27017, update it.
  • Qdrant gained a healthcheck, so the backend waits on service_healthy rather than starting against a vector store that cannot answer yet.
  • The remaining uncapped production containers (the one-shots and Caddy) have memory limits. An unbounded container that grows gets resolved by the kernel's OOM killer, which is free to pick a long-running service instead of the offender.

Interface and fixes

  • A status-aware notification bell in the top bar surfaces task outcomes and can be dismissed.
  • Locally available Ollama models are annotated and suggested in the per-role model picker.
  • JetBrains Mono and Space Grotesk are self-hosted instead of fetched from Google Fonts.
  • Trace spans render human-readable labels and formatted timestamps; the architect graph uses compact status icons.
  • The config panel no longer auto-opens on remount when a task is already running.
  • Best-effort RAG failures are logged instead of swallowed silently.
  • Editing a custom API tool from a credential-free auth mode to a credentialed one now requires a secret; a failed tool delete surfaces instead of throwing an unhandled rejection.
  • Muted foreground colour lightened for contrast.

Full changelog: v0.1.1...v0.1.2

Maestro v0.1.1 — two-command trial

Choose a tag to compare

@Yigtwxx Yigtwxx released this 26 Jul 14:57

A maintenance release focused on how long it takes to get from finding Maestro to seeing it run, plus a dependency sweep.

Try it in two commands

git clone https://github.com/Yigtwxx/Maestro.git && cd Maestro
docker compose -f docker-compose.quickstart.yml up -d

Then open http://localhost:8080. This runs the published images, so nothing compiles locally and there is nothing to configure. Set MAESTRO_PORT if 8080 is taken on your machine — it moves the proxy and the generated links together.

Embedding models are pulled automatically, so RAG and document upload need no key. Chat runs on whichever provider you connect, and Gemini's free tier needs no credit card. For chat locally too:

docker compose -f docker-compose.quickstart.yml exec ollama ollama pull qwen3.5:9b
docker compose -f docker-compose.quickstart.yml restart backend

One thing that would otherwise catch you out: task start is gated on an active plan and the bundled payment gateway is a mock, so a fresh account gets HTTP 402. In a local trial, make your own account unmetered rather than pretending to pay:

docker compose -f docker-compose.quickstart.yml exec backend \
  python -m app.scripts.grant_admin --email you@example.com

The quickstart file uses fixed, publicly documented credentials and binds only to 127.0.0.1. It is a trial environment, not a deployment — production still goes through docker-compose.prod.yml and your own .env.prod.

Dependencies

Fifteen advisories against the frontend lockfile are closed — 13 high, 1 moderate, 1 low, from five roots: postcss and sharp under Next, fast-uri via ajv under the Sentry webpack plugin, brace-expansion through the eslint chain, and @eslint/plugin-kit. No direct dependency had shipped a release carrying the fixes, so each is pinned through an override at the lowest patched version inside the same major, leaving every parent's declared range satisfied.

minimatch is overridden as well, and not because it is vulnerable: the eslint config-array chain still resolves minimatch 3.x, whose only compatible brace-expansion line has no patched release at all, and forcing the fixed major underneath it throws on every lint run. npm audit now reports zero.

Documentation

The README was a 757-line manual. Configuration and the endpoint list moved to docs/CONFIGURATION.md and docs/API.md, and a positioning section now says plainly where CrewAI, LangGraph or n8n are the better choice.

Fixes

  • The quickstart image pin now moves with the release. It previously referenced a tag built before the quickstart existed, which worked by accident and would have drifted silently on the next backend change.
  • The deploy workflow is gated behind a DEPLOY_ENABLED repository variable. It triggers on release tags, and with no host configured it could only fail — leaving a red run against a published release for a rollout nobody requested.

Full changelog: v0.1.0...v0.1.1

Maestro v0.1.0

Choose a tag to compare

@Yigtwxx Yigtwxx released this 26 Jul 12:05
45e3c0b

Maestro is a bring-your-own-key AI agent orchestration platform. One prompt goes in, an Orchestrator classifies the domain, a Main Agent decomposes the work, subagents execute atomic subtasks, an optional Reviewer enforces quality, and a synthesized answer comes out.

This is the first tagged release. Everything described below runs today from docker compose up, including a fully local, zero-cost path on Ollama.

Agent runtime

  • Four-layer hierarchy. Orchestrator routes, Main Agent plans, subagents execute, Reviewer validates. Every contract between layers is structured JSON, never free text, and every loop carries an explicit bound (max_iterations, max_review_iterations, task_timeout_seconds).
  • 15 built-in domain squads — software, finance, marketing, seo, searching, research, data, content, legal, education, social, community, opensource, local, general. Each ships a fixed specialist team; the Main Agent briefs the relevant members rather than inventing new ones. Effort scaling decides how many members actually run.
  • Durable execution. Task state lives in PostgreSQL as checkpoints with leases, heartbeats and a reconciliation sweep, so a crashed worker resumes or finalizes instead of leaving a task stuck in running. Cancellation, human-in-the-loop questions and multi-worker coordination go over a Redis event bus.
  • Quality controls. Deterministic pre-review validators, weighted review_criteria with hard_fail gates, hierarchical token budgets per wave and per call, context compaction, and partial failure surfaced as completed_with_warnings with a stated gap list rather than a silent pass.
  • Failure honesty. A blank subagent answer is a failure, not a success. A fruitless web search self-heals through a bounded query ladder invisible to the tool budget. A hallucinated GitHub repo slug re-resolves through an anonymous probe and a search fallback, and the answer states which repository was actually read.

Bring your own key

  • 67 providers — 25 chat brains and 42 service integrations. Every base URL was probed live before it shipped.
  • Keys are encrypted with AES-256-GCM under a master key held only in the environment, and are never returned to the frontend; only provider and label are.
  • Per-role model routing, so planning, execution, review and synthesis can run on different models behind one token counter.
  • Cost accounting for 24 priced providers, surfaced per task and per trace span.

Tools

Seven executable tools: web_search, data_fetch, repo_intel, social_search, community_read, places_intel, and code_execution (off by default; enabling it requires mounting the Docker socket).

data_fetch runs on Scrapling with curl_cffi TLS impersonation and optional CSS-selector extraction. Every user- or model-supplied URL passes an SSRF guard, redirects are refused before the request when they point at an internal address, and fetched content is delimited, marked untrusted and injection-scanned before a model sees it.

A missing service key degrades rather than stopping the task: the tool is withheld, the squad falls back to web_search, and a mandatory data-coverage section states what could not be reached. repo_intel works with no key at all against GitHub's anonymous read quota.

Knowledge, marketplace, accounts

  • RAG memory — per-user conversation embeddings and document chunks in Qdrant, retrieved at task start and injected into agent prompts. Memory is scoped per user and never crosses accounts.
  • Marketplace — publish agent teams behind a mandatory security scan, one-click install, ratings and reviews, install trends, and a report queue backed by admin moderation with an audit log.
  • Accounts and security — JWT with refresh-token rotation and reuse detection, TOTP 2FA with Argon2-hashed single-use recovery codes, session listing and revocation, email verification and password reset, and an explicit rate limit on every single route including WebSockets.
  • Data rights — GDPR Article 17 erasure with a 30-day recovery window and a purge that clears MongoDB and Qdrant before the PostgreSQL row, plus Article 20 export. Legal pages ship for both GDPR and KVKK.

Billing and quota

Three plans — starter $5, pro $15, scale $50 per month, at 500K / 3M / 10M tokens. Quota is enforced solely through an append-only PostgreSQL ledger, written in the task's finally block on every terminal path including timeout and cancellation.

Only the mock payment provider is included in this repository. A real processor is one adapter file behind the existing PaymentProvider protocol.

Observability

First-party tracing with OpenTelemetry gen_ai.* attributes and per-span cost, a span waterfall in the UI, Sentry on both frontend and backend (fully off with zero egress when the DSN is empty), structured JSON logs with request ids, and /health plus /health/ready probes.

Deployment

Single-origin production topology behind Caddy, which means CORS disappears entirely and the frontend image stays domain-agnostic. Migrations run as a gated one-shot service, so a failed migration leaves the previous backend running. Tagged rollouts pass a health gate with automatic rollback to the previous tag. A backup script covers PostgreSQL, MongoDB and Qdrant with local and offsite retention.

Container images for this release:

ghcr.io/yigtwxx/maestro-backend:0.1.0
ghcr.io/yigtwxx/maestro-frontend:0.1.0

Tagged releases are built for linux/amd64 and linux/arm64.

Verification

Backend: 1013 tests passing, ruff check and ruff format --check clean. Frontend: type-check, lint and build clean. Migration head 0014_default_tracing_enabled. Dependency locks are hash-pinned and regenerated by CI on every pull request, which fails if a lock has drifted from its .in file.

Known limitations

  • A hosted instance cannot reach an Ollama server on a user's own machine, because all LLM calls are made backend-side. Running the whole stack locally is the intended self-hosting path and stays free.
  • Payments are mocked. BILLING_LIVE is false and both /terms and /pricing say so.
  • The social_search, community_read and places_intel response parsers were written from documentation and have not been exercised against their live providers. They are defensive and degrade to "no results" on a shape mismatch, but the field names are unverified. Only repo_intel is verified live.
  • Telegram community reading is structurally partial: the Bot API exposes recently delivered updates, never channel history. The result header says so.
  • google_drive, gmail and shopify are stored-only. They need an OAuth connect flow rather than a single-key paste, so no tool consumes them yet.
  • The deployment pipeline has not been exercised against a production host.

License

Sustainable Use License v1.0. The source is available to read, run and modify for your own use; reselling it as a commercial service to third parties is not permitted. See LICENSE and CONTRIBUTING.md.