Claims & evidence
Every load-bearing claim in the README and on the site should map to one of three things: a gate (a tagged, CI-guarded milestone), a test (a named test or CI job you can read), or an honest "not yet." This page is that map. If a sentence in our marketing can't point at a row here, it shouldn't ship.
Maidan is pre-1.0 and solo-maintained. The tags are the engineering record; there is no marketing "1.0." This page is kept honest by hand — if you find a claim that outruns its evidence, that's a bug: open an issue.
Product gates (tagged, CI-guarded)
| Gate | Tag | What it certifies |
|---|---|---|
maidan-2.0 | v58.0.0 | Core collaboration surface |
maidan-agent-1.0 | v76.0.0 | Agent-facing surface (MCP tools, subscribe) |
maidan-operator-1.0 | v101.0.0 | Operator surface: the web UI, health, metrics, OpenAPI (maidan_operator_gate_e2e) |
maidan-scale-1.0 | v120.0.0 | Scale-out (multi-replica, sharded fan-out, SLOs) |
All four gate tags are cut. Post-120 work ships on the same vX.0.0 ladder as
post-gate hardening (no new gate tag).
Claims → evidence
| Claim (README / site) | Evidence | Status |
|---|---|---|
| "Durable, shared memory: threads, results, artifacts, tool-call transcripts, all searchable" | maidan-store (Postgres + SQLite Store parity, backend_parity test); content-addressed artifacts; thread_results; tool_transcript; full-text (tsvector/FTS5) + semantic (pgvector) search | Shipped |
| "Tasks with dependencies, skill-based claiming, assignment + leases, scheduled runs, blocking waits" | Task-DAG + queue, scheduled/recurring tasks, skill routing, coordination waits (wait_for_ready/wait_for_result) — store tests thread_deps, skill_routing, task_schedules, run_ready_dependents_suite; e2es thread_dependencies_e2e, thread_result_e2e | Shipped |
| "Pull exactly the context a step needs — far fewer tokens" | Thread/workspace context packs (lean edits by default, include_edits opt-in); snippet_only search; capability-filtered tools/list; opt-in lean event frames; omit-empty metadata. Measured: a scoped pack is ~6.8× fewer tokens than dumping the whole channel (token_pack harness → Benchmark.md) | Shipped + measured |
| "Access is scoped on every token; private channels enforced on reads, events, and search; privileged actions are audited" | Capability model (every route + tool checks caps); per-channel/thread RBAC — e2es channel_access_e2e, dm_participation_e2e; filtered-ANN search excludes private channels in-query; subscribe-grant enforcement; every successful change made with a Maidan token or session attributed, unless its only record is a best-effort generic audit row and that write fails; 52 named audit actions, authority changes written transactionally (fail-closed) — audit_coverage_e2e | Shipped — see the audit-scope note below, including the changes it does not cover |
| "A member id from another workspace is answered as an id that names no member, and writes nothing; a route or tool that acts only on your own member refuses another member's id" | foreign_member_e2e calls every operation and MCP tool that takes a member id (100+ probes, found from the OpenAPI document and the MCP catalog) with both ids and requires the same status, problem type and detail, or the same JSON-RPC error; 29 named probes must answer 404 not-found / -32004. The store suite member_references (SQLite and Postgres) refuses a foreign member as owner, assignee, mention and DM, and checks that no row or event was written. Self-scoped: follows_rest_e2e (403 on another member's manager digest), a_member_tool_cannot_mutate_another_members_personal_state (MCP -32003 for a member of either workspace). Writes on the other routes are checked only through their answer matching the no-member one | Shipped |
"Queue depth and occupancy, per channel or for the whole workspace, count only the threads the caller may read, never another workspace's; the workspace-wide wait_for_ready wakes by the same read rule" | GET /workspaces/{wid}/queue-depth, GET /workspaces/{wid}/occupancy, MCP get_workspace_queue_depth / get_workspace_occupancy, one query in crates/maidan-store/src/queue_counts.rs filtered by claim_next's rule (thread_access::readable_thread). Store suite workspace_queue_counts (SQLite and Postgres): workspace_counts_sum_the_channels_the_reader_may_read (private channel, DM, the __dm__ channel, a reader from another workspace). crates/maidan-server/tests/workspace_queue_depth_e2e.rs: the_workspace_counts_only_what_the_caller_may_read, two_tenants_count_only_their_own_work (REST and MCP agree; another workspace's id refused with no count), wait_for_ready_in_the_workspace_skips_what_the_caller_may_not_read (live and since_log_id replay). tenant_isolation_e2e probes both routes and both tools | Shipped |
| "Speaks MCP, REST, and WebSocket over one data model and one login" | One AppState/Store; REST (OpenAPI 3.1, openapi_e2e bijection), MCP (JSON-RPC + streamable HTTP), WebSocket subscribe — all bearer-authed | Shipped |
| "MCP-native — an MCP client connects directly and gets typed tools + live notifications" | POST /mcp + streamable HTTP; MCP 2026-07-28 (negotiated, default) with 2024-11-05 fallback; resources/updated; live-verified LangChain + AutoGen recipes (docs/Framework Integrations.md) | Shipped |
| "A pasted token is exchanged for an HttpOnly session with that token's authority; an unsafe session request or socket from another origin is refused" | crates/maidan-server/tests/session_from_token_e2e.rs: a_pasted_token_becomes_a_session_with_that_tokens_authority, a_cross_origin_unsafe_request_on_a_session_is_refused, a_cross_origin_socket_cannot_use_a_session, only_a_bearer_is_exchanged_and_sign_out_ends_the_session. A request with neither Origin nor Sec-Fetch-Site is accepted and is not claimed to be origin-safe. MCP stays bearer-only in token_session; that exclusion is not asserted by the e2e | Shipped; MCP exclusion not yet covered by session_from_token_e2e |
"Send-time secret:// substitution on webhook, automation HTTP and A2A push is https only, for a host the workspace lists, and leaves any other reference literal" | crates/maidan-server/tests/secret_broker_e2e.rs: broker_substitutes_only_for_hosts_the_workspace_trusts, an_http_target_keeps_the_literal_ref, the_instance_ceiling_bounds_what_a_workspace_can_trust, one_workspaces_secret_never_reaches_another_workspaces_egress. A host outside the ceiling is 400 only when added; at send time the ref stays literal | Shipped |
"MCP distinguishes JSON-RPC parse errors (-32700) from invalid requests (-32600), including invalid batch items, and uses a null error id" | crates/maidan-mcp/src/protocol.rs test json_that_is_not_a_request_is_an_invalid_request_and_bytes_that_are_not_json_a_parse_error; crates/maidan-server/tests/mcp_e2e.rs test json_that_is_not_a_request_is_an_invalid_request_not_a_parse_error | Shipped |
"MCP stdio runs a valid notification (a request object with no id) with no response line, and emits subscribed resource updates after the handled request; malformed JSON and an invalid request get an error response" | crates/maidan-mcp/src/stdio.rs tests, both reading what serve_lines writes: a_notification_on_stdio_gets_no_response (notifications get no line, malformed JSON -32700 and an invalid request -32600 with a null id) and a_subscribed_update_is_written_after_the_response_that_caused_it | Shipped |
| "Single static binary, laptop SQLite → multi-replica Postgres cluster" | One binary selected by DATABASE_URL; scale-out smoke required CI job; workspace-sharded fan-out; LSN causal read-replica routing (read_routing e2e vs real streaming replication) | Shipped (maidan-scale-1.0) |
| "Built to be run, not just demoed — probes, Prometheus, OTLP, durable event log + replay, cross-replica correctness" | /health/{live,ready}; /metrics; otlp smoke + promtool (alert rules) required CI; transactional outbox (events commit atomically with their domain write); leased outbox claim so N replicas relay each row once (concurrent_relays_claim_disjoint_outbox_rows); self-healing NOTIFY floor (notify_floor::sim unit test, 400 seeds by default, replayed with MAIDAN_SIM_SEED) | Shipped |
| "Signed release artifacts" | Keyless cosign bundles on every release and cosign signatures on every image digest (release.yml); per-arch tarballs SHA-256-pinned in the quickstart image. A CycloneDX SBOM per image, attested to its digest and published beside the tarballs, starts with the first tag after v412.0.0: no earlier release has one, because the old SBOM step never produced a file. Verify: see SECURITY.md | Signatures shipped; SBOMs from the next tag |
| "A2A transport" | A2A v1.0 over JSON-RPC, REST §11 and gRPC §10, all complete. The gRPC binding (opt-in) serves the official lf.a2a.v1.A2AService from the unmodified v1.0.1 a2a.proto, every operation over the same handlers as the other two. Agent Card §4.4.1. The official A2A TCK runs over all three bindings in the non-required a2a tck CI job; exclusions are listed in scripts/a2a-tck/exclusions.txt | Shipped (all three bindings) |
| "An installed app can be authorized with a one-time code exchange, with optional PKCE" | crates/maidan-server/src/app_oauth.rs (POST /workspaces/{wid}/apps/{app_id}/oauth/authorize, POST /oauth/app/token). crates/maidan-server/tests/app_oauth_e2e.rs: oauth_authorize_then_exchange_is_single_use, oauth_pkce_requires_matching_verifier | Shipped. The OAuth 2.1 authorization server has its own row |
| "Maidan is an OAuth 2.1 authorization server for pre-registered clients: the authorization-code flow with PKCE S256 only, consent in the console that issues one code per request, refresh tokens that rotate and revoke their lineage on reuse, RFC 7009 revocation, client authentication by HTTP Basic, form secret or none, and tokens bound to the MCP resource that cannot accept an approval gate" | crates/maidan-server/src/oauth/ (GET /oauth/authorize, /ui/oauth/consent, POST /oauth/token, POST /oauth/revoke). crates/maidan-server/tests/approval_gate_e2e.rs: an_oauth_token_cannot_accept_a_gate (authorize, consent and an S256 code exchange, end to end), concurrent_consent_allow_issues_one_code, foreign_consent_post_leaves_request_usable. Refresh: crates/maidan-server/tests/oauth_token_endpoint_e2e.rs: a_refresh_rotates_the_pair_and_the_new_pair_works, reusing_a_refresh_token_revokes_the_whole_lineage, reusing_an_authorization_code_revokes_its_lineage, a_refresh_can_narrow_the_scope_but_never_widen_it, a_refresh_token_stays_with_the_tenant_and_client_it_was_issued_to, and on both backends crates/maidan-store/tests/oauth_refresh.rs: oauth_refresh_rotation_and_lineage_revocation_sqlite, oauth_refresh_rotation_and_lineage_revocation_postgres. Revocation: revocation_ends_an_access_token_or_a_refresh_lineage_and_ignores_other_clients_tokens. Resource binding: an_oauth_access_token_is_accepted_on_the_mcp_resource_and_nowhere_else, a_refresh_must_name_the_resource_its_token_is_bound_to, and crates/maidan-auth/tests/token_lifecycle.rs: an_oauth_token_resolves_only_through_the_resource_resolver. Client authentication and errors: a_confidential_client_authenticates_with_basic_or_post_and_never_both, each_refused_token_request_answers_its_rfc_6749_error, the_authorization_server_endpoints_answer_404_without_a_public_origin. ui-tests/tests/oauth-consent.spec.ts. The resource metadata: oauth_resource_metadata_e2e | Shipped for phases zero to three: discovery, refresh tokens with rotation and lineage revocation on reuse, revocation and resource binding. Client metadata documents and phases four to six (Open Work Next 23) are not yet |
"A result delivered to Slack replies in the thread its target names with thread_ts, and the delivery allowlist is set by audited operator calls" | crates/maidan-server/tests/change_flow_e2e.rs: a_review_result_with_a_slack_thread_ts_replies_in_that_thread. The allowlist route writes its audit row in the change's own transaction, enforced by crates/maidan-server/tests/authority_audit_contract.rs, which refuses the unaudited .allow_egress_target( and .revoke_egress_target( in request code | Shipped (#1245) |
With MAIDAN_PUBLIC_ORIGIN set, the MCP endpoint serves its RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource/mcp/streamable naming the authorization server, the server serves its RFC 8414 metadata at /.well-known/oauth-authorization-server listing only what exists (the authorization-code and refresh-token grants, S256, the three client authentication methods, and the authorize, token and revocation endpoints), the authorization response carries iss (RFC 9207), and a 401 from /mcp/streamable points at the resource document; nothing is served or pointed at without it. The origin must be https, or http on a loopback host, with no path, query or credentials, or boot refuses it | crates/maidan-server/src/oauth/metadata.rs. crates/maidan-server/tests/oauth_resource_metadata_e2e.rs: without_a_public_origin_no_oauth_metadata_is_served_or_pointed_at, with_a_public_origin_the_mcp_resource_describes_itself_and_its_401_points_there (no root-form document, no challenge on /mcp or a non-MCP 401), with_a_public_origin_the_authorization_server_lists_only_the_flows_that_exist. The iss parameter: crates/maidan-server/tests/approval_gate_e2e.rs: the_authorization_response_names_its_issuer_when_a_public_origin_is_set (without an origin the authorization server is off: the_authorization_server_endpoints_answer_404_without_a_public_origin). Unit tests a_public_origin_is_an_https_scheme_and_host_or_loopback_http, the_resource_document_names_the_mcp_endpoint_and_its_authorization_server, the_authorization_server_document_lists_only_what_the_server_does, the_issuer_parameter_names_the_configured_origin_and_nothing_when_unset | Shipped, config-gated |
| A member deactivated or deprovisioned through SCIM loses everything it held in the workspace in the same transaction: its own tokens, the delegation grants it holds as delegate (so tokens exchanged under them stop working), and its browser sessions, each audited with the reason. It cannot sign in through OIDC afterwards | crates/maidan-store/src/{postgres,sqlite}/scim_audited.rs (end_member_authority). crates/maidan-store/tests/scim_audit_tx.rs: ending_a_member_ends_its_delegate_grants_and_sessions_{sqlite,postgres}. crates/maidan-server/tests/deactivated_delegate_token_e2e.rs: a_deactivated_delegates_token_is_refused_only_in_its_workspace, a_deprovisioned_delegate_loses_its_delegated_token_and_its_sessions. Sign-in: oidc_loopback_e2e | Shipped |
A stream re-checks its credential while it is open: a revoked or expired token, an ended session, or a member deactivated through SCIM ends the member's SSE, MCP, AG-UI and A2A streams at its next credential re-check (before a frame, at most once per RECHECK_GAP, so a frame can go out between a revocation and that check), and an idle one within the re-check tick. Other members' streams are untouched | crates/maidan-server/src/stream_guard.rs (re-check before a frame, at most every RECHECK_GAP, and every RECHECK_TICK while idle), used by the MCP, AG-UI and A2A JSON-RPC, REST and gRPC streams. crates/maidan-server/tests/sse_deactivated_member_e2e.rs: deactivating_alice_in_a_ends_only_her_streams_at_their_next_frame, idle_streams_of_every_kind_end_within_the_recheck_tick, a_live_token_whose_scim_link_is_inactive_ends_too, deactivating_alice_ends_her_a2a_subscriptions_on_every_binding_and_nobody_elses (JSON-RPC and REST end with stream_ended; gRPC ends with an UNAUTHENTICATED status) | Shipped for SSE, MCP, AG-UI and the A2A SubscribeToTask streams on all three bindings |
An approval gate is accepted only with a browser session a person signed in to (sent from the console page), or a token holding approval:grant; a plain human bearer token, or a session made from one, cannot accept; nobody accepts their own request | crates/maidan-server/src/routes/approval_gate.rs (acceptance_credential, answer_approval_gate), crates/maidan-server/tests/approval_gate_e2e.rs (a_plain_human_bearer_token_cannot_accept_a_gate, a_session_made_from_a_token_accepts_only_with_the_tokens_approval_grant, a_signed_in_session_accepts_from_the_console_page_and_not_without_it, no_credential_lets_the_requester_accept_its_own_gate, another_workspaces_session_or_approval_grant_cannot_see_or_accept_a_gate, an_agent_cannot_accept_another_agents_gate_without_approval_grant), crates/maidan-server/tests/attestation_e2e.rs (no_one_accepts_their_own_approval_request, a_delegate_cannot_accept_a_request_it_made_under_another_identity) | Shipped |
| "Off-platform reach: notifications, email, Slack, GitHub" | Per-recipient notification ledger + router + unified inbox; SMTP transport + durable mail retry queue (outbox + worker + DLQ); Slack + GitHub projectors (bidirectional, loop-safe) | Shipped, config-gated — inert until you set MAIDAN_SMTP_* / MAIDAN_SLACK_* / MAIDAN_GITHUB_* and create the apps |
| "The change flow commits a coding result to a branch and opens a draft pull request" | crates/maidan-server/src/change_flow.rs (guards, branch write, draft PR) + change_patch.rs (diff apply). crates/maidan-server/tests/change_flow_e2e.rs: a_change_creates_the_branch_commits_once_and_opens_one_draft_pr, plus the refusals (a_prod_base_is_refused_even_when_an_operator_tries_to_bless_it, a_protected_branch_is_never_written, a_branch_without_the_agent_prefix_is_refused, a_base_outside_the_repos_allowlist_is_refused, the_token_reaches_no_delivery_record_or_audit_row_when_github_fails) | Shipped, config-gated — inert until MAIDAN_GITHUB_TOKEN is set and an operator allows a github_branch target (owner/name@base) |
| "A result comments on its Linear issue and can move it to a workflow state" | crates/maidan-server/src/linear.rs (GraphQL sender) + egress_worker.rs (linear_result: team check, edit in place, state move). crates/maidan-server/tests/change_flow_e2e.rs: a_change_comments_on_its_linear_issue_with_the_draft_pr_and_moves_it, an_issue_outside_the_named_team_is_refused_before_any_write, one_workspace_cannot_comment_through_another_workspaces_linear_team, a_new_result_edits_the_linear_comment_and_a_deleted_one_is_posted_again | Shipped, config-gated: inert until MAIDAN_LINEAR_API_KEY is set and an operator allows a linear target (a team id). Not yet run against Linear itself |
| "Outbound GitHub calls can authenticate as a GitHub App" | crates/maidan-server/src/github_app.rs (RS256 JWT through ring, the installation-token exchange, the cache with a five-minute margin and one exchange at a time). crates/maidan-server/tests/github_app_auth_e2e.rs runs the real client against a fake GitHub that verifies the JWT's signature and claims: comments_check_runs_and_git_reads_use_one_installation_token, a_token_inside_the_refresh_margin_is_exchanged_again, concurrent_calls_share_one_exchange, a_write_outside_the_listed_repositories_costs_no_exchange, a_refused_exchange_is_a_misconfiguration_and_reaches_no_repository, and the boot refusals | Shipped, config-gated: inert until MAIDAN_GITHUB_APP_ID, MAIDAN_GITHUB_APP_INSTALLATION_ID and MAIDAN_GITHUB_APP_PRIVATE_KEY are set. Not yet run against github.com: the fake follows GitHub's documented exchange |
| "The usage ledger prices every cache tier, budgets fresh tokens, and rolls up spend and cost per completed task" | crates/maidan-types/src/usage_ledger.rs unit tests: price_snapshot_uses_both_write_tiers_and_rounds_up, fresh_tokens_omit_cache_reads, anthropic_attributes_keep_input_uncached_and_split_writes, openai_attributes_subtract_cached_tokens_from_input, deepseek_miss_and_hit_are_uncached_and_read, otel_provider_names_follow_the_short_name_input_rule, vertex_ai_stays_unclassified, genai_report_computes_the_charge_and_lets_explicit_evidence_win, rollup_rates_and_cost_per_completed_task. Store: crates/maidan-store/tests/thread_budget.rs (run_cache_pricing_suite, accounted_usage_is_idempotent_and_claim_fenced, both backends). Budget stop: crates/maidan-server/tests/budget_enforce_e2e.rs (report_usage_over_budget_stops_and_dead_letters). Routes: crates/maidan-server/src/routes/usage.rs, crates/maidan-mcp/src/tools/budget.rs | Shipped for pricing, the fresh-token budget, provider classification and the rollup arithmetic. Not yet: an end-to-end test of the three usage-rollup routes and POST /threads/{id}/usage/otel (only their capability denials are tested, in http_capability_matrix_e2e.rs), a test of the tiered maidan_usage_* Prometheus counters (crates/maidan-server/src/metrics.rs), and a test of the manager digest's spend and cost-per-task line (crates/maidan-server/src/digest.rs) |
| "Each SDK turns an Anthropic, Bedrock Converse, OpenAI Responses or Chat, Gemini, DeepSeek, Mistral, xAI or vLLM response into the ledger's tokens, and agrees with the ledger" | Shared fixtures in sdk/usage-fixtures/: providers/ holds 14 recorded responses, each with its source URL (12 copied from a provider's reference or a public report, 2 transcribed from an AWS SDK printout); constructed/ holds 5 cases whose numbers are chosen, for fields no published response shows; plus 2 refused shapes and 4 charge cases. Read by sdk/typescript/usage.test.mjs, sdk/python/tests/test_usage.py, sdk/go/usage_test.go, sdk/rust/tests/usage.rs, and by the ledger in crates/maidan-types/tests/sdk_usage_fixtures.rs (the_ledger_reads_every_provider_fixture_as_the_sdks_must, the_ledger_reads_every_constructed_case_as_the_sdks_must, the_ledger_refuses_every_invalid_fixture_as_the_sdks_must, the_ledger_prices_every_charge_fixture_as_the_sdks_must). Each suite fails when a provider has no recorded fixture, or when a file in providers/ is not marked recorded | Shipped. The ledger test runs in the required unit tests job; the SDK suites run in the report-only sdk interop job. Bedrock's cacheDetails TTL split, Gemini's cachedContentTokenCount, a vLLM cache hit, an OpenAI Chat cache write and a DeepSeek cache hit are tested only by constructed cases; xAI reasoning is read from a reference example, not a stated rule |
| "Client SDKs" | Four clients (TypeScript, Python, Go, Rust) at 0.3.0 in sdk/, to the frozen v1 contract, with typed responses, an error per problem type, retries with idempotency keys and paging. Each is black-box-verified (scripts/sdk-test.sh), plus a report-only sdk interop CI job | Shipped in the tree at 0.3.0. Not yet on a registry: the sdk-*-v0.3.0 tags wait for the maintainer's go |
| The published server image has no HTTP bootstrap routes | crates/maidan-server/Dockerfile defaults MAIDAN_ENABLE_BOOTSTRAP to 0 and then builds --no-default-features. .github/workflows/release.yml (build + push maidan-server) passes only MAIDAN_VERSION, so the published image keeps that default. CI job bootstrap compile-time strip fails a default build that still compiles the routes in | Shipped |
| An admin connects an agent (an agent member and its worker token) on the published image, without the bootstrap routes | POST /workspaces/{wid}/agents is mounted outside the bootstrap feature (crates/maidan-server/src/app.rs). crates/maidan-server/tests/workspace_agents_e2e.rs runs with bootstrap off: the bootstrap member route is refused, the agent route answers 201 with a maidan.agent.worker token that works, refusals create nothing, and another tenant's admin cannot create one. crates/maidan-store/tests/agent_with_token.rs (SQLite and Postgres): the member, token and audit rows roll back together | Shipped |
| A production Helm render refuses a development image, and placeholders only where the chart looks | scripts/helm-template-smoke.sh. A production render refuses image.repository: maidan-server, a dev/latest/empty tag without image.digest, and, unless existingSecret is set, an unset or development DATABASE_URL or an empty secrets value. config, image.tag and image.digest holding CHANGE_ME fail every render; secrets and contentKek holding it fail only when existingSecret is unset. existingSecret skips those checks and does not prove the Secret exists or holds its keys | Shipped |
A production maidan-stack render refuses development store passwords and unpinned store images, and a stack with both stores installs and serves /health/ready | scripts/helm-template-smoke.sh renders helm/maidan-stack with production values and requires a refusal for an empty or development Postgres or MinIO password and for a Postgres or MinIO image without a tag or digest (the MinIO user, password and bucket checks apply while minio.enabled is true); the helm install (kind) job installs the stack with both stores (values-ci.yaml) and waits for /health/ready; scripts/check-deploy-contract.sh requires one full-digest MinIO pin across the stack, compose and k8s/. The stack's stores are single-replica with no backups; moving off the old Bitnami subcharts is a replacement (docs/Production.md) | Shipped |
Behavior the reference docs state
These are not README slogans. They are sentences in the reference docs, and each maps the same way: a test, the code that does it, or an honest gap.
| Claim | Evidence | Status |
|---|---|---|
With OIDC off, MAIDAN_SUBSCRIBE_RESUME_SECRET lets the server start without MAIDAN_SESSION_SECRET | main.rs calls subscribe_resume::secret_from_env() only when OIDC is off. That function uses MAIDAN_SUBSCRIBE_RESUME_SECRET when set, otherwise the session secret. With authentication off and neither secret set, startup uses the built-in test secret. OIDC still refuses to start without MAIDAN_SESSION_SECRET (oidc::OidcSettings::from_env) | Shipped — no test covers the env choice |
Maidan-Room-LSN is the caller's workspace head; a narrower tap waits on its own shape's head | the_header_reports_the_callers_room_not_the_instance (room_lsn_scope_e2e) stamps the caller's room, not the instance. A tap compares history to the head it was given (history_caught_up, live_waits_for_workspace_head_not_global_room). For a channel, thread or kind filter that head is the shape's own high-water, not the workspace header. The server does not publish a second header for the shape | Shipped |
| A lapsed lease returns within a reaper tick (5 s by default), and one replica frees at most 1,000 claims per tick | Tick default: the_reaper_is_on_by_default_and_zero_turns_it_off. The store sweep stops at the limit it is given and leaves the rest (a_batch_is_bounded_and_takes_the_oldest_deadline_first in reap_expired_claims_sqlite / _postgres). The replica cap is MAX_PER_TICK (1,000) in claim_reaper.rs | Shipped — no test fills the 1,000 cap |
assign_thread and claim_thread carry no lease, and assign_thread ends the previous hold | a_new_holder_never_inherits_a_lease_deadline_sqlite / _postgres: releasing and then calling claim_thread or assign_thread, or assigning over a live lease, leaves the new holder no deadline, and it keeps the thread past the old one. reap_expired_claims_* leaves a claim with no lease alone | Shipped |
| Review and land-gate history starts at migration 0121; verdicts overwritten before it were not kept | decision_history_keeps_every_verdict_sqlite / _postgres drops the history tables, re-runs 0121, and checks the backfill of the rows that existed then | Shipped |
| A member holds one verdict on a message and can take a vote back | crates/maidan-server/tests/vote_integrity_e2e.rs: a_member_holds_one_verdict_and_can_take_it_back (with auth on, a second verdict replaces the first, an ack stands beside it, another member's and another workspace's retract remove nothing, and both removals are vote_retracted events). The store roundtrip in tests/common/mod.rs runs the same rule on both backends. Migration 0143 adds the unique index | Tested |
| A change request that does not send work back still notifies the last worker | every_review_verdict_appends_review_submitted_with_the_last_worker_sqlite / _postgres records the worker on a change request that sends nothing back. The notification router notifies ReviewSubmitted / RequestChanges whenever that worker is set; it does not look at whether the thread was sent back | Shipped — no test asserts the inbox row for the non-send-back case |
A tombstone leaves ciphertext in the database, its exports and every peer that ingests it. A pre-shred backup read with MAIDAN_CONTENT_KEK, and a peer that never ingests the tombstone, still have the words | withdrawn_words_are_unreadable_on_every_surface and peers_get_ciphertext_only_for_shredded_words (crypto_shredding_e2e). The two surviving copies are Threat Model T7; neither is exercised by a test | Shipped for the shred; the two copies are the threat model, not a test |
Every secret the server reads can come from a mounted file (<NAME>_FILE) | crates/maidan-env/src/secret_files.rs (SECRET_FILE_ENV, resolved once at the start of main by the server and the CLI) and crates/maidan-env/src/lib.rs: every_secret_the_server_reads_can_come_from_a_file (every secret-shaped server variable and the non-MAIDAN_ secrets have a _FILE form, apart from a reviewed list of names that only look secret). every_secret_file_the_chart_mounts_is_one_the_server_reads keeps the Helm chart's list equal to the server's. scripts/helm-template-smoke.sh (the lint job) checks that the chart and the stack mount their secrets as files with no secret in envFrom, and every new refusal. The helm install (kind) job installs the stack with its secrets as files | Tested |
An MCP 2026-07-28 request is held to its revision's _meta, routing-header and removed-method rules, and an earlier revision's request is unchanged | crates/maidan-server/src/mcp.rs: era_tests (era detection, every refusal with its code and status and the request's id, each removed method refused on 2026-07-28 and admitted on 2025-11-25, legacy routing headers optional). scripts/mcp-conformance.sh (report-only CI) passes server-stateless and http-header-validation apart from fixture checks. Checked by hand on 2026-10-07 with Claude Code, which sends 2026-07-28 requests, and the MCP Inspector, which sends 2025-11-25 | Tested |
| Claude Code discovers Maidan's tools, searches, posts, and searches then posts in that order. Claude Desktop discovers and searches | Run by hand on 2026-10-07 against a local Maidan at 42d56c5a with auth on and a seeded workspace. Claude Code (Streamable HTTP, 2026-07-28) passed all four prompts, and Claude Desktop (stdio, the earlier handshake) passed discovery and search before the run stopped. Each tool call was matched in the server's maidan_mcp::request log. Not in CI, and no test re-runs it | Verified by hand (not in CI) |
maidan mcp-stdio writes nothing but JSON-RPC messages to stdout | crates/maidan-cli/tests/stdio_smoke.rs: every_line_on_stdout_is_a_json_rpc_message (logging at info, an initialize, a tools/list and a tools/call, every stdout line parsed as JSON-RPC, and the request log found on stderr) | Tested |
| An MCP search hit names its channel, thread and author | crates/maidan-server/tests/search_e2e.rs: mcp_search_messages_tool_works asserts channel_name, thread_title and author_handle on every hit | Tested |
| A change request says what to change | crates/maidan-mcp/src/server.rs: submit_review_tool_publishes_review_submitted (a blank note on request_changes refused, a plain approve accepted). maidan_types::review_note is the one rule both the MCP tool and the REST route apply | Tested |
| Needs you lists a task blocked on a person to its owner, with Unblock, and never to another workspace | ui-tests/tests/needs-you-blocked.spec.ts (Unblock clears the block on the server, a refused or dropped Unblock keeps the row, the row holds through a refused load and a dropped connection, and a second workspace's blocked task is not shown). crates/maidan-server/tests/ui_js_contract.rs holds blocked in NY_KINDS | Tested (the browser job is not a required check) |
A review note stays in the review history and out of the review_submitted event | crates/maidan-server/tests/review_signals_e2e.rs: a_change_request_notifies_the_last_worker_and_stays_in_its_workspace asserts the event payload carries no note, and request_changes_e2e reads the note back from change_requests | Tested |
Maidan answers tasks/* as an unknown method, since it does not implement the optional tasks extension | scripts/mcp-conformance/2026-07-28.yml lists each of the suite's tasks checks under that reason, and the run fails when a listed check starts passing | Checked by the report-only mcp conformance job |
A cluster-connected Helm upgrade restarts the stack's MinIO when its root user or password changes; an offline helm template render does not | helm/maidan-stack/templates/minio.yaml sets the pod annotation checksum/rollout-nonce to the SHA-256 of Secret key rollout-nonce. While Helm is connected, an upgrade reuses that nonce when root-user and root-password are unchanged and replaces it when they change. helm template has no Secret, so the nonce is the fixed value stable. scripts/helm-template-smoke.sh requires the annotation to match that hash, requires two offline renders to share the nonce stable, requires a different password to leave the nonce unchanged, and requires the render not to contain a hash of the root user or password. No test applies the chart to a cluster and changes the credentials | Shipped — the live Secret lookup is not covered by the smoke |
| A dev instance lets an MCP client with no credential read one synthetic workspace and nothing else | crates/maidan-server/tests/dev_anonymous_mcp_e2e.rs: a_caller_with_no_credential_reads_the_synthetic_workspace_and_nothing_else (read-only tools marked noauth on /mcp and /mcp/streamable, the synthetic workspace read, another workspace not, a write refused even when it asks only workspace:read, a subscription refused, nothing created), only_an_mcp_post_is_open_and_never_a_session (listener and REST still 401, no session on a 2024-11-05 initialize), a_bearer_caller_is_unchanged, only_a_workspace_named_synthetic_is_read_without_a_credential, the_anonymous_reader_is_no_member_and_sees_no_private_channel. The production and AUTH_DISABLED refusals are dev_anonymous::tests | Tested |
| A request with no credential cannot reach an open mode through a rebound host name | crates/maidan-server/src/rebinding.rs: a_rebound_name_is_refused_however_the_server_is_bound, a_listed_host_is_this_server, another_origin_is_refused_wherever_the_server_is (a dotted host outside MAIDAN_ALLOWED_HOSTS and a foreign Origin are refused under AUTH_DISABLED and the anonymous reader, however the server is bound). crates/maidan-server/tests/dev_anonymous_mcp_e2e.rs runs the anonymous reader with dev.example.com listed | Tested |
The official MCP conformance suite runs against Maidan for 2025-11-25 and 2026-07-28, and every failing check has a stated reason | scripts/mcp-conformance.sh with the per-check baselines in scripts/mcp-conformance/, run by the report-only mcp conformance CI job. On 2026-10-07 it passed 36 of 61 checks for 2025-11-25 and 78 of 162 for 2026-07-28. The failures then were fixture scenarios, undeclared features, the anonymous harness, and real gaps since closed by #1289 (DNS rebinding) and #1302 (the 2026-07-28 rules). Later on 2026-10-07, with both, it passed 37 of 61 and 102 of 167, and every remaining failure is listed with its reason in the baselines. ping is held by mcp_e2e | Shipped (report-only, not a required check) |
| The stack refuses to change MinIO persistence on an existing StatefulSet | A cluster-connected upgrade looks up the MinIO StatefulSet and fails when minio.persistence.enabled, size or storageClass would add, remove or edit volumeClaimTemplates (helm/maidan-stack/templates/minio.yaml). scripts/helm-template-smoke.sh still renders offline with another size and with persistence off, because helm template has no live StatefulSet. The replacement steps are in helm/maidan-stack/README.md and docs/Production.md. No test drives the live refusal | Shipped — the refusal is not covered by the smoke |
server/discover returns the supported revisions, capabilities, instructions and serverInfo with no handshake, on POST /mcp, POST /mcp/streamable and stdio | server_discover_returns_the_instructions_capabilities_and_versions (crates/maidan-mcp/tests/cache_hints_contract.rs) checks the instructions, the capabilities, supportedVersions, and serverInfo at _meta["io.modelcontextprotocol/serverInfo"], in-process through McpServer::handle. server_discover_answers_a_cold_2026_request_on_both_posts (crates/maidan-server/tests/mcp_streamable_e2e.rs) sends that cold request to POST /mcp and POST /mcp/streamable and checks the instructions and supportedVersions; it does not check serverInfo. Stdio dispatches through the same handle_in (crates/maidan-mcp/src/stdio.rs); no stdio test sends server/discover | Shipped on both POSTs; stdio discover is not covered by a test |
| "The SDKs place Maidan's boot prefix with a cache breakpoint, derive cache keys that never cross workspaces, and pass the thread id as a gateway session id" | sdk/cache-fixtures/cases.json, read by sdk/typescript/cache.test.mjs, sdk/python/tests/test_cache.py, sdk/go/cache_test.go and sdk/rust/tests/cache.rs (the same breakpoint, key, key placement and gateway field in every language; two workspaces with one group give two keys). channels.boot against a live server: channels.boot returns the served boot bytes and their sha256 (TypeScript), test_channels_boot_returns_the_served_boot_bytes_and_their_sha256 (Python), TestChannelsBootReturnsTheServedBootBytesAndTheirSHA256 (Go), channels_boot_returns_the_served_boot_bytes_and_their_sha256 (Rust). Two workspaces: channels.boot from a second workspace sees nothing of the first (TypeScript), test_channels_boot_from_a_second_workspace_sees_nothing_of_the_first (Python), TestChannelsBootFromASecondWorkspaceSeesNothingOfTheFirst (Go) and channels_boot_from_a_second_workspace_sees_nothing_of_the_first (Rust) boot the first workspace's channel with the second's token (refused, naming neither the workspace nor the boot hash) and boot the second's own channel (naming only the second); scripts/sdk-test.sh provisions the second workspace through POST /operator/workspaces | Shipped in the SDK suites, which run in the report-only sdk interop job. Whether a provider then caches the prefix is not tested: no test calls a provider, and a prefix below a model's minimum is not cached |
Every result of the six cacheable operations (server/discover, tools/list, prompts/list, resources/list, resources/templates/list, resources/read) carries a ttlMs and a cacheScope, and public is only a result that is the same bytes for every caller | every_cacheable_result_carries_a_ttl_and_a_cache_scope (cache_hints_contract) requires ttlMs and public or private on those five methods and on resources/read of a workspace, a channel, a thread and an artifact, with each read's ttlMs equal to caching::resource_read and cacheScope private. an_artifact_read_uses_the_record_ttl_and_a_thread_read_is_stale requires an artifact read at RECORD_TTL_MS (60000) and a thread read at 0. the_documented_cache_hints_are_the_servers requires each row of the Protocols cache-hint table to match crates/maidan-mcp/src/caching.rs. a_public_result_is_the_same_bytes_for_two_tenants_and_a_private_one_need_not_be requires a public result to be the same bytes for two tenants with different capabilities. Over HTTP, server_discover_answers_a_cold_2026_request_on_both_posts checks discover's cacheScope (public) and ttlMs on both POSTs, and a_2025_client_negotiates_its_revision_and_is_served_statelessly checks that tools/list has a ttlMs on both POSTs. The other four operations are not asserted on the wire | Shipped — the six hints are checked in-process; HTTP checks discover and tools/list only |
A review that names no reviewer reaches its owner, or, with no owner, the workspace admins, filtered by access and never across workspaces; start_review on a gated thread needs a posted result; a close with no approval reads as closed without review | Store, both backends: run_unassigned_suite, run_result_gate_suite, run_closed_without_review_suite (crates/maidan-store/tests/review_requests.rs). REST and MCP: a_review_nobody_was_named_for_reaches_its_owner_or_an_admin_and_no_one_else (two workspaces and a private channel) and start_review_on_a_gated_thread_needs_a_posted_result (crates/maidan-server/tests/waiting_inbox_e2e.rs). Board: ui-tests/tests/unassigned-review.spec.ts (not a required check). Demo: demo_board_script_hits_the_server | Tested |
| The first-run card signs in on Enter or a Sign in button, exchanging the token once, and sends no channels request and shows no "not accepted" sentence before a credential exists | ui-tests/tests/first-run-sign-in.spec.ts: pasting a token and pressing Enter signs in, the Sign in button signs in, and spends the token once, a workspace id before any token asks the server for nothing and blames no one. The Playwright job is not a required check | Tested (Playwright, not required) |
| A failed Needs you load never reads as an empty queue: a refused load names the fix, an unreachable server marks the rows stale and retries, and neither clears the rows or the tab count | ui-tests/tests/needs-you-truth.spec.ts: a revoked token is told what to fix, and the queue and tab count stay and a dropped connection marks the queue stale, and the next good load clears it; ui_js_needs_you_reports_failed_loads_and_keeps_rows_in_use (crates/maidan-server/tests/ui_js_contract.rs). The Playwright job is not a required check | Tested (Playwright, not required) |
| The MCP worker and reviewer tool profiles serve a sorted list that is the same bytes for every client, and the worker profile refuses at call time a tool the token cannot call | a_profile_list_is_public_sorted_and_the_same_bytes_for_two_tenants and a_profile_refuses_a_tool_the_token_cannot_call_and_one_it_does_not_list (crates/maidan-mcp/tests/tool_profiles_contract.rs, #1240) | Tested |
| The instance's GitHub token writes only to repositories the operator names, and mark-ready answers only the operator-designated app | crates/maidan-server/src/github.rs (every request is built by request_accepting or graphql, which call ensure_writable for anything but a read, held there by every_github_request_is_built_where_the_repository_is_checked), crates/maidan-server/tests/mark_ready_e2e.rs (a_write_to_a_repository_the_operator_did_not_name_is_refused_before_any_request, a_look_alike_app_in_another_workspace_cannot_mark_ready) | Shipped |
| Every write to what a reviewer reads of a thread moves its version, and only an artifact the thread's workspace holds can be linked to it | crates/maidan-store/tests/thread_version.rs on both backends (a post, an edit, a result, a title, a tombstone, a link and an unlink each move it once, the same title and a claim do not, another workspace's hash is not found, and erasing the workspace still works). crates/maidan-server/tests/thread_evidence_e2e.rs over REST and MCP with two workspaces | Tested |
| Every hand-off to review records an immutable packet of what the reviewers were handed | crates/maidan-store/tests/review_packets.rs on both backends (the version recorded, the result's hash and the linked artifacts pinned, the stored root equal to the manifest's, a second hand-off after rework writing a new packet with a new root while earlier ones stay, and a raw UPDATE refused). crates/maidan-server/tests/thread_evidence_e2e.rs: a_hand_off_to_review_pins_what_was_handed_over over REST and MCP, with another workspace refused | Tested |
| An approval counts only for the evidence it was shown | crates/maidan-store/tests/review_packets.rs on both backends (a stale root refused, an unbound approval kept but not counted, a bound one counted, a comment after it leaving it standing, a newly linked artifact refusing both the close and a re-approval under the old root, and an approval of the next hand-off closing). thread_evidence_e2e, review_rest_e2e, attestation_e2e, critical_review_e2e, the MCP review tests and the Playwright Needs you specs approve with the packet's root. scripts/demo-handoff.sh and scripts/demo-board.sh run against a server | Tested |
An agent declares its own status on a thread, a newer declaration supersedes the older one, and stalled cannot be declared | crates/maidan-server/tests/declare_status_e2e.rs (a_declared_status_is_read_back_logged_and_superseded_and_stays_in_its_workspace, a_stranger_cannot_declare_status_on_anothers_thread), crates/maidan-mcp/src/server.rs (status_tools_declare_read_back_and_log_status_declared, mcp_human_dm_reply_clears_the_declared_status) | Shipped (#1273) |
A thread whose agent holds a live lease but has shown no progress (acknowledgement, own message, own declaration) for MAIDAN_STALL_SECS reads back stalled; a renewal is not progress, a declared wait is not stalled, and no workspace sees another's | crates/maidan-store/tests/claim_unacknowledged/thread_progress.rs (both backends), crates/maidan-server/tests/declare_status_e2e/stuck_detection.rs (a_quiet_agent_reads_back_stalled_and_only_in_its_own_workspace), crates/maidan-server/tests/ui_js_contract.rs (ui_js_board_marks_a_stalled_card_and_counts_it_in_the_summary) | Shipped (#1434) |
| An agent's question reaches the thread's owner, or an admin when nobody else can answer, and leaves the queue once a person replies | crates/maidan-server/tests/waiting_inbox_e2e.rs: an_agents_question_reaches_its_owner_or_an_admin_until_a_human_answers (two workspaces, a private thread, the asker, a bystander and the MCP twin). crates/maidan-store/tests/needs_input.rs runs the query on both backends. ui-tests/tests/needs-you-question.spec.ts answers one in the console | Tested |
| A board write sent twice makes one change, and a blank task title is refused | crates/maidan-server/tests/idempotency_keys_e2e.rs (ui_api_writes_are_idempotent), ui-tests/tests/board.spec.ts ("a double click on Add task makes one task"), crates/maidan-server/tests/thread_create_validation_e2e.rs | Shipped (#1274). The double-click check is a Playwright spec, which is not a required check |
| Every MCP tool declares a title and read-only, destructive, idempotent and open-world hints, and each value has a reviewed reason | crates/maidan-mcp/tests/tool_annotations_contract.rs: every_tool_declares_a_title_and_the_four_hints_the_reviewed_table_gives_it (every catalog tool, against tests/fixtures/tool-annotations.json; also refuses read-only and destructive together), the_reviewed_table_names_only_tools_that_exist, every_reviewed_value_has_a_one_line_reason, read_only_hints_agree_with_the_servers_read_only_list (readOnlyHint equals READ_ONLY_TOOLS), and tools_list_serves_the_reviewed_annotations_on_every_endpoint (the full list and both profiles, in-process). The reasons were set by reading each handler; the test checks agreement with the table, not the handler's behaviour | Tested |
| An MCP argument that does not parse gets an error that names it, and every tool group answers a valid call | crates/maidan-mcp/src/tools/mod.rs: parse_args (every tool's argument parsing) and a_malformed_or_missing_argument_is_refused_by_name. scripts/mcp-inspector.sh drives one valid call and one malformed or missing argument per tool group through the official Inspector, in the report-only mcp inspector CI job, and fails when a tool module has no probe A check a tool makes after parsing words its own error and is not covered by this claim | Tested (the per-group run is report-only) |
| Each MCP client in the matrix connects to Maidan the way its recipe says | crates/maidan-server/tests/client_matrix_e2e.rs reads contracts/mcp-clients.json. every_client_connects_the_way_the_matrix_says connects with a bearer header or with no credential against a dev instance, per row, on a real server with auth enabled. every_client_has_a_recipe_a_matrix_row_and_its_release_step and every_client_row_is_well_formed hold docs/Clients.md and the file together. The clients themselves are not run in CI. Their configuration comes from each client's documentation as read on 2026-10-06, and the release check covers the three a person checks by hand | Tested (the server side, the clients by hand) |
| Every MCP tool name fits Claude, OpenAI and Claude Code clients, its leading verb says whether it writes, and the four mixed tools are documented writes | crates/maidan-mcp/tests/tool_annotations_contract.rs: every_tool_name_fits_every_client_and_its_verb_says_whether_it_writes (snake_case, at most 64 characters, at most 64 behind mcp__maidan__, and a leading verb on the fixture's read_verbs exactly when the tool is in READ_ONLY_TOOLS), the_name_rules_refuse_the_names_they_exist_to_refuse (each rule against a name built to break it), and the_mixed_tools_are_documented_writes (each tool under mixed in tests/fixtures/tool-annotations.json is not read-only and its description contains the reviewed words naming its write). A write a model could mistake for a lookup is found by review, and the test keeps the reviewed list honest | Tested |
| Every MCP request writes one operational log line with its tool, declared argument names, principal, latency and outcome, and no argument value, result or undeclared name | crates/maidan-mcp/tests/request_log_e2e.rs: every_request_logs_its_tool_and_argument_names_and_never_a_value (a real subscriber at the default filter, a tool call whose body and thread id never reach the log, and a made-up tool logged as (unknown)). crates/maidan-mcp/src/request_log.rs unit tests cover a credential-shaped method, tool and argument name never logged, every dispatched method logged by name, a tool error outcome, a captured frame hiding secret-shaped keys at any depth and inside JSON result text, and a credential tool's arguments and result withheld | Tested |
| Full MCP frame capture cannot be compiled into a release build | crates/maidan-mcp/src/request_log.rs raises compile_error! when frame-capture meets a build without debug assertions, checked by hand on 2026-10-07 with cargo check -p maidan-mcp --features frame-capture --config 'profile.dev.debug-assertions=false'. CI's lint job compiles the feature in a dev build | Shipped (the release refusal is checked by hand, not in CI) |
Every secret-bearing variable can be read from a mounted <NAME>_FILE; setting both forms or an unreadable file refuses boot, and no value is logged | the_server_boots_on_secrets_read_from_files_and_never_logs_them, a_secret_set_both_ways_refuses_boot_without_printing_it, an_unreadable_secret_file_refuses_boot (crates/maidan-server/tests/secret_files_boot.rs); init_reads_the_database_url_and_kek_from_files (crates/maidan-cli/tests/secret_files.rs); unit tests in crates/maidan-env/src/secret_files.rs. That docker inspect shows no secret follows from the compose example setting only _FILE paths, and is an operator check in Production's smoke step, not a test | Tested (the docker inspect result is an operator check) |
A token:admin bearer creates a member over SCIM, with userName as its handle, and a bearer without token:admin is refused | crates/maidan-server/tests/scim_e2e.rs: scim_user_lifecycle_over_http creates a user through POST /scim/v2/Users with a token:admin bearer, then reads, filters by userName, patches and deletes it; scim_requires_token_admin gets 403 with a workspace:read bearer. crates/maidan-server/src/scim.rs maps userName to the member handle. The Keycloak recipe in docs/OIDC.md relies on it to pre-provision people | Tested |
What "audited" covers
Every successful change made with a Maidan token or browser session leaves an attributed record — who acted, and on whose behalf — and the privileged ones leave a named audit row, unless the change wrote no event or audit row of its own and the best-effort generic row fails to write. Authority changes are not on that path: a failed audit write aborts the change. Two kinds of change are outside attribution entirely, listed under "Every other mutation" below.
- Privileged actions have named audit rows — 52 action kinds besides the
generic
mutationrow (counted from theactionnames the server writes): token and app-token mint, rotation, delegation and revoke; browser sign-in and sign-out; delegation grants and policy; share tickets; channel membership; member freeze; SCIM provisioning; skill governance; secrets and egress targets; legal hold; retention policy; message purge, workspace purge, erase, export and import; artifact erase; app revoke; reviewer removal and review-requirement changes; gate and review-requirement clears; delivery, outbox and automation replays and result-delivery attempts; reindex; and denials of a delegated actor (below).audit_coverage_e2eandauthority_audit_contractexercise them. - Authority changes fail closed. Tokens, grants, share tickets, browser
sessions, the grant ceiling, purge, erase, import and legal hold write their audit row inside the
change's own transaction, so a failed audit write aborts the change
(
authority_audit_contract). Routine rows are best-effort: a failed write is counted inmaidan_audit_write_failures_totaland pagesMaidanAuditWriteFailureson the first. - Every other mutation is attributed, best-effort. A successful
POST/PUT/PATCH/DELETEthat wrote no attributed event or audit row of its own gets a genericmutationrow (operation, path, status). That write is the best-effort path above: if it fails, the change still stands and has no attributed record. The row is skipped whencontracts/http-operation-kinds.jsonclassifies it as a read (http_operation_kinds_e2echecks that each one writes nothing). Ordinary content — posts, edits, reactions — is recorded in the event log, which is durable, ordered and replayable. MCP records per tool call and A2A per method on every binding, gRPC included, so a read or a refused call is not recorded as a change (a2a_operation_kinds_e2e). Two kinds of change are not covered. Slack and GitHub ingress (/integrations/slack/events,/integrations/github/events) is authenticated by the sender's request signature, not a Maidan credential: the message posts as the member the channel link names, and its event carries no attribution (no actor or subject). AndGET /mcp/streamandGET /ws/subscribeadvance a named consumer's delivery cursor (the WebSocket also stamps a member's last-seen time) with no event or audit row;contracts/http-operation-kinds.jsongives the reasons. - Denials are counted, not stored, with one exception. Anonymous and ordinary
401/403s go to
maidan_authorization_decisions_totaland sampled logs, since an attacker-controlled request stream would otherwise be an unbounded write amplifier against the audit table. A denial of a delegated actor — a named member holding an expiring, revocable grant — is stored asauthorization.decision. - Reads are not audited, except the reads that release a whole workspace or a
secret (
workspace.export,secret.resolve,legal_hold.preserved_read), which write their row before releasing anything.audit:read-globalandoperator:globalbound who can read across tenants.
Not yet / honest limits
- No hosted SaaS. Maidan is self-hosted only. There is no managed cloud and no public playground.
- Not a library on crates.io. The workspace is
publish = falseon purpose; the "release" is the tagged binary + container image, not a crate. - Projectors + email are config-gated and unproven in public. The code ships and is tested with mocks; a live Slack/GitHub/SMTP deployment needs you to create the app and set the secrets. We don't claim a running public instance.
- OIDC human login is present but maturing.
MAIDAN_OIDC_*enables/auth/oidc/*+ session mint; treat it as config-gated, not a polished consumer login. - SDKs are 0.1.0. Usable against the frozen v1 contract, dependency-light, but early.
Typed responses and an error per problem type are on
main(scripts/sdk-test.sh) but not yet tagged, so the published packages return plain JSON and raise one error type; registry-published interop CI is a follow-up. - Not an orchestration planner or an agent runtime. Maidan does not run your models or decide how an agent reasons. It is the durable place agents coordinate.
How this stays honest
- New public claims add a row here in the same PR.
- The required CI checks (lint, secrets scan, unit, integration, docker-compose smoke,
scale-out smoke, promtool, otlp smoke) gate every merge; the non-required
a2a tckjob and the report-onlysdk interopjob prove the client/interop surface without blocking. - Release artifacts are cosign-signed; verify before trusting a tag (SECURITY.md).