Skip to content

Add act-as-agent mode for human credentials#127

Merged
jgpruitt merged 4 commits into
mainfrom
jgpruitt/as-agent
Jul 2, 2026
Merged

Add act-as-agent mode for human credentials#127
jgpruitt merged 4 commits into
mainfrom
jgpruitt/as-agent

Conversation

@jgpruitt

@jgpruitt jgpruitt commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds ambient act-as-agent mode so a human-authenticated caller can send X-Me-As-Agent and be authorized exactly as one of their own agents, without distributing the agent's API key.

What changed

  • Added AS_AGENT_HEADER / X-Me-As-Agent and server-side switching on both memory and user RPC middleware.
  • Resolves act-as targets against owned agents by id first, then case-insensitive name; unknown or unowned values return INVALID_AGENT.
  • Preserves agent-key precedence: if the bearer is already an agent key, the header is ignored.
  • Threads asAgent through the TS clients, CLI clients, me mcp, me serve, and Claude/OpenCode capture paths.
  • Adds --as-agent <idOrName> and ME_AS_AGENT; the .me sentinel resolves to .me/config.yaml's agent value but never activates mode by itself.
  • Makes me login and me logout explicit local-session exceptions that fail fast in act-as mode instead of mutating the human session store.
  • Records authenticatedAs for observability while keeping authorization based only on the switched agent identity/access.

Tests

  • ./bun test packages/cli/util.test.ts
  • ./bun test packages/cli/credentials.test.ts
  • ./bun test packages/cli/serve/http-server.test.ts
  • ./bun run typecheck
  • ./bun run check
  • ./bun test --timeout 30000 ./e2e/cli.e2e.test.ts (loaded successfully; e2e cases skipped in this environment)

Copilot AI review requested due to automatic review settings July 1, 2026 20:50
@jgpruitt jgpruitt self-assigned this Jul 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an ambient “act as agent” capability that lets a human-authenticated caller (session/OAuth/user PAT) send X-Me-As-Agent and be authorized as one of their owned agents, without requiring distribution of an agent API key. This is implemented on both RPC endpoints and is threaded through the TS clients and CLI surfaces, with observability via an authenticatedAs field.

