4 Commits
Author SHA1 Message Date
Martin Vogel 33534653f1 Merge branch 'main' into fix/1954-uninstall-symlinked-config 2026-09-11 23:26:33 +02:00
Martin VogelandClaude Fable 5.1 3a4160b461 fix(cli): complete the OpenHands integration (settings.json mcp_config + agent-profile refs) (#1826)
install wrote only part of the OpenHands config. Register the server globally
under ~/.openhands/settings.json -> mcp_config {transport:stdio, command, enabled}
(owned-entry, unrelated content preserved), and add "codebase-memory-mcp" to
each ~/.openhands/agent-profiles/<p>.json mcp_server_refs array (null -> [it],
existing -> appended once). uninstall removes exactly what it added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 20:38:22 +02:00
Martin VogelandClaude Fable 5.1 e5aabdf86e fix(cli): follow user-owned symlinked config files in the editors
Every config editor (JSON-like, TOML, YAML, text) opens with O_NOFOLLOW
and refuses symlinks, so a link planted at ~/.cursor/mcp.json cannot
point a privileged writer at another file. Users keep exactly those
files as symlinks into a dotfiles checkout; the blanket refusal made
install and uninstall fail on them and, before the previous commit,
turned uninstall into data loss (#1954).

Decision C: a symlink the INVOKING user owns, resolving to a regular
file the same user owns, is followed — as an explicit opt-in of the
agent-configuration writers only. cbm_install_agent_configs and the
uninstall activation register the user's configuration roots (HOME,
XDG_CONFIG_HOME) for the duration of their work; a link whose own
directory is outside every registered root, or any link seen by another
editor caller, stays refused, so repository paths never follow.

One shared helper (src/cli/config_edit_path.c) runs the race-safe
sequence: lstat(link) → owner == invoking uid (a root process follows
root-owned links only) → realpath → the target's parent directory
opened as a dirfd (O_DIRECTORY|O_NOFOLLOW; owned by the invoking user
or root, not writable by others unless sticky) → openat(dirfd, base,
O_NOFOLLOW) → fstat: regular file, same owner, link count 1, and the
same (dev, ino) as stat(realpath). Each editor reads through that
validated descriptor, stages its temp file with openat(dirfd, …,
O_CREAT|O_EXCL|O_NOFOLLOW), runs its pre-publish snapshot comparison
against the resolved path, and commits with renameat on the pinned
dirfd, so the link itself is never replaced. Every other link is refused
with an observable reason (surfaced in the uninstall diagnostics):
another owner, root over a non-root link, dangling, a directory or
special target, a hard-linked or swapped target, a foreign or
world-writable parent. Whether the target must also stay under a root
is a pending user decision behind CBM_CONFIG_EDIT_TARGET_UNDER_ROOT
(default: anywhere the user owns). Windows never follows reparse points.

Tests: each editor suite gains a same-owner link edited in place under
the opt-in (link preserved and still pointing at the same target, temp
file staged beside the target, bytes identical to the same edit on a
plain file, no stray temp files) and keeps its refusal cases; the
JSON-like suite adds no-opt-in, outside-root, dangling, directory,
identity-skew and world-writable-parent refusals. The foreign-owner and
root-run refusals move the observer through a test seam (a test cannot
create a foreign-owned link without root; a real root run demotes the
link first). test_cli.c drives `uninstall -y` end to end through a
symlinked ~/.cursor/mcp.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 02:04:51 +02:00
Martin Vogel a3903caa0b feat(cli): expand agent integration coverage
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-13 01:50:02 +02:00