`octop models active` prints " / " when no global default model is set:
the provider/model separator with nothing on either side and no hint.
`octop agent list` leaves the model cell empty for every agent without
its own model, which reads as "this agent has no model" and hides that
an agent with its own model overrides the global default.
When no default is set, `models active` now says so and shows the
command that sets one. `agent list` fills empty model cells with
"(global: provider/model)", or "(not set)" when there is no global
default either; agents with their own model are unchanged. Both strings
come from the zh/en bundles. JSON output (`--json`) is unchanged.
Refs #1930, #1931
The entrypoint guard only looked for ~/.octop/octop.db, which PostgreSQL
(and other external) backends never create in the data volume, so every
container restart re-ran `octop init`. The unconditional admin INSERT
then aborted the container with UniqueViolation on users_username_key.
- docker/docker-entrypoint.sh: write a backend-agnostic marker
(~/.octop/.initialized) after a successful `octop init`; skip the
bootstrap block on restarts when the marker or octop.db exists. When
init reports the admin already exists (upgrade of a pre-marker
deployment), keep the existing credential card instead of writing a
password that was never applied; also mention external DBs in the
generic init-failure hint.
- src/octop/cli/commands/init.py: make the admin bootstrap idempotent —
when the username already exists, skip creation with an explicit
message (the entrypoint greps for it) instead of hitting
UNIQUE(username).
Tests: entrypoint tests run the real script with a stub `octop` on PATH
(first boot + restart for an external backend, sqlite regression guard,
credential preservation, failed-init retry); a CLI test covers rerunning
`octop init` when the DB lives outside ~/.octop.
Fixes#1866
Signed-off-by: fengyue-xve <1759519137@qq.com>
Composer, IM/CLI /help, and slash catalogs showed /approve even when neither HITL nor command-guard require_approval was on. CLI also went silent on a pause, and typed /approve hit a stub instead of resuming the turn.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Move JWT helpers and peer-turn runners out of infra→api imports, align
dashboard en/zh leaf keys, and let octop init proceed when only sidecar
files exist. Changelog records LDAP group mapping and skill lock scope.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(chat): add Ask / Plan / Craft conversation modes
Keep mode sticky per thread, enforce Ask/Plan in harness-agent, and let Plan write a workspace plan before Craft executes it.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Update pyproject.toml
* fix(chat): clean up sessions bound to deleted threads (#833)
Deleting a thread left its sessions row pointing at a dead thread_id, so
later turns silently failed to persist history (and `octop chats create`
crashed on the dangling binding). ThreadRegistry.delete_thread now drops
the sessions bound to the thread, and the read paths treat a session
whose thread no longer exists as unbound so the next turn rebinds to a
fresh thread.
* docs(changelog): note dangling-session fix (#833)
* feat: add in-tree plugin marketplace and related dashboard polish
Ship a catalog install flow (market API + Admin UI), stop auto-seeding new
bundled plugins, and include storage rename, plugin tools listing, agent
lifecycle locking, and history/trajectory path cleanup in the same change.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(dashboard): use ToolsTabs in Experts tools drawer
Align the Experts tools catalog with Personalization so builtin, plugin,
and ACP tabs are available in the same drawer.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: per-tool chat icons and init seed expectation
Show distinct Lucide icons for common built-in tools in chat tool rows,
and stop expecting octop init to auto-copy catalog plugins after the
marketplace seed change.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(test): drain captcha siteverify POST bodies on Windows
Unread form bodies from the mock HTTP server could leave the connection
half-closed and flake reCAPTCHA v3 multi-call assertions under Windows CI.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(config): never rewrite an unparseable config.json as empty (#730)
Four call sites read config.json, treated a parse failure as an empty dict and
wrote that back, destroying every other key. One trailing comma plus one
`octop run --port 8088` wiped 17 of 18 keys including the whole `database`
section; the next boot silently came up as a greenfield SQLite instance (fresh
setup-wizard password, zero warnings) while the PostgreSQL data sat unreachable.
Losing `bind_host` the same way is what makes #730's reporter unable to reach
the server from the LAN.
A torn write creates the same precondition with no user error: three of the four
sites wrote config.json with a bare write_text, so a crash / OOM / full disk
mid-write left invalid JSON that the next plugin toggle then wiped.
One shared policy in a new leaf helper, infra/utils/json_file.py:
absent file -> {} and write (first run, unchanged)
valid JSON object -> merge and write (unchanged)
unparseable/non-object -> raise CONFIG_FILE_CORRUPT, write nothing
The error names the file and the parser line/column but never the contents,
which hold database credentials. Writes now go through write_json_atomic (temp +
os.replace, fd closed on every path, temp removed on failure) so no reader can
observe a torn file.
Sites: cli/commands/run.py (_save_configfile_overrides, plus
_load_configfile_overrides which used to fall back to launch defaults),
infra/agents/plugins/manager.py (_write_global_plugin_enabled = the dashboard
toggle), infra/setup/service.py (persist_bind_options),
infra/agents/plugins/seed.py. `octop run` / `octop service start` report the
error and exit 1 via fail_octop (now NoReturn) instead of a traceback.
Supersedes the pinned "treat a malformed file as empty so the next run rewrites
it cleanly" contract: its fixture (`{not valid json`) had nothing to lose, so
the test could not observe the destruction. Rewritten as
test_run_reports_corrupt_config_cleanly and
test_run_does_not_overwrite_corrupt_config.
Verified end-to-end against a real OctopServer + real PostgreSQL. Before: one
`octop run --port` flipped driver postgresql -> sqlite, the instance reverted to
the first-run wizard, and users=1/agents=1 were orphaned in PG with no warning.
After: exit 1, config.json byte-identical, boot fails loudly instead of
switching backends. Mutation check: restoring the lenient policy in the helper
reddens 9 tests spanning all four sites.
make all green (3299 passed, 16 skipped); mypy --strict clean; dashboard
`npx tsc --noEmit` clean.
* fix(config): name the file when config.json is corrupt (#730)
`load_config` let a bare `json.JSONDecodeError` escape: it reports a line and
column but never says *which* file failed, and this entry point is reached from
boot, backup and several CLI commands — not only `octop run`. Raise a ValueError
naming the path and the parser position, refuse a non-object document, and never
echo file contents (config.json holds database credentials).
Same policy as infra/utils/json_file.py, duplicated here because octop.config
must stay free of infra imports (AGENTS.md §5).
`octop run` already exits cleanly on a corrupt config through resolve_bind; this
covers the remaining entry points.
make all green (3301 passed, 16 skipped); mypy --strict clean.
* fix(config): preserve config.json permissions in atomic writes
Self-review against CONTRIBUTING.md / AGENTS.md §7 caught a regression in the
atomic writer added earlier in this branch: tempfile.mkstemp creates the temp
file 0600, so replacing config.json silently tightened an existing 0644 file to
0600. Under `octop service start --scope system` that runs as root and writes
into the service user's home, a root-owned 0600 config.json is unreadable by the
service user and the unit cannot start; the bare write_text it replaced produced
the umask default.
write_json_atomic now chmods the temp file to the mode of the file being
replaced, or to `0o666 & ~umask` for a new file — matching the two pre-existing
config.json writers (infra/db/rebind.py, infra/backup/auto.py; both
write_text + replace) rather than mkstemp's 0600. POSIX-guarded, so Windows
behaviour is unchanged and no test asserts mode bits off POSIX (AGENTS.md §7).
Also adds the missing CHANGELOG line for the load_config error-message change.
Verified: new file -> 0644, existing 0644 -> 0644, existing 0600 -> 0600.
make all green (3303 passed, 16 skipped); make typecheck-frontend green;
eslint 0 errors (67 pre-existing react-hooks warnings).
Note: `make check-all` is red on develop itself — prettier --check fails on
dashboard/src/pages/Agent/Skills/components/SkillsTabs.tsx exactly as committed
by 83378876. Left untouched here; confirmed pre-existing by checking develop's
version of that file in isolation.
* feat(auth): add optional login captcha (slider, turnstile/hcaptcha/recaptcha, tencent)
Password login can now be guarded by a captcha. Default slider stays
client-only; strong providers are verified server-side with the vendor
(Cloudflare Turnstile, hCaptcha, reCAPTCHA v2/v3, Tencent Cloud Captcha).
- infra/auth/captcha: env snapshot + settings blob resolution, provider
registry with per-vendor verify calls, siteverify with 10s timeout
- GET /api/auth/captcha (public widget config); POST /api/auth/login
accepts captcha_token; tencent token carries ticket:randstr and is
checked via GET with the client IP
- admin GET/PUT /api/settings/captcha behind the new captcha permission;
OCTOP_CAPTCHA_SECRET redacted in envs API; boot validation for strong
env providers
- dashboard: CaptchaField with slider/checkbox/invisible/popup modes,
login page wiring, advanced-settings captcha panel
- docs + i18n (en/zh) + unit/integration/frontend tests
* fix(captcha): verify Tencent tickets via DescribeCaptchaResult (TC3)
The legacy ssl.captcha.qq.com/ticket/verify endpoint rejects current
tickets with response=15 "decrypt fail" even for correct
CaptchaAppId/AppSecretKey pairs. Switch to the official ticket-check API:
- POST captcha.tencentcloudapi.com DescribeCaptchaResult (2019-07-22),
TC3-HMAC-SHA256 signed with CAM API keys; AppSecretKey stays in the body
- captcha settings gain cam_secret_id/cam_secret (encrypted at rest,
env fallback OCTOP_CAPTCHA_CAM_SECRET_ID/KEY); settings view exposes
cam_secret_id + has_cam_secret; dashboard shows the two fields for the
tencent provider only
- interpret maps CaptchaCode (1 OK; 7/8/9/15/16/21/100 fail) and
EvilLevel=100 (malicious) instead of the legacy response=="1"
- TC3 signer moved voice/tencent_sign.py -> utils/tencent_sign.py (shared)
- drop the redundant Host header from signed requests: httpx sets Host
from the URL (same value the signature covers), and an explicit Host
let host-routing system proxies intercept localhost mock calls
- secrets no longer appear in httpx URL logs (payload is in the body)
- move the Login Captcha settings tab next to HTTPS
* polish(dashboard): show reCAPTCHA v3 min score only when that provider is active
* fix(dashboard): captcha widgets follow the site locale, not the browser
- tencent popup: userLanguage from i18n.language (was navigator.language)
- turnstile: pass language render option
- hcaptcha/recaptcha/recaptcha-v3: hl script param from UI locale
- slider already renders site-provided labels
* feat(captcha): unlist reCAPTCHA v2 from the settings catalog
No verified deployment key for v2; keep the provider registered so
existing configs still verify, but stop offering it in the dropdown
(list_providers now filters on a per-provider listed flag).
* feat(cli): octop captcha reset — offline lockout escape hatch
Clears the stored captcha settings blob so login falls back to the
built-in slider when a misconfigured provider blocks all logins
(settings UI requires login, so it cannot fix itself). Boot-env
captcha is untouched; a running server needs a restart.
CHANGELOG: cover reset command, TC3 switch, widget language, v2 unlist.
* feat(cli): register octop captcha reset command and add changelog
Without the registry entry the command module from c5a7649d was
unreachable; CHANGELOG covers the login captcha feature, the TC3
ticket-check switch, widget locale, v2 unlisting, and captcha reset.
* fix(captcha): close pre-merge gaps + document the provider architecture
- envs API: redact OCTOP_CAPTCHA_CAM_SECRET_KEY like OCTOP_CAPTCHA_SECRET
(was exposed in plaintext by GET /api/envs)
- validate_boot: env-only tencent provider now requires the CAM keys,
failing fast at boot instead of at first login
- docs/configuration.md: document the two CAM env vars; note recaptcha
v2 is unlisted but resolvable
- providers.py: architecture docstring (four layers + settled design
decisions + adding-a-vendor recipe); rename _SuccessProvider to
_FormPostProvider; drop the register() cast, which exposed a real
structural bug: frozen-dataclass providers never satisfied the
Protocol's mutable metadata members — declare them read-only
properties so the structural check is real
- verify.py: docstring naming it the execution layer
Keep automatic update reminders on the latest stable release, pin upgrades
to the confirmed version, and require --allow-prerelease for CLI beta installs.
Co-authored-by: Cursor <cursoragent@cursor.com>
`octop run` read host/port straight from config.json, bypassing the env
overrides that `load_config` applies — so with a config.json present the
server bound to the file port while the app config reported the env port.
- config.env_bind_overrides(): single source of truth for
OCTOP_BIND_HOST/OCTOP_PORT semantics (invalid port warns, no override),
reused by load_config and the CLI
- cli resolve_bind(): precedence CLI flag > env > config.json, extracted
for testability
* feat(chat): subagent mention picker, skill slash insertion, ask_agent thread
- Chat @ menu adds a subagent picker and mention-based peer injection
- skill button inserts a leading `/slug` via the slash menu instead of the
skills whitelist; @ menu no longer lists skills
- ask_agent leaves a normal conversation in the target expert using the
caller session_key and a stable thread_id~peer id, without hijacking the
peer's bound session or routing through the gateway inbox
- workspace manifest.json guidance cards are written to harness metadata on
agent start and re-read on list_peers/agent_list/@ injection
- octop service start/restart sets LimitNOFILE via a systemd drop-in
Note: the pre-commit gate is bypassed because this sandbox's installed
harness-agent (orcakit-harness-agent 1.0.6) lacks HarnessAgent.aappend_messages,
which the project's actual runtime provides (already used by
infra/cron/delivery.py on develop). Verified locally: ruff, mypy (against an
API-complete harness-agent checkout), dashboard build, and pytest -m "not live"
(3068 passed; the single failure is a harness-agent-version artifact in
test_build_harness_config_defaults_local_shell_backend, unrelated to this change).
* fix(providers): carry x-opencode-session header for OpenCode Go gateway
OpenCode Go (https://opencode.ai/zen/go) rejects chat requests without
x-opencode-session (HTTP 400 MissingSessionID). Store sets ProviderConfig
session_header for Go URLs so harness fills it with the live thread_id;
probes and model listing inject a throwaway UUID since they never enter
HarnessAgent. User-configured headers win. Requires
orcakit-harness-agent>=1.0.7.
* fix(harness): pass log_dir to HarnessAgentManager to keep diagnostics out of ~/.harness-agent/logs
HarnessAgentConfig.log_dir is a discarded InitVar in 1.0.7, so route the
process log directory through HarnessAgentManager(log_dir=...) at boot.
* fix(setup): guard RLIMIT_NOFILE read on POSIX for mypy + Windows
resource.getrlimit/RLIMIT_NOFILE are POSIX-only; short-circuit on
sys.platform == 'win32' so mypy narrows the module and Windows runtimes
skip the call.
---------
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
* feat(desktop): Octop 桌面客户端 (#462)
* feat: add multi-platform green portable packaging
Ship relocatable Octop-<plat>.zip via scripts/green (portable CPython,
frozen uv.lock deps, Windows pywin32 DLL fix) and octop-portable.yml.
No OOB overlay or product trimming; first run uses upstream setup.
Co-authored-by: Harry <forcemeter@users.noreply.github.com>
* feat(desktop): Wails v3 shell with green portable under ~/.octop
Move green packaging to desktop/portable, add a Wails v3 client that
downloads Octop-<plat>.zip, spawns launch.py with OCTOP_HOME=~/.octop,
and exposes locale/theme/autostart/tray/macOS caffeinate settings.
Green upgrades install into packages via OCTOP_GREEN_PACKAGES.
Co-authored-by: Harry <forcemeter@users.noreply.github.com>
* fix(desktop): add Wails v3 Taskfile and build/config.yml
wails3 build/dev look for Taskfile.yml and build/config.yml in
desktop/src. Un-ignore that tree from the repo-wide build/ gitignore.
Co-authored-by: Harry <forcemeter@users.noreply.github.com>
* 初始化桌面客户端
* add drag and toggle
* add prevent sleep
* update
* change dock icon
* clean package/useless files
* add Attach zips to GitHub Release
* pack file into windows bin-file
* Packages a production app
* chanage workflow name
* chanage workflow name
* linux package in single ifle
* linux package in single ifle
* 在浏览器中打开_blank 的链接
* upgrade python to 3.12
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Harry <forcemeter@users.noreply.github.com>
Co-authored-by: 杨树海 <yangshuhai@pdnews.cn>
* ci: cascade desktop package on release and stop all-branch builds (#463)
Auto Tag dispatches Octop Desktop with Release/Docker. Restrict
octop-desktop.yml to v* tags + workflow_dispatch only.
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
---------
Co-authored-by: Harry <56063441+forcemeter@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Harry <forcemeter@users.noreply.github.com>
Co-authored-by: 杨树海 <yangshuhai@pdnews.cn>
Co-authored-by: jubaoliang <jubaoliang@tencent.com>
Chinese Windows (GBK/cp936) encode piped/redirected CLI stdout with the
ANSI code page, so the checkmark success message in `octop init` raised
UnicodeEncodeError after the DB and admin user were already created.
Re-encode stdout/stderr to utf-8 at the CLI entry to cover the whole bug
class (init, stub, QR art, future emoji).
Add 3 unit tests (tests/unit/cli/test_main_encoding.py): two for the
guard helper and an end-to-end `octop init` run under
PYTHONIOENCODING=gbk. pytest -m "not live" passes locally; the 12
failures are pre-existing Windows/db-migration environment issues,
unchanged by this patch.
* feat: PostgreSQL dual-backend for control plane
Add SqlitePool|PostgresPool behind DatabasePool, parallel PG migrations,
setup wizard defer/bind, pg_dump backups with cross-engine refusal, and
memory defaulting to the control-plane DSN when using PostgreSQL.
* fix(chat): delete checkpoint data when a conversation thread is deleted
Deleting a thread only removed Octop's own metadata row; the actual
message history stayed in the agent's LangGraph checkpointer forever,
making "delete conversation" cosmetic. Also confirm before deleting a
session in the UI, since the action is now genuinely destructive.
Checkpoint deletion runs before the metadata row is removed: if it
fails outright (not just "nothing to delete"), the thread stays
visible/retryable instead of orphaning data with no remaining handle.
* fix(memory): stop suggesting pg_dump for portable export of postgres memory
The 501 hint told users to "use pg_dump on the memory schema instead".
That advice predates the shared-table layout: memory for ALL agents now
lives in one harness_memory schema isolated by a namespace column, so a
schema-level pg_dump would export every agent's memory, not just the
requester's — a cross-agent data exposure if followed on a multi-user
install.
Document the two migration models where the refusal is implemented:
sqlite memory is per-agent files moved via pack/adopt; postgres memory
is shared, and the supported way to use it from another host (e.g.
OpenClaw's harness-memory bridge with --backend postgres) is pointing
that host at the same DSN and namespace — shared, not migrated.
* fix(test): update test_slash_compact to renamed SqlitePool
The /compact test module still imported the pre-rename DBPool, which
broke collection (ImportError) on this branch after develop's compact
feature was merged in — DBPool was renamed to SqlitePool (the concrete
class) with DatabasePool now the Protocol.
DBPool -> SqlitePool at all three sites: the import, the _agent_manager
type hint, and the fixture's DBPool(path) instantiation. SqlitePool is
the byte-for-byte successor of DBPool and matches how every other test
constructs a pool; DatabasePool cannot be used at the instantiation
site (a Protocol is not instantiable).
pytest -m live now collects cleanly (31 selected, 0 errors);
test_slash_compact 3 tests pass.
* fix(test): make memory_backend db_path assertion separator-agnostic
test_explicit_sqlite_overrides_postgresql_control_plane compared the
resolved db_path against the literal "/tmp/ws/memory.sqlite", which the
code never produces on Windows: it builds the path with pathlib, so the
Windows CI job got "\tmp\ws\memory.sqlite" and the string equality failed.
Compare as Path objects so the assertion holds on every OS separator,
matching the .as_posix() discipline already used in test_config.py.
Normalize backslashes in extract_workspace_rel, allow Windows paths in
channel subprocess args, and catch PermissionError when importing external
files; skip or adjust POSIX-specific tests and hardcoded /tmp paths.
Co-authored-by: Cursor <cursoragent@cursor.com>