跳到主要内容

Releases

Release history that reads like product progress.

This page combines committed changelog data with live release metadata when the GitHub API is available during the build. It gives evaluators one place to inspect shipping cadence and scope.

Latest published release

The hero release card is sourced from GitHub releases when available and falls back to the committed changelog if the API cannot be reached during the build.

Recent tagged releases

These cards summarize what each recent tagged release changed in product terms, not just commit volume.

v2.1.0

Sep 27, 2026

One isolation setting, more sandboxes, tighter tenancy

  • Workspace isolation is one workspace.isolation {level, backend} block; package and Docker installs convert old configs automatically, and the declared level is enforced as a floor.
  • New backends without Kubernetes: bubblewrap confinement for OS-account workspaces, and local gVisor or Kata Containers sandboxes with allowlisted egress.
  • Messages and analysis APIs are now strictly tenant-scoped, and the admin dashboard went from tens of seconds to milliseconds on large databases.
View release

v2.0.1

Sep 25, 2026

Published to PyPI

  • First PyPI release as open-ace-server, published with Trusted Publishing on every GitHub release.
View release

v2.0.0

Sep 25, 2026

A sandbox-isolated, multi-user platform

  • Interactive and autonomous agents can run in OpenSandbox gVisor/Kata pods behind a fail-closed SandboxProvider contract, with CLI state carried across ephemeral sandboxes.
  • Autonomous workflows gained an independent acceptance-verification phase with mechanical gates, human override, and resume-with-feedback.
  • SAML SSO, DingTalk and Feishu org sync, signed alert webhooks, and tenant-scoped OS isolation for shared projects. Upgrading needs Python 3.10+, a non-root image, and OPENACE_ENCRYPTION_KEY.
View release

What shipped in v2.1.0

Representative pull requests behind the latest tagged release, grouped by the product area they strengthened.

Workspace isolation

  • #3448 one workspace.isolation {level, backend} configuration
  • #3447 bilingual workspace isolation guide
  • #3435 bubblewrap confinement for os_user workspaces

Local sandboxes without Kubernetes

  • #3437 local gVisor container backend
  • #3445 Kata Containers runtime for the local container backend

Tenant boundaries

  • #3441 tenant-scope every messages endpoint
  • #3443 scope tenant_admin to its tenant on /api/analysis/*

Dashboard performance

  • #3439 stop full daily_messages scans on the admin dashboard
  • #3444 index-walk tenant-scoped tool and host lists

Changelog highlights

The content below is parsed from CHANGELOG.md. Each release section stays close to the repository source while becoming easier to scan on the public site.

v2.1.0

