Skip to content

fix(agent): exec refuses a typed ssh to a machine the registry knows - #552

Merged
0xKT merged 5 commits into
mainfrom
fix/exec_refuses_raw_ssh_to_registered_machine
Sep 24, 2026
Merged

0xKT merged 5 commits into
mainfrom
fix/exec_refuses_raw_ssh_to_registered_machine

Conversation

@silverLXT

Copy link
Copy Markdown
Contributor

Two field runs on 2026-09-14 put a registered machine's address in the task
statement, and the coding nodes typed "ssh -p root@ '... &'" from
the local shell 58 times to start GPU work. The look-at-it channel is capped
at 60 s and the on-call agent's job runner was not theirs to call, so the raw
address was the path of least resistance. The cap, the process-group sweep
that stops an orphan from holding a GPU, and the ledger all live on the other
two paths, so none of that work was metered.

The plain shell now refuses a command that names ssh and a host the registry
knows, and the refusal names both the machine and the two paths that are
meant for it. Everything else is untouched: an unknown host still works, scp
and rsync still work (they move files and start nothing on the far side), and
a registry that cannot be read recognises nothing, so ops being broken never
takes the plain shell down with it.

Type: fix

Verification:

  • tests/test_shell_machine_channel.py: 32 passed, up from 28 on 0464aa1.
    The four new ones cover a typed ssh to a registered machine being refused
    with nothing run, an unknown host still running, a command that names the
    host without ssh still running, and a malformed registry refusing nothing.
  • Wider run over the shell surface (machine channel, approval, background,
    policy reasons, rpc): 316 passed, 0 failed.

Risk: low, but it does close a path some prompt may rely on. The refusal is
text, not an exception, and it names the replacement, so a model that reaches
for the old path is told where to go. This does not add the missing channel
itself: work between 60 s and a few minutes still has no first-class home
outside the on-call agent, which is a separate design question.

@silverLXT
silverLXT requested a review from LivXue as a code owner September 20, 2026 09:35

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the SSH detector must identify the executable and exact registered destination before refusing or naming a replacement.

I found three concrete failures in the new guard, detailed inline: ordinary path-qualified SSH invocations bypass it; same-host connections on different ports are resolved to the first row; and an unregistered address sharing a registered address's prefix is falsely refused.

Verification: uv run pytest tests/test_shell_machine_channel.py -q (32 passed), and uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q (454 passed). I also reproduced all three cases directly against raw_ssh_target; the current tests do not cover them. GitHub's full check suite is green.

Coverage: I checked the repository rules and canonical Machine definition, the full diff, ExecTool callers and permission ordering, relevant registry/transport code and history, backward-compatibility claims, test changes for weakening, and the machine-identity architecture constraint.

Comment thread raven/agent/tools/machine_exec.py Outdated
Comment thread raven/agent/tools/machine_exec.py Outdated
Comment thread raven/agent/tools/machine_exec.py Outdated
@silverLXT
silverLXT force-pushed the fix/exec_refuses_raw_ssh_to_registered_machine branch from 84b22ed to 8ab4d06 Compare September 20, 2026 10:01

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: compound-shell SSH and explicit -o Port destinations still need to be refused.

The three findings from the prior revision are fixed and their threads are resolved. This revision still has two concrete bypasses, detailed inline: valid compound shell lines can hide a registered SSH execution, and OpenSSH's explicit -o Port=... form is treated as port 22.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 37 tests, and uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 454 tests. Direct reproductions confirmed both bypasses. GitHub's checks otherwise pass, but the coverage gate currently fails at 87.72% diff coverage (50/57 changed executable lines).

Coverage: I checked the repository rules and canonical Machine definition, the old-to-new delta and full diff, ExecTool callers and permission ordering, registry/transport behavior and relevant history, backward compatibility, test changes for weakening, and the machine-identity architecture constraints.

Comment thread raven/agent/tools/machine_exec.py Outdated
Comment thread raven/agent/tools/machine_exec.py Outdated
@silverLXT
silverLXT force-pushed the fix/exec_refuses_raw_ssh_to_registered_machine branch from 8ab4d06 to 244f610 Compare September 20, 2026 10:11

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the open compound-command and explicit-port findings still need code changes.

