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;
}