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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -791,3 +791,11 @@ For opt-in activation of queued Codex sessions, see [explicit wake](docs/wake.md

See [multi-home Codex listing](docs/multi-home-list.md) for candidate sources,
per-home errors, duplicate UUIDs, and selecting the exact home for send.

### Internal extension architecture

Agent adapters and local/SSH execution share an internal versioned contract while
retaining the single-file CLI. See [adapter development](docs/agent-adapters.md)
and the [architecture decision](docs/architecture/agent-transports.md). External
plugin loading is not available. `doctor.capabilities.agents` describes implemented
list/send/wake/wait/ack support; it does not grant permission or prove readiness.
65 changes: 65 additions & 0 deletions docs/agent-adapters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Developing an internal agent adapter

This is a source-extension guide, not an external plugin installation interface.
See the [architecture decision](architecture/agent-transports.md) for contract
versioning, compatibility, result semantics and trust boundaries.

An adapter subclasses `AgentAdapter`, declares a unique lowercase `name`, and
implements `list(context)` and `submit(context, text)`. Register it explicitly
with `AGENTS.register(...)` before `main()` executes. Keep its implementation in
the standalone source if it must work over SSH. No CLI router or SSH dispatcher
change is needed for a basic new adapter.

Minimal deterministic example (test use only):

```python
class ExampleAdapter(AgentAdapter):
name = "example"
capabilities = AgentCapabilities() # list/send; no wake/wait/ack

def list(self, context):
return {"sessions": [{"agent": self.name, "id": "one", "status": "idle"}],
"discovery": {"status": "ok"}}

def submit(self, context, text):
target = self.identity(context.options.to, context)
return {"ok": True, "target": {"id": target.identifier},
"status": "validated" if context.options.dry_run else "submitted",
"submitted": not context.options.dry_run,
"consumptionConfirmed": False}

AGENTS.register(ExampleAdapter())
```

The example has no real inbox; never ship it as an actual delivery adapter.
The test-only `tests/fixtures/agent_adapter.py` supplies the executable example
used in contract tests and is excluded from published distributions.

- `list` returns `sessions` and a `discovery` object with `status` equal to `ok`,
`error` (with `error` text), or `not_installed`. Every row identifies its agent.
Preserve native identity fields. Implement `display_row`/`render` for different
row layouts. Codex additionally preserves existing top-level home metadata.
- `submit` receives the already-validated, wrapped message exactly once. Honor
dry-run before native side effects. Return native submission facts; do not
infer acknowledgement from socket/queue acceptance. Native failures after a
known submission must retain its ID and partial outcome.
- `identity` and `target` translate native identity and CLI/Reply-To targets.
The default is `name:identifier`; Claude preserves unprefixed names/PIDs.
- `diagnose`, `diagnostic_text`, `listing_notes`, `submission_text`,
`remote_submission` and `remote_options` specialize evidence and existing
output compatibility without changing common routing. Argument options must
still be explicitly declared in the parser; agents cannot inject shell code.
- Feature booleans report implementation support only. False capabilities fail
before submission. Advertising wake does not skip native runtime checks or MCP
authorization; wait/ack have no CLI implementation in this version.

MCP permits only explicitly authorized destinations and agents. Source
registration alone never grants remote/send privileges. Optional dependencies
must stay outside core import paths; adding a backend dependency requires a
separate packaging decision. Public external loading and installation are not
implemented.

Run the existing suite and the shared contract tests both without and with the
optional MCP SDK. Also build/install wheel and sdist independently and run the
isolated standalone installer tests. Use fixture transports for new contract
coverage, not production sessions.
106 changes: 106 additions & 0 deletions docs/architecture/agent-transports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# ADR: internal agent and execution transport contracts

Status: accepted for the post-v0.8 refactor (#47). This is an internal source
contract, not a public external-plugin ABI. The package version remains 0.8.0
until a separately prepared release.

## Decision

Keep the dependency-free `session_peer.py` standalone artifact and define the
contracts in that file. SSH streams its source to destination-side `python3 -`;
ordinary remote operations still require no installed session-peer service or
package. Splitting the runtime into importable files now would require a bundle
builder, remote module installation, or a second distribution mechanism. None is
needed to validate the agent/transport boundary.

An agent owns native identity, discovery, validation, submission, diagnosis,
agent-specific command options, and presentation. A transport owns where those
operations execute. `LocalTransport` invokes the registered adapter;
`SshTransport` resolves the destination and invokes the existing safely quoted,
single-source SSH protocol. The receiver parses the same CLI and invokes its
local adapter. SSH is not an agent.

The initial registry explicitly contains Claude followed by Codex. CLI choices,
listing, sends, doctor, Reply-To validation and MCP agent policy use that registry.
MCP remains a policy-enforcing subprocess client of the CLI; registration never
grants send/wake permission. Update deployment is still distinct from agent
submission.

## Evidence and compatibility

- Claude discovery produces PID/name/inbox evidence and posts to a native socket.
A completed write is not proof of consumption.
- Codex uses native queue and app-server interfaces. One UUID can occur in
multiple homes, so identity preserves host and home rather than collapsing by
UUID. Multi-home inventory, ambiguity checks and active-writer validation remain
within the Codex implementation. Caller-side defaults are not evidence about a
remote receiver's active home.
- Native queue acceptance may precede a failed wake. Preserve the native payload,
including queue ID, submission and wake status; never automatically retry.
- Wake is optional and remains constrained to the previously verified runtime
and lifecycle. Wait and acknowledgement are unsupported by both adapters.
- Host identity, sender envelopes and return-route normalization are independent
of the agent. Construct the message envelope once at the sender and transmit
its existing base64 representation over SSH.

Existing options, wire fields, response envelope schema version, ordering and
exit codes remain unchanged. The additive exception is
`doctor.capabilities.agents`, mapping registered names to boolean `list`, `send`,
`wake`, `wait`, and `ack` support. These describe implemented mechanisms, not
current credentials, inbox availability, model readiness or destination
permission. `doctor.capabilities.replyObservation` retains its prior meaning.

Remote list partial failures also accept `not_installed` discovery status;
otherwise a missing optional Codex install could discard another agent's
failure diagnostics. A failed remote submission with `submitted: true` likewise
retains its native result instead of discarding its ID. A Reply-To cannot claim one agent while targeting a
registered prefix for another.

## Contract and trust boundary

`ADAPTER_CONTRACT_VERSION = 1` is independent of JSON response schemaVersion.
The registry validates names, exact contract version, capabilities and methods
before registration; duplicates are rejected. `SessionIdentity` records agent,
host, identifier and optional Codex home. `ExecutionContext` provides destination
identity and parsed options. These are process-local values, not a new wire
protocol. Discovery and submission retain typed dictionary contracts so native
result fields are not flattened away.

`AdapterError` derives from the existing CLI error and adds agent/reason fields.
Contract errors use `invalid_adapter_contract`, `duplicate_agent`, `unknown_agent`,
`unsupported_capability`, or `invalid_target`. Malformed discovery and diagnosis
use `invalid_adapter_result`. Unexpected operation exceptions are contained as
`adapter_failed`, without echoing exception text. Unexpected submission errors
or malformed submission results become `outcome_unknown` with no retry, because
native submission might already have happened. Existing intentional CLI/native
errors and their structured details remain intact.

Discovery validates each adapter's result before aggregation. One adapter's
failure must not discard successful rows or diagnostics from another. No adapter
is allowed to claim consumption merely because its transport returned.

There is no entry-point discovery, import-by-name, plugin directory scan, or
message-driven loading. A name in a message, CLI argument, URI or MCP policy
cannot cause code installation or execution. Unknown explicit agents fail;
unregistered prefixes in ordinary target strings remain literal Claude names
for compatibility. The `claude:` prefix is likewise still part of a literal
Claude name; Codex and newly registered adapters use their registered prefixes.

Future external plugins require a separate design: explicit operator installation
and allowlisting, package ownership and dependency isolation, version negotiation,
error containment and documentation of the fact that in-process Python plugins
execute trusted code. This change supplies no sandbox or external loading flag.

## Validation

`tests/test_adapters.py` exercises a common Claude/Codex/fixture contract, third
agent CLI and Reply-To routing, capabilities, incompatible registrations,
malformed results, exception containment, MCP policy and native submission counts.
A source-registered fixture is inserted only into a test copy of the streamed
program and executed in a fresh Python subprocess through the existing SSH
serializer. No production dispatch edits, external plugin loader, live model
account, SSH connection or message delivery are needed for that test.

Existing transport, native socket, queue, multi-home, reply, MCP and wake suites
remain the compatibility baseline. Package and standalone installation tests
ensure the single-file and optional-extra distribution boundaries remain intact.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ path = "session_peer.py"
pattern = '__version__ = "(?P<version>[^"]+)"'

[tool.hatch.build.targets.sdist]
include = ["session_peer.py", "session_peer_mcp.py", "LICENSE", "README.md", "docs/mcp.md", "docs/wake.md", "docs/multi-home-list.md", "plugins", ".agents/plugins/marketplace.json"]
include = ["session_peer.py", "session_peer_mcp.py", "LICENSE", "README.md", "docs/mcp.md", "docs/wake.md", "docs/multi-home-list.md", "docs/agent-adapters.md", "docs/architecture/agent-transports.md", "plugins", ".agents/plugins/marketplace.json"]

[tool.hatch.build.targets.wheel]
only-include = ["session_peer.py", "session_peer_mcp.py"]
Loading