Skip to content

feat(j5): an outside agent can message, start and stop agents - #560

Open
Jacksondr5 wants to merge 8 commits into
j5code/spawn-519from
j5code/outside-agents-498
Open

Jacksondr5 wants to merge 8 commits into
j5code/spawn-519from
j5code/outside-agents-498

Conversation

@Jacksondr5

@Jacksondr5 Jacksondr5 commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Problem

An agent signed in from outside the app (Claude Code or Codex in a terminal, ChatGPT, a bot) could read the fleet on J5 but could not message, start or stop an agent. Upstream's docs promise all three, and J5 hides upstream's own send and launch tools. Closes #498.

Stacked on #557 (which is stacked on #553). Review this PR's own four commits.

What changed

An outside agent gets an identity in J5's ledger, one per sign-in: outside:<session id>, shown by the label the client registered. The design and each decision are in #498 (Jackson, 2026-10-11).

  • Messaging. j5_send_message accepts an outside agent. It sends plain messages to agents on this server only. Nothing can be delivered to it, so asks, replies and messages addressed to it are refused, each with the legal move named. Its message is recorded in the receiver's project, and the envelope tells the receiving agent who sent it and that no reply can reach it.
  • Starting an agent. j5_spawn_agent accepts an outside agent that names a project, or asks for none. The new agent sits at the top level of its project with the outside agent as its spawner.
  • Listing and stopping. j5_list_participants (with a project_id) and j5_stop_agent accept an outside agent.
  • The cap. The approved sign-in level limits every send (a queued one included), every stop and every spawn (a persona included). A send is checked when it is sent, so a message already queued still lands if the receiver's modes are raised, or the sign-in revoked, before delivery. Whether to check again at delivery is an open question for Jackson.
  • In-app agents. Their checks are unchanged, with two exceptions: delegate_task now also requires the orchestration capability (every thread session has it), and listing a project that has no agent threads yet returns an empty listing where it returned an error.
  • Refusals. Crews, playbook tools, delegate_task, j5_stop_crew, j5_archive_crew and j5_clear_own_ask tell an outside agent to start an agent and brief it. One helper makes that refusal for every thread-only tool.
  • Clients. On web, the thread card, the "Spawned by" line and the Fleet row name the outside agent. Mobile renders no agent-to-agent cards and has no Fleet page, so nothing changes there.

A new table, j5_a2a_outside_agent (ledger migration 33), keeps each outside agent's label for the envelope and the clients.

UI changes

