Skip to content

This is an explanation doc. It explains why the fleet uses separate identities per agent and how that affects message delivery.


The fleet today ​

protoWorkstacean routes messages to multiple agents, each with a distinct identity and a distinct role. When an agent posts back to GitHub, Discord, or Linear, the response should appear as that agent, not as a generic "protoBot" catch-all. Attribution, filtering, and trust signals all depend on the posting identity matching the agent that produced the reply.

The current roster:

AgentRoleRuntimeGitHub identityDiscord identity
AvaFree-form conversational chat, delegation suggestionsIn-process (workspace/agents/ava.yaml)none — Ava is DM-only today@ava#3387 (dedicated pool bot)
QuinnPR review, bug triage, security triageExternal A2A (${QUINN_BASE_URL}/a2a)@protoquinn[bot]shared — output via protoBot primary client
Jon / protoContentContent strategy, GTM, outreachExternal A2A (${PROTOCONTENT_BASE_URL}/a2a)n/a@protoJon#6536 (dedicated pool bot)
protoPenSecurity / pentest / RF reconExternal A2A (${PROTOPEN_BASE_URL}/a2a)n/ashared — output via protoBot primary client
FrankPersonal chaos lab runtimeExternal (future)n/a@frank#2599 (dedicated pool bot)

Discord output has two tiers. The shared primary client (protoBot, DISCORD_BOT_TOKEN) handles guild messages, slash commands, HITL interactions, and team-level output from external agents like Quinn. The agent pool clients (ava, jon, frank) handle DMs directed at their specific identities and pre-warm DM channels on startup so the gateway delivers MESSAGE_CREATE to the right bot.


GitHub App identities ​

