Skip to content

fix(server): restrict dev-mode cross-origin requests - #2

Closed
sponge225 wants to merge 706 commits into
mainfrom
fix/dev-cors-loopback
Closed

sponge225 wants to merge 706 commits into
mainfrom
fix/dev-cors-loopback

Conversation

@sponge225

@sponge225 sponge225 commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Description

本 PR 修复 OpenViking 本地开发模式下的跨源请求安全问题。

当用户未配置 root_api_key 且未显式指定 auth_mode 时,服务会自动进入 dev 模式。该模式不要求 API Key,并以 ROOT 身份处理请求。同时,服务默认将 CORS 配置为 [*]。在默认监听本机地址的情况下,任意恶意网页仍可借助浏览器请求 127.0.0.1:1933,从而访问本地 OpenViking 实例。

Human Involvement

  • A human participated in the implementation or review loop
  • This PR was generated entirely by AI agents without human participation in the loop

Related Issue

本 PR 暂无关联 Issue。

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement
  • Test update

Changes Made

  • 在 dev 模式下将 CORS 来源限制为 loopback 地址:localhost、127.0.0.1 和 [::1],允许任意本地开发端口。
  • 在 dev 模式下增加服务端请求级 Origin 校验:带有非 loopback Origin 的 HTTP 请求在进入路由前直接返回 403。
  • 保持无 Origin 的 CLI、SDK 和 curl 请求行为不变。
  • 保持非 dev 鉴权模式下原有 cors_origins 配置行为不变。
  • 增加回归测试,覆盖恶意来源的 preflight、实际请求、本地来源以及非 dev 模式的兼容行为。

原始问题与修复后的行为

修复前,攻击链如下:

  1. 用户本机启动 OpenViking,默认进入 dev 模式。
  2. 用户打开 https://evil.example 等恶意网页。
  3. 网页脚本请求 http://127.0.0.1:1933。
  4. CORS 默认配置允许任意来源,dev 鉴权插件又直接返回 ROOT 身份。
  5. 恶意网页可以读取响应,并调用本地实例的 API。

修复后:

  • https://evil.example 等非 loopback 来源无法通过 CORS preflight。
  • 即使请求属于浏览器的简单请求,例如 multipart/form-data,也会在服务端 Origin guard 处被拒绝,不会进入业务路由。
  • http://localhost:5173、http://127.0.0.1:3000 等本地开发页面仍然可以访问 dev server。
  • 本地客户端不发送 Origin 时不受影响。

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this on the following platforms:
    • Linux
    • macOS
    • Windows

验证命令:

HOME=$PWD/.tmp/ovtest-home uv run --all-extras python -m pytest -q tests/server/test_cors.py
uv run ruff check openviking/server/app.py tests/server/test_cors.py

验证结果:4 个测试通过,Ruff 检查通过。

本问题先通过代码路径分析确认,再通过新增测试验证修复行为;未进行真实恶意网页浏览器端到端测试。

Checklist

  • My code follows the project's coding style
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

Screenshots (if applicable)

不适用。

Additional Notes

本修复只针对 dev 模式下“免鉴权 + 任意网页跨源访问”的问题。非 dev 模式的 CORS 配置语义未改变。

r266-tech and others added 30 commits September 29, 2026 11:29
* docs(cli): clarify snapshot commit path requirements

* docs(cli): scope snapshot commit help examples

---------

Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
* feat(reindex): adopt RFV durable planning

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(reindex): transfer root lease ownership

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* feat(reindex): configure root queue concurrency

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(reindex): preserve request context on replay

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(reindex): close RFV planner review gaps

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(reindex): restore telemetry in durable worker

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(reindex): reuse overview summaries for missing vectors

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* perf(reindex): cache overview summary parsing

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(reindex): reuse parent overview for file rebuilds

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* test(sdk): update reindex client contract

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
…volcengine#5407)

Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
* docs(hermes): align setup and capability guidance

* docs(hermes-plugin): prepare README for catalog publication

* docs(hermes-plugin): keep release compatibility detail in review notes

* docs: keep Hermes plugin README focused on users
…engine#5157)

* feat(agent-plugins): add ov-tasks skill for structured task handoff in OpenViking

One task = one markdown file under viking://agent/tasks/<scope>/ (falls
back to viking://resources/tasks on servers that reject agent writes):
frontmatter header + Goal/Context/Decisions/Progress/Next/Questions/Log.
Any agent with the ov CLI can claim, run one slice, verify, write back,
and hand off; references/loop.sh drives unattended ticks. Design notes,
dropped PowerContext/LoopX concepts, and the herdr rehearsal are in
docs/design/ov-tasks.md.

* docs(ov-tasks): write the task body in the task's main language
* refactor(retrieval): 改为全局召回并统一候选精排

移除逐层检索、递归 rerank 和父级分数传播。
仅在 THINKING 且有可用 rerank 客户端时召回 3 × limit 候选并精排一次。
同步现有测试、中英文文档和配置示例,保留原文件名与类名。

* refactor(retrieval): 将精排候选池缩小至两倍 limit
* fix(dsh): forward MCP diagnostic settings

Signed-off-by: Hao Zhe <haozhe4547@gmail.com>

* chore(dsh): bump memory plugin version

Signed-off-by: Hao Zhe <haozhe4547@gmail.com>

* docs(dsh): clarify MCP proxy diagnostic settings

Signed-off-by: Hao Zhe <haozhe4547@gmail.com>

---------

Signed-off-by: Hao Zhe <haozhe4547@gmail.com>
…ne#5449)

* fix(openviking): keep a .env file's own line endings when writing one variable

_write_env_vars round-tripped through text mode, so on Windows "\n".join
came back out as CRLF and an LF .env had every untouched line rewritten --
the exact corruption the surrogateescape read was added to prevent. The
universal-newline read hid it too: CRLF was folded to LF before anything
could observe the file's real ending.

