@absolutejs/agent-control 0.2.0 → 0.4.0
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/README.md +9 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +181 -17
- package/dist/manifest.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/playground.d.ts +53 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -6,7 +6,8 @@ before cleanup, reports partial source failures, restores deliberately, and uses
|
|
|
6
6
|
leased idempotency records so operator retries cannot change the requested input.
|
|
7
7
|
|
|
8
8
|
Scopes are `agents:read`, `agents:revoke`, and `agents:restore`. Mutations require
|
|
9
|
-
an `operationId
|
|
9
|
+
an `operationId`, bounded `reason`, same-origin `Origin` header, and
|
|
10
|
+
`X-Agent-Control-Intent: mutate`; PostgreSQL operations can be safely reclaimed
|
|
10
11
|
after a crashed operator process's lease expires.
|
|
11
12
|
|
|
12
13
|
`createAgentControlConsoleHandler()` adds a dependency-free authenticated
|
|
@@ -32,3 +33,10 @@ all remote data with DOM `textContent`, cannot be framed, stores no cache, and
|
|
|
32
33
|
has no third-party assets. The data adapter keeps it provider-neutral and lets
|
|
33
34
|
applications compose `@absolutejs/agency`, `agent-runtime`, `agent-memory`, and
|
|
34
35
|
`agent-reputation` without creating hard dependencies.
|
|
36
|
+
|
|
37
|
+
`createAgentPlaygroundHandler()` adds a provider-neutral plan-then-execute UI
|
|
38
|
+
and API. Planning is a separate, explicitly effect-free adapter operation. The
|
|
39
|
+
server stores the full input and only returns its digest, expires plans quickly,
|
|
40
|
+
binds them to the operator, refuses denied or approval-pending plans, and uses
|
|
41
|
+
the same leased idempotency store for execution. The browser cannot mutate a
|
|
42
|
+
plan between review and execution.
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -135,25 +135,178 @@ var createAgentControlConsoleHandler = (options) => {
|
|
|
135
135
|
return Response.json(response);
|
|
136
136
|
};
|
|
137
137
|
};
|
|
138
|
+
// src/playground.ts
|
|
139
|
+
var createMemoryAgentPlaygroundPlanStore = (now = Date.now) => {
|
|
140
|
+
const plans = new Map;
|
|
141
|
+
return {
|
|
142
|
+
get: async (id, operatorId) => {
|
|
143
|
+
const plan = plans.get(id);
|
|
144
|
+
if (!plan || plan.operatorId !== operatorId || plan.expiresAt <= now())
|
|
145
|
+
return;
|
|
146
|
+
return structuredClone(plan);
|
|
147
|
+
},
|
|
148
|
+
save: async (plan) => {
|
|
149
|
+
for (const [id, existing] of plans)
|
|
150
|
+
if (existing.expiresAt <= now())
|
|
151
|
+
plans.delete(id);
|
|
152
|
+
if (plans.has(plan.planId))
|
|
153
|
+
throw new Error("Playground plan already exists");
|
|
154
|
+
plans.set(plan.planId, structuredClone(plan));
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
var digest = async (value) => [
|
|
159
|
+
...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value))))
|
|
160
|
+
].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
161
|
+
var text = (value, field, maximum = 2000) => {
|
|
162
|
+
if (typeof value !== "string" || value === "" || value.length > maximum)
|
|
163
|
+
throw new Error(`Invalid playground ${field}`);
|
|
164
|
+
return value;
|
|
165
|
+
};
|
|
166
|
+
var list = (values, map) => {
|
|
167
|
+
if (!Array.isArray(values) || values.length > 1000)
|
|
168
|
+
throw new Error("Playground collection is too large");
|
|
169
|
+
return values.map(map);
|
|
170
|
+
};
|
|
171
|
+
var safeCatalog = (items) => list(items, (item) => ({
|
|
172
|
+
...item.description === undefined ? {} : { description: text(item.description, "description") },
|
|
173
|
+
id: text(item.id, "agent id", 256),
|
|
174
|
+
name: text(item.name, "agent name", 256),
|
|
175
|
+
operations: list(item.operations, (value) => text(value, "operation", 256)),
|
|
176
|
+
protocols: list(item.protocols, (value) => text(value, "protocol", 128))
|
|
177
|
+
}));
|
|
178
|
+
var page2 = (nonce) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>AbsoluteJS Agent Playground</title><style nonce="${nonce}">:root{color-scheme:dark;font:15px/1.5 system-ui;background:#09090b;color:#fafafa}body{max-width:70rem;margin:auto;padding:2rem}label{display:block;margin:1rem 0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}select,textarea,button{font:inherit;background:#18181b;color:#fff;border:1px solid #3f3f46;border-radius:.5rem;padding:.7rem;width:100%;box-sizing:border-box}textarea{min-height:14rem;font-family:ui-monospace,monospace}button{cursor:pointer;margin:.4rem 0}.execute{background:#14532d}pre{white-space:pre-wrap;background:#18181b;padding:1rem;border-radius:.5rem}.error{color:#fca5a5}@media(max-width:700px){.grid{grid-template-columns:1fr}}</style></head><body><h1>Agent Playground</h1><p>Plan first. Review exact effects. Execute only a bound server-side plan.</p><div class="grid"><div><label>Agent<select id="agent"></select></label><label>Operation<select id="operation"></select></label><label>JSON input<textarea id="input">{}</textarea></label><button id="plan">Create effect-free plan</button><button id="execute" class="execute" disabled>Execute reviewed plan</button></div><div><h2>Bound plan</h2><pre id="output">No plan yet.</pre></div></div><script nonce="${nonce}">(()=>{const base=location.pathname.replace(//$/,""),agent=document.querySelector("#agent"),operation=document.querySelector("#operation"),input=document.querySelector("#input"),output=document.querySelector("#output"),execute=document.querySelector("#execute");let plan;const headers={"content-type":"application/json","x-agent-control-intent":"mutate"};async function catalog(){const data=await fetch(base+"/api/catalog").then(r=>r.json());for(const item of data){const option=document.createElement("option");option.value=item.id;option.textContent=item.name;option.dataset.operations=JSON.stringify(item.operations);agent.append(option)}changed()}function changed(){operation.replaceChildren();for(const value of JSON.parse(agent.selectedOptions[0]?.dataset.operations??"[]")){const option=document.createElement("option");option.value=value;option.textContent=value;operation.append(option)}}agent.addEventListener("change",changed);document.querySelector("#plan").addEventListener("click",async()=>{try{const response=await fetch(base+"/api/plans",{method:"POST",headers,body:JSON.stringify({agentId:agent.value,operation:operation.value,input:JSON.parse(input.value)})});const data=await response.json();if(!response.ok)throw new Error(data.error??("HTTP "+response.status));plan=data;output.textContent=JSON.stringify(data,null,2);execute.disabled=data.status!=="ready"}catch(error){output.textContent=error.message;output.className="error"}});execute.addEventListener("click",async()=>{if(!plan)return;execute.disabled=true;const response=await fetch(base+"/api/plans/"+encodeURIComponent(plan.planId)+"/execute",{method:"POST",headers:{...headers,"idempotency-key":crypto.randomUUID()},body:"{}"});output.textContent=JSON.stringify(await response.json(),null,2)});catalog()})();</script></body></html>`;
|
|
179
|
+
var createAgentPlaygroundHandler = (options) => {
|
|
180
|
+
const base = (options.basePath ?? "/agent-playground").replace(/\/$/u, "");
|
|
181
|
+
const now = options.now ?? Date.now;
|
|
182
|
+
const mutationAllowed = (request, url) => request.headers.get("origin") === url.origin && request.headers.get("x-agent-control-intent") === "mutate";
|
|
183
|
+
return async (request) => {
|
|
184
|
+
const url = new URL(request.url);
|
|
185
|
+
if (url.pathname !== base && !url.pathname.startsWith(`${base}/`))
|
|
186
|
+
return null;
|
|
187
|
+
const operator = await options.authorize(request);
|
|
188
|
+
if (!operator)
|
|
189
|
+
return new Response("Unauthorized", { status: 401 });
|
|
190
|
+
const scope = request.method === "GET" ? "agents:read" : url.pathname.endsWith("/execute") ? "agents:execute" : "agents:simulate";
|
|
191
|
+
if (!operator.scopes.includes(scope))
|
|
192
|
+
return new Response("Forbidden", { status: 403 });
|
|
193
|
+
if (request.method === "GET" && (url.pathname === base || url.pathname === `${base}/`)) {
|
|
194
|
+
const nonce = crypto.randomUUID().replaceAll("-", "");
|
|
195
|
+
return new Response(page2(nonce), {
|
|
196
|
+
headers: {
|
|
197
|
+
"cache-control": "no-store",
|
|
198
|
+
"content-security-policy": `default-src 'none'; connect-src 'self'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; base-uri 'none'; frame-ancestors 'none'`,
|
|
199
|
+
"content-type": "text/html; charset=utf-8",
|
|
200
|
+
"x-content-type-options": "nosniff"
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (request.method === "GET" && url.pathname === `${base}/api/catalog`)
|
|
205
|
+
return Response.json(safeCatalog(await options.adapter.catalog(operator)), {
|
|
206
|
+
headers: { "cache-control": "no-store" }
|
|
207
|
+
});
|
|
208
|
+
if (request.method === "POST" && url.pathname === `${base}/api/plans`) {
|
|
209
|
+
if (!mutationAllowed(request, url))
|
|
210
|
+
return new Response("Cross-site mutation denied", { status: 403 });
|
|
211
|
+
const raw = await request.text();
|
|
212
|
+
if (new TextEncoder().encode(raw).byteLength > 256000)
|
|
213
|
+
return Response.json({ error: "Input is too large" }, { status: 413 });
|
|
214
|
+
let body;
|
|
215
|
+
try {
|
|
216
|
+
body = JSON.parse(raw);
|
|
217
|
+
} catch {
|
|
218
|
+
return Response.json({ error: "Input must be valid JSON" }, { status: 400 });
|
|
219
|
+
}
|
|
220
|
+
const agentId = text(body.agentId, "agent id", 256);
|
|
221
|
+
const operation = text(body.operation, "operation", 256);
|
|
222
|
+
const inputDigest = await digest(body.input ?? null);
|
|
223
|
+
const proposed = await options.adapter.plan({ agentId, input: structuredClone(body.input ?? null), operation }, operator);
|
|
224
|
+
if (proposed.status !== "ready" && proposed.status !== "approval-required" && proposed.status !== "denied")
|
|
225
|
+
throw new Error("Invalid playground plan status");
|
|
226
|
+
if (proposed.spend && (!Number.isSafeInteger(proposed.spend.amountMinor) || proposed.spend.amountMinor < 0 || !/^[A-Z]{3}$/u.test(proposed.spend.currency)))
|
|
227
|
+
throw new Error("Invalid playground plan spend");
|
|
228
|
+
const plan2 = {
|
|
229
|
+
agentId,
|
|
230
|
+
effects: list(proposed.effects, (value) => text(value, "effect", 128)),
|
|
231
|
+
expiresAt: now() + (options.planTtlMs ?? 10 * 60000),
|
|
232
|
+
input: structuredClone(body.input ?? null),
|
|
233
|
+
inputDigest,
|
|
234
|
+
operation,
|
|
235
|
+
operatorId: operator.id,
|
|
236
|
+
planId: `plan_${crypto.randomUUID()}`,
|
|
237
|
+
...proposed.spend ? {
|
|
238
|
+
spend: {
|
|
239
|
+
amountMinor: proposed.spend.amountMinor,
|
|
240
|
+
currency: text(proposed.spend.currency, "currency", 3)
|
|
241
|
+
}
|
|
242
|
+
} : {},
|
|
243
|
+
status: proposed.status,
|
|
244
|
+
steps: list(proposed.steps, (step) => ({
|
|
245
|
+
description: text(step.description, "step description"),
|
|
246
|
+
...step.protocol ? { protocol: text(step.protocol, "protocol", 128) } : {},
|
|
247
|
+
...step.target ? { target: text(step.target, "target") } : {}
|
|
248
|
+
})),
|
|
249
|
+
summary: text(proposed.summary, "summary")
|
|
250
|
+
};
|
|
251
|
+
await options.plans.save(plan2);
|
|
252
|
+
const { input: _input, ...visible } = plan2;
|
|
253
|
+
return Response.json(visible, {
|
|
254
|
+
status: 201,
|
|
255
|
+
headers: { "cache-control": "no-store" }
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const execute = new RegExp(`^${base.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/api/plans/([^/]+)/execute$`, "u").exec(url.pathname);
|
|
259
|
+
if (request.method !== "POST" || !execute?.[1])
|
|
260
|
+
return new Response(null, { status: 405 });
|
|
261
|
+
if (!mutationAllowed(request, url))
|
|
262
|
+
return new Response("Cross-site mutation denied", { status: 403 });
|
|
263
|
+
const operationId = request.headers.get("idempotency-key");
|
|
264
|
+
if (!operationId)
|
|
265
|
+
return Response.json({ error: "Idempotency-Key is required" }, { status: 400 });
|
|
266
|
+
const plan = await options.plans.get(decodeURIComponent(execute[1]), operator.id);
|
|
267
|
+
if (!plan)
|
|
268
|
+
return Response.json({ error: "Plan not found or expired" }, { status: 404 });
|
|
269
|
+
if (plan.status !== "ready")
|
|
270
|
+
return Response.json({ error: `Plan is ${plan.status}` }, { status: 409 });
|
|
271
|
+
const binding = await digest({
|
|
272
|
+
inputDigest: plan.inputDigest,
|
|
273
|
+
planId: plan.planId
|
|
274
|
+
});
|
|
275
|
+
let claim;
|
|
276
|
+
try {
|
|
277
|
+
claim = await options.operations.claim(`playground:${plan.planId}`, binding, now(), 30000);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return Response.json({ error: error instanceof Error ? error.message : "conflict" }, { status: 409 });
|
|
280
|
+
}
|
|
281
|
+
if (claim.response !== undefined)
|
|
282
|
+
return Response.json(claim.response);
|
|
283
|
+
if (!claim.acquired)
|
|
284
|
+
return Response.json({ error: "operation in progress" }, { status: 409, headers: { "retry-after": "5" } });
|
|
285
|
+
const result = await options.adapter.execute(structuredClone(plan), operator);
|
|
286
|
+
if (!await options.operations.complete(`playground:${plan.planId}`, binding, result))
|
|
287
|
+
throw new Error("Playground execution completion lost");
|
|
288
|
+
return Response.json(result);
|
|
289
|
+
};
|
|
290
|
+
};
|
|
138
291
|
|
|
139
292
|
// src/index.ts
|
|
140
293
|
var createMemoryOperationStore = () => {
|
|
141
294
|
const rows2 = new Map;
|
|
142
295
|
return {
|
|
143
|
-
claim: async (key,
|
|
296
|
+
claim: async (key, digest2, now, leaseMs) => {
|
|
144
297
|
const row = rows2.get(key);
|
|
145
|
-
if (row?.digest !== undefined && row.digest !==
|
|
298
|
+
if (row?.digest !== undefined && row.digest !== digest2)
|
|
146
299
|
throw new Error("Idempotency key reused with different input");
|
|
147
300
|
if (row?.response !== undefined)
|
|
148
301
|
return { acquired: false, response: structuredClone(row.response) };
|
|
149
302
|
if (row && row.leaseUntil > now)
|
|
150
303
|
return { acquired: false };
|
|
151
|
-
rows2.set(key, { digest, key, leaseUntil: now + leaseMs });
|
|
304
|
+
rows2.set(key, { digest: digest2, key, leaseUntil: now + leaseMs });
|
|
152
305
|
return { acquired: true };
|
|
153
306
|
},
|
|
154
|
-
complete: async (key,
|
|
307
|
+
complete: async (key, digest2, response) => {
|
|
155
308
|
const row = rows2.get(key);
|
|
156
|
-
if (!row || row.digest !==
|
|
309
|
+
if (!row || row.digest !== digest2)
|
|
157
310
|
return false;
|
|
158
311
|
rows2.set(key, { ...row, response: structuredClone(response) });
|
|
159
312
|
return true;
|
|
@@ -186,15 +339,24 @@ var createAgentControlHandler = ({
|
|
|
186
339
|
return Response.json(await control.inventory(agentId));
|
|
187
340
|
if (request.method !== "POST" || action !== "revoke" && action !== "restore")
|
|
188
341
|
return new Response(null, { status: 405 });
|
|
189
|
-
|
|
190
|
-
return;
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
return Response.json({ error: "
|
|
194
|
-
const
|
|
342
|
+
if (request.headers.get("origin") !== url.origin || request.headers.get("x-agent-control-intent") !== "mutate")
|
|
343
|
+
return new Response("Cross-site mutation denied", { status: 403 });
|
|
344
|
+
const raw = await request.text();
|
|
345
|
+
if (new TextEncoder().encode(raw).byteLength > 4096)
|
|
346
|
+
return Response.json({ error: "Request body is too large" }, { status: 413 });
|
|
347
|
+
const body = (() => {
|
|
348
|
+
try {
|
|
349
|
+
return JSON.parse(raw);
|
|
350
|
+
} catch {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
if (!body?.operationId || body.operationId.length > 200 || !body.reason || body.reason.length > 1000)
|
|
355
|
+
return Response.json({ error: "A bounded operationId and reason are required" }, { status: 400 });
|
|
356
|
+
const digest2 = await hash2({ action, agentId, reason: body.reason });
|
|
195
357
|
let claim;
|
|
196
358
|
try {
|
|
197
|
-
claim = await operations.claim(body.operationId,
|
|
359
|
+
claim = await operations.claim(body.operationId, digest2, now(), 30000);
|
|
198
360
|
} catch (error) {
|
|
199
361
|
return Response.json({ error: error instanceof Error ? error.message : "conflict" }, { status: 409 });
|
|
200
362
|
}
|
|
@@ -207,7 +369,7 @@ var createAgentControlHandler = ({
|
|
|
207
369
|
agentId,
|
|
208
370
|
reason: body.reason
|
|
209
371
|
}) : (await control.restore(agentId), { agentId, restored: true, restoredBy: operator.id });
|
|
210
|
-
if (!await operations.complete(body.operationId,
|
|
372
|
+
if (!await operations.complete(body.operationId, digest2, response))
|
|
211
373
|
throw new Error("Control operation completion lost");
|
|
212
374
|
return Response.json(response);
|
|
213
375
|
};
|
|
@@ -226,12 +388,12 @@ var createPostgresOperationStore = ({
|
|
|
226
388
|
}) => {
|
|
227
389
|
const ns = nsOf(namespace);
|
|
228
390
|
return {
|
|
229
|
-
claim: async (key,
|
|
230
|
-
const result = await client.query(`INSERT INTO ${ns}.operations (operation_key,digest,lease_until) VALUES ($1,$2,$3) ON CONFLICT (operation_key) DO UPDATE SET lease_until=$3 WHERE ${ns}.operations.digest=$2 AND ${ns}.operations.response IS NULL AND ${ns}.operations.lease_until <= $4 RETURNING digest,response`, [key,
|
|
391
|
+
claim: async (key, digest2, now, leaseMs) => {
|
|
392
|
+
const result = await client.query(`INSERT INTO ${ns}.operations (operation_key,digest,lease_until) VALUES ($1,$2,$3) ON CONFLICT (operation_key) DO UPDATE SET lease_until=$3 WHERE ${ns}.operations.digest=$2 AND ${ns}.operations.response IS NULL AND ${ns}.operations.lease_until <= $4 RETURNING digest,response`, [key, digest2, now + leaseMs, now]);
|
|
231
393
|
const row = result.rows[0];
|
|
232
394
|
if (!row) {
|
|
233
395
|
const existing = (await client.query(`SELECT digest,response FROM ${ns}.operations WHERE operation_key=$1`, [key])).rows[0];
|
|
234
|
-
if (existing?.digest !== undefined && existing.digest !==
|
|
396
|
+
if (existing?.digest !== undefined && existing.digest !== digest2)
|
|
235
397
|
throw new Error("Idempotency key reused with different input");
|
|
236
398
|
return {
|
|
237
399
|
acquired: false,
|
|
@@ -240,13 +402,15 @@ var createPostgresOperationStore = ({
|
|
|
240
402
|
}
|
|
241
403
|
return { acquired: row.response === null || row.response === undefined };
|
|
242
404
|
},
|
|
243
|
-
complete: async (key,
|
|
405
|
+
complete: async (key, digest2, response) => (await client.query(`UPDATE ${ns}.operations SET response=$3::jsonb WHERE operation_key=$1 AND digest=$2 AND response IS NULL RETURNING operation_key`, [key, digest2, JSON.stringify(response)])).rows.length === 1
|
|
244
406
|
};
|
|
245
407
|
};
|
|
246
408
|
export {
|
|
247
409
|
sanitizeAgentControlSnapshot,
|
|
248
410
|
createPostgresOperationStore,
|
|
249
411
|
createMemoryOperationStore,
|
|
412
|
+
createMemoryAgentPlaygroundPlanStore,
|
|
413
|
+
createAgentPlaygroundHandler,
|
|
250
414
|
createAgentControlHandler,
|
|
251
415
|
createAgentControlConsoleHandler,
|
|
252
416
|
agentControlPostgresSchemaSql
|
package/dist/manifest.js
CHANGED
|
@@ -5943,7 +5943,7 @@ var manifest = defineManifest()({
|
|
|
5943
5943
|
identity: {
|
|
5944
5944
|
accent: "#dc2626",
|
|
5945
5945
|
category: "security",
|
|
5946
|
-
description: "Authenticated operator API
|
|
5946
|
+
description: "Authenticated operator API, CSP-hardened console, and bound plan-then-execute playground for approvals, runs, budgets, delegations, memory metadata, reputation, durable kill switches, and leased idempotent operations.",
|
|
5947
5947
|
docsUrl: "https://github.com/absolutejs/agent-control",
|
|
5948
5948
|
name: "@absolutejs/agent-control",
|
|
5949
5949
|
tagline: "See and stop every capability an agent holds."
|
package/dist/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"identity": {
|
|
4
4
|
"accent": "#dc2626",
|
|
5
5
|
"category": "security",
|
|
6
|
-
"description": "Authenticated operator API
|
|
6
|
+
"description": "Authenticated operator API, CSP-hardened console, and bound plan-then-execute playground for approvals, runs, budgets, delegations, memory metadata, reputation, durable kill switches, and leased idempotent operations.",
|
|
7
7
|
"docsUrl": "https://github.com/absolutejs/agent-control",
|
|
8
8
|
"name": "@absolutejs/agent-control",
|
|
9
9
|
"tagline": "See and stop every capability an agent holds."
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { OperationStore, Operator } from "./index";
|
|
2
|
+
export type AgentPlaygroundCatalogItem = {
|
|
3
|
+
description?: string;
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
operations: string[];
|
|
7
|
+
protocols: string[];
|
|
8
|
+
};
|
|
9
|
+
export type AgentPlaygroundPlan = {
|
|
10
|
+
agentId: string;
|
|
11
|
+
effects: string[];
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
input: unknown;
|
|
14
|
+
inputDigest: string;
|
|
15
|
+
operation: string;
|
|
16
|
+
operatorId: string;
|
|
17
|
+
planId: string;
|
|
18
|
+
spend?: {
|
|
19
|
+
amountMinor: number;
|
|
20
|
+
currency: string;
|
|
21
|
+
};
|
|
22
|
+
status: "ready" | "approval-required" | "denied";
|
|
23
|
+
steps: Array<{
|
|
24
|
+
description: string;
|
|
25
|
+
protocol?: string;
|
|
26
|
+
target?: string;
|
|
27
|
+
}>;
|
|
28
|
+
summary: string;
|
|
29
|
+
};
|
|
30
|
+
export type AgentPlaygroundPlanStore = {
|
|
31
|
+
get: (planId: string, operatorId: string) => Promise<AgentPlaygroundPlan | undefined>;
|
|
32
|
+
save: (plan: AgentPlaygroundPlan) => Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
export type AgentPlaygroundAdapter = {
|
|
35
|
+
catalog: (operator: Operator) => Promise<AgentPlaygroundCatalogItem[]> | AgentPlaygroundCatalogItem[];
|
|
36
|
+
/** Must not produce external effects. Return only a proposed plan. */
|
|
37
|
+
plan: (request: {
|
|
38
|
+
agentId: string;
|
|
39
|
+
input: unknown;
|
|
40
|
+
operation: string;
|
|
41
|
+
}, operator: Operator) => Promise<Omit<AgentPlaygroundPlan, "agentId" | "expiresAt" | "input" | "inputDigest" | "operation" | "operatorId" | "planId">> | Omit<AgentPlaygroundPlan, "agentId" | "expiresAt" | "input" | "inputDigest" | "operation" | "operatorId" | "planId">;
|
|
42
|
+
execute: (plan: AgentPlaygroundPlan, operator: Operator) => Promise<unknown>;
|
|
43
|
+
};
|
|
44
|
+
export declare const createMemoryAgentPlaygroundPlanStore: (now?: () => number) => AgentPlaygroundPlanStore;
|
|
45
|
+
export declare const createAgentPlaygroundHandler: (options: {
|
|
46
|
+
adapter: AgentPlaygroundAdapter;
|
|
47
|
+
authorize: (request: Request) => Promise<Operator | undefined> | Operator | undefined;
|
|
48
|
+
basePath?: string;
|
|
49
|
+
now?: () => number;
|
|
50
|
+
operations: OperationStore;
|
|
51
|
+
plans: AgentPlaygroundPlanStore;
|
|
52
|
+
planTtlMs?: number;
|
|
53
|
+
}) => (request: Request) => Promise<Response | null>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/agent-control",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Authenticated AI agent operator API
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Authenticated AI agent operator API, console, and bound plan-then-execute playground.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"check:package": "bun run format && bun run typecheck && bun run test && bun run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@absolutejs/agency": "^0.
|
|
43
|
+
"@absolutejs/agency": "^0.5.0",
|
|
44
44
|
"@absolutejs/manifest": "^0.3.0",
|
|
45
45
|
"@sinclair/typebox": "^0.34.0"
|
|
46
46
|
},
|