From f2ec1e9437b1161292b568e06f62e3b3c93c018f Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 12:18:28 -0400 Subject: [PATCH 1/6] Reuse target-scoped approval receipts --- src/server.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/server.ts b/src/server.ts index 01b5fb2..ac94b07 100755 --- a/src/server.ts +++ b/src/server.ts @@ -1111,10 +1111,19 @@ function approvalMatchesGateScope(approval: ApprovalRequest, action: GuardAction } function findApproval(body: Record, action: GuardAction, target: string): ApprovalRequest | null { - const id = typeof body.approvalId === 'string' ? body.approvalId : ''; - const approval = id ? approvalRequests.get(id) : undefined; - if (!approval || !approvalIsFresh(approval) || !approvalMatches(approval, action, target)) return null; - return approval; + const ids = [ + typeof body.approvalId === 'string' ? body.approvalId : '', + ...(Array.isArray(body.approvalIds) ? body.approvalIds.filter((id): id is string => typeof id === 'string') : []), + ].filter(Boolean); + + for (const id of ids) { + const approval = approvalRequests.get(id); + 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; } function createApprovalRequest(action: GuardAction, target: string, reason: string, body: Record): ApprovalRequest { @@ -4825,6 +4834,52 @@ app.post('/api/approvals/:id/approve', (req: Request, res: Response) => { res.json(approval); }); +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' }); + return; + } + if (target === '*') { + res.status(400).json({ + error: 'Wildcard target authorization is not allowed for active actions', + next: 'Authorize one concrete host or URL so receipts remain target-scoped.', + }); + 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 + ); + 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; + }); + + res.status(201).json({ target, expiresAt, approvals }); +}); + app.post('/api/approvals/:id/reject', (req: Request, res: Response) => { const approval = approvalRequests.get(req.params.id); if (!approval) { @@ -6001,6 +6056,14 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise Date: Sun, 5 Jul 2026 18:18:18 -0400 Subject: [PATCH 2/6] Ask approval for Admiral expanded scope --- docs/index.html | 13 ++++++++++++ .../local-api-hardening-static.test.ts | 5 +++-- src/server.ts | 21 +++++++++++++++++-- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/docs/index.html b/docs/index.html index 8a06e30..78ebfdd 100755 --- a/docs/index.html +++ b/docs/index.html @@ -25169,6 +25169,19 @@ Be specific and actionable. Format as structured JSON.`; throw new Error('Approval required for ' + tgt + ' — choose Autonomous mode, or approve in the receipts panel.'); } } + if (res.status === 403 && Array.isArray(d.approvals) && d.approvals.length) { + const approvalIds = d.approvals.map(function(a){ return a && a.id; }).filter(Boolean); + const targets = (d.outOfScopeTargets || []).join(', '); + if (isLab || autonomous) { + await Promise.all(approvalIds.map(function(id){ + return fetch('/api/approvals/' + id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' }); + })); + res = await fetch('/api/admiral/launch', launchOpts({ approvalIds: approvalIds })); + d = await res.json(); + } else { + throw new Error('Expanded target approval required for ' + (targets || 'planner-added targets') + ' — approve the receipts or choose Autonomous mode.'); + } + } if (res.status === 409 && d.review && d.review.status === 'hold') throw new Error('Op Admiral plan gate is HOLD — ' + ((d.review && d.review.reason) || 'manual review required before execution')); if (!res.ok || d.error) throw new Error(d.error || ('launch failed (' + res.status + ')')); const nOps = (d.operators || []).length; diff --git a/src/__tests__/local-api-hardening-static.test.ts b/src/__tests__/local-api-hardening-static.test.ts index 3e14770..4749899 100644 --- a/src/__tests__/local-api-hardening-static.test.ts +++ b/src/__tests__/local-api-hardening-static.test.ts @@ -33,9 +33,10 @@ describe('local API authorization hardening invariants', () => { it('Admiral live launch re-checks every General-produced execution target before mission bring-up', () => { const route = routeBlock("app.post('/api/admiral/launch'", '// =============================================================================\n// BOUNTY PLATFORM INTEGRATIONS'); - expect(route).toMatch(/ensureExecTargetsWithinApprovedTarget\(execConfig\.targets, brief\.target\)/); + expect(route).toMatch(/ensureExecTargetsAuthorized\(execConfig\.targets,\s*brief\.target,\s*req\.body/); expect(route).toMatch(/outOfScopeTargets\.length/); - expect(route.indexOf('ensureExecTargetsWithinApprovedTarget(execConfig.targets, brief.target)')) + expect(route.indexOf('ensureExecTargetsAuthorized(execConfig.targets, brief.target')) .toBeLessThan(route.indexOf('bringUpMissionFromPlan(execConfig, generalConfig)')); + expect(route).toMatch(/approvals/); }); }); diff --git a/src/server.ts b/src/server.ts index ac94b07..6a185b7 100755 --- a/src/server.ts +++ b/src/server.ts @@ -1104,6 +1104,15 @@ function ensureExecTargetsWithinApprovedTarget(targets: string[], approvedTarget .filter(target => hostFromTarget(target) !== approvedHost); } +function ensureExecTargetsAuthorized( + targets: string[], + approvedTarget: string, + body: Record, +): string[] { + return ensureExecTargetsWithinApprovedTarget(targets, approvedTarget) + .filter(target => !findApproval(body, 'autonomous_execution', target)); +} + function approvalMatchesGateScope(approval: ApprovalRequest, action: GuardAction, operationId: string, target: string): boolean { if (!approvalMatches(approval, action, target)) return false; if (approval.operationId && operationId && approval.operationId !== operationId) return false; @@ -7326,12 +7335,20 @@ app.post('/api/admiral/launch', async (req: Request, res: Response): Promise); if (outOfScopeTargets.length) { + const approvals = outOfScopeTargets.map(target => createApprovalRequest( + 'autonomous_execution', + target, + `Admiral plan expanded execution scope from ${brief.target} to ${target}`, + req.body as Record + )); res.status(403).json({ - error: 'Admiral LIVE plan contains targets outside the approved brief target', + error: 'Admiral LIVE plan needs approval for expanded target scope', approvedTarget: brief.target, outOfScopeTargets, + approvals, + next: 'Approve the expanded target receipts, then retry /api/admiral/launch with approvalIds.', }); return; } From 964fe5b3bab324b632e1a3dc409b85011bc252c1 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 20:38:26 -0400 Subject: [PATCH 3/6] Add scope receipts approval UI --- docs/index.html | 169 +++++++++++++++++++++++++++++++++++++++++------- src/server.ts | 8 ++- 2 files changed, 152 insertions(+), 25 deletions(-) diff --git a/docs/index.html b/docs/index.html index 78ebfdd..20c06a0 100755 --- a/docs/index.html +++ b/docs/index.html @@ -3305,6 +3305,10 @@ + + +
+
+
+
ScopeGuard
+
Scope Receipts
+
+
+ + +
+
+
+
+
+
Approval Requests
+
0 receipts
+
+
+
+
+
@@ -6907,13 +6933,106 @@ Correlating findings across agents, identifying patterns, producing actionable i })); } + const ReceiptsState = { approvals: [] }; + + function receiptStatusColor(status) { + const s = String(status || '').toLowerCase(); + if (s === 'approved') return '#00ff88'; + if (s === 'pending') return '#ffaa00'; + if (s === 'rejected' || s === 'expired') return '#ff6666'; + return 'var(--text-secondary)'; + } + + window.refreshReceiptsPage = async function refreshReceiptsPage() { + const data = await T3MP3ST_API.get('/api/approvals'); + ReceiptsState.approvals = Array.isArray(data.approvals) ? data.approvals : []; + renderReceiptsPage(); + }; + + window.approveReceipt = async function approveReceipt(id) { + await T3MP3ST_API.post('/api/approvals/' + id + '/approve', {}); + await window.refreshReceiptsPage(); + if (window.toast) toast('Receipt approved: ' + id, 'success'); + }; + + window.rejectReceipt = async function rejectReceipt(id) { + await T3MP3ST_API.post('/api/approvals/' + id + '/reject', {}); + await window.refreshReceiptsPage(); + if (window.toast) toast('Receipt rejected: ' + id, 'warning'); + }; + + window.approveAllPendingReceipts = async function approveAllPendingReceipts() { + const pending = ReceiptsState.approvals.filter(function(a){ return a.status === 'pending'; }); + await Promise.all(pending.map(function(a){ return T3MP3ST_API.post('/api/approvals/' + a.id + '/approve', {}); })); + await window.refreshReceiptsPage(); + if (window.toast) toast('Approved ' + pending.length + ' pending receipt(s)', pending.length ? 'success' : 'info'); + }; + + function renderReceiptsPage() { + const approvals = ReceiptsState.approvals || []; + const pending = approvals.filter(function(a){ return a.status === 'pending'; }).length; + const approved = approvals.filter(function(a){ return a.status === 'approved'; }).length; + const expired = approvals.filter(function(a){ return a.status === 'expired' || a.status === 'rejected'; }).length; + const badge = document.getElementById('pendingReceiptCount'); + if (badge) { + badge.textContent = String(pending); + badge.style.background = pending ? '#ffaa00' : 'rgba(255,255,255,0.12)'; + badge.style.color = pending ? '#1b1200' : ''; + } + const summary = document.getElementById('receiptsSummary'); + if (summary) { + const cells = [ + ['Pending', pending, pending ? '#ffaa00' : 'var(--text-secondary)'], + ['Approved', approved, '#00ff88'], + ['Closed', expired, expired ? '#ff6666' : 'var(--text-secondary)'], + ['Total', approvals.length, '#00aaff'] + ]; + summary.innerHTML = cells.map(function(cell){ + return '
' + + '
' + escapeHtml(cell[0]) + '
' + + '
' + escapeHtml(cell[1]) + '
' + + '
'; + }).join(''); + } + const count = document.getElementById('receiptsCount'); + if (count) count.textContent = approvals.length + ' receipts'; + const list = document.getElementById('receiptsList'); + if (!list) return; + if (!approvals.length) { + list.innerHTML = '
No receipts yet. Live external actions will create target-scoped approval requests here.
'; + return; + } + list.innerHTML = approvals.map(function(a){ + const color = receiptStatusColor(a.status); + const pendingActions = a.status === 'pending' + ? '
' + : ''; + return '
' + + '
' + + '
' + escapeHtml(a.id) + '' + escapeHtml(a.status || 'unknown') + '
' + + '
' + escapeHtml(a.target || '-') + '
' + + '
' + escapeHtml(a.action || '-') + ' · ' + escapeHtml(a.requestedBy || '-') + '
' + + '
' + escapeHtml(a.reason || '') + '
' + + (a.expiresAt ? '
Expires ' + escapeHtml(new Date(a.expiresAt).toLocaleString()) + '
' : '') + + '
' + + pendingActions + + '
'; + }).join(''); + } + + setInterval(() => { + if (document.getElementById('page-receipts')?.classList.contains('active')) { + window.refreshReceiptsPage?.(); + } + }, 4000); // Navigation function navigateTo(page) { document.querySelectorAll('.nav-item').forEach(i => i.classList.toggle('active', i.dataset.page === page)); document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === `page-${page}`)); - const titles = { warroom: 'War Room', operators: 'Operatives', evidence: 'Evidence Vault', arsenal: 'Arsenal', terminal: 'Terminal', benchmarks: 'Benchmarks', configs: 'Config Library', 'ctf-range': 'CTF Range', general: 'Op Admiral', selfimprove: 'Self-Improvement', settings: 'Settings', about: 'About' }; + const titles = { warroom: 'War Room', receipts: 'Scope Receipts', operators: 'Operatives', evidence: 'Evidence Vault', arsenal: 'Arsenal', terminal: 'Terminal', benchmarks: 'Benchmarks', configs: 'Config Library', 'ctf-range': 'CTF Range', general: 'Op Admiral', selfimprove: 'Self-Improvement', settings: 'Settings', about: 'About' }; document.getElementById('pageTitle').textContent = titles[page] || 'War Room'; document.getElementById('sidebar').classList.remove('open'); + if (page === 'receipts') setTimeout(() => window.refreshReceiptsPage?.(), 50); if (page === 'operators' && typeof window.loadOperativesRoster === 'function') setTimeout(window.loadOperativesRoster, 80); if (page === 'evidence' && typeof window.hydrateFindings === 'function') setTimeout(window.hydrateFindings, 60); if (page === 'selfimprove' && typeof window.renderSelfImprove === 'function') setTimeout(window.renderSelfImprove, 60); @@ -25147,6 +25266,10 @@ Be specific and actionable. Format as structured JSON.`; if (typeof updateSitRep === 'function') updateSitRep('reconnaissance', '⚓ Op Admiral is planning the ' + t.label + ' operation on ' + tgt + '…', 'ACTIVE'); if (typeof addIntel === 'function') addIntel('ADMIRAL', '⚓ Brief handed to Op Admiral — planning ' + brief.family + ' on ' + tgt + ' (~30-90s)', 'info'); if (window.toast) toast('⚓ Op Admiral is planning the operation…', 'info'); + const admiralApiBase = (typeof getApiBase === 'function') + ? getApiBase() + : ((typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API.baseUrl) || ''); + const admiralFetch = function(path, opts){ return fetch(admiralApiBase + path, opts); }; const body = function(extra){ return JSON.stringify(Object.assign({ brief: brief, confirmed: true }, bb, extra || {})); }; // Op Admiral planning is a slow LLM call, and on success the SERVER brings up a LIVE // mission. The server budgets up to 300s for planning, so the client cap MUST exceed that @@ -25156,31 +25279,29 @@ Be specific and actionable. Format as structured JSON.`; // cautious rather than declaring failure. const launchOpts = function(extra){ return { method:'POST', headers:{'Content-Type':'application/json'}, body: body(extra), signal: AbortSignal.timeout(330000) }; }; try { - let res = await fetch('/api/admiral/launch', launchOpts()); + let approvalIds = []; + let res = await admiralFetch('/api/admiral/launch', launchOpts()); let d = await res.json(); - // autonomous-execution approval receipt — auto-grant for loopback/lab or Autonomous mode, else require it. - // The server gates this with HTTP 403 (blockForApproval); the HOLD gate below is 409. - if (res.status === 403 && d.approval && d.approval.id) { - if (isLab || autonomous) { - await fetch('/api/approvals/' + d.approval.id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' }); - res = await fetch('/api/admiral/launch', launchOpts({ approvalId: d.approval.id })); - d = await res.json(); - } else { - throw new Error('Approval required for ' + tgt + ' — choose Autonomous mode, or approve in the receipts panel.'); - } - } - if (res.status === 403 && Array.isArray(d.approvals) && d.approvals.length) { - const approvalIds = d.approvals.map(function(a){ return a && a.id; }).filter(Boolean); - const targets = (d.outOfScopeTargets || []).join(', '); - if (isLab || autonomous) { - await Promise.all(approvalIds.map(function(id){ - return fetch('/api/approvals/' + id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' }); - })); - res = await fetch('/api/admiral/launch', launchOpts({ approvalIds: approvalIds })); - d = await res.json(); - } else { - throw new Error('Expanded target approval required for ' + (targets || 'planner-added targets') + ' — approve the receipts or choose Autonomous mode.'); + for (let approvalRound = 0; approvalRound < 5 && res.status === 403; approvalRound++) { + const requested = []; + if (d.approval && d.approval.id) requested.push(d.approval); + if (Array.isArray(d.approvals)) d.approvals.forEach(function(a){ if (a && a.id) requested.push(a); }); + if (!requested.length) break; + const requestedIds = requested.map(function(a){ return a.id; }).filter(Boolean); + const targets = (d.outOfScopeTargets || requested.map(function(a){ return a.target; }).filter(Boolean)).join(', '); + if (!(isLab || autonomous)) { + addIntel?.('SCOPEGUARD', 'Approval required: ' + (targets || tgt) + ' — open Scope Receipts', 'warning'); + navigateTo?.('receipts'); + throw new Error((d.error || 'Approval required') + ' — open Scope Receipts to approve ' + (targets || 'pending targets') + '.'); } + await Promise.all(requestedIds.map(function(id){ + return admiralFetch('/api/approvals/' + id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' }); + })); + approvalIds = approvalIds.concat(requestedIds).filter(function(id, idx, arr){ return id && arr.indexOf(id) === idx; }); + addIntel?.('SCOPEGUARD', 'Auto-approved ' + requestedIds.length + ' receipt(s): ' + (targets || requestedIds.join(', ')), 'success'); + await window.refreshReceiptsPage?.(); + res = await admiralFetch('/api/admiral/launch', launchOpts({ approvalIds: approvalIds })); + d = await res.json(); } if (res.status === 409 && d.review && d.review.status === 'hold') throw new Error('Op Admiral plan gate is HOLD — ' + ((d.review && d.review.reason) || 'manual review required before execution')); if (!res.ok || d.error) throw new Error(d.error || ('launch failed (' + res.status + ')')); diff --git a/src/server.ts b/src/server.ts index 6a185b7..c8701e5 100755 --- a/src/server.ts +++ b/src/server.ts @@ -1094,7 +1094,13 @@ function approvalIsFresh(approval: ApprovalRequest): boolean { function approvalMatches(approval: ApprovalRequest, action: GuardAction, target: string): boolean { if (approval.action !== action) return false; if (approval.target === '*') return action === 'model_call' || action === 'autonomous_execution'; - return hostFromTarget(approval.target) === hostFromTarget(target); + const approvalHost = hostFromTarget(approval.target); + const targetHost = hostFromTarget(target); + if (approvalHost.startsWith('*.')) { + const suffix = approvalHost.slice(1); + return targetHost.endsWith(suffix) && targetHost !== approvalHost.slice(2); + } + return approvalHost === targetHost; } function ensureExecTargetsWithinApprovedTarget(targets: string[], approvedTarget: string): string[] { From 06e1edb4b86728ac2bd385fbb5d4e84e43f8a30c Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 20:46:04 -0400 Subject: [PATCH 4/6] Retry Admiral launch after receipt approval --- docs/index.html | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/index.html b/docs/index.html index 20c06a0..5abcbc9 100755 --- a/docs/index.html +++ b/docs/index.html @@ -6950,9 +6950,13 @@ Correlating findings across agents, identifying patterns, producing actionable i }; window.approveReceipt = async function approveReceipt(id) { + const receipt = ReceiptsState.approvals.find(function(a){ return a.id === id; }); await T3MP3ST_API.post('/api/approvals/' + id + '/approve', {}); await window.refreshReceiptsPage(); if (window.toast) toast('Receipt approved: ' + id, 'success'); + if (receipt?.action === 'autonomous_execution' && window.__t3mpAdmiralPendingRetry && typeof window.retryPendingAdmiralLaunch === 'function') { + window.retryPendingAdmiralLaunch(); + } }; window.rejectReceipt = async function rejectReceipt(id) { @@ -6966,6 +6970,9 @@ Correlating findings across agents, identifying patterns, producing actionable i await Promise.all(pending.map(function(a){ return T3MP3ST_API.post('/api/approvals/' + a.id + '/approve', {}); })); await window.refreshReceiptsPage(); if (window.toast) toast('Approved ' + pending.length + ' pending receipt(s)', pending.length ? 'success' : 'info'); + if (pending.some(function(a){ return a.action === 'autonomous_execution'; }) && window.__t3mpAdmiralPendingRetry && typeof window.retryPendingAdmiralLaunch === 'function') { + window.retryPendingAdmiralLaunch(); + } }; function renderReceiptsPage() { @@ -25266,6 +25273,14 @@ Be specific and actionable. Format as structured JSON.`; if (typeof updateSitRep === 'function') updateSitRep('reconnaissance', '⚓ Op Admiral is planning the ' + t.label + ' operation on ' + tgt + '…', 'ACTIVE'); if (typeof addIntel === 'function') addIntel('ADMIRAL', '⚓ Brief handed to Op Admiral — planning ' + brief.family + ' on ' + tgt + ' (~30-90s)', 'info'); if (window.toast) toast('⚓ Op Admiral is planning the operation…', 'info'); + window.__t3mpAdmiralPendingRetry = false; + window.retryPendingAdmiralLaunch = function(){ + if (!window.__t3mpAdmiralPendingRetry) return; + window.__t3mpAdmiralPendingRetry = false; + if (typeof addIntel === 'function') addIntel('SCOPEGUARD', 'Receipt approved — retrying Admiral launch', 'info'); + if (window.toast) toast('Receipt approved — retrying Admiral launch', 'info'); + setTimeout(function(){ window.admiralLaunch(); }, 50); + }; const admiralApiBase = (typeof getApiBase === 'function') ? getApiBase() : ((typeof T3MP3ST_API !== 'undefined' && T3MP3ST_API.baseUrl) || ''); @@ -25290,9 +25305,10 @@ Be specific and actionable. Format as structured JSON.`; const requestedIds = requested.map(function(a){ return a.id; }).filter(Boolean); const targets = (d.outOfScopeTargets || requested.map(function(a){ return a.target; }).filter(Boolean)).join(', '); if (!(isLab || autonomous)) { + window.__t3mpAdmiralPendingRetry = true; addIntel?.('SCOPEGUARD', 'Approval required: ' + (targets || tgt) + ' — open Scope Receipts', 'warning'); navigateTo?.('receipts'); - throw new Error((d.error || 'Approval required') + ' — open Scope Receipts to approve ' + (targets || 'pending targets') + '.'); + throw new Error((d.error || 'Approval required') + ' — approve ' + (targets || 'pending targets') + ' in Scope Receipts; launch will retry after approval.'); } await Promise.all(requestedIds.map(function(id){ return admiralFetch('/api/approvals/' + id + '/approve', { method:'POST', headers:{'Content-Type':'application/json'}, body:'{}' }); From 3d6dc62957cd30919413b72a9b28803c1d60f2a0 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 6 Jul 2026 08:00:07 -0400 Subject: [PATCH 5/6] 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']; From 6fef62aa8e457425dabefc9312bbb632f065de63 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 12 Jul 2026 09:50:34 -0400 Subject: [PATCH 6/6] 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) => {