openclaw-zero-agency 0.1.0 → 0.2.1
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/dist/gate.js +51 -0
- package/dist/index.js +70 -99
- package/dist/receipts.js +22 -0
- package/dist/tools.js +143 -0
- package/openclaw.plugin.json +5 -4
- package/package.json +17 -4
package/dist/gate.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Zero Agency receipt gate. Consequential tool calls require a routing receipt.
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { get, prune } from "./receipts.js";
|
|
6
|
+
async function telemetry(path, record) {
|
|
7
|
+
if (!path)
|
|
8
|
+
return;
|
|
9
|
+
try {
|
|
10
|
+
await mkdir(dirname(path), { recursive: true });
|
|
11
|
+
await appendFile(path, JSON.stringify({ ts: new Date().toISOString(), ...record }) + "\n");
|
|
12
|
+
}
|
|
13
|
+
catch { /* never break a turn */ }
|
|
14
|
+
}
|
|
15
|
+
export default definePluginEntry({
|
|
16
|
+
id: "zero-agency/gate",
|
|
17
|
+
name: "Zero Agency Gate",
|
|
18
|
+
description: "Requires a Zero Agency routing receipt before consequential tool calls.",
|
|
19
|
+
register(api) {
|
|
20
|
+
const cfg = api.config ?? api.pluginConfig ?? {};
|
|
21
|
+
const mode = cfg.gateMode ?? "advisory";
|
|
22
|
+
const tools = cfg.consequentialTools ?? ["exec", "apply_patch"];
|
|
23
|
+
const tel = cfg.telemetryPath;
|
|
24
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
25
|
+
if (mode === "off")
|
|
26
|
+
return undefined;
|
|
27
|
+
const runId = event?.runId ?? ctx?.runId ?? "unknown-run";
|
|
28
|
+
prune();
|
|
29
|
+
const r = get(runId);
|
|
30
|
+
if (r) {
|
|
31
|
+
await telemetry(tel, { kind: "gate", decision: "receipt-ok", tool: event?.toolName, runId, receipt: r.id });
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
await telemetry(tel, { kind: "gate", decision: mode === "enforce" ? "blocked" : "advisory-miss", tool: event?.toolName, runId });
|
|
35
|
+
if (mode !== "enforce")
|
|
36
|
+
return undefined;
|
|
37
|
+
return {
|
|
38
|
+
requireApproval: {
|
|
39
|
+
title: `Zero Agency: no capability receipt (${event?.toolName ?? "tool"})`,
|
|
40
|
+
description: "Consequential call without a capability lookup. Approve for this call only, or deny and run zero_agency_route first.",
|
|
41
|
+
severity: "warning",
|
|
42
|
+
timeoutMs: 120000,
|
|
43
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
44
|
+
onResolution: async (decision) => {
|
|
45
|
+
await telemetry(tel, { kind: "gate", decision, tool: event?.toolName, runId, receipt: null });
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}, { matcher: tools, priority: 50 });
|
|
50
|
+
},
|
|
51
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,50 +1,61 @@
|
|
|
1
|
-
// Zero Agency — capability routing
|
|
1
|
+
// Zero Agency — single-entry plugin: capability routing tools + receipt gate.
|
|
2
2
|
// Packs inform judgement; they never confer authority.
|
|
3
|
+
//
|
|
4
|
+
// NOTE: kept as one entry on purpose. A hook-bearing plugin cannot pass
|
|
5
|
+
// `openclaw plugins validate` (it requires defineToolPlugin metadata), and a
|
|
6
|
+
// multi-entry package cannot satisfy the manifest-id check. Runtime load is the
|
|
7
|
+
// authoritative test for this package.
|
|
3
8
|
import { Type } from "typebox";
|
|
4
9
|
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
5
10
|
import { readFile, appendFile, mkdir } from "node:fs/promises";
|
|
11
|
+
import { statSync } from "node:fs";
|
|
6
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
13
|
import { dirname } from "node:path";
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
14
|
+
const KEY = "openclaw.zero-agency.receipts.v1";
|
|
15
|
+
function store() {
|
|
16
|
+
const g = globalThis;
|
|
17
|
+
if (!g[KEY])
|
|
18
|
+
g[KEY] = new Map();
|
|
19
|
+
return g[KEY];
|
|
20
|
+
}
|
|
21
|
+
const prune = () => { const t = Date.now(); for (const [k, v] of store())
|
|
22
|
+
if (v.expires < t)
|
|
23
|
+
store().delete(k); };
|
|
24
|
+
const cache = new Map();
|
|
11
25
|
const norm = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim();
|
|
12
26
|
async function loadRegistry(path) {
|
|
13
|
-
const { statSync } = await import("node:fs");
|
|
14
27
|
const st = statSync(path);
|
|
15
|
-
const hit =
|
|
28
|
+
const hit = cache.get(path);
|
|
16
29
|
if (hit && hit.mtimeMs === st.mtimeMs)
|
|
17
30
|
return hit.data;
|
|
18
31
|
const data = JSON.parse(await readFile(path, "utf8"));
|
|
19
|
-
|
|
32
|
+
cache.set(path, { mtimeMs: st.mtimeMs, data });
|
|
20
33
|
return data;
|
|
21
34
|
}
|
|
22
|
-
function
|
|
23
|
-
let
|
|
35
|
+
function score(cap, q) {
|
|
36
|
+
let s = 0;
|
|
24
37
|
const hits = [];
|
|
25
38
|
for (const t of cap.triggers ?? []) {
|
|
26
39
|
const nt = norm(t);
|
|
27
40
|
if (nt && q.includes(nt)) {
|
|
28
|
-
|
|
41
|
+
s += 3;
|
|
29
42
|
hits.push(t);
|
|
43
|
+
continue;
|
|
30
44
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
hits.push(t);
|
|
37
|
-
}
|
|
38
|
-
else if (overlap > 0) {
|
|
39
|
-
score += overlap / words.length;
|
|
40
|
-
}
|
|
45
|
+
const words = nt.split(" ").filter((w) => w.length > 3);
|
|
46
|
+
const overlap = words.filter((w) => q.includes(w)).length;
|
|
47
|
+
if (words.length && overlap === words.length) {
|
|
48
|
+
s += 2;
|
|
49
|
+
hits.push(t);
|
|
41
50
|
}
|
|
51
|
+
else if (overlap)
|
|
52
|
+
s += overlap / words.length;
|
|
42
53
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return { score, hits };
|
|
54
|
+
const dw = norm(cap.description).split(" ").filter((w) => w.length > 4);
|
|
55
|
+
s += 0.5 * dw.filter((w) => q.includes(w)).length;
|
|
56
|
+
return { score: s, hits };
|
|
46
57
|
}
|
|
47
|
-
async function
|
|
58
|
+
async function telemetry(path, record) {
|
|
48
59
|
if (!path)
|
|
49
60
|
return;
|
|
50
61
|
try {
|
|
@@ -53,12 +64,6 @@ async function writeTelemetry(path, record) {
|
|
|
53
64
|
}
|
|
54
65
|
catch { /* telemetry must never break a turn */ }
|
|
55
66
|
}
|
|
56
|
-
function prune(ttl) {
|
|
57
|
-
const t = now();
|
|
58
|
-
for (const [k, v] of receipts)
|
|
59
|
-
if (v.expires < t)
|
|
60
|
-
receipts.delete(k);
|
|
61
|
-
}
|
|
62
67
|
export default definePluginEntry({
|
|
63
68
|
id: "zero-agency",
|
|
64
69
|
name: "Zero Agency",
|
|
@@ -66,121 +71,87 @@ export default definePluginEntry({
|
|
|
66
71
|
register(api) {
|
|
67
72
|
const cfg = api.config ?? api.pluginConfig ?? {};
|
|
68
73
|
const registryPath = cfg.registryPath ?? "";
|
|
69
|
-
const telemetryPath = cfg.telemetryPath
|
|
74
|
+
const telemetryPath = cfg.telemetryPath;
|
|
70
75
|
const gateMode = cfg.gateMode ?? "advisory";
|
|
71
|
-
const
|
|
72
|
-
const ttl = cfg.receiptTtlMs ??
|
|
76
|
+
const consequential = cfg.consequentialTools ?? ["exec", "apply_patch"];
|
|
77
|
+
const ttl = cfg.receiptTtlMs ?? 900000;
|
|
73
78
|
api.registerTool({
|
|
74
79
|
name: "zero_agency_route",
|
|
75
|
-
description: "Find the
|
|
80
|
+
description: "Find the capability pack governing a domain task. Call before acting on security review, release verification or incident triage. Issues a receipt the gate accepts.",
|
|
76
81
|
parameters: Type.Object({
|
|
77
82
|
task: Type.String({ description: "The task or question, in natural language." }),
|
|
78
83
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5 })),
|
|
79
84
|
}),
|
|
80
85
|
outputSchema: Type.Object({
|
|
81
|
-
candidates: Type.Array(Type.Object({
|
|
82
|
-
id: Type.String(), pack: Type.String(), score: Type.Number(), triggers: Type.Array(Type.String()),
|
|
83
|
-
})),
|
|
86
|
+
candidates: Type.Array(Type.Object({ id: Type.String(), pack: Type.String(), score: Type.Number(), triggers: Type.Array(Type.String()) })),
|
|
84
87
|
receipt: Type.Union([Type.String(), Type.Null()]),
|
|
85
88
|
note: Type.String(),
|
|
86
89
|
}, { additionalProperties: false }),
|
|
87
90
|
async execute(_id, params, ctx) {
|
|
88
91
|
const q = norm(params.task);
|
|
89
|
-
const limit = params.limit ?? 3;
|
|
90
92
|
let candidates = [];
|
|
91
93
|
let note = "";
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
try {
|
|
95
|
+
const reg = await loadRegistry(registryPath);
|
|
96
|
+
candidates = (reg.capabilities ?? [])
|
|
97
|
+
.map((c) => ({ c, ...score(c, q) }))
|
|
98
|
+
.filter((x) => x.score > 0)
|
|
99
|
+
.sort((a, b) => b.score - a.score)
|
|
100
|
+
.slice(0, params.limit ?? 3)
|
|
101
|
+
.map((x) => ({ id: x.c.id, pack: x.c.pack, score: Number(x.score.toFixed(2)), triggers: x.hits }));
|
|
102
|
+
note = candidates.length ? `load ${candidates[0].pack} before acting` : "no capability matched; if this action is consequential, stop and ask";
|
|
94
103
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const reg = await loadRegistry(registryPath);
|
|
98
|
-
candidates = (reg.capabilities ?? [])
|
|
99
|
-
.map((c) => ({ c, ...scoreCapability(c, q) }))
|
|
100
|
-
.filter((x) => x.score > 0)
|
|
101
|
-
.sort((a, b) => b.score - a.score)
|
|
102
|
-
.slice(0, limit)
|
|
103
|
-
.map((x) => ({ id: x.c.id, pack: x.c.pack, score: Number(x.score.toFixed(2)), triggers: x.hits }));
|
|
104
|
-
if (!candidates.length)
|
|
105
|
-
note = "no capability matched; if this action is consequential, stop and ask";
|
|
106
|
-
else
|
|
107
|
-
note = `load ${candidates[0].pack} before acting`;
|
|
108
|
-
}
|
|
109
|
-
catch (e) {
|
|
110
|
-
note = `registry unavailable: ${e.message}`;
|
|
111
|
-
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
note = `registry unavailable: ${e?.message ?? "unknown"}`;
|
|
112
106
|
}
|
|
113
107
|
let receipt = null;
|
|
114
108
|
if (candidates.length) {
|
|
115
|
-
const runId = ctx?.runId ?? "unknown-run";
|
|
116
|
-
receipt = createHash("sha256").update(`${runId}:${candidates[0].id}:${now()}:${randomUUID()}`).digest("hex").slice(0, 16);
|
|
117
|
-
|
|
118
|
-
prune(
|
|
109
|
+
const runId = ctx?.runId ?? ctx?.sessionId ?? "unknown-run";
|
|
110
|
+
receipt = createHash("sha256").update(`${runId}:${candidates[0].id}:${Date.now()}:${randomUUID()}`).digest("hex").slice(0, 16);
|
|
111
|
+
store().set(runId, { id: receipt, pack: candidates[0].pack, at: Date.now(), expires: Date.now() + ttl });
|
|
112
|
+
prune();
|
|
119
113
|
}
|
|
120
|
-
await
|
|
121
|
-
|
|
122
|
-
session: ctx?.sessionKey ?? null, query: params.task.slice(0, 200),
|
|
123
|
-
selected: candidates[0]?.id ?? null, candidates: candidates.length, receipt,
|
|
124
|
-
});
|
|
125
|
-
return {
|
|
126
|
-
content: [{ type: "text", text: JSON.stringify({ candidates, receipt, note }) }],
|
|
127
|
-
details: { candidates, receipt, note },
|
|
128
|
-
};
|
|
114
|
+
await telemetry(telemetryPath, { kind: "route", agent: ctx?.agentId ?? null, session: ctx?.sessionKey ?? null, query: params.task.slice(0, 200), selected: candidates[0]?.id ?? null, candidates: candidates.length, receipt });
|
|
115
|
+
return { content: [{ type: "text", text: JSON.stringify({ candidates, receipt, note }) }], details: { candidates, receipt, note } };
|
|
129
116
|
},
|
|
130
117
|
});
|
|
131
118
|
api.registerTool({
|
|
132
119
|
name: "zero_agency_status",
|
|
133
|
-
description: "Report Zero Agency registry
|
|
120
|
+
description: "Report Zero Agency registry state: capability count, token total and live receipts.",
|
|
134
121
|
parameters: Type.Object({}),
|
|
135
|
-
outputSchema: Type.Object({
|
|
136
|
-
gateMode: Type.String(), registry: Type.String(), capabilities: Type.Integer(),
|
|
137
|
-
receipts: Type.Integer(), registryError: Type.Union([Type.String(), Type.Null()]),
|
|
138
|
-
}, { additionalProperties: false }),
|
|
122
|
+
outputSchema: Type.Object({ registry: Type.String(), capabilities: Type.Integer(), tokens: Type.Integer(), receipts: Type.Integer(), registryError: Type.Union([Type.String(), Type.Null()]) }, { additionalProperties: false }),
|
|
139
123
|
async execute() {
|
|
140
|
-
let capabilities = 0;
|
|
141
|
-
let registryError = null;
|
|
142
124
|
try {
|
|
143
125
|
const reg = await loadRegistry(registryPath);
|
|
144
|
-
|
|
126
|
+
const caps = reg.capabilities ?? [];
|
|
127
|
+
return { content: [{ type: "text", text: `zero-agency: ${caps.length} capabilities` }], details: { registry: registryPath, capabilities: caps.length, tokens: caps.reduce((s, c) => s + (c.tokens ?? 0), 0), receipts: store().size, registryError: null } };
|
|
145
128
|
}
|
|
146
129
|
catch (e) {
|
|
147
|
-
registryError
|
|
130
|
+
return { content: [{ type: "text", text: `zero-agency: registry error` }], details: { registry: registryPath, capabilities: 0, tokens: 0, receipts: store().size, registryError: e?.message ?? "unknown" } };
|
|
148
131
|
}
|
|
149
|
-
return {
|
|
150
|
-
content: [{ type: "text", text: `zero-agency: ${capabilities} capabilities, gate=${gateMode}` }],
|
|
151
|
-
details: { gateMode, registry: registryPath, capabilities, receipts: receipts.size, registryError },
|
|
152
|
-
};
|
|
153
132
|
},
|
|
154
133
|
});
|
|
155
|
-
// Receipt gate. Fail-closed in enforce mode: no receipt, no consequential call.
|
|
156
134
|
api.on("before_tool_call", async (event, ctx) => {
|
|
157
135
|
if (gateMode === "off")
|
|
158
136
|
return undefined;
|
|
159
137
|
const runId = event?.runId ?? ctx?.runId ?? "unknown-run";
|
|
160
|
-
prune(
|
|
161
|
-
const r =
|
|
138
|
+
prune();
|
|
139
|
+
const r = store().get(runId);
|
|
162
140
|
if (r) {
|
|
163
|
-
await
|
|
141
|
+
await telemetry(telemetryPath, { kind: "gate", decision: "receipt-ok", tool: event?.toolName, runId, receipt: r.id });
|
|
164
142
|
return undefined;
|
|
165
143
|
}
|
|
166
|
-
await
|
|
167
|
-
kind: "gate", decision: gateMode === "enforce" ? "blocked" : "advisory-miss",
|
|
168
|
-
tool: event?.toolName, runId,
|
|
169
|
-
});
|
|
144
|
+
await telemetry(telemetryPath, { kind: "gate", decision: gateMode === "enforce" ? "blocked" : "advisory-miss", tool: event?.toolName, runId });
|
|
170
145
|
if (gateMode !== "enforce")
|
|
171
146
|
return undefined;
|
|
172
147
|
return {
|
|
173
148
|
requireApproval: {
|
|
174
149
|
title: `Zero Agency: no capability receipt (${event?.toolName ?? "tool"})`,
|
|
175
|
-
description: "
|
|
176
|
-
severity: "warning",
|
|
177
|
-
|
|
178
|
-
allowedDecisions: ["allow-once", "deny"],
|
|
179
|
-
onResolution: async (decision) => {
|
|
180
|
-
await writeTelemetry(telemetryPath, { kind: "gate", decision, tool: event?.toolName, runId, receipt: null });
|
|
181
|
-
},
|
|
150
|
+
description: "Consequential call without a capability lookup. Approve for this call only, or deny and run zero_agency_route first.",
|
|
151
|
+
severity: "warning", timeoutMs: 120000, allowedDecisions: ["allow-once", "deny"],
|
|
152
|
+
onResolution: async (decision) => { await telemetry(telemetryPath, { kind: "gate", decision, tool: event?.toolName, runId, receipt: null }); },
|
|
182
153
|
},
|
|
183
154
|
};
|
|
184
|
-
}, { matcher:
|
|
155
|
+
}, { matcher: consequential, priority: 50 });
|
|
185
156
|
},
|
|
186
157
|
});
|
package/dist/receipts.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const KEY = Symbol.for("openclaw.zero-agency.receipts.v1");
|
|
2
|
+
function store() {
|
|
3
|
+
const g = globalThis;
|
|
4
|
+
if (!g[KEY])
|
|
5
|
+
g[KEY] = new Map();
|
|
6
|
+
return g[KEY];
|
|
7
|
+
}
|
|
8
|
+
export function put(runId, receipt) {
|
|
9
|
+
store().set(runId, receipt);
|
|
10
|
+
}
|
|
11
|
+
export function get(runId) {
|
|
12
|
+
return store().get(runId);
|
|
13
|
+
}
|
|
14
|
+
export function prune() {
|
|
15
|
+
const t = Date.now();
|
|
16
|
+
for (const [k, v] of store())
|
|
17
|
+
if (v.expires < t)
|
|
18
|
+
store().delete(k);
|
|
19
|
+
}
|
|
20
|
+
export function size() {
|
|
21
|
+
return store().size;
|
|
22
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Zero Agency — capability routing tools. Packs inform judgement; they never confer authority.
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
|
4
|
+
import { readFile, appendFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { statSync } from "node:fs";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { dirname } from "node:path";
|
|
8
|
+
import { put, prune, size } from "./receipts.js";
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
const norm = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim();
|
|
11
|
+
async function loadRegistry(path) {
|
|
12
|
+
const st = statSync(path);
|
|
13
|
+
const hit = cache.get(path);
|
|
14
|
+
if (hit && hit.mtimeMs === st.mtimeMs)
|
|
15
|
+
return hit.data;
|
|
16
|
+
const data = JSON.parse(await readFile(path, "utf8"));
|
|
17
|
+
cache.set(path, { mtimeMs: st.mtimeMs, data });
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
function score(cap, q) {
|
|
21
|
+
let s = 0;
|
|
22
|
+
const hits = [];
|
|
23
|
+
for (const t of cap.triggers ?? []) {
|
|
24
|
+
const nt = norm(t);
|
|
25
|
+
if (nt && q.includes(nt)) {
|
|
26
|
+
s += 3;
|
|
27
|
+
hits.push(t);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const words = nt.split(" ").filter((w) => w.length > 3);
|
|
31
|
+
const overlap = words.filter((w) => q.includes(w)).length;
|
|
32
|
+
if (words.length && overlap === words.length) {
|
|
33
|
+
s += 2;
|
|
34
|
+
hits.push(t);
|
|
35
|
+
}
|
|
36
|
+
else if (overlap)
|
|
37
|
+
s += overlap / words.length;
|
|
38
|
+
}
|
|
39
|
+
const dw = norm(cap.description).split(" ").filter((w) => w.length > 4);
|
|
40
|
+
s += 0.5 * dw.filter((w) => q.includes(w)).length;
|
|
41
|
+
return { score: s, hits };
|
|
42
|
+
}
|
|
43
|
+
async function telemetry(path, record) {
|
|
44
|
+
if (!path)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
await mkdir(dirname(path), { recursive: true });
|
|
48
|
+
await appendFile(path, JSON.stringify({ ts: new Date().toISOString(), ...record }) + "\n");
|
|
49
|
+
}
|
|
50
|
+
catch { /* telemetry must never break a turn */ }
|
|
51
|
+
}
|
|
52
|
+
export default defineToolPlugin({
|
|
53
|
+
id: "zero-agency/tools",
|
|
54
|
+
name: "Zero Agency",
|
|
55
|
+
description: "Capability routing for the fleet. Packs inform judgement; they never confer authority.",
|
|
56
|
+
configSchema: Type.Object({
|
|
57
|
+
registryPath: Type.String({ description: "Absolute path to capabilities.json (read-only)." }),
|
|
58
|
+
telemetryPath: Type.Optional(Type.String({ description: "JSONL telemetry path." })),
|
|
59
|
+
receiptTtlMs: Type.Optional(Type.Integer({ minimum: 1000 })),
|
|
60
|
+
gateMode: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("advisory"), Type.Literal("enforce")], { description: "Receipt gate mode (default advisory)." })),
|
|
61
|
+
consequentialTools: Type.Optional(Type.Array(Type.String(), { description: "Tool ids gated by receipt." })),
|
|
62
|
+
}),
|
|
63
|
+
tools: (tool) => [
|
|
64
|
+
tool({
|
|
65
|
+
name: "zero_agency_route",
|
|
66
|
+
label: "Zero Agency Route",
|
|
67
|
+
description: "Find the capability pack governing a domain task. Call before acting on security review, release verification or incident triage. Issues a receipt the gate accepts.",
|
|
68
|
+
parameters: Type.Object({
|
|
69
|
+
task: Type.String({ description: "The task or question, in natural language." }),
|
|
70
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5 })),
|
|
71
|
+
}),
|
|
72
|
+
outputSchema: Type.Object({
|
|
73
|
+
candidates: Type.Array(Type.Object({
|
|
74
|
+
id: Type.String(), pack: Type.String(), score: Type.Number(), triggers: Type.Array(Type.String()),
|
|
75
|
+
})),
|
|
76
|
+
receipt: Type.Union([Type.String(), Type.Null()]),
|
|
77
|
+
note: Type.String(),
|
|
78
|
+
}, { additionalProperties: false }),
|
|
79
|
+
async execute(params, config, context) {
|
|
80
|
+
context?.signal?.throwIfAborted?.();
|
|
81
|
+
const q = norm(params.task);
|
|
82
|
+
const limit = params.limit ?? 3;
|
|
83
|
+
const ttl = config.receiptTtlMs ?? 900000;
|
|
84
|
+
let candidates = [];
|
|
85
|
+
let note = "";
|
|
86
|
+
try {
|
|
87
|
+
const reg = await loadRegistry(config.registryPath);
|
|
88
|
+
candidates = (reg.capabilities ?? [])
|
|
89
|
+
.map((c) => ({ c, ...score(c, q) }))
|
|
90
|
+
.filter((x) => x.score > 0)
|
|
91
|
+
.sort((a, b) => b.score - a.score)
|
|
92
|
+
.slice(0, limit)
|
|
93
|
+
.map((x) => ({ id: x.c.id, pack: x.c.pack, score: Number(x.score.toFixed(2)), triggers: x.hits }));
|
|
94
|
+
note = candidates.length
|
|
95
|
+
? `load ${candidates[0].pack} before acting`
|
|
96
|
+
: "no capability matched; if this action is consequential, stop and ask";
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
note = `registry unavailable: ${e?.message ?? "unknown"}`;
|
|
100
|
+
}
|
|
101
|
+
let receipt = null;
|
|
102
|
+
if (candidates.length) {
|
|
103
|
+
const runId = context?.runId ?? context?.sessionId ?? "unknown-run";
|
|
104
|
+
receipt = createHash("sha256").update(`${runId}:${candidates[0].id}:${Date.now()}:${randomUUID()}`).digest("hex").slice(0, 16);
|
|
105
|
+
put(runId, { id: receipt, pack: candidates[0].pack, at: Date.now(), expires: Date.now() + ttl });
|
|
106
|
+
prune();
|
|
107
|
+
}
|
|
108
|
+
await telemetry(config.telemetryPath, {
|
|
109
|
+
kind: "route", agent: context?.agentId ?? null,
|
|
110
|
+
session: context?.sessionId ?? null, query: params.task.slice(0, 200),
|
|
111
|
+
selected: candidates[0]?.id ?? null, candidates: candidates.length, receipt,
|
|
112
|
+
});
|
|
113
|
+
return { candidates, receipt, note };
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
tool({
|
|
117
|
+
name: "zero_agency_status",
|
|
118
|
+
label: "Zero Agency Status",
|
|
119
|
+
description: "Report the Zero Agency registry state: capability count, token total and live receipts.",
|
|
120
|
+
parameters: Type.Object({}),
|
|
121
|
+
outputSchema: Type.Object({
|
|
122
|
+
registry: Type.String(), capabilities: Type.Integer(), tokens: Type.Integer(),
|
|
123
|
+
receipts: Type.Integer(), registryError: Type.Union([Type.String(), Type.Null()]),
|
|
124
|
+
}, { additionalProperties: false }),
|
|
125
|
+
async execute(_params, config) {
|
|
126
|
+
try {
|
|
127
|
+
const reg = await loadRegistry(config.registryPath);
|
|
128
|
+
const caps = reg.capabilities ?? [];
|
|
129
|
+
return {
|
|
130
|
+
registry: config.registryPath,
|
|
131
|
+
capabilities: caps.length,
|
|
132
|
+
tokens: caps.reduce((s, c) => s + (c.tokens ?? 0), 0),
|
|
133
|
+
receipts: size(),
|
|
134
|
+
registryError: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
return { registry: config.registryPath, capabilities: 0, tokens: 0, receipts: size(), registryError: e?.message ?? "unknown" };
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
],
|
|
143
|
+
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
10
|
+
"required": ["registryPath"],
|
|
10
11
|
"properties": {
|
|
11
|
-
"registryPath": { "type": "string", "description": "Absolute path to capabilities.json (read-only
|
|
12
|
-
"telemetryPath": { "type": "string", "description": "
|
|
12
|
+
"registryPath": { "type": "string", "description": "Absolute path to capabilities.json (read-only)." },
|
|
13
|
+
"telemetryPath": { "type": "string", "description": "JSONL telemetry path." },
|
|
14
|
+
"receiptTtlMs": { "type": "integer", "minimum": 1000, "default": 900000 },
|
|
13
15
|
"gateMode": { "type": "string", "enum": ["off", "advisory", "enforce"], "default": "advisory" },
|
|
14
|
-
"consequentialTools": { "type": "array", "items": { "type": "string" }, "default": ["exec", "apply_patch"] }
|
|
15
|
-
"receiptTtlMs": { "type": "integer", "minimum": 1000, "default": 900000 }
|
|
16
|
+
"consequentialTools": { "type": "array", "items": { "type": "string" }, "default": ["exec", "apply_patch"] }
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-zero-agency",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Zero Agency: capability packs, programmatic routing and a receipt gate for OpenClaw. Packs inform judgement; they never confer authority.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"keywords": [
|
|
9
|
-
|
|
8
|
+
"keywords": [
|
|
9
|
+
"openclaw",
|
|
10
|
+
"openclaw-plugin",
|
|
11
|
+
"agents",
|
|
12
|
+
"capability-routing",
|
|
13
|
+
"governance"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"openclaw.plugin.json",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
10
21
|
"scripts": {
|
|
11
22
|
"build": "tsc -p tsconfig.build.json",
|
|
12
23
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -23,7 +34,9 @@
|
|
|
23
34
|
"typescript": "^7.0.2"
|
|
24
35
|
},
|
|
25
36
|
"openclaw": {
|
|
26
|
-
"extensions": [
|
|
37
|
+
"extensions": [
|
|
38
|
+
"./dist/index.js"
|
|
39
|
+
],
|
|
27
40
|
"compat": {
|
|
28
41
|
"pluginApi": ">=2026.10.1",
|
|
29
42
|
"minGatewayVersion": "2026.10.1"
|