This revision adds four coverage tests only; raven/agent/tools/machine_exec.py is unchanged from the prior revision. Re-running the exact reproductions still returns None for both reported bypasses, so those two threads remain open. I found no new defect in the test-only delta, and the added tests do not weaken existing assertions.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 41 tests, and uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 454 tests. At review time all completed GitHub checks pass; unit shard 4/4 is still running and the coverage gate has not yet reported.

Coverage: I checked the repository rules and canonical Machine definition, the prior-to-current delta and full diff, callers and permission ordering, relevant history, backward compatibility, test-strength changes, and the machine-identity architecture constraints.

@silverLXT
silverLXT force-pushed the fix/exec_refuses_raw_ssh_to_registered_machine branch from 244f610 to 4696085 Compare September 20, 2026 10:22

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

Both open findings are fixed in this revision: compound commands now expose every SSH destination, including after unspaced operators, and -o Port=... participates in registry matching. I reproduced the original failing cases, replied to both threads, and resolved them. A sweep confirms every thread I opened is now resolved.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 46 tests, and uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 454 tests. At review time every completed GitHub check passes; unit shard 4/4 is still running.

Coverage: I checked the repository rules and canonical Machine definition, the prior-to-current delta and full diff, callers and permission ordering, relevant history, backward compatibility, test-strength changes, and the machine-identity architecture constraints. I also applied the fourth-round convergence bar to parser edge cases and found none that justify another blocker.

Comment thread raven/agent/tools/machine_exec.py Outdated
Comment thread raven/agent/tools/machine_exec.py Outdated
@LivXue

LivXue commented Sep 20, 2026

Copy link
Copy Markdown
Member

Blocking: 1 finding needs correction before this revision can merge; see the inline notes.

