yetuge 8a3350ac8a
fix(tools): publish the default agent's SOUL.md atomically (#6668)
* fix(tools): publish the default agent's SOUL.md atomically

setup_agent persisted the default agent's global SOUL.md with a
truncating write_text. The open truncates the previous file before the
new content lands, so an interrupted setup — process kill, disk full,
an exception while flushing — leaves a truncated persona file, and the
prompt builder reads SOUL.md on every run, so the agent then silently
runs with part of its instructions missing.

The same file is already published atomically everywhere else: the
custom-agent store stages it to a temporary and commits with
os.replace, and #6618 replaced the identical truncating pattern for
the Lark CLI runtime files the same day. The default agent's copy was
the one remaining write of this file that could tear.

Publish through a staged temporary plus os.replace, mirroring the
store's _stage_temp commit, and clean the temporary up when the
publish fails so no partial artifact survives.

Tests: a failed publish (os.replace raising) keeps the previous
SOUL.md byte-for-byte and leaves no temporary behind, a control pins
that the old truncating write loses the content in the same failure,
and the happy path writes the full file.

Fixes #6667

Signed-off-by: yetuge <2219677952@qq.com>

* fix(tools): keep SOUL.md's mode and clean the temporary on staging failure

Review follow-up on #6668, both non-blocking suggestions that guard the
publish itself:

- NamedTemporaryFile creates the temporary with mode 0600, so the first
  publish made the global SOUL.md 0600 (the old write_text produced 0644
  & ~umask) and every re-publish clobbered a mode an operator had set.
  Stat the target and chmod the temporary to that mode (0644 for a first
  publish) before the replace — the order #6618 codified for the Lark
  runtime files and atomic_write_extensions_config already follows.
- A failure inside the staging block (handle.write or the implicit
  flush/close, e.g. ENOSPC) raised before the try started, orphaning the
  delete=False temporary in base_dir. One try/finally now spans staging
  and publishing, so neither phase can leave a temporary behind.

The exact POSIX mode semantics were verified outside the Windows host
(WSL Ubuntu: mkstemp is 0600, first publish is 0644, a re-publish keeps
an operator-set 0640, and both failure phases leave no temporary); the
exact-mode tests are therefore POSIX-gated, with a cross-platform test
pinning that the chmod lands on the temporary before the replace and a
staging-failure test that is red on the previous head.

The shared-helper suggestion is left as a follow-up: folding six
independent publishers into one helper is a cross-module refactor, not
part of this bug fix.

Fixes #6667

Signed-off-by: yetuge <2219677952@qq.com>

---------

Signed-off-by: yetuge <2219677952@qq.com>
2026-10-11 14:49:41 +08:00
..