@absolutejs/agent-control 0.1.0 → 0.2.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 CHANGED
@@ -8,3 +8,27 @@ leased idempotency records so operator retries cannot change the requested input
8
8
  Scopes are `agents:read`, `agents:revoke`, and `agents:restore`. Mutations require
9
9
  an `operationId` and `reason`; PostgreSQL operations can be safely reclaimed
10
10
  after a crashed operator process's lease expires.
11
+
12
+ `createAgentControlConsoleHandler()` adds a dependency-free authenticated
13
+ operator console to the same package. Its snapshot shows agent status, pending
14
+ approvals, durable runs and budgets, delegation scope and expiry, memory
15
+ metadata, and scope-specific reputation without exposing memory contents or raw
16
+ action inputs. Approval decisions require `agents:approve`, same-origin
17
+ requests, an explicit mutation-intent header, a reason, and an idempotency key.
18
+
19
+ ```ts
20
+ const console = createAgentControlConsoleHandler({
21
+ authorize: verifyOperator,
22
+ operations: createPostgresOperationStore({ client }),
23
+ data: {
24
+ snapshot: (operator) => loadSafeOperatorSnapshot(operator),
25
+ decideApproval: (decision) => applyAgencyDecision(decision),
26
+ },
27
+ });
28
+ ```
29
+
30
+ The HTML console uses a per-response Content Security Policy nonce, constructs
31
+ all remote data with DOM `textContent`, cannot be framed, stores no cache, and
32
+ has no third-party assets. The data adapter keeps it provider-neutral and lets
33
+ applications compose `@absolutejs/agency`, `agent-runtime`, `agent-memory`, and
34
+ `agent-reputation` without creating hard dependencies.
@@ -0,0 +1,63 @@
1
+ import type { OperationStore, Operator } from "./index";
2
+ export type AgentControlSnapshot = {
3
+ agents: Array<{
4
+ description?: string;
5
+ id: string;
6
+ status: string;
7
+ }>;
8
+ approvals: Array<{
9
+ action: string;
10
+ agentId: string;
11
+ id: string;
12
+ requestedAt: string;
13
+ risk?: string;
14
+ summary: string;
15
+ }>;
16
+ delegations: Array<{
17
+ expiresAt?: string;
18
+ from: string;
19
+ id: string;
20
+ scopes: string[];
21
+ to: string;
22
+ }>;
23
+ memories: Array<{
24
+ agentId: string;
25
+ id: string;
26
+ scope: string;
27
+ updatedAt: string;
28
+ }>;
29
+ reputations: Array<{
30
+ confidence: number;
31
+ scope: string;
32
+ score: number;
33
+ subject: string;
34
+ }>;
35
+ runs: Array<{
36
+ agentId: string;
37
+ budget?: {
38
+ limit: number;
39
+ spent: number;
40
+ unit: string;
41
+ };
42
+ id: string;
43
+ startedAt: string;
44
+ status: string;
45
+ }>;
46
+ };
47
+ export type AgentControlConsoleData = {
48
+ decideApproval: (input: {
49
+ approvalId: string;
50
+ decision: "approve" | "deny";
51
+ operatorId: string;
52
+ reason: string;
53
+ }) => Promise<unknown>;
54
+ snapshot: (operator: Operator) => Promise<AgentControlSnapshot>;
55
+ };
56
+ export declare const sanitizeAgentControlSnapshot: (source: AgentControlSnapshot) => AgentControlSnapshot;
57
+ export declare const createAgentControlConsoleHandler: (options: {
58
+ authorize: (request: Request) => Promise<Operator | undefined> | Operator | undefined;
59
+ basePath?: string;
60
+ data: AgentControlConsoleData;
61
+ now?: () => number;
62
+ operations: OperationStore;
63
+ }) => (request: Request) => Promise<Response | null>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AgentControlPlane } from "@absolutejs/agency";
2
+ export * from "./console";
2
3
  export type Operator = {
3
4
  id: string;
4
5
  scopes: readonly string[];
package/dist/index.js CHANGED
@@ -1,29 +1,166 @@
1
1
  // @bun
2
+ // src/console.ts
3
+ var hash = async (value) => [
4
+ ...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value))))
5
+ ].map((byte) => byte.toString(16).padStart(2, "0")).join("");
6
+ var bounded = (value, field) => {
7
+ if (typeof value !== "string" || value.length > 2000)
8
+ throw new Error(`Invalid agent control snapshot ${field}`);
9
+ return value;
10
+ };
11
+ var boundedNumber = (value, field) => {
12
+ if (typeof value !== "number" || !Number.isFinite(value))
13
+ throw new Error(`Invalid agent control snapshot ${field}`);
14
+ return value;
15
+ };
16
+ var rows = (value, map) => {
17
+ if (!Array.isArray(value) || value.length > 1000)
18
+ throw new Error("Agent control snapshot collection is too large");
19
+ return value.map(map);
20
+ };
21
+ var sanitizeAgentControlSnapshot = (source) => ({
22
+ agents: rows(source.agents, (item) => ({
23
+ ...item.description === undefined ? {} : { description: bounded(item.description, "agent.description") },
24
+ id: bounded(item.id, "agent.id"),
25
+ status: bounded(item.status, "agent.status")
26
+ })),
27
+ approvals: rows(source.approvals, (item) => ({
28
+ action: bounded(item.action, "approval.action"),
29
+ agentId: bounded(item.agentId, "approval.agentId"),
30
+ id: bounded(item.id, "approval.id"),
31
+ requestedAt: bounded(item.requestedAt, "approval.requestedAt"),
32
+ ...item.risk === undefined ? {} : { risk: bounded(item.risk, "approval.risk") },
33
+ summary: bounded(item.summary, "approval.summary")
34
+ })),
35
+ delegations: rows(source.delegations, (item) => ({
36
+ ...item.expiresAt === undefined ? {} : { expiresAt: bounded(item.expiresAt, "delegation.expiresAt") },
37
+ from: bounded(item.from, "delegation.from"),
38
+ id: bounded(item.id, "delegation.id"),
39
+ scopes: rows(item.scopes, (scope) => bounded(scope, "delegation.scope")),
40
+ to: bounded(item.to, "delegation.to")
41
+ })),
42
+ memories: rows(source.memories, (item) => ({
43
+ agentId: bounded(item.agentId, "memory.agentId"),
44
+ id: bounded(item.id, "memory.id"),
45
+ scope: bounded(item.scope, "memory.scope"),
46
+ updatedAt: bounded(item.updatedAt, "memory.updatedAt")
47
+ })),
48
+ reputations: rows(source.reputations, (item) => ({
49
+ confidence: boundedNumber(item.confidence, "reputation.confidence"),
50
+ scope: bounded(item.scope, "reputation.scope"),
51
+ score: boundedNumber(item.score, "reputation.score"),
52
+ subject: bounded(item.subject, "reputation.subject")
53
+ })),
54
+ runs: rows(source.runs, (item) => ({
55
+ agentId: bounded(item.agentId, "run.agentId"),
56
+ ...item.budget === undefined ? {} : {
57
+ budget: {
58
+ limit: boundedNumber(item.budget.limit, "run.budget.limit"),
59
+ spent: boundedNumber(item.budget.spent, "run.budget.spent"),
60
+ unit: bounded(item.budget.unit, "run.budget.unit")
61
+ }
62
+ },
63
+ id: bounded(item.id, "run.id"),
64
+ startedAt: bounded(item.startedAt, "run.startedAt"),
65
+ status: bounded(item.status, "run.status")
66
+ }))
67
+ });
68
+ var page = (nonce) => `<!doctype html>
69
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
70
+ <title>AbsoluteJS Agent Control</title><style nonce="${nonce}">
71
+ :root{color-scheme:dark;font:15px/1.45 system-ui,sans-serif;background:#09090b;color:#fafafa}body{margin:0}header{padding:1.2rem 1.5rem;border-bottom:1px solid #27272a;display:flex;align-items:center;justify-content:space-between}main{padding:1.5rem;display:grid;gap:1.25rem}.status{color:#a1a1aa}.error{color:#fca5a5}section{background:#18181b;border:1px solid #27272a;border-radius:.75rem;overflow:auto}h2{font-size:1rem;margin:0;padding:1rem}table{border-collapse:collapse;width:100%;min-width:42rem}th,td{text-align:left;padding:.7rem 1rem;border-top:1px solid #27272a;vertical-align:top}th{color:#a1a1aa;font-size:.8rem;text-transform:uppercase}button{background:#27272a;color:#fff;border:1px solid #3f3f46;border-radius:.4rem;padding:.4rem .65rem;margin:.15rem;cursor:pointer}button.approve{background:#14532d}button.deny{background:#7f1d1d}code{font-size:.8rem} @media(max-width:700px){main{padding:.75rem}header{padding:1rem}}
72
+ </style></head><body><header><strong>AbsoluteJS Agent Control</strong><span id="status" class="status">Loading\u2026</span></header><main id="content"></main>
73
+ <script nonce="${nonce}">(()=>{const base=location.pathname.replace(//$/,"");const root=document.querySelector("#content"),status=document.querySelector("#status");const columns={agents:["id","status","description"],approvals:["id","agentId","action","summary","risk","requestedAt"],runs:["id","agentId","status","startedAt","budget"],delegations:["id","from","to","scopes","expiresAt"],memories:["id","agentId","scope","updatedAt"],reputations:["subject","scope","score","confidence"]};const text=v=>v==null?"":typeof v==="object"?JSON.stringify(v):String(v);function render(data){root.replaceChildren();for(const [name,fields] of Object.entries(columns)){const section=document.createElement("section"),heading=document.createElement("h2"),table=document.createElement("table"),head=document.createElement("tr");heading.textContent=name[0].toUpperCase()+name.slice(1);for(const field of fields){const th=document.createElement("th");th.textContent=field;head.append(th)}if(name==="approvals"){const th=document.createElement("th");th.textContent="Decision";head.append(th)}table.append(head);for(const item of data[name]??[]){const row=document.createElement("tr");for(const field of fields){const td=document.createElement("td");td.textContent=text(item[field]);row.append(td)}if(name==="approvals"){const td=document.createElement("td");for(const decision of ["approve","deny"]){const button=document.createElement("button");button.className=decision;button.textContent=decision;button.addEventListener("click",()=>decide(item.id,decision));td.append(button)}row.append(td)}table.append(row)}section.append(heading,table);root.append(section)}}async function load(){try{const response=await fetch(base+"/api/snapshot",{headers:{accept:"application/json"}});if(!response.ok)throw new Error("Snapshot failed: "+response.status);render(await response.json());status.textContent="Live";status.className="status"}catch(error){status.textContent=error.message;status.className="error"}}async function decide(id,decision){const reason=prompt("Reason for "+decision+":");if(!reason)return;status.textContent="Applying\u2026";const response=await fetch(base+"/api/approvals/"+encodeURIComponent(id)+"/"+decision,{method:"POST",headers:{"content-type":"application/json","idempotency-key":crypto.randomUUID(),"x-agent-control-intent":"mutate"},body:JSON.stringify({reason})});if(!response.ok){status.textContent="Decision failed: "+response.status;status.className="error";return}await load()}load()})();</script></body></html>`;
74
+ var createAgentControlConsoleHandler = (options) => {
75
+ const basePath = (options.basePath ?? "/agent-control").replace(/\/$/u, "");
76
+ const now = options.now ?? Date.now;
77
+ return async (request) => {
78
+ const url = new URL(request.url);
79
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
80
+ return null;
81
+ const operator = await options.authorize(request);
82
+ if (!operator)
83
+ return new Response("Unauthorized", { status: 401 });
84
+ const required = request.method === "GET" ? "agents:read" : "agents:approve";
85
+ if (!operator.scopes.includes(required))
86
+ return new Response("Forbidden", { status: 403 });
87
+ if (request.method === "GET" && (url.pathname === basePath || url.pathname === `${basePath}/`)) {
88
+ const nonce = crypto.randomUUID().replaceAll("-", "");
89
+ return new Response(page(nonce), {
90
+ headers: {
91
+ "cache-control": "no-store",
92
+ "content-security-policy": `default-src 'none'; connect-src 'self'; img-src 'self'; style-src 'nonce-${nonce}'; script-src 'nonce-${nonce}'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'`,
93
+ "content-type": "text/html; charset=utf-8",
94
+ "referrer-policy": "no-referrer",
95
+ "x-content-type-options": "nosniff"
96
+ }
97
+ });
98
+ }
99
+ if (request.method === "GET" && url.pathname === `${basePath}/api/snapshot`) {
100
+ return Response.json(sanitizeAgentControlSnapshot(await options.data.snapshot(operator)), {
101
+ headers: { "cache-control": "no-store" }
102
+ });
103
+ }
104
+ const match = new RegExp(`^${basePath.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/api/approvals/([^/]+)/(approve|deny)$`, "u").exec(url.pathname);
105
+ if (!match?.[1] || !match[2] || request.method !== "POST")
106
+ return new Response(null, { status: 405 });
107
+ if (request.headers.get("origin") !== url.origin || request.headers.get("x-agent-control-intent") !== "mutate")
108
+ return new Response("Cross-site mutation denied", { status: 403 });
109
+ const operationId = request.headers.get("idempotency-key");
110
+ const body = await request.json().catch(() => {
111
+ return;
112
+ });
113
+ if (!operationId || !body?.reason || body.reason.length > 1000)
114
+ return Response.json({ error: "Idempotency-Key and a bounded reason are required" }, { status: 400 });
115
+ const input = {
116
+ approvalId: decodeURIComponent(match[1]),
117
+ decision: match[2],
118
+ operatorId: operator.id,
119
+ reason: body.reason
120
+ };
121
+ const digest = await hash(input);
122
+ let claim;
123
+ try {
124
+ claim = await options.operations.claim(operationId, digest, now(), 30000);
125
+ } catch (error) {
126
+ return Response.json({ error: error instanceof Error ? error.message : "conflict" }, { status: 409 });
127
+ }
128
+ if (claim.response !== undefined)
129
+ return Response.json(claim.response);
130
+ if (!claim.acquired)
131
+ return Response.json({ error: "operation in progress" }, { status: 409, headers: { "retry-after": "5" } });
132
+ const response = await options.data.decideApproval(input);
133
+ if (!await options.operations.complete(operationId, digest, response))
134
+ throw new Error("Approval operation completion lost");
135
+ return Response.json(response);
136
+ };
137
+ };
138
+
2
139
  // src/index.ts
