entroly-openclaw 1.0.62 → 1.0.64

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
@@ -22,7 +22,7 @@ Install the Python engine, then install the plugin from OpenClaw's official
22
22
  ClawHub registry:
23
23
 
24
24
  ```bash
25
- pip install "entroly>=1.0.57"
25
+ pip install "entroly>=1.0.64"
26
26
  openclaw plugins install clawhub:entroly-openclaw
27
27
  openclaw plugins enable entroly
28
28
  ```
@@ -37,7 +37,7 @@ openclaw plugins enable entroly
37
37
  From an Entroly source checkout:
38
38
 
39
39
  ```bash
40
- pip install "entroly>=1.0.57"
40
+ pip install "entroly>=1.0.64"
41
41
  openclaw plugins install ./integrations/openclaw
42
42
  openclaw plugins enable entroly
43
43
  ```
@@ -65,7 +65,7 @@ After the first agent turn, run `/entroly-context` in any connected channel to
65
65
  see the estimated before/after context size, reduction, warnings, and receipt.
66
66
  Run `/entroly-context doctor` to verify the configured Python executable and
67
67
  local JSONL bridge before inviting users onto the Gateway. The plugin requires
68
- the Entroly 1.0.57 bridge v2 protocol; doctor reports an actionable upgrade
68
+ the Entroly 1.0.64 bridge v2 protocol; doctor reports an actionable upgrade
69
69
  instead of accepting an older, incompatible Python installation.
70
70
 
71
71
  OpenClaw's resolved prompt token budget is authoritative. When an older or
@@ -100,6 +100,54 @@ actionable warning instead of risking a provider overflow. Explicit OpenClaw
100
100
  budgets always win, followed by the operator's `fallbackTokenBudget`; automatic
101
101
  registry discovery is used only when neither is available.
102
102
 
103
+ ## Optional proof-guided exact recovery
104
+
105
+ Default behavior performs local context assembly only and makes no additional
106
+ model call. To let Entroly verify a draft, recover exact omitted messages, and
107
+ ask OpenClaw for a bounded revision, opt in explicitly:
108
+
109
+ ```json5
110
+ {
111
+ plugins: {
112
+ slots: { contextEngine: "entroly" },
113
+ entries: {
114
+ entroly: {
115
+ hooks: { allowConversationAccess: true },
116
+ config: {
117
+ proofGuidedRecovery: true,
118
+ proofGuidedMaxRounds: 2,
119
+ proofGuidedRecoveryTokens: 1200,
120
+ proofGuidedMaxMessages: 3
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ ```
127
+
128
+ `proofGuidedMaxRounds` includes the first response, so the default maximum of
129
+ two allows at most one additional model call. Entroly never receives provider
130
+ credentials and never calls the provider itself: it returns an idempotent
131
+ revision request to OpenClaw, which keeps its normal provider routing and
132
+ billing behavior. The retry contains exact recovered message text and SHA-256
133
+ commitments, not a generated summary.
134
+
135
+ The local bridge checks the draft with EICV and the context firewall. If a
136
+ claim is unsupported, it either identifies exact omitted evidence for the
137
+ bounded revision or supplies a safer verified output. If bridge verification
138
+ fails or model output is unsafe, the delivery hook substitutes a clear withheld
139
+ response instead of silently passing unverified text. `/entroly-context` shows
140
+ the proof status, attempt count, and local audit artifact ID.
141
+
142
+ This feature requires a current OpenClaw build that exposes `llm_output`,
143
+ `before_agent_finalize`, and `reply_payload_sending`, plus the explicit
144
+ `allowConversationAccess` grant shown above. Verify registration after restart:
145
+
146
+ ```bash
147
+ openclaw plugins inspect entroly --runtime --json
148
+ openclaw plugins doctor
149
+ ```
150
+
103
151
  ## Reproduce the evidence-pinning control
104
152
 
