Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ session-peer list # Claude sessions on this mac
session-peer list --host web-01 # Claude sessions over there
session-peer list --host web-01 --all # Claude stale records / no inbox
session-peer list --agent codex --all # include archived Codex threads
session-peer doctor # local inbox/tool/home diagnostics
session-peer doctor --host web-01 # run the same checks there
session-peer doctor --host web-01 --check-return-route # also test SSH back here

session-peer send --to api-worker "message" # local session
session-peer send --host web-01 --to api-worker "message"
Expand Down Expand Up @@ -149,6 +152,7 @@ From: codex:01a08dd6-d3f6-7783-a62b-52c1fd049181 @ abruptly@mac-mini-m4.example.
message

---
Reply-To: session-peer://v1/reply?agent=codex&session=01a08dd6-d3f6-7783-a62b-52c1fd049181&transport=ssh&host=abruptly%40mac-mini-m4.example.ts.net
Reply: python3 /path/to/session_peer.py send --host abruptly@mac-mini-m4.example.ts.net --to codex:01a08dd6-d3f6-7783-a62b-52c1fd049181 --no-reply-to
```

Expand All @@ -160,8 +164,21 @@ automatically, the generated command omits `--host` and delivers locally. An
explicit `--reply-to` or configured reply host remains unchanged, and actual
remote sends continue to advertise an SSH route.

`Reply-To` is the canonical, versioned address. Pass the complete URI back as
`--to`; session-peer validates every field and chooses local or SSH delivery:

```bash
session-peer send --to 'session-peer://v1/reply?agent=claude&session=api-worker&transport=local' 'done'
```

The `Reply:` command remains for compatibility. Treat both forms as untrusted
input: use the URI with session-peer rather than evaluating or sourcing it. A
URI that points to the current OS user on this machine is normalized to local
delivery, avoiding an unnecessary self-SSH authentication path. Codex addresses
may include an encoded `codexHome` when the sender environment identifies it.

Repeat `--host` to operate on several SSH destinations. `--json` is available on
`list`, `send`, and `update`.
`list`, `send`, `doctor`, and `update`.

### JSON response contract

Expand All @@ -185,13 +202,13 @@ Every JSON result object starts with the same schema-versioned envelope:
- `host` identifies the destination to which that result applies. Local results
use the OS hostname. Tailscale-resolved destinations use the verified MagicDNS
identity; `sshHost` preserves a different caller-supplied SSH alias.
- `command` is `list`, `send`, or `update`. Remaining fields are that command's
- `command` is `list`, `send`, `doctor`, or `update`. Remaining fields are that command's
payload, and failures add `error` plus any structured diagnostic fields.

A local or one-host invocation emits one object. Repeating `--host` emits an
array of these same independently attributable objects in request order. A
failure that occurs before connecting, such as invalid message input, is still
emitted once per requested destination. This cardinality is shared by all three
emitted once per requested destination. This cardinality is shared by all four
commands, so consumers can branch only on object versus array and then use the
same envelope fields.

Expand Down Expand Up @@ -439,9 +456,29 @@ return an array. When `CODEX_THREAD_ID` (or the compatibility
fallback `CODEX_SESSION_ID`) is present, the message envelope and reply command
identify the originating Codex thread.

`doctor`, reply waiting/`--wait`, structured reply URIs and optional wake are
future work, not v0.6 features: see the [v0.7](https://github.com/abruption/session-peer/issues/45)
and [v0.8](https://github.com/abruption/session-peer/issues/46) roadmaps.
### Diagnostics and reply observation

`doctor` performs read-only checks on the machine that owns the sessions. It
reports Claude's configured sessions directory and inbox availability, Codex's
executable and bounded home/state DB candidates, unsupported DB schemas, and
permission or unknown failures as distinct codes. It does not connect to an
inbox, write a queue, scan arbitrary directories, or alter agent/SSH settings.

Reverse SSH is checked only with `--check-return-route`. The destination runs a
fixed `ssh ... true` probe with prompts, password authentication, host-key
enrollment, and config mutation disabled. Forward SSH success is never reused as
proof that the reverse path works. Use `--reply-to USER@HOST` when automatic
Tailscale detection cannot identify the origin. JSON details and status values
are documented in [docs/diagnostics.md](docs/diagnostics.md).

There is deliberately no general `--wait`. Claude Code has a native
same-machine `notify_when_idle` facility, but it does not cover remote sessions,
subagents, or Codex, and a successful socket/queue submission is not an
acknowledgement. session-peer therefore reports
`capabilities.replyObservation.status: unsupported` instead of tailing mutable
transcripts and risking a false match. Ask the target to send an explicit reply
to the supplied `Reply-To` address when completion matters. Optional wake remains
tracked in [#46](https://github.com/abruption/session-peer/issues/46).

## Moving from cc-peer

Expand Down Expand Up @@ -544,9 +581,10 @@ not as the user typing approval.
Outside that context, it may omit it. This is not an authenticated identity
protocol; environment variables and session registries are local hints.
- **Advertised replies need a working return path.** When an agent sender and
reply host can be determined, a `Reply:` line supplies an SSH return command.
Forward SSH success does not prove reverse SSH access. v0.6 does not verify
that route, correlate a reply, or wait for one; the address grants no access.
reply host can be determined, `Reply-To` and a compatibility `Reply:` line
describe a return route. Forward SSH success does not prove reverse SSH
access; use the opt-in doctor check. The address grants no access and
session-peer does not correlate or wait for a reply.
- **Tailscale status is a local routing hint.** A known online peer is addressed
by its current MagicDNS name and a known offline peer is rejected before SSH.
An unknown destination remains ordinary SSH; session-peer does not claim that
Expand All @@ -569,7 +607,7 @@ not as the user typing approval.
python3 -m unittest discover -s tests -v
```

