Add act-as-agent mode for human credentials#127
Conversation
There was a problem hiding this comment.
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, recordingauthenticatedAsfor telemetry. - Threads
asAgentthrough the TypeScript clients, CLI,me serve, MCP server, and capture hooks; adds--as-agentandME_AS_AGENT(including.mesentinel 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.
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
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
let's add a safety check here that options.asAgent != '.me';
| ): 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; |
There was a problem hiding this comment.
lets add a safety check that options.asAgent != '.me'
| }, | ||
| setAsAgent(asAgent: string) { | ||
| const headers = { ...config.headers }; | ||
| if (asAgent) headers[AS_AGENT_HEADER] = asAgent; |
There was a problem hiding this comment.
let's make sure asAgent != '.me'
| // 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 } |
There was a problem hiding this comment.
safety check that options.asAgent != '.me'
| }, | ||
| setAsAgent(asAgent: string) { | ||
| const headers = { ...config.headers }; | ||
| if (asAgent) headers[AS_AGENT_HEADER] = asAgent; |
There was a problem hiding this comment.
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 alreadyasync, but it currently callsoutput(...)withoutawait.
output({ server, loggedOut: true }, fmt, () => {
clack.log.success(`Logged out from ${server}`);
});
|
Also addressed Copilot's suppressed logout note in 9b65c3f: |
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.
Summary
Adds ambient act-as-agent mode so a human-authenticated caller can send
X-Me-As-Agentand be authorized exactly as one of their own agents, without distributing the agent's API key.What changed
AS_AGENT_HEADER/X-Me-As-Agentand server-side switching on both memory and user RPC middleware.INVALID_AGENT.asAgentthrough the TS clients, CLI clients,me mcp,me serve, and Claude/OpenCode capture paths.--as-agent <idOrName>andME_AS_AGENT; the.mesentinel resolves to.me/config.yaml'sagentvalue but never activates mode by itself.me loginandme logoutexplicit local-session exceptions that fail fast in act-as mode instead of mutating the human session store.authenticatedAsfor 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)