3
140
  var createMemoryOperationStore = () => {
4
- const rows = new Map;
141
+ const rows2 = new Map;
5
142
  return {
6
143
  claim: async (key, digest, now, leaseMs) => {
7
- const row = rows.get(key);
144
+ const row = rows2.get(key);
8
145
  if (row?.digest !== undefined && row.digest !== digest)
9
146
  throw new Error("Idempotency key reused with different input");
10
147
  if (row?.response !== undefined)
11
148
  return { acquired: false, response: structuredClone(row.response) };
12
149
  if (row && row.leaseUntil > now)
13
150
  return { acquired: false };
14
- rows.set(key, { digest, key, leaseUntil: now + leaseMs });
151
+ rows2.set(key, { digest, key, leaseUntil: now + leaseMs });
15
152
  return { acquired: true };
16
153
  },
17
154
  complete: async (key, digest, response) => {
18
- const row = rows.get(key);
155
+ const row = rows2.get(key);
19
156
  if (!row || row.digest !== digest)
20
157
  return false;
21
- rows.set(key, { ...row, response: structuredClone(response) });
158
+ rows2.set(key, { ...row, response: structuredClone(response) });
22
159
  return true;
23
160
  }
24
161
  };
25
162
  };
26
- var hash = async (value) => [
163
+ var hash2 = async (value) => [
27
164
  ...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(value))))
