From 024f14000e5e78b3a48742e9d0b663cde2590384 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 15 May 2026 20:53:07 -0300 Subject: [PATCH] =?UTF-8?q?release:=200.1.4=20=E2=80=94=20forward=20cp=5Fj?= =?UTF-8?q?wt=20+=20bucket=20via=20params.=5Fmeta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On login the gateway now captures user.id from /v1/auth/login, derives the user's MinIO bucket (user--files), and persists both in ~/.a2a/credentials.json. UpstreamAgent.callTool injects ``params._meta = { cp_jwt, cp_url, bucket }`` on every tools/call so upstream agents see the same caller context the platform orchestrator provides. Unauthenticated clients omit _meta — back-compat preserved. Requires a2a-pack with the matching _meta handler in ``a2a_pack/mcp/server.py`` (companion change in apps/a2a). Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 ++-- package.json | 2 +- src/api.ts | 20 +++++++++----------- src/cli.ts | 9 ++++++++- src/credentials.ts | 17 ++++++++++++----- src/gateway.ts | 21 ++++++++++++++++----- src/upstream.ts | 23 +++++++++++++++++++---- 7 files changed, 67 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20bc8a1..ef945db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "a2amcp", - "version": "0.1.3", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "a2amcp", - "version": "0.1.3", + "version": "0.1.4", "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", diff --git a/package.json b/package.json index 2e63451..6aaa58c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "a2amcp", - "version": "0.1.3", + "version": "0.1.4", "description": "MCP gateway for a2acloud agents. Run as a local MCP server; expose any number of deployed A2A agents to Claude Code, Cursor, and other MCP clients.", "type": "module", "bin": { diff --git a/src/api.ts b/src/api.ts index 008ad37..0582afa 100644 --- a/src/api.ts +++ b/src/api.ts @@ -52,23 +52,21 @@ export class ControlPlaneClient { } login(email: string, password: string) { - return this.request<{ access_token: string; user: { email: string } }>( - "POST", - "/v1/auth/login", - { email, password }, - ); + return this.request<{ + access_token: string; + user: { id: number; email: string }; + }>("POST", "/v1/auth/login", { email, password }); } signup(email: string, password: string) { - return this.request<{ access_token: string; user: { email: string } }>( - "POST", - "/v1/auth/signup", - { email, password }, - ); + return this.request<{ + access_token: string; + user: { id: number; email: string }; + }>("POST", "/v1/auth/signup", { email, password }); } me() { - return this.request<{ email: string }>("GET", "/v1/me"); + return this.request<{ id: number; email: string }>("GET", "/v1/me"); } listAgents() { diff --git a/src/cli.ts b/src/cli.ts index 6b57f19..4d6a6f7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -92,7 +92,14 @@ program const api = resolveApiUrl(opts.api); try { const out = await new ControlPlaneClient(api, null).login(email, password); - await saveCredentials({ apiUrl: api, token: out.access_token, email: out.user.email }); + const bucket = `user-${out.user.id}-files`; + await saveCredentials({ + apiUrl: api, + token: out.access_token, + email: out.user.email, + userId: out.user.id, + bucket, + }); output.write(`logged in as ${out.user.email} @ ${api}\n`); } catch (err) { fail(err instanceof ApiError ? err.message : (err as Error).message); diff --git a/src/credentials.ts b/src/credentials.ts index de9221e..4e40a42 100644 --- a/src/credentials.ts +++ b/src/credentials.ts @@ -14,6 +14,8 @@ export interface Credentials { apiUrl: string; token: string; email: string; + userId?: number; + bucket?: string; } const credsDir = () => path.join(os.homedir(), ".a2a"); @@ -28,6 +30,8 @@ export async function loadCredentials(): Promise { apiUrl: data.api_url ?? DEFAULT_API_URL, token: data.token, email: data.email ?? "", + userId: typeof data.user_id === "number" ? data.user_id : undefined, + bucket: typeof data.bucket === "string" ? data.bucket : undefined, }; } catch (err: any) { if (err?.code === "ENOENT") return null; @@ -37,11 +41,14 @@ export async function loadCredentials(): Promise { export async function saveCredentials(c: Credentials): Promise { await fs.mkdir(credsDir(), { recursive: true }); - await fs.writeFile( - credsFile(), - JSON.stringify({ api_url: c.apiUrl, token: c.token, email: c.email }), - { mode: 0o600 }, - ); + const out: Record = { + api_url: c.apiUrl, + token: c.token, + email: c.email, + }; + if (c.userId !== undefined) out.user_id = c.userId; + if (c.bucket !== undefined) out.bucket = c.bucket; + await fs.writeFile(credsFile(), JSON.stringify(out), { mode: 0o600 }); } export async function clearCredentials(): Promise { diff --git a/src/gateway.ts b/src/gateway.ts index 4c9f3aa..39286cf 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -39,14 +39,25 @@ export interface GatewayHandle { export async function buildGateway(opts: GatewayOptions = {}): Promise { const cfg = opts.agents ?? (await loadConfig()).agents; - const token = - opts.token !== undefined - ? opts.token - : (await loadCredentials())?.token ?? null; + const creds = opts.token !== undefined ? null : await loadCredentials(); + const token = opts.token !== undefined ? opts.token : creds?.token ?? null; + // Forward CP credentials + bucket as params._meta so upstream agents + // can act on behalf of the caller (mirrors the platform-orchestrator + // call shape). When the user isn't logged in, _meta is omitted and + // upstream sees the same unauthenticated context as before. + const cpJwt = creds?.token ?? null; + const cpUrl = creds?.apiUrl ?? null; + const bucket = creds?.bucket ?? null; const upstreams = new Map(); for (const a of cfg) { - upstreams.set(a.name, new UpstreamAgent({ name: a.name, url: a.url, token })); + upstreams.set( + a.name, + new UpstreamAgent({ + name: a.name, url: a.url, token, + cpJwt, cpUrl, bucket, + }), + ); } const server = diff --git a/src/upstream.ts b/src/upstream.ts index 1fb0c78..e89c6f6 100644 --- a/src/upstream.ts +++ b/src/upstream.ts @@ -12,6 +12,18 @@ export interface UpstreamConfig { name: string; url: string; token: string | null; + /** Forwarded to the upstream as ``params._meta.cp_jwt`` on tools/call. + * Lets the upstream agent act on the caller's behalf (file/agent CRUD + * on /v1/me/*) — same path the platform orchestrator uses. */ + cpJwt?: string | null; + /** Paired with ``cpJwt``. The upstream agent uses this as the base URL + * when calling back into /v1/me/*. */ + cpUrl?: string | null; + /** User's MinIO bucket (``user--files``). Forwarded as + * ``params._meta.bucket`` so agents that touch the workspace + * (agent-builder, file tools) get a context whose + * ``ctx.workspace.bucket`` resolves to the right bucket. */ + bucket?: string | null; } export interface UpstreamTool { @@ -88,9 +100,12 @@ export class UpstreamAgent { name: string, arguments_: Record, ): Promise { - return this.rpc("tools/call", { - name, - arguments: arguments_, - }); + const meta: Record = {}; + if (this.cfg.cpJwt) meta.cp_jwt = this.cfg.cpJwt; + if (this.cfg.cpUrl) meta.cp_url = this.cfg.cpUrl; + if (this.cfg.bucket) meta.bucket = this.cfg.bucket; + const params: Record = { name, arguments: arguments_ }; + if (Object.keys(meta).length > 0) params._meta = meta; + return this.rpc("tools/call", params); } }