Web only, captured at 1280×800 on the live-test servers. Every state is new: on the base (#557) an outside agent's send and spawn are refused, so there is no "before" to show.

Before After
Outside-agent message card new Outside-agent message card, after
Outside-agent message queued behind a held turn new Outside-agent message queued behind a held turn, after
Outside agent named in Spawned by new Outside agent named in Spawned by, after
Outside agent named in Fleet new Outside agent named in Fleet, after
Fleet hover shows the full outside-agent label new Fleet hover shows the full outside-agent label, after

No video was captured in this pass, so the shots do not show the queued row turning into a delivered card.

Live testing

Pass, at 25b8bee1a3 against base af1d67b344, by a Peer Agent following j5-live-test. Report: test-reports/j5code-outside-agents-498.md. The head has since been rebased over docs-only commits beneath it.

Ran live: a real OAuth sign-in as an outside agent; listing, sending, spawning, retrying and stopping; every refusal, with its text; one receiving turn each on Codex (Luna) and Claude (Haiku); one turn sent through the UI; a restart at rest and a restart during a held turn; migration 33 on a copy and on a fresh home; a refused send to an agent on a peer server; a read-only sign-in; revoking the sign-in; an in-app agent asked in plain words to answer an outside agent's message and to message another agent; the browser pass after the rebase.

Not exercised live:

  • The desktop shell, mobile, relay and tunnel connections, and providers other than Codex and Claude. The rig cannot reach them.
  • A persona above the approved level being refused, and j5_archive_crew, j5_clear_own_ask and the playbook tools other than start and current. Unit tests cover these.
  • Releasing a queue held by a restart. A queued outside message stayed held after a restart during a held turn, which is existing restart behaviour.
  • Starting a Haiku agent with j5_spawn_agent. The tool refused the test home's Haiku model because it exposes no reasoning options, so the spawn was verified on Luna.

Nothing needs a person to test by hand.

Observed and not caused by this PR: a Haiku thread switched from Full access to Auto failed its next turn with "auto mode unavailable for this model"; and an in-app agent listing another, empty project sees the person's row labelled with the agent's own project.

Checklist

  • One concern: the description has no "also"
  • Tests cover the changed behavior (backend changes ship with focused tests)
  • UI changes: before/after screenshots above, and a video for motion or interaction
  • Upstream-owned files: each one is recorded in FORK.md (case text and file-table row) in this PR (none edited)
  • Upstream product: any change to what upstream's product does has a human decision linked above and a register entry in docs/j5/product/upstream.md (D2 rewritten)
  • Live testing: the section above says what ran live and what did not
  • Surfaces: entry points, clients, providers, contracts, reverse states, connection modes (see AGENTS.md)
  • Docs: definitions under docs/j5/product/ and user docs rewritten where this changes them

Surfaces: web changed; desktop wraps web; mobile has no matching surface. Not provider-shaped. An outside agent reaches this server only, in every connection mode. Reverse state: revoking the session in Settings → Connections retires the identity.

Built by Claude Opus 5.5 in Claude Code, with subagents for the three slices.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
j5-code Ready Ready Preview Oct 12, 2026 7:37am UTC

Request Review

@Jacksondr5
Jacksondr5 force-pushed the j5code/outside-agents-498 branch from 0a9dd9d to b971c45 Compare October 12, 2026 05:40
@Jacksondr5
Jacksondr5 force-pushed the j5code/outside-agents-498 branch from dada229 to c7f1531 Compare October 12, 2026 06:20
@Jacksondr5
Jacksondr5 force-pushed the j5code/spawn-519 branch 3 times, most recently from af1d67b to 572fb63 Compare October 12, 2026 06:45
@Jacksondr5
Jacksondr5 force-pushed the j5code/outside-agents-498 branch from c7f1531 to 25b8bee Compare October 12, 2026 06:49
Jacksondr5 added a commit that referenced this pull request Oct 12, 2026
@Jacksondr5
Jacksondr5 force-pushed the j5code/outside-agents-498 branch from 25b8bee to 9ad520d Compare October 12, 2026 07:16
@Jacksondr5
Jacksondr5 force-pushed the j5code/outside-agents-498 branch from 9ad520d to 69791c2 Compare October 12, 2026 07:30
Jacksondr5 and others added 7 commits October 12, 2026 03:31
…stop them

An agent signed in from outside the app (an MCP OAuth client) had no identity
in J5's ledger, so every J5 tool that acts for a calling thread refused it with
upstream's generic error. It could watch the fleet but not take part (#498).

Each sign-in is now the participant `outside:<session id>`, shown by the label
its client registered and kept in a new `j5_a2a_outside_agent` table.

- `j5_send_message`: a plain message to an agent on this server, recorded in
  the receiver's project. The level the sign-in was approved at caps every
  send, a queued one included. Asks, replies, people, peer servers and unknown
  ids are refused with the legal move named. Retry keys use the client
  session's namespace.
- Delivery uses a new envelope that names the outside agent by its label and
  id and says no reply can reach it. Nothing can be delivered to one, for any
  sender, and a peer server may not deliver as one.
- `j5_list_participants` opens to every outside agent and needs `project_id`.
- `j5_stop_agent` opens under the same cap, with no Captain exemption.
- The Crew tools, `j5_clear_own_ask` and the playbook run tools stay refused,
  now with an error that names what an outside agent can do instead. They are
  declared `writes` and refuse in the handler. `j5_spawn_agent` and
  `delegate_task` keep upstream's refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Slice 1 of #498 told an outside agent to "start an agent with j5_spawn_agent
and brief it" for everything it is refused, but the spawn tool itself still
refused it.

- `j5_spawn_agent` is declared `startsThreads` with no modes. That keeps every
  check a thread caller gets, admits an outside agent and refuses a read-only
  client. The handler's own mode checks, wording and retry refusal are
  unchanged; for an outside agent their limit is the level the sign-in was
  approved at, and omitted modes are that level.
- An outside agent names the project (`project_id` or `scratch`) and cannot
  pick the `shared` workspace. Both refusals name the legal move.
- A persona's policy must be within the approved level for an outside agent.
  A thread caller's persona spawn keeps having no ceiling.
- The new agent has no placement parent and records `outside:<session id>`
  as its spawner. The label is written with the placement, through the same
  upsert a send uses. Placement accepts an outside spawner it has a record of.
- The first turn names the outside agent by its label and says no reply can
  reach it, in the identity block the web parser already reads.
- Retry keys use the client session's namespace.
- The participant-identities read resolves an `outside:` id to its label, and
  the Fleet read names the outside spawner in a new optional `outsideSpawner`
  field on `FleetAgent`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A message from an outside agent arrived as raw envelope text, and an agent one
started read "Agent-spawned" on the Fleet page with nothing naming who started
it (#498).

- Thread card: the outside-agent envelope renders as the peer card, named by
  the outside agent's label and marked "Outside agent" the way a machine
  sender is marked "Automation". The label in the envelope stands in until the
  participant-identities read answers, so no new read is made. The instruction
  sentence stays envelope text for the receiving agent. A queued delivery is
  labelled the same way.
- Fleet page: an agent with `outsideSpawner` reads "Started by outside agent
  <label>", or its id when the client registered no label.
- Spawn brief: "Spawned by" and "Brief from" mark an `outside:` spawner as an
  outside agent, and say "an outside agent" instead of "Unnamed participant"
  while its label is unread.

Mobile renders no A2A cards and has no Fleet surface, so nothing changes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, and review findings

- `actsAsThreadCaller` and `readsAsThreadCaller` declare a tool that needs a
  calling thread and refuse an outside agent with its legal move. The five
  Crew and ask tools, the playbook tools and `delegate_task` use them, so a
  new handler cannot admit an outside agent by forgetting a check. The
  playbook reads and `delegate_task` gave upstream's generic refusal before.
- `j5_spawn_agent` is declared `writes`, as send and stop are.
- `j5_list_participants` lists a project that has no thread yet as its people
  and no agents, for every caller, instead of failing on the missing ledger.
  For an outside agent no row accepts an Exchange or an urgency, and a person
  accepts nothing.
- An outside send resolves its receiver once before the send and once inside
  it, and checks replay once. It returns no backlog notice.
- The outside agent's label is written once. Placement no longer checks for
  the row its own transaction writes.
- The thread card names an outside sender from the identities read alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cap is checked at send

The tool reference kept two "Outside agents" paragraphs beside rules that
read as universal. Each rule now states its outside-agent case, and the
Errors lists cover them. The definition and D2 say the approved level is
checked when a call is made: a message already sent is still delivered after
the receiver's modes are raised or the sign-in is revoked. D2 no longer says
`delegate_task` keeps upstream's refusal. The module README keeps only what
the code does not say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keeps the name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 83c224a6 Deployed Oct 12, 2026 by vercel[bot]
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.

1 participant