openclaw-zero-agency 0.2.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 +1 -1
- package/dist/index.js +157 -0
- package/dist/tools.js +1 -1
- package/openclaw.plugin.json +10 -50
- package/package.json +2 -3
package/dist/gate.js
CHANGED
|
@@ -13,7 +13,7 @@ async function telemetry(path, record) {
|
|
|
13
13
|
catch { /* never break a turn */ }
|
|
14
14
|
}
|
|
15
15
|
export default definePluginEntry({
|
|
16
|
-
id: "zero-agency
|
|
16
|
+
id: "zero-agency/gate",
|
|
17
17
|
name: "Zero Agency Gate",
|
|
18
18
|
description: "Requires a Zero Agency routing receipt before consequential tool calls.",
|
|
19
19
|
register(api) {
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Zero Agency — single-entry plugin: capability routing tools + receipt gate.
|
|
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.
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
10
|
+
import { readFile, appendFile, mkdir } from "node:fs/promises";
|
|
11
|
+
import { statSync } from "node:fs";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import { dirname } from "node:path";
|
|
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();
|
|
25
|
+
const norm = (s) => String(s ?? "").toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim();
|
|
26
|
+
async function loadRegistry(path) {
|
|
27
|
+
const st = statSync(path);
|
|
28
|
+
const hit = cache.get(path);
|
|
29
|
+
if (hit && hit.mtimeMs === st.mtimeMs)
|
|
30
|
+
return hit.data;
|
|
31
|
+
const data = JSON.parse(await readFile(path, "utf8"));
|
|
32
|
+
cache.set(path, { mtimeMs: st.mtimeMs, data });
|
|
33
|
+
return data;
|
|
34
|
+
}
|
|
35
|
+
function score(cap, q) {
|
|
36
|
+
let s = 0;
|
|
37
|
+
const hits = [];
|
|
38
|
+
for (const t of cap.triggers ?? []) {
|
|
39
|
+
const nt = norm(t);
|
|
40
|
+
if (nt && q.includes(nt)) {
|
|
41
|
+
s += 3;
|
|
42
|
+
hits.push(t);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
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);
|
|
50
|
+
}
|
|
51
|
+
else if (overlap)
|
|
52
|
+
s += overlap / words.length;
|
|
53
|
+
}
|
|
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 };
|
|
57
|
+
}
|
|
58
|
+
async function telemetry(path, record) {
|
|
59
|
+
if (!path)
|
|
60
|
+
return;
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(dirname(path), { recursive: true });
|
|
63
|
+
await appendFile(path, JSON.stringify({ ts: new Date().toISOString(), ...record }) + "\n");
|
|
64
|
+
}
|
|
65
|
+
catch { /* telemetry must never break a turn */ }
|
|
66
|
+
}
|
|
67
|
+
export default definePluginEntry({
|
|
68
|
+
id: "zero-agency",
|
|
69
|
+
name: "Zero Agency",
|
|
70
|
+
description: "Capability routing with a receipt gate. Packs inform judgement; they never confer authority.",
|
|
71
|
+
register(api) {
|
|
72
|
+
const cfg = api.config ?? api.pluginConfig ?? {};
|
|
73
|
+
const registryPath = cfg.registryPath ?? "";
|
|
74
|
+
const telemetryPath = cfg.telemetryPath;
|
|
75
|
+
const gateMode = cfg.gateMode ?? "advisory";
|
|
76
|
+
const consequential = cfg.consequentialTools ?? ["exec", "apply_patch"];
|
|
77
|
+
const ttl = cfg.receiptTtlMs ?? 900000;
|
|
78
|
+
api.registerTool({
|
|
79
|
+
name: "zero_agency_route",
|
|
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.",
|
|
81
|
+
parameters: Type.Object({
|
|
82
|
+
task: Type.String({ description: "The task or question, in natural language." }),
|
|
83
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 5 })),
|
|
84
|
+
}),
|
|
85
|
+
outputSchema: Type.Object({
|
|
86
|
+
candidates: Type.Array(Type.Object({ id: Type.String(), pack: Type.String(), score: Type.Number(), triggers: Type.Array(Type.String()) })),
|
|
87
|
+
receipt: Type.Union([Type.String(), Type.Null()]),
|
|
88
|
+
note: Type.String(),
|
|
89
|
+
}, { additionalProperties: false }),
|
|
90
|
+
async execute(_id, params, ctx) {
|
|
91
|
+
const q = norm(params.task);
|
|
92
|
+
let candidates = [];
|
|
93
|
+
let note = "";
|
|
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";
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
note = `registry unavailable: ${e?.message ?? "unknown"}`;
|
|
106
|
+
}
|
|
107
|
+
let receipt = null;
|
|
108
|
+
if (candidates.length) {
|
|
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();
|
|
113
|
+
}
|
|
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 } };
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
api.registerTool({
|
|
119
|
+
name: "zero_agency_status",
|
|
120
|
+
description: "Report Zero Agency registry state: capability count, token total and live receipts.",
|
|
121
|
+
parameters: Type.Object({}),
|
|
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 }),
|
|
123
|
+
async execute() {
|
|
124
|
+
try {
|
|
125
|
+
const reg = await loadRegistry(registryPath);
|
|
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 } };
|
|
128
|
+
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
return { content: [{ type: "text", text: `zero-agency: registry error` }], details: { registry: registryPath, capabilities: 0, tokens: 0, receipts: store().size, registryError: e?.message ?? "unknown" } };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
api.on("before_tool_call", async (event, ctx) => {
|
|
135
|
+
if (gateMode === "off")
|
|
136
|
+
return undefined;
|
|
137
|
+
const runId = event?.runId ?? ctx?.runId ?? "unknown-run";
|
|
138
|
+
prune();
|
|
139
|
+
const r = store().get(runId);
|
|
140
|
+
if (r) {
|
|
141
|
+
await telemetry(telemetryPath, { kind: "gate", decision: "receipt-ok", tool: event?.toolName, runId, receipt: r.id });
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
await telemetry(telemetryPath, { kind: "gate", decision: gateMode === "enforce" ? "blocked" : "advisory-miss", tool: event?.toolName, runId });
|
|
145
|
+
if (gateMode !== "enforce")
|
|
146
|
+
return undefined;
|
|
147
|
+
return {
|
|
148
|
+
requireApproval: {
|
|
149
|
+
title: `Zero Agency: no capability receipt (${event?.toolName ?? "tool"})`,
|
|
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 }); },
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}, { matcher: consequential, priority: 50 });
|
|
156
|
+
},
|
|
157
|
+
});
|
package/dist/tools.js
CHANGED
|
@@ -50,7 +50,7 @@ async function telemetry(path, record) {
|
|
|
50
50
|
catch { /* telemetry must never break a turn */ }
|
|
51
51
|
}
|
|
52
52
|
export default defineToolPlugin({
|
|
53
|
-
id: "zero-agency",
|
|
53
|
+
id: "zero-agency/tools",
|
|
54
54
|
name: "Zero Agency",
|
|
55
55
|
description: "Capability routing for the fleet. Packs inform judgement; they never confer authority.",
|
|
56
56
|
configSchema: Type.Object({
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,59 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "zero-agency",
|
|
3
3
|
"name": "Zero Agency",
|
|
4
|
-
"description": "Capability routing
|
|
5
|
-
"activation": {
|
|
6
|
-
|
|
7
|
-
},
|
|
4
|
+
"description": "Capability routing with a receipt gate. Packs inform judgement; they never confer authority.",
|
|
5
|
+
"activation": { "onStartup": true },
|
|
6
|
+
"contracts": { "tools": ["zero_agency_route", "zero_agency_status"] },
|
|
8
7
|
"configSchema": {
|
|
9
8
|
"type": "object",
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
],
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"required": ["registryPath"],
|
|
13
11
|
"properties": {
|
|
14
|
-
"registryPath": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
},
|
|
18
|
-
"
|
|
19
|
-
"type": "string",
|
|
20
|
-
"description": "JSONL telemetry path."
|
|
21
|
-
},
|
|
22
|
-
"receiptTtlMs": {
|
|
23
|
-
"type": "integer",
|
|
24
|
-
"minimum": 1000
|
|
25
|
-
},
|
|
26
|
-
"gateMode": {
|
|
27
|
-
"anyOf": [
|
|
28
|
-
{
|
|
29
|
-
"type": "string",
|
|
30
|
-
"const": "off"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"type": "string",
|
|
34
|
-
"const": "advisory"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"type": "string",
|
|
38
|
-
"const": "enforce"
|
|
39
|
-
}
|
|
40
|
-
],
|
|
41
|
-
"description": "Receipt gate mode (default advisory)."
|
|
42
|
-
},
|
|
43
|
-
"consequentialTools": {
|
|
44
|
-
"type": "array",
|
|
45
|
-
"items": {
|
|
46
|
-
"type": "string"
|
|
47
|
-
},
|
|
48
|
-
"description": "Tool ids gated by receipt."
|
|
49
|
-
}
|
|
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 },
|
|
15
|
+
"gateMode": { "type": "string", "enum": ["off", "advisory", "enforce"], "default": "advisory" },
|
|
16
|
+
"consequentialTools": { "type": "array", "items": { "type": "string" }, "default": ["exec", "apply_patch"] }
|
|
50
17
|
}
|
|
51
|
-
},
|
|
52
|
-
"version": "0.2.0",
|
|
53
|
-
"contracts": {
|
|
54
|
-
"tools": [
|
|
55
|
-
"zero_agency_route",
|
|
56
|
-
"zero_agency_status"
|
|
57
|
-
]
|
|
58
18
|
}
|
|
59
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-zero-agency",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
|
@@ -35,8 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"openclaw": {
|
|
37
37
|
"extensions": [
|
|
38
|
-
"./dist/
|
|
39
|
-
"./dist/gate.js"
|
|
38
|
+
"./dist/index.js"
|
|
40
39
|
],
|
|
41
40
|
"compat": {
|
|
42
41
|
"pluginApi": ">=2026.10.1",
|