Skip to content

Agent participants on every surface + pairux CLI - #113

Merged
ralyodio merged 3 commits into
masterfrom
feat/agent-participants
Sep 24, 2026
Merged

ralyodio merged 3 commits into
masterfrom
feat/agent-participants

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

AI agents (Claude Code, moshcode, scripts) can join a live PairUX session as labelled participants that follow and post in the chat. Before this, "agentic" existed only as marketing copy (#101, #102); there was no agent in the roster and no CLI.

What's in it

Surface Change
DB session_participants.kind (human/agent), agent_owner_id, agent_client. join_session_as_agent RPC, service-role only: max 5 live agents, settings.allowAgents=false opt-out, agents idle 2+ min retired. cli_auth_codes + cli_tokens (hashes only, RLS on, no policies).
API /api/v1/cli/{authorize,token,revoke,me} is OAuth 2.1 auth code + PKCE S256 over a loopback redirect, with rotating refresh tokens and family revocation on reuse. /api/v1/agents/join, GET/DELETE /api/v1/agents/:id (poll = roster + chat + heartbeat; 410 when removed/ended), POST /api/v1/agents/:id/messages.
Web + PWA Agent badge in the chat roster and host sidebar. Agents get only "Remove agent" (no DM, mute, grant control, make-host). Host removal goes through the existing kick route. /cli/authorize consent page and /docs/agents.
Desktop Same badge and action rules in both roster components. Desktop's kick already deletes the DB row, so removal works as-is.
Mobile Had no roster at all. New AgentStrip on host and viewer screens (polls every 10s); hosts long-press to remove.
CLI New packages/cli → @profullstack/pairux, bin pairux: login / logout / whoami / join / listen [--json] / say / who / status / leave. No runtime dependencies, config file at 0600.

Before merge

  • Apply 20260924120000_agent_participants.sql to prod (yuwjbjskkghlyrdkhexu). No CI step applies migrations. Until it's applied, the agent routes return errors; existing UI is unaffected (kind is optional and absent means human). I checked prod read-only: none of the new columns or tables exist yet, and profiles.display_name/username are there.
  • Publish @profullstack/pairux to npm.
  • Desktop ships only on a v* tag.

Known limits

  • Agents don't subscribe to LiveKit, so they can't see or hear the screen. That's deliberate for v1.
  • pairux login needs the browser on the same machine as the CLI (loopback redirect). On remote boxes, join anonymously; a device-code flow would be the follow-up.
  • Chat messages don't record a participant id, so an agent's chat line isn't badged, only its roster entry.

Tests

  • web: 93 passed (new: cli-auth 20, agent routes 14, roster components 3)
  • desktop: 41 passed (+2 agent row)
  • mobile: +1 (liveAgents)
  • cli: 19 passed (including a real loopback socket test)

Typecheck and lint are clean on web, desktop, mobile and cli, and prettier is clean on changed files. Full next build was not run: the dev box root disk is at 100%.

🤖 Generated with Claude Code

AI agents (Claude Code, moshcode, scripts) can now join a live PairUX
session as labelled participants that read and post in the chat.

- DB: session_participants.kind ('human'|'agent'), agent_owner_id,
  agent_client; join_session_as_agent RPC (service role only, max 5 live
  agents, settings.allowAgents, stale agents retired after 2 min).
- CLI sign-in: OAuth 2.1 auth code + PKCE S256 over a loopback redirect,
  rotating refresh tokens with family revocation on reuse; hashes only.
  /cli/authorize consent page, /api/v1/cli/{authorize,token,revoke,me}.
- Agent API: /api/v1/agents/join, GET/DELETE /api/v1/agents/:id (poll =
  roster + chat + heartbeat, 410 when removed/ended), POST .../messages.
- Web + PWA: Agent badge in chat roster and host sidebar; agents get only
  "Remove agent" (no DM, mute, control, make-host).
- Desktop: same in both roster components.
- Mobile: new AgentStrip on host + viewer screens, long-press remove.
- packages/cli: @profullstack/pairux with `pairux login|logout|whoami|
  join|listen|say|who|status|leave`, --json for agents.
- Docs: /docs/agents, llms.txt, sitemap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
url.searchParams.set('error', 'access_denied');
url.searchParams.set('state', state);
setDone('denied');
window.location.href = url.toString();
).saveConfig(
{
tokens: {
accessToken: 'pux_at_x',
{
tokens: {
accessToken: 'pux_at_x',
refreshToken: 'pux_rt_x',
return {
status: 200,
body: {
access_token: 'pux_at_new',
status: 200,
body: {
access_token: 'pux_at_new',
refresh_token: 'pux_rt_new',
).saveConfig(
{
tokens: {
accessToken: 'pux_at_old',
{
tokens: {
accessToken: 'pux_at_old',
refreshToken: 'pux_rt_old',
d.configPath
);
expect(await run(['whoami'], d)).toBe(0);
expect((await readConfig()).tokens).toMatchObject({ refreshToken: 'pux_rt_2' });
).saveConfig(
{
tokens: {
accessToken: 'pux_at_x',
{
tokens: {
accessToken: 'pux_at_x',
refreshToken: 'pux_rt_x',
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

ThreatCrush Security Scan

47 finding(s)

HIGH/CRITICAL: 4 | MEDIUM: 27 | LOW: 16

Severity Rule Location
HIGH sh-eval-expansion .githooks/pre-commit:33
HIGH js-electron-node-integration apps/desktop/src/main/window.ts:49
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:715
HIGH sh-unquoted-expansion-destructive apps/installer/scripts/install.sh:910
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:691
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:813
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:815
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:1064
MEDIUM sh-remote-script-execution apps/installer/scripts/install.sh:1066
MEDIUM sh-remote-script-execution apps/livekit/setup-livekit-server.sh:93
MEDIUM sh-remote-script-execution apps/turn/deploy-droplet.sh:62
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:48
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:73
MEDIUM js-unescaped-html-sink apps/web/src/app/c/[handle]/page.tsx:192
MEDIUM js-open-redirect apps/web/src/app/cli/authorize/consent.tsx:66
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:129
MEDIUM js-unescaped-html-sink apps/web/src/app/l/[joinCode]/page.tsx:213
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:142
MEDIUM js-unescaped-html-sink apps/web/src/app/live/page.tsx:145
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:122
MEDIUM js-unescaped-html-sink apps/web/src/app/pricing/page.tsx:284
MEDIUM js-open-redirect apps/web/src/app/pricing/UpgradeButton.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/u/[username]/page.tsx:282
MEDIUM js-open-redirect apps/web/src/hooks/useDesktopHandoff.ts:24
MEDIUM redos-nested-quantifier apps/web/src/lib/deliverable.ts:11
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:124
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:393
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:396
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:405
MEDIUM js-unescaped-html-sink apps/web/src/lib/player/player.ts:608
MEDIUM sql-template-interpolation packages/ai-core/src/prompts.ts:36
LOW secret-generic-credential apps/livekit/fly.toml:12
LOW secret-generic-credential apps/turn/fly.toml:11
LOW secret-generic-credential docs/API.md:747
LOW secret-generic-credential docs/API.md:753
LOW secret-generic-credential packages/cli/src/cli.test.ts:138
LOW secret-generic-credential packages/cli/src/cli.test.ts:139
LOW secret-generic-credential packages/cli/src/cli.test.ts:340
LOW secret-generic-credential packages/cli/src/cli.test.ts:341
LOW secret-generic-credential packages/cli/src/cli.test.ts:371
LOW secret-generic-credential packages/cli/src/cli.test.ts:372
LOW secret-generic-credential packages/cli/src/cli.test.ts:383
LOW secret-generic-credential packages/cli/src/cli.test.ts:397
LOW secret-generic-credential packages/cli/src/cli.test.ts:398
LOW secret-generic-credential packages/cli/src/cli.test.ts:405
LOW secret-generic-credential packages/cli/src/cli.test.ts:415
LOW secret-generic-credential packages/cli/src/cli.test.ts:416

Snippets are redacted; ThreatCrush never prints matched credential material.

Comment thread packages/cli/src/config.ts Fixed
Comment thread packages/cli/src/auth.ts Dismissed
ralyodio and others added 2 commits September 24, 2026 06:25
… tolerates payloads without participants

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ct at the consent page's navigation

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio marked this pull request as ready for review September 24, 2026 06:32
@ralyodio
ralyodio merged commit 6ac25bc into master Sep 24, 2026
14 checks passed
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.

2 participants