Skip to content

fix(session): opt-in direct AT-SPI start when its activation is denied - #95

Merged
isac322 merged 22 commits into
isac322:mainfrom
superbiche:fix/atspi-direct-launch
Oct 15, 2026
Merged

isac322 merged 22 commits into
isac322:mainfrom
superbiche:fix/atspi-direct-launch

Conversation

@superbiche

@superbiche superbiche commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On Fedora with SELinux enforcing, virtual sessions had no accessibility: the policy denies the D-Bus activation of at-spi-bus-launcher from kwin-mcp's private dbus-run-session bus (fedora-selinux/selinux-policy#3385), and the registry's activation from the accessibility bus is denied the same way. wait_for_element and find_ui_elements then failed with AT-SPI2 query failed: AT-SPI2 worker exited.

With KWIN_MCP_ATSPI_DIRECT_START=1 in the server's environment, when the activation fails, the session wrapper starts the launcher and then the registry directly and waits up to 5 s for each to own its bus name. Their commands come from the Exec= lines of the same D-Bus service files (searched on XDG_DATA_DIRS), read and split into arguments the way dbus-daemon does (kwin_mcp/dbus_service_file.py), and run as bash arrays. Started this way they run in kwin-mcp's own SELinux domain rather than gnome_atspi_t, which is why it is opt-in; the desktop session's own launcher, started by systemd --user, runs as unconfined_t too. session_start reports the direct start in its Warning: line. If the direct start cannot complete, the previous warning is kept and the half-started launcher is stopped.

Without the opt-in, a failed activation starts nothing, as before, and the warning now names the variable. Distros where activation works are unaffected.

Motivation

Fedora 44 KDE is a common kwin-mcp host, and its virtual sessions had no working accessibility_tree, find_ui_elements or wait_for_element.

How to Test

  • scripts/run-e2e-docker.sh on 1d3228e: quality step passed; 673 passed, 1 skipped.
  • tests/e2e/test_dbus_service_file.py: the parsed argv for quoting, escapes, comments and malformed files, each expectation observed from dbus-daemon 1.16.2 activation on a private bus.
  • tests/e2e/test_session_lifecycle.py:
    • by default an activation denied with Fedora's exact error starts nothing, and the warning names the variable;
    • with the opt-in, the same denial starts the bus directly and KCalc is visible to AT-SPI2;
    • a launcher at a quoted path containing a space, with --label="two words", receives exactly that argv;
    • a registry that exits at once leaves neither the bus name nor the launcher's daemon behind.
  • Manually in a Fedora 44 KDE VM (selinux-policy-targeted 44.11, enforcing):
    • by default session_start reported the activation failure naming the variable, and wait_for_element failed with AT-SPI2 worker exited (status -6), as on main;
    • with the opt-in it reported the direct start, and KCalc exposed 237 elements with screen coordinates.

Checklist

  • uv run ruff check . passes
  • uv run ruff format --check . passes
  • uv run ty check passes (ty check src tests with the locked ty)
  • CHANGELOG.md updated (if user-facing change)
  • README.md updated (if new tools or changed behavior)

@superbiche · user · drafted with Claude Opus 5.5, reviewed before posting; the voice and decisions are mine.

…nied

On Fedora with SELinux enforcing, a virtual session had no accessibility:
the policy denies the D-Bus activation of at-spi-bus-launcher from
kwin-mcp's private dbus-run-session bus (fedora-selinux/selinux-policy#3385),
and the registry's activation from the accessibility bus is denied the same
way, so AT-SPI2 queries failed (AT-SPI2 worker exited).

When the activation fails, the wrapper now starts the launcher and then the
registry directly, from the Exec= lines of the same service files, and waits
up to 5 s of elapsed time for each to own its bus name. They run unconfined,
as the desktop session's own launcher does. session_start reports the
fallback in its Warning line. If the direct start cannot complete, the
warning is the previous one and the half-started launcher is stopped. The
direct start adds at most 17 s to a startup whose activation already failed.
@haechibot

haechibot Bot commented Oct 9, 2026

Copy link
Copy Markdown

Issue agent stopped automated processing of this pull request and needs an operator.

Reason:

Session status (review): resume failed: HTTP 500 resume_failed

To retry after fixing the cause, an operator calls the bridge op retry_event with {"delivery_id": "da2de210-c3d0-11f1-82ae-b5517faa1001"}; the workflow resumes from its recorded stages and agent:needs-attention is removed when the run finishes.


Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@haechibot haechibot Bot added the agent:needs-attention The issue agent stopped on an error; see the latest agent comment. label Oct 9, 2026
@superbiche

Copy link
Copy Markdown
Contributor Author

@haechibot review

haechibot[bot]
haechibot Bot previously approved these changes Oct 10, 2026

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: GREEN (PR-ready: true) reviewed at 7080337. No blocking findings.

The fallback recovers from simulated activation denial, makes KCalc visible through AT-SPI, and removes the half-started bus when registry startup fails. Independent runtime, test, and challenger reviews found no actionable regression.

Verified

  • Ran the current source in an existing Debian KWin test image with Python 3.13.5: all four focused accessibility-bus lifecycle tests passed, covering healthy activation, fallback recovery, failed address retrieval, and registry-failure cleanup.
  • The activation-denied regression test fails with merge-base af20dd9's session implementation: it reports accessibility unavailable instead of recovering.
  • All reported CI checks passed; publishing was skipped. GitHub reports the unchanged head as mergeable.

Not verified

  • Fedora with SELinux enforcing was not exercised locally; the local denial was simulated.
  • The fresh installed-package Docker build was interrupted. Local execution used the current source mounted into an existing test image.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@bulgasaribot bulgasaribot Bot removed the agent:needs-attention The issue agent stopped on an error; see the latest agent comment. label Oct 10, 2026
@haechibot

haechibot Bot commented Oct 10, 2026

Copy link
Copy Markdown

Issue agent stopped automated processing of this pull request and needs an operator.

Reason:

Session status (review): resume failed: HTTP 500 resume_failed

To retry after fixing the cause, an operator calls the bridge op retry_event with {"delivery_id": "a03a17a0-c49c-11f1-8b19-cf0b9342bb6d"}; the workflow resumes from its recorded stages and agent:needs-attention is removed when the run finishes.


Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@haechibot haechibot Bot added the agent:needs-attention The issue agent stopped on an error; see the latest agent comment. label Oct 10, 2026
# Conflicts:
#	README.md
#	src/kwin_mcp/session.py
The startup bound comment claimed the wrapper always reports before the
60 s parent deadline, but its steps (including the 30 s KWin bus-name wait
and the switch Set) can add up past it. The cleanup and activation comments
now cover the direct start of the launcher and registry.
haechibot[bot]
haechibot Bot previously approved these changes Oct 10, 2026

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: GREEN (PR-ready: false: E2E checks pending) re-reviewed at caee67a. No blocking findings.

Previous findings

None. The previous review approved 7080337 without findings; there are no review threads to answer.

New findings

None. The fallback correctly integrates with the accessibility switch: successful recovery enables IsEnabled on the private bus before apps launch; failed recovery skips that write. Independent review found no actionable regression.

Verified

  • Re-ran all four focused accessibility-bus lifecycle tests against this head's source in an existing Debian KWin test image: 4 passed. This includes the new IsEnabled assertion, KCalc accessibility after activation denial, healthy activation, and cleanup after fallback failure.
  • Lint, type-check, build, and documentation checks passed. GitHub reports this head as mergeable. Seven E2E jobs remain in progress.

Not verified

Fedora with SELinux enforcing and a fresh installed-package build were not exercised locally; local tests simulated activation denial and mounted the current source into an existing runtime image.


Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@bulgasaribot bulgasaribot Bot removed the agent:needs-attention The issue agent stopped on an error; see the latest agent comment. label Oct 10, 2026
haechibot[bot]
haechibot Bot previously approved these changes Oct 10, 2026

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: GREEN (PR-ready: false: E2E CI failing/pending) re-reviewed at 8151930. No blocking findings in the diff.

Previous findings

None. Both prior reviews approved without findings, and there are no review threads to answer.

New findings

None. This head merges upstream main. The fallback implementation and lifecycle tests are unchanged from caee67a; independent review found no conflict with the upstream accessibility window-matching changes.

