mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-02 09:24:43 +08:00
* ci: run CI for the owner identity seam integration branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(auth): owner identity seam, phase 1 — pluggable authenticator with today's behavior * feat(auth): add the owner identity seam with built-in authenticators Introduce lib/server/identity/: an OwnerAuthenticator contract (SubjectKind, OwnerPrincipal, AuthOutcome), a single-shot, boot-validated configureOwnerAuthenticator() registry, and the anonymousCookie and sharedTeam built-ins that reproduce today's owner resolution exactly (same anonymous_id cookie, owner id strings, statuses and env semantics). Every owner-scoped route handler and the workspace Server Action now resolve their owner through one memoized per-request resolution instead of the three previous copies (agent-runtime/owner.ts, with-owner.ts and the Server Action re-implementation). Publish and unpublish decide from the principal's course:publish role instead of an anon: id prefix. An invalid credential from an authenticator is answered with 401 on every surface and never falls back to an anonymous owner. A source scan keeps the anonymous owner cookie and anon: prefix checks out of code outside the identity module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(auth): describe the owner identity seam and host registration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(auth): tighten the identity boundary guard and Server Action cookies - Scan workspace package sources too, flag imports of the concrete built-in authenticators outside lib/server/identity, and police the slice/substring/indexOf/includes spellings of an anon: id-shape check. Stop re-exporting the built-ins from the host-facing index. - Refuse setCookies returned by authenticateFromContext, as the authenticate() fallback already did, and document that it must write cookies itself through next/headers. - Let the route-test owner stub carry explicit kind/roles, and cover the 403 forbidden branch of publish and unpublish for a signed-in owner without course:publish. - README: registration conflicts fail the boot; an invalid principal is rejected per request with a 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(auth): owner identity seam, phase 2 — runtime and assets keyed by the owner * feat(auth): owner identity seam, phase 2 — runtime and assets keyed by the owner Runtime sessions and assets on /api/persistence now take their identity from the same memoized owner resolution as documents, instead of a client-chosen x-learner-key behind a development bearer token that shipped in the public bundle. - Runtime: the storage handler's learner key is principal.ownerId. A path or body naming another learner answers 403 FORBIDDEN_LEARNER and another learner's session answers 404, whatever x-learner-key says; the header is no longer read. The browser learns its key from the new GET /api/persistence/learner-key and sends no credential of its own. The Pi whiteboard routes derive the same key from the owner. PERSISTENCE_DEV_TOKEN, NEXT_PUBLIC_PERSISTENCE_TOKEN and PERSISTENCE_ALLOW_INSECURE_DEV_AUTH are gone from the server path and the client, and server-auth.ts is deleted. Learner merge stays refused. - Assets: allocations land in a per-owner partition (owner:<ownerId>), so quota is per owner and replace/delete reach only the owner's entries. Reads stay capability-by-id where a course viewer needs them: another owner's committed entry is readable while a live course references it. Legacy entries in the old shared partition stay readable by id, and may be replaced or deleted only by an owner who owns every course referencing them. Server-generated media is stored under the run owner's partition; asset-id extraction and vision image resolution use the same rule. - Tombstones: runtime of a deleted course reads as absent (404, empty lists) and takes no new sessions or records, through a guard in the app's runtime composition; no package schema change. The identity boundary scan now also fails on any source that reads x-learner-key or the retired development token variables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(persistence): describe owner-keyed runtime and assets, drop the dev token Remove the development persistence token from the server-persistence recipe, .env.example, the Docker build arguments and the deployment docs; describe runtime learner keys, per-owner asset partitions and quota, the legacy shared-partition rule and the tombstone behavior; and record the breaking change for existing server-persistence runtime data under Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(storage): scope asset references to principals, typed stage refusal, uniform create collision @openmaic/storage 0.32.0. - PgDocumentStore takes assetReferencePrincipals: with reference tracking on, a write references and commits only entries held by the listed principals. An id naming another principal's entry records no row and changes no lifecycle column, like an unknown id, and the write is never refused. Without the option any held entry is referenced, as before. AssetCollector takes a matching per-document-owner function so the one-time backfill scopes each document the same way. This keeps a document naming a leaked id from committing, exposing or pinning another owner's allocation. - RuntimeStageNotFoundError: a store may refuse createSession for a stage its host considers absent; the runtime handler answers 404 STAGE_NOT_FOUND, recognizing the error by class or by code. - A session create over a taken id answers 409 SESSION_ALREADY_EXISTS whoever holds it. It used to answer 403 for another learner's session and 409 for one's own, which told a caller whose session an id was. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(persistence): close owner-seam boundary gaps in references, tombstones and legacy assets - Document writes reference and commit only the writer's own asset entries and legacy shared ones (owner-bound document store and the collector backfill), so another owner's course naming a pending id can no longer commit, expose or pin it. - The foreign-read rule additionally requires the referencing live course to belong to the entry's owner, as defense in depth for reference rows that did not come from a scoped write. - The tombstone refusal for new runtime sessions moved from a raw path comparison in the route into the guarded store's createSession (RuntimeStageNotFoundError, 404 STAGE_NOT_FOUND), so no encoded spelling of the path routes around it. - Replacing or deleting a legacy shared entry checks reference ownership and mutates in one transaction under the entry row lock, which a concurrent reference insert must wait for. - Tests build the uncommitted-but-referenced, tombstoned-with-rows and foreign-reference states directly so each half of the read rule is observable, and a PostgreSQL suite holds a competing reference open across a legacy delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(persistence): upgrade note for dev-token-gated deployments, owner-scoped references Tell operators whose only access gate was the development token to put the deployment behind an access code or gateway, register an owner authenticator, or turn server persistence off before upgrading, and describe that a course references only its owner's media. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(storage): per-owner reference principals and a typed taken-id conflict - PgDocumentStore's assetReferencePrincipals is now a function of the store's bound owner, evaluated per write, instead of a fixed list. A store re-bound with forOwner therefore scopes to the new owner rather than carrying the previous owner's principals onto its writes. It has the same shape as AssetCollector's backfill option, so one function serves both. - PgRuntimeStore and BrowserRuntimeStore raise RuntimeSessionExistsError for a taken session id, and the runtime handler classifies it as 409 SESSION_ALREADY_EXISTS even when its re-read cannot see the holder (for example a session a host-side wrapper hides). Recognized by class or by code. Still the unreleased 0.32.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(persistence): guard the Pi whiteboard runtime and answer hidden collisions with 409 - Every server-side runtime writer now uses the tombstone-guarded store: the Pi native whiteboard service gets it through the same helper as /api/persistence/runtime/*, so a deleted course takes no new whiteboard runtime either. - Creating a session whose id is held by a session a tombstone hides answers the uniform 409 SESSION_ALREADY_EXISTS instead of a 500, covered on PGlite and on real PostgreSQL. - The owner-bound document store passes its reference-principal function, so the scoping follows the bound owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(auth): owner identity seam, phase 3 — accounts through an identity gateway (trustedProxyHeader) * feat(auth): owner identity seam, phase 3 — trusted-proxy-header authenticator Add the trustedProxyHeader built-in: real accounts through an identity gateway (oauth2-proxy, Authelia, a Keycloak-based proxy, an institutional reverse proxy) that signs the user in and forwards the verified user in a request header. OpenMAIC never handles passwords or tokens. - Selected with OWNER_AUTHENTICATOR=trusted-proxy and validated in instrumentation register(). TRUSTED_PROXY_SECRET (32-1024 printable ASCII characters) is required; header names default to x-openmaic-proxy-secret and x-forwarded-user, and an optional groups header with TRUSTED_PROXY_ADMIN_GROUPS grants admin. An unknown selector, a TRUSTED_PROXY_* variable without the selector, a shared owner id or a host-registered authenticator alongside it, and malformed, reserved or repeated header names fail the boot. - Trust boundary: Next.js exposes no TCP peer address to route handlers, middleware or Server Actions, and fills x-forwarded-for from the socket only when the client sent none, so no address allowlist is offered. The gateway secret is compared in constant time (hashed, timingSafeEqual). - Per request: a missing or wrong secret, a missing or blank user, a user header with a comma (duplicate lines arrive comma-joined), or a user whose proxy:<user> id fails the owner id guard is 401 INVALID_CREDENTIAL, never an anonymous owner. The guard failure is a 401 rather than a 500 because the value comes with the request. The user is trimmed and keeps its case. Every gateway user holds course:publish; kind user, assurance verified, channel proxy; no cookie. Route handlers and Server Actions share one code path. - The unset-ACCESS_CODE boot warning is skipped in this mode; the gateway is the access gate and ACCESS_CODE stays independent. - The identity boundary scan now also fails on gateway identity headers (x-forwarded-user/-groups/-email, x-auth-request-*, remote-user, the secret header) and TRUSTED_PROXY_* read outside lib/server/identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(auth): document accounts through an identity gateway Describe the trustedProxyHeader built-in in the README owner identity section (and its Chinese counterpart): configuration, request semantics, the trust boundary and why it is a shared secret, the interaction with ACCESS_CODE, boot-time validation, and an oauth2-proxy example that injects the user, groups and secret headers. Add the .env.example block, a SECURITY.md note and an Unreleased changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(auth): harden the trusted-proxy authenticator after review - Reserve the header names HTTP, Next.js and forwarding proxies set themselves: configuring the user, groups or secret header to rsc, next-action, forwarded, x-forwarded-for/-host/-proto/-port, x-real-ip, or anything starting with x-middleware-, x-invoke-, x-nextjs- or next-router- now fails the boot. - Log a one-time boot warning when TRUSTED_PROXY_ADMIN_GROUPS is set: the admin role is granted from the groups header, which is trusted on the strength of the shared secret alone, so the gateway must overwrite or strip it. - POST /api/chat/pi resolves the owner up front and answers 401 when the authenticator rejects the credential, like every other owner-resolving route, instead of continuing without an owner. The anonymous default always resolves, so it is unaffected. - The identity boundary scan also covers the Authentik, Azure App Service authentication, WebAuth, Cloudflare Access, Google IAP and AWS ALB OIDC header families. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(auth): admin-groups warning and a corrected oauth2-proxy example - Warn next to TRUSTED_PROXY_ADMIN_GROUPS (README, README-zh, .env.example) that the groups header is trusted on the strength of the shared secret alone, and list the reserved header names. - The oauth2-proxy example now states it targets v7.14+ (nested claimSource/secretSource), uses the canonical bindAddress, sets preserveRequestValue: false on all three headers and insecureSkipNonce: false, forwards the subject with `claim: user`, and notes that the secret variable holds the literal secret. It adds the legacy-option migration caveat, a warning not to exempt app routes via skip-auth or trusted-ip options, and notes that the IdP must release the groups claim and group names must not contain commas. - Changelog: the new boot checks and the /api/chat/pi 401. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(persistence): owner identity seam, phase 4 — host extension hooks * feat(persistence): owner identity seam, phase 4 — host extension hooks Let a host add product behavior at four points without forking a route, registered once from instrumentation.ts register() like the owner authenticator and sealed on first use. Nothing changes when no hook is registered. - configurePersistenceHooks({ name, authorizeCreate, onCreate, library, beforeAssetAllocate }): - authorizeCreate / onCreate run once per created course, inside the create transaction, in the transaction that inserts the course's ownership row. Saves of a course the owner already holds, and a create that loses a race to a concurrent create of the same id, are updates and run no create hook. A refusal answers 403 CREATE_REFUSED; a throw rolls the course, its ownership row and the host's writes back together. The hooks receive the request principal, or only the owner id for a background agent run. - library chooses which stage ids GET /api/stages lists; the route builds the usual summaries in the provider's order and drops every id the read path would refuse (deleted or unclaimed). folderId is shown only on the principal's own courses. - beforeAssetAllocate runs inside the storage handler's asset authorization step for POST /assets, after the owner is resolved and before the body is read; a returned Response is sent instead, with nothing stored and no quota counted. - configureAssetByteStore({ name, create, signsReadUrls }) replaces the ASSET_S3_BUCKET switch for both the persistence provider and the asset collector. A host store must keep bytes outside the registry database. Under ASSET_BYTE_EGRESS=redirect a store that does not declare signsReadUrls stops the server at boot; the built-in layers keep their behavior. - @openmaic/storage 0.33.0: DocumentWriteRefusedError lets a store refuse a write as policy; the document HTTP handler answers 403 with its code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(persistence): harden the host extension hooks after review Correctness: - Registration reads every hook once and stores it bound to the object the host passed, so hooks defined on a class prototype or as non-enumerable properties are kept. A spread copy dropped them silently, including the authorizeCreate and beforeAssetAllocate gates. Same for configureAssetByteStore. The unknown-key check now applies to plain objects only, since class instances carry their own state fields. - The owner-bound document store carries each call's operation in its own async context (AsyncLocalStorage) instead of a field on the instance. One store is shared by every tool of an agent run; a concurrent call could overwrite the field before the transaction read it, so a create was gated as a read (no ownership row, no create hooks) or claimed another stage. create_stage also runs sequentially within a tool batch. Hardening: - isDocumentWriteRefusedError recognizes a refusal from another copy of the storage package by name and shape (not by code alone). The document handler and POST /api/stages use it. - beforeAssetAllocate also gates PUT /assets/{id}/content. The request carries operation ('create' | 'replace') and the decoded assetId from the handler's own routing. - Agent runs: a refusal on a background write carries a fixed message, and create_stage answers it with a stable tool result. The host's message never reaches the model. - A byte store declaring signsReadUrls without a signer degrades to direct bytes with a warning instead of failing reads, and the collector no longer requires signing. - DocumentActor is discriminated by source ('request' with the principal, 'background' without one). - Library ids the read path cannot address are dropped, and a provider answer is capped at 5000 ids. - The boot-time ownership backfill logs how many courses it adopted without hooks. - The server persistence provider no longer exposes an unscoped document store. - The writesOutsideRegistryDatabase trust boundary and the scope of upload admission (HTTP uploads, not server-generated media) are documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(persistence): refuse misspelled hooks on host classes too A class instance skipped the unknown-key check, so a misspelled hook such as authorizeCreat registered cleanly and never ran, leaving creation ungated. Both configurePersistenceHooks and configureAssetByteStore now reject any function-valued property of a class instance (own or on its prototype chain up to Object.prototype, constructor excluded) that is not a known key, and any unknown key of a plain object. The error names the key and suggests the known key within edit distance 2. Host classes keep their helpers private (#helper) or register a plain object; the README says so. Also: - isDocumentWriteRefusedError requires a string message from a cross-copy candidate, and documents that the name is the effective discriminator. - CHANGELOG: ServerPersistenceProvider no longer exposes an unscoped documentStore; use the owner-bound store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(persistence): owner identity seam, phase 5 — tenancy lives in stage_meta only * feat(persistence): owner identity seam, phase 5 — tenancy lives in stage_meta only Course ownership was recorded twice: in stage_meta, which already answers every access decision, and in document_stages.owner_id, which the storage package scoped by. Two tenancy records double what a later claim has to re-key, and a host that keeps ownership in its own table had to rewrite the package's SQL to strip the column. @openmaic/storage 0.34.0: - PgDocumentStore never reads or writes document_stages.owner_id. An owner-bound store scopes listings, writes, deletes, the freshness manifest and folder membership through the host's ownership relation, named with the new documentOwnership option ({ table, stageIdColumn, ownerIdColumn, tombstoneColumn, claimOnCreate }, or false for no document scoping). Identifiers are validated, never quoted into SQL from arbitrary input. - forOwner / ownerId without documentOwnership throws at construction, so a direct consumer cannot upgrade into a store that silently scopes nothing. An unbound store is tenant-agnostic. - claimOnCreate inserts the ownership row in the create transaction; a concurrent create of the same id by another owner rolls back. - folders: false lets a host whose document_stages has no folder_id use the store; folder ids are unique per owner, so membership goes through the ownership relation too. - AssetCollector reads each document's owner from documentOwnership for assetReferencePrincipals, and refuses the function without it. - Schema: fresh installs get no owner_id column and none of its indexes; an existing column is kept for one release, made nullable with its default dropped (each ALTER guarded by a catalog check, so a replay takes no lock), and its two indexes are dropped. document_stages_folder_idx replaces the owner/folder index. Application: - The owner-bound store binds the package to stage_meta (STAGE_META_OWNERSHIP) and lists through it in one query; the asset collector schedule passes the same relation. - The boot backfill copies column-only owners into stage_meta before any store is built, only when the legacy column exists, idempotently, and logs how many it adopted and how many disagree (stage_meta stands). Tests cover a fresh install and an upgrade from the previous schema on PGlite and PostgreSQL 16, a host table with neither ownership nor folder columns, folder isolation across owners sharing a folder id, and the concurrent-create race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(persistence): serialize schema bootstrap; guard unscoped owner-bound stores Review follow-ups for the tenancy change. - Concurrent starts: two instances starting against one database raced on the schema DDL (a duplicate pg_class / pg_type entry, or "tuple concurrently updated"), failing one instance's first request. Every schema bootstrap the application runs -- the persistence provider's sequence (runtime, documents, stage_meta and its ownership backfill, owner materials, assets), the agent-session, session-material and user-skill stores, and the asset collector's -- now runs under one PostgreSQL advisory lock held on a dedicated connection and released in finally. It lives in the application because what needs serializing is the whole sequence, including tables the package does not own. A PostgreSQL test boots eight instances at once, four rounds each, on a fresh and on an upgraded database; without the lock it fails every run. - documentOwnership: false on an owner-bound PgDocumentStore now also requires allowCrossOwnerDocumentAccess: true, so binding an owner for folders or asset principals cannot silently expose every owner's documents. - Documented that an ownership relation must cascade with the document rows (a leftover row keeps the id reserved for its owner, which is also how a host keeps retired ids from being reused), with tests for both. Leftover rows are deliberately not made claimable: a host cannot be told apart from one that keeps them as retirement markers. - Upgrade notes: the one-time table locks on the first start, and the out-of-band CREATE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY path that leaves the start with nothing to build or drop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(persistence): leave the schema bootstrap lock out of the held-lock check The create-hooks suite asserts that its host lock is released with the create transaction by counting granted advisory locks database-wide. A suite booting a provider against the same database at that moment now holds the schema bootstrap lock, which is not the lock under test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(auth): owner identity seam, phase 6 — claiming anonymous work on sign-in * feat(storage): owner merge primitives for claiming anonymous work (0.35.0) Add the per-store pieces a host needs to move one owner's data to another inside a single transaction of its own: - reassignDocumentFolders(tx, { fromOwnerId, toOwnerId, documentOwnership }) moves folders and filing: a same-named folder merges into the target's, a colliding id is renumbered, and filing is rewritten in one statement from the old ids. - PgAssetStore.reassignPrincipal(fromKey, toKey) moves a principal's entries under both principals' write locks, in key order; quota is not re-checked. - PgUserSkillStore.mergeOwner(from, to) moves skills, renaming a live handle the target already uses with the first free numeric suffix. - PgUserSkillStore takes a resolveFinalOwner hook, run as the create transaction's first statement, like the agent-session store's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(auth): owner identity seam, phase 6 — claiming anonymous work on sign-in A visitor who works anonymously and then signs in can move that work to their account in one transaction, and the anonymous id is retired afterwards. - Principal: pendingClaim { fromOwnerId, assurance }. The trusted-proxy built-in sets it when a gateway request also carries a valid anonymous cookie; the anonymous and shared-team built-ins never do. Authenticators may implement describeStoredOwner, canonicalize and clearPendingClaim; principalFromStoredOwner describes a stored id for work without a request. - Claims: claimOwner / claimPendingOwner and registerClaimParticipant (sealed on first claim). One transaction, both owners' identity locks taken exclusively in key order, participants in a fixed order (folders, courses, materials, agent sessions, skills, runtime, assets), recorded in the new owner_merges table. Idempotent per pair; refuses a non-anonymous source, an anonymous target, a source already claimed elsewhere, and chains. Same-named folders merge, colliding folder ids are renumbered, colliding skill handles get a suffix, and quotas are not applied to what moves. - Identity lock: every owner write (documents, folders, asset allocations, material registration, agent sessions, skills) takes the owner's advisory lock in shared mode first, so a write racing a claim is moved or refused, never orphaned or deadlocked. - Retired ids: request writes are refused with 403 OWNER_RETIRED; agent runs and media generation that started before the claim follow the id to the account (forwarding document store, canonicalized stage probes, forwarded generated assets and skills). - Trigger: POST /api/identity/claim (same-origin JSON only; clears the anonymous cookie), or OWNER_CLAIM_TRIGGER=auto on the first request carrying a pending claim. The runtime contract's learner merge now performs the same claim, allowed only for the request's own pending claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(persistence): count only the suite's own advisory locks after concurrent creates The check that a host's transaction-scoped lock is released counted every granted advisory lock in the database. Suites that run against the same database at the same time hold advisory locks of their own (a schema bootstrap, the owner identity locks a claim takes), which made it fail depending on timing. Count only locks held by this suite's connections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(auth): use a neutral example table for host claim participants Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(storage): owner merge fixes and store policy errors over HTTP (review round 1) - PgUserSkillStore.mergeOwner reserves every handle either owner holds before renaming: a claimed skill whose handle the target uses could be renamed into a handle another claimed skill still held, which violated the per-owner unique index and made that merge fail every time. - PgUserSkillStore.list orders ties by id. - PgRuntimeStore takes resolveFinalLearner(tx, learnerKey), run as the create transaction's first statement, so a host can fence session creates against its owner merges; reassignLearner(from, to) re-keys sessions without re-validating them, so one session a newer version wrote cannot block a merge. - Every HTTP handler (runtime, documents, assets) answers a DocumentWriteRefusedError as 403 with its code, and the new StorageBusyError (code, message, retryAfterSeconds) as 503 with Retry-After. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(auth): claims review round 1 — fenced runtime creates, bounded locks, retired-cookie recovery - Runtime session creates take the owner's identity lock as the create transaction's first statement and refuse a retired owner, closing the window in which a create racing a claim was left under the retired id. - Lock waits are bounded: a claim waits OWNER_CLAIM_LOCK_WAIT_MS (default 5 s) for its identity locks, a write OWNER_WRITE_LOCK_WAIT_MS (default 30 s); running out, or losing a deadlock or serialization race, answers 503 OWNER_BUSY with Retry-After on the claim routes and on every fenced write. - Every 403 OWNER_RETIRED carries the Set-Cookie values that drop the retired anonymous credential (the anonymous built-in now implements clearPendingClaim), so a browser whose claim response was lost recovers. Writes by id to rows that moved (skill delete, session messages) answer OWNER_RETIRED instead of not-found or a bare 403. - OWNER_CLAIM_TRIGGER=auto no longer claims ahead of the explicit claim routes, which now report their own claim instead of a false refusal. - The owner-events stream tells a retired owner only that it moved, never the claiming account's id. - Creates of one course id take turns (a per-id advisory lock before the ownership probe), so a concurrent second create saves as an update instead of failing as reserved-document. - A claim's source must be described as anonymous by the authenticator; the write fences skip the retirement lookup for every other owner. The host canonicalize hook is removed: forwarding is core's owner_merges alone. - The folders participant locks the source's stage_meta rows first; runtime sessions are re-keyed without re-validation; the owner asset store fences every allocation and refuses to allocate without transactions; background skill reads and patches follow a mid-run claim; the forwarding document store forwards methods only. - Docs: when a pending claim can arise with the shipped authenticators, the anonymous cookie as a bearer credential, bounded waits, the exact scope of OWNER_RETIRED, and claims narrowed to what the tests show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(auth): claims review round 2 — merge-record guard, boot-checked lock waits, stream credential drop - owner_merges records claims of anonymous owners only, since the write fences enforce retirement only for ids the authenticator describes as anonymous. Reading a row that retires any other owner now fails loudly instead of being followed but not enforced. The comment that pointed hosts at claimOwner for their own account merges is corrected: a host merging two signed-in accounts moves the rows itself and refuses the merged-away account in its authenticator. describeStoredOwner must classify ids stably. - OWNER_WRITE_LOCK_WAIT_MS and OWNER_CLAIM_LOCK_WAIT_MS are validated at boot with the same parser the lock paths use, so a malformed value stops the server instead of failing every write. - The owner-events stream answers a connect by an already retired identity with one owner_moved event and the Set-Cookie values that drop the retired credential, so a stale tab's reconnect loop ends after one round trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * chore(auth): final polish before landing the owner identity seam * chore(auth): final polish before landing the owner identity seam - Pi routes refuse a rejected credential with the seam's INVALID_CREDENTIAL body. - Drop the unused LegacyAssetMutations alias and resolveLockWaitMs re-export. - Scope the document_stages.owner_id deprecation wording: the store no longer reads or writes it; the boot backfill reads it and a claim mirrors ownership into it for rollback. - Document OWNER_CLAIM_TRIGGER and the two lock-wait variables in .env.example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(storage): 0.35.1 for the README wording fix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(identity): one identity entry point with composable auth methods * feat(identity): compose owner auth methods behind one entry point Owner resolution now asks an ordered list of owner auth methods instead of one configured authenticator. Each method answers `authenticated`, `not-applicable` (no credential of its kind) or `invalid` (its credential is present but bad). Core keeps the first `authenticated` answer, refuses any `invalid` answer with 401 INVALID_CREDENTIAL without asking later methods, and when no method applies falls back to the anonymous cookie owner, or answers 401 when the host turned the fallback off. Route handlers and Server Actions share the same order and per-request memoization. - configureOwnerAuthentication({ methods, anonymousFallback? }) replaces configureOwnerAuthenticator: single-shot, sealed on first use, and validated at registration and at boot. - anonymousCookie becomes the built-in fallback method; sharedTeam becomes a method, selected by PERSISTENCE_SHARED_OWNER_ID as before when nothing is registered, and kept beside host methods only when the host lists sharedTeamAuthMethod() last. The variable set beside a registration that leaves it out fails the boot. - Core attaches pendingClaim itself: a host method's non-anonymous principal on a request that also carries a valid anonymous cookie. A method may not set one. Claim cookie clearing and OWNER_RETIRED recovery go through the anonymous method's clearCredential. - principalFromStoredOwner consults the anonymous method first, then the configured methods' describeStoredOwner in order. - Remove the built-in trusted-proxy header authenticator with OWNER_AUTHENTICATOR and every TRUSTED_PROXY_* variable. The boundary test now also keeps the resolution internals inside lib/server/identity/ and asserts no source reads the removed variables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(identity): document owner auth methods and a gateway JWT recipe Rewrite the owner identity section around the ordered auth methods: the three answers and what resolution does with each, the anonymous fallback, registration and its boot checks, and the sharedTeam rules. Replace the removed built-in gateway authenticator's documentation with a short host-owned recipe that verifies a gateway-forwarded signed JWT (oauth2-proxy, Cloudflare Access, IAP) against the identity provider's keys with the jose library. Update the claim section for the core-owned claim candidate, and the Chinese README, SECURITY.md, CHANGELOG and the deployment docs in every locale to match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(identity): tighten retired-owner cookies, retired config and the boundary - On 403 OWNER_RETIRED, core now calls clearCredential only on the anonymous fallback and on methods that declare `issuesAnonymousOwners: true`, so an account method's session cookie is never cleared there. A clearCredential that throws is logged and skipped instead of turning the refusal into a 500. - Setting any retired OWNER_AUTHENTICATOR / TRUSTED_PROXY_* variable fails the boot with a pointer to the auth-methods docs, instead of silently serving every request anonymously. Only the names are matched, in one module the boundary test checks never reads a value. - Add lib/server/identity/host/ for host auth methods. The boundary test now scans core identity files for gateway identity headers too, polices reads of an incoming Authorization header everywhere but the host directory, and keeps resolution internals out of host code. - Document on OwnerAuthMethodResult that a present but unusable credential must answer `invalid`, never `not-applicable`, and that a method that cannot decide throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(identity): fix the gateway JWT recipe's error split and add notes - The recipe now answers `invalid` only for token failures (expired, claim validation, malformed JWS/JWT, bad signature, no or several matching keys, disallowed or unsupported algorithm) and rethrows everything else: jose reports a key endpoint that is down, answers non-200 or returns unparsable data as a generic JOSEError, which the previous version turned into a 401 for every user. - The recipe file goes in lib/server/identity/host/, with an accurate description of what the boundary test enforces. - Document invalid versus not-applicable for malformed credentials, issuesAnonymousOwners, the boot failure on retired variables, and that the claim candidate is an unsigned bearer cookie a sibling subdomain can shadow (serve on a registrable domain of its own; cookie hardening is deferred). Mirror the notes in the Chinese README, SECURITY.md and the changelog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
667 lines
30 KiB
Bash
667 lines
30 KiB
Bash
# =============================================================================
|
|
# OpenMAIC Environment Variables
|
|
# Copy this file to .env.local and fill in the values you need.
|
|
# All variables are optional — only configure the providers you want to use.
|
|
# You can also use server-providers.yml for configuration (see docs).
|
|
# =============================================================================
|
|
|
|
# --- LLM Providers -----------------------------------------------------------
|
|
# Format: {PROVIDER}_API_KEY, {PROVIDER}_BASE_URL (optional), {PROVIDER}_MODELS (optional, comma-separated)
|
|
|
|
OPENAI_API_KEY=
|
|
OPENAI_BASE_URL=
|
|
OPENAI_MODELS=
|
|
# For relays whose non-streaming Chat Completions response is incompatible.
|
|
# Forces custom OpenAI base URLs to use Chat Completions and buffers SSE responses.
|
|
# Has no effect on the official OpenAI base URL. Disabled by default.
|
|
# OPENAI_COMPAT_USE_STREAMING_CHAT=true
|
|
|
|
# Azure uses deployment names as model IDs.
|
|
AZURE_OPENAI_API_KEY=
|
|
AZURE_OPENAI_BASE_URL=https://YOUR-RESOURCE.openai.azure.com/openai
|
|
AZURE_OPENAI_MODELS=
|
|
|
|
ATLASCLOUD_API_KEY=
|
|
ATLASCLOUD_BASE_URL=https://api.atlascloud.ai/v1
|
|
# Example: qwen/qwen3.5-flash,deepseek-ai/deepseek-v4-pro
|
|
ATLASCLOUD_MODELS=
|
|
|
|
ANTHROPIC_API_KEY=
|
|
ANTHROPIC_BASE_URL=
|
|
ANTHROPIC_MODELS=
|
|
|
|
GOOGLE_API_KEY=
|
|
GOOGLE_BASE_URL=
|
|
GOOGLE_MODELS=
|
|
|
|
DEEPSEEK_API_KEY=
|
|
DEEPSEEK_BASE_URL=
|
|
# Example: deepseek-v4-pro,deepseek-v4-flash,deepseek-v4-flash-vision-exp
|
|
DEEPSEEK_MODELS=
|
|
|
|
QWEN_API_KEY=
|
|
QWEN_BASE_URL=
|
|
QWEN_MODELS=
|
|
|
|
KIMI_API_KEY=
|
|
KIMI_BASE_URL=
|
|
KIMI_MODELS=
|
|
|
|
MINIMAX_API_KEY=
|
|
# MiniMax Anthropic-compatible endpoint for the built-in Anthropic SDK integration
|
|
MINIMAX_BASE_URL=https://api.minimaxi.com/anthropic/v1
|
|
# Example: MiniMax-M2.7-highspeed,MiniMax-M2.7,MiniMax-M2.5-highspeed,MiniMax-M2.5
|
|
MINIMAX_MODELS=
|
|
|
|
GLM_API_KEY=
|
|
GLM_BASE_URL=
|
|
GLM_MODELS=
|
|
|
|
SILICONFLOW_API_KEY=
|
|
SILICONFLOW_BASE_URL=
|
|
SILICONFLOW_MODELS=
|
|
|
|
DOUBAO_API_KEY=
|
|
DOUBAO_BASE_URL=
|
|
DOUBAO_MODELS=
|
|
|
|
OPENROUTER_API_KEY=
|
|
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
|
# Example: deepseek/deepseek-v4-pro,deepseek/deepseek-v4-flash
|
|
OPENROUTER_MODELS=
|
|
|
|
GROK_API_KEY=
|
|
GROK_BASE_URL=
|
|
# Example: grok-4.6,grok-4.5
|
|
GROK_MODELS=
|
|
|
|
TENCENT_API_KEY=
|
|
# Tencent TokenHub OpenAI-compatible endpoint. Hy3 is a model ID, not an env prefix.
|
|
# TENCENT_HUNYUAN_* is also accepted as an alias.
|
|
TENCENT_BASE_URL=https://tokenhub.tencentmaas.com/v1
|
|
# Example: hy3-preview,hunyuan-2.0-thinking-20251109,hunyuan-2.0-instruct-20251111
|
|
TENCENT_MODELS=
|
|
|
|
XIAOMI_API_KEY=
|
|
# MIMO_* is also accepted as an alias. Use tp-... keys only with Token Plan URLs.
|
|
XIAOMI_BASE_URL=https://api.xiaomimimo.com/v1
|
|
# Token Plan regional examples:
|
|
# XIAOMI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
|
|
# XIAOMI_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
|
|
# XIAOMI_BASE_URL=https://token-plan-ams.xiaomimimo.com/v1
|
|
# Example: mimo-v2.6-pro,mimo-v2.6-flash,mimo-v2.5-pro,mimo-v2-pro,mimo-v2.5,mimo-v2-omni,mimo-v2-flash
|
|
XIAOMI_MODELS=
|
|
|
|
TOKENDANCE_API_KEY=
|
|
# OpenAI-compatible gateway. The same key also works for the image, video, TTS
|
|
# and web-search routes on this host (see the README quick example).
|
|
TOKENDANCE_BASE_URL=https://tokendance.space/gateway/v1
|
|
# Example: deepseek-v4.1-flash,deepseek-v4-pro,glm-5.3,kimi-k3,qwen3.8-max
|
|
TOKENDANCE_MODELS=
|
|
|
|
# --- Ollama (Local Models) ---------------------------------------------------
|
|
# No API key needed. Configure BASE_URL here (server-side) so it bypasses SSRF
|
|
# protection automatically. Client-supplied localhost URLs are blocked in production.
|
|
# OLLAMA_BASE_URL=http://localhost:11434/v1
|
|
# OLLAMA_MODELS=llama3.3,llama3.2,qwen2.5,mistral,gemma3
|
|
|
|
# Lemonade local server (OpenAI-compatible, no API key required)
|
|
# LEMONADE_BASE_URL=http://localhost:13305/v1
|
|
# LEMONADE_MODELS=Qwen3-0.6B-GGUF,Llama-3.2-1B-Instruct-Hybrid,Qwen2.5-VL-7B-Instruct
|
|
|
|
# Amazon Bedrock LLMs (no OpenAI-style API key required)
|
|
# Set BEDROCK_REGION to enable Bedrock server-side provider config.
|
|
# AWS credentials are resolved from the standard AWS environment / credential chain.
|
|
# BEDROCK_REGION=us-east-1
|
|
# BEDROCK_MODELS=us.anthropic.claude-sonnet-5,us.anthropic.claude-opus-4-8
|
|
# Optional bearer-token authentication or custom Bedrock-compatible endpoint.
|
|
# AWS_BEARER_TOKEN_BEDROCK=
|
|
# BEDROCK_API_KEY=
|
|
# BEDROCK_BASE_URL=
|
|
# DEFAULT_MODEL=bedrock:us.anthropic.claude-sonnet-5
|
|
|
|
# --- TTS (Text-to-Speech) ----------------------------------------------------
|
|
|
|
TTS_OPENAI_API_KEY=
|
|
TTS_OPENAI_BASE_URL=
|
|
|
|
TTS_AZURE_API_KEY=
|
|
TTS_AZURE_BASE_URL=
|
|
|
|
TTS_GLM_API_KEY=
|
|
TTS_GLM_BASE_URL=
|
|
|
|
TTS_QWEN_API_KEY=
|
|
TTS_QWEN_BASE_URL=
|
|
# Qwen voice cloning reuses TTS_QWEN_API_KEY. Override the target model if needed.
|
|
# TTS_QWEN_VOICE_CLONE_MODEL=qwen3-tts-vc-2026-01-22
|
|
|
|
TTS_DOUBAO_API_KEY=
|
|
TTS_DOUBAO_BASE_URL=
|
|
|
|
TTS_MINIMAX_API_KEY=
|
|
# MiniMax TTS endpoint (speech-2.8 / 2.6 / 02 / 01 series)
|
|
TTS_MINIMAX_BASE_URL=https://api.minimaxi.com
|
|
TTS_ELEVENLABS_API_KEY=
|
|
TTS_ELEVENLABS_BASE_URL=
|
|
|
|
# VoxCPM2 TTS (local, OpenAI-compatible; API key is optional)
|
|
# TTS_VOXCPM_API_KEY=
|
|
# TTS_VOXCPM_BASE_URL=http://localhost:8000/v1
|
|
|
|
# Lemonade TTS (local, no API key required)
|
|
# TTS_LEMONADE_BASE_URL=http://localhost:13305/v1
|
|
|
|
# Operators can force-disable any built-in TTS provider. Examples:
|
|
# TTS_OPENAI_ENABLED=false
|
|
# TTS_BROWSER_NATIVE_ENABLED=false
|
|
|
|
# Minimum spacing between successful classroom TTS requests, in milliseconds.
|
|
# Read at runtime (default 0). Requests are not paced until a rate limit.
|
|
# Back-off then starts from a 1000ms floor (first retry waits 2000ms), doubles
|
|
# up to 15000ms, and steps back toward this value after consecutive successes.
|
|
# That floor does not follow this value.
|
|
# TTS_MIN_INTERVAL_MS=0
|
|
|
|
# Total rate-limit back-off budget for one classroom TTS phase, in milliseconds
|
|
# (default 120000). Successful-call spacing does not consume it. Once the
|
|
# budget is spent, remaining speech clips are left silent and reported in
|
|
# ttsCoverage. 0 refuses further rate-limit waits.
|
|
# TTS_BACKOFF_BUDGET_MS=120000
|
|
|
|
# --- ASR (Automatic Speech Recognition) --------------------------------------
|
|
|
|
ASR_OPENAI_API_KEY=
|
|
ASR_OPENAI_BASE_URL=
|
|
|
|
ASR_QWEN_API_KEY=
|
|
ASR_QWEN_BASE_URL=
|
|
|
|
ASR_AZURE_API_KEY=
|
|
ASR_AZURE_BASE_URL=https://{region}.api.cognitive.microsoft.com
|
|
|
|
# FunASR (local, WAV input only, no API key required)
|
|
# ASR_FUNASR_BASE_URL=http://localhost:8000/v1
|
|
|
|
# Lemonade ASR (local, WAV input only, no API key required)
|
|
# ASR_LEMONADE_BASE_URL=http://localhost:13305/v1
|
|
|
|
# Operators can force-disable any built-in ASR provider. Examples:
|
|
# ASR_OPENAI_ENABLED=false
|
|
# ASR_BROWSER_NATIVE_ENABLED=false
|
|
|
|
# Optional local audio/video material extraction uses the first enabled server
|
|
# ASR provider above. It also requires the system `ffmpeg` and `ffprobe`
|
|
# executables on PATH; no bundled binary or npm dependency is installed.
|
|
# Without both executables, OpenMAIC skips the local extractor and uses a
|
|
# configured AliDocMind cloud extractor when available. With neither path
|
|
# enabled, media materials fail cleanly with setup guidance.
|
|
|
|
# --- PDF Processing -----------------------------------------------------------
|
|
|
|
PDF_UNPDF_API_KEY=
|
|
PDF_UNPDF_BASE_URL=
|
|
|
|
PDF_MINERU_API_KEY=
|
|
PDF_MINERU_BASE_URL=
|
|
# Optional. Defaults to "pipeline"; use "hybrid-auto-engine" only when your MinerU
|
|
# service has the required GPU/device configuration.
|
|
PDF_MINERU_BACKEND=
|
|
|
|
PDF_MINERU_CLOUD_API_KEY=
|
|
PDF_MINERU_CLOUD_BASE_URL=https://mineru.net/api/v4
|
|
# Self-hosted MinerU never falls back to MinerU Cloud implicitly: a request that
|
|
# selects self-hosted MinerU without a configured base URL fails loudly. Set this
|
|
# to "true" to explicitly opt in to MinerU Cloud as a fallback (documents then
|
|
# leave your infrastructure). Default: off.
|
|
ALLOW_MINERU_CLOUD_FALLBACK=
|
|
|
|
# AliDocMind uses an Alibaba Cloud AccessKey pair instead of a single API key.
|
|
ALIDOCMIND_ACCESS_KEY_ID=
|
|
ALIDOCMIND_ACCESS_KEY_SECRET=
|
|
ALIDOCMIND_BASE_URL=
|
|
|
|
# --- Image Generation ---------------------------------------------------------
|
|
|
|
IMAGE_OPENAI_API_KEY=
|
|
IMAGE_OPENAI_BASE_URL=https://api.openai.com/v1
|
|
|
|
IMAGE_SEEDREAM_API_KEY=
|
|
IMAGE_SEEDREAM_BASE_URL=
|
|
|
|
IMAGE_QWEN_IMAGE_API_KEY=
|
|
IMAGE_QWEN_IMAGE_BASE_URL=
|
|
|
|
IMAGE_NANO_BANANA_API_KEY=
|
|
IMAGE_NANO_BANANA_BASE_URL=
|
|
|
|
IMAGE_MINIMAX_API_KEY=
|
|
# Example models: image-01, image-01-live
|
|
IMAGE_MINIMAX_BASE_URL=https://api.minimaxi.com
|
|
|
|
IMAGE_GROK_API_KEY=
|
|
IMAGE_GROK_BASE_URL=
|
|
|
|
# OpenRouter image generation. Optional; read at runtime. One key reaches every
|
|
# image model OpenRouter hosts (FLUX, Seedream, GPT Image, Gemini, Qwen Image,
|
|
# Recraft, Krea, ...). The model list in Settings is fetched live from
|
|
# GET /images/models, so no model id is pinned here. Base URL defaults to
|
|
# https://openrouter.ai/api/v1 when left blank.
|
|
IMAGE_OPENROUTER_API_KEY=
|
|
IMAGE_OPENROUTER_BASE_URL=
|
|
|
|
# Lemonade image generation (local, no API key required)
|
|
# IMAGE_LEMONADE_BASE_URL=http://localhost:13305/v1
|
|
|
|
# Operators can force-disable any built-in image provider, including the
|
|
# client-only ComfyUI provider (it has no credential env). Examples:
|
|
# IMAGE_OPENAI_ENABLED=false
|
|
# IMAGE_COMFYUI_ENABLED=false
|
|
|
|
# --- Video Generation ---------------------------------------------------------
|
|
|
|
VIDEO_SEEDANCE_API_KEY=
|
|
VIDEO_SEEDANCE_BASE_URL=
|
|
|
|
VIDEO_KLING_API_KEY=
|
|
VIDEO_KLING_BASE_URL=
|
|
|
|
VIDEO_VEO_API_KEY=
|
|
VIDEO_VEO_BASE_URL=
|
|
|
|
VIDEO_SORA_API_KEY=
|
|
VIDEO_SORA_BASE_URL=
|
|
|
|
VIDEO_MINIMAX_API_KEY=
|
|
# Example models: MiniMax-Hailuo-2.3, MiniMax-Hailuo-2.3-Fast, MiniMax-Hailuo-02
|
|
VIDEO_MINIMAX_BASE_URL=https://api.minimaxi.com
|
|
|
|
VIDEO_GROK_API_KEY=
|
|
VIDEO_GROK_BASE_URL=
|
|
|
|
VIDEO_HAPPYHORSE_API_KEY=
|
|
VIDEO_HAPPYHORSE_BASE_URL=https://dashscope.aliyuncs.com
|
|
|
|
# OpenRouter video generation. Optional; read at runtime. One key reaches every
|
|
# video model OpenRouter hosts (Veo, Kling, Runway, Seedance, Hailuo, Wan,
|
|
# Sora, Grok Imagine, ...). The model list in Settings is fetched live from
|
|
# GET /videos/models, so no model id is pinned here. Base URL defaults to
|
|
# https://openrouter.ai/api/v1 when left blank.
|
|
VIDEO_OPENROUTER_API_KEY=
|
|
VIDEO_OPENROUTER_BASE_URL=
|
|
|
|
# Operators can force-disable any built-in video provider. Examples:
|
|
# VIDEO_GROK_ENABLED=false
|
|
# VIDEO_KLING_ENABLED=false
|
|
|
|
# --- Web Search ---------------------------------------------------------------
|
|
# Note: Grok (xAI) web search is available via chat completions + search tools,
|
|
# not as a standalone search API. Use Grok LLM provider with search_parameters
|
|
# in chat requests. See: https://docs.x.ai/docs/guides/tools/search-tools
|
|
|
|
TAVILY_API_KEY=
|
|
TAVILY_BASE_URL=
|
|
EXA_API_KEY=
|
|
EXA_BASE_URL=https://api.exa.ai
|
|
BOCHA_API_KEY=
|
|
BOCHA_BASE_URL=https://api.bocha.cn
|
|
BRAVE_API_KEY=
|
|
BRAVE_BASE_URL=
|
|
BAIDU_API_KEY=
|
|
BAIDU_BASE_URL=https://qianfan.baidubce.com
|
|
# Self-hosted SearXNG instance (no API key required)
|
|
SEARXNG_BASE_URL=
|
|
# Dedicated MiniMax web-search vars avoid conflicting with the LLM MINIMAX_* endpoint.
|
|
WEB_SEARCH_MINIMAX_API_KEY=
|
|
WEB_SEARCH_MINIMAX_BASE_URL=https://api.minimaxi.com
|
|
# Dedicated Doubao web-search vars avoid conflicting with the Doubao LLM provider.
|
|
WEB_SEARCH_DOUBAO_API_KEY=
|
|
WEB_SEARCH_DOUBAO_BASE_URL=https://open.feedcoopapi.com
|
|
# Claude (Anthropic) native web search. Dedicated vars avoid conflicting with
|
|
# ANTHROPIC_* LLM provider vars. Optional WEB_SEARCH_CLAUDE_MODELS pins the
|
|
# search model server-side (first entry wins), e.g. claude-sonnet-5.
|
|
WEB_SEARCH_CLAUDE_API_KEY=
|
|
WEB_SEARCH_CLAUDE_BASE_URL=https://api.anthropic.com/v1
|
|
WEB_SEARCH_CLAUDE_MODELS=
|
|
|
|
# Operators can force-disable any built-in web search provider. Examples:
|
|
# TAVILY_ENABLED=false
|
|
# EXA_ENABLED=false
|
|
# WEB_SEARCH_DOUBAO_ENABLED=false
|
|
# SEARXNG_ENABLED=false
|
|
|
|
# Server-only, default-OFF selector for the Native Child execution harness.
|
|
# OPENMAIC_ENABLE_PI_NATIVE_CHILD_RUNTIME=true
|
|
# Server-only, default-OFF Native Spotlight capability; does not select the runtime.
|
|
# OPENMAIC_ENABLE_PI_NATIVE_CHILD_SPOTLIGHT=true
|
|
|
|
# --- Feature Flags -----------------------------------------------------------
|
|
|
|
# Boolean feature flags accept "true" or "1". NEXT_PUBLIC_* values are compiled
|
|
# into the browser bundle at build time, so changing them requires a rebuild.
|
|
|
|
# Enable the Pro workbench entry (the workbench also requires the agent
|
|
# runtime to be configured server-side; see the Agent Runtime section).
|
|
# Implies the MAIC Editor gate below — Pro mode always ships with the editor.
|
|
# NEXT_PUBLIC_PRO_WORKBENCH_ENABLED=true
|
|
|
|
# Master gate for the MAIC Editor Pro-mode entry point. Implied by
|
|
# NEXT_PUBLIC_PRO_WORKBENCH_ENABLED; set it alone to enable the classroom
|
|
# editor on a deployment without the workbench.
|
|
# NEXT_PUBLIC_MAIC_EDITOR_ENABLED=true
|
|
|
|
# Select @openmaic/editor inside Pro mode. This does not enable Pro mode by itself.
|
|
# NEXT_PUBLIC_MAIC_EDITOR_RENDERER_ENABLED=true
|
|
|
|
# Use @openmaic/renderer for the classroom playback canvas.
|
|
# NEXT_PUBLIC_MAIC_PLAYBACK_RENDERER_ENABLED=true
|
|
|
|
# Pi-based classroom chat is enabled by default. Set this build-time flag to
|
|
# false or 0 and rebuild to roll back to the legacy classroom chat runtime.
|
|
# Pi requires model/provider tool (function) calling support.
|
|
# Docker Compose's env_file loads .env.local at runtime, not into build args.
|
|
# To rebuild with legacy chat: NEXT_PUBLIC_PI_CHAT_ENABLED=false docker compose up -d --build openmaic
|
|
# Runtime-only changes leave the built client/server choice unchanged.
|
|
# NEXT_PUBLIC_PI_CHAT_ENABLED=false
|
|
|
|
# Enable the unified PPT/Interactive courseware-reference entry in Pi playback.
|
|
# Pi chat and editor element references remain independent from this default-off build-time gate.
|
|
# Changing a NEXT_PUBLIC_* value requires rebuilding the application.
|
|
# NEXT_PUBLIC_COURSEWARE_REFERENCE_ENABLED=true
|
|
|
|
# Enable the server-side vocational task-engine generation path.
|
|
# OPENMAIC_ENABLE_VOCATIONAL=true
|
|
|
|
# Show the experimental vocational task-engine control in the client.
|
|
# NEXT_PUBLIC_SHOW_VOCATIONAL_TEST_UI=true
|
|
|
|
# Show the video export and PPTX import entry points.
|
|
# NEXT_PUBLIC_ENABLE_VIDEO_EXPORT=true
|
|
# NEXT_PUBLIC_ENABLE_PPTX_IMPORT=true
|
|
|
|
# Informational destination shown on exported Quiz/PBL cover cards. Unset or
|
|
# blank defaults to open.maic.chat; set to "off" to omit it.
|
|
# NEXT_PUBLIC_VIDEO_EXPORT_CTA_DESTINATION=open.maic.chat
|
|
|
|
# --- Agent Runtime (experimental) ---------------------------------------------
|
|
|
|
# Server-only gate for durable background agent sessions: the /api/agent
|
|
# session and owner-event control-plane routes plus the in-process session
|
|
# runner. Default OFF — while disabled, every /api/agent/sessions* and
|
|
# /api/agent/owner-events route answers 404. Truthy values are "true" or "1";
|
|
# anything else (including unset) is treated as disabled.
|
|
# OPENMAIC_AGENT_RUNTIME_ENABLED=true
|
|
|
|
# The runtime is server-backed and requires the PostgreSQL connection from the
|
|
# "Server-backed Persistence" section below: without a non-empty DATABASE_URL
|
|
# the runner never starts and the session store rejects requests, even with the
|
|
# flag above enabled.
|
|
# DATABASE_URL=postgres://openmaic:password@postgres:5432/openmaic
|
|
|
|
# REQUIRED while the runtime is enabled: MODEL_ROUTES must explicitly route the
|
|
# "maic-agent-driver" stage to a provider-prefixed model id. There is
|
|
# intentionally no fallback — without this route every agent session fails at
|
|
# run start. The route object must set api (or its alias dialect) to
|
|
# "openai-completions" or "openai-responses"; any other value is rejected, and
|
|
# a bare model id without a provider prefix is rejected too. Optional fields:
|
|
# contextWindow pins the effective context window below the provider catalog
|
|
# value (used by compaction thresholds), and thinking must never set effort
|
|
# (the tool-using driver cannot combine reasoning_effort with function tools on
|
|
# this transport).
|
|
# MODEL_ROUTES='{"maic-agent-driver":{"model":"openai:gpt-5.5","api":"openai-completions"}}'
|
|
|
|
# Runner tuning. Defaults are shown; only relevant once the runtime is enabled.
|
|
# OPENMAIC_AGENT_RUNTIME_SCAN_INTERVAL_MS=1000
|
|
# OPENMAIC_AGENT_RUNTIME_HEARTBEAT_MS=2000
|
|
# OPENMAIC_AGENT_RUNTIME_LEASE_TTL_MS=10000
|
|
# OPENMAIC_AGENT_RUNTIME_MAX_CONCURRENT=2
|
|
# OPENMAIC_AGENT_RUNTIME_MAX_ATTEMPTS=5
|
|
|
|
# Global per-tool-call execution bound for every agent run (ms). A tool call
|
|
# that neither resolves nor rejects within the budget is aborted and settles as
|
|
# an error tool-result the agent can retry or proceed from; the session does
|
|
# not die. Default 600000 (10 minutes). Tools with known longer budgets (media
|
|
# synthesis, material extraction) carry their own explicit bounds in code.
|
|
# OPENMAIC_AGENT_TOOL_TIMEOUT_MS=600000
|
|
|
|
# Conversation compaction is reserved and OFF by default. The reusable
|
|
# compaction runtime is not implemented yet — it lands in a later slice of
|
|
# work — and until then the runner runs without context transformation, so
|
|
# these knobs are inert placeholders.
|
|
# OPENMAIC_AGENT_COMPACTION_ENABLED=true
|
|
# OPENMAIC_AGENT_COMPACTION_RESERVE_TOKENS=0
|
|
# OPENMAIC_AGENT_COMPACTION_KEEP_RECENT_TOKENS=0
|
|
|
|
# Session prompts and follow-up messages are capped server-side at a fixed
|
|
# 100,000 characters; this limit is a constant and is not configurable.
|
|
|
|
# --- Proxy (optional) --------------------------------------------------------
|
|
|
|
# HTTP_PROXY=
|
|
# HTTPS_PROXY=
|
|
# Comma-separated hosts that bypass the proxy. Supports domain suffixes and *.
|
|
# NO_PROXY=localhost,127.0.0.1,.internal.example.com
|
|
|
|
# --- Misc ---------------------------------------------------------------------
|
|
|
|
# Server-side default model for API routes like /api/generate-classroom.
|
|
# Required for server-side stages (those that don't receive a client x-model):
|
|
# resolveModel throws if a stage resolves to no model (no MODEL_ROUTES entry, no
|
|
# x-model, no DEFAULT_MODEL) — there is intentionally no hardcoded vendor fallback.
|
|
# Example: anthropic:claude-3-5-haiku-20241022 or google:gemini-3-flash-preview
|
|
# OpenAI example: openai:gpt-5.5
|
|
# MiniMax example: minimax:MiniMax-M2.7-highspeed
|
|
# Bedrock example: bedrock:us.anthropic.claude-sonnet-5
|
|
DEFAULT_MODEL=
|
|
|
|
# Optional per-stage model routing (#745). A JSON object mapping a generation
|
|
# stage to a model string (`provider:model`). For most stages, resolution order
|
|
# is stage route > x-model (client) > DEFAULT_MODEL. A configured route is the
|
|
# operator's deliberate choice and wins even when the browser sends its saved
|
|
# model as x-model. `conversation-title` is the exception: when unconfigured it
|
|
# reuses the exact `maic-agent-driver` connection, never x-model or DEFAULT_MODEL,
|
|
# and keeps thinking disabled unless its own route explicitly enables it.
|
|
# At boot the server validates MODEL_ROUTES / DEFAULT_MODEL / <PREFIX>_MODELS
|
|
# and prints [config] warnings for unknown stages, unregistered providers,
|
|
# providers with no API key, and bare model ids (which still default to openai
|
|
# but are deprecated — write provider:model). Warnings only: the server starts
|
|
# regardless, so a bad value is caught here instead of at request time.
|
|
# Routable stages: scene-outlines-stream, scene-content, scene-actions,
|
|
# agent-profiles, quiz-grade, pbl-chat, pbl-v2-runtime, chat-adapter,
|
|
# generate-classroom, web-search-query-rewrite, maic-agent,
|
|
# maic-agent-driver, conversation-title.
|
|
# scene-content can also be routed per scene type with composite keys:
|
|
# scene-content:slide, scene-content:quiz, scene-content:interactive,
|
|
# scene-content:pbl. A type falls back to the base scene-content route when it
|
|
# has no key of its own (so scene-content:<type> > scene-content > x-model >
|
|
# DEFAULT_MODEL).
|
|
# pbl-v2-runtime follows the same composite fallback pattern with
|
|
# pbl-v2-runtime:instructor, pbl-v2-runtime:open-task, pbl-v2-runtime:evaluate
|
|
# and pbl-v2-runtime:simulator, falling back to the base pbl-v2-runtime route.
|
|
# maic-agent-driver is REQUIRED when the agent runtime is enabled; see the
|
|
# "Agent runtime (experimental)" section for its api/dialect constraints.
|
|
# A route value can be a model string, OR an object {"model","thinking"} where
|
|
# `thinking` is the full ThinkingConfig: mode (default|disabled|enabled|auto),
|
|
# effort (none|minimal|low|medium|high|xhigh|max), level (minimal|low|medium|
|
|
# high, Gemini), enabled (bool), budgetTokens (number), excludeReasoningOutput
|
|
# (bool). It is normalized per the model's capability. When a stage is routed:
|
|
# a set `thinking` wins over the client's thinking; with no `thinking` the routed
|
|
# model uses its own default and the client's thinking is dropped. Unrouted
|
|
# stages keep the client thinking.
|
|
# Example: cheap default, stronger model only for the heavy/conversational stages:
|
|
# MODEL_ROUTES='{"scene-content":"openai:gpt-5.4","scene-actions":"openai:gpt-5.4","pbl-chat":"anthropic:claude-sonnet-4","chat-adapter":"anthropic:claude-sonnet-4"}'
|
|
# Example: per scene type + pinned thinking (qwen budget, deepseek off):
|
|
# MODEL_ROUTES='{"scene-content:interactive":{"model":"qwen:qwen3.7-plus","thinking":{"enabled":true,"budgetTokens":8000}},"scene-content:quiz":{"model":"deepseek:deepseek-v4-pro","thinking":{"enabled":false}}}'
|
|
# MODEL_ROUTES=
|
|
|
|
# LOG_LEVEL=info
|
|
# LOG_FORMAT=pretty
|
|
# LLM_THINKING_DISABLED=false
|
|
|
|
# Opt-in parallel scene-content generation (#572). 0/unset = serial (default).
|
|
# A value > 1 fetches scene content concurrently (capped at 10); actions + TTS
|
|
# stay serial. Leave off if your API key has a low per-key concurrency quota.
|
|
# PARALLEL_SCENE_CONCURRENCY=3
|
|
|
|
# --- Local/Self-hosted Deployment ---------------------------------------------
|
|
# Set to "true" to allow private/local network URLs. That covers private
|
|
# (RFC1918), loopback, link-local and CGNAT (100.64.0.0/10, used by Tailscale
|
|
# and similar overlay networks) targets. Required for self-hosted models like
|
|
# Ollama. Do NOT enable on public deployments.
|
|
# The known cloud instance-metadata and credential endpoints (169.254.169.254,
|
|
# 169.254.170.2, 169.254.170.23, 100.100.100.200, 168.63.129.16, 192.0.0.192,
|
|
# fd00:ec2::254, fd00:ec2::23, metadata.google.internal) are blocked with or
|
|
# without this flag, as are IANA reserved, multicast and broadcast ranges. That
|
|
# is a fixed address list, not a general link-local block, and under the flag a
|
|
# hostname whose DNS lookup fails, times out or returns no answer is still
|
|
# allowed through.
|
|
# ALLOW_LOCAL_NETWORKS=true
|
|
|
|
# Build-time, space-separated CSP frame-ancestor sources in addition to 'self'.
|
|
# Configure only origins you trust to embed OpenMAIC, then rebuild the app. The
|
|
# Docker and Compose builds also accept this value as a build argument.
|
|
# ALLOWED_FRAME_ANCESTORS=https://partner.example.com
|
|
|
|
# Optional MP4 render service (issue #866). When set, the in-app "Export Video"
|
|
# menu offers one-click MP4 rendering; when unset, it degrades to downloading a
|
|
# project ZIP for local CLI rendering. Point this at the isolated render-service
|
|
# container (see render-service/ and the "video-export" docker-compose profile).
|
|
# This is operator-supplied trusted config: the app forwards uploads to it
|
|
# without the SSRF guard, so a private/compose-network target works WITHOUT
|
|
# setting ALLOW_LOCAL_NETWORKS.
|
|
# RENDER_SERVICE_URL=http://render-service:9000
|
|
|
|
# Render-service-only opt-in for bounded local chunk execution. These variables
|
|
# are read at runtime by the isolated render-service container; the public HTTP
|
|
# API is unchanged. Defaults keep the existing in-process renderer.
|
|
# RENDER_CHUNK_EXECUTION=false
|
|
# RENDER_CHUNK_COUNT=1
|
|
# RENDER_CHUNK_WORKERS=1
|
|
# RENDER_MAX_PARALLEL_CHUNKS=1
|
|
# RENDER_CHUNK_SIZE_FRAMES=0
|
|
# RENDER_TARGET_CHUNK_FRAMES=0
|
|
|
|
# Honor x-forwarded-for / x-real-ip when deriving client identity for both
|
|
# render-service admission and access-code verification throttling.
|
|
# Enable only behind a trusted reverse proxy that overwrites these headers.
|
|
# TRUST_PROXY_HEADERS=true
|
|
|
|
# --- Server-backed Persistence ------------------------------------------------
|
|
|
|
# Build-time client switch: the browser uses the embedded /api/persistence
|
|
# endpoint. Requests are attributed to the owner the owner identity seam
|
|
# resolves (by default the anonymous owner cookie); there is no separate
|
|
# persistence credential.
|
|
# NEXT_PUBLIC_PERSISTENCE=1
|
|
|
|
# Runtime PostgreSQL connection.
|
|
# DATABASE_URL=postgres://openmaic:password@postgres:5432/openmaic
|
|
|
|
# Logical bytes of live assets each owner may hold (default 10 GiB; 0 opts
|
|
# out). Per owner, not per deployment: with anonymous-cookie owners a cleared
|
|
# cookie is a new owner with a fresh quota.
|
|
# ASSET_QUOTA_BYTES=
|
|
|
|
# The anonymous owner cookie carries the `Secure` flag in production builds.
|
|
# Safari refuses to store `Secure` cookies served over plain http://localhost
|
|
# (unlike Chromium/Firefox, it does not special-case localhost), so every
|
|
# request mints a fresh anonymous owner and owner-scoped document writes fail
|
|
# with 403. Deployments that serve plain HTTP (no TLS) opt out with the exact
|
|
# value 0 — any other spelling (false, no) leaves `Secure` on:
|
|
# COOKIE_SECURE=0
|
|
# Only do this on a trusted network or locally: without `Secure` the cookie
|
|
# travels in the clear and can be replayed by anyone on-path.
|
|
|
|
# Single-tenant deployments: resolve every request to this fixed owner id
|
|
# instead of a per-browser anonymous cookie. One team behind one ACCESS_CODE
|
|
# then shares one course library — a second browser no longer sees an empty
|
|
# list, and `publish` becomes usable (it refuses anonymous owners, since
|
|
# publishing a cookie partition is not something the product allows).
|
|
# Unset, every browser keeps its own partition and this block changes nothing.
|
|
#
|
|
# This answers WHO owns a course; it is not a substitute for DATABASE_URL
|
|
# above, which answers WHERE one lives. Ownership is resolved the same way on
|
|
# either backend, so a second browser sees an empty list on Postgres too — a
|
|
# multi-user or multi-instance deployment still wants server persistence.
|
|
#
|
|
# ACCESS_CODE is required: without one the middleware lets every request
|
|
# through, so a single shared owner would expose one readable, editable and
|
|
# publishable course library to anyone who can reach the deployment. Setting
|
|
# this without an access code fails startup. Everyone holding the access code
|
|
# then sees, edits and can publish the same courses, with no per-person
|
|
# attribution — which is what SECURITY.md already says ACCESS_CODE is, and not
|
|
# user authentication. 1-128 characters of [A-Za-z0-9._-]; the reserved `anon:`
|
|
# prefix is rejected, and a malformed value fails startup rather than being
|
|
# ignored.
|
|
# PERSISTENCE_SHARED_OWNER_ID=
|
|
|
|
# Real accounts (your own sessions, API keys, or an identity gateway such as
|
|
# oauth2-proxy or Cloudflare Access) are not configured here: a host registers
|
|
# owner auth methods in instrumentation.ts. See "Owner identity" in README.md,
|
|
# which includes a recipe for verifying a gateway-signed JWT. When a host
|
|
# registers methods and still wants the shared owner above, it includes
|
|
# sharedTeamAuthMethod() last; setting PERSISTENCE_SHARED_OWNER_ID beside a
|
|
# registration that leaves it out fails startup.
|
|
|
|
# Store asset bytes in S3 instead of PostgreSQL. Region, endpoint, and credentials
|
|
# are resolved through the standard AWS SDK environment / credential chain.
|
|
# ASSET_S3_BUCKET=
|
|
|
|
# Opt into indirect asset byte egress: answer asset byte GETs with a short-lived
|
|
# signed S3 URL (a 302, or a JSON descriptor for the packaged client) instead of
|
|
# the bytes. Unset or "direct" keeps direct egress (the safe default). Requires
|
|
# the object store's CORS to admit this app's origin and expose Content-Type, and
|
|
# the signing identity to hold s3:ListBucket on the bucket so a missing key
|
|
# answers 404 NoSuchKey rather than 403.
|
|
# ASSET_BYTE_EGRESS=redirect
|
|
|
|
# Claiming anonymous work on sign-in (see README "Claiming anonymous work").
|
|
# explicit (default): the app calls POST /api/identity/claim; auto: the first
|
|
# request that carries both an account and an anonymous owner claims it.
|
|
# OWNER_CLAIM_TRIGGER=explicit
|
|
|
|
# How long an owner-scoped write (default 30000) and a claim (default 5000) wait
|
|
# for the owner's identity lock before answering 503 OWNER_BUSY with
|
|
# Retry-After. Positive integers in milliseconds, checked at startup.
|
|
# OWNER_WRITE_LOCK_WAIT_MS=30000
|
|
# OWNER_CLAIM_LOCK_WAIT_MS=5000
|
|
|
|
# The asset collector is enabled by default when DATABASE_URL is configured.
|
|
# ASSET_COLLECTION_ENABLED=true
|
|
# ASSET_COLLECTION_INTERVAL_MS=900000
|
|
# ASSET_COLLECTION_GRACE_MS=3600000
|
|
|
|
# Root directory for the file-backed classroom store: the classroom JSON
|
|
# documents plus their generated media/audio. Defaults to <cwd>/data/classrooms.
|
|
# This moves ONLY the classrooms — the classroom generation job store is not
|
|
# configurable and stays at <cwd>/data/classroom-jobs.
|
|
# OPENMAIC_CLASSROOMS_DIR=/var/lib/openmaic/classrooms
|
|
|
|
# How long an allocated asset stays pending -- stored, but not yet named by any
|
|
# document -- before the collector expires it. A client stores bytes first and
|
|
# writes the id into the document afterwards, so this window has to outlive a
|
|
# whole generation pass plus a write-back that is waiting for its slide to be
|
|
# built; the default is one day for that reason. A value that is not a positive
|
|
# integer stops the server from starting.
|
|
# ASSET_PENDING_TTL_MS=86400000
|
|
|
|
# --- Access Control -----------------------------------------------------------
|
|
# Set a password to restrict site access. When set, users must enter this code
|
|
# before using the app. Leave empty or remove to disable access control
|
|
# (fail-open: middleware lets every request through with no credential).
|
|
# Read at runtime. When unset, the server logs a one-time startup warning;
|
|
# GET /api/health reports accessCodeConfigured: false. Set this before exposing
|
|
# the server to a network. The warning does not prevent local zero-config use.
|
|
# Use a long random value (at least 16 characters from a random generator):
|
|
# this code is the only secret guarding the deployment. The code is remembered
|
|
# in a signed token stored in an HTTP-only cookie for 7 days; the lifetime is
|
|
# enforced server-side, so visitors re-verify after it expires.
|
|
# ACCESS_CODE=your-secret-code
|
|
#
|
|
# Verification is rate limited only when TRUST_PROXY_HEADERS=true (see above):
|
|
# behind a trusted reverse proxy that overwrites x-forwarded-for / x-real-ip,
|
|
# each client is limited to 10 attempts per 60 seconds, and a trusted client's
|
|
# successful verification clears its own counter. Without a trusted proxy the
|
|
# app cannot attribute a request to a client, so there is no throttle; rely on
|
|
# the length and randomness of the code instead.
|