fix: Cloud Run service-to-service auth and agent card URL

- Add identity token auth in A2ABridgeClient for Cloud Run (K_SERVICE)
- Support CODER_AGENT_PUBLIC_URL env var for agent card URL on Cloud Run
- Strip @Bot mention prefix before slash command detection (Add-ons)
- Grant bridge SA roles/run.invoker on A2A server via IAM
This commit is contained in:
Adam Weidman
2026-02-15 00:22:08 -07:00
parent 6872805274
commit 45c9545f78
3 changed files with 33 additions and 5 deletions
@@ -27,6 +27,7 @@ import {
RestTransportFactory,
JsonRpcTransportFactory,
} from '@a2a-js/sdk/client';
import { GoogleAuth } from 'google-auth-library';
import { v4 as uuidv4 } from 'uuid';
import { logger } from '../utils/logger.js';
@@ -162,17 +163,37 @@ export class A2ABridgeClient {
/**
* Initializes the client connection to the A2A server.
* On Cloud Run (K_SERVICE is set), wraps fetch with an identity token
* for service-to-service authentication.
*/
async initialize(): Promise<void> {
if (this.client) return;
const resolver = new DefaultAgentCardResolver({});
// On Cloud Run, create an authenticated fetch that adds identity tokens
let fetchImpl: typeof fetch = fetch;
if (process.env['K_SERVICE']) {
const auth = new GoogleAuth();
const idTokenClient = await auth.getIdTokenClient(this.agentUrl);
fetchImpl = async (input, init?) => {
const authHeaders = await idTokenClient.getRequestHeaders();
const merged = new Headers(init?.headers);
for (const [key, value] of Object.entries(authHeaders)) {
merged.set(key, value);
}
return fetch(input, { ...init, headers: merged });
};
logger.info(
'[ChatBridge] Using Cloud Run identity token for A2A server auth',
);
}
const resolver = new DefaultAgentCardResolver({ fetchImpl });
const options = ClientFactoryOptions.createFrom(
ClientFactoryOptions.default,
{
transports: [
new RestTransportFactory({}),
new JsonRpcTransportFactory({}),
new RestTransportFactory({ fetchImpl }),
new JsonRpcTransportFactory({ fetchImpl }),
],
cardResolver: resolver,
},
@@ -100,7 +100,10 @@ export class ChatBridgeHandler {
return { text: 'Error: Missing thread information.' };
}
const text = message.argumentText || message.text || '';
// argumentText has bot mentions stripped (legacy format only).
// For Add-ons format, strip leading @mention manually.
const rawText = message.argumentText || message.text || '';
const text = rawText.replace(/^@\S+\s*/, '');
if (!text.trim()) {
return { text: "I didn't receive any text. Please try again." };
}
+5 -1
View File
@@ -77,7 +77,11 @@ const coderAgentCard: AgentCard = {
};
export function updateCoderAgentCardUrl(port: number) {
coderAgentCard.url = `http://localhost:${port}/`;
// On Cloud Run, use the public service URL so remote clients can reach us
const publicUrl = process.env['CODER_AGENT_PUBLIC_URL'];
coderAgentCard.url = publicUrl
? publicUrl.replace(/\/$/, '') + '/'
: `http://localhost:${port}/`;
}
async function handleExecuteCommand(