28
165
  ].map((byte) => byte.toString(16).padStart(2, "0")).join("");
29
166
  var createAgentControlHandler = ({
@@ -54,7 +191,7 @@ var createAgentControlHandler = ({
54
191
  });
55
192
  if (!body?.operationId || !body.reason)
56
193
  return Response.json({ error: "operationId and reason are required" }, { status: 400 });
57
- const digest = await hash({ action, agentId, reason: body.reason });
194
+ const digest = await hash2({ action, agentId, reason: body.reason });
58
195
  let claim;
59
196
  try {
60
197
  claim = await operations.claim(body.operationId, digest, now(), 30000);
@@ -107,8 +244,10 @@ var createPostgresOperationStore = ({
107
244
  };
108
245
  };
109
246
  export {
247
+ sanitizeAgentControlSnapshot,
110
248
  createPostgresOperationStore,
111
249
  createMemoryOperationStore,
112
250
  createAgentControlHandler,
251
+ createAgentControlConsoleHandler,
113
252
  agentControlPostgresSchemaSql
114
253
  };
package/dist/manifest.js CHANGED
@@ -5901,6 +5901,14 @@ var serializedTool = Type.Object({
5901
5901
  });
5902
5902
  var manifestSchema = Type.Object({
5903
5903
  contract: Type.Union([Type.Literal(1), Type.Literal(2)]),
5904
+ discovery: Type.Optional(Type.Object({
5905
+ audiences: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5906
+ certificationUrl: Type.Optional(Type.String()),
5907
+ intents: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5908
+ keywords: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5909
+ protocols: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
5910
+ url: Type.Optional(Type.String())
5911
+ })),
5904
5912
  identity: Type.Object({
5905
5913
  accent: Type.Optional(Type.String({ pattern: "^#[0-9a-fA-F]{3,8}$" })),
5906
5914
  category: Type.String({ minLength: 1 }),
@@ -5935,7 +5943,7 @@ var manifest = defineManifest()({
5935
5943
  identity: {
5936
5944
  accent: "#dc2626",
5937
5945
  category: "security",
5938
- description: "Authenticated operator API for agent inventory, durable kill switches, source revocation, restoration, and leased idempotent operations.",
5946
+ description: "Authenticated operator API and CSP-hardened console for agent inventory, approvals, runs, budgets, delegations, memory metadata, reputation, durable kill switches, and leased idempotent operations.",
5939
5947
  docsUrl: "https://github.com/absolutejs/agent-control",
5940
5948
  name: "@absolutejs/agent-control",
5941
5949
  tagline: "See and stop every capability an agent holds."
@@ -3,7 +3,7 @@
3
3
  "identity": {
4
4
  "accent": "#dc2626",
5
5
  "category": "security",
6
- "description": "Authenticated operator API for agent inventory, durable kill switches, source revocation, restoration, and leased idempotent operations.",
6
+ "description": "Authenticated operator API and CSP-hardened console for agent inventory, 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."
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@absolutejs/agent-control",
3
- "version": "0.1.0",
4
- "description": "Authenticated operator API for AI agent inventory, kill switches, revocation, and restoration.",
3
+ "version": "0.2.0",
4
+ "description": "Authenticated AI agent operator API and console for approvals, runs, delegations, memory, reputation, and kill switches.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/absolutejs/agent-control.git"
9
+ "url": "git+https://github.com/absolutejs/agent-control.git"
10
10
  },
11
11
  "publishConfig": {
12
12
  "access": "public"
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@absolutejs/agency": "^0.4.0",
44
- "@absolutejs/manifest": "^0.2.0",
44
+ "@absolutejs/manifest": "^0.3.0",
45
45
  "@sinclair/typebox": "^0.34.0"
46
46
  },
47
47
  "devDependencies": {