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 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: "Zero Agency",
110
- description: "Capability routing with a receipt gate. Packs inform judgement; they never confer authority.",
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
- candidates = (reg.capabilities ?? [])
155
+ const matched = (reg.capabilities ?? [])
140
156
  .map((c) => ({ c, ...score(c, q) }))
141
- .filter((x) => x.score > 0 && (x.c.status ?? 'draft') === 'live')
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
- : drafts.length
151
- ? `no live capability matched; ${drafts.length} draft pack(s) matched but are unpublished: ${drafts.map((c) => c.id).join(", ")}`
152
- : "no capability matched; if this action is consequential, stop and ask";
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
- 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 });
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 count, token total and live receipts.",
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
- 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 } };
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
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "zero-agency",
3
- "name": "Zero Agency",
4
- "description": "Capability routing with a receipt gate. Packs inform judgement; they never confer authority.",
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.1",
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
- });