From 3d6dc62957cd30919413b72a9b28803c1d60f2a0 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 6 Jul 2026 08:00:07 -0400 Subject: [PATCH] Tighten approval receipt scoping --- docs/index.html | 60 +++++++++++++++++++ .../local-api-hardening-static.test.ts | 13 +++- src/server.ts | 37 ++++++++++-- 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/docs/index.html b/docs/index.html index 5abcbc9..98c159c 100755 --- a/docs/index.html +++ b/docs/index.html @@ -3666,6 +3666,10 @@
Waiting for gated tool calls…
+
+
Scope Receipts0 pending
+
No pending scope receipts.
+
@@ -5916,12 +5920,18 @@ tempest.operators.spawn('scan this._eventSource.addEventListener('connected', () => { console.log('[BackendDispatch] SSE connected'); try { fetchToolApprovals(); } catch {} + try { fetchScopeReceipts(); } catch {} }); // Capability-approval gate: every gated tool decision (allow/deny) + spicy warning. this._eventSource.addEventListener('arsenal.approval', (e) => { try { pushApprovalAuditEvent(JSON.parse(e.data)); } catch {} }); + ['approval.requested', 'approval.approved', 'approval.rejected'].forEach((eventName) => { + this._eventSource.addEventListener(eventName, () => { + try { fetchScopeReceipts(); } catch {} + }); + }); this._eventSource.addEventListener('operator:spawned', (e) => { try { @@ -24399,6 +24409,55 @@ Be specific and actionable. Format as structured JSON.`; if (data.audit.length) { feed.dataset.live = '1'; feed.innerHTML = ''; data.audit.slice(-40).forEach(rec => pushApprovalAuditEvent(rec, true)); } } } + async function fetchScopeReceipts() { + try { + if (!T3MP3ST_API.baseUrl) return; + const r = await fetch(`${T3MP3ST_API.baseUrl}/api/approvals`); + if (!r.ok) return; + const data = await r.json(); + renderScopeReceipts((data && data.approvals) || []); + } catch (e) { /* offline — panel stays in its last known state */ } + } + function renderScopeReceipts(approvals) { + const feed = document.getElementById('scopeReceiptFeed'); + const count = document.getElementById('scopeReceiptCount'); + if (!feed) return; + const pending = (approvals || []).filter(a => a && a.status === 'pending'); + const recent = (approvals || []).slice().sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || ''))).slice(0, 12); + if (count) count.textContent = `${pending.length} pending`; + if (!recent.length) { + feed.innerHTML = 'No scope receipts yet.'; + return; + } + feed.innerHTML = recent.map(a => { + const status = String(a.status || 'pending'); + const color = status === 'approved' ? '#00ff88' : status === 'rejected' ? '#ff6b6b' : status === 'expired' ? '#888' : '#ffaa00'; + const expires = a.expiresAt ? ` · expires ${escapeHtml(new Date(a.expiresAt).toLocaleTimeString())}` : ''; + const controls = status === 'pending' + ? `` + : ''; + return `
${escapeHtml(status.toUpperCase())} ${escapeHtml(a.action || '')} → ${escapeHtml(a.target || '')}
${escapeHtml(a.id || '')}${expires}${controls}
`; + }).join(''); + setOpsReady('approval', pending.length ? 'block' : 'ok', pending.length ? `${pending.length} pending` : 'receipts ok'); + } + window.approveScopeReceipt = async function(id) { + try { + await T3MP3ST_API.post(`/api/approvals/${id}/approve`, { approvedBy: 'local-operator' }); + try { addIntel('SCOPEGUARD', `Approved receipt ${id}`, 'success'); } catch (e) {} + await fetchScopeReceipts(); + } catch (e) { + try { toast('Receipt approval failed', 'error'); } catch (err) {} + } + }; + window.rejectScopeReceipt = async function(id) { + try { + await T3MP3ST_API.post(`/api/approvals/${id}/reject`, {}); + try { addIntel('SCOPEGUARD', `Rejected receipt ${id}`, 'warning'); } catch (e) {} + await fetchScopeReceipts(); + } catch (e) { + try { toast('Receipt rejection failed', 'error'); } catch (err) {} + } + }; function pushApprovalAuditEvent(rec, silent) { const feed = document.getElementById('approvalAuditFeed'); if (!feed || !rec) return; @@ -24442,6 +24501,7 @@ Be specific and actionable. Format as structured JSON.`; try { addMissionLog('info', `Approval receipt requested: ${approval.id}`); } catch (e) {} try { addIntel('SCOPEGUARD', `Receipt ${approval.id} is ${approval.status}`, 'info'); } catch (e) {} try { toast(`Receipt requested: ${approval.id}`, 'success'); } catch (e) {} + try { fetchScopeReceipts(); } catch (e) {} } catch (e) { setOpsReady('approval', 'block', 'request failed'); try { addMissionLog('warning', `Approval receipt failed: ${e.message || e}`); } catch (err) {} diff --git a/src/__tests__/local-api-hardening-static.test.ts b/src/__tests__/local-api-hardening-static.test.ts index 4749899..4e190d8 100644 --- a/src/__tests__/local-api-hardening-static.test.ts +++ b/src/__tests__/local-api-hardening-static.test.ts @@ -30,7 +30,7 @@ describe('local API authorization hardening invariants', () => { expect(route).toMatch(/guardAction\(body,\s*['"]command_execution['"],\s*targetResolution\.target/); }); - it('Admiral live launch re-checks every General-produced execution target before mission bring-up', () => { + it('Admiral live launch permits expanded targets only with explicit operator approval receipts', () => { const route = routeBlock("app.post('/api/admiral/launch'", '// =============================================================================\n// BOUNTY PLATFORM INTEGRATIONS'); expect(route).toMatch(/ensureExecTargetsAuthorized\(execConfig\.targets,\s*brief\.target,\s*req\.body/); @@ -38,5 +38,16 @@ describe('local API authorization hardening invariants', () => { expect(route.indexOf('ensureExecTargetsAuthorized(execConfig.targets, brief.target')) .toBeLessThan(route.indexOf('bringUpMissionFromPlan(execConfig, generalConfig)')); expect(route).toMatch(/approvals/); + expect(route).toMatch(/approvalIds/); + }); + + it('approval lookup requires explicit receipt ids and wildcard hosts require opt-in', () => { + const findApproval = routeBlock('function findApproval(', 'function createApprovalRequest'); + + expect(findApproval).toMatch(/body\.approvalId/); + expect(findApproval).toMatch(/body\.approvalIds/); + expect(findApproval).not.toMatch(/\[\.\.\.approvalRequests\.values\(\)\]/); + expect(serverSource).toMatch(/Wildcard host approval requires explicit opt-in/); + expect(serverSource).toMatch(/body\.allowWildcard !== true/); }); }); diff --git a/src/server.ts b/src/server.ts index c8701e5..bbc35be 100755 --- a/src/server.ts +++ b/src/server.ts @@ -507,6 +507,7 @@ interface ApprovalRequest { target: string; reason: string; status: ApprovalStatus; + wildcardOptIn?: boolean; operationId?: string; requestedBy: DraftSource | 'system'; createdAt: string; @@ -1091,10 +1092,23 @@ function approvalIsFresh(approval: ApprovalRequest): boolean { return Date.parse(approval.expiresAt) > Date.now(); } +function targetUsesWildcard(target: string): boolean { + const raw = normalizeTargetValue(target).toLowerCase(); + if (raw === '*') return true; + const withoutScheme = raw.replace(/^[a-z][a-z0-9+.-]*:\/\//, ''); + return withoutScheme.startsWith('*.') || hostFromTarget(raw).startsWith('*.'); +} + +function wildcardHostFromTarget(target: string): string { + const raw = normalizeTargetValue(target).toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, ''); + return raw.replace(/\/.*$/, '').replace(/:\d+$/, ''); +} + function approvalMatches(approval: ApprovalRequest, action: GuardAction, target: string): boolean { if (approval.action !== action) return false; + if (targetUsesWildcard(approval.target) && !approval.wildcardOptIn) return false; if (approval.target === '*') return action === 'model_call' || action === 'autonomous_execution'; - const approvalHost = hostFromTarget(approval.target); + const approvalHost = targetUsesWildcard(approval.target) ? wildcardHostFromTarget(approval.target) : hostFromTarget(approval.target); const targetHost = hostFromTarget(target); if (approvalHost.startsWith('*.')) { const suffix = approvalHost.slice(1); @@ -1136,9 +1150,7 @@ function findApproval(body: Record, action: GuardAction, target if (approval && approvalIsFresh(approval) && approvalMatches(approval, action, target)) return approval; } - return [...approvalRequests.values()] - .filter(approval => approvalIsFresh(approval) && approvalMatches(approval, action, target)) - .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))[0] || null; + return null; } function createApprovalRequest(action: GuardAction, target: string, reason: string, body: Record): ApprovalRequest { @@ -1149,6 +1161,7 @@ function createApprovalRequest(action: GuardAction, target: string, reason: stri target, reason, status: 'pending', + wildcardOptIn: body.allowWildcard === true && targetUsesWildcard(target), operationId: typeof operationDraft?.operation_id === 'string' ? operationDraft.operation_id : undefined, requestedBy: ['human', 'agent', 't3mp3st'].includes(String(body.source)) ? body.source as DraftSource : 'system', createdAt: nowIso(), @@ -4826,6 +4839,15 @@ app.post('/api/approvals/request', (req: Request, res: Response) => { }); return; } + if (targetUsesWildcard(target) && target !== '*' && body.allowWildcard !== true) { + res.status(400).json({ + error: 'Wildcard host approval requires explicit opt-in', + action, + target, + next: 'Send allowWildcard:true only when the operator intentionally approves the whole subdomain wildcard.', + }); + return; + } const reason = typeof body.reason === 'string' && body.reason.trim() ? body.reason.trim() : `Approval requested for ${action} against ${target}`; @@ -4863,6 +4885,13 @@ app.post('/api/approvals/authorize-target', (req: Request, res: Response) => { }); return; } + if (targetUsesWildcard(target) && target !== '*' && body.allowWildcard !== true) { + res.status(400).json({ + error: 'Wildcard host authorization requires explicit opt-in', + next: 'Send allowWildcard:true only when the operator intentionally approves the whole subdomain wildcard.', + }); + return; + } const requestedActions = Array.isArray(body.actions) ? body.actions.map(String) : []; const allowedActions: GuardAction[] = ['mission_execution', 'network_request', 'command_execution', 'autonomous_execution'];