Quinn has its own GitHub App installation. The app has an App ID and a private key (PKCS#1 PEM). The GitHub-acting code paths (the GitHub plugin, pr-inspector) mint short-lived installation tokens from those credentials via the shared makeGitHubAuth helper, so comments and reviews post with the right attribution.

When a PR review response lands for Quinn:

  1. The GitHub context is resolved from the inbound correlationId (owner, repo, number)
  2. The Quinn container mints a JWT using GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY
  3. Exchanges it for a GitHub installation token (refreshed every 45 min by an entrypoint daemon)
  4. Submits the formal review via the GitHub API — shows as @protoquinn[bot]

If GitHub App env vars are missing, calls fall back to the PAT (GITHUB_TOKEN), which posts as the PAT owner's account. That fallback is a debugging convenience only — production always uses App credentials.


Discord: multi-bot pool architecture ​

Discord gives each bot a distinct identity, so the fleet runs multiple Discord clients from within the same workstacean process.

The shared primary client (DISCORD_BOT_TOKEN → protoBot) listens to guild messages, slash commands, HITL button interactions, and any DM that isn't directed at a specific agent-pool bot. It's the operational backbone — one client for cross-channel output.

Each agent pool client has its own token, logs in as its own identity, and pre-warms DM channels with known users at startup (Discord's gateway only pushes MESSAGE_CREATE events for DM channels in the session's private_channels list, so a fresh login needs to createDM() once per user to start receiving events).

The pool is defined by merging two sources:

  • workspace/agents.yaml — A2A registry entries with discordBotTokenEnvKey (external agents)
  • workspace/agents/*.yaml — in-process agent definitions with discordBotTokenEnvKey (runtime agents)

DM routing:

  1. User DMs @ava in Discord
  2. Ava's pool client (logged in via DISCORD_BOT_TOKEN_AVA) receives MessageCreate
  3. _handleDM(message, agentName="ava", bus) publishes message.inbound.discord.<channel> with meta.agentId=ava
  4. Router forwards to skill-dispatcher with the explicit target
  5. Dispatcher resolves ava in the ExecutorRegistry → in-process DeepAgentExecutor (LangGraph)
  6. Response flows back to message.outbound.discord.<channel> and the pool client posts as @ava

If a message hits the shared protoBot client (guild @-mention, DM to protoBot directly), routing falls back to ROUTER_DM_DEFAULT_AGENT + ROUTER_DM_DEFAULT_SKILL env vars — currently ava + chat, so generic DMs still land on the conversational Ava agent.


Project onboarding side-effects ​

Bringing a project online runs in-process via the OnboardingPlugin (lib/plugins/onboarding.ts): it registers the repo's GitHub webhook, creates its Google Drive folder, and publishes a completion event. It does not create or persist a project record of its own — project metadata comes from the GitHub-topic-driven registry (see operator-flows), and channel→agent bindings live in workspace/channels.yaml via the ChannelRegistry.

The standard per-project channel structure (dev, alerts, releases) is bound in workspace/channels.yaml; the registered channel IDs are made available to both the agent running in the target repo context and the workstacean routing layer.


The contextId in A2A calls ​

Agent conversation memory is scoped by contextId in the JSON-RPC call:

json
{
  "params": {
    "contextId": "workstacean-{channelId}"
  }
}

contextId is derived from the message channel. For Discord, it's the Discord channel ID. For GitHub, it's {owner}/{repo}#{number}. For Linear, it's the correlationId (linear-{issueId}).

This means:

  • All messages in the same GitHub issue share a conversation thread in the agent's memory
  • All messages in the same Discord channel share a thread
  • The HITL resume flow (days later) uses the same correlationId as the original request — the agent's memory of the conversation is preserved across the approval gap

Env var naming — why "AVA_*" stays ​

The environment variables that describe a remote A2A connection (AVA_BASE_URL, AVA_API_KEY, AVA_APP_ID, AVA_APP_PRIVATE_KEY) keep the AVA_* prefix for historical reasons (the repo has always been called the "ava" server). These describe the HTTP service identity of a remote A2A agent server, and renaming them would require a coordinated infisical secret migration + homelab-iac redeploy for no functional gain.

The split is conceptual:

  • AVA_* env vars = the HTTP/auth identity of a remote A2A agent server
  • ava (in-process) = the conversational chat agent, a separate entity

DISCORD_BOT_TOKEN_AVA belongs to the in-process Ava chat agent. DISCORD_BOT_TOKEN_PROTO is protoBot, the shared primary client used for team-level output.


Secrets management ​

All per-agent secrets live in Infisical (AI project 11e172e0). The workstacean container receives them via infisical run at deploy time. The homelab-iac docker-compose.yml is the canonical reference for which secrets map to which services.

No secrets are stored in this repository. workspace/agents.yaml uses apiKeyEnv to reference the env var name (e.g., AVA_API_KEY), not the key value itself.


Adding a new agent identity ​

External (A2A) agent ​

  1. Create the agent runtime in its own repo with a JSON-RPC /a2a endpoint
  2. (Optional) Create a GitHub App for bot attribution; generate and store the private key in Infisical
  3. Add APP_ID, APP_PRIVATE_KEY, and any API_KEY env vars to homelab-iac/stacks/ai/docker-compose.yml
  4. Add an entry to workspace/agents.yaml with name, url, apiKeyEnv, and declared skills
  5. (Optional) If the agent should have its own Discord bot, add discordBotTokenEnvKey pointing at a new DISCORD_BOT_TOKEN_* env var
  6. Restart workstacean — [skill-broker] Registered N A2A agent(s) in logs should increment

In-process agent ​

  1. Create workspace/agents/{name}.yaml with name, role, model, systemPrompt, tools, skills
  2. (Optional) Add discordBotTokenEnvKey for a dedicated Discord identity
  3. Restart workstacean — [agent-runtime] Loaded agent "{name}" in logs confirms the definition loaded
  4. ExecutorRegistry.resolve(skill, [name]) will route work to the in-process DeepAgentExecutor (LangGraph) for that agent

Both paths coexist. The ExecutorRegistry resolves by explicit target first, then by skill — so in-process and A2A agents can share a skill space without collision as long as dispatches carry meta.agentId.

protoWorkstacean — a switchboard, not an agent.