From 6fef62aa8e457425dabefc9312bbb632f065de63 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 12 Jul 2026 09:50:34 -0400 Subject: [PATCH] Narrow authorize-target to pending approvals --- .../local-api-hardening-static.test.ts | 13 ++++ src/server.ts | 59 +++++++++++-------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/__tests__/local-api-hardening-static.test.ts b/src/__tests__/local-api-hardening-static.test.ts index 4e190d8..64d7ce1 100644 --- a/src/__tests__/local-api-hardening-static.test.ts +++ b/src/__tests__/local-api-hardening-static.test.ts @@ -50,4 +50,17 @@ describe('local API authorization hardening invariants', () => { expect(serverSource).toMatch(/Wildcard host approval requires explicit opt-in/); expect(serverSource).toMatch(/body\.allowWildcard !== true/); }); + + it('authorize-target only approves existing pending receipts and cannot mint broad active-action approvals', () => { + const route = routeBlock("app.post('/api/approvals/authorize-target'", "app.post('/api/approvals/:id/reject'"); + + expect(route).toMatch(/approvalId/); + expect(route).toMatch(/approvalIds/); + expect(route).toMatch(/approvalRequests\.get\(id\)/); + expect(route).toMatch(/approval\.status !== 'pending'/); + expect(route).toMatch(/Math\.min\(30/); + expect(route).not.toMatch(/createApprovalRequest/); + expect(route).not.toMatch(/allowedActions/); + expect(route).not.toMatch(/command_execution['"],\s*['"]autonomous_execution/); + }); }); diff --git a/src/server.ts b/src/server.ts index bbc35be..9e33113 100755 --- a/src/server.ts +++ b/src/server.ts @@ -4874,8 +4874,15 @@ app.post('/api/approvals/:id/approve', (req: Request, res: Response) => { app.post('/api/approvals/authorize-target', (req: Request, res: Response) => { const body = req.body as Record; const target = normalizeTargetValue(body.target); - if (!target) { - res.status(400).json({ error: 'target required' }); + const approvalIds = [ + typeof body.approvalId === 'string' ? body.approvalId : '', + ...(Array.isArray(body.approvalIds) ? body.approvalIds.filter((id): id is string => typeof id === 'string') : []), + ].filter(Boolean); + if (!approvalIds.length) { + res.status(400).json({ + error: 'approvalId or approvalIds required', + next: 'Request a pending receipt first, then approve that exact receipt id.', + }); return; } if (target === '*') { @@ -4893,35 +4900,37 @@ app.post('/api/approvals/authorize-target', (req: Request, res: Response) => { return; } - const requestedActions = Array.isArray(body.actions) ? body.actions.map(String) : []; - const allowedActions: GuardAction[] = ['mission_execution', 'network_request', 'command_execution', 'autonomous_execution']; - const actions = (requestedActions.length ? requestedActions : allowedActions) - .filter((action): action is GuardAction => allowedActions.includes(action as GuardAction)); - if (!actions.length) { - res.status(400).json({ error: 'No supported approval actions requested' }); - return; - } - - const ttlMinutes = Number(body.ttlMinutes || process.env.T3MP3ST_APPROVAL_TTL_MINUTES || 240); - const expiresAt = new Date(Date.now() + Math.max(1, ttlMinutes) * 60_000).toISOString(); - const approvals = actions.map((action) => { - const approval = createApprovalRequest( - action, - target, - typeof body.reason === 'string' && body.reason.trim() - ? body.reason.trim() - : `Authorize ${action} against ${target}`, - body - ); + const ttlMinutes = Math.max(1, Math.min(30, Number(body.ttlMinutes || 30))); + const expiresAt = new Date(Date.now() + ttlMinutes * 60_000).toISOString(); + const approvals: ApprovalRequest[] = []; + for (const id of [...new Set(approvalIds)]) { + const approval = approvalRequests.get(id); + if (!approval) { + res.status(404).json({ error: 'Approval request not found', approvalId: id }); + return; + } + if (approval.status !== 'pending') { + res.status(409).json({ error: 'Approval request is not pending', approvalId: id, status: approval.status }); + return; + } + if (target && !approvalMatches(approval, approval.action, target)) { + res.status(400).json({ + error: 'Approval target mismatch', + approvalId: id, + approvalTarget: approval.target, + target, + }); + return; + } approval.status = 'approved'; approval.approvedBy = typeof body.approvedBy === 'string' ? body.approvedBy : 'local-operator'; approval.expiresAt = expiresAt; approval.updatedAt = nowIso(); emitContractEvent('approval.approved', { approvalId: approval.id, action: approval.action, target: approval.target }); - return approval; - }); + approvals.push(approval); + } - res.status(201).json({ target, expiresAt, approvals }); + res.json({ target: target || null, expiresAt, approvals }); }); app.post('/api/approvals/:id/reject', (req: Request, res: Response) => {