Verified

  • Re-ran the four focused accessibility-bus lifecycle tests against this head's source in an existing Debian KWin runtime: 4 passed, including accessibility-switch enablement, KCalc visibility after activation denial, and failure cleanup.
  • Lint, type-check, build, and documentation checks passed. GitHub reports the head as mergeable.
  • One openSUSE amd64 E2E job failed at Build the KWin test image; its quality checks and tests were skipped. Other E2E jobs remain in progress.

Not verified

The image-build failure's cause is unknown: GitHub CLI could not retrieve its logs while the workflow remained in progress. Fedora with SELinux enforcing and a fresh installed-package build were not exercised locally; local tests used simulated denial and mounted source.


Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@isac322

isac322 commented Oct 11, 2026

Copy link
Copy Markdown
Owner

Thanks for this, @superbiche. The Fedora diagnosis is clear, and I can see how much it hurts to have no accessibility in virtual sessions there. I'm holding off on merging for now, for two reasons.

1. It goes around an SELinux denial. When activation is refused, the fallback runs at-spi-bus-launcher and the registry itself. They then inherit kwin-mcp's context instead of entering gnome_atspi_t, which is unconfined on a stock Fedora session. I didn't find a privilege escalation or any way to reach the host bus: both processes stay on the private bus and in the session's process group. Still, kwin-mcp would be choosing to skip a confinement that the policy enforces, and I don't want the default code path to do that. The PR says the desktop's own launcher also runs unconfined, but I couldn't verify that. The options I'd be comfortable with are an explicit opt-in (a flag or an env var) or waiting for selinux-policy#3385 upstream. I'm open to other ideas.

2. Exec= handling differs from D-Bus. $launcher >/dev/null & relies on bash word splitting, so it ignores the quoting that D-Bus applies when it parses Exec=. In a scratch harness, an Exec= path containing a space failed with exit 127, and --label="two words" reached the helper as two arguments. This can't be used for injection, but on such systems the fallback silently doesn't work. If we keep the direct start, it should split Exec= the way D-Bus does.

Your latest commits (5e4bb09..caee67a) only touch tests and comments, so both points still apply to the current head. Happy to discuss either one.

superbiche and others added 3 commits October 11, 2026 19:57
…D-Bus does

The direct start of the AT-SPI launcher and registry runs them in
kwin-mcp's own SELinux domain instead of the gnome_atspi_t domain the
activation would give them. It now runs only with
KWIN_MCP_ATSPI_DIRECT_START=1; by default a failed activation is reported
as before, and the warning names the opt-in.

The launcher and registry commands were word-split by bash, so a quoted
path with a space or a quoted argument broke. kwin_mcp.dbus_service_file
ports dbus-daemon's service file reader (desktop-file unescaping, then
_dbus_shell_parse_argv), and the wrapper runs the resulting argv as bash
arrays. Its tests carry argv observed from dbus-daemon 1.16 activation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The direct start read any service file at the expected path. dbus-daemon
ignores a file over 128 KiB or not valid UTF-8, and activates a service
by its Name= key, so a file without the requested Name never starts it.
The direct start now applies the same rules.

The opt-in is read once per wrapper build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
haechibot[bot]
haechibot Bot previously approved these changes Oct 11, 2026

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: GREEN (PR-ready: false: E2E CI pending) re-reviewed at 1d3228e. No blocking findings.

Previous findings

Prior bot reviews contained no findings, and there are no review threads to answer. The maintainer's subsequent comment raised two concerns:

  • Closed: automatic direct startup after an SELinux denial. The fallback now requires KWIN_MCP_ATSPI_DIRECT_START=1 in the server environment; session extra_env cannot enable it. Documentation explains the domain change.
  • Closed: incorrect Exec= splitting. Service files are now parsed into argument arrays, preserving quoted paths and arguments without shell expansion. Invalid service files are rejected.

New findings

None. Independent parser and lifecycle reviews found no actionable defect.

Verified

  • Ran the parser tests and focused lifecycle tests against this head's source in an existing Debian KWin runtime: 44 passed. Coverage includes quoted commands, malformed service files, default-off behavior, opt-in recovery with KCalc visibility, accessibility-switch enablement, and failure cleanup.
  • Lint, type-check, build, and documentation CI checks passed. GitHub reports this head as mergeable; seven E2E jobs remain in progress.

Not verified

Fedora with SELinux enforcing and a fresh installed-package build were not exercised locally. Local execution used mounted source and simulated activation denial; the parser fixtures' claimed upstream observations were not independently reproduced against dbus-daemon.


Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@superbiche

Copy link
Copy Markdown
Contributor Author

Thanks for the careful look. Both points are fair, and both are addressed in f589ab1 and 1d3228e, with main (0.11.0) merged in b13e2ee.

1. Opt-in. The direct start now runs only with KWIN_MCP_ATSPI_DIRECT_START=1 in the server's environment; a session's env cannot turn it on. By default, a failed activation starts nothing, as on main, and the Warning: line now ends with ; set KWIN_MCP_ATSPI_DIRECT_START=1 to start it directly from its service files. The README and CHANGELOG say what the opt-in trades: the launcher and registry run in kwin-mcp's own domain, not gnome_atspi_t.

On the desktop's own launcher: on a stock Fedora 44 KDE install (VM, selinux-policy-targeted 44.11, enforcing), systemd --user starts it through at-spi-dbus-bus.service, not through D-Bus activation, and it runs unconfined:

$ ps -eo label,pid,ppid,args | grep -E 'at-spi|atspi'
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1356 927 /usr/libexec/at-spi-bus-launcher
unconfined_u:unconfined_r:unconfined_dbusd_t:s0-s0:c0.c1023 1372 1356 /usr/bin/dbus-broker-launch --config-file=/usr/share/defaults/at-spi2/accessibility.conf --scope user
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 1394 927 /usr/libexec/at-spi2-registryd --use-gnome-session
$ ps -o label,pid,args -p 927
LABEL                                                   PID COMMAND
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 927 /usr/lib/systemd/systemd --user

My Fedora 44 Plasma workstation shows the same labels. With the opt-in, the directly started launcher and registry get exactly these labels, and they are gone after session_stop.

