openclaw-zero-agency 1.0.1 → 1.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/dist/index.js +41 -14
- package/openclaw.plugin.json +8 -2
- package/package.json +1 -1
- package/dist/tools.js +0 -148
package/dist/index.js
CHANGED
|
@@ -95,6 +95,21 @@ function score(cap, q) {
|
|
|
95
95
|
s += 0.5 * dw.filter((w) => q.includes(w)).length;
|
|
96
96
|
return { score: s, hits };
|
|
97
97
|
}
|
|
98
|
+
const DAY_MS = 86_400_000;
|
|
99
|
+
function ageDays(cap) {
|
|
100
|
+
if (!cap.reviewed_at)
|
|
101
|
+
return null;
|
|
102
|
+
const t = Date.parse(cap.reviewed_at);
|
|
103
|
+
if (Number.isNaN(t))
|
|
104
|
+
return null;
|
|
105
|
+
return Math.floor((Date.now() - t) / DAY_MS);
|
|
106
|
+
}
|
|
107
|
+
// A pack that has not been re-reviewed within maxPackAgeDays is stale: upstream
|
|
108
|
+
// guidance rots, so promotion expires instead of persisting forever.
|
|
109
|
+
function isStale(cap, maxDays) {
|
|
110
|
+
const a = ageDays(cap);
|
|
111
|
+
return a !== null && a > maxDays;
|
|
112
|
+
}
|
|
98
113
|
async function telemetry(path, record) {
|
|
99
114
|
if (!path)
|
|
100
115
|
return;
|
|
@@ -106,8 +121,8 @@ async function telemetry(path, record) {
|
|
|
106
121
|
}
|
|
107
122
|
export default definePluginEntry({
|
|
108
123
|
id: "zero-agency",
|
|
109
|
-
name: "
|
|
110
|
-
description: "Capability routing with
|
|
124
|
+
name: "Use Agency",
|
|
125
|
+
description: "Capability routing with evidence-gated pack promotion. Packs inform judgement; they never confer authority.",
|
|
111
126
|
register(api) {
|
|
112
127
|
// api.config is the GLOBAL gateway config; plugin-scoped config lives on api.pluginConfig.
|
|
113
128
|
const cfg = api.pluginConfig ?? {};
|
|
@@ -118,6 +133,7 @@ export default definePluginEntry({
|
|
|
118
133
|
const gateMode = cfg.gateMode ?? "advisory";
|
|
119
134
|
const consequential = cfg.consequentialTools ?? ["exec", "apply_patch"];
|
|
120
135
|
const ttl = cfg.receiptTtlMs ?? 900000;
|
|
136
|
+
const maxPackAgeDays = cfg.maxPackAgeDays ?? 180;
|
|
121
137
|
api.registerTool({
|
|
122
138
|
name: "zero_agency_route",
|
|
123
139
|
description: "Find the capability pack governing a domain task. Call before acting on security review, release verification or incident triage. Returns the governing pack and logs the routing decision.",
|
|
@@ -136,20 +152,24 @@ export default definePluginEntry({
|
|
|
136
152
|
let note = "";
|
|
137
153
|
try {
|
|
138
154
|
const reg = await loadRegistry(registryPath);
|
|
139
|
-
|
|
155
|
+
const matched = (reg.capabilities ?? [])
|
|
140
156
|
.map((c) => ({ c, ...score(c, q) }))
|
|
141
|
-
.filter((x) => x.score > 0
|
|
157
|
+
.filter((x) => x.score > 0);
|
|
158
|
+
const evaluated = matched.filter((x) => (x.c.status ?? 'draft') === 'evaluated');
|
|
159
|
+
const stale = evaluated.filter((x) => isStale(x.c, maxPackAgeDays));
|
|
160
|
+
const drafts = matched.filter((x) => (x.c.status ?? 'draft') !== 'evaluated');
|
|
161
|
+
candidates = evaluated
|
|
162
|
+
.filter((x) => !isStale(x.c, maxPackAgeDays))
|
|
142
163
|
.sort((a, b) => b.score - a.score)
|
|
143
164
|
.slice(0, params.limit ?? 3)
|
|
144
165
|
.map((x) => ({ id: x.c.id, pack: x.c.pack, score: Number(x.score.toFixed(2)), triggers: x.hits }));
|
|
145
|
-
const drafts = (reg.capabilities ?? [])
|
|
146
|
-
.filter((c) => (c.status ?? 'draft') !== 'live')
|
|
147
|
-
.filter((c) => score(c, q).score > 0);
|
|
148
166
|
note = candidates.length
|
|
149
167
|
? `load ${candidates[0].pack} before acting`
|
|
150
|
-
:
|
|
151
|
-
? `no
|
|
152
|
-
:
|
|
168
|
+
: stale.length
|
|
169
|
+
? `no fresh capability matched; ${stale.length} evaluated pack(s) exceeded maxPackAgeDays (${maxPackAgeDays}) and need re-review: ${stale.map((x) => x.c.id).join(", ")}`
|
|
170
|
+
: drafts.length
|
|
171
|
+
? `no evaluated capability matched; ${drafts.length} draft pack(s) matched but are unpublished: ${drafts.map((x) => x.c.id).join(", ")}`
|
|
172
|
+
: "no capability matched; if this action is consequential, stop and ask";
|
|
153
173
|
}
|
|
154
174
|
catch (e) {
|
|
155
175
|
note = `registry unavailable: ${e?.message ?? "unknown"}`;
|
|
@@ -162,23 +182,30 @@ export default definePluginEntry({
|
|
|
162
182
|
if (!wrote)
|
|
163
183
|
await telemetry(telemetryPath, { kind: "receipt-write-failed", path: receiptPath, receipt });
|
|
164
184
|
}
|
|
165
|
-
|
|
185
|
+
const audience = ctx?.memoryAudience ?? null;
|
|
186
|
+
const agentId = audience?.agentId ?? ctx?.agentId ?? null;
|
|
187
|
+
const sessionKey = audience?.sessionKey ?? ctx?.sessionKey ?? null;
|
|
188
|
+
const sessionId = audience?.sessionId ?? ctx?.sessionId ?? null;
|
|
189
|
+
await telemetry(telemetryPath, { kind: "route", agent: agentId, session: sessionKey, sessionId, audience: audience?.kind ?? null, query: params.task.slice(0, 200), selected: candidates[0]?.id ?? null, pack: candidates[0]?.pack ?? null, candidates: candidates.length, receipt });
|
|
166
190
|
return { content: [{ type: "text", text: JSON.stringify({ candidates, receipt, note }) }], details: { candidates, receipt, note } };
|
|
167
191
|
},
|
|
168
192
|
});
|
|
169
193
|
api.registerTool({
|
|
170
194
|
name: "zero_agency_status",
|
|
171
|
-
description: "Report Zero Agency registry state: capability
|
|
195
|
+
description: "Report Zero Agency registry state: capability counts by promotion state, token total, pack freshness and live receipts.",
|
|
172
196
|
parameters: Type.Object({}),
|
|
173
197
|
outputSchema: Type.Object({ registry: Type.String(), capabilities: Type.Integer(), tokens: Type.Integer(), receipts: Type.Integer(), registryError: Type.Union([Type.String(), Type.Null()]) }, { additionalProperties: false }),
|
|
174
198
|
async execute() {
|
|
175
199
|
try {
|
|
176
200
|
const reg = await loadRegistry(registryPath);
|
|
177
201
|
const caps = reg.capabilities ?? [];
|
|
178
|
-
|
|
202
|
+
const evaluated = caps.filter((c) => (c.status ?? 'draft') === 'evaluated');
|
|
203
|
+
const stale = evaluated.filter((c) => isStale(c, maxPackAgeDays)).map((c) => c.id);
|
|
204
|
+
const summary = `zero-agency: ${caps.length} capabilities (${evaluated.length} evaluated, ${caps.length - evaluated.length} draft)`;
|
|
205
|
+
return { content: [{ type: "text", text: summary }], details: { registry: registryPath, capabilities: caps.length, evaluated: evaluated.length, draft: caps.length - evaluated.length, stale, maxPackAgeDays, tokens: caps.reduce((s, c) => s + (c.tokens ?? 0), 0), receipts: store().size, registryError: null } };
|
|
179
206
|
}
|
|
180
207
|
catch (e) {
|
|
181
|
-
return { content: [{ type: "text", text: `zero-agency: registry error` }], details: { registry: registryPath, capabilities: 0, tokens: 0, receipts: store().size, registryError: e?.message ?? "unknown" } };
|
|
208
|
+
return { content: [{ type: "text", text: `zero-agency: registry error` }], details: { registry: registryPath, capabilities: 0, evaluated: 0, draft: 0, stale: [], maxPackAgeDays, tokens: 0, receipts: store().size, registryError: e?.message ?? "unknown" } };
|
|
182
209
|
}
|
|
183
210
|
},
|
|
184
211
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "zero-agency",
|
|
3
|
-
"name": "
|
|
4
|
-
"description": "Capability routing with
|
|
3
|
+
"name": "Use Agency",
|
|
4
|
+
"description": "Capability routing with evidence-gated pack promotion. Packs inform judgement; they never confer authority.",
|
|
5
5
|
"activation": {
|
|
6
6
|
"onStartup": true
|
|
7
7
|
},
|
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
"exec",
|
|
51
51
|
"apply_patch"
|
|
52
52
|
]
|
|
53
|
+
},
|
|
54
|
+
"maxPackAgeDays": {
|
|
55
|
+
"type": "integer",
|
|
56
|
+
"minimum": 1,
|
|
57
|
+
"default": 180,
|
|
58
|
+
"description": "Screened packs older than this (by reviewed_at) are not routed and must be re-reviewed."
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-zero-agency",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Capability routing for OpenClaw agents: distilled packs, a compiled registry, and routing telemetry. Packs inform judgement; they never confer authority.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/dist/tools.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
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 && (x.c.status ?? 'draft') === 'live')
|
|
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
|
-
const drafts = (reg.capabilities ?? [])
|
|
95
|
-
.filter((c) => (c.status ?? 'draft') !== 'live')
|
|
96
|
-
.filter((c) => score(c, q).score > 0);
|
|
97
|
-
note = candidates.length
|
|
98
|
-
? `load ${candidates[0].pack} before acting`
|
|
99
|
-
: drafts.length
|
|
100
|
-
? `no live capability matched; ${drafts.length} draft pack(s) matched but are unpublished: ${drafts.map((c) => c.id).join(", ")}`
|
|
101
|
-
: "no capability matched; if this action is consequential, stop and ask";
|
|
102
|
-
}
|
|
103
|
-
catch (e) {
|
|
104
|
-
note = `registry unavailable: ${e?.message ?? "unknown"}`;
|
|
105
|
-
}
|
|
106
|
-
let receipt = null;
|
|
107
|
-
if (candidates.length) {
|
|
108
|
-
const runId = context?.runId ?? context?.sessionId ?? "unknown-run";
|
|
109
|
-
receipt = createHash("sha256").update(`${runId}:${candidates[0].id}:${Date.now()}:${randomUUID()}`).digest("hex").slice(0, 16);
|
|
110
|
-
put(runId, { id: receipt, pack: candidates[0].pack, at: Date.now(), expires: Date.now() + ttl });
|
|
111
|
-
prune();
|
|
112
|
-
}
|
|
113
|
-
await telemetry(config.telemetryPath, {
|
|
114
|
-
kind: "route", agent: context?.agentId ?? null,
|
|
115
|
-
session: context?.sessionId ?? null, query: params.task.slice(0, 200),
|
|
116
|
-
selected: candidates[0]?.id ?? null, candidates: candidates.length, receipt,
|
|
117
|
-
});
|
|
118
|
-
return { candidates, receipt, note };
|
|
119
|
-
},
|
|
120
|
-
}),
|
|
121
|
-
tool({
|
|
122
|
-
name: "zero_agency_status",
|
|
123
|
-
label: "Zero Agency Status",
|
|
124
|
-
description: "Report the Zero Agency registry state: capability count, token total and live receipts.",
|
|
125
|
-
parameters: Type.Object({}),
|
|
126
|
-
outputSchema: Type.Object({
|
|
127
|
-
registry: Type.String(), capabilities: Type.Integer(), tokens: Type.Integer(),
|
|
128
|
-
receipts: Type.Integer(), registryError: Type.Union([Type.String(), Type.Null()]),
|
|
129
|
-
}, { additionalProperties: false }),
|
|
130
|
-
async execute(_params, config) {
|
|
131
|
-
try {
|
|
132
|
-
const reg = await loadRegistry(config.registryPath);
|
|
133
|
-
const caps = reg.capabilities ?? [];
|
|
134
|
-
return {
|
|
135
|
-
registry: config.registryPath,
|
|
136
|
-
capabilities: caps.length,
|
|
137
|
-
tokens: caps.reduce((s, c) => s + (c.tokens ?? 0), 0),
|
|
138
|
-
receipts: size(),
|
|
139
|
-
registryError: null,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
catch (e) {
|
|
143
|
-
return { registry: config.registryPath, capabilities: 0, tokens: 0, receipts: size(), registryError: e?.message ?? "unknown" };
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
}),
|
|
147
|
-
],
|
|
148
|
-
});
|