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[] {