Sep 27, 2026

  • **Workspace isolation configuration changed (Issue #3446).** Package and Docker installs convert existing configs automatically; source installs and read-only mounted configs must run `scripts/convert_workspace_isolation.py` (or edit by hand) before starting 2.1.0. Isolation of interactive workspaces is configured in one block, `workspace.isolation {"level": ..., "backend": ...}`, with the backends `shared` (level `none`), `plain` / `bwrap` (`os_user`) and `local-gvisor` / `local-kata` / `opensandbox` (`sandboxed`), plus optional `tier`, `limits` (`memory` / `cpu_percent` / `tasks`), `egress_allow` and `container_webui`. The keys it replaces — `multi_user_mode`, `required_isolation_level`, `os_user_confinement`, `sandbox_tier`, `confinement_*` — are rejected (the server refuses to start and names the replacement), as are a level/backend pair that does not belong together and a backend the install method cannot provide (`OPENACE_INSTALL_METHOD`, set by the Docker image and the package installer's units; the Docker install cannot run `bwrap`/`local-*`). Existing configs are converted by `scripts/convert_workspace_isolation.py`, which the package installer runs on fresh install, local upgrade and remote upgrade (before it rewrites sudoers) and the Docker entrypoint runs on every start (a read-only mounted config is not converted: the server then names each key to replace); each old key is read the way the old server read it: a tier with a `webui_image` in `sandbox-backends.json` (which ran pods whatever `multi_user_mode` said) or `required_isolation_level: sandboxed` becomes `opensandbox` with its tier, a floor above the old mode is kept by raising the backend, and an `os_user_confinement` value the old server refused stops the install instead of being guessed. Semantics: the declared level is the floor (a degraded launch path refuses launches instead of serving on the shared account); the backend is explicit (pods only for `opensandbox`, never inferred from `webui_image`; a backend that is not ready is reported unsupported with its reason, never replaced by another; the single shared pod is gone). The capability snapshot reports the same backend names (`shared` / `plain` / `bwrap` / `local-gvisor` / `local-kata` / `opensandbox:<tier>`), `isolation_backend_shared` replaces `multi_user_mode_disabled`, and it adds `install_method` and `available_backends` (policy revision `2026-09-26.3`). The root policy `/etc/openace/webui-confine.json` keys its container sections by backend (`local-gvisor` / `local-kata` with `image` + `runtime`, top-level `docker`), and `openace-webui-confine --backend` takes the backend names. Docker: `WORKSPACE_ISOLATION_BACKEND=shared|plain|opensandbox` replaces `WORKSPACE_MULTI_USER_MODE` / `WORKSPACE_REQUIRED_ISOLATION_LEVEL`.
  • Workspace isolation admin guide in English and Chinese (`docs/en/WORKSPACE_ISOLATION.md`, `docs/cn/WORKSPACE_ISOLATION.md`, Issue #3446): the six isolation modes side by side, **which modes each install method allows** (the Docker install cannot run the confined or local-container sandboxes, and why), a minimal configuration for each mode, how to verify what is in force, and the common mistakes. The isolation capability reference and the sandbox-backends reference are now bilingual pairs under `docs/en/` and `docs/cn/` (previously `docs/WORKSPACE_ISOLATION_CAPABILITIES.md`, Chinese only, and `docs/SANDBOX_BACKENDS.md`, English only); the doc-executing tests run against both languages.
  • Local Kata Containers sandbox for interactive workspaces (Issue #3438): `workspace.isolation {"level": "sandboxed", "backend": "local-kata"}` is the local-container form of #3431 Option 2 on a Kata runtime (Kata >= 3.32.0, `/dev/kvm`), reported as the `sandboxed` level with backend `local-kata`. A Kata guest cannot reach host UNIX sockets over virtio-fs, so ingress and egress are multiplexed as framed streams with per-stream credit windows over the container's stdin/stdout: the container keeps `--network none`, with no bridge network and no host firewall rules, and any framing violation ends the launch. `launch --probe --backend local-kata` verifies Kata positively from the host (docker's `State.Pid` is a hypervisor whose parent is the Kata shim for that container id, and the guest kernel differs from the host's). The root policy file's `local-kata` section names the Docker runtime; `io.containerd.kata.v2` works without a `daemon.json` entry.
  • Local gVisor container sandbox for interactive workspaces without Kubernetes (Issue #3431, Option 2): `workspace.isolation {"level": "sandboxed", "backend": "local-gvisor"}` runs each user's WebUI in a Docker container on a registered gVisor runtime (`runsc` with `--host-uds=open`) from a digest- or ID-pinned image, as the user's OS account (`--user` + `--group-add`), with `--network none`, a read-only image root, dropped capabilities, `no-new-privileges`, memory/CPU limits, an RLIMIT_NPROC task bound enforced by the gVisor kernel, and only the user's home, `<base>/shared` and the log dir bind-mounted. It reuses the Option 1 root entry point, reverse-tunnel ingress, allowlist egress proxy and root-owned audit log; the WebUI environment reaches the container on stdin. `launch --probe` (root) verifies the docker CLI, the runtime, the local image, a gVisor guest kernel and host UNIX-socket access; the capability contract then reports the `sandboxed` level with backend `local-gvisor` and every dimension enforced, while keeping the os_user entry-point matrix, the OS-account identity gate and the local launch form (the OpenSandbox pod form is now selected by backend, not by level). A root parent process stays alive for the container's lifetime (signal forwarding, force-removal if sudo is killed, leftover-container and run-dir cleanup), requires `fs.protected_symlinks=1`, and an in-container watchdog stops the WebUI if the host-side supervisor disappears. Egress log: equivalent IP spellings canonicalized, and summaries past the byte ceiling are written at most once a minute.
  • Optional confinement for `os_user` interactive workspaces (Issue #3431, Option 1): with `workspace.isolation {"level": "os_user", "backend": "bwrap"}`, each per-user qwen-code-webui runs in a systemd scope (`MemoryMax`/`MemorySwapMax=0`/`CPUQuota`/`TasksMax`) entered through `setpriv --init-groups --no-new-privs` with all capabilities dropped, inside bubblewrap with a read-only host root, empty tmpfs over `/tmp`, `/var/tmp`, `/run` and the workspace base (only the user's home and `<base>/shared` are bound back — other homes are invisible), and a private network namespace whose only way out is a host-side proxy that allows allowlisted `host:port` pairs (the Open ACE API / LLM proxy endpoints plus `workspace.isolation.egress_allow`) and logs every decision. The root entry point `/usr/local/bin/openace-webui-confine launch` validates typed arguments, builds every systemd property itself, refuses executables not listed in the root-owned `/etc/openace/webui-confine.json`, and takes the WebUI environment on stdin so the proxy token never appears on a command line. The capability contract reports backend `bwrap` with `resources` and `network_egress` enforced; a host that cannot confine refuses the launch (the declared level is the floor) instead of serving unconfined. The package installer installs the wrapper, the policy file and a `launch`-only sudoers rule. Real-Linux acceptance: `scripts/webui_confine_acceptance.py`.
  • Messages API is tenant-scoped (Issue #3440): all six endpoints in `app/routes/messages.py` (`/api/senders`, `/api/messages`, `/api/messages/count`, `/api/conversation-history`, `/api/conversation-timeline/<id>`, `/api/conversation-details/<id>`) were guarded by authentication alone, so any user of any tenant could read every tenant's senders, message content and conversations. A blueprint gate now gives global scope only to platform admins, pins everyone else (including `tenant_admin`) to their own tenant, and returns 403 for a non-platform-admin without a tenant; another tenant's conversation looks like a nonexistent one. The module-global senders cache, shared by every caller, is gone and the service cache key includes the tenant.
  • `tenant_admin` is scoped to its own tenant on all 14 `/api/analysis/*` endpoints (Issue #3442): global scope was decided with `is_admin_role()`, which includes `tenant_admin`, so tenant admins saw usage, rankings, conversation stats and anomalies across all tenants. Global scope now requires `is_platform_admin_role()`. Note: tenant admins' analysis pages now show only their tenant, and the legacy `admin` role is no longer global under strict mode.
  • Admin dashboard no longer rescans `daily_messages` on every request (Issue #3424, measured on PostgreSQL 14 with 484k messages / 604k stats rows): `GET /api/trend` 91 s → 23 ms, `/api/tools` 10.8 s → 2 ms, `/api/senders` 19 s → 7 ms, `/api/hosts` 3–22 ms; the slowest parameter-less admin GET is now 25 ms (was 19 s). Fixes found on the way: `daily_stats` accumulated NULL-sender duplicate rows on every refresh (603k copies of 67 groups), which **doubled trend totals**; `needs_refresh` was permanently true; `/api/summary` returned 500 and the usage summary stopped refreshing since 2026-08-21 when a tool had rows in both sources (mixed `date`/string types on PostgreSQL); unfiltered `SELECT DISTINCT` is replaced by a loose index scan.
  • Tenant-scoped `/api/tools` and `/api/hosts` walk two new partial indexes instead of scanning `daily_messages` (Issue #3424): tenant admins' `/api/hosts` 2.9 s → 12 ms, `/api/tools` up to 3.1 s → 28 ms. Migration `20260926_002` builds `idx_messages_user_tool` and `idx_messages_user_host` `CONCURRENTLY` on PostgreSQL (writes are not blocked; an INVALID leftover is rebuilt). Code deployed ahead of the migration falls back to the old query.

v2.0.1

Sep 25, 2026

  • Python packages are published to PyPI as `open-ace-server` (`pip install open-ace-server`) via Trusted Publishing; PyPI rejects `open-ace` as too similar to the unrelated `openace` project. Import names and the `openace` CLI are unchanged. Release wheels/sdists are now named `open_ace_server-X.Y.Z*`.
  • Internal: `webui_sandbox.py` split into a shared module and `webui_sandbox_opensandbox.py` (Issue #3431); no behavior change.

v2.0.0

Sep 25, 2026

  • **Python 3.10+ is required** (#1935, #1936); Python 3.9 is no longer supported. Python 3.13 is supported for source and offline installs (#2219, #2240).
  • **The Docker image runs as the non-root `open-ace` user (uid 1000)** by default (#1797). Mounted `./data`, `./config` and log volumes must be writable by uid 1000; multi-user workspace mode, which needs root, must use `docker-compose.multi-user.yml` (or explicitly opt in with `user: "0"` + `OPENACE_ALLOW_ROOT_MULTI_USER=1`).
  • **Set `OPENACE_ENCRYPTION_KEY` before the first restart after upgrading.** Stored secrets are now encrypted with a dedicated key; existing deployments must set it to their previous `SECRET_KEY` value, or previously encrypted secrets cannot be decrypted (#2275, #2362).
  • **Kubernetes reference manifests run the web pod non-root with 3 replicas + HPA**; deployments relying on root must opt in explicitly.
  • **Autonomous development is enabled by default** in Docker-generated config, and the scheduler's global concurrency default rose from 3 to 10 with a new per-user cap (#2295, #2297).
  • **Sandboxed agent execution**: a `SandboxProvider` contract (#2022) with Legacy POSIX, Remote Machine and OpenSandbox providers; the OpenSandbox backend runs autonomous agents in gVisor or Kata pods with attestation-derived capabilities, fail-closed policy translation, CNI-enforced egress, orphan reconciliation and a TTL reaper (#2023, #3205). Per-task HOME/TMP/XDG isolation and cgroup-v2 resource policy (`scripts/setup-cgroup-v2.sh`) harden the local provider (#2020).
  • **Agent state carry across ephemeral sandboxes**: the CLI session transcript is exported/imported per session line so `--resume` works for Claude Code, Codex, Qwen Code and ZCode across milestones (#3237, #3263, #3325, #3329, #3330).
  • **Independent acceptance verification** for autonomous workflows: a verifier on a dedicated merged-main checkout, five mechanical gates (negative-test, legacy, call-chain, deployment, regression), scope gate, verdict aggregation, owner/admin override, in-page acceptance report viewer and resume-with-feedback (#2335, #2344, #2346, #2658, #2701, #3005).
  • Evidence-based autonomous pipeline: structured command/test evidence replaces agent-prose heuristics, verify-before-act merge readiness, per-domain test-evidence requirement (shadow-first), and CI auto-repair raised to 5 attempts (#2045, #2046, #2194, #2391, #2460).
  • Autonomous operations: GLM usage-window 429s pause the workflow and auto-resume at the window reset (#2709, #2714), loop-detection aborts auto-resume (#3149), lazy dependency preparation for agent worktrees (#2700), per-workflow `max_changed_files_override` with a scope-exceeded retry banner (#2309), real-time activity summary + heartbeat (#1734), final diff stats (#1717) and dedicated timeline icons for all milestone types (#3044).
  • Cross-user symlink escape in the file API (Issue #3410): `POST /api/fs/upload` derived its final target with `realpath(join(validated_dir, filename))`. realpath follows a symlink in the last component, and the follow-up check only compared the workspace-base prefix and dropped the home-subtree lock, so a user could place a same-named symlink in their own home pointing at another user's file and the root branch (Docker multi-user) would overwrite the victim's file (reproduced: 200 response that also echoed the victim's absolute path). Upload now descends from the home root one component at a time with `O_NOFOLLOW|O_DIRECTORY` directory fds, creates the temp file inside that fd with `O_EXCL|O_NOFOLLOW`, `fchown`s it and replaces the entry with `renameat`, which replaces the directory entry rather than a symlink target and removes the check-to-write TOCTOU window; the root branch also asserts the trust anchor with `fstat` (the home root belongs to the target account). A target name that is itself a symlink or an existing directory is an explicit 400 on both branches. **`download`, `delete-file` and `search` no longer operate by path either:** they validated the realpath and then opened, deleted or walked by path (download even opened the file in the response generator after the view returned), so under a root process swapping a component for a symlink between the check and the operation could read or delete any file on the host. They now work relative to the same component-by-component directory fd: `open(O_NOFOLLOW|O_NONBLOCK)` plus an `fstat` regular-file check (a FIFO cannot hang the request), `unlinkat`, and `os.fwalk` (symlinks themselves are skipped under root). In the non-root package install, `openace-write-as` and `openace-rm` perform every filesystem probe, write and delete after the user/prefix check through `runuser` as the target account (`openace-rm` used to run the final `rm` as root; `openace-write-as` ran its `-L`/`-d` probes as root and before the prefix check, leaking the type of arbitrary paths). `openace-write-as` also rejects symlinks (exit 5) and directories (exit 6), writes atomically with `mktemp` + `mv -fT`, and exits right after its INT/TERM trap cleans up. Both wrappers carry a capability marker, and the routes **fail closed on upload/delete** when the installed version lacks it. When a wrapper rejects early, an upload no longer turns into a 500 from manually closing an already-broken stdin. Under a root process, a user with no mapped `system_account` whose username equals another user's `system_account` no longer gets any home root, and an account named `shared` (including same-named users from SSO or org sync) never gets the shared-project namespace root `<base>/shared` as its home root in any deployment. uid resolution is a single `getpwnam` (account names starting with `-` are no longer parsed by `id` as options). Fixed in the same batch: `POST /api/fs/create-directory` gets the home-subtree lock it always lacked (same creatable set as check-path); single-file `/api/fs` paths, `/api/fs/home` and the default `search` root use per-base home roots, since on multi-base deployments (`WORKSPACE_BASE_DIR=/a,/b`) the old single-base helper produced the literal `"/a,/b/<account>"` and these endpoints all returned 400; `<base>/<account>` is tightened from 0755 to 0700 (matching `/home/<account>`; old volumes converge at startup and login provisioning, only for directories owned by that account, with the shared namespace root explicitly skipped); login provisioning under root sets owner and mode of `<base>/<account>` and its `.qwen` through directory descriptors.
  • Dropped the "others" permission bits from the multi-user shared-namespace root `<base>/shared` (3775 → 3770; sticky and setgid kept): the root is group openace-shared, which every active account joins for namespace creation, but the old others r-x additionally let ANY non-member process on the host (other containers' agents, host services) enumerate the root and read shared project NAMES — metadata only (content access always required the per-tenant group), yet project names can be sensitive. Existing deployments are re-normalized on the next container boot (the provisioning is idempotent, and the unconditional chgrp+chmod also converges mode drift). Declared residual, documented in the acceptance handbooks: because namespace creation is a global right, cross-tenant members can still list the root via the group r-x — per-tenant name secrecy is not achievable in this design, while content access stays fenced by the per-tenant `openace-shared-<tenant_id>` groups (2770/660).
  • Hardened the boot-time tenant shared-group convergence against two edge shapes in its `getent`-driven member filter: (1) the numeric-suffix guard for `openace-shared-<t>` groups used bare `str.isdigit()`, which ACCEPTS non-ASCII digits — a root-created `openace-shared-<unicode-digits>` lookalike passed the guard and the convergence emptied it member by member (no such tenant exists in the DB); the suffix must now be ASCII digits. (2) A transient NSS failure (`getent passwd` exit code neither 0 nor 2 — socket timeout, sssd restart, nscd hiccup) used to drop a PRESENT member from this boot's `gpasswd -M` list, silently removing their tenant content access until the next boot; the lookup is now retried once, and a member whose presence stays ambiguous after the retry skips that group's convergence entirely for this boot with a loud warning — never a list built on an unreliable answer (that either poisons `gpasswd -M` with a missing account, the shadow-utils rejection, or strips a member who is actually present). Both shapes self-heal on the next boot.
  • Fresh multi-user production installs can now boot without manual migration steps (Issue #3397 — the multi-user acceptance run's declared deviation): a genuinely fresh database (no application schema AND no `alembic_version` table) used to be refused by the entrypoint, because `scripts/check_min_revision.py` exits 1 in production mode on exactly that state ("Fresh database detected in production"), so the documented DEPLOYMENT.md compose path could not start an empty database at all — the acceptance had to pre-initialize with a one-shot `alembic upgrade head && python3 scripts/init_db.py` container. The entrypoint now probes for the `alembic_version` table as well (quoted-heredoc python, so the #3399 raw-quote class cannot recur) and, when the database is provably fresh, self-initializes with the same two commands the deviation used before continuing the normal boot flow ("Fresh database detected — self-initializing schema and seed"). The branch is deliberately narrow: a database with existing schema or a recorded revision is never touched by it (the minimum-revision refusal and the regular upgrade path keep applying, and `init_db.py` still seeds only when no application schema existed at boot), and a failed probe reads as "unknown" — NOT fresh — so detection fails strict instead of bootstrapping blind over a database that might have data. **User-visible change**: a first `docker compose up` against an empty volume now starts instead of exiting; existing deployments are unaffected.- On-demand provisioning of the multi-user shared-namespace root from the API (Issue #3393, follow-up to #3379's entrypoint-only provisioning): the `openace-shared`-group sticky root `<base>/shared` was created only by the Docker entrypoint at boot, so on a deployment where the entrypoint did not run — package-method multi-user installs, or any shape where the root went missing — the FIRST shared-project creation (`POST /api/projects` with `create_dir: true` at `<base>/shared/<name>`) ran the user-side `mkdir -p` against a root-owned 0755 parent and failed 403, with no way to bootstrap the #3376 namespace from the API. `POST /api/projects` now provisions the one namespace root the project path lives under BEFORE the user-side mkdir (`app.utils.workspace.ensure_shared_namespace_root`): root-owned, `openace-shared` group (`groupadd -f` if missing, mirroring the entrypoint), mode 3770 (sticky + setgid, no others bits — the entrypoint's cross-tenant rename/inheritance semantics). The provisioning is idempotent and non-intrusive (an EXISTING root is never re-chgrp/chmod'd), a no-op outside Docker multi-user mode, fail-closed on the account-named-`shared` home collision (defense in depth under the registration-time path_guard), and any failing step degrades to a clean 5xx JSON error instead of a crash. **User-visible change**: shared-project creation bootstraps its namespace root on any deployment shape, not only Docker boots (the package installer's install-time equivalent lands separately).- Fixed (P0) the multi-user boot user-sync silently dying on every container start since 2026-09-08 (`5a20fd7bc`, auto-dev round 1): the SSH-sync hardening embedded python strings with RAW double quotes (e.g. `_log_sync_failure(username, "script_missing", ...)`) inside the bash DOUBLE-QUOTED `python3 -u -c` payload — bash stripped the quotes, split the argument at the first unquoted whitespace (~7.5KB in), and handed python a truncated program (SyntaxError, exit 1), while the mis-parsed pipeline tail kept the traceback out of `/app/logs/open-ace-user-sync.log` — zero-output silent death, the exact #3399 signature discovered by the #3385 acceptance runs (Issue #3399). Every boot's sync since that commit created NO OS accounts; deployments only appeared healthy because logins/WebUI launches provision accounts via the app-side `ensure_system_user` — the gap surfaced whenever a container was recreated (`--force-recreate`, image upgrade) and only the boot sync could restore `/etc/passwd`. The 49 payload lines are now properly `\"`-escaped, and a permanent guard test runs the invocation through REAL bash with a PATH-shimmed python3, asserting bash's expansion is byte-identical to the source extraction and parses — `bash -n` cannot catch this class (quotes stay balanced), and the functional harnesses' own unescape extraction masked it.- Install-time provisioning of the shared-namespace root in the package-method installer (Issue #3393, the installer half of the app-side on-demand fix): the package multi-user setup path (sudoers/wrappers/systemd configuration in `scripts/install-central/package-method/install.sh`) never created `<base>/shared` — the #3379 entrypoint provisioning is Docker-only, leaving package deployments dependent on the newly-added API-side fallback. The installer's multi-user section now runs `provision_shared_namespace` on fresh installs AND upgrades: per comma-separated `WORKSPACE_BASE_DIR` entry (Package default `/home`, whitespace- and trailing-slash-trimmed — no literal `a,b` directory), `as_root groupadd -f openace-shared` + `mkdir -p` + `chgrp` + `chmod 3770`, mirroring the entrypoint's semantics (sticky+setgid, no others bits). It skips loudly when the path collides with a real account named `shared` or an existing non-root-owned root, idempotently re-converges an existing root-owned root (mkdir -p no-ops, chgrp/chmod fix drift), and degrades to a WARNING on failure — the app's request-time provisioning retries. The remote deploy path (`install_deploy`) has no multi-user setup section (remote sudoers remain manual) and is unchanged; `scripts/start-multi-user.sh` only drives Docker Compose, where the entrypoint provisions inside the container — the wiring belongs in install.sh alone. **User-visible change**: a package-method multi-user install can create shared projects immediately, without waiting for the API-side on-demand fallback.- The single-user WebUI instance now honors the configured port range instead of the hardcoded 3100 (registered leftover "single-user 3100 launch limit", #3379 acceptance tail / handbook §5.8): `_start_single_user_instance` pinned `port = 3100` and `get_user_webui_url` advertised `:3100` regardless of `workspace.port_range_start`, so (1) a deployment that offsets the published range — the #3379 acceptance runs the single-user stack beside the multi-user stack with `WORKSPACE_PORT_RANGE` 13100-13200 because host 3100-3200 is held — got an instance bound on an UNPUBLISHED container port advertising an unreachable `:3100` URL (handbook §5.8's declared "not reachable at its advertised host URL"), and (2) a busy 3100 was never detected: no availability pre-check ran and the connect-based readiness probe succeeds against ANY listener, so a foreign service on 3100 would be wired in as "our webui" with a false-positive ready. The port is now the FIRST FREE port of the configured range (the same `_is_port_available` probe the multi-user `allocate_port` uses), the URL and the embedded token port follow the instance's actual port, and an exhausted range raises the same loud `ValueError` shape as the multi-user allocation. Default deployments are unchanged (the default range starts at 3100). The "many launches" reading was investigated and is not a defect: single-user mode has exactly ONE shared instance, reused idempotently by every user and never reaped — there is no per-launch allocation to exhaust. The OTHER §5.8(8) sub-item (the default admin has no OS account in the single-user container, so the sudo launch path cannot start the instance for it) remains the declared capability-separation exemption — the acceptance script's real-capability assertion (a user with `system_account=open-ace` takes the sudo-free direct branch) still guards the launch path, and the exemption is unchanged. Existing single-user tests were made port-deterministic (the availability probe is stubbed) so they no longer depend on the runner's port table. **User-visible change**: co-located stacks and offset-range single-user deployments get a reachable webui URL; a busy 3100 falls through to the next free port instead of silently wiring a foreign listener. the package multi-user setup path (sudoers/wrappers/systemd configuration in `scripts/install-central/package-method/install.sh`) never created `<base>/shared` — the #3379 entrypoint provisioning is Docker-only, leaving package deployments dependent on the newly-added API-side fallback. The installer's multi-user section now runs `provision_shared_namespace` on fresh installs AND upgrades: per comma-separated `WORKSPACE_BASE_DIR` entry (Package default `/home`, whitespace- and trailing-slash-trimmed — no literal `a,b` directory), `as_root groupadd -f openace-shared` + `mkdir -p` + `chgrp` + `chmod 3770`, mirroring the entrypoint's semantics (sticky+setgid, no others bits). It skips loudly when the path collides with a real account named `shared` or an existing non-root-owned root, idempotently re-converges an existing root-owned root (mkdir -p no-ops, chgrp/chmod fix drift), and degrades to a WARNING on failure — the app's request-time provisioning retries. The remote deploy path (`install_deploy`) has no multi-user setup section (remote sudoers remain manual) and is unchanged; `scripts/start-multi-user.sh` only drives Docker Compose, where the entrypoint provisions inside the container — the wiring belongs in install.sh alone. **User-visible change**: a package-method multi-user install can create shared projects immediately, without waiting for the API-side on-demand fallback.- Session tokens now honor `users.tokens_valid_after` — the same floor URL tokens have had since R-5 (registered leftover "session-side tokens_valid_after reuse", #3379 review follow-up): deactivation stamps the column so pre-stamp WebUI URL tokens stay dead across a later reactivation (the stamp is never cleared), but the SESSION side only had the `is_active` JOIN gate in `get_session_by_token` — which the same reactivation flips back to passing. A pre-deactivation session row therefore VALIDATED AGAIN after reactivation in two real shapes: the admin deactivation route's session revocation is fail-soft (a failed `delete_all_sessions_for_user` is logged, not fatal), and org-sync deactivations (feishu/dingtalk) write `is_active` directly and delete no session rows at all — in both, the old cookie resurrected on an account the operator had disabled, while the equivalent URL token stayed dead. `get_session_by_token` (the single funnel every session-token path goes through — before_request hooks, `/api/auth/me`, `validate_session`) now also refuses sessions whose `created_at` predates the stamp: reactivated users simply log in again and mint a fresh post-stamp session (the declared R-5 residual, aligned across both token families), and never-deactivated users are unaffected (the floor only bites when a stamp exists). Timestamp note: the comparison is a plain timestamp comparison on both dialects; SQLite stores `created_at` at second resolution while the stamp carries microseconds, so a session minted in the same second as the stamp can be refused — bounded by one second, the mirror of the URL-token side's declared same-second tolerance. **User-visible change**: deactivating an account kills its cookies for good — reactivation requires a fresh login instead of resurrecting the pre-deactivation browser session.
  • The single-user WebUI instance now honors the configured port range instead of the hardcoded 3100 (registered leftover "single-user 3100 launch limit", #3379 acceptance tail / handbook §5.8): `_start_single_user_instance` pinned `port = 3100` and `get_user_webui_url` advertised `:3100` regardless of `workspace.port_range_start`, so (1) a deployment that offsets the published range — the #3379 acceptance runs the single-user stack beside the multi-user stack with `WORKSPACE_PORT_RANGE` 13100-13200 because host 3100-3200 is held — got an instance bound on an UNPUBLISHED container port advertising an unreachable `:3100` URL (handbook §5.8's declared "not reachable at its advertised host URL"), and (2) a busy 3100 was never detected: no availability pre-check ran and the connect-based readiness probe succeeds against ANY listener, so a foreign service on 3100 would be wired in as "our webui" with a false-positive ready. The port is now the FIRST FREE port of the configured range (the same `_is_port_available` probe the multi-user `allocate_port` uses), the URL and the embedded token port follow the instance's actual port, and an exhausted range raises the same loud `ValueError` shape as the multi-user allocation. Default deployments are unchanged (the default range starts at 3100). The "many launches" reading was investigated and is not a defect: single-user mode has exactly ONE shared instance, reused idempotently by every user and never reaped — there is no per-launch allocation to exhaust. The OTHER §5.8(8) sub-item (the default admin has no OS account in the single-user container, so the sudo launch path cannot start the instance for it) remains the declared capability-separation exemption — the acceptance script's real-capability assertion (a user with `system_account=open-ace` takes the sudo-free direct branch) still guards the launch path, and the exemption is unchanged. Existing single-user tests were made port-deterministic (the availability probe is stubbed) so they no longer depend on the runner's port table. **User-visible change**: co-located stacks and offset-range single-user deployments get a reachable webui URL; a busy 3100 falls through to the next free port instead of silently wiring a foreign listener. the package multi-user setup path (sudoers/wrappers/systemd configuration in `scripts/install-central/package-method/install.sh`) never created `<base>/shared` — the #3379 entrypoint provisioning is Docker-only, leaving package deployments dependent on the newly-added API-side fallback. The installer's multi-user section now runs `provision_shared_namespace` on fresh installs AND upgrades: per comma-separated `WORKSPACE_BASE_DIR` entry (Package default `/home`, whitespace- and trailing-slash-trimmed — no literal `a,b` directory), `as_root groupadd -f openace-shared` + `mkdir -p` + `chgrp` + `chmod 3770`, mirroring the entrypoint's semantics (sticky+setgid, no others bits). It skips loudly when the path collides with a real account named `shared` or an existing non-root-owned root, idempotently re-converges an existing root-owned root (mkdir -p no-ops, chgrp/chmod fix drift), and degrades to a WARNING on failure — the app's request-time provisioning retries. The remote deploy path (`install_deploy`) has no multi-user setup section (remote sudoers remain manual) and is unchanged; `scripts/start-multi-user.sh` only drives Docker Compose, where the entrypoint provisions inside the container — the wiring belongs in install.sh alone. **User-visible change**: a package-method multi-user install can create shared projects immediately, without waiting for the API-side on-demand fallback.
  • Workspace isolation capability contract `policy_revision` bumped to `2026-09-16.1` (Issue #3410): in `entry_points`, `filesystem_api` goes from `partial` to `enforced` (the fix above plus a per-operation review), and `terminal`/`vscode` move from `partial` to the new status `remote_machine_scope`. Their old partial reasons (terminal tokens not bound to the session owner, VSCode owner taken from the machine creator) were already fixed by #3376 / PR #3380 without updating the matrix, but `enforced` would be wrong too: both run on the remote machine as that machine's agent account, outside the local `os_user` level, governed by machine ACLs, session ownership and tenancy. New `entry_point_details` give each entry point its `scope`, `covered_by_isolation_level`, per-operation `roots` and `symlink_policy`, `access_control`, plus `limitations` (real gaps that count toward admission) and `residuals` (known, accepted properties that do not), so integrators can tell "meets the chosen level", "remote scope" and "real gap" apart; an unknown `status` or an unreviewed `policy_revision` must fail closed. `filesystem_api` upload/download/delete-file/search declare `symlink_policy: never_followed_with_elevated_privilege` (below the home root no symlink is followed with more privilege than the target account); other operations are `resolved_then_rejected_if_outside`; `residuals` lists the shared project root, the package-install wrapper prerequisite and unmapped users. Sandboxed snapshots keep each entry point's `limitations` and append `sandboxed_entry_not_wired`, and `filesystem_api` no longer inherits the host-tree boundary/residuals. Docs §4 (entry-point matrix) is rewritten against the current implementation with the gaps closed by #3376/#3380 removed, and §7 adds an executable admission example that a unit test runs verbatim.
  • Multi-user deployment documentation now recommends using docker-compose.multi-user.yml as the primary method
  • The Docker image now defaults to the non-root `open-ace` user (uid 1000) via a `USER 1000` directive in the production stage, so `docker run`, docker-compose, and Kubernetes all run non-root without relying solely on the manifest `securityContext`. Multi-user workspace mode (which needs root for `useradd`/`chown`/`sudo -u`) is now an explicit, fail-fast opt-in: run as uid 0 (`docker run --user 0` / manifest `runAsUser: 0`) and set `OPENACE_ALLOW_ROOT_MULTI_USER=1`, otherwise the entrypoint exits with a clear error instead of silently swallowing permission failures.
  • Kubernetes reference manifests now run the web pod as non-root with 3 replicas, HPA, and Prometheus scrape annotations restored; multi-user workspace deployments that require root must opt in via an explicit overlay.
  • Docker-generated default config now enables AI autonomous development by default and only enables multi-user workspace mode when `WORKSPACE_MULTI_USER_MODE=true`.

v1.2.0

Jul 14, 2026

  • Dark-mode contrast on Manage → Settings → Model Gateway: the status banners (the "gateway routing not enabled" warning and the "enabled" success state) had too little text/background contrast. New `--color-{success,warning,danger,info}-{bg,text}` CSS variables use low-opacity semantic backgrounds with bright text in dark mode, meeting WCAG AA (≥ 4.5:1).
  • The code-review reminder issue (auto-created every 3 weeks) now includes a health report: the workflow runs a Bandit security scan, pip-audit dependency check, single-migration-head check, frontend bundle size, and SQL lists of dead DB indexes, large tables and slow queries, and the template gains a "🛡️ Security" dimension. Every check step is `continue-on-error`, so one broken tool never blocks the issue.
  • `MessageRepository.get_user_conversation_samples` replaces its per-session N+1 queries with one batched query (`IN` list + grouping by session in the application), keeping the per-session `(agent_session_id = S OR conversation_id = S)` semantics and equivalence for value collisions and rows matching both fields; if the batch fails it falls back to the old per-session loop.
  • Removed the one-off `baseline_2026_06_23` cutover call from the install/upgrade paths and added `scripts/check_min_revision.py` as a minimum-revision guard; the minimum supported upgrade starting point is `baseline_2026_06_23`, and a database below that revision must be restored from a known-good backup before upgrading (Issue #1215).

v1.1.0

Jul 2, 2026

  • AI autonomous development workflow with retry, fork/cancel milestones, and PR-issue linkage (#716, #740, #886)
  • Real-time agent activity streaming for autonomous workflows (Issue #771)
  • Persisted remote-agent run provenance timeline with milestone TL;DR and code-change views (#988, #993)
  • Batched GitHub issues and `auto_merge` for autonomous workflows
  • AI GitHub account configuration for the autonomous agent (Issue #786)
  • Schema single source of truth — authoritative `schema.sql` loaded directly (#1273, #1276)
  • Baseline migration `baseline_2026_06_23` absorbs #1125/#1128 transcript columns
  • Dashboard UI layout optimization and Card actions (#1085)
  • Autonomous workflow timeline UI redesign and milestone compact cards (#1025, #1037)
  • Shared workspace base-dir helper + macOS symlink blacklist fix (#1138)
  • Project management total count now includes smart-categorized projects (Issue #1384)
  • Removed default project-category initialization in favor of smart categorization (Issue #1382)
  • "View latest milestone" button in the fork view did nothing when clicked (Issue #1375) (#1380)
  • Added the `autonomous_workflows.retry_count` column (Issue #1376)
  • Added retries to the Sync to Gitee workflow (Issue #1378) (#1379)

v1.0.0

Jun 2, 2026

  • **Self-hosted AI workspace**: Work Mode for AI sessions, prompt reuse, conversation history, and project context.
  • **Remote Workspace and Remote Agent**: Run AI CLIs on development, staging, or GPU machines through a browser-managed remote agent.
  • **Multi-CLI support**: Claude Code, Qwen Code, Codex, and OpenClaw adapters with session recovery, permission modes, and history sync.
  • **API Key proxy**: Encrypt LLM API keys on the server and issue short-lived proxy tokens to local and remote sessions.
  • **Browser development loop**: Remote terminal, directory browsing, and VSCode/code-server proxy support.
  • Flask backend with SQLAlchemy repositories and Alembic migrations.
  • React 18, TypeScript, Vite, Bootstrap 5, and Chart.js frontend.
  • PostgreSQL and SQLite support for development and deployment workflows.
  • Remote agent package with CLI adapters, WebSocket terminal support, and session synchronization.
  • Docker Compose quick start with PostgreSQL.
  • Source installation path for local development.
  • Kubernetes deployment reference.
  • Linux systemd and macOS launchd support for remote agents.