Reuse target-scoped approval receipts

This commit is contained in:
Codex committed 2026-07-12 09:50:42 -04:00
1 parent ae32cf5051
commit f2ec1e9437
1 file changed
+67 -4
+67 -4
View File
@@ -1111,10 +1111,19 @@ function approvalMatchesGateScope(approval: ApprovalRequest, action: GuardAction
} }
function findApproval(body: Record<string, unknown>, action: GuardAction, target: string): ApprovalRequest | null { function findApproval(body: Record<string, unknown>, action: GuardAction, target: string): ApprovalRequest | null {
const id = typeof body.approvalId === 'string' ? body.approvalId : ''; const ids = [
const approval = id ? approvalRequests.get(id) : undefined; typeof body.approvalId === 'string' ? body.approvalId : '',
if (!approval || !approvalIsFresh(approval) || !approvalMatches(approval, action, target)) return null; ...(Array.isArray(body.approvalIds) ? body.approvalIds.filter((id): id is string => typeof id === 'string') : []),
return approval; ].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<string, unknown>): ApprovalRequest { function createApprovalRequest(action: GuardAction, target: string, reason: string, body: Record<string, unknown>): ApprovalRequest {
@@ -4825,6 +4834,52 @@ app.post('/api/approvals/:id/approve', (req: Request, res: Response) => {
res.json(approval); res.json(approval);
}); });
app.post('/api/approvals/authorize-target', (req: Request, res: Response) => {
const body = req.body as Record<string, unknown>;
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) => { app.post('/api/approvals/:id/reject', (req: Request, res: Response) => {
const approval = approvalRequests.get(req.params.id); const approval = approvalRequests.get(req.params.id);
if (!approval) { if (!approval) {
@@ -6001,6 +6056,14 @@ app.post('/api/mission/start', async (req: Request, res: Response): Promise<void
if (!guard.allowed) { blockForApproval(res, guard); return; } if (!guard.allowed) { blockForApproval(res, guard); return; }
} }
if (provider === 'local-agent') {
const localAgent = await requireLiveLocalAgent(model);
if (!localAgent.ok) {
res.status(503).json({ error: localAgent.error });
return;
}
}
// B-03: if an OPTIONAL white-box repoPath was supplied, containment-check it HERE // B-03: if an OPTIONAL white-box repoPath was supplied, containment-check it HERE
// (canonicalize + confine to the allowed root) before we build/start the command, // (canonicalize + confine to the allowed root) before we build/start the command,
// so a bad/escaping path 400s cleanly instead of half-starting a run. // so a bad/escaping path 400s cleanly instead of half-starting a run.