Tighten approval receipt scoping

This commit is contained in:
Codex committed 2026-07-12 09:50:43 -04:00
1 parent 06e1edb4b8
commit 3d6dc62957
3 files changed
+105 -5

No files matched your search

+60
View File
@@ -3666,6 +3666,10 @@
<div id="spicyWarnStrip" style="display:none;padding:7px 9px;background:rgba(255,68,68,0.10);border-left:3px solid #ff4444;border-radius:4px;margin-bottom:8px;font-size:11px;color:#ff8a8a;"></div>
<div id="approvalAuditFeed" style="max-height:180px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:10px;line-height:1.55;"><span style="color:#5a5f67;font-size:10.5px;">Waiting for gated tool calls…</span></div>
</section>
<section class="hunt-panel">
<div class="ops-card-title"><span>Scope Receipts</span><span id="scopeReceiptCount">0 pending</span></div>
<div id="scopeReceiptFeed" style="max-height:180px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:10px;line-height:1.55;"><span style="color:#5a5f67;font-size:10.5px;">No pending scope receipts.</span></div>
</section>
</div>
<div class="hunt-pulse-board" id="zeroDayHuntPulse">
<section class="hunt-panel">
@@ -5916,12 +5920,18 @@ tempest.operators.<span class="function">spawn</span>(<span class="string">'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 = '<span style="color:#5a5f67;font-size:10.5px;">No scope receipts yet.</span>';
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'
? `<button onclick="approveScopeReceipt('${escapeHtml(a.id)}')" style="margin-left:6px;padding:2px 6px;border:1px solid rgba(0,255,136,0.4);background:rgba(0,255,136,0.08);color:#00ff88;border-radius:4px;font-size:10px;">Approve</button><button onclick="rejectScopeReceipt('${escapeHtml(a.id)}')" style="margin-left:4px;padding:2px 6px;border:1px solid rgba(255,107,107,0.4);background:rgba(255,107,107,0.08);color:#ff6b6b;border-radius:4px;font-size:10px;">Reject</button>`
: '';
return `<div style="padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.05);color:${color};"><b>${escapeHtml(status.toUpperCase())}</b> <span style="color:#aaa;">${escapeHtml(a.action || '')}</span> → ${escapeHtml(a.target || '')}<br><span style="color:#666;">${escapeHtml(a.id || '')}${expires}</span>${controls}</div>`;
}).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) {}
@@ -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/);
});
});
+33 -4
View File
@@ -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<string, unknown>, 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<string, unknown>): 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'];