105
153
  ```bash
package/engine.js CHANGED
@@ -45,6 +45,7 @@ function statusSnapshot(status) {
45
45
  "context_window",
46
46
  "context_output_reserve",
47
47
  "context_safety_tokens",
48
+ "proof_guided_attempts",
48
49
  ];
49
50
  const booleanFields = ["changed", "provider_independent"];
50
51
  const stringFields = [
@@ -58,6 +59,8 @@ function statusSnapshot(status) {
58
59
  "model",
59
60
  "provider_hint",
60
61
  "assembly_strategy",
62
+ "proof_guided_status",
63
+ "proof_guided_audit_artifact_id",
61
64
  "error",
62
65
  ];
63
66
  for (const field of numericFields) {
@@ -385,6 +388,17 @@ export function formatEntrolyStatus(status) {
385
388
  );
386
389
  }
387
390
  if (status.receipt_id) lines.push(`Receipt: ${boundedString(status.receipt_id, 160)}`);
391
+ if (status.proof_guided_status) {
392
+ lines.push(
393
+ `Proof-guided recovery: ${boundedString(status.proof_guided_status, 160)}`,
394
+ `Proof model attempts: ${Number(status.proof_guided_attempts ?? 0).toLocaleString()}`,
395
+ );
396
+ }
397
+ if (status.proof_guided_audit_artifact_id) {
398
+ lines.push(
399
+ `Proof audit: ${boundedString(status.proof_guided_audit_artifact_id, 160)}`,
400
+ );
401
+ }
388
402
  if (status.warnings?.length) {
389
403
  lines.push(`Warnings: ${status.warnings.map((warning) => safeDiagnostic(warning)).join(" | ")}`);
390
404
  }
@@ -418,6 +432,7 @@ export function createEntrolyContextEngine({
418
432
  config = {},
419
433
  logger = console,
420
434
  statusBySession = new Map(),
435
+ proofStateBySession = new Map(),
421
436
  maxStatusSessions = 512,
422
437
  }) {
423
438
  if (typeof delegateCompaction !== "function") {
@@ -548,6 +563,7 @@ export function createEntrolyContextEngine({
548
563
  context_safety_tokens:
549
564
  assemblyRuntime.runtimeMetadata.context_discovery?.safety_tokens,
550
565
  });
566
+ proofStateBySession.delete(sessionId);
551
567
  return {
552
568
  messages: sourceMessages,
553
569
  estimatedTokens: estimateTokens(sourceMessages),
@@ -627,7 +643,36 @@ export function createEntrolyContextEngine({
627
643
  "Entroly bridge wrote a receipt without the required acceptance handshake",
628
644
  );
629
645
  }
630
- storeStatus(sessionId, result);
646
+ if (config.proofGuidedRecovery === true) {
647
+ const existing = proofStateBySession.get(sessionId);
648
+ if (!existing || existing.prompt !== prompt || existing.disabled) {
649
+ proofStateBySession.set(sessionId, {
650
+ prompt,
651
+ workspaceDir: config.workspaceDir,
652
+ sourceMessages: structuredClone(sourceMessages),
653
+ assembledMessages: structuredClone(result.messages),
654
+ recoveredMessages: [],
655
+ attempts: 0,
656
+ runId: null,
657
+ lastOutputSha256: null,
658
+ lastProofResult: null,
659
+ retryIssued: false,
660
+ disabled: false,
661
+ });
662
+ }
663
+ while (proofStateBySession.size > statusLimit) {
664
+ proofStateBySession.delete(proofStateBySession.keys().next().value);
665
+ }
666
+ } else {
667
+ proofStateBySession.delete(sessionId);
668
+ }
669
+ storeStatus(sessionId, {
670
+ ...result,
671
+ proof_guided_status:
672
+ config.proofGuidedRecovery === true ? "armed" : "disabled",
673
+ proof_guided_attempts:
674
+ proofStateBySession.get(sessionId)?.attempts ?? 0,
675
+ });
631
676
  return {
632
677
  messages: result.messages,
633
678
  estimatedTokens: result.estimated_tokens,
package/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  formatEntrolyDoctor,
10
10
  formatEntrolyStatus,
11
11
  } from "./engine.js";
12
+ import { createProofGuidedHooks } from "./proof-hooks.js";
12
13
 
13
14
  export default definePluginEntry({
14
15
  id: "entroly",
@@ -22,6 +23,7 @@ export default definePluginEntry({
22
23
  logger: api.logger,
23
24
  });
24
25
  const statusBySession = new Map();
26
+ const proofStateBySession = new Map();
25
27
  api.registerContextEngine("entroly", (factoryContext) => {
26
28
  latestWorkspaceDir = factoryContext.workspaceDir;
27
29
  return createEntrolyContextEngine({
@@ -31,8 +33,27 @@ export default definePluginEntry({
31
33
  config: { ...config, workspaceDir: factoryContext.workspaceDir },
32
34
  logger: api.logger,
33
35
  statusBySession,
36
+ proofStateBySession,
34
37
  });
35
38
  });
39
+ if (config.proofGuidedRecovery === true) {
40
+ if (typeof api.on !== "function") {
41
+ api.logger.error?.(
42
+ "entroly: this OpenClaw host does not expose typed proof-guided hooks; disable proofGuidedRecovery or upgrade OpenClaw",
43
+ );
44
+ } else {
45
+ const proofHooks = createProofGuidedHooks({
46
+ bridge,
47
+ config,
48
+ logger: api.logger,
49
+ proofStateBySession,
50
+ statusBySession,
51
+ });
52
+ api.on("llm_output", proofHooks.onLlmOutput);
53
+ api.on("before_agent_finalize", proofHooks.onBeforeAgentFinalize);
54
+ api.on("reply_payload_sending", proofHooks.onReplyPayloadSending);
55
+ }
56
+ }
36
57
  api.registerCommand({
37
58
  name: "entroly-context",
38
59
  description: "Show Entroly context savings or run `doctor`.",
@@ -63,6 +63,28 @@
63
63
  "evidencePinning": {
64
64
  "type": "boolean",
65
65
  "default": true
66
+ },
67
+ "proofGuidedRecovery": {
68
+ "type": "boolean",
69
+ "default": false
70
+ },
71
+ "proofGuidedMaxRounds": {
72
+ "type": "integer",
73
+ "minimum": 1,
74
+ "maximum": 4,
75
+ "default": 2
76
+ },
77
+ "proofGuidedRecoveryTokens": {
78
+ "type": "integer",
79
+ "minimum": 1,
80
+ "maximum": 100000,
81
+ "default": 1200
82
+ },
83
+ "proofGuidedMaxMessages": {
84
+ "type": "integer",
85
+ "minimum": 1,
86
+ "maximum": 16,
87
+ "default": 3
66
88
  }
67
89
  }
68
90
  },
@@ -110,6 +132,22 @@
110
132
  "evidencePinning": {
111
133
  "label": "Query-Aware Evidence Pinning",
112
134
  "help": "Keep older messages that match the current request verbatim when they fit the bounded evidence reserve."
135
+ },
136
+ "proofGuidedRecovery": {
137
+ "label": "Proof-Guided Recovery",
138
+ "help": "Opt in to local output verification and at most the configured number of OpenClaw revision passes using exact recovered evidence. Requires hooks.allowConversationAccess=true because it inspects model input and output."
139
+ },
140
+ "proofGuidedMaxRounds": {
141
+ "label": "Maximum Proof Rounds",
142
+ "help": "Hard cap on model attempts, including the first response. Additional paid calls occur only when Proof-Guided Recovery is explicitly enabled."
143
+ },
144
+ "proofGuidedRecoveryTokens": {
145
+ "label": "Recovery Token Budget",
146
+ "help": "Maximum estimated exact-evidence tokens recovered for one revision decision."
147
+ },
148
+ "proofGuidedMaxMessages": {
149
+ "label": "Recovery Message Limit",
150
+ "help": "Maximum exact omitted messages appended to one revision instruction."
113
151
  }
114
152
  }
115
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entroly-openclaw",
3
- "version": "1.0.62",
3
+ "version": "1.0.64",
4
4
  "description": "Provider-independent, auditable context engineering for every OpenClaw model route",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -30,12 +30,13 @@
30
30
  "bridge-client.js",
31
31
  "engine.js",
32
32
  "index.js",
33
+ "proof-hooks.js",
33
34
  "openclaw.plugin.json",
34
35
  "README.md"
35
36
  ],
36
37
  "scripts": {
37
38
  "test": "node --test test/*.test.js",
38
- "check": "node --check bridge-client.js && node --check engine.js && node --check index.js",
39
+ "check": "node --check bridge-client.js && node --check engine.js && node --check proof-hooks.js && node --check index.js",
39
40
  "pack:check": "npm pack --dry-run"
40
41
  },
41
42
  "peerDependencies": {
package/proof-hooks.js ADDED
@@ -0,0 +1,198 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { ENTROLY_BRIDGE_SCHEMA } from "./bridge-client.js";
4
+
5
+ function sha256(value) {
6
+ return createHash("sha256").update(String(value), "utf8").digest("hex");
7
+ }
8
+
9
+ function safeDiagnostic(value, limit = 400) {
10
+ return String(value?.message ?? value ?? "unknown error")
11
+ .replace(/\s+/g, " ")
12
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]")
13
+ .replace(
14
+ /\b(api[_-]?key|authorization|password|secret|token)\b\s*[:=]\s*["']?[^\s,;"']+/gi,
15
+ "$1=[REDACTED]",
16
+ )
17
+ .slice(0, limit);
18
+ }
19
+
20
+ function positiveInteger(value, fallback, maximum) {
21
+ return typeof value === "number" &&
22
+ Number.isInteger(value) &&
23
+ value > 0 &&
24
+ value <= maximum
25
+ ? value
26
+ : fallback;
27
+ }
28
+
29
+ function validateProofResult(result) {
30
+ if (
31
+ !result ||
32
+ result.ok !== true ||
33
+ result.schema_version !== ENTROLY_BRIDGE_SCHEMA ||
34
+ result.provider_call_performed !== false ||
35
+ result.local_only !== true ||
36
+ typeof result.status !== "string" ||
37
+ typeof result.verified_output !== "string" ||
38
+ !Array.isArray(result.recovered_messages) ||
39
+ typeof result.audit_artifact_id !== "string"
40
+ ) {
41
+ throw new Error("Entroly bridge returned an invalid proof-guided result");
42
+ }
43
+ if (
44
+ result.status === "retry_with_exact_evidence" &&
45
+ (typeof result.retry_instruction !== "string" || !result.retry_instruction.trim())
46
+ ) {
47
+ throw new Error("Entroly bridge omitted the exact-evidence retry instruction");
48
+ }
49
+ return result;
50
+ }
51
+
52
+ function mergeRecoveredMessages(existing, recovered) {
53
+ const seen = new Set(existing.map((message) => sha256(JSON.stringify(message))));
54
+ const merged = [...existing];
55
+ for (const message of recovered) {
56
+ const fingerprint = sha256(JSON.stringify(message));
57
+ if (seen.has(fingerprint)) continue;
58
+ seen.add(fingerprint);
59
+ merged.push(message);
60
+ }
61
+ return merged;
62
+ }
63
+
64
+ export function createProofGuidedHooks({
65
+ bridge,
66
+ config = {},
67
+ logger = console,
68
+ proofStateBySession = new Map(),
69
+ statusBySession = new Map(),
70
+ }) {
71
+ const maxRounds = positiveInteger(config.proofGuidedMaxRounds, 2, 4);
72
+ const recoveryTokenBudget = positiveInteger(
73
+ config.proofGuidedRecoveryTokens,
74
+ 1200,
75
+ 100000,
76
+ );
77
+ const maxRecoveryMessages = positiveInteger(
78
+ config.proofGuidedMaxMessages,
79
+ 3,
80
+ 16,
81
+ );
82
+
83
+ return {
84
+ async onLlmOutput(event) {
85
+ const state = proofStateBySession.get(event?.sessionId);
86
+ if (!state || state.disabled || state.attempts >= maxRounds) return;
87
+ const output = Array.isArray(event.assistantTexts)
88
+ ? event.assistantTexts.filter((value) => typeof value === "string").join("\n\n")
89
+ : "";
90
+ if (!output.trim()) return;
91
+ const outputSha = sha256(output);
92
+ if (state.lastOutputSha256 === outputSha && state.lastProofResult) return;
93
+ try {
94
+ const result = validateProofResult(
95
+ await bridge.request({
96
+ operation: "verify_proof_guided_output",
97
+ session_id: event.sessionId,
98
+ run_id: event.runId,
99
+ round_index: state.attempts,
100
+ source_messages: state.sourceMessages,
101
+ assembled_messages: state.assembledMessages,
102
+ recovered_messages: state.recoveredMessages,
103
+ model_output: output,
104
+ workspace_dir: state.workspaceDir,
105
+ profile: config.proofGuidedProfile ?? "rag",
106
+ recovery_token_budget: recoveryTokenBudget,
107
+ max_recovery_messages: maxRecoveryMessages,
108
+ }),
109
+ );
110
+ state.attempts += 1;
111
+ state.runId = event.runId;
112
+ state.lastOutputSha256 = outputSha;
113
+ state.lastProofResult = result;
114
+ state.recoveredMessages = mergeRecoveredMessages(
115
+ state.recoveredMessages,
116
+ result.recovered_messages,
117
+ );
118
+ state.retryIssued = false;
119
+ statusBySession.set(event.sessionId, {
120
+ ...(statusBySession.get(event.sessionId) ?? {}),
121
+ proof_guided_status: result.status,
122
+ proof_guided_attempts: state.attempts,
123
+ proof_guided_audit_artifact_id: result.audit_artifact_id,
124
+ });
125
+ } catch (error) {
126
+ state.disabled = true;
127
+ state.error = safeDiagnostic(error);
128
+ state.runId = event?.runId;
129
+ state.lastProofResult = {
130
+ status: "verification_error",
131
+ verified_output:
132
+ "Entroly withheld this response because local proof verification failed. " +
133
+ "Review the OpenClaw plugin log and retry after resolving the reported error.",
134
+ changed: true,
135
+ recovered_messages: [],
136
+ audit_artifact_id: "",
137
+ };
138
+ statusBySession.set(event.sessionId, {
139
+ ...(statusBySession.get(event.sessionId) ?? {}),
140
+ proof_guided_status: "verification_error",
141
+ proof_guided_attempts: state.attempts,
142
+ error: state.error,
143
+ });
144
+ logger.warn?.(
145
+ `entroly: proof-guided verification failed; delivery will be withheld: ${state.error}`,
146
+ );
147
+ }
148
+ },
149
+
150
+ async onBeforeAgentFinalize(event) {
151
+ const state = proofStateBySession.get(event?.sessionId);
152
+ const result = state?.lastProofResult;
153
+ if (
154
+ !state ||
155
+ !result ||
156
+ state.runId !== event?.runId ||
157
+ result.status !== "retry_with_exact_evidence" ||
158
+ state.attempts >= maxRounds ||
159
+ state.retryIssued
160
+ ) {
161
+ return;
162
+ }
163
+ state.retryIssued = true;
164
+ return {
165
+ action: "revise",
166
+ reason: "Entroly found unsupported claims and recovered exact omitted evidence.",
167
+ retry: {
168
+ instruction: result.retry_instruction,
169
+ idempotencyKey: `entroly-proof-${sha256(
170
+ `${event.runId}:${result.audit_artifact_id}:${state.attempts}`,
171
+ ).slice(0, 24)}`,
172
+ maxAttempts: 1,
173
+ },
174
+ };
175
+ },
176
+
177
+ async onReplyPayloadSending(event) {
178
+ const sessionId = event?.usageState?.sessionId;
179
+ const state = sessionId ? proofStateBySession.get(sessionId) : undefined;
180
+ const result = state?.lastProofResult;
181
+ if (
182
+ !state ||
183
+ !result ||
184
+ (event?.runId && state.runId !== event.runId) ||
185
+ result.changed !== true ||
186
+ typeof event?.payload?.text !== "string"
187
+ ) {
188
+ return;
189
+ }
190
+ return {
191
+ payload: {
192
+ ...event.payload,
193
+ text: result.verified_output,
194
+ },
195
+ };
196
+ },
197
+ };
198
+ }