Scope: I reviewed the raven/agent/** portion of this PR -- 2 of 4 changed files, which is the
whole of the review zone. The other two are tests/test_shell_machine_channel.py and
CHANGELOG.md, read as context. Ring 3 (raven/agent/subagent/) and ring 4 (the dispatch
tools and the DAG skill) are untouched, so no change here carries a sign-off.

I reviewed the new detector and its single call site, re-ran every reproduction the earlier
rounds filed, and probed the two syntax families they did not cover.

Confirmed findings:

  • Blocker, raven/agent/tools/machine_exec.py:168: the -o value parser accepts only the
    Port=<n> spelling, so -o "Port <n>" discards the value, leaves the port at 22, and a raw
    ssh to a machine registered at that port on that host runs on the plain shell path.
    Reproduced through ExecTool; the executed command's own stderr names port 58717.
  • Nonblocking, raven/agent/tools/machine_exec.py:104: _SHELL_OPERATORS omits >&, <&,
    &> and |&; a redirection token written before the destination is taken as the
    destination, and the real host is then skipped. Reproduced against _ssh_destinations.

Verification:

  • Every reproduction from the earlier rounds, re-run at this head: path-qualified and
    backslash-escaped ssh, a second machine at the same address on another port, the
    longer-address case, unspaced compound operators, two ssh commands in one line, and
    -o Port=. All seven behave as those threads say they were fixed to.
  • uv run pytest tests/test_shell_machine_channel.py -q: 46 passed. With
    tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py: 454 passed.
  • lint-imports: 10 kept, 0 broken.
  • Both findings here were reproduced by running the code, not by reading it.

Checked and deliberately not reported: the detector is spelling-based, so bash -c 'ssh ...',
a backticked command, and a host built from a shell variable all still reach the machine -- I
reproduced all three. Not filed as a defect: they need deliberate indirection, the docstring
frames this guard as closing the path of least resistance rather than as a sandbox, and no
token-level fix reaches them. Also checked and clean -- a trailing 2>&1, |& or here-string,
where the destination has already been parsed; the guard sitting ahead of _guard_command,
which by design leaves no approve-then-run path for this shape; and the complete absence of
deletions in the diff, so no earlier guard was dropped.

@silverLXT
silverLXT force-pushed the fix/exec_refuses_raw_ssh_to_registered_machine branch from 4696085 to c1f84cc Compare September 21, 2026 07:38
@silverLXT

Copy link
Copy Markdown
Contributor Author

Revision c1f84cc answers both threads from the last round.

Blocker (machine_exec.py, the -o branch): the option value is now split on an
equals sign or on whitespace, so -o "Port 58717", -o 'Port = 58717' and
-o Port=58717 all reach registry matching. Reproduction from the thread:
_ssh_destinations("ssh -o 'Port 58717' root@203.0.113.7 true") now returns
[('203.0.113.7', 58717)], and raw_ssh_target names conn_gpu for the
58717 row.

Nonblocking (the operator set): adding >&, &> and friends to the
terminator set was not enough on its own. 2>&1 tokenises as three words --
2, >&, 1 -- so the bare descriptor was taken for the destination before
the operator was ever seen, and treating &> as a terminator ended the scan
before the real host. The set is split in two: command separators still end
the ssh; redirections (< > >> << <<< <& >& &> &>> >| <>) and the file they
name are stepped over, as is a bare digit standing in front of one. A
redirection left dangling before a separator does not swallow the separator.
The one shape the tokens cannot distinguish -- a destination that is itself a
bare integer written with a space before >& -- is documented at the check;
no registry row has that shape.

Tests: 12 new cases (two parametrised ExecTool tests for the spaced -o
spelling and for a redirection ahead of the host, plus a direct
_ssh_destinations table for after-host, dangling, dangling-then-separator,
heredoc and later-option-wins). All 12 fail on the previous head and pass
here. tests/test_shell_machine_channel.py 58 passed; the five wider suites
454 passed; ruff, ruff format and ty clean. Every new line in machine_exec.py
is covered; the three uncovered lines in the file (196->199, 288-289, 346)
predate this PR.

Two field runs on 2026-09-14 put a registered machine's address in the task
statement, and the coding nodes typed "ssh -p <port> root@<ip> '... &'" from
the local shell 58 times to start GPU work. The look-at-it channel is capped
at 60 s and the on-call agent's job runner was not theirs to call, so the raw
address was the path of least resistance. The cap, the process-group sweep
that stops an orphan from holding a GPU, and the ledger all live on the other
two paths, so none of that work was metered.

The plain shell now refuses a command whose tokens run the ssh client to a
destination the registry holds, naming both the machine and the two paths
meant for it.

The command is a shell line, not an argv. It is tokenised with
punctuation_chars so an unspaced operator separates commands the way a shell
reads them -- "true&&ssh" is two of them, and plain splitting hands back one
word that is neither -- and every ssh in the line is read, because a compound
line reaches each of its commands and stopping at the first let a registered
destination through on the strength of an unregistered one beside it.

A redirection is not the end of a command, and it is not a word either:
"ssh 2>&1 -p 58717 root@host" is one ssh whose host follows the plumbing.
Reading ">" as a terminator, or "&>" as the destination, lost the registered
host after it; the operator and the file it names are stepped over instead,
and the bare descriptor that punctuation_chars hands back in front of "2>&1"
with it.

The executable is recognised as "ssh", "/usr/bin/ssh" or "\ssh" alike: a
leading backslash only suppresses alias lookup and an absolute path only
skips PATH. The destination comes from ssh's own arguments rather than a
search of the text, and the port from "-p" or from an "-o" port option in
either spelling OpenSSH honours -- "-o Port=N" and "-o 'Port N'" print the
same "port N" under ssh -G: the registry holds several machines at one
address, so the address alone names the wrong one, and 203.0.113.70 is not
203.0.113.7.

Untouched: an unregistered host still runs, scp and rsync still run (they
move files and start nothing on the far side), and a registry that cannot be
read or a line that cannot be tokenised recognises nothing, so ops being
broken never takes the plain shell down with it.

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; suggestions only, and they are marked inline.

The prior blocker and operator-set note both reproduce as fixed: spaced -o Port values now match the registered port, and redirections before the destination no longer hide it. I withdrew my confirming blocker on LivXue's thread; that thread remains for its owner to resolve.

Named nonblocking follow-up: repeated conflicting Port settings use the wrong precedence. OpenSSH reports port 58717 for ssh -G -o Port=58717 -o 'Port 22' example.com because the first obtained value wins, while _ssh_destinations and the new test choose 22. This requires contradictory duplicate settings and removing the later duplicate is an immediate working path, so it does not meet the late-round blocker bar.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 58 tests, and uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 454 tests. GitHub had not reported checks for this force-pushed revision at review time.

Coverage: I checked the repository rules and canonical Machine definition, the prior-to-current delta and full diff, callers and permission ordering, relevant history, backward compatibility, test-strength changes, and the machine-identity architecture constraints. All threads I opened remain resolved.

@silverLXT
silverLXT force-pushed the fix/exec_refuses_raw_ssh_to_registered_machine branch from c1f84cc to ecc9cc1 Compare September 21, 2026 07:44

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; suggestions only, and they are marked inline.

The rebase onto current main preserved the reviewed detector and its tests byte-for-byte; the PR diff is still the same four files, and the prior fixes for spaced Port options and shell redirections still reproduce.

Named nonblocking follow-ups: the repeated conflicting-Port precedence mismatch remains as documented in the prior review. Separately, the detector treats any token named ssh as an executable position: echo ssh -p 58717 root@203.0.113.7 is refused as reaching the registered machine even though the shell only prints those words. That can obstruct diagnostics or script generation. This change introduces the false positive and it is reachable in ordinary use, but an operator has working alternatives and it does not reopen the raw-SSH bypass, so it does not meet the round-seven blocker bar.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 58 tests; uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 454 tests. At review time, 17 GitHub checks passed, the coverage gate was still pending, and docs deploy was skipped.

Coverage: I checked the repository rules and canonical Machine definition, the full diff and rebase delta, the detector's single caller and permission ordering, relevant history, backward compatibility, test-strength changes, and the machine-identity architecture constraints. All threads I opened remain resolved; the two open LivXue threads are not mine to resolve.

xiaotian.luo and others added 2 commits September 21, 2026 18:13
Reviewed 2026-09-21. ssh takes the first obtained value for every option,
and -p and -o Port queue together: `ssh -G -p 2222 -o Port=58717 host`
prints 2222, and the two reversed prints 58717 (OpenSSH 9.9p2). The
detector overwrote instead, so `-p 58717 -p 22` read as port 22 -- a
command that really reaches the registered machine on 58717 read as
unregistered and ran on the plain shell path, past the cap, the
process-group sweep and the ledger. The prior test pinned the wrong
semantics under the name "later-option-wins"; it is corrected and joined
by six cases covering both spellings in both orders, all failing on the
previous head.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@silverLXT

Copy link
Copy Markdown
Contributor Author

Revision ba233ba: the named nonblocking precedence finding is fixed, and the
branch is merged up to current main.

You were right and my earlier test had it backwards. Measured on OpenSSH 9.9p2,
ssh takes the FIRST obtained value for every option, with -p and -o Port
queueing together:

ssh -G -o Port=58717 -o 'Port 22' example.com  ->  port 58717
ssh -G -o 'Port 22' -o Port=58717 example.com  ->  port 22
ssh -G -p 2222 -o Port=58717 example.com       ->  port 2222
ssh -G -o Port=58717 -p 2222 example.com       ->  port 58717

The detector overwrote instead, so it was not only the conflicting-option case:
ssh -p 58717 -p 22 root@<registered> read as port 22, which means a command
that really reaches the registered machine read as unregistered and ran on the
plain shell path -- past the cap, the process-group sweep and the ledger. That
is the direction this guard exists to close, so I took the finding as a defect
rather than a suggestion.

The prior case in the redirection table asserted 22 under the id
"later-option-wins"; it now asserts 58717 as "first-port-wins", joined by
test_a_repeated_port_keeps_the_first_value_as_ssh_does covering both spellings
in both orders and -p twice in both orders. All six fail on the previous head.

The second named follow-up -- echo ssh -p 58717 root@203.0.113.7 being
refused although the shell only prints it -- I am leaving as is and recording
why: deciding that a token named ssh is not in executable position needs a real
parser, and the failure direction here is a false refusal with the reason
printed, not a missed bypass. I would rather over-refuse echo ssh ... than
under-refuse something that runs.

Verification on this head: tests/test_shell_machine_channel.py 64 passed; all
tests/test_shell_*.py plus test_rpc_spine and test_sandbox_unit 652 passed;
ruff, ruff format and ty clean. Merge-tree against main is clean and the branch
is 0 behind.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; suggestions only, and they are marked inline.

The repeated-Port precedence finding is fixed. I compared all six new -p/-o Port orderings with OpenSSH and the parser now keeps the same first obtained value; the corrected expectation is backed by broader coverage rather than a weakened test. The merge-up preserves the prior fixes and, against the refreshed current main, the PR remains the same four-file scope.

The previously named executable-position false-positive remains the only nonblocking follow-up. I found no new defect on this revision.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 64 tests; uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 454 tests. GitHub CI was still in progress at review time, with 16 checks passed and three unit shards pending.

Coverage: I checked the repository rules and current canonical Machine definition, the full diff and prior-to-current delta, the detector's caller and permission ordering, relevant history, backward compatibility, test-strength changes, and the machine-identity architecture constraints. All threads I opened remain resolved; the two open LivXue threads are not mine to resolve.

Comment thread raven/agent/tools/machine_exec.py Outdated
Comment thread raven/agent/tools/machine_exec.py Outdated
Comment thread raven/agent/tools/machine_exec.py Outdated
@LivXue

LivXue commented Sep 21, 2026

Copy link
Copy Markdown
Member

Blocking: 2 findings need correction before this revision can merge; see the inline notes.

Scope: I reviewed the raven/agent/** portion of this PR -- 2 of 4 changed files, which is the
whole of the review zone. The other two are tests/test_shell_machine_channel.py and
CHANGELOG.md, read as context. Ring 3 (raven/agent/subagent/) and ring 4 (the dispatch tools
and the DAG skill) are untouched by this revision, so no change here carries a sign-off.

Both findings I filed at 46960856 are fixed at this head and I re-ran every reproduction from
the earlier rounds. One correction to my own last round: the remedy I named for the operator-set
note was wrong. The author's reply showing that adding those four tokens to the one set leaves
2>&1 worse, because the bare descriptor is taken for the destination before the operator is
ever seen, is correct; the split into _COMMAND_SEPARATORS and _REDIRECTIONS with a step over
a bare descriptor is a better fix than the one I suggested. I am also not re-filing the repeated
Port precedence point, which is the other reviewer's and is fixed at a61113df6.

Confirmed findings:

  • Blocker, raven/agent/tools/machine_exec.py:195: a bundled short-flag group such as
    -vp 58717 matches neither the -p/-o test nor the two-character value-flag test, so it is
    skipped as a no-argument flag and its port argument becomes the destination.
    ssh -vp 58717 root@203.0.113.7 true reads as [('58717', 22)] -- host and port both wrong --
    and runs on the plain shell path. -4p and -vvvp behave the same, and OpenSSH honours all
    three.
  • Blocker, raven/agent/tools/machine_exec.py:191: the option scan ends at the destination, but
    ssh re-enters its option loop after the host until the first non-option word.
    ssh root@203.0.113.7 -p 58717 true connects on 58717 and reads here as port 22.
  • Nonblocking, raven/agent/tools/machine_exec.py:118: B is the one value-taking flag missing
    from _SSH_VALUE_FLAGS, so ssh -B lo -p 58717 root@203.0.113.7 takes the interface name for
    the destination.

All three share one mechanism: an option shape the scan does not model yields not an unknown
destination but a wrong one, because the destination is assigned from any word not recognised as
a flag. For what it is worth, the stdlib getopt accepts ssh's own option string and reproduces
all four measured cases, including both boundary cases, when a second pass over the words after
the destination mirrors ssh's own re-entry.

Verification:

  • Reproduced through ExecTool with the registry holding 203.0.113.7:58717. For each bypass
    the line ran on the plain shell path and the executed command's own stderr reports connect to host 203.0.113.7 port 58717; a ; touch <marker> tail confirms the whole line executed. The
    control ssh -p 58717 root@203.0.113.7 is refused as conn_gpu at the same head.
  • ssh -G comparisons on OpenSSH_8.9p1 for every form above, including the boundary cases
    ... true -p 58717 and ... -- -p 58717, which both stay on port 22 and which this scan
    already reads correctly.
  • Every reproduction from the earlier rounds, re-run at this head: path-qualified and
    backslash-escaped ssh, a second machine at the same address on another port, the
    longer-address case, unspaced compound operators, two ssh commands in one line, -o Port=,
    the spaced -o "Port n" spelling, and a redirection ahead of the host. All still behave as
    those threads say they were fixed to.
  • uv run pytest tests/test_shell_machine_channel.py -q: 64 passed. lint-imports: 10 kept,
    0 broken. GitHub checks at review time: 20 passed, 0 failed, 2 skipped.
  • All three findings were reproduced by running the code, not by reading it.

Checked and deliberately not reported: the documented deviation for a destination that is a bare
integer written before >& holds -- the registry does not validate the host field, but no real
address is purely numeric and the failure direction is to keep scanning for the real host. The
echo ssh ... over-refusal named by the other reviewer is a false refusal with its reason
printed rather than a missed bypass, and the author has recorded why it stays. bash -c 'ssh ...', a backticked command and a host built from a shell variable all still reach the machine;
unchanged from my last round, they need deliberate indirection and no token-level fix reaches
them. Also checked and clean: the separated -v -p 58717 spelling, which parses correctly; the
guard sitting ahead of _guard_command, which leaves no approve-then-run path for this shape;
and the complete absence of deletions in the diff, so no earlier guard was dropped.

xiaotian.luo and others added 2 commits September 23, 2026 12:38
Two spellings ssh honours let a connection to a registered machine past the
guard (review on #552, 2026-09-21):

- a bundled short-flag group: `ssh -vp 58717 root@h` was skipped as a group
  taking no argument, so 58717 became the destination;
- an option after the host: OpenSSH re-enters its option loop once it has the
  destination and reads on until the first non-option word or `--`, so
  `ssh root@h -p 58717 true` connects on 58717 while the scan, which stopped at
  the host, read port 22.

Groups are now read letter by letter until one takes a value, and the scan
continues past the host until the remote command starts. The value-taking set
is derived from ssh's own getopt string (OpenSSH_9.9p2) rather than kept by
hand; the hand-kept one had lost `B`, so `ssh -B lo ...` read the interface
name as the destination.

The expectations are measured with `ssh -G`, and a parametrised test
re-measures them against the ssh on the machine running the suite.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
…ssh_to_registered_machine

# Conflicts:
#	CHANGELOG.md

@gloryfromca gloryfromca left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; suggestions only, and they are marked inline.

I reproduced the three author-described parser corrections on this revision: bundled option groups now consume their values as getopt does, options after the destination affect the connection only until the remote command or --, and the value-taking option table includes -B and current -P. The new compatibility table agrees with the installed OpenSSH client, so the tests strengthen rather than mask the behavior. The merge-up leaves the pull request at the same four-file scope.

The previously named executable-position false positive remains the only nonblocking follow-up. I found no new defect on this revision.

Verification: uv run pytest tests/test_shell_machine_channel.py -q passed 94 tests; uv run pytest tests/test_shell_comments.py tests/test_shell_approval.py tests/test_shell_background.py tests/test_rpc_spine.py tests/test_sandbox_unit.py -q passed 475 tests. GitHub checks are green except unit shard 4/4, which is still running.

Coverage: repository rules and the canonical Machine definition, the full pull-request diff and revision delta, the detector's caller and ordering, relevant history, backward compatibility, test strength, and the stated architecture constraints. All threads I opened remain resolved; the remaining review threads were opened by another reviewer.

@silverLXT
silverLXT requested a review from 0xKT September 23, 2026 06:29
@0xKT
0xKT merged commit e595203 into main Sep 24, 2026
29 checks passed
@0xKT
0xKT deleted the fix/exec_refuses_raw_ssh_to_registered_machine branch September 24, 2026 07:19
silverLXT pushed a commit that referenced this pull request Sep 24, 2026
…onfig's own list

Review findings on the registry-writer PR (2026-09-24).

Blocker: a process that started with no machine kept an `exec` schema without
`machine` after the first `ops_connection_add`, because the registry serves a
tool from the copy it took at admission and `ExecTool` authored no
`to_schema` -- so the reply, the typed-ssh refusal from #552 and the new
Raven-Code guide all sent the model to `machine=<id>` while the schema it was
shown had no such parameter. `ExecTool.to_schema` is authored now, which is
how a tool declares a dynamic shape; the shape moves only when the registry
goes from empty to not.

The registry falls back to the owner's home only when there is none beside the
instance's own config, in `raven.ops.connections.store_path` and in the on-call
launcher's `connections_registry`. Home-first was right for a sub-agent, whose
config sits in a state directory nothing writes, but it silently moved a
`--config /x/config.json` host off `/x/connections.json` as soon as a registry
appeared in the home -- which an agent's first add now writes with no owner
action. A sub-agent still resolves the home: nothing is ever written beside its
rendered config.

Also: `ops_connection_add` joins the settings tool table's `run` group, and the
guard that diffs that table against a loop's registry turns the flag on, so it
can see opt-in tools; `CONTEXT.md`'s Machine entry and both on-call doc pages
describe the lookup and the tool; the plugin's orphaned `_PROBE` is gone; and
#552's ssh-version test asks the client whether `-P` takes a value instead of
looking for the string "tag", which an older client prints as the hostname --
so it never skipped on OpenSSH 8.9.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
LivXue pushed a commit that referenced this pull request Sep 24, 2026
… can add a machine (#690)

## Why

In a research -> code -> oncall chain run on 2026-09-22, Raven-Code
called
`exec(machine="conn_cpu_32c")` and got "No connection is registered", on
a home
whose `connections.json` listed seven machines. It then read
host/port/key out
of the config and ran a raw `ssh root@<ip> -p <port> -i ~/.ssh/id_rsa`,
putting
the way in into the transcript.

Two gaps:

1. **Read side.** `raven.ops.connections.store_path()` looked only
beside the
instance's own config. A sub-agent runs on a rendered config in its own
state
directory, so it looked in an empty directory. The host already hands
every
sub-agent `RAVEN_HOME`; the registry just was not looked for there. Only
the
on-call launcher worked around this (it sets `RAVEN_CONNECTIONS`
itself).
2. **Write side.** `ops_connection_add` (#545) lived in the on-call
plugin. In the
usual chain the coding agent runs first, meets the empty registry first,
and
   had no way to take the owner's answer.

## What changes

- `store_path()`: env var -> `raven_home()/connections.json` if it
exists ->
beside the config if THAT exists -> the home (where a first add lands).
An
  install that kept its own list beside its config keeps working.
- New `raven/ops/connection_add.py`: `probe`, `ssh_defaults`,
`parse_probe`,
`write`, `write_ssh_alias`, and `ASK_OWNER` (the questions relayed to
the owner).
The tool calls these functions directly; `raven ops connection add` (the
terminal command for a person) calls the same ones, so the logic exists
once.
- New `raven/agent/tools/connection_add.py`: `ops_connection_add` as a
trunk tool,
**opt-in** via `tools.connectionAdd` (on in raven-code and raven-oncall,
off in
  the other three products, whose pinned tool faces are unchanged).
- `raven.ops.transport.make_ssh_runner(identities_only=)` +
`ISOLATE_IDENTITY`,
moved from the plugin: a key the tool picked itself is probed with that
key
  alone (see #545 review).
- On-call plugin: `oncall_flow/connections.py` imports trunk's reader
instead of
keeping a hand-aligned copy (758 -> 212 lines); keeps only campaign
lookups
(`get`, `display_name`, `resolve_into`, `describe`). Its own tool file,
  factory and manifest row are removed.
- Raven-Code guide (`TOOLS_CODE.md`) gains when to leave this computer:
write
here; verify where the software is; ask the owner for a machine only
when
installing the software here is wrong (GPU, memory) or heavy (compiled
solver,
toolchain, long download); never ssh from an address in the task
statement.
- `SHOWN` made public in `raven.ops.connections` (the plugin may not
import an
  underscored trunk name; `_SHOWN` stays as an alias).
- Docs: `docs-site/docs/oncall.md` / `oncall.zh.md` updated for the new
lookup
  and the tool.

## Not in scope

- The pre-dispatch registry gate removed in a44664d is NOT restored.
Where a
job runs is still decided inside the agent that runs it; this only makes
that
  agent find the owner's registry and able to add to it.
- #552 (exec refuses a typed ssh to a registered machine) is
complementary: it
only fires once the registry is visible, which this makes true for
Raven-Code.

## Tests

- `tests/test_ops_connection_add.py` (moved from the plugin suite,
patches trunk
modules), including the ssh `-G` measurement that only the candidate key
is
  offered under isolation.
- Five new cases in `tests/test_ops_connections.py` for the lookup
order.
- Plugin parity tests replaced by an identity pin (the plugin's names
ARE trunk's
  objects).
- Code launcher face ledger gains a machine lane; oncall face unchanged.
- Full suite: see CI.

@LivXue -- review requested: this touches `raven/ops` and the
`RAVEN_CONNECTIONS`
handoff described in a44664d.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>

Generated with Claude Code (https://claude.com/claude-code)

---------

Co-authored-by: xiaotian.luo <xiaotian.luo@thetahealth.ai>
Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
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.

4 participants