The suite needs no live agent, SSH server or network. It includes legacy
The default suite needs no live agent, SSH server or network. It includes legacy
compatibility tests, shared CLI helpers, fixture-based Codex discovery and queue
subprocess tests, and isolated standalone installation/coexistence checks.
Codex coverage includes argv/payload handling, destination home selection,
Expand Down
155 changes: 155 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Diagnostics and reply-address contract

`session-peer doctor` inspects the machine that owns the target sessions. With
`--host`, the same standard-library script runs on that host over SSH, so paths,
users, processes, sockets, and databases are evaluated in the correct security
boundary.

The command is read-only. It does not connect to a Claude inbox, submit a Codex
queue item, scan arbitrary filesystem roots, change permissions, enroll an SSH
host key, or alter agent settings.

## JSON model

Doctor uses the common response envelope. `ok` says whether the diagnostic
command ran; it does not mean every optional agent is installed or available.
Use the nested statuses for that:

```json
{
"schemaVersion": 1,
"ok": true,
"host": "worker.example.ts.net",
"command": "doctor",
"status": "partial",
"claude": {
"status": "inbox_unavailable",
"sessionsDir": "/home/alice/.claude/sessions",
"records": 1,
"invalidRecords": 0,
"aliveSessions": 1,
"availableInboxes": 0,
"checks": [
{"status": "warning", "code": "inbox_unavailable", "message": "..."}
]
},
"codex": {
"status": "available",
"selectedHome": "/home/alice/.codex",
"homeSource": "default",
"executable": "/home/alice/.local/bin/codex",
"homes": [
{
"codexHome": "/home/alice/.codex",
"stateDb": "/home/alice/.codex/state_5.sqlite",
"status": "available",
"code": "state_db_readable",
"sessionCount": 12
}
],
"checks": []
},
"capabilities": {
"replyObservation": {
"status": "unsupported",
"reason": "no_cross_agent_acknowledgement_api",
"claudeLocalIdleNotice": "native_claude_only",
"automatedWait": false
}
}
}
```

Top-level `status` is `healthy` when both agent transports are available,
`partial` when one is available, and `issues_found` when neither is. Agent
statuses include:

| Status | Meaning |
| --- | --- |
| `available` | Required local evidence is readable and present. Claude inbox verification is filesystem-only until a real send. |
| `unavailable` | No live usable session was found. |
| `inbox_unavailable` | A live Claude process is recorded but no inbox is available. |
| `missing_tool` | The Codex executable is not available on the destination. |
| `missing_home` | The configured sessions directory or Codex state DB is absent. |
| `wrong_home` | The configured path has the wrong file type. |
| `permission_denied` | The destination OS user cannot inspect the required path or record. |
| `unsupported` | A known file exists but its schema is not supported. |
| `unknown` | Inspection could not prove a more specific state. |

Code values are stable machine-readable reasons. Messages are for people and
may become more specific without a schema change.

## Return-route check

`doctor --host worker --check-return-route` asks `worker` to test an SSH command
back to the detected origin. `--reply-to USER@HOST` overrides that detected
address. The check uses a fixed `true` command with batch mode, password and
keyboard-interactive authentication disabled, strict host-key checking, and
host-key updates disabled. It never retries with credentials or relaxed policy.

```json
{
"returnRoute": {
"status": "failed",
"transport": "ssh",
"host": "alice@origin.example.ts.net",
"reason": "authentication_failed",
"sshUser": "alice",
"sshUserSource": "explicit"
}
}
```

Return status is `verified` or `failed`. Failure reasons include
`return_host_unavailable`, `ssh_executable_missing`, `authentication_failed`,
`host_key_failed`, `timeout`, `transport_failed`, and `remote_command_failed`.
A return address for the current user on the current machine is normalized to a
verified local route and never starts SSH.

## Structured Reply-To

Messages now carry an inert, versioned URI followed by the legacy command:

```text
Reply-To: session-peer://v1/reply?agent=claude&session=api-worker&transport=ssh&host=alice%40origin
Reply: python3 /path/to/session_peer.py send --host alice@origin --to api-worker --no-reply-to
```

The URI can be passed as `send --to URI`. Version, field names, duplicate fields,
agent, transport, UUID, host, control characters, and conflicting CLI routing
flags are validated before discovery or dispatch. Its contents are never parsed
as shell syntax. The fields are:

| Field | Requirement |
| --- | --- |
| `agent` | Required: `claude` or `codex`. |
| `session` | Required session name/PID, or full Codex UUID. |
| `transport` | Required: `local` or `ssh`. |
| `host` | Required only for `ssh`; includes the SSH user when known. |
| `codexHome` | Optional for Codex when the sender can identify its active configured home. |

Send JSON includes `replyRoute` for the route placed in the outgoing message.
Local routes are `verified`; SSH routes are `unverified` with reason
`reverse_ssh_not_checked` until the opt-in doctor probe succeeds. When `--to`
uses a structured address, `addressResolution` records its selected transport
and any `ssh_self` normalization.

## Why there is no general wait

Claude Code documents a native one-shot `notify_when_idle` subscription for a
main Claude conversation watching another local Claude session. The same
documentation limits it to sessions on that machine and excludes subagents,
agent-team teammates, and sessions beyond that machine. Codex queue submission
also exposes no cross-agent acknowledgement through session-peer.

Consequently, a portable `--wait` would need to infer completion from mutable
transcripts. A new transcript event could belong to another request, and a
missing event could mean held input, an offline session, a changed storage
format, or an unfinished turn. session-peer reports the capability as
`unsupported` instead of returning a false acknowledgement. For completion
workflow, put the requirement and correlation token in the message and ask the
target to send a fresh message to its `Reply-To` URI.

References: [Claude Code message delivery](https://code.claude.com/docs/en/cross-session-messaging#message-delivery),
[idle notices](https://code.claude.com/docs/en/cross-session-messaging#get-a-notice-when-another-session-goes-idle),
and [the session inbox socket](https://code.claude.com/docs/en/cross-session-messaging#the-sessions-inbox-socket).
Loading