Read and write with newline="" and adopt the ending the file already uses,
so an LF file stays LF and a CRLF file stays CRLF on every platform.

test_non_utf8_env_preserves_unrelated_bytes failed on Windows for this
reason; it passes now, with LF and CRLF cases pinned explicitly.

(cherry picked from commit ea977a9a757b4bf8daa170cba2be2d2587cdfd70)

* test(hermes-plugin): cover credential and setup line endings

* test(hermes-plugin): limit Unix permission checks to POSIX

---------

Co-authored-by: anhtahaylove <everest.kill1@gmail.com>
直接使用向量或 rerank 分数,删除热度混合和无用配置传参。
同步配置示例、现有测试及父目录继承分的遗留文档。
Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
…ine#5444)

DSH 0.1.7 removed `agent/session-start`; per-agent startup now runs from
the serial `agent/created` event with `{ agent, source, signal? }`. The
plugin only listened on the old name, so on 0.1.7+ startup initialize,
early profile injection and the per-session disposal commit never ran.

Start the agent from `agent/created` when the payload carries `source`
(0.1.7+), and keep `agent/session-start` for 0.1.0-rc.6 / 0.1.5, whose
`agent/created` is composition-only and has no `source`. Startup runs
once per agent, resolves to undefined so it never bails the serial
dispatch, returns on abort, and swallows failures instead of failing
agent creation. Bump to 0.5.9.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(ls): 支持返回目录摘要与概览

为 ls 补充 L0/L1 内容控制参数,并同步 CLI、Go/TypeScript SDK、文档与测试。

* feat(go-sdk): 支持获取列表分页状态

新增 ListPage 和 TreePage 接口以返回 has_more,并保持原有 List 和 Tree 接口兼容。

* feat(sdk): 对齐各语言 ls 和 tree 能力

补齐 Python 和 TypeScript 的分页状态支持,并统一 L0/L1、目录过滤及扩展字段参数。

* test: 精简 ls/tree 与 SDK 分页测试

---------

Co-authored-by: zhanghe.1216 <@bytedance.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
…nto archive (volcengine#5451)

* feat(agent-plugins): rename ov-tasks to ov-kanban, fold task detail into archive

Follow-up to volcengine#5157.

- Rename the skill to ov-kanban. A board holds many tasks; the old name
  collided with `ov task` (server processing tasks). Root moves to
  viking://agent/kanban (fallback viking://resources/kanban), `<scope>`
  becomes `<board>`, env vars become OV_KANBAN_*.
- Fold instead of compress: text leaves a task file only by moving to
  archive/<id>.md verbatim (old Log lines, replaced Next/Decisions, long
  evidence). Agents read in layers: boards, board view, task, archive.
- Board view: one `ov grep` returns title/status/owner/updated per task.
- Add the exit rule for `cancelled`; pin the `updated` format.
- loop.sh: run under bash (3.2+) and zsh, quote cwd, remove the prompt
  temp file after each tick.
- Add the skill README with the idea and roadmap (task DAG, scoped
  kanban, recall on session start, claim-aware loop, atomic claim,
  ov compile).

* docs(ov-kanban): link the skill README by GitHub URL

The relative link left the docs site, so check-built-anchors.py could
not resolve its #roadmap fragment.
…olcengine#5457)

Record constraints and workarounds found while deploying v0.3.0 on EKS
from the package and these guides alone: one delivery per cluster and
the cluster-wide license webhook, online license activation, init images
missing from vikinglist, crane import of gzip archives, vdb.yaml fields
init config leaves out or gets wrong (hdfs ConfigMap, oneAgent, tbase
tolerations, sizing), the HDFS root write permission behind indexes stuck
in INIT, and the root_api_key field needed by recent ov CLI.
Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
* fix(cli): accept oidc_token and trusted auth_mode in ovcli.conf

OVCLIConfig says it accepts every field the Rust CLI can write, but it
rejected two ovcli.conf settings that the Rust CLI and the Python SDK
both accept: the `oidc_token` field and `auth_mode: "trusted"` (the
trusted-mode client example in the authentication guide). Either one
made load_ovcli_config raise, so `ov doctor`'s VikingBot check reported
an ovcli.conf api_key as not configured.

* fix(cli): limit shared config fix to OIDC token

Remove the trusted auth mode extension and its test case so this PR only accepts the existing OIDC token field.

* test(cli): remove added OIDC config test

---------

Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
…olcengine#5427)

When a Markdown document is large enough to be split, _build_structure took
the pre-heading virtual section from content before the first heading of
any level, but only added sections for headings at the minimum level. Text
under lower-level headings that appear before the first top-level heading
(e.g. a "## Preface" ahead of "# Chapter 1") belonged to no section and was
silently dropped from the imported resource.

Start the leading virtual section at the first top-level heading instead,
so that text is kept in the document-named section.
Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
…ine#5426)

When a Jina dense config omits `dimension`,
EmbeddingModelConfig.get_effective_dimension() fell through to the generic
2048 fallback, while JinaDenseEmbedder returns the model's native size
(1024 for jina-embeddings-v5-text-small, 768 for -nano). The vector
collection was therefore created with a dimension that does not match the
vectors the embedder writes.

Resolve the Jina default through the same model table the embedder uses,
in both get_effective_dimension() and the credential cross-check.
…olcengine#5435)

format_table_to_markdown, used for Feishu docx tables, embedded sheets,
spreadsheets and bitables, wrote cell text into the table verbatim. A cell
containing "|" added a column, and a multi-line cell ended the row early,
so the rest of the table was misaligned or not a table at all. A column
that is empty in every row also got an empty delimiter cell, which
Markdown renderers do not accept, so the whole table fell back to text.

Escape "|" as "\|", join cell lines with "<br>" (as the AnyDoc table
renderer does), and give every column at least a "---" delimiter.
* feat(retrieval): add event time-decay ranking

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): preserve time-decay compatibility

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): simplify event time-decay ranking

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* chore: keep main formatting after merge

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): keep time decay out of reindex

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): expose time decay score details

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* refactor(retrieval): simplify event time decay

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* feat(retrieval): apply event time decay in cloud and find

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): simplify cloud time-decay routing

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): recall event memories by indexed type tags