2. Exec= splitting. kwin_mcp/dbus_service_file.py ports what dbus-daemon does with a service file: the desktop-file reader (\s, \t, \n, \r, \\ escapes; first Exec of the first [D-BUS Service] group; files over 128 KiB, not UTF-8, or without the requested Name= are ignored) and _dbus_shell_parse_argv (quotes, backslashes, # comments, no expansion). The wrapper gets the result as bash arrays (shlex.join, run as "${ATSPI_LAUNCHER[@]}"), so there is no word splitting, globbing or expansion. Every expectation in tests/e2e/test_dbus_service_file.py was observed from dbus-daemon 1.16.2 activating the same file on a private bus, including your two cases: a quoted path with a space runs, and --label="two words" arrives as one argument. A lifecycle test starts the launcher through such a path end to end. A service file dbus-daemon would ignore disables the direct start instead of running something else.

Testing on 1d3228e:

  • scripts/run-e2e-docker.sh: quality step passed; 673 passed, 1 skipped.
  • Fedora 44 KDE VM, enforcing: by default, the warning names the variable and wait_for_element fails as on main; with the opt-in, KCalc exposes 237 elements with screen coordinates.

@superbiche · user · drafted with Claude Opus 5.5, reviewed before posting; the voice and decisions are mine.

@superbiche superbiche changed the title fix(session): start the AT-SPI bus directly when its activation is denied fix(session): opt-in direct AT-SPI start when its activation is denied Oct 11, 2026
@haechibot

haechibot Bot commented Oct 14, 2026

Copy link
Copy Markdown

@isac322 the review of 8279d657d66aec870c5f3ad1021e83502b823ffd is COMMENT, not an approval: these items need a maintainer decision or answer. Merge stays blocked (issue-agent/review is not successful) until they are resolved.

  • Maintainer hold by isac322 remains in place; confirm acceptance of the opt-in fallback and release the hold.

Reply on the pull request, then comment @haechibot review to request another review.

@isac322

isac322 commented Oct 14, 2026

Copy link
Copy Markdown
Owner

The original default-on fallback and Exec parsing concerns are technically addressed. I am retaining the documented operator-only opt-in and keeping this pull request unmerged. This pass covers fixes and branch updates only; merge authorization remains a separate maintainer decision. Please evaluate technical code readiness separately from permission to merge. No hold release, merge, or check bypass is requested.

@haechibot review

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: OPEN re-reviewed at 12db7f0. One new P2 ownership defect needs correction; the maintainer's merge hold remains a separate open item.

Previous findings

  • Closed: P2 unbounded failed-bootstrap cleanup. src/kwin_mcp/session.py:1197-1214 now shares a 3-second TERM grace and a 2-second KILL grace across owned jobs and performs no unbounded wait.
  • Closed: P2 service lookup outside the startup watchdog. session.py:1124-1129 constructs only the helper pathname; filesystem acquisition runs in children at 1234,1240, after failed activation at 1254-1260.
  • Closed technically: default-on startup after an SELinux denial. The server must explicitly set the exact opt-in value 1 at session.py:1124; application environment overrides cannot enable it.
  • Closed technically: incorrect Exec= splitting. src/kwin_mcp/dbus_service_file.py:63-232 follows D-Bus desktop-value and argv parsing, and 282 executes the resulting argv directly.
  • Closed: acceptance of the documented operator-only opt-in. isac322, whose repository admin permission was verified, explicitly retained it in the latest maintainer response.
  • Open: the same response expressly retains the merge hold and reserves merge authorization for a separate decision.

The change supplies an operator-selected workaround for the distribution policy denial; it does not change SELinux policy. The two previous fixes are supported by independently challenged source-path arguments. A final Bash-option probe exposed a new ownership hazard.

Fix hazards

  • This head applies the fix suggested in the cleanup finding. The grace periods are bounded, but the new ownership argument assumes Bash job control is off (session.py:1178-1179) without enforcing it. Inherited monitor mode changes helper process groups and native jobspec signaling; this produced the new finding below. Explicitly disabling monitor mode before any background launch adds no blocking I/O or lock.
  • This head applies the fix suggested in the lookup finding. All new stat/open/read operations are child-side (dbus_service_file.py:235-262), outside the registry lock and parent wrapper construction. Spawn registration remains serialized with close (session.py:294,317-329), but the newly added helpers inherit that registered group only with job control disabled. Individual D-Bus reply limits do not cover connection/authentication; the outer handshake deadline at 1047-1048 remains the final caller bound.

New findings

  • P2 src/kwin_mcp/session.py:1234: inherited SHELLOPTS=monitor enables job control in bash -c. The new launcher/registry helpers then enter separate process groups, while the registry tracks only the wrapper group (725-731) and the wrapper trap tracks only KWin (1146-1150). If startup is cancelled before direct-start rollback, a TERM-ignoring helper can survive group teardown. _build_env preserves this option (1364), and 1178 only comments that job control should be off. The native cleanup's dead-member safety proof also depends on its non-job-control branch. Explicitly run set +m at wrapper startup, before any background jobs, and cover cancellation with inherited monitor mode. Evidence: the targeted Bash probe below, plus the independently checked ownership path. This is a hazard of the fixes linked above.

Open items

  • Maintainer hold by isac322 remains in place; merge authorization is reserved for a separate maintainer decision.

Verified

  • Reviewed the full merge-base diff and complete changed files against main at dfcc4ea. The pinned head includes that base, and the worktree is clean.
  • Ran targeted probes in an official bash:5.2 scratch container with docker run --rm --network none. Under default options, the literal role-job marker was preserved, a TERM-ignoring job disappeared after the observed 3-second grace, and a deliberately stale numeric target left a neighbor alive after bounded polling. With literal SHELLOPTS=monitor, Bash reported monitor on and the background child had a different process group from the shell. The control with set +m reported monitor off and both shared the shell's group. These are Bash probes, not complete KWin session tests.
  • Compared parser branches with upstream D-Bus 1.16.2 source and cleanup ownership with Bash 5.2 primary source. Read the linked SELinux issue and its scope correction.
  • Applied base conventions from CLAUDE.md, CONTRIBUTING.md, .claude/agents/docs-seo.md, .claude/skills/check-docs-seo/SKILL.md, .claude/skills/release-notes/SKILL.md, .claude/positioning.yml, the documentation hook/settings, PR template, all three workflow definitions, docs/design/*.md, and ROADMAP.md. Module/test inventories, release notes and the Conventional Commit title are synchronized. The change adds no tool or shared numeric registry entry.

Not verified

Fedora with SELinux enforcing, GUI accessibility, live NFS/FUSE stalls, kernel D-state disappearance, and long-running suites were not exercised locally. Their runtime claims were assessed through the specific source paths and independent challenges below. No end-to-end suite or PR container image was built or run, and CI results did not determine this verdict.

Lenses

  • correctness, always: normal activation remains at session.py:1254; the exact opt-in is 1124; successful recovery enables accessibility at 1272-1274; failed recovery preserves the warning at 1264-1266. Launcher preflight and registry reacquisition are dbus_service_file.py:271-279. These paths address both previous defects by argument+challenger(/root/wrapper_12db); cancellation ownership under inherited monitor mode is the new finding. The new stdlib script supports the declared Python 3.12 minimum.
  • concurrency/lifecycle, triggered by child creation and rollback at session.py:1234,1240,1245: spawn/close serialization is 294,317-329; TERM/KILL grace gaps are 1200-1210; final group cleanup is 767-789. No same-marker job replaces an exited role during rollback, and pending traps cannot run inside native kill's resolution gap. Default-mode reap/PID handling was covered by argument+challenger(/root/bootstrap_12db). The inherited-monitor cancellation near miss was confirmed by the Bash probe and the independent challenge; it is reported as the new finding.
  • concurrency/lifecycle, triggered by file acquisition and exec in dbus_service_file.py:44,282: the acquired descriptor is validated at 48-50 and closed in 58-60 on every path; the helper adds no lock, thread, named artifact, or closeable shared registry. Path replacement is checked against the opened descriptor. Exec preserves the child's PID/group; whether that group is registered is addressed by the new finding. Covered by argument+challenger(/root/wrapper_12db).
  • input, triggered by the new server flag and helper roles: session.py:1124 leaves unset, empty, zero, boolean-looking and spaced values off. Public MCP/CLI application env reaches app launch at core.py:618, after startup; internal session overrides at session.py:1413 do not change the server gate. Helper argv accepts exactly one fixed role at dbus_service_file.py:268-269. Lookup defaults/skips and fail-closed permission/I/O behavior are 237-262; size, UTF-8, Name and Exec checks are 221-232. Covered by the independent input audit and argument+challenger(/root/wrapper_12db), including denied-first-root and invalidated-registry near misses.
  • injection, triggered by generated Bash at session.py:1173 and service Exec=: the helper argv is encoded once with shlex.join, and quoted array expansions at 1234,1240 preserve literal arguments. -I -S at 1127 suppresses Python startup customization; dbus_service_file.py:282-286 uses direct execv without PATH lookup or implicit shell fallback. Child stdout is discarded before the handshake reader (session.py:1234,1240,1084-1095). Covered by argument+challenger(/root/wrapper_12db).
  • math, triggered by byte and time bounds: dbus_service_file.py:26,51,54-56 permits 131072 bytes and reads at most one extra sentinel byte to reject concurrent growth. session.py:1201,1207,1219 uses seconds; reply limits at 1221,1237 use milliseconds. Whole-second rounding can shorten a grace by less than one second; cumulative waits and setup stalls remain subject to 1048. No coordinate conversion or caller-provided numeric bound is introduced. Covered by the Bash probe and argument+challenger(/root/wrapper_12db).
  • test audit, triggered by all five changed test files: lifecycle tests check no parent lookup (test_session_lifecycle.py:665-703), lookup stalls (708-768), registry invalidation (771-809), actual bus ownership/accessibility (942-1007), literal argv (1210-1274), and TERM-resistant rollback before fixture recovery (1289-1439). Exit assertions check original PID/start-time identities, pipe EOF, group disappearance and neighbor survival before finally cleanup (test_exit_cleanup.py:665-746). Parser, acquisition and execution-security tests check byte boundaries, same-fd replacement, literal arguments and inert hostile markers (test_dbus_service_file.py:54-135, test_atspi_service_acquisition.py:67-729, test_atspi_exec_security.py:159-523). Source analysis shows these assertions distinguish the corresponding missing fixes; predicates provide synchronization and no new type/lint suppressions were added.
  • docs/convention, always: README.md:439-445, CHANGELOG.md:12-17, CONTRIBUTING.md:167-184,334-343, and docker/README.md:194-208 describe server-only opt-in, lazy lookup and confinement limits. Their registered-group ownership claim requires the monitor-mode fix above. The helper and three new tests appear in the inventories (CONTRIBUTING.md:236,308-310; docker/README.md:141-142,148). Applicable base sync triggers were checked; tool count remains 35.
Considered
  • [concurrency/lifecycle] session.py:1212 a helper already sent SIGKILL during rollback resumes and executes; dismissed: argument+challenger(/root/bootstrap_12db), a pending fatal signal prevents return to user code. Cancellation that misses the helper's group is separately reported above.
  • [input] dbus_service_file.py:46 regular-to-FIFO replacement blocks acquisition; dismissed: argument+challenger(/root/wrapper_12db), nonblocking open and the same-fd regular check reject the replacement before read.
  • [math] dbus_service_file.py:54 growth after fstat permits unbounded allocation; dismissed: argument+challenger(/root/wrapper_12db), read(limit+1) caps bytes and the length check rejects growth.
  • [injection] dbus_service_file.py:282 parsed executable names or metacharacters trigger implicit shell evaluation; dismissed: argument+challenger(/root/wrapper_12db), literal execv bypasses shell dispatch and ENOEXEC has no fallback.
  • [concurrency/lifecycle] dbus_service_file.py:271 lazy acquisition still blocks the server before its watchdog; dismissed: argument+challenger(/root/wrapper_12db), acquisition is reachable only through the post-activation-failure child, while parent construction is lexical.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

Comment thread src/kwin_mcp/session.py Resolved
Comment thread src/kwin_mcp/session.py Resolved
Comment thread src/kwin_mcp/session.py Resolved
@haechibot

haechibot Bot commented Oct 14, 2026

Copy link
Copy Markdown

@isac322 the review of 12db7f0065a14d118d86458416ff8c230ce8ca17 is COMMENT, not an approval: these items need a maintainer decision or answer. Merge stays blocked (issue-agent/review is not successful) until they are resolved.

  • Maintainer hold by isac322 remains in place; merge authorization is reserved for a separate maintainer decision.

Reply on the pull request, then comment @haechibot review to request another review.

@isac322

isac322 commented Oct 14, 2026

Copy link
Copy Markdown
Owner

The original default-on fallback and Exec parsing concerns are technically addressed. I am retaining the documented operator-only opt-in and keeping this pull request unmerged. This pass covers fixes and branch updates only; merge authorization remains a separate maintainer decision. Please evaluate technical code readiness separately from permission to merge. No hold release, merge, or check bypass is requested.

@haechibot review

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: OPEN re-reviewed at 27a43bd. The monitor ownership finding is fixed. One new parser correctness finding remains, alongside the retained maintainer hold.

Previous findings

  • Closed: unbounded failed-bootstrap cleanup. src/kwin_mcp/session.py:1199 shares bounded TERM/KILL grace periods and performs no unconditional wait.
  • Closed: eager service-file lookup outside the startup watchdog. src/kwin_mcp/session.py:1124 constructs only a lexical helper command; filesystem acquisition runs in owned children at 1236,1242 after failed activation.
  • Closed: inherited monitor mode separates helpers from the registered process group. src/kwin_mcp/session.py:1138 disables job control before activation and every background launch. New cancellation assertions check actual child PGIDs, identities, pipe EOF and unrelated neighbors before recovery cleanup.
  • Closed technically: default-on confinement workaround. The fallback requires exactly KWIN_MCP_ATSPI_DIRECT_START=1 in the server environment at src/kwin_mcp/session.py:1124.
  • Closed technically: the original quoted-path and argument splitting defect. Arguments are parsed and executed directly through src/kwin_mcp/dbus_service_file.py:210,282. The whole-file validation mismatch below is a separate finding.
  • Open: the maintainer hold. isac322's repository admin permission was verified; the comment explicitly retains the hold.

The change provides an operator-selected workaround for denied activation, while normal activation retains its existing path. It does not fix the upstream SELinux policy issue. Technical code readiness still requires the new parser correction below; merge authorization remains a separate maintainer decision.

New findings

Blocking

  • P2 src/kwin_mcp/dbus_service_file.py:224: reject embedded NUL throughout the service file. Python's UTF-8 decode accepts NUL, and comments/localized entries skip value validation at 104,113. Consequently the direct helper executes a file that D-Bus rejects entirely. A focused comparison with D-Bus 1.16.2 reproduced ServiceUnknown and no execution for both ignored-line cases, while this head's actual helper executed their commands. Reject NUL before parsing and add both malformed-file cases. This check belongs on the already bounded byte buffer and requires no extra filesystem operations or locks.

Fix hazards

This head applies the fix suggested in the monitor ownership finding. Disabling monitor mode before spawning preserves the registered group across the launch-to-$! assignment window. It adds no lock or I/O. The earlier cleanup and lookup fixes retain bounded rollback and child-side acquisition. Parent containment uses the complete handshake deadline, including setup stalls; individual D-Bus reply timeouts alone do not provide that bound.

Open items

  • Maintainer hold by isac322 remains in place; merge authorization is reserved for a separate maintainer decision.

The documented operator-only opt-in is retained as requested. I recommend correcting whole-file validation while keeping the existing hold; this review requests no merge or hold release.

Verified

  • Read the full merge-base diff and all changed files at this head against base dfcc4ea50a327a056e7cfc896d7314f5114d70d9, including all five changed test modules. Read PR history, review threads and the linked upstream issue.
  • A targeted Bash 5.2 probe ran with docker run --rm --network none -e SHELLOPTS=monitor <probe-image>. The control created separate groups; the current prologue kept the helper in the wrapper group. Rollback removed a TERM-resistant owned role in 3 seconds, preserved its neighbor and did not signal a deliberately stale numeric target with no matching job.
  • A targeted Python 3.12 / D-Bus 1.16.2 comparison ran with docker run --rm --network none <probe-image>. A valid service file executed through both paths. With NUL in a comment or localized entry, D-Bus rejected the file and the current direct helper executed it.
  • Repository conventions applied from the base branch: CLAUDE.md, CONTRIBUTING.md, the PR template, .claude documentation guidance/settings/hook, workflow commands, docs/design and ROADMAP.md. README and Docker warnings match the implementation; both test inventories include the new modules. No tool/version counter or generated-plugin synchronization is triggered. The PR title follows the Conventional Commit convention.

Not verified

  • Full KWin/GUI suites, installed-wheel matrices and Fedora SELinux behavior were not run locally. Their behavior was assessed through current source paths and independently challenged arguments. The verdict does not depend on CI results.
  • Actual hung network filesystems and uninterruptible processes were not exercised. Group ownership and deadline coverage were checked by source arguments; Bash rollback was exercised separately. Arbitrary operator startup scripts or service executables can deliberately change process ownership and remain trusted configuration.

Lenses

  • correctness: denied activation and healthy/default-off paths at session.py:1256; launcher preflight and registry revalidation at dbus_service_file.py:271; original quoted argv handling at 124,175,282. Covered by source arguments, independent challenges and the real D-Bus comparison. Whole-file NUL rejection is the new finding.

  • concurrency/lifecycle, session module: session.py:317 guards spawn/registration against close; 405,543 preserve leaders through escalation; 725,1138,1236 contain launch/registration windows; 1047 bounds complete setup; 1183,1199 use native job signals and shared grace periods. Covered by the Bash probe and argument+challenger(/root/docs_8279).

  • concurrency/lifecycle, helper module: dbus_service_file.py:44,48,58 assign, validate and finally close the acquired descriptor; 282 preserves PID/group through exec. There is no shared registry or lock interval. Covered by argument+challenger(/root/wrapper_12db).

  • input/injection: server-only exact opt-in at session.py:1124; isolated interpreter and quoted helper array at 1127,1175; fixed role argv at dbus_service_file.py:268; root/error ordering at 237,243,254; bounded UTF-8/name/Exec validation at 221; direct exec at 282. Covered by argument+challenger(/root/wrapper_12db) and the parser comparison, with the NUL exception reported.

  • math/bounds: dbus_service_file.py:26,51,54,55,221 cover exact byte limits and growth after metadata inspection; session.py:1199,1221 distinguish shared rollback graces from readiness polling and the outer deadline. Covered by the Bash probe and argument+challenger(/root/wrapper_12db).

  • test audit: test_exit_cleanup.py:607,705,739,748 establish the actual installed-server option, child identity and PGID, then verify exit/EOF/group removal and neighbor survival; test_session_lifecycle.py:1006,1032,1217,1458 cover success, healthy/default-off controls and rollback. Parser/acquisition/security tests check exact argv, descriptor races, markers and ownership before fixture recovery. Source audit, with the missing whole-file NUL cases identified.

  • docs/convention: always triggered; README.md:439, CHANGELOG.md:12, CONTRIBUTING.md:334 and docker/README.md:193 agree on server opt-in, ownership, service roots and rollback limits; both test inventories list the new modules. Current title and multi-commit squash convention agree. Covered by the current full documentation/base-convention audit.

Considered
  • [lifecycle] session.py:1210 a helper already sent SIGKILL resumes from stalled I/O and executes later; dismissed: argument+challenger(/root/bootstrap_12db), a delivered fatal signal prevents return to user code. Kernel task disappearance is not claimed; group containment remains registered.
  • [filesystem] dbus_service_file.py:44 replacement by a FIFO blocks acquisition; dismissed: argument+challenger(/root/wrapper_12db), nonblocking open and same-descriptor fstat reject the FIFO before read.
  • [bounds] dbus_service_file.py:54 growth after stat causes unbounded reads; dismissed: argument+challenger(/root/wrapper_12db), the fixed limit-plus-one read and post-read length check reject growth.
  • [injection] dbus_service_file.py:282 metacharacters, builtins, PATH or ENOEXEC invoke a shell; dismissed: argument+challenger(/root/wrapper_12db), execv uses literal argv/path and returns failure on OSError.
  • [parsing] dbus_service_file.py:148 escaped quotes, comment newlines or empty tokens alter argv; dismissed: argument+challenger(/root/wrapper_12db), odd-backslash tracking, two-stage unquoting and comment/final-token rules match the compared D-Bus branches.
  • [lifecycle] dbus_service_file.py:271 lookup stalls the parent before ownership/deadline establishment; dismissed: argument+challenger(/root/wrapper_12db), lookup is reached only in a helper child after failed activation.
  • [lifecycle] session.py:1236 a signal before role PID assignment leaks the helper; dismissed: argument+challenger(/root/docs_8279), the pre-registered group already contains the role with monitor disabled.
  • [lifecycle] session.py:1193 a stale numeric PID signals an unrelated process; dismissed: probe, numeric polling with an absent role marker left a live unrelated target untouched and completed within the shared grace bounds.
  • [lifecycle] session.py:1194 a role exits during native jobspec resolution and its recycled PID is signaled; dismissed: argument+challenger(/root/docs_8279), Bash's non-monitor branch skips completed job members; native reaping retains this ordinary async job in the resolution-to-signal gap.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

Comment thread src/kwin_mcp/dbus_service_file.py Resolved
Comment thread src/kwin_mcp/session.py Resolved
@isac322

isac322 commented Oct 14, 2026

Copy link
Copy Markdown
Owner

@haechibot review

Please re-review current head ba77b8ca8a434eb34dcb62fbbf0273b9d317b6c8. The whole-file NUL finding is corrected and the original thread has a verification reply. The new installed wheel passed the complete Debian and Fedora suites (772 passed, 1 existing GPU-backed screenshot skip each), actual CLI scenarios, and native D-Bus/launcher/registry NUL rejection with healthy controls.

The maintainer merge hold remains unchanged. This requests technical re-review only, not merge or hold-release authorization.

@haechibot

haechibot Bot commented Oct 14, 2026

Copy link
Copy Markdown

Issue agent stopped automated processing of this pull request and needs an operator.

Reason:

Turn state (review): model provider error ended the agent turn: Codex error: stream disconnected before completion: stream closed before response.completed

To retry after fixing the cause, an operator calls the bridge op retry_event with {"delivery_id": "948575b0-c7d2-11f1-8b61-299cbbf5e465"}; the workflow resumes from its recorded stages and agent:needs-attention is removed when the run finishes. To give up on it instead, the op dismiss_event with the same delivery_id and a reason closes it and removes the label.


Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@haechibot haechibot Bot added the agent:needs-attention The issue agent stopped on an error; see the latest agent comment. label Oct 14, 2026
@isac322

isac322 commented Oct 14, 2026

Copy link
Copy Markdown
Owner

@haechibot review

The previous review stopped with a model-provider stream error and did not submit a review. Please retry technical review of unchanged head ba77b8ca8a434eb34dcb62fbbf0273b9d317b6c8. The full local installed suites, CLI, and NUL execution checks are complete.

The maintainer merge hold remains unchanged. This is not authorization to merge or release the hold.

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: OPEN re-reviewed at ba77b8c. No remaining technical findings; the maintainer's merge hold remains in place.

Previous findings

  • Closed: default-on direct activation workaround. src/kwin_mcp/session.py:1124 requires the exact server-side opt-in; session arguments cannot enable it.
  • Closed: quoted service commands losing argument boundaries. src/kwin_mcp/dbus_service_file.py:210 parses argv and :282 executes it literally. The fresh D-Bus comparison preserved a quoted executable path and --label="two words".
  • Closed: unbounded failed-bootstrap cleanup (thread). src/kwin_mcp/session.py:1199 shares bounded TERM/KILL grace periods without an unconditional wait.
  • Closed: eager service lookup outside the startup watchdog (thread). Lookup now runs in owned helper children at src/kwin_mcp/dbus_service_file.py:271, :272 and :277.
  • Closed: inherited monitor mode separating helpers from session ownership (thread). src/kwin_mcp/session.py:1138 disables it before spawning helpers.
  • Closed: NUL bytes in ignored service-file lines (thread). src/kwin_mcp/dbus_service_file.py:221 rejects them before decoding or skipping comments/localized entries. Regression inputs at tests/e2e/test_dbus_service_file.py:106 and :110 place the NUL after valid Name/Exec entries.
  • Open: the maintainer hold. isac322's latest request explicitly retains it.

The code addresses the earlier technical concerns. This remains an operator-controlled workaround for denied activation, rather than a fix to Fedora's SELinux policy. Technical review does not release the retained hold.

New findings

None.

Fix hazards

This head applies the fix suggested in the NUL finding. The new scan operates on the already bounded byte buffer, adds no I/O or lock, and precedes all parser branches. Removing the narrower value check is safe because both executable roles pass through this guard; UTF-8 decoding and the escape map cannot introduce NUL afterward. The fresh execution comparison below verifies both roles.

Open items

  • Maintainer hold by isac322 remains in place; merge authorization is reserved for a separate maintainer decision.

Verified

  • Re-read the complete merge-base diff and all changed files against main 6d047425ece3ed9b338c6b28257286ea0ec2b6be, including prior findings and discarded hypotheses. The merged uv_build upper-bound change matches main and does not interact with the AT-SPI runtime path.
  • Fresh targeted probe on official Python 3.12 with D-Bus 1.16.2: docker run --rm --network none kwin-pr95-parser-probe:ba77-retry-quoted. Both prior helper roles executed service files containing NUL in comments/localized entries; both current roles rejected them without recorder execution, matching native D-Bus rejection. Valid files and both NUL-free controls preserved exact argv, including the executable path containing spaces. The native control records execution, not successful bus-name acquisition.
  • Fresh Bash 5.2 probe: docker run --rm --network none -e SHELLOPTS=monitor kwin-pr95-bash-probe:ba77-retry. The control separated background process groups; the probe using the wrapper's current set +m kept its helper in the owned group. Escalation removed a TERM-resistant helper in three seconds, preserved a neighbor, and did not signal a stale numeric target lacking the role marker.
  • Applied base conventions from CLAUDE.md, CONTRIBUTING.md, .claude/**, the PR template, CI workflow commands, docs/design/*.md and ROADMAP.md. Checked documentation triggers, new module/test inventories, the unchanged 35-tool count, conventional title/commit subjects and absence of added type/lint suppressions.

Not verified

Local GUI/E2E suites, SELinux enforcement, other distribution matrices and kernel-uninterruptible filesystem I/O were not exercised. Their results do not determine this verdict. Lifecycle and filesystem boundaries below use independently challenged source arguments where the targeted probes do not exercise them. No public tool parameter or geometry behavior changes, so CLI-schema and coordinate probes were not applicable.

Lenses

  • Correctness: denied activation triggers the opt-in path at src/kwin_mcp/session.py:1256; healthy activation skips it. Both service roles validate argv before execution at src/kwin_mcp/dbus_service_file.py:271 and :277. Python 3.12 and native D-Bus behavior are covered by the fresh parser probe.
  • Input/injection: exact server-environment opt-in at src/kwin_mcp/session.py:1124; isolated Python invocation at :1127; quoted Bash arrays and discarded helper stdout at :1236 and :1242; fixed role validation at src/kwin_mcp/dbus_service_file.py:268; literal execv at :282. Quoting/NUL controls use the fresh probe; escape, Unicode, no-shell and no-PATH arguments were checked by argument+challenger(/root/wrapper_12db).
  • Filesystem/lifecycle, helper: same-descriptor regular-file validation at src/kwin_mcp/dbus_service_file.py:48, nonblocking open at :46, bounded growth detection at :54, guaranteed descriptor closure at :60, fail-closed root selection at :237, launcher pair preflight at :271 and registry reread at :277. Blocking metadata/read operations occur only in owned children. Covered by argument+challenger(/root/wrapper_12db).
  • Concurrency/lifecycle, session: spawn/registration share the close lock at src/kwin_mcp/session.py:317; wrapper ownership is established at :725; helper group inheritance at :1138; setup/auth and lookup stalls are bounded externally by the monotonic handshake deadline at :1048 and group teardown at :767. Rollback at :1194 uses owned jobs, shared grace periods and no unconditional wait. Spawn-to-PID-assignment and jobspec signal windows use argument+challenger(/root/docs_8279); inherited monitor mode and TERM resistance use the fresh Bash probe.
  • Time arithmetic: src/kwin_mcp/session.py:1203, :1209 and :1221 use shared seconds-based deadlines. Integer-second precision can shorten the effective grace period by less than a second; an in-progress D-Bus connection/authentication can overrun the nominal five-second role interval. The independent parent's sixty-second handshake deadline at :1048 bounds startup waiting. Covered by argument+challenger(/root/wrapper_12db) and argument+challenger(/root/docs_8279).
  • Test audit: tests/e2e/test_dbus_service_file.py:106 exercises the previously accepted ignored-line cases; acquisition and execution-security tests assert exact argv, reached race paths and descriptor identity. tests/e2e/test_session_lifecycle.py:708, :943, :1061, :1092 and :1392 check lookup stalls, ownership, default-off behavior, environment isolation and rollback before fixture recovery. tests/e2e/test_exit_cleanup.py:606 verifies actual TERM-resistant execution, open stdin, pipe EOF, original process identities and neighbor survival. Fresh complete source/diff audit found no vacuous assertions or new suppressions.
  • Docs/convention: README.md:439, :441, :443 and :445 cover the opt-in, trusted roots, bounded rollback, ownership limits and SELinux domain. CONTRIBUTING.md:236 and :308, plus docker/README.md:141, :142 and :148, include the helper and all new tests; CHANGELOG.md:21 records whole-file NUL rejection. These match the base documentation triggers; no new synchronization omission.
Considered
  • [input] src/kwin_mcp/dbus_service_file.py:221 ignored-line NUL permits execution of a file native D-Bus rejects; dismissed: probe rejected both malformed variants in both current roles while the prior helper executed them; NUL-free twins executed.
  • [injection] src/kwin_mcp/dbus_service_file.py:282 a builtin name, shell metacharacter or executable without a shebang gains shell execution; dismissed: argument+challenger(/root/wrapper_12db) literal execv performs no PATH lookup or ENOEXEC shell fallback; argv is never reparsed by Bash.
  • [input] src/kwin_mcp/dbus_service_file.py:122 and :224 quoting or Unicode decoding diverges from D-Bus; dismissed: probe covers quoted-path/argument controls; argument+challenger(/root/wrapper_12db) checked quote/backslash/comment state transitions and UTF-8 scalar restrictions against D-Bus 1.16.2 source.
  • [filesystem] src/kwin_mcp/dbus_service_file.py:46 a regular file replaced by a FIFO blocks acquisition; dismissed: argument+challenger(/root/wrapper_12db) O_NONBLOCK precedes same-descriptor fstat rejection; slower filesystem I/O is confined to the owned child and parent deadline.
  • [filesystem] src/kwin_mcp/dbus_service_file.py:54 growth after fstat creates an unbounded read; dismissed: argument+challenger(/root/wrapper_12db) limit-plus-one read and post-read size rejection bound bytes, with descriptor closure in finally.
  • [input] src/kwin_mcp/dbus_service_file.py:63 removing the value-level NUL check bypasses validation; dismissed: argument+challenger(/root/wrapper_12db) every executable ingress passes the whole-file guard and subsequent decoding/escapes cannot synthesize NUL.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1127 lookup stalls before the watchdog starts; dismissed: argument+challenger(/root/docs_8279) parent construction is lexical, while acquisition occurs only in background helpers after activation failure.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1236 a signal before PID assignment leaks a helper; dismissed: argument+challenger(/root/docs_8279) the assignment is not atomic, but group ownership predates spawning; parent failure/shutdown escalates against that group even if the wrapper trap waits.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1194 recycled numeric PID receives rollback signals; dismissed: probe preserves the stale-marker neighbor; argument+challenger(/root/docs_8279) jobspec lookup unmasks SIGCHLD before kill_pid remasks. Completion in that gap retains the ordinary async job record, so PALIVE skips its dead member; removal before lookup yields INVALID_JOB. Trapped TERM/INT/HUP remain pending in this builtin. No replacement role job is launched during rollback.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1215 a kernel-stalled helper resumes execution after failed-bootstrap cleanup; dismissed: argument+challenger(/root/docs_8279) pending SIGKILL prevents return to userspace after the stall, while ownership remains with the private group. This does not promise immediate kernel task disappearance.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

Comment thread src/kwin_mcp/dbus_service_file.py Resolved
@bulgasaribot bulgasaribot Bot removed the agent:needs-attention The issue agent stopped on an error; see the latest agent comment. label Oct 14, 2026
@isac322

isac322 commented Oct 14, 2026

Copy link
Copy Markdown
Owner

@haechibot review

Please review the technical correctness of head 7347b07aed2f241fbf541c2601ea33eaa4b41c84, now merged with main 3655841d6af72913f984c94650a24bef3e3ed23e. The PR's own scope remains 12 files; main's spoken_name code, tests and documentation are preserved.

The built wheels installed into Debian arm64 (KWin 6.3.6) and Fedora arm64 (KWin 6.7.5) images passed the full pytest tests suite without a source overlay: 1,113 collected, 1,112 passed, 0 failed, 0 errors and the existing GPU-backed ScreenShot2 capture test skipped on each. On Debian arm64, Ruff, formatting, typing and documentation/plugin checks passed. Three CLI modes passed with a controlled first-activation denial: initial bus ownership was false for default and app-env-only, and true for server opt-in. Five Debian dbus-daemon 1.16.2/installed-helper cases rejected NUL in comments and localized entries, and executed the valid command and both NUL-free twins.

These local results do not substitute for current-head GitHub Actions. Real SELinux-enforcing Fedora, live NFS/FUSE, PID-reuse stress and GPU-backed ScreenShot2 remain unverified. Please judge technical correctness separately from merge authorization: merging, closing, auto-merge and releases remain unauthorized.

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: OPEN re-reviewed at 7347b07. No remaining technical findings; the maintainer's merge hold remains in place.

Previous findings

  • Closed: default-on direct activation workaround. src/kwin_mcp/session.py:1124 requires the exact server-side opt-in; a session or application environment cannot enable it.
  • Closed: quoted service commands losing argument boundaries. src/kwin_mcp/dbus_service_file.py:208-210 parses argv and :282 executes it literally.
  • Closed: unbounded failed-bootstrap cleanup (thread). src/kwin_mcp/session.py:1199-1215 uses shared TERM/KILL grace periods without an unconditional wait.
  • Closed: eager service lookup outside the startup watchdog (thread). Lookup runs in owned helper children at src/kwin_mcp/dbus_service_file.py:271-277, after activation fails.
  • Closed: inherited monitor mode separating helpers from session ownership (thread). src/kwin_mcp/session.py:1138 disables it before any background launch.
  • Closed: NUL bytes in ignored service-file lines (thread). src/kwin_mcp/dbus_service_file.py:221-222 rejects them before decoding or line parsing; regression inputs are at tests/e2e/test_dbus_service_file.py:107 and :111.
  • Open: the maintainer hold. isac322's latest request explicitly retains it.

The merge with main adds the unrelated spoken-name feature and expands the E2E time budget. Those changes do not share the direct-start runtime path. Technical review does not release the retained hold.

New findings

None.

Fix hazards

The direct-start implementation is unchanged from the previous reviewed head. The whole-file NUL guard operates on the already bounded byte buffer, adds no I/O or lock, and precedes every parser branch. Both executable roles pass through it; strict UTF-8 decoding and the escape map cannot introduce a NUL afterward.

Open items

  • Maintainer hold by isac322 remains in place; merge authorization is reserved for a separate maintainer decision.

Verified

  • Re-read the complete diff against main 3655841d6af72913f984c94650a24bef3e3ed23e and all changed files at this head, including the merged-main interaction.
  • In a fresh official Python 3.12 container with D-Bus 1.16.2, the current helper rejected NUL in comments and localized entries for both launcher and registry roles with no execution. Native D-Bus rejected the same files. Valid files and NUL-free controls preserved the quoted executable path and exact --label=two words argv. A comment/newline control also matched native D-Bus.
  • Source and independent challenge review covered helper ownership, set +m, service lookup and D-Bus setup under the parent handshake deadline, jobspec signaling, PID-reuse boundaries, bounded rollback, literal execv, file-size and descriptor limits, and cleanup after close.
  • The linked Fedora SELinux issue remains open and distinguishes the observed name-acquisition denial from unresolved complete private AT-SPI support; this PR documents an opt-in workaround rather than claiming an upstream policy fix.
  • Applied base conventions from CLAUDE.md, CONTRIBUTING.md, docker/README.md, .claude/**, the PR template, workflows, design documents and ROADMAP.md. Documentation inventories, the 37-tool count from main, opt-in warnings, changelog entries and the E2E timeout change are synchronized.

Not verified

Live GUI/E2E suites, SELinux enforcement, other distribution matrices, live NFS/FUSE behavior and kernel-uninterruptible filesystem I/O were not exercised locally. They do not determine this verdict. The lifecycle and filesystem conclusions use source arguments independently challenged against the relevant interleavings and boundaries.

Lenses

  • Correctness: denied activation reaches the opt-in path at src/kwin_mcp/session.py:1256-1269; healthy activation skips it. Both service roles validate argv at src/kwin_mcp/dbus_service_file.py:271-282.
  • Input/injection: exact server-environment opt-in at src/kwin_mcp/session.py:1124; isolated helper invocation at :1127; quoted Bash arrays at :1236 and :1242; fixed role validation at src/kwin_mcp/dbus_service_file.py:268; literal execv at :282. Current-container controls cover quoting, comments, newline handling and NUL rejection; escape, Unicode, no-shell and no-PATH boundaries were checked by argument+challenger(/root/fresh_parser_docs).
  • Filesystem/lifecycle, helper: same-descriptor regular-file validation at src/kwin_mcp/dbus_service_file.py:48-50, nonblocking open at :46, bounded growth detection at :54-56, descriptor closure at :58-60, fail-closed root selection at :237-263, launcher preflight at :271 and registry reread at :277. Blocking metadata and reads occur only in owned children, covered by argument+challenger(/root/fresh_lifecycle).
  • Concurrency/lifecycle, session: spawn and registration share the close lock at src/kwin_mcp/session.py:317-329; wrapper ownership is established at :725-731; helper group inheritance at :1138; setup/auth and lookup stalls are bounded externally by the monotonic handshake deadline at :1048 and group teardown at :767-789. Rollback at :1199-1216 uses owned jobs, shared grace periods and no unconditional wait. Signal windows and PID reuse were checked by argument+challenger(/root/fresh_lifecycle).
  • Time arithmetic: src/kwin_mcp/session.py:1203, :1209 and :1221 use bounded shared deadlines. Integer-second precision can shorten a grace period by less than a second; an in-progress D-Bus call can exceed the nominal five-second role interval, while the parent's sixty-second handshake deadline remains the outer bound.
  • Test audit: the changed service-file tests exercise ignored-line NUL cases and parser controls. Acquisition, execution-security, lifecycle and exit-cleanup tests assert exact argv, descriptor identity, process groups, owned child identities, pipe EOF and neighbor survival before recovery cleanup. No new vacuous assertion or suppression was introduced.
  • Docs/convention: README.md:444-448, CONTRIBUTING.md:237 and :313-315, docker/README.md:141-142 and :148, and CHANGELOG.md:24 document the opt-in, helper, limits, test inventory and NUL behavior. The workflow's .github/workflows/e2e.yml:55,91 timeout change is internally consistent.
Considered
  • [input] src/kwin_mcp/dbus_service_file.py:221 ignored-line NUL could execute a file native D-Bus rejects; dismissed: current-container probe rejected both malformed variants in both roles, while NUL-free twins executed.
  • [injection] src/kwin_mcp/dbus_service_file.py:282 a builtin, shell metacharacter or no-shebang executable could gain shell execution; dismissed: argument+challenger(/root/fresh_parser_docs) literal execv performs no PATH lookup or ENOEXEC shell fallback.
  • [input] src/kwin_mcp/dbus_service_file.py:122 and :224 quoting or Unicode decoding could diverge from D-Bus; dismissed: current-container probe covers quoted arguments and comment/newline behavior, and argument+challenger(/root/fresh_parser_docs) checked quote state and UTF-8 scalar rules against D-Bus 1.16.2.
  • [filesystem] src/kwin_mcp/dbus_service_file.py:46 a regular file replaced by a FIFO could block acquisition; dismissed: argument+challenger(/root/fresh_lifecycle) O_NONBLOCK precedes same-descriptor fstat rejection, and slower I/O remains under the parent deadline.
  • [filesystem] src/kwin_mcp/dbus_service_file.py:54 growth after fstat could make reads unbounded; dismissed: argument+challenger(/root/fresh_lifecycle) limit-plus-one reads and post-read validation bound bytes, with closure in finally.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1127 lookup could stall before the watchdog; dismissed: argument+challenger(/root/fresh_lifecycle) parent construction is lexical and acquisition occurs only in children after activation failure.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1236 a signal before PID assignment could leak a helper; dismissed: argument+challenger(/root/fresh_lifecycle) group ownership predates spawning and parent teardown escalates the owned group.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1194 a recycled numeric PID could receive rollback signals; dismissed: argument+challenger(/root/fresh_lifecycle) termination uses native jobspec markers, while numeric probes only control polling.
  • [concurrency/lifecycle] src/kwin_mcp/session.py:1215 a kernel-stalled helper could resume after cleanup; dismissed: argument+challenger(/root/fresh_lifecycle) a pending SIGKILL prevents return to userspace, while the owned group remains available for final teardown.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: OPEN re-reviewed at 7347b07. No maintainer answer has changed the open item since the previous review; the technical verdict and open items are unchanged.

Previous findings

  • Closed: default-on direct activation workaround. The exact server-side opt-in is enforced at src/kwin_mcp/session.py:1124.
  • Closed: quoted Exec= argument splitting and literal execution at src/kwin_mcp/dbus_service_file.py:208-210 and :282.
  • Closed: unbounded failed-bootstrap cleanup at src/kwin_mcp/session.py:1199-1215.
  • Closed: parent-side service lookup outside the watchdog; lookup is child-owned at src/kwin_mcp/dbus_service_file.py:271-277.
  • Closed: inherited monitor-mode ownership; set +m runs before background launches at src/kwin_mcp/session.py:1138.
  • Closed: ignored-line NUL acceptance; the whole-file guard is at src/kwin_mcp/dbus_service_file.py:221-222.
  • Open: isac322's explicit maintainer merge hold remains in place.

New findings

None.

Verified

The PR head is still 7347b07, all review threads are resolved, and the merge with main has no interaction with the direct-start runtime path. The previous full review at the current head covered parser parity, NUL rejection, helper ownership, filesystem bounds, startup deadlines, rollback signaling, cleanup, test coverage and documentation. No new technical evidence or code change requires reopening a finding.

Open items

  • Maintainer hold by isac322 remains in place; merge authorization is reserved for a separate maintainer decision.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@isac322 isac322 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: GREEN. PR-ready: false. Reviewed 7347b07aed2f241fbf541c2601ea33eaa4b41c84 against main 3655841d6af72913f984c94650a24bef3e3ed23e. No P0–P3 technical findings remain, and all six earlier findings are closed. The required issue-agent/review check is still failing; its latest review identifies the retained maintainer merge hold as the only open item.

Verified: all seven native E2E matrix jobs passed on this head, as did lint, typing, build, documentation and semantic-title checks. The built wheels, installed into each image, passed the full suite on Debian arm64 and Fedora arm64: 1,113 collected, 1,112 passed, 0 failures, 0 errors and one existing opt-in ScreenShot2 test skipped on each. On Debian arm64, installed CLI checks confirmed server-only opt-in under controlled first-activation denial. Five cases against native dbus-daemon 1.16.2 and both installed helper roles on Debian arm64 confirmed rejection of NUL-bearing comments/localized entries and exact execution of the valid control and both NUL-free variants. The PR's 12-file scope and main's spoken-name feature are preserved.

Not verified: real SELinux-enforcing persistent denial, live NFS/FUSE stalls, prompt cleanup of processes stuck in uninterruptible I/O, actual PID reuse, and GPU-backed ScreenShot2. Direct startup remains an operator-selected workaround with the documented confinement trade-off. This technical approval does not authorize merging, closing, auto-merge or a release; the maintainer hold remains.

@isac322

isac322 commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Thanks for diagnosing the Fedora SELinux activation denial and putting together the initial fix and parser tests, @superbiche!

The opt-in boundary, service-file validation, and process-group lifecycle are all verified on 7347b07aed2f241fbf541c2601ea33eaa4b41c84. I'm releasing the maintainer merge hold so we can merge this now.

@haechibot review

@haechibot haechibot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: GREEN re-reviewed at 7347b07aed2f241fbf541c2601ea33eaa4b41c84. No blocking findings remain.

Previous findings

  • Closed: default-on direct activation workaround; exact server-side opt-in at src/kwin_mcp/session.py:1124.
  • Closed: quoted Exec= parsing and literal execution at src/kwin_mcp/dbus_service_file.py:208-210 and :282.
  • Closed: unbounded failed-bootstrap cleanup at src/kwin_mcp/session.py:1199-1215.
  • Closed: parent-side service lookup outside the watchdog; lookup is child-owned at src/kwin_mcp/dbus_service_file.py:271-277.
  • Closed: inherited monitor-mode ownership; set +m runs before background launches at src/kwin_mcp/session.py:1138.
  • Closed: ignored-line NUL acceptance; the whole-file guard is at src/kwin_mcp/dbus_service_file.py:221-222.
  • Closed: maintainer merge hold, explicitly released by isac322 in comment 6182279371.

New findings

None.

Verified

The full 12-file diff was reviewed against main at merge-base 3655841d6af72913f984c94650a24bef3e3ed23e. The direct-start path is opt-in, service-file commands are validated and executed literally, startup lookup remains inside the child watchdog, process groups are owned and cleaned up with bounded TERM/KILL handling, and the parser rejects NUL-containing files. An isolated container probe compared the helper with native D-Bus behavior for quoted arguments, comments, localized entries, malformed input, and NUL-free controls. The base-branch CLAUDE.md and CONTRIBUTING.md conventions were also checked.

Not verified

Live SELinux-enforcing Fedora behavior, live NFS/FUSE races, PID-reuse stress, and GPU-backed ScreenShot2 behavior were not reproduced here; no code-level issue remains from those environmental limits.

Lenses

Security and input validation, quoting and argument execution, startup deadlines, process-group lifecycle and rollback, filesystem races and bounds, documentation consistency, and interaction with the merged main changes were reviewed.

Considered

  • [lifecycle] src/kwin_mcp/session.py:1199-1215 rollback could leave a failed bootstrap behind — dismissed: argument+challenger(/root/challenger) bounded group signaling and cleanup cover the failure window.
  • [filesystem] src/kwin_mcp/dbus_service_file.py:230-277 service-file replacement or non-regular-file races could bypass validation — dismissed: argument+challenger(/root/final_challenge) regular-file, size, and descriptor checks constrain the path.
  • [input] src/kwin_mcp/dbus_service_file.py:221-222 ignored comments or localized entries could bypass NUL rejection — dismissed: probe whole-file rejection matched native D-Bus behavior.

Every push to this pull request is reviewed automatically. To request another review without pushing, after replying to the findings, comment @haechibot review. The same comment restarts a review that stopped with an error. The pull request author or a maintainer can request it.

@isac322
isac322 merged commit c0c86c7 into isac322:main Oct 15, 2026
17 checks passed
isac322 added a commit that referenced this pull request Oct 15, 2026
Rebased onto origin/main (ac3b821, kwin-mcp 0.12.0 + #111) and integrated
every 0.12.0 capability and bug fix into the 29-tool 1.0 surface:
- Locator.match ('substring' | 'exact' | 'spoken') unifies exact-name (#109)
  and spoken-name (#110, spoken_name.py) matching across element_query,
  waits, and element_act strict locators.
- element_act accepts '|'-separated fallback action names (e.g. 'Press|click'),
  deduplicates candidate elements, and reports the invoked action (#109).
- session.py and dbus_service_file.py port opt-in KWIN_MCP_ATSPI_DIRECT_START=1
  direct AT-SPI startup for SELinux-enforcing hosts (#95), without re-triggering
  D-Bus activation from core.py when activation failed.
- geometry.py ports native equality no-op, related-window traversal stop, and
  global X11 group-transient checks (#103) while keeping check_only and all
  window kinds.
- cli.py rejects unknown boolean literals before session dispatch (#109).
- Ported unit/E2E suites, QA trace entries (QA-SES-026..028, QA-ELEM-045..046,
  QA-CLI-009), mutants M83..M86, CHANGELOG 0.12.0 history, and docs.
isac322 added a commit that referenced this pull request Oct 15, 2026
… 1.0) (#106)

* feat!: replace the 37-tool surface with 29 orthogonal tools (kwin-mcp 1.0)

* feat: sync 1.0 orthogonal surface with 0.12.0 and #111 on main

Rebased onto origin/main (ac3b821, kwin-mcp 0.12.0 + #111) and integrated
every 0.12.0 capability and bug fix into the 29-tool 1.0 surface:
- Locator.match ('substring' | 'exact' | 'spoken') unifies exact-name (#109)
  and spoken-name (#110, spoken_name.py) matching across element_query,
  waits, and element_act strict locators.
- element_act accepts '|'-separated fallback action names (e.g. 'Press|click'),
  deduplicates candidate elements, and reports the invoked action (#109).
- session.py and dbus_service_file.py port opt-in KWIN_MCP_ATSPI_DIRECT_START=1
  direct AT-SPI startup for SELinux-enforcing hosts (#95), without re-triggering
  D-Bus activation from core.py when activation failed.
- geometry.py ports native equality no-op, related-window traversal stop, and
  global X11 group-transient checks (#103) while keeping check_only and all
  window kinds.
- cli.py rejects unknown boolean literals before session dispatch (#109).
- Ported unit/E2E suites, QA trace entries (QA-SES-026..028, QA-ELEM-045..046,
  QA-CLI-009), mutants M83..M86, CHANGELOG 0.12.0 history, and docs.

* fix(docs): align element_act fallback syntax in docs/skills and add 0.12 arg remedies

Address review findings on 6d3895c:
- Align SKILL.md, README.md, and CHANGELOG.md with the MCP string schema
  action='Press|click' (noting the in-process Python engine also accepts a
  sequence).
- Add unknown-argument migration remedies for exact_name, spoken_name, and
  fallback_action_names in server.py and test_unknown_args.py.
- Date 1.0.0 as 2026-10-15 in CHANGELOG.md and update M01-M86 range comments.

* test(e2e): align session_start warning and Locator.match default in e2e assertions

* docs: remove AI-ceremony files, dead design docs, and non-English prose in docs

- Delete the self-referential .claude/ docs-seo loop (docs-seo.md, positioning.yml,\n  docs-seo-trigger.sh, settings.json, check-docs-seo/SKILL.md), the one-off\n  .github/social-preview-prompt.md, obsolete docs/design/* and\n  docs/ai-agent-integration.md (folding unique tips into README.md and\n  CONTRIBUTING.md), and the one-off scripts/gen_qa_trace.py scaffold.\n- Keep the deterministic checks in scripts/check_docs_seo.py and\n  .github/workflows/docs-seo.yml while dropping the .claude/ prompt checks and\n  PR comment bot.\n- Tighten CLAUDE.md, ROADMAP.md, CONTRIBUTING.md, README.md, SKILL.md, and\n  plugin/README.md, and replace non-English examples in docs/skills with pure\n  English descriptions.

* fix(docker): drop removed docs/ directory from E2E image COPY
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.

2 participants