Changes:

  • Introduces AS_AGENT_HEADER (X-Me-As-Agent) and server-side principal switching (id first, then case-insensitive name) on both user and memory RPC auth middleware, recording authenticatedAs for telemetry.
  • Threads asAgent through the TypeScript clients, CLI, me serve, MCP server, and capture hooks; adds --as-agent and ME_AS_AGENT (including .me sentinel resolution).
  • Adds integration/unit/e2e coverage for act-as behavior, precedence rules, and CLI UX (including login/logout refusal in act-as mode).

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/server/rpc/user/types.ts Adds authenticatedAs to user RPC context; tweaks user-only forbidden message.
packages/server/rpc/memory/types.ts Adds authenticatedAs to space RPC context for observability.
packages/server/rpc/handler.ts Records authenticatedAs into RPC span identity attributes.
packages/server/router.ts Threads authenticatedAs from middleware contexts into RPC handler contexts.
packages/server/middleware/authenticate-user.ts Implements act-as-agent switching for user RPC auth (human → owned agent).
packages/server/middleware/authenticate-user.integration.test.ts Adds integration tests for user-RPC act-as switching, precedence, and failures.
packages/server/middleware/authenticate-space.ts Implements act-as-agent switching for memory RPC auth before access/admin resolution.
packages/server/middleware/authenticate-space.integration.test.ts Adds integration tests for memory-RPC act-as switching, clamping, and parity.
packages/protocol/headers.ts Defines and documents AS_AGENT_HEADER constant.
packages/client/user.ts Adds asAgent option + setAsAgent() to user client and emits header.
packages/client/memory.ts Adds asAgent option + setAsAgent() to memory client and emits header.
packages/client/memory.test.ts Tests header seeding/clearing for both memory and user clients.
packages/cli/util.ts Adds login/logout refusal in act-as mode; forwards asAgent; improves forbidden messaging for account-scope ops.
packages/cli/util.test.ts Adds tests for describeForbiddenError() act-as messaging.
packages/cli/serve/http-server.ts Forwards X-Me-As-Agent through the local proxy in me serve.
packages/cli/serve/http-server.test.ts Verifies X-Me-As-Agent forwarding on both memory and user RPC proxy paths.
packages/cli/project-config.ts Updates .me/config.yaml agent semantics to act as a value source for the .me sentinel only.
packages/cli/opencode/capture.ts Threads asAgent into OpenCode capture hook config.
packages/cli/mcp/server.ts Threads asAgent into MCP server’s memory client configuration.
packages/cli/index.ts Adds global --as-agent flag and wires it via credentials override.
packages/cli/credentials.ts Implements ME_AS_AGENT / --as-agent resolution, .me sentinel, and ResolvedCredentials.asAgent.
packages/cli/credentials.test.ts Adds tests for resolveAsAgent() precedence, .me sentinel behavior, and explicit-only activation.
packages/cli/commands/serve.ts Passes resolved asAgent into me serve.
packages/cli/commands/opencode.ts Passes resolved asAgent into OpenCode importer client.
packages/cli/commands/mcp.ts Passes resolved asAgent into MCP server runner.
packages/cli/commands/logout.ts Refuses logout when act-as-agent mode is requested.
packages/cli/commands/login.ts Refuses login when act-as-agent mode is requested.
packages/cli/commands/claude.ts Passes resolved asAgent into Claude importer client.
packages/cli/claude/capture.ts Threads asAgent into Claude capture hook config/environment resolution.
e2e/cli.e2e.test.ts Adds e2e coverage for act-as behavior across whoami/space/memory ops and local login/logout refusal.
CLAUDE.md Documents the X-Me-As-Agent feature, invariants, and CLI/env activation model.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/server/middleware/authenticate-space.ts Outdated
Comment thread packages/server/middleware/authenticate-user.ts Outdated
Postgres emits uuids lowercase, but the CLI's UUID gate is case-insensitive
and passes the value through verbatim — so an uppercase agent UUID failed the
strict id equality and spuriously 403'd INVALID_AGENT. Lowercase both sides
(reusing the name match's normalized value), per Copilot review on #127.
Adds uppercase-id regression tests to both middleware integration suites.

@cevian cevian left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM suggested some additional safety checks

// send an uppercase UUID, which must not spuriously 403).
const agent =
agents.find((a) => a.id.toLowerCase() === wanted) ??
agents.find((a) => a.name.toLowerCase() === wanted);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to error if it finds more than one agent using these methods. I know it shouldn't happen but a safety check would be nice

// lowercase, but a client may send an uppercase UUID).
const agent =
agents.find((a) => a.id.toLowerCase() === wanted) ??
agents.find((a) => a.name.toLowerCase() === wanted);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment about the find logic here.

if (contentType) outHeaders.set("Content-Type", contentType);
if (token) outHeaders.set("Authorization", `Bearer ${token}`);
if (space) outHeaders.set(SPACE_HEADER, space);
if (options.asAgent) outHeaders.set(AS_AGENT_HEADER, options.asAgent);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add a safety check here that options.asAgent != '.me';

Comment thread packages/client/memory.ts
): Record<string, string> | undefined {
const headers: Record<string, string> = {};
if (options.space) headers[SPACE_HEADER] = options.space;
if (options.asAgent) headers[AS_AGENT_HEADER] = options.asAgent;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets add a safety check that options.asAgent != '.me'

Comment thread packages/client/memory.ts
},
setAsAgent(asAgent: string) {
const headers = { ...config.headers };
if (asAgent) headers[AS_AGENT_HEADER] = asAgent;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make sure asAgent != '.me'

Comment thread packages/client/user.ts
// createUserClient carries no headers by default; seed one only when acting
// as an agent, then lazily create/merge in setAsAgent.
headers: options.asAgent
? { [AS_AGENT_HEADER]: options.asAgent }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safety check that options.asAgent != '.me'

Comment thread packages/client/user.ts
},
setAsAgent(asAgent: string) {
const headers = { ...config.headers };
if (asAgent) headers[AS_AGENT_HEADER] = asAgent;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

safety check asAgent != .me

Copilot AI review requested due to automatic review settings July 2, 2026 12:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

packages/cli/commands/logout.ts:24

  • output() returns a Promise that should be awaited to guarantee JSON/YAML is fully flushed to stdout (especially when piping). This command’s action is already async, but it currently calls output(...) without await.
      output({ server, loggedOut: true }, fmt, () => {
        clack.log.success(`Logged out from ${server}`);
      });

Comment thread packages/server/middleware/authenticate-user.ts Outdated
Comment thread CLAUDE.md Outdated
@jgpruitt

jgpruitt commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Also addressed Copilot's suppressed logout note in 9b65c3f: me logout now awaits output() so JSON/YAML output is flushed before the command exits.

@jgpruitt jgpruitt merged commit 4c645e9 into main Jul 2, 2026
4 checks passed
jgpruitt added a commit that referenced this pull request Jul 2, 2026
Postgres emits uuids lowercase, but the CLI's UUID gate is case-insensitive
and passes the value through verbatim — so an uppercase agent UUID failed the
strict id equality and spuriously 403'd INVALID_AGENT. Lowercase both sides
(reusing the name match's normalized value), per Copilot review on #127.
Adds uppercase-id regression tests to both middleware integration suites.
@jgpruitt jgpruitt deleted the jgpruitt/as-agent branch July 2, 2026 13:24
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.

3 participants