* fix(retrieval): use nullable protection for multiplicative event decay

* test(memory): cover extracted peer event updates

* fix(search): align event time decay behavior

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* docs(retrieval): document find decay protection

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): finalize event time decay contract

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): align event decay with tag routing contract

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(retrieval): align extracted tags and account decay routing

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix: rank bounded time-decay candidates inside vector engines

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* refactor(retrieval): drop unused event URI classifiers

* fix(retrieval): remove stale hotness test and align decay contracts

* fix(retrieval): run deferred decay fusion in native engine

* fix(retrieval): apply event decay during vector recall

* refactor(retrieval): remove redundant decay plumbing and tests

* test(retrieval): consolidate event time-decay coverage

* refactor(storage): remove redundant search result processing

* refactor(retrieval): leave openGauss decay options unprocessed

---------

Signed-off-by: lusendong.6789 <lusendong.6789@bytedance.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
`/viking commit` only showed "OpenViking: commit failed". Takeover reports
failure for several different outcomes -- a server `skipped` result, an HTTP
error, an undelivered queue, a capture gap, an archive that failed without a
Working Memory summary -- and every reason stayed in the debug log.

On 0.4.22 a user hit the most common one: the server answered 200
`skipped: all_within_keep_window` because it had already archived everything
older than pi's last three user turns (e.g. an earlier archive whose summary
failed and was dropped, a native-compaction archive with keep 0, or a 0.3.x
commit that kept 3 messages). Nothing was wrong, but pi said "failed".

