agentgate-runtime-control 2.13.10 → 2.13.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/approval.js +18 -1
- package/src/runtime.js +23 -5
package/package.json
CHANGED
package/src/approval.js
CHANGED
|
@@ -12,10 +12,18 @@ export class ApprovalStore {
|
|
|
12
12
|
|
|
13
13
|
export function createApprovalStore(options = {}) { return options.store || new ApprovalStore(options.limit || 500); }
|
|
14
14
|
|
|
15
|
+
// Default: a pending approval expires after 15 minutes if nobody acts on it.
|
|
16
|
+
// Pass ttlMs: null (or 0) to disable expiry for a given request.
|
|
17
|
+
export const DEFAULT_APPROVAL_TTL_MS = 15 * 60 * 1000;
|
|
18
|
+
|
|
15
19
|
export function createApprovalRequest(data = {}, store = new ApprovalStore()) {
|
|
20
|
+
const createdAt = new Date();
|
|
21
|
+
const ttlMs = data.ttlMs === undefined ? DEFAULT_APPROVAL_TTL_MS : data.ttlMs;
|
|
22
|
+
const expiresAt = ttlMs ? new Date(createdAt.getTime() + ttlMs).toISOString() : null;
|
|
16
23
|
return store.add({
|
|
17
24
|
id: data.id || `approval_${randomUUID()}`,
|
|
18
|
-
createdAt:
|
|
25
|
+
createdAt: createdAt.toISOString(),
|
|
26
|
+
expiresAt,
|
|
19
27
|
status: APPROVAL_STATUS.PENDING,
|
|
20
28
|
runId: data.runId,
|
|
21
29
|
tenantId: data.tenantId || data.request?.tenantId || null,
|
|
@@ -27,4 +35,13 @@ export function createApprovalRequest(data = {}, store = new ApprovalStore()) {
|
|
|
27
35
|
});
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
// A pending approval is expired once `expiresAt` has passed. Every approval
|
|
39
|
+
// is single-use by construction: approve()/deny() flip `status` away from
|
|
40
|
+
// PENDING synchronously (before any await), so a second call — concurrent
|
|
41
|
+
// or not — always sees a non-pending status and is rejected. There is no
|
|
42
|
+
// separate "already used" flag to track.
|
|
43
|
+
export function isApprovalExpired(approval, now = Date.now()) {
|
|
44
|
+
return Boolean(approval?.expiresAt) && new Date(approval.expiresAt).getTime() <= now;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
function clone(value) { try { return structuredClone(value); } catch { return JSON.parse(JSON.stringify(value)); } }
|
package/src/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { evaluate } from './policy-engine.js';
|
|
2
2
|
import { createPersistentRunStore, createPersistentApprovalStore } from './persistent-store.js';
|
|
3
|
-
import { createApprovalStore, createApprovalRequest, APPROVAL_STATUS } from './approval.js';
|
|
3
|
+
import { createApprovalStore, createApprovalRequest, APPROVAL_STATUS, isApprovalExpired } from './approval.js';
|
|
4
4
|
|
|
5
5
|
export class RunStore {
|
|
6
6
|
constructor(limit = 25000) { this.limit = limit; this.runs = []; }
|
|
@@ -45,6 +45,7 @@ export function createRuntime(options = {}) {
|
|
|
45
45
|
decision: event.decision,
|
|
46
46
|
risk: event.risk,
|
|
47
47
|
reason: event.reason,
|
|
48
|
+
ttlMs: options.approvalTTLMs,
|
|
48
49
|
metadata: { action: request.action || request.tool || null }
|
|
49
50
|
}, approvalStore);
|
|
50
51
|
event.status = 'pending_approval';
|
|
@@ -65,11 +66,28 @@ export function createRuntime(options = {}) {
|
|
|
65
66
|
return { status: 'executed', value, agentgate: event };
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// Lazily flips a PENDING-but-expired approval to EXPIRED and drops its
|
|
70
|
+
// pending execution, so a stale approval can never be actioned late.
|
|
71
|
+
function expireIfStale(approval) {
|
|
72
|
+
if (!approval || approval.status !== APPROVAL_STATUS.PENDING) return approval;
|
|
73
|
+
if (!isApprovalExpired(approval)) return approval;
|
|
74
|
+
approval.status = APPROVAL_STATUS.EXPIRED;
|
|
75
|
+
approval.resolvedAt = new Date().toISOString();
|
|
76
|
+
const item = pending.get(approval.id);
|
|
77
|
+
if (item) { item.event.status = 'expired'; item.event.approval = { status: approval.status, resolvedAt: approval.resolvedAt }; }
|
|
78
|
+
pending.delete(approval.id);
|
|
79
|
+
approvalStore.save?.();
|
|
80
|
+
return approval;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function approvals(status) {
|
|
84
|
+
approvalStore.list(APPROVAL_STATUS.PENDING).forEach(expireIfStale);
|
|
85
|
+
return approvalStore.list(status);
|
|
86
|
+
}
|
|
87
|
+
function getApproval(id) { return expireIfStale(approvalStore.get(id)); }
|
|
70
88
|
|
|
71
89
|
async function approve(approvalId) {
|
|
72
|
-
const approval = approvalStore.get(approvalId);
|
|
90
|
+
const approval = expireIfStale(approvalStore.get(approvalId));
|
|
73
91
|
if (!approval) return { ok: false, error: 'Approval not found' };
|
|
74
92
|
if (approval.status !== APPROVAL_STATUS.PENDING) return { ok: false, error: `Approval is already ${approval.status}`, approval };
|
|
75
93
|
const item = pending.get(approvalId);
|
|
@@ -99,7 +117,7 @@ export function createRuntime(options = {}) {
|
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
function deny(approvalId, reason = 'Denied by approver') {
|
|
102
|
-
const approval = approvalStore.get(approvalId);
|
|
120
|
+
const approval = expireIfStale(approvalStore.get(approvalId));
|
|
103
121
|
if (!approval) return { ok: false, error: 'Approval not found' };
|
|
104
122
|
if (approval.status !== APPROVAL_STATUS.PENDING) return { ok: false, error: `Approval is already ${approval.status}`, approval };
|
|
105
123
|
const item = pending.get(approvalId);
|