From b6364a3f8ab14735c36322efa5cd36af3c21e163 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:46:55 -0700 Subject: [PATCH 1/3] Log refused agent installs and machine placements Co-authored-by: flint Agent-Model: codex/gpt-6-sol/medium --- .../middleware/logAgentInstallRefusal.test.js | 37 +++++++++++++ .../registry.install-reserved-names.test.js | 34 +++++++++++- backend/middleware/logAgentInstallRefusal.ts | 52 +++++++++++++++++++ backend/routes/agentBinding.ts | 3 +- backend/routes/registry/install.ts | 4 +- 5 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js create mode 100644 backend/middleware/logAgentInstallRefusal.ts diff --git a/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js b/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js new file mode 100644 index 0000000000..fdfb44957b --- /dev/null +++ b/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js @@ -0,0 +1,37 @@ +const { logAgentInstallRefusal } = require('../../../middleware/logAgentInstallRefusal'); + +describe('agent install refusal logging', () => { + let warn; + + beforeEach(() => { warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); }); + afterEach(() => { warn.mockRestore(); }); + + test('machine placement refusal records wrapper and omits credentials', () => { + const req = { + body: { agentName: 'my-agent', instanceId: 'default', machineId: 'machine-1', token: 'secret' }, + user: { id: 'owner-1' }, + }; + const res = { + statusCode: 200, + locals: {}, + json: jest.fn(), + }; + const next = jest.fn(); + logAgentInstallRefusal('/api/agent-binding/request', 'wrapper')(req, res, next); + expect(next).toHaveBeenCalledTimes(1); + res.statusCode = 404; + res.json({ message: 'Agent identity not found' }); + expect(warn).toHaveBeenCalledTimes(1); + const line = JSON.parse(warn.mock.calls[0][1]); + expect(line).toEqual({ + route: '/api/agent-binding/request', + agentName: 'my-agent', + instanceId: 'default', + runtimeType: 'wrapper', + reason: 'agent_identity_not_found', + userId: 'owner-1', + }); + expect(warn.mock.calls[0].join(' ')).not.toContain('secret'); + expect(warn.mock.calls[0].join(' ')).not.toContain('machine-1'); + }); +}); diff --git a/backend/__tests__/unit/routes/registry.install-reserved-names.test.js b/backend/__tests__/unit/routes/registry.install-reserved-names.test.js index 4eaadc619c..0575336b06 100644 --- a/backend/__tests__/unit/routes/registry.install-reserved-names.test.js +++ b/backend/__tests__/unit/routes/registry.install-reserved-names.test.js @@ -87,7 +87,12 @@ const buildSelectLeanChain = (result) => ({ select: jest.fn().mockReturnValue({ lean: jest.fn().mockResolvedValue(result) }), }); -const makeRes = () => ({ status: jest.fn().mockReturnThis(), json: jest.fn() }); +const makeRes = () => ({ + statusCode: 200, + locals: {}, + status: jest.fn(function status(code) { this.statusCode = code; return this; }), + json: jest.fn(), +}); const officialRow = (agentName) => ({ @@ -99,7 +104,7 @@ const officialRow = (agentName) => ({ manifest: { context: { required: [] }, runtime: { type: 'standalone' } }, }); -const installAs = async ({ role = 'user', agentName, runtimeType }) => { +const installAs = async ({ role = 'user', agentName, runtimeType, recordRefusal = false }) => { User.findById.mockReturnValue(buildSelectLeanChain({ username: 'installer', role, entitlements: { cloudAgents: true } })); const req = { body: { @@ -113,6 +118,10 @@ const installAs = async ({ role = 'user', agentName, runtimeType }) => { userId: 'user-1', }; const res = makeRes(); + if (recordRefusal) { + const layer = installRouter.stack.find((entry) => entry.route?.path === '/install'); + layer.route.stack[0].handle(req, res, () => {}); + } await installHandler(req, res); return res; }; @@ -173,4 +182,25 @@ describe('registry install — built-in names are reserved for self-serve instal const res = await installAs({ agentName: 'standup-bot', runtimeType: 'webhook' }); expect(reservedCalls(res)).toHaveLength(0); }); + + test('the registry 404 emits one structured refusal line without credentials', async () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + try { + AgentRegistry.getByName.mockResolvedValue(null); + const res = await installAs({ agentName: 'new-agent', runtimeType: 'unsupported', recordRefusal: true }); + expect(res.statusCode).toBe(404); + const lines = warn.mock.calls.filter(([prefix]) => prefix === '[agent-install-refused]'); + expect(lines).toHaveLength(1); + expect(JSON.parse(lines[0][1])).toEqual({ + route: '/api/registry/install', + agentName: 'new-agent', + instanceId: 'default', + runtimeType: 'unsupported', + reason: 'agent_not_found_in_registry', + userId: 'user-1', + }); + } finally { + warn.mockRestore(); + } + }); }); diff --git a/backend/middleware/logAgentInstallRefusal.ts b/backend/middleware/logAgentInstallRefusal.ts new file mode 100644 index 0000000000..d07c2a124c --- /dev/null +++ b/backend/middleware/logAgentInstallRefusal.ts @@ -0,0 +1,52 @@ +import { NextFunction, Request, Response } from 'express'; + +type RefusalBody = { code?: unknown; error?: unknown; message?: unknown; msg?: unknown }; + +// Only static response text is mapped. Unknown responses retain a bounded HTTP +// code rather than turning user-supplied error text into a log dimension. +const reasonByMessage: Record = { + 'Agent not found in registry': 'agent_not_found_in_registry', + 'Agent identity not found': 'agent_identity_not_found', + 'Machine not found': 'machine_not_found', + 'Pod not found': 'pod_not_found', + Unauthorized: 'unauthorized', + 'agentName required': 'agent_name_required', + 'agentName must be a string': 'agent_name_invalid', + 'machineId required (null to withdraw)': 'machine_id_required', +}; + +const logValue = (value: unknown): string | null => { + if (typeof value !== 'string') return null; + return value.trim().replace(/[^a-zA-Z0-9@/_-]/g, '').slice(0, 120) || null; +}; + +const reasonCode = (body: RefusalBody, status: number): string => { + if (typeof body.code === 'string' && /^[a-z][a-z0-9_]{0,79}$/.test(body.code)) return body.code; + const message = body.error || body.message || body.msg; + return typeof message === 'string' ? reasonByMessage[message] || `http_${status}` : `http_${status}`; +}; + +/** Record one structured line for each refused install or machine placement. */ +export const logAgentInstallRefusal = (route: string, defaultRuntimeType?: string) => ( + req: Request & { user?: { id?: unknown } }, res: Response, next: NextFunction, +): void => { + const originalJson = res.json.bind(res); + let logged = false; + res.json = ((body: RefusalBody) => { + if (!logged && res.statusCode >= 400) { + logged = true; + const requestBody = req.body && typeof req.body === 'object' ? req.body : {}; + const runtime = requestBody.config?.runtime?.runtimeType || requestBody.runtimeType || defaultRuntimeType; + console.warn('[agent-install-refused]', JSON.stringify({ + route, + agentName: logValue(requestBody.agentName), + instanceId: logValue(res.locals.agentInstallInstanceId || requestBody.instanceId) || 'default', + runtimeType: logValue(runtime), + reason: reasonCode(body && typeof body === 'object' ? body : {}, res.statusCode), + userId: req.user?.id == null ? null : String(req.user.id), + })); + } + return originalJson(body); + }) as Response['json']; + next(); +}; diff --git a/backend/routes/agentBinding.ts b/backend/routes/agentBinding.ts index f7ecfa6aa0..9c5bbe08e8 100644 --- a/backend/routes/agentBinding.ts +++ b/backend/routes/agentBinding.ts @@ -20,6 +20,7 @@ import { seatEnvironmentKey, } from '../services/seatEnvironmentProjection'; import { ownsRuntimeIdentity, OwnershipFailure } from '../services/runtimeIdentityOwnership'; +import { logAgentInstallRefusal } from '../middleware/logAgentInstallRefusal'; // eslint-disable-next-line @typescript-eslint/no-require-imports const auth = require('../middleware/auth'); @@ -201,7 +202,7 @@ router.post('/adopt', bindingRateLimit, daemonAuth('agents:adopt'), async (req: // machine sees the request in /assigned and performs the adopt CAS itself, // so D3's "no adoption without a user choice" and "one binding writer" both // hold. machineId: null withdraws the request. -router.post('/request', bindingRateLimit, auth, async (req: express.Request & { user?: { id?: string } }, res: express.Response) => { +router.post('/request', logAgentInstallRefusal('/api/agent-binding/request', 'wrapper'), bindingRateLimit, auth, async (req: express.Request & { user?: { id?: string } }, res: express.Response) => { try { const agentName = normalize(req.body?.agentName); const instanceId = normalize(req.body?.instanceId) || 'default'; diff --git a/backend/routes/registry/install.ts b/backend/routes/registry/install.ts index 401143250f..6772f80a44 100644 --- a/backend/routes/registry/install.ts +++ b/backend/routes/registry/install.ts @@ -49,6 +49,7 @@ const { const { validateEnvironmentMcpEntries } = require('../../utils/environmentSpecValidation'); const { willSolelyInstallRuntimeIdentity, webhookUrlRefusal } = require('../../services/runtimeIdentityOwnership'); const { personAgentWakeDefault } = require('../../services/wakeDefaultService'); +const { logAgentInstallRefusal } = require('../../middleware/logAgentInstallRefusal'); const { assertAgentRuntimeMayJoinPod } = require('../../services/outsiderGrantAgentJoinPolicy'); // Inlined per-route limiter. This comment used to attribute its clean CodeQL @@ -151,7 +152,7 @@ const findExistingAgentInstance = async (agentName: any, instanceId: any) => { * POST /api/registry/install * Install an agent to a pod */ -installRouter.post('/install', installRateLimit, auth, async (req: any, res: any) => { +installRouter.post('/install', logAgentInstallRefusal('/api/registry/install'), installRateLimit, auth, async (req: any, res: any) => { try { const { agentName, podId, version, config = {}, scopes = [], instanceId, displayName, gatewayId, description, @@ -245,6 +246,7 @@ installRouter.post('/install', installRateLimit, auth, async (req: any, res: any } else { normalizedInstanceId = deriveInstanceId(displayName, agentName); } + res.locals.agentInstallInstanceId = normalizedInstanceId; let agent = await AgentRegistry.getByName(agentName); From 3f13948cc402f7a7ccc79f97fe046bfa23efedbc Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:50:30 -0700 Subject: [PATCH 2/3] Keep direct route test responses compatible with install logging Co-authored-by: flint Agent-Model: codex/gpt-6-sol/medium --- backend/routes/registry/install.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/routes/registry/install.ts b/backend/routes/registry/install.ts index 6772f80a44..24c07439c3 100644 --- a/backend/routes/registry/install.ts +++ b/backend/routes/registry/install.ts @@ -246,7 +246,7 @@ installRouter.post('/install', logAgentInstallRefusal('/api/registry/install'), } else { normalizedInstanceId = deriveInstanceId(displayName, agentName); } - res.locals.agentInstallInstanceId = normalizedInstanceId; + if (res.locals) res.locals.agentInstallInstanceId = normalizedInstanceId; let agent = await AgentRegistry.getByName(agentName); From a47d515279ccae764917a0508098d398d0145ae8 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:07:16 -0700 Subject: [PATCH 3/3] Redact credential-shaped install log fields Co-authored-by: flint Agent-Model: codex/gpt-6-sol/medium --- .../middleware/logAgentInstallRefusal.test.js | 26 ++++++++++++++ .../registry.install-reserved-names.test.js | 2 +- backend/middleware/logAgentInstallRefusal.ts | 34 +++++++++++++++---- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js b/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js index fdfb44957b..b08e247efc 100644 --- a/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js +++ b/backend/__tests__/unit/middleware/logAgentInstallRefusal.test.js @@ -34,4 +34,30 @@ describe('agent install refusal logging', () => { expect(warn.mock.calls[0].join(' ')).not.toContain('secret'); expect(warn.mock.calls[0].join(' ')).not.toContain('machine-1'); }); + + test('credential-shaped values in logged fields are redacted before serialization', () => { + const credential = 'cm_agent_example123'; + const req = { + body: { + agentName: credential, + instanceId: credential, + config: { runtime: { runtimeType: credential } }, + }, + user: { id: 'owner-1' }, + }; + const res = { statusCode: 404, locals: {}, json: jest.fn() }; + logAgentInstallRefusal('/api/registry/install')(req, res, () => {}); + res.json({ error: 'Agent not found in registry' }); + expect(warn).toHaveBeenCalledTimes(1); + const fullLine = warn.mock.calls[0].join(' '); + expect(fullLine).not.toContain(credential); + expect(JSON.parse(warn.mock.calls[0][1])).toEqual({ + route: '/api/registry/install', + agentName: '[redacted]', + instanceId: '[redacted]', + runtimeType: 'unknown', + reason: 'agent_not_found_in_registry', + userId: 'owner-1', + }); + }); }); diff --git a/backend/__tests__/unit/routes/registry.install-reserved-names.test.js b/backend/__tests__/unit/routes/registry.install-reserved-names.test.js index 0575336b06..e94a328a91 100644 --- a/backend/__tests__/unit/routes/registry.install-reserved-names.test.js +++ b/backend/__tests__/unit/routes/registry.install-reserved-names.test.js @@ -195,7 +195,7 @@ describe('registry install — built-in names are reserved for self-serve instal route: '/api/registry/install', agentName: 'new-agent', instanceId: 'default', - runtimeType: 'unsupported', + runtimeType: 'unknown', reason: 'agent_not_found_in_registry', userId: 'user-1', }); diff --git a/backend/middleware/logAgentInstallRefusal.ts b/backend/middleware/logAgentInstallRefusal.ts index d07c2a124c..0658eb7ba4 100644 --- a/backend/middleware/logAgentInstallRefusal.ts +++ b/backend/middleware/logAgentInstallRefusal.ts @@ -15,13 +15,35 @@ const reasonByMessage: Record = { 'machineId required (null to withdraw)': 'machine_id_required', }; -const logValue = (value: unknown): string | null => { +const runtimeTypes = new Set([ + 'webhook', 'wrapper', 'hosted', 'native', 'moltbot', 'internal', + 'managed-agents', 'claude-code', 'codex', +]); + +const credentialPrefix = /^(?:cm[_-]agent[_-]|gh[opurs][_-]|github[_-]pat[_-]|sk-|xox[abprs]-|bearer|eyj)/i; + +const logIdentity = (value: unknown, allowScopedName = false): string | null => { + if (typeof value !== 'string') return null; + const name = value.trim().toLowerCase(); + const valid = allowScopedName + ? /^(@[a-z0-9-]+\/)?[a-z0-9-]+$/.test(name) + : /^[a-z0-9-]+$/.test(name); + // Refused requests can supply credentials in place of names. Never "clean" + // an invalid value into something that resembles a safe identity. + if (!valid || name.length > 64 || credentialPrefix.test(name)) return '[redacted]'; + return name || null; +}; + +const logRuntimeType = (value: unknown): string | null => { if (typeof value !== 'string') return null; - return value.trim().replace(/[^a-zA-Z0-9@/_-]/g, '').slice(0, 120) || null; + const runtimeType = value.trim().toLowerCase(); + return runtimeTypes.has(runtimeType) ? runtimeType : 'unknown'; }; const reasonCode = (body: RefusalBody, status: number): string => { - if (typeof body.code === 'string' && /^[a-z][a-z0-9_]{0,79}$/.test(body.code)) return body.code; + if (typeof body.code === 'string' + && /^[a-z][a-z0-9_]{0,79}$/.test(body.code) + && !credentialPrefix.test(body.code)) return body.code; const message = body.error || body.message || body.msg; return typeof message === 'string' ? reasonByMessage[message] || `http_${status}` : `http_${status}`; }; @@ -39,9 +61,9 @@ export const logAgentInstallRefusal = (route: string, defaultRuntimeType?: strin const runtime = requestBody.config?.runtime?.runtimeType || requestBody.runtimeType || defaultRuntimeType; console.warn('[agent-install-refused]', JSON.stringify({ route, - agentName: logValue(requestBody.agentName), - instanceId: logValue(res.locals.agentInstallInstanceId || requestBody.instanceId) || 'default', - runtimeType: logValue(runtime), + agentName: logIdentity(requestBody.agentName, true), + instanceId: logIdentity(res.locals.agentInstallInstanceId || requestBody.instanceId) || 'default', + runtimeType: logRuntimeType(runtime), reason: reasonCode(body && typeof body === 'object' ? body : {}, res.statusCode), userId: req.user?.id == null ? null : String(req.user.id), }));