TakeoverCore now records `lastFailure` on every path that returns false, the
sync layer keeps the HTTP status and server message of a failed commit, and
`/viking commit` shows the reason (a warning, not an error, when there is
simply nothing new to archive). Without takeover, a `skipped` result is no
longer reported as "committed successfully".

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…anging, add the bootstrap (volcengine#5464)

* ci(release): publish a Claude Code URL marketplace to TOS

Each v* release now uploads the Claude Code plugin alone as an immutable
releases/<tag>/openviking-memory-claude.zip, plus a marketplace.json whose
only entry is an archive source (url + sha256) pointing at that zip, with
the entry version taken from the plugin manifest. The manifest is kept
under releases/<tag>/ and copied, no-cache, to plugins/claude/marketplace.json.

Claude Code 2.1.224+ adds such a URL marketplace and installs and updates
the plugin over plain HTTPS, with no git clone. The existing
memory-plugin-marketplace.zip keeps publishing for the other harnesses.

* feat(installer): auto-updating TOS channel for Claude Code 2.1.224+

With --dist tos, a native claude at 2.1.224 or later (the first release
with the archive plugin source) now registers the TOS URL marketplace
(plugins/claude/marketplace.json) instead of the unpacked archive
directory, and sets extraKnownMarketplaces.openviking.autoUpdate in
~/.claude/settings.json, next to the declaration `marketplace add`
writes. Claude Code leaves auto-update off for third-party marketplaces;
an explicit false set by the user is kept on re-runs.

An existing registration with another source (local directory or the
GitHub git-subdir manifest) is re-registered through the existing
claude_marketplace_sync. If the URL marketplace cannot be added, and on
older or custom Claude-format CLIs, the installer keeps the local
directory path it used before.

* fix(claude-plugin): recognize URL marketplaces in the memory doctor

Claude Code caches a url-sourced marketplace as a bare manifest file, so
the doctor's installLocation directory check reported the TOS URL
marketplace as a missing directory. Report it as registered instead,
with its effective auto-update setting (the user-settings declaration
first, since known_marketplaces.json only syncs on the next startup),
and update the doctor skill's notes on TOS updates. Bump the plugin to
0.6.6.

* chore(installer): add a version stamp for release builds

* feat(installer): install every harness from the release bundle

The installer used to clone the repository for anything that needed plugin
files on disk (hook hosts, pi, the OpenCode MCP proxy, the statusline, legacy
Claude Code). The clone ran inside command substitutions, so its state was
lost and every later caller fetched again.

Every file-based harness now reads the plugin bundle
(memory-plugin-marketplace.zip), downloaded at most once per run by
ensure_bundle at top level and only when a step needs it; dev mode uses the
checkout's examples/ directory. The clone, the source-zip fallback
(OPENVIKING_REPO_ARCHIVE_URL) and OPENVIKING_REPO_DIR are gone. The
installer's own helpers now prefer the bundle over the copy an earlier
install left under agent-integrations, which may be older.

OpenCode always gets the file plugin from the bundle. The npm registration
and the MCP proxy snapshot are removed, and jsonc-edit drops an existing
@openviking/opencode-plugin entry so the plugin does not load twice.

The statusline command now looks up the installed plugin's path in
installed_plugins.json on every run, so it follows Claude Code plugin updates
and needs no bundle. A statusline an earlier installer registered is
repointed without a prompt; a foreign one still asks. The memory doctor and
STATUSLINE.md describe the new command.

Bump the plugins the shared library change reaches: codex 0.10.7,
agent-hook 0.5.5, opencode 0.5.3, dsh 0.5.9, pi 0.4.7.

* feat(installer): install from the TOS release channel only

The installer no longer offers a GitHub channel. Claude Code 2.1.224+ uses
the TOS URL marketplace, Codex-format CLIs the TOS git marketplace, and the
rest (and the fallbacks) the release bundle; a checkout still installs
itself in dev mode. The channel menu, the generated git-subdir manifest for
Claude Code and the GitHub git marketplace for Codex are gone.

--dist github, --source remote and OPENVIKING_REPO_URL / _REF / _BRANCH are
still accepted: the installer says once that they are ignored and carries
on. --dist tos is accepted as a no-op. A re-run moves Claude and Codex
registrations that point at GitHub to TOS through the existing sync
helpers, and never deletes the old directory marketplace, which other
Claude-format CLIs may still use. When ~/.openviking/openviking-repo is no
longer referenced from ~/.claude/settings.json, the run ends by saying it
can be deleted.

A copy of install.sh that is neither a release build nor in a checkout (for
example one fetched from raw.githubusercontent.com) now downloads the
release installer, checks its marker line and runs it with --dist tos and
the original arguments, so the script and the bundle come from one release.
If that fails it continues with itself. OPENVIKING_INSTALLER_REEXEC=0 turns
this off.

Codex's TOS path is folded into codex_marketplace_sync with the directory
fallback, and codex_unregister replaces the repeated remove/uninstall/
marketplace-remove triple.

* feat(installer): detect the UI language instead of asking

The language menu is gone. The language comes from --lang, then
OPENVIKING_LANG, then LC_ALL / LANG. When those are unset or C/POSIX on
macOS, which is common in terminals started from the GUI, the system
language is read from `defaults read -g AppleLocale`. Anything else is
English.

With no menu to wait for, the language is settled before the release
re-exec, so its one message no longer needs its own lookup.

* feat(installer): check the server and confirm before changing anything

The credentials step used to write ovcli.conf as soon as it had an answer,
so a run abandoned at any later prompt had already changed the machine.
Now the run gathers the connection, checks it, shows what it will do and
asks, and only then writes ovcli.conf and installs.

- A first-time setup offers the local server first; the keep/reconfigure
  menu for an existing config is unchanged.
- The check asks the server for /health, then /api/v1/system/status with
  the key and the account/user headers, and tells an unreachable server,
  a rejected or missing key and any other HTTP status apart. Headers go to
  curl on stdin, so the key never appears in the process list. On a problem
  an interactive run offers to re-enter the URL and key or to continue; an
  unreachable server defaults to continue, since a local server is often
  not started yet. Without curl the check is skipped.
- The review lists the harnesses and commands, the server, the masked key
  and, per harness, the files and commands the install will touch.
  Cancelling exits 0 with nothing written. --yes and runs without a tty
  print it and go on.
- A run without a tty that got no --url, no --api-key and found no config
  says after the review that it configured http://127.0.0.1:1933 without a
  key, with a command to re-run against another server.

* feat(installer): number the steps and end with what to do next

Headings read [n/N], with N counted from this run: credentials, review,
one step per install (each Claude- or Codex-format CLI counts once) and
validation. TRAE and TRAE CN get a heading of their own. The environment
check prints only errors and warnings.

The closing section lists, per installed harness, what to do next (restart
or /reload), how updates arrive (automatic for Claude Code on the URL
marketplace and for Codex-format CLIs on the TOS git marketplace, a re-run
of the installer otherwise), a verify command where one exists, and how to
uninstall.

* feat(installer): pin the release through the install-site resolver

After the confirmation step, a default-channel install asks
$OPENVIKING_INSTALL_SITE/install/v1/<harness>.json which release to
install, one parallel request per selected harness under its public name
(a TraeCode CLI alias counts as trae-cli). An answer is used only when it
is schema 1 and every URL points into the TOS bucket: the bundle comes from
its bundle_url and must match its sha256, and Codex registers its git_url
unless OPENVIKING_CODEX_TOS_GIT_URL is set. Anything else, including a
timeout, keeps the built-in latest-release URLs without output.
DO_NOT_TRACK, dev mode and --uninstall skip the request, and
OPENVIKING_MARKETPLACE_ARCHIVE_URL replaces the bundle URL and its check.
The final summary shows the resolved plugin version.

Tests run the installer against a local resolver; every other installer
test sets DO_NOT_TRACK so none reaches the live site.

* feat(installer): skip Claude Code without the plugin command

Claude Code older than 2.0 has no 'claude plugin'. The installer used to
wire such builds up with 'claude mcp add' plus a hooks merge into
settings.json, which needed the release bundle and never updated. It now
skips them with a warning to upgrade Claude Code; the plugin README keeps
the manual steps.

* refactor(installer): validate the hook hosts through one function

Cursor, TRAE, TRAE CN and ZCode run the same hook runtime and were
validated by four near-identical blocks that differed only in paths and
the session-start event name. One function now takes the host and keeps
every check: the hooks and MCP entries, the installed files, node --check
and the hook smoke test. The TraeCode CLI block is gone: trae-cli is
installed as a Codex-format plugin, so the harness never reaches
validation.

* docs(installer): describe what the installer does, contacts and writes

The header of install.sh now states, for people and agents reading it
before a run: what it installs, the hosts it contacts, the files it writes
per harness, that it needs no sudo and stays inside $HOME, the
non-interactive form, how to uninstall, how to compare a release copy
with the repository, and what the release lookup sends and how
DO_NOT_TRACK skips it. usage() lists the environment variables and the
retired --dist values; the shared README gains an Installer section and
the capability reference drops the GitHub channel.

* feat(installer): add the bootstrap served at openviking.net/install

The bootstrap downloads the installer from the TOS bucket, refuses anything
that is not the installer script, and runs it with --dist tos, the optional
per-harness --harness value and the caller's arguments, reading input from
/dev/tty when there is one so `curl ... | bash` can still prompt.

* ci(release): stamp the release version into the published installer

The uploaded copies of memory-plugin-shared/install.sh now carry
INSTALLER_VERSION="<tag without v>"; the repository file keeps "dev". The
job fails before uploading anything when the dev line is missing or
duplicated.

* ci(release): publish the installer bootstrap next to the installer

bootstrap.sh is uploaded as an immutable per-release copy and, with the
stable paths, as memory-plugin-shared/bootstrap.sh.

* ci(release): publish the Codex git marketplace in a safe order

A single `s3 sync --delete` uploaded info/refs, the pack list and the new
pack in no set order and deleted the old pack in the same pass, so a Codex
client that checks the marketplace on startup could see refs naming a pack
that was not there yet. Upload the new packs first, then objects/info/packs,
packed-refs, info/refs and HEAD, and only then delete what the new
repository no longer contains.

Stop uploading releases/<tag>/memory-plugins.git; nothing references it.

* ci(release): keep stable paths on manual TOS runs unless asked, document hotfix and rollback

A manual run of release-tos on an older tag used to overwrite the Claude
Code marketplace and the Codex git repository by default, which moves every
auto-updating user to that release. update_latest now defaults to false.

RELEASE.md and RELEASE_CN.md describe how to publish a memory plugin hotfix
without a product release, how to roll back, and when each host picks the
change up.

* fix(plugins): run entrypoint CLIs started through a symlinked path

Scripts that are also imported as modules ran their CLI only when
fileURLToPath(import.meta.url) equalled path.resolve(process.argv[1]).
Node resolves the entrypoint's module URL through symlinks, argv[1] keeps
the path as typed, so with a symlinked ~/.openviking or HOME the check
failed and the script exited 0 without doing anything. The installer's
host-json-config helper was one of them: an uninstall that read it from
$OPENVIKING_HOME left every hook and MCP entry in place.

Compare the real paths of both sides, the check ov-memory-doctor and the
URI guard already use, in the installer helpers, every plugin's MCP proxy,
sync.mjs, the pi experiment's credentials CLI and the release scripts.

* fix(installer): finish validation when the Codex plugin cache is missing

Validation looked for the cached stdio proxy with a find pipeline under
pipefail, so a missing cache directory made the whole run exit 1 after a
successful install. Add tests for the native Codex path: moving a GitHub
marketplace to the TOS git repository, the bundle fallback and the updates
line.

* fix(installer): honor CLAUDE_CONFIG_DIR and CODEX_HOME

Claude Code and Codex read their settings, marketplaces, plugin registry
and cache from CLAUDE_CONFIG_DIR and CODEX_HOME when set, but the installer
always used ~/.claude and ~/.codex. With CLAUDE_CONFIG_DIR set it enabled
marketplace auto-update and registered the statusline in a file Claude Code
does not read, leaving a stray declaration that the printed uninstall
commands never clean. With CODEX_HOME set it wrote config.toml where Codex
does not look. CODEX_CONFIG_FILE still takes precedence for config.toml.

* fix(installer): fall back to the bundle when the URL marketplace install fails

Registering the TOS URL marketplace first removes an earlier GitHub or
directory registration together with its plugin. When the plugin then
failed to install from the URL marketplace, the installer aborted and left
Claude Code with no plugin, and the remaining harnesses were not installed.
It now falls back to the bundle's directory marketplace and retries once,
as it already did when the URL marketplace could not be added.

* test(installer): keep test installs off the default server port

Every installer run checks the server it is given, and the tests passed
http://127.0.0.1:1933, the port a developer's own OpenViking server
listens on. Runs whose assertions do not depend on the check now use a
closed port, and the run that relies on the default URL gets a curl that
cannot connect.

* fix(installer): keep handled CLI failures from tripping the error report

claude_cmd and codex_cmd ran the CLI through `command`. When a program
started that way fails, bash 3.2 (macOS /bin/bash) runs the ERR trap even
inside `if`, `!` or `||`. So every handled failure, such as a TOS
marketplace that cannot be added or `claude plugin enable` exiting 1 right
after an install, printed "OpenViking installer stopped unexpectedly" and
disarmed the trap, and a real failure later in the run exited silently.
The CLIs are now called directly.

A Codex git marketplace that cannot be added is followed by the fallback
to the bundle, so its error output is silenced again, as before the TOS
channel moved into codex_marketplace_sync.

* fix(installer): run the downloaded release installer from its own directory

The re-exec saved the release installer as a file directly in $TMPDIR
and ran it there. The installer takes a lib/install found beside the
running script as its own helpers, so a stray or planted $TMPDIR/lib/install
became the installer runtime. The copy now runs from a private mktemp -d
directory, which is removed afterwards.

* fix(installer): run when bash reads the script from stdin

With the script piped into bash (`curl ... | bash`), BASH_SOURCE is
unset, and resolve_self_checkout aborted the run under set -u. It now
treats such a run as one outside any checkout.

* fix(installer): keep the API key out of node's arguments

Writing ovcli.conf passed the URL, API key, account and user to node as
process arguments, which any user can read in the process list while it
runs. They now reach that one node process through its environment.

* fix(installer): print an uninstall command Codex accepts

The next steps told Codex users to run `codex plugin uninstall`, which
Codex rejects (it only has `plugin remove`), and the `&&` then skipped
the marketplace removal. Codex-format CLIs now get `plugin remove`, and
TraeCode CLI, which only has `plugin uninstall`, keeps it.

* fix(installer): ask about the statusline before the review

The review listed the statusline under ~/.claude/settings.json for every
native Claude Code install, but a run without a tty skips it unless
--statusline is given, and an interactive run only asked about it after
the install had been confirmed. The question now comes before the review,
and the review names the statusline only when the install will write it.
Without a tty nothing changes: only --statusline, or a statusline an
earlier installer registered, gets written.

* fix(installer): list the TRAE CLI cleanup and the shared hook runtime in the review

The review left out two things the install changes: the deprecated TRAE
CLI Hooks integration a TraeCode CLI install removes from
~/.trae/cli/hooks.json, ~/.trae/traecli.toml and
~/.openviking/agent-integrations/trae-cli, and the shared hook runtime in
~/.openviking/agent-integrations/memory-plugin-shared that Cursor, TRAE,
TRAE CN and ZCode run from.

* fix(installer): translate the statusline lines and name the install source

A Chinese run still printed "Sample:", "statusline registered" and the
OPENVIKING_STATUSLINE hint in English, and the review showed the internal
source mode ("archive"). Those lines go through t now, and the review
names the source as the OpenViking release or this checkout.

* ci(release): build the plugin zips reproducibly

zip stores each file's mtime, so every run of the workflow built
memory-plugin-marketplace.zip and openviking-memory-claude.zip with new
bytes and overwrote the releases/<tag>/ keys. A manual re-run keeps the
stable paths by default, so plugins/claude/marketplace.json and
releases/latest/channels.json went on pinning the old sha256, and every
Claude Code URL-marketplace install or update and every checksum-verified
bundle download then failed. Both zips now come from one script that
stamps every entry with the tagged commit's time, sorts the entries and
leaves out extra attributes, so a rebuild of the same tag gives the same
bytes.
volcengine#5477)

* feat(installer): download from the docs site, from the host that answers first

The installer, the plugin bundle and the Claude Code and Codex marketplaces are fetched from the documentation site, which serves them under /dl from docs.openviking.net and docs.openviking.ai. The bootstrap downloads the installer from both at once and runs the copy that arrives first; the installer run on its own asks both for one small file. The host that answered serves the rest of the install and is the one Claude Code and Codex keep for their own updates.

OPENVIKING_DOWNLOAD_BASE names other locations, separated by spaces. OPENVIKING_TOS_BASE is still read. The commands the installer prints use the https://openviking.net/install entry.

* ci(docs): publish the plugin downloads with the docs site

Both docs deployments build the installer, the plugin bundle, the Claude Code URL marketplace and the Codex git marketplace from the commit they deploy and publish them under /dl, laid out like the TOS release bucket. They now also run when a plugin, the installer or a release script changes on main, so a plugin change is published when it is merged.

The zips are stamped with a fixed time there, so a zip changes only when its content does and the Claude Code zip keeps its bytes for as long as the plugin version names it.

* docs(plugins): install through the site entry in plugin guides and doctor hints

* chore(plugins): bump the versions past the ones main released

Main released the previous versions with the installer rework; the plugin READMEs, doctor hints and skills change here, and main released pi 0.4.7 in the meantime.

* docs(plugins): say what the installer's header holds in the note for AI agents

* feat(installer): publish the version check's channel data with the docs downloads

The docs download tree carries releases/latest/channels.json for the install site's resolver. It names the published version and no checksum: the two hosts deploy one after the other and keep no earlier build, so a pinned bundle would fail to verify while they differ.

The resolver answers with one host's addresses, so the installer reads the same paths from the download location it is using.
…ine#5487)

The /install/v1 route that answers the version check can only go live on openviking.ai for now, so the installer asks there by default. The bootstrap no longer sets OPENVIKING_INSTALL_SITE: the site serves a copy of it that changes rarely, while the installer is republished with the docs on every merge, so the installer alone decides where the check goes and moving it back needs no site release. The /install entry itself works on both hosts and the commands that name it are unchanged.
* feat(search): add the keywords search type

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix: import cycle and fulltext capability caching

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
…tovers, and plainer prompts (volcengine#5490)

* feat(installer): ask for the language before anything else again

An interactive run opens with a Language / 语言 menu, preselecting the language the locale suggests, so a wrong guess costs one keypress instead of a whole run in the other language. --lang, OPENVIKING_LANG, --yes and a run without a terminal skip the menu and use the locale as before. The menu comes after the hand-over to the release installer, so the bootstrap path asks once.

* fix(installer): clear what earlier installers left in Claude Code and Codex

Installers before the unified `openviking` marketplace registered `openviking-plugins-local`, and their fallback for older Claude Code merged hooks into settings.json and added a user-scope `openviking` MCP server running the plugin's proxy. volcengine#4773 dropped the cleanup on the assumption that every install had moved on, but machines that skipped the releases in between still carry them.

In Codex, once that marketplace's directory is gone every `codex plugin marketplace list` fails. The installer then cannot see the `openviking` registration a GitHub install made, skips removing it, and both the git and the directory `marketplace add` fail with "already added from a different source". While the directory is still there, the old plugin runs next to the new one. In Claude Code, the old plugin, the merged hooks and the MCP server all run next to the plugin, so every hook and tool runs twice.

The Claude Code and Codex steps now remove those leftovers first, and the review lists them. Only hooks that run the plugin's scripts and an `openviking` server that runs its proxy are touched. Codex also retries `marketplace add` after unregistering the name when it reports a different source, since a registration `marketplace list` cannot report still holds it. The other harnesses already reclaim what their earlier installs wrote.

* fix(installer): plainer prompts and key hints for first-time users

- An interactive install opens with two lines on what it does and that every change is confirmed first.
- The key hints sit beside each question, in bold, instead of in a line under the options that people missed; the language menu's hint is bilingual since the language is not known yet.
- The tool menu says "AI tools" instead of "harness", "(not installed)" instead of "(not found in PATH)", and marks adding another command as advanced.
- The DeepSeek Harness profile is no longer asked for; --dsh-profile still picks one.
- Choosing the cloud service shows where to get an API key; the key prompt says that typing is not shown and only mentions unauthenticated servers when one can be; "keep current" is offered only when there is a current URL.
- The statusline question says it is for Claude Code.
- The review lists removing the deprecated TRAE CLI Hooks integration only when there is one.
* feat(debug): expose vector metric and dense score semantics

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* refactor(observer): reuse vikingdb status for score semantics

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(observer): preserve status contract and docs links

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
…engine#5483)

Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
t0saki and others added 29 commits October 15, 2026 02:23
…state (volcengine#5936)

* fix(opencode-plugin): prune stale fully-captured sessions from local state

* fix(opencode-plugin): persist pruned session state right after load
…onger has the session (volcengine#5935)

* fix(opencode-plugin): drop local session mapping when the server no longer has the session

* fix(opencode-plugin): tighten stale-mapping comment; assert commitSession returns not_found
…5981)

* fix(gateway): log the capture status a request ends with

The request log row carried the capture status read when the request was
prepared. A long request that started while delivery was retrying finished
after the worker's delivery_recovered row, so its stale "retrying" became the
newest status for the session and /admin/overview kept counting the capture
in capture_issues until another request arrived.

Re-read the capture document just before the row is written.

* fix(gateway): keep the request log row when the capture refresh fails
…nt (volcengine#6102)

Ark/OpenAI SDK read timeouts say "Request timed out." and bare timeout
exceptions carry no message, so both were classified as unknown and
never retried. Minimal fix extracted from volcengine#5927.

Fixes volcengine#5513
* refactor(session): 清理无生产调用的旧实现

移除仅测试使用的图展示和提示词生成器,以及未使用的文本补丁解析路径。
保留结构化补丁和描述渲染契约,删除对应的过期测试。

Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>

* refactor(session): 保留记忆图展示实现

按维护者要求保留 graph_view.py,仍删除对应的专属单测。

Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>

---------

Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
… and outputs (volcengine#5937)

* fix(plugins): apply capture filter substitutions to tool parts and warn on invalid rules

filterCaptureParts carried tool call/result parts through untouched, so a
captureFilters redaction rule never reached a secret that appeared in a tool
input or output, and those payloads were stored verbatim. Substitutions now
run over tool_input values and tool_output too; only text parts still decide
the drop/keep verdict.

A rule that failed to compile was skipped without any signal at capture time.
It is now reported on stderr once per process.

* fix(plugins): keep capture filter change scoped to tool-part redaction

Drop the capture-time invalid-rule warning so it does not overlap the
separate config-load warning work, and document that s rules now rewrite
tool call inputs and tool result outputs while d/k rules only read text.
Session 记忆提取统一走归档和后台任务流程,删除 Web Studio 的重复入口、客户端声明及过期测试,并更新中英文迁移说明。

BREAKING CHANGE: 移除 POST /api/v1/sessions/{session_id}/extract,调用方需改用 commit 并查询返回的任务 ID。

Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
Remove the estimator left without production callers after the truncation
rewrite, along with its test-only assertions. Keep truncation coverage.

Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
…ngine#5760) (volcengine#5771)

* fix(embedding): truncate plain list inputs to max_input_tokens (volcengine#5760)

The OpenAI-compatible embedder's _prepare_embedding_input passed plain
list[str] inputs straight through to the provider, skipping the
max_input_tokens guard entirely, while the base prepare_embedding_input
collapsed the same input to an empty string for text-only embedders and
left items unbounded for multimodal embedders. The embedding queue
contract explicitly allows list inputs, so any producer could submit
oversized items that fail with provider errors (e.g. bigmodel 1210).

Both guard layers now keep the list shape and bound every item
individually via truncate_embedding_input; max_input_tokens=None keeps
the historical passthrough. Single-string and dict-part multimodal
behavior is unchanged.

* fix(embedding): narrow batch truncation to OpenAI input guard

---------

Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
…#6137)

Avoid invalid Chat Completions requests by selecting none for affected
models when tools are present. Preserve explicit settings and Codex defaults.

Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
A short skill that teaches agents how to search and read resources under
viking://resources/: pick find/grep/glob/list, then drill from a
directory's .abstract.md -> .overview.md -> the file.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…l task errors (volcengine#5622) (volcengine#5772)

on_dequeue only guarded _parse_message; any exception raised inside
_process — e.g. the json.JSONDecodeError from Session.load() reading a
torn line of the live messages.jsonl during a compact commit — escaped
the handler. The queue layer then swallowed it: the delivery was never
acked, the queue row stayed in processing, and the task record stayed
pending until the client gave up with "Phase 2 timed out". A worker
restart replays the same payload into the same race, so recovery never
completes on its own.

Wrap the _process call and mirror the existing session-missing branch:
reuse the Phase-1 task record (tracker.create is idempotent for an
explicit task_id owned by the same user), record the real error via
tracker.fail, and settle the delivery as ProcessResult.failed so clients
see the underlying crash instead of a timeout.

Out of scope (noted in the PR): locking Session.load() against the
Phase-1 drain-rewrite, switching Phase 2 to the immutable archive, and
client timeout semantics.

Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
…ine#6148)

* fix(studio): preserve setup across upgrades and respect asset base

* fix(studio): remove code badge from open-source connections

* fix(studio): remove redundant setup back button

* fix(studio): use green for active connection status
Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
…s queued (volcengine#6077)

Fixes volcengine#6076

Co-authored-by: mayunxiang.26 <mayunxiang.26@bytedance.com>
)

* fix(storage): avoid blocking executor on pathlock waits

Run contended PathLock acquisition on the native Tokio runtime so unrelated storage work can keep using Python's shared executor.

* refactor(storage): simplify async pathlock bridge

* test(storage): 对齐账户测试后端的异步锁接口

替换已有测试接口,保留原契约和用例;测试总行数不增加。

Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>

---------

Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com>
…key docs (volcengine#6107)

* docs(gateway): explain how to find or reset a lost management key

Add a short "Lost the management key" section to the operations guide and
link it from the troubleshooting entry and the management_key_invalid error.
In Studio, the unlock dialog now lists the server configuration page and how
to reset the key, and the enable-gateway reveal no longer implies the key is
shown only once, since it is saved in ov.conf.

* feat(gateway): add a quick mode card to the Studio overview

Before the first key exists, the Get started checklist offers a quick mode
card that does the first three steps in one click: it imports OpenViking's
VLM settings when no upstream is usable, reuses or creates the "default"
context profile, and issues a key for the current user. Root outside dev mode
gets the issue dialog prefilled to pick the key's user. When the management
key is locked and something is missing, the card offers unlocking instead.
…ts (volcengine#6101)

* feat(gateway): support queries, {api_key} and key_header on upstreams

Builds on append_endpoint_path: base_url may carry a query (stored
parameters win over client ones), {api_key} fills the key into the URL
with no auth header, and key_header picks Bearer, api-key or x-api-key.
Exact endpoints serve their protocol's path and its sub-paths only, so
count_tokens and stored responses continue after the endpoint while
embeddings and the connection test no longer reach them.

* feat(gateway): import Azure OpenAI VLM credentials as exact-endpoint upstreams

* feat(studio): choose the key header and accept queries and {api_key} in upstream URLs

* docs(gateway): document upstream queries, {api_key}, the key header and Azure
…teway (volcengine#6119)

* feat(mcp): add download tool that returns a single-use download link

The MCP server cannot write onto the agent's disk, so `download(uri)` mirrors
the add_resource local-file flow in reverse: it stats the file under the
caller, mints a single-use token bound to the caller identity and the
resolved URI, and returns a curl command for
GET /api/v1/content/download?token=. A token request serves only the bound
URI and re-checks ACL at fetch; API-key requests (ov get) are unchanged.

Download tokens live in a separate store so they can never be redeemed on
temp_upload, or the reverse. Upload and download tokens are now 10
characters instead of 6.

* feat(gateway): proxy MCP download links and add mcp_file_links option

The gateway calls OpenViking's /mcp on its internal URL, so links OpenViking
mints point at an address clients cannot reach. Upload links were already
rewritten to the gateway's /uploads; download links were passed through
unchanged.

New gateway setting mcp_file_links governs both kinds of link:
- proxy (default): rewrite upload and download links to public_url and
  forward them through /gateway/uploads and the new /gateway/downloads,
  dropping OpenViking's "auto-detected URL" note that no longer applies.
- direct: pass OpenViking's links through unchanged; the server's
  OPENVIKING_PUBLIC_BASE_URL must be reachable by the client.

download requires a client shell tool, like local-path uploads. A missing
public_url in proxy mode is now reported to the model instead of being
masked by the generic tool error.
…y and reply lookup (volcengine#6026)

* perf(gateway): look up reply owners through replay_kinds

Without ANALYZE statistics SQLite plans replies() as a scan of the
covering primary key over every record of the scope, which costs about
20 ms per request at 1k stored sessions. Force the (scope,kind,anchor)
index: 0.2 ms at 1k sessions, and cheaper on small stores too.

* perf(gateway): search auxiliary patterns case-sensitively on lowercased text

classify() ran six re.I searches over str(system) on every request; re.I
disables re's literal search, so each scanned every offset (~3.8 ms at 80k
chars). Lowercase the probe once and search lowercased plain patterns
instead, falling back to re.I when the text has one of the three letters
where lower() and re.I disagree. Also skip the deferred-tool regex in
client_openviking_tools for texts without '__'.

* perf(gateway): cut per-event cost of the streamed reply relay

- SSEDecoder.feed scans a chunk once with a precompiled newline-first pattern
  and copies the remainder once, instead of an uncompiled `\r?\n\r?\n` search
  that tried a match at every byte and re-sliced the buffer per frame.
- The hidden tool loop writes all events decoded from one upstream chunk as
  one client write (events before a failure included), so a loaded worker
  makes one ASGI send per chunk instead of per event.
- The relayed-reply capture reads the assembled reply once per chunk.
- Streamed text, thinking, signatures and tool arguments grow in place, so a
  long reply no longer costs time quadratic in its length. A delta that cannot
  be appended leaves the text streamed so far intact.

Client bytes and replay records are unchanged.

* perf(gateway): hash only the reply when anchoring it after the request chain

relayed, record_reasoning, the tool loop's persist and capture staging each
recomputed hidden_chain over [*request.messages, *reply], re-normalizing and
re-hashing the whole history (about 1 ms at 140k tokens per call, three or
four times per request, mostly through a thread handoff). The request's
capture_chain already is that history's chain, so prefix_chain/hidden_chain
now take the chain to continue (`after`) and reply_chain hashes only the
reply. Anchors are bit-identical: the continued chain seeds from the last
non-empty digest, exactly the state the full pass carries.

* perf(gateway): skip rereading the captured reply for chunks that complete no event

A chunk that ends inside an event, common when the upstream splits large
frames, made the relayed-reply capture rebuild the whole reply so far for
nothing. With 64-byte chunks a 395-event reply cost 4.0 ms of capture CPU
instead of 2.9 ms.
…6157)

Since volcengine#6077 on_resource_added returns once the reason commit is queued, but
memory_linking.status still said "success", which reads as a finished link.
It now says "accepted" (the word connector submissions already use), and the
API docs say to poll commit_task_id for the result. Follow-up to the volcengine#6077
review.

Co-authored-by: mayunxiang.26 <mayunxiang.26@bytedance.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@sponge225 sponge225 closed this Oct 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.