mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-02 01:15:18 +08:00
* ci: run CI for the persistence-default integration branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(docker): start PostgreSQL by default and add a single-user owner * feat(docker): start PostgreSQL by default and add a single-user owner `docker compose up` now runs a server-backed app against a bundled PostgreSQL, started only once PostgreSQL is healthy and published on 127.0.0.1 only, with a new built-in singleUser owner auth method: every request resolves to one fixed owner that may publish, and no anonymous cookie is minted. Single-user mode (OWNER_SINGLE_USER=true, OWNER_SINGLE_USER_ID default "local") runs with or without ACCESS_CODE. Without one, the server logs one prominent startup warning that anyone who can reach it shares, edits and can delete the single library; it never inspects request peers or forwarding headers. It excludes PERSISTENCE_SHARED_OWNER_ID and follows the sharedTeam registration rules. Its principal gets a claim candidate, so a browser's earlier anonymous work is claimed into the single owner (OWNER_CLAIM_TRIGGER=auto in the Compose defaults). Compose defaults live in docker-compose.defaults.env, read before .env.local so they can be overridden. The app warns when its declared published address (OPENMAIC_PUBLISH_ADDRESS, passed by Compose) is not loopback while PostgreSQL uses the default password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(docker): keep claims explicit and let .env.local set DATABASE_URL - Compose no longer defaults OWNER_CLAIM_TRIGGER to auto: an irreversible merge of every browser's anonymous library into the single owner must not happen on upgrade. The single-user principal still gets a claim candidate, so POST /api/identity/claim works; the docs explain it and warn about auto on a deployment several people used. - The bundled DATABASE_URL moves to docker-compose.defaults.env, read before .env.local, so an external database or a rotated password set there keeps working. The password is inserted unencoded, so it must be letters and digits (documented). - Upgrade docs no longer claim browser-stored courses are copied as they are opened: they stay in the browser, are not deleted, and are moved by the automatic browser-to-server migration shipped with this work. - Single-user mode without ACCESS_CODE now logs one warning instead of two; the docs note that the single owner id is permanent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(persistence)!: server-backed persistence is the only persistence (#1707) * feat(persistence)!: make server-backed persistence the only persistence Courses, folders and folder membership, chat history and learner runtime, and generated media are now always stored on the server through the embedded /api/persistence endpoint. The browser keeps no durable data of its own. - Remove the build-time NEXT_PUBLIC_PERSISTENCE switch: the client bootstrap always configures the HTTP document, runtime and asset seams, and the Dockerfile, docker-compose.yml, the Pi route and the whiteboard runtime no longer read it. Unconfigured seams refuse to resolve instead of falling back to IndexedDB, and the learner key is always the server-derived one. - Remove the browser write paths: the browser document, runtime and asset stores as backends, the browser-mode branches of stage, folder, chat, media, narration and import storage, the Dexie backup export/import and the whole-database clear. The library lists through /api/stages, folders through /api/folders, and membership now goes through /api/folders/members. Regeneration always forks to a fresh asset. - Device-local state moves to its own IndexedDB database (lib/device-storage, maic-device-cache): the local media and narration cache and refused-bytes retention, staged PDF images, undo history, browser voice profiles (carried over once from the old database) and the auto-voice cache. "Clear Local Cache" clears exactly that and no longer claims to delete classrooms or chat history. - Keep the pre-server data readable for the one-way importer: the Dexie schema and read-only accessors for its tables and for the browser document, runtime and asset databases live in lib/legacy-browser-storage, which nothing on the regular paths reads and which has no write path. - Require a database: the server exits at boot without DATABASE_URL, with a message naming `pnpm db:up` and `docker compose up`. `pnpm db:up` / `pnpm db:down` start and stop the Compose postgres service alone, published on 127.0.0.1 through docker-compose.db.yml; .env.example carries the matching local DATABASE_URL. - E2E specs seed their courses through the persistence endpoint, each under a fresh id, instead of writing IndexedDB. - Guard tests pin the boot requirement, the absence of any NEXT_PUBLIC_PERSISTENCE read, and the legacy-storage boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: run the E2E suite against PostgreSQL The app refuses to start without DATABASE_URL, so the E2E job gets a postgres:16 service and a job-level DATABASE_URL. The unit, storage and render-service jobs are unchanged and need no database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: document always-on server persistence and the database requirement README (English and Chinese), the deployment guide in every locale, the extension cookbook and the changelog now describe PostgreSQL as required: `pnpm db:up` for local development, `docker compose up` for a deployment, and an external database for Vercel and other serverless hosts (the Deploy button prompts for DATABASE_URL). They list what stays in the browser, drop the NEXT_PUBLIC_PERSISTENCE instructions, and say that courses an earlier browser-only build stored stay in the browser until the one-way importer moves them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(pbl): use the server-derived learner key for PBL v2 runtime PBL v2 synchronization, hydration and drain resolved the learner key from their default watermark KV store, which reads or mints a device key in localStorage instead of asking runtime configuration. The runtime store is the server one, which refuses any key but the owner's, so saving a course with a PBL v2 scene failed before the document write, and the minted key landed in the slot the one-way importer reads as the pre-server runtime partition. The learner key now comes from getLearnerKey(args.kv): the configured server-derived key, or an explicitly injected store. The default KV store still holds the drain watermarks and nothing else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(persistence): pin that clearing the cache keeps legacy databases - Seed the four pre-server databases (MAIC-Database, maic-documents, maic-runtime, maic-asset-pool) with real rows, run Clear Local Cache, and assert every database and row survives; assert the device database name differs from every legacy name. - The legacy-module import rule now also catches relative static, side-effect, dynamic and require imports at any depth. - New rule: the runtime learner key is never read from a KV store the caller made up, only from configuration or an injected store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(dev-db): run the development database as its own Compose project `pnpm db:up` / `db:down` shared the checkout's default Compose project with `docker compose up`, so they recreated or stopped a running stack's database. docker-compose.db.yml now declares its own project (`openmaic-dev-db`), which gives the development database its own container and data volume; it no longer touches the stack, and the two do not share data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: start the development database before `pnpm dev` CONTRIBUTING, the getting-started guide in every locale and the startup modes reference now run `pnpm db:up` and set DATABASE_URL before `pnpm dev`. README, the deployment guides, the cookbook and the changelog describe `pnpm db:up` as a separate development database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(persistence): sharpen the legacy-storage and learner-key guards - The Clear Local Cache test closes its seeding connections to maic-documents and maic-runtime, so a regression deleting either fails on the assertion that names the database instead of by timeout. - The legacy-module and Dexie import rules accept comments inside import() / require(), so a webpackIgnore hint no longer hides one. - The learner-key rule also flags an injected-looking name that is bound to a default local store in the same file; its remaining limit is documented, with the behavioural PBL test as the guard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * build(dev-db): pin the development database's Compose project `pnpm db:up` / `db:down` now pass `-p openmaic-dev-db`, which takes precedence over COMPOSE_PROJECT_NAME from the shell or a .env file; the file's `name:` alone could be overridden and point the scripts at a running stack's database. The compose test pins the flag. The docs (all locales) and the file header say the development database is shared by every checkout on the machine, and how to run a separate one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * feat(persistence): import legacy browser data into the server, once (#1708) * refactor(persistence): prepare the seams the legacy browser importer uses - Legacy module: read-only accessors for the auto-voice cache, the course ids the old media and narration tables name, the pre-server learner key, and asset bytes from the browser asset pool. - Narration adoption exports its row ownership rule so the importer applies the same one to rows of the old database. - The quiz legacy migration is split into a non-deleting core (importLegacyQuizSnapshot) and the regular path, which still deletes the keys it migrated. - The lazy document migration exports its snapshot canonicalizer. - Clear Local Cache keeps the importer's per-owner ledgers, as it keeps the pre-server learner key. - A library-changed window event lets an open library list courses that arrive in the background. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * feat(persistence): import legacy browser data into the server, once On the first load after the upgrade, the client moves what earlier builds kept in this browser to the server, for the owner the server resolves: courses from the browser document store and the original tables (with chat, learner runtime, playback position, roster, folders and membership, pre-runtime quiz state), their media bytes through commitToPool and the existing write-back funnels, the bytes of server courses from earlier opt-in server builds that only the old tables hold, and device-only rows (failure records, refused bytes, auto-voice clips) into the device cache. It is automatic and silent, runs after the page is idle, never writes to a legacy store, and records every step in a per-owner ledger so it resumes after a crash and never imports twice. Runs are serialized across tabs with Web Locks. A course the owner already has stays authoritative; one whose id another owner holds is imported under a derived fresh id; one the owner deleted on the server stays deleted. Transient failures retry on a later load with backoff; permanent ones are recorded per item. The module is temporary and self-contained; its README lists the removal steps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): pin that each owner keeps its own import ledger Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): run the legacy import against the app routes and a browser A PostgreSQL suite routes every request the importer and the app seams send to the real persistence, library and folder handlers, owner resolved from the anonymous cookie: a full import, a course another owner holds, a course the owner deleted, and an existing course whose media is filled in. An end-to-end spec seeds an old pre-server database in Chromium, loads the app, and checks that the course reaches the library with its narration uploaded, stays in the browser untouched, and is visible from a fresh browser context with the same owner cookie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs: describe the one-way import of legacy browser data The README (and its Chinese version), the deployment guide in every locale and the changelog now say what the importer does instead of announcing it: courses an earlier browser-only build kept in the browser move to the server automatically on the first load after the upgrade, and the browser copy stays untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): probe server assets through the shared asset-URL owner The importer asked the pool directly whether an id exists, which the asset-URL ownership boundary forbids outside use-asset-url. It now uses assetRefExists, the existing metadata-only probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): guard the importer's pool probe like every other entry point Only a reference the pool could have issued is probed, as the lease guard requires; the importer is added to the list of guarded entry points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): import legacy browser data once per browser Review round 1 of the legacy browser importer. - Once per browser, not once per owner: the data belongs to whoever used the browser before the upgrade, so the first owner the import runs for claims it and any other owner gets nothing. The ledger is one key, maic:legacy-import:v2, recording the owner only as a SHA-256 digest (an anonymous owner id is a bearer credential) and a random salt. - Handoff: when the claiming owner is retired (403 OWNER_RETIRED), or the current owner holds courses or folders the importer created (a claim moved them), the current owner continues the unfinished items; courses already done are never imported again, so a claim neither duplicates nor resurrects them, and a half-copied course keeps its runtime. - Fresh ids come from the salt and the legacy id through SHA-256, not from the owner: unlinkable, unpredictable, and the same in every tab. - 401 pauses with backoff instead of closing the import; 403 FORBIDDEN_LEARNER stops the run and leaves the item pending. - Without Web Locks: ledger writes merge with the stored copy, and the library is listed again right before a course is placed, so a second tab does not import a course the first one just created. - Media the server refuses for good gets the app's failed-media record instead of a dangling reference; the legacy bytes stay. - A legacy whiteboard or PBL session is not created when the server already has an active one of that kind. - A folder that is gone leaves the course unfiled instead of failed, and a membership naming a folder the old database lacks counts as unfiled. - Narration rows from before the course column are adopted when exactly one legacy course names their key; unconvertible chat rows are noted; an unreadable document-store copy falls back to the original tables; PBL v2 documents go through the save-path strip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): cover the importer's handoff, tabs, refusals and review gaps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs: say the legacy import runs once per browser The README (and its Chinese version), the deployment guide in every locale, the changelog and the module README now say that the first owner to load the upgraded app in a browser claims its legacy data, what the handoff to an account does, that the ledger holds no owner id, and how refused media and the new pause and skip cases behave. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): note runtime the importer cannot find without the old learner key Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * feat(identity): let an owner confirm it absorbed a given owner through a claim GET /api/identity/merged-from?salt=<hex>&digest=<hex> answers whether a claim merged into the requesting owner an owner whose id hashes to SHA-256(salt, id). It reads only the requester's own owner_merges rows, never lists or names an owner, resolves the owner like every other owner-scoped route, and is uncacheable. The one-way import of pre-server browser data uses it to decide whether a different owner may continue an import the first owner started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): hand the legacy import to another owner only on a confirmed claim Review round 2 of the legacy browser importer. - The only way the browser's legacy data moves from the owner that claimed it to a different owner is the server confirming, through GET /api/identity/merged-from, that the current owner absorbed that owner through a claim. The inferred signals (an owner that never wrote, a library listing that holds imported courses) and the retired flag are gone; a 403 OWNER_RETIRED only stops the run and leaves items pending. - The first owner is bound only once the run's first authenticated request of its own (the library listing) succeeded, so a run that dies before that leaves the ledger unclaimed. - The owner is recorded as SHA-256 of the per-browser salt and the owner id; the server computes the same value from the salt the browser sends. - A "not absorbed" answer is reused for an hour instead of asking on every load, and the claiming owner stored first survives another tab's save unless that tab handed the import over; completion survives too. - Run-level failures (401, OWNER_RETIRED, FORBIDDEN_LEARNER, OWNER_BUSY) from any call stop the run and leave the course pending, including the "is the id taken" read and the library re-list, which could previously mark the course failed and close the import. - Refused media without a generation request (the user's own) gets a new non-retryable ASSET_REFUSED code, so it shows as failed without a Retry that could only fail; a skipped singleton runtime session leaves a note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): pin the importer ledger's owner merge and the singleton-skip note Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs: the legacy import moves to another owner only on a confirmed claim The README (and its Chinese version), the deployment guide in every locale, the changelog and the module README now say that the import runs once per browser and moves to another owner only when that owner claimed the original one, confirmed by GET /api/identity/merged-from; that the owner is recorded as a salted digest; and how refused user media shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): never let a transient failure settle a legacy import item Review round 3 of the legacy browser importer. - A read of the old browser stores that fails in storage (an aborted IndexedDB transaction, a closed database) now pauses the run and leaves the course pending. Only a record that cannot be migrated, parsed or validated is skipped. This covers the course read (which no longer falls back to the older table copy on a storage failure), and the quiz-scene and speech indexes, which used to read a failure as "no scenes" or "no holder" and drop quiz state or narration for good. - After a session-id collision, a failed read of the session propagates and is retried; only a successful read that shows the id taken skips it. - Without Web Locks, two tabs of different owners can both find the ledger unbound. Every ledger write keeps the owner stored first, and the run now re-reads the stored ledger after binding, before folders, before each course and before each course document is created, and stops if another owner holds it. - The "not yours" answer is reused for ten minutes instead of an hour, a stored wait further ahead than the longest the importer sets (a clock that ran ahead) counts as passed, and expired answers are pruned. - Generated media refused without an error code keeps its Retry; only media nothing can regenerate gets ASSET_REFUSED. A poster that fails transiently leaves the video pending instead of being dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): cover transient reads, racing owners, the not-yours cache and the claim client Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(persistence): say what the ledger's salt protects, and the new pauses The salt defeats precomputed and cross-browser tables, not a targeted guess against one browser's ledger; the module README and the digest's comment now say so. The README also covers storage read failures, which pause instead of skipping, the two-owner race without Web Locks, and the ten-minute recheck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * feat(identity): bind a browser's legacy data to one owner on the server The one-way import of pre-server browser data now asks the server whose data it is, instead of deciding in the browser. - legacy_import_bindings (browser_id primary key, owner_id, timestamps), provisioned with owner_merges on fresh and upgraded databases. - POST /api/identity/legacy-import-binding {browserId}: one atomic insert (ON CONFLICT DO NOTHING) and a read-back; answers only whether the requesting owner holds the browser. Same-origin JSON, resolved and write-fenced like every owner write, 400 for a malformed id. - A claim participant re-keys the claimed owner's bindings to the account inside the claim transaction, so the account simply holds the browser. - Owner resolution refuses any request carrying X-OpenMAIC-Legacy-Import with 409 LEGACY_IMPORT_NOT_BOUND unless the owner it resolves to holds that browser: one central fence every owner-scoped route goes through. Requests without the header are unaffected. - GET /api/identity/merged-from is removed; the binding replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): import through fenced clients bound by the server Review round 4 of the legacy browser importer. - The ledger holds a random browser id and no owner information; the owner digest, the "not yours" cache and the client-side handoff are gone. A run first asks the server to bind the browser and imports only when the requesting owner holds it. - Every importer request goes through its own clients that carry the browser id, so the server refuses any write under an owner that does not hold the browser: after a cookie switch in another tab, from a page whose memoized owner is stale, or from a tab that lost the binding race. The runtime learner key is asked for during each run, never taken from the page. 409 LEGACY_IMPORT_NOT_BOUND stops the run with items pending. - The write-back funnels, commitToPool and the PBL save-path strip take an optional store, put or runtime, so the importer passes its fenced ones. - A failed read of one legacy course holds up only what it could affect: the quiz-scene and speech indexes leave the course out as unindexed, and only quiz state or narration it could own waits. Read failures are counted per course; after five failing runs over at least a day, a course whose document-store copy cannot be read falls back to the table copy, or is skipped with the reason when there is none. - Runtime ids are rewritten by replacing only their course segment, so a short course id cannot corrupt the prefix or the learner segment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): cover the server binding, its fence, bounded reads and short ids Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs: the server binds a browser's legacy data to its first owner The README (and its Chinese version), the deployment guide in every locale, the changelog and the module README now say: once per browser; the server binds the browser to the first owner; a claim carries the binding to the account; the importer's requests are refused for any other owner. They also describe the bounded pause for a course the old storage cannot read, and the removal steps for the server side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): keep a dropped asset request pending in the legacy import The asset client reports a fetch that got no answer (a dropped request, an aborted upload, the existence probe's deadline) as status 0 with HTTP_REQUEST_FAILED, which the importer read as a final refusal: the media was marked failed and the course could complete without it. Read that code as transient, keep the client's local validation failures final, and treat a 2xx/3xx answer a client could not use as transient too. Unit tests drive every client the importer uses with a rejected fetch and a non-JSON answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(persistence): ask a refused binding again only every ten minutes A browser whose legacy data another owner holds sent the binding request (a write) on every page load. Record a ten-minute recheck in the ledger (clamped like every other deferral), so a claim that moves the binding is still picked up, and open the old databases only once the browser is bound. The read budget now counts consecutive failing runs (a run that reads the course resets it) and counts a first failure dated in the future from the next one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(identity): answer 503 with the cookie when the import fence cannot read The legacy import fence now reads the header name from the shared constant, and a database error while reading the binding answers the usual JSON error (503 PERSISTENCE_UNAVAILABLE) with the resolution's Set-Cookie values instead of escaping as a bare 500. Route tests cover that, a bind by an owner a claim retired (403 OWNER_RETIRED, no row), and one header name on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(persistence): cover dropped asset requests, the recheck and the read budget With the real asset client's error for an upload and an existence probe, the media stays pending and a later run imports it. A non-holder's five loads send one binding request and never open the old databases, and a claim still hands the import to the account after the delay. The read budget's run count and time span each hold on their own, a clock that ran ahead does not hold a course open, and a good read resets the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(persistence): complete the legacy import removal steps List every file the removal touches (the claim scenarios test, the claim participant table and list, every doc), keep the bindings table for one release after the code goes because of rolling deploys and give the DROP statement for that release. Add the claim's eighth participant to the README lists, fix the fresh-id wording in the changelog, and describe the recheck delay, the transient status-0 asset failures and the consecutive read budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * fix(persistence): keep legacy quiz state through Clear Local Cache until the import completes Pre-runtime quiz drafts, answers, results and attempt ids exist only in localStorage, and the one-way importer copies them to the server with their course. Clear Local Cache deleted them even while the import was still pending (it waits for an idle page and can stay pending after a failure), so that quiz progress was lost for good. Clear Local Cache now keeps the four quiz key families unless the importer's ledger records the import as complete, read through a new `legacyImportIsComplete` helper in the ledger module. The ledger key moves there too, so the two modules do not import each other. Once the import is complete, the keys are cleared as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs: show DATABASE_URL set in the local development setup The getting-started guide showed the required DATABASE_URL line commented out, so copying the block left the server unable to start. Each locale now shows `pnpm db:up` and, separately, the `.env.local` line to set. The `.env.example` header said every variable is optional; it now says that provider variables are, and that DATABASE_URL is required except under docker-compose.yml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(changelog): drop two upgrade notes the final code contradicts The Compose entry said its default DATABASE_URL overrides .env.local; the defaults file is read first, so a value in .env.local wins, as the same entry says further on. The runtime-session entry offered turning server persistence off as an upgrade path; that switch no longer exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(env): restore the ACCESS_CODE warning notes in .env.example A later change to the example file dropped the lines saying that an unset ACCESS_CODE fails open, that the server then logs a one-time startup warning and GET /api/health reports accessCodeConfigured: false. They are back, and say that single-user mode logs its own warning in place of the generic one, as the startup code does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * ci: run every app PostgreSQL suite in the contract job The app-domain step named its suites one by one, and seven had been left off, among them the legacy importer's route suite. No other job gives the app suites a database, so they skipped everywhere. The step now lists every app `*.pg.test.ts`; all of them work in a schema or database of their own, or truncate only their own tables, and pass before and after the storage package suite in the same database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(identity): establish the anonymous owner before the first API request (#1721) * fix(identity): establish the anonymous owner on the page response On a browser's first load the page sent several API requests without an owner cookie; each minted its own anonymous owner and set its own cookie, and the last answer to arrive won. Work done in between (the legacy import binding, a first course write) then belonged to an owner the browser no longer presented. The middleware now mints the anonymous cookie on document navigations that carry no valid one, with the route handlers' own value format and attributes (the cookie module is Edge-safe now: Web Crypto instead of node:crypto), and forwards it on the request so the page render resolves the same owner. API, RSC, prefetch and Server Action requests never mint there, and a valid cookie is never replaced. It mints only when neither OWNER_SINGLE_USER nor PERSISTENCE_SHARED_OWNER_ID is set; host registrations are invisible to an Edge middleware, so the README tells hosts where to skip it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(legacy-import): bind only an owner the browser already presents A binding request whose owner was minted by that very request answers 409 OWNER_NOT_ESTABLISHED with the minted cookie and binds nothing: other cookieless requests may still be minting owners of their own, and a binding to this one could be left with an owner nobody presents. The importer treats it as transient and retries on a later load. It also says plainly when a request after its own successful bind is refused as another owner (the owner cookie changed mid-run); items stay pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(legacy-import): cover a first visit whose first answers arrive out of order Against the real routes and PostgreSQL: a browser without a cookie loads a page, sends two requests at once, and the first answer reaches it only after the importer bound the browser; the import completes under the one owner the page established. A second case binds nothing for an owner the bind itself minted and imports on the next run. The E2E drives the same ordering in Chromium by holding the first owner-scoped answer until the binding answered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * test(legacy-import): drop an unused initial assignment Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(changelog): note the page response now sets the anonymous owner cookie Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(identity): let hosts turn page-response minting off The middleware cannot see owner auth methods a host registers, so a host with anonymousFallback: false still got an anonymous cookie on a cookieless page load. Beside the host credential it is a claim candidate; with OWNER_CLAIM_TRIGGER=auto it is claimed and cleared on the next request, again after every such page load. OWNER_ANONYMOUS_PREMINT (default on; true/1/false/0, anything else fails startup) is read by the middleware before minting. At startup the server warns once when a registration turns the anonymous fallback off while pre-minting is still on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(identity): document OWNER_ANONYMOUS_PREMINT Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(identity): keep the anonymous identity for 400 days and renew it in use The anonymous cookie had a fixed 30-day lifetime from its mint and was never renewed, so every anonymous visitor lost their whole library 30 days after the first visit however active they were; with persistence on the server only, that affects every anonymous deployment. It now lasts 400 days (the browser cap, one shared constant), and every route handler and Server Action response that resolves to a valid anonymous cookie re-sends the same value with a fresh Max-Age (best-effort where next/headers refuses writes). Page responses never renew, so pages stay cacheable, and a host principal never renews the anonymous cookie beside it. A response that clears the cookie (a claim, a retired owner) must not renew it, whatever order a route merged its headers in: withRequestOwner, and the owner-events retired answer, keep only the clearing value for a cookie a value clears. A renewal that lands after a claim cleared the cookie is pinned by a test: the next write with it, alone or beside the account, writes nothing, and its answer clears it again without renewing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * docs(identity): 400-day renewed anonymous cookie; when to turn pre-minting off Hosts set OWNER_ANONYMOUS_PREMINT=false only when their registration sets anonymousFallback: false; hosts that keep anonymous visitors need pre-minting and skip it per request in middleware for requests their methods authenticate. The CHANGELOG notes the new lifetime and renewal, and that a lost cookie means a new owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz * fix(identity): forward owner cookies from routes that resolve the owner directly The Pi chat and whiteboard-visibility routes and asset-id document extraction resolved the owner themselves and never sent its Set-Cookie values back, so an anonymous identity used only through them was never renewed (and a minted one never stored). attachOwnerCookies attaches a resolution's cookies to a response a route built itself, with the clear-wins rule, before a stream's body starts. Both Pi routes answer through it, success and error alike; resolveServerAsset hands the cookies back with every answer and the extraction route attaches them. A guard test scans for every module outside the identity seam that calls the owner resolution (or the asset helper) directly and requires it to forward the cookies; the two callers that cannot are listed with the reason. Behaviour tests cover the three paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRME5T4oEaQ4EBUzrDXMKz --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: wyuc <dhq1204@yahoo.com>
728 lines
33 KiB
Bash
728 lines
33 KiB
Bash
# =============================================================================
|
|
# OpenMAIC Environment Variables
|
|
# Copy this file to .env.local and fill in the values you need.
|
|
# Provider variables are optional — only configure the providers you want to use.
|
|
# DATABASE_URL is required, except under docker-compose.yml, which sets it for
|
|
# the bundled PostgreSQL (see "Persistence" below).
|
|
# 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 uses the PostgreSQL connection (DATABASE_URL) from the
|
|
# "Persistence" section below, which every deployment configures.
|
|
|
|
# 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=
|
|
|
|
# Retryable-failure model fallback (optional). When a generation call fails with
|
|
# a retryable failure (SDK-classified transient error, timeout, network error,
|
|
# quota 429, capacity 503), the shared callLLM layer retries ONCE on the
|
|
# fallback model. Content-safety rejections and other 4xx failures never fall
|
|
# back. Unset = behaviour unchanged.
|
|
#
|
|
# Scope notes:
|
|
# - Per-stage fallback only applies to stages listed in LLM_STAGES
|
|
# (see lib/server/model-routes.ts); keys outside it are ignored.
|
|
# - On the shared callLLM layer the fallback fires on retryable ERRORS and,
|
|
# when a fallback model is configured, on genuinely empty output as well.
|
|
# Non-empty results that fail a caller-supplied validator never fall back.
|
|
#
|
|
# Two ways to configure (per-stage wins over global):
|
|
# A) per route: add a "fallback" field to a MODEL_ROUTES route object
|
|
# MODEL_ROUTES='{"scene-content":{"model":"openai:gpt-5.4","fallback":"qwen:deepseek-v4-pro"}}'
|
|
# B) global: one fallback for every callLLM stage
|
|
# MODEL_FALLBACK=
|
|
|
|
# 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
|
|
|
|
# --- Persistence (PostgreSQL, required) ---------------------------------------
|
|
|
|
# Courses, chat history, learner progress and generated media are stored in
|
|
# PostgreSQL through the embedded /api/persistence endpoint. The server refuses
|
|
# to start without DATABASE_URL. Requests are attributed to the owner the owner
|
|
# identity seam resolves (by default the anonymous owner cookie); there is no
|
|
# separate persistence credential.
|
|
#
|
|
# Local development: `pnpm db:up` starts a separate development PostgreSQL
|
|
# (its own Compose project, container and volume, see docker-compose.db.yml)
|
|
# on 127.0.0.1 (port OPENMAIC_DB_PORT, default 5432); then uncomment this line.
|
|
# `pnpm db:down` stops it again (the data volume is kept).
|
|
# DATABASE_URL=postgres://openmaic:openmaic-dev@127.0.0.1:5432/openmaic
|
|
#
|
|
# Under docker-compose.yml, the default points at the bundled PostgreSQL (from
|
|
# PERSISTENCE_POSTGRES_PASSWORD, which must then be letters and digits); a value
|
|
# set here overrides it. Serverless and other hosted deployments point this at
|
|
# an external PostgreSQL database.
|
|
|
|
# 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; DATABASE_URL above answers WHERE one lives.
|
|
# Without this setting (or OWNER_SINGLE_USER), a second browser is a different
|
|
# anonymous owner and sees an empty list.
|
|
#
|
|
# 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=
|
|
|
|
# Personal installations: resolve every request to one fixed owner for a
|
|
# single person, who may publish. On by default under Docker Compose (see
|
|
# docker-compose.defaults.env); unset elsewhere. "true"/"1" or "false"/"0";
|
|
# anything else fails startup. Excludes PERSISTENCE_SHARED_OWNER_ID above:
|
|
# setting both fails startup.
|
|
# OWNER_SINGLE_USER=true
|
|
#
|
|
# The owner id (default "local"), same format as PERSISTENCE_SHARED_OWNER_ID.
|
|
# Setting it while OWNER_SINGLE_USER is off fails startup.
|
|
# OWNER_SINGLE_USER_ID=local
|
|
#
|
|
# Exposure: every request becomes the owner of the whole library. The mode runs
|
|
# with or without ACCESS_CODE; without one, anyone who can reach the server
|
|
# shares, edits and can delete the single library, so the server logs a
|
|
# prominent startup warning. Keep it on 127.0.0.1 or a private network
|
|
# (docker-compose.yml publishes on 127.0.0.1 by default; OPENMAIC_PUBLISH_ADDRESS
|
|
# changes that), or set ACCESS_CODE below.
|
|
#
|
|
# A browser that used the deployment anonymously before keeps its anonymous
|
|
# cookie; single-user mode can claim that earlier work into the single owner
|
|
# with POST /api/identity/claim. OWNER_CLAIM_TRIGGER=auto below would claim on
|
|
# every visiting browser's first request, irreversibly merging the libraries of
|
|
# everyone who used the deployment anonymously; set it only knowingly.
|
|
# The owner id is permanent: changing OWNER_SINGLE_USER_ID (or switching from
|
|
# PERSISTENCE_SHARED_OWNER_ID) strands the previous owner's library.
|
|
|
|
# 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
|
|
|
|
# The page response of a browser's first load mints the anonymous owner cookie
|
|
# (default on), so the page's first API requests do not each mint an owner.
|
|
# Set it to false only when registered owner auth methods set
|
|
# anonymousFallback: false (startup warns when they do and this is on); hosts
|
|
# that keep anonymous visitors need it, and skip it per request in
|
|
# middleware.ts for requests their methods authenticate.
|
|
# "true", "1", "false" or "0"; anything else fails startup.
|
|
# OWNER_ANONYMOUS_PREMINT=true
|
|
|
|
# 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.
|
|
# 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 (in
|
|
# single-user mode, the single-user warning, which names ACCESS_CODE, instead);
|
|
# 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.
|