openclaw-lark-multi-agent 1.6.0 → 1.7.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/README.md +11 -0
- package/README.zh-CN.md +10 -0
- package/dist/feishu-bot.d.ts +16 -0
- package/dist/feishu-bot.js +303 -32
- package/dist/go-controller.d.ts +86 -12
- package/dist/go-controller.js +435 -131
- package/dist/go-execution.d.ts +10 -0
- package/dist/go-execution.js +1 -0
- package/dist/go-runtime-gate.d.ts +32 -0
- package/dist/go-runtime-gate.js +56 -0
- package/dist/message-store.d.ts +83 -0
- package/dist/message-store.js +142 -2
- package/dist/openclaw-client.d.ts +114 -0
- package/dist/openclaw-client.js +166 -12
- package/dist/provider-errors.d.ts +24 -0
- package/dist/provider-errors.js +344 -0
- package/dist/terminal-errors.d.ts +3 -0
- package/dist/terminal-errors.js +2 -1
- package/package.json +8 -3
- package/plugins/lma-go/README.md +22 -0
- package/plugins/lma-go/dist/index.js +35 -0
- package/plugins/lma-go/dist/policy.js +25 -0
- package/plugins/lma-go/dist/registry.js +37 -0
- package/plugins/lma-go/openclaw.plugin.json +1 -0
- package/plugins/lma-go/package.json +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Never serialize unknown error objects. Only traverse error-envelope fields;
|
|
2
|
+
// request/config/headers/prompt/tool definitions are deliberately not visited.
|
|
3
|
+
const ENVELOPE_FIELDS = ["response", "data", "body", "error", "cause", "message", "msg", "errorMessage", "details"];
|
|
4
|
+
const MAX_TEXT = 16_384;
|
|
5
|
+
const CODES = new Set([
|
|
6
|
+
"model_not_supported", "invalid_model", "model_not_found", "unsupported_model", "incompatible_api", "model_disabled", "model_unavailable", "unknown_model",
|
|
7
|
+
"invalid_request_error", "invalid_request", "bad_request", "invalid_argument", "schema_rejected", "schema_validation_error", "validation_error", "malformed_request",
|
|
8
|
+
"context_length_exceeded", "context_window_exceeded", "prompt_too_long", "input_too_long",
|
|
9
|
+
"insufficient_quota", "quota_exceeded", "usage_limit_exceeded", "credits_exhausted", "rate_limit_exceeded", "too_many_requests", "rate_limit_error",
|
|
10
|
+
"authentication_error", "permission_denied", "unauthorized", "forbidden", "invalid_api_key",
|
|
11
|
+
"econnrefused", "econnreset", "enotfound", "eai_again", "etimedout", "und_err_connect_timeout", "cert_has_expired", "unable_to_verify_leaf_signature", "err_tls_cert_altname_invalid",
|
|
12
|
+
"ttft_timeout", "first_token_timeout", "response_header_timeout", "und_err_headers_timeout", "total_timeout", "execution_timeout", "request_deadline_exceeded",
|
|
13
|
+
"server_error", "internal_error", "internal_server_error", "api_error", "service_unavailable", "overloaded_error", "timeout_error", "abort_err",
|
|
14
|
+
]);
|
|
15
|
+
const TYPES = new Set([...CODES, "error", "request_error", "invalid_argument_error", "permission_error", "not_found_error", "invalid_response_error", "gateway_error"]);
|
|
16
|
+
// A known model identifier is classification evidence, not public provider prose.
|
|
17
|
+
// Reject URLs and credential-shaped IDs; even a valid captured ID is never echoed.
|
|
18
|
+
const isSafeModelId = (value) => /^[a-z0-9_./:-]{1,128}$/i.test(value)
|
|
19
|
+
&& !value.includes("://") && !/(?:^|[/:._-])(?:sk-|gh[pousr]_|eyJ)|secret|private_|password|credential|api[_-]?key|access[_-]?token/i.test(value)
|
|
20
|
+
&& !value.split("/").some(part => /^[a-f0-9]{24,}$/i.test(part) || /^[a-z0-9_+]{48,}$/i.test(part));
|
|
21
|
+
const isDisabledModelBody = (value) => {
|
|
22
|
+
const id = value.match(/^model '([a-z0-9_./:-]{1,128})' is disabled$/)?.[1];
|
|
23
|
+
return id !== undefined && isSafeModelId(id);
|
|
24
|
+
};
|
|
25
|
+
const UNKNOWN_MODEL_NOTE = "The selected model is not registered in the current model catalog or is unavailable.";
|
|
26
|
+
const isUnknownModelBody = (value) => {
|
|
27
|
+
const id = value.match(/^Unknown model: ([a-z0-9][a-z0-9_.-]{0,63}\/([a-z0-9][a-z0-9_./-]{0,127}))$/i)?.[1];
|
|
28
|
+
return id !== undefined && isSafeModelId(id) && id.split("/").every(part => part.length > 0);
|
|
29
|
+
};
|
|
30
|
+
// Exact observed Core copy, not a substring/context heuristic. No arbitrary
|
|
31
|
+
// reason suffix, model ID or provider prose is accepted or shown to the chat.
|
|
32
|
+
const CORE_COMPACTION_FAILURE = "⚠️ Context is too large and auto-compaction could not recover this turn. Try again, use /compact, or use /new to start a fresh session.";
|
|
33
|
+
const diagnostic = (value, values) => typeof value === "string"
|
|
34
|
+
&& values.has(value.toLowerCase()) ? value : undefined;
|
|
35
|
+
/** Classify by a validated root; expose only known structural path segments.
|
|
36
|
+
* Arbitrary property/tool/function names never become public metadata. */
|
|
37
|
+
const parameter = (value) => {
|
|
38
|
+
if (typeof value !== "string" || value.length > 256
|
|
39
|
+
|| !/^(?:model|api|api_version|endpoint|messages|input|max_tokens|max_completion_tokens|context|tools|functions|tool_choice)(?:\[\d{1,4}\]|\.[a-z0-9_-]+)*$/i.test(value))
|
|
40
|
+
return;
|
|
41
|
+
const parts = value.match(/^[a-z_]+|\[\d+\]|\.[a-z0-9_-]+/gi);
|
|
42
|
+
let safe = parts[0];
|
|
43
|
+
for (const part of parts.slice(1)) {
|
|
44
|
+
if (!/^(?:\[\d{1,4}\]|\.(?:\d{1,4}|function|parameters|name|type|properties|required|items))$/i.test(part))
|
|
45
|
+
break;
|
|
46
|
+
safe += part;
|
|
47
|
+
}
|
|
48
|
+
return safe;
|
|
49
|
+
};
|
|
50
|
+
const requestId = (value) => typeof value === "string"
|
|
51
|
+
&& /^(?:gw-[a-f0-9]{8,64}|req[_-][a-z0-9_-]{4,80}|[a-f0-9]{8}-[a-f0-9-]{27,40}|[a-f0-9]{16,64})$/i.test(value)
|
|
52
|
+
&& !/private_|secret|token|password|credential|api[_-]?key/i.test(value) ? value : undefined;
|
|
53
|
+
const httpStatus = (value) => {
|
|
54
|
+
const n = Number(value);
|
|
55
|
+
return Number.isInteger(n) && n >= 400 && n <= 599 ? n : undefined;
|
|
56
|
+
};
|
|
57
|
+
// Exact, known diagnostic sentences only. Arbitrary provider prose can echo
|
|
58
|
+
// user input, credentials or tool schemas; metadata survives when prose cannot.
|
|
59
|
+
const SAFE_MESSAGES = [
|
|
60
|
+
"The requested model is not supported.", "The requested model is not supported",
|
|
61
|
+
"Invalid model", "Model not found", "Invalid argument", "invalid argument",
|
|
62
|
+
"model not allowed by agents.defaults.modelPolicy.allow",
|
|
63
|
+
"authentication rejected", "tool execution failed", "Request timed out", "Request timeout",
|
|
64
|
+
"Gateway disconnected", "Gateway disconnected during run", "Not connected and reconnect failed",
|
|
65
|
+
"execution budget exhausted", "Authentication failed", "Forbidden", "Unauthorized",
|
|
66
|
+
];
|
|
67
|
+
/** Bounded parsing of SDK strings (e.g. Agent error: 400 {"error":...})
|
|
68
|
+
* and nested JSON errors. Deeper structured provider fields beat wrapper fields.
|
|
69
|
+
* Message text is used for classification, never returned verbatim. */
|
|
70
|
+
export function classifyProviderError(error) {
|
|
71
|
+
const candidates = [];
|
|
72
|
+
const texts = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
let nodes = 0;
|
|
75
|
+
let cancelled = false;
|
|
76
|
+
let abortSignal = false;
|
|
77
|
+
function visit(value, depth = 0, rank = 0, envelopeStatus, rootDiagnostic = true, compactionDiagnostic = true) {
|
|
78
|
+
if (depth > 8 || ++nodes > 64)
|
|
79
|
+
return;
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
const text = value.slice(0, MAX_TEXT).trim();
|
|
82
|
+
texts.push(text);
|
|
83
|
+
const unwrapped = text.replace(/^(?:(?:Agent error|RPC error|Error|HTTP(?: error)?)\s*:\s*)+/i, "");
|
|
84
|
+
const statusPrefix = unwrapped.match(/^(?:HTTP\s*)?([45]\d{2})\b\s*:?\s*/i);
|
|
85
|
+
const localStatus = statusPrefix ? httpStatus(statusPrefix[1]) : envelopeStatus;
|
|
86
|
+
if (statusPrefix)
|
|
87
|
+
candidates.push({ score: rank + depth, status: localStatus });
|
|
88
|
+
if (/<(?:!doctype|html|body)\b/i.test(text))
|
|
89
|
+
return;
|
|
90
|
+
const body = statusPrefix ? unwrapped.slice(statusPrefix[0].length) : unwrapped;
|
|
91
|
+
// Only a complete JSON value in an explicit SDK/HTTP envelope. Never
|
|
92
|
+
// search arbitrary prose (which may contain prompt echoes) for JSON or
|
|
93
|
+
// code/type/request_id assignments.
|
|
94
|
+
if (value.length <= MAX_TEXT && /^[{"]/.test(body)) {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(body);
|
|
97
|
+
if (parsed !== value)
|
|
98
|
+
visit(parsed, depth + 1, rank, localStatus, rootDiagnostic, compactionDiagnostic && typeof parsed === "string" && /^(?:Agent error|RPC error|Error)\s*:/i.test(text));
|
|
99
|
+
}
|
|
100
|
+
catch { /* not JSON */ }
|
|
101
|
+
}
|
|
102
|
+
const plain = body.trim();
|
|
103
|
+
if (rootDiagnostic && compactionDiagnostic && value.length <= MAX_TEXT && plain === CORE_COMPACTION_FAILURE) {
|
|
104
|
+
candidates.push({ score: rank + depth, compactionFailedBody: true });
|
|
105
|
+
}
|
|
106
|
+
// Mark only a complete known sentence, never a "disabled" substring.
|
|
107
|
+
// Classification below additionally requires the enclosing HTTP503 and
|
|
108
|
+
// compatible diagnostic provenance, including sibling response.status.
|
|
109
|
+
// Do not classify a truncated long body as a complete known sentence.
|
|
110
|
+
if (value.length <= MAX_TEXT && isDisabledModelBody(plain)) {
|
|
111
|
+
candidates.push({ score: rank + depth, disabledModelBody: true });
|
|
112
|
+
}
|
|
113
|
+
// A root Core/SDK diagnostic (including complete quoted envelopes), not
|
|
114
|
+
// an arbitrary response-data/details/tool tree or a substring search.
|
|
115
|
+
if (rootDiagnostic && value.length <= MAX_TEXT && isUnknownModelBody(plain)) {
|
|
116
|
+
candidates.push({ score: rank + depth, unknownModelBody: true });
|
|
117
|
+
}
|
|
118
|
+
if (SAFE_MESSAGES.some(message => message.toLowerCase() === plain.toLowerCase())) {
|
|
119
|
+
candidates.push({ score: rank + depth, safeMessage: plain });
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
124
|
+
return;
|
|
125
|
+
seen.add(value);
|
|
126
|
+
const obj = value;
|
|
127
|
+
// Control intent is supplied by the caller's typed error, never by JSON
|
|
128
|
+
// quoted inside provider prose. A message echo must not synthesize a stop.
|
|
129
|
+
if (depth === 0) {
|
|
130
|
+
if (obj.userStop === true || ["user", "user_stop", "user_cancel", "stop"].includes(String(obj.stopReason)))
|
|
131
|
+
cancelled = true;
|
|
132
|
+
if (obj.name === "AbortError" || obj.code === "ABORT_ERR")
|
|
133
|
+
abortSignal = true;
|
|
134
|
+
}
|
|
135
|
+
const candidate = { score: rank + depth,
|
|
136
|
+
code: diagnostic(obj.code, CODES), type: diagnostic(obj.type, TYPES), param: parameter(obj.param),
|
|
137
|
+
status: httpStatus(obj.status) ?? httpStatus(obj.statusCode) ?? httpStatus(obj.httpStatus)
|
|
138
|
+
// Go writeErr's closed envelope uses a numeric HTTP code, not a string
|
|
139
|
+
// provider code. Accept it only with its known gateway_error type.
|
|
140
|
+
?? (obj.type === "gateway_error" ? httpStatus(obj.code) : undefined),
|
|
141
|
+
requestId: requestId(obj.request_id) ?? requestId(obj.req_id) ?? requestId(obj.requestId) };
|
|
142
|
+
candidates.push(candidate);
|
|
143
|
+
for (const field of ENVELOPE_FIELDS) {
|
|
144
|
+
// An authoritative response.data.error outranks a sibling wrapper message
|
|
145
|
+
// regardless of iteration order. Keep code/type/param as one envelope.
|
|
146
|
+
const weight = field === "error" ? 100 : field === "response" ? 80 : field === "data" ? 20
|
|
147
|
+
: field === "body" || field === "cause" || field === "details" ? 10 : 0;
|
|
148
|
+
visit(obj[field], depth + 1, rank + weight, candidate.status ?? envelopeStatus, rootDiagnostic && ["error", "message", "msg", "errorMessage"].includes(field), compactionDiagnostic);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
visit(error);
|
|
152
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
153
|
+
const chosen = candidates.find(c => c.code || c.type || c.param);
|
|
154
|
+
const result = { category: "unknown",
|
|
155
|
+
...(chosen?.code ? { code: chosen.code } : {}), ...(chosen?.type ? { type: chosen.type } : {}),
|
|
156
|
+
...(chosen?.param ? { param: chosen.param } : {}),
|
|
157
|
+
};
|
|
158
|
+
for (const field of ["status", "requestId", "safeMessage"]) {
|
|
159
|
+
const candidate = candidates.find(c => c[field] !== undefined);
|
|
160
|
+
if (candidate)
|
|
161
|
+
Object.assign(result, { [field]: candidate[field] });
|
|
162
|
+
}
|
|
163
|
+
// Exclude HTML and credential/payload text from heuristic classification.
|
|
164
|
+
const text = texts.filter(t => !/<(?:!doctype|html|body)\b/i.test(t))
|
|
165
|
+
.join(" ").replace(/https?:\/\/\S+/gi, "[url]").toLowerCase();
|
|
166
|
+
const code = (result.code || "").toLowerCase();
|
|
167
|
+
const type = (result.type || "").toLowerCase();
|
|
168
|
+
const param = (result.param || "").toLowerCase();
|
|
169
|
+
const tags = `${code} ${type}`;
|
|
170
|
+
const disabledBody = candidates.find(c => c.disabledModelBody);
|
|
171
|
+
const neutralCode = /^(?:server_error|internal_error|internal_server_error|api_error|service_unavailable|invalid_request_error|bad_request)$/.test(code);
|
|
172
|
+
const knownDisabledBody = result.status === 503 && disabledBody !== undefined
|
|
173
|
+
&& disabledBody.score >= (chosen?.score ?? -Infinity) && (!param || param === "model")
|
|
174
|
+
&& (!code || neutralCode)
|
|
175
|
+
&& (!type || /^(?:error|request_error|gateway_error|api_error|server_error|invalid_request_error|service_unavailable)$/.test(type));
|
|
176
|
+
const unknownBody = candidates.find(c => c.unknownModelBody);
|
|
177
|
+
const knownUnknownBody = unknownBody !== undefined && unknownBody.score >= (chosen?.score ?? -Infinity)
|
|
178
|
+
&& !abortSignal && ![401, 403, 429].includes(result.status ?? 0) && (!param || param === "model")
|
|
179
|
+
&& (!code || neutralCode)
|
|
180
|
+
&& (!type || /^(?:error|request_error|gateway_error|api_error|server_error|invalid_request_error|service_unavailable)$/.test(type));
|
|
181
|
+
if (cancelled)
|
|
182
|
+
result.category = "cancelled";
|
|
183
|
+
else if (code === "unknown_model" || ((!code || neutralCode) && type === "unknown_model") || knownUnknownBody) {
|
|
184
|
+
result.category = "model_unavailable";
|
|
185
|
+
// Fixed semantic explanation, not an invented provider code or captured ID.
|
|
186
|
+
result.safeMessage = UNKNOWN_MODEL_NOTE;
|
|
187
|
+
}
|
|
188
|
+
else if (/^(?:model_disabled|model_unavailable)$/.test(code)
|
|
189
|
+
|| ((!code || neutralCode) && /^(?:model_disabled|model_unavailable)$/.test(type)) || knownDisabledBody)
|
|
190
|
+
result.category = "model_unavailable";
|
|
191
|
+
else if (/\b(?:model_not_supported|invalid_model|model_not_found|unsupported_model|incompatible_api)\b/.test(tags) || param === "model")
|
|
192
|
+
result.category = "model";
|
|
193
|
+
else if (/^(?:tools|functions|tool_choice)(?:\b|[.\[])/.test(param)
|
|
194
|
+
&& (/^(?:invalid_argument|invalid_request_error|invalid_request|bad_request|schema_rejected|schema_validation_error|validation_error|malformed_request|invalid_argument_error)$/.test(code)
|
|
195
|
+
|| (!code && (/^(?:invalid_argument|invalid_request_error|validation_error|schema_validation_error|invalid_argument_error)$/.test(type)
|
|
196
|
+
|| /json schema|invalid (?:argument|schema)|tool payload/.test(text)))))
|
|
197
|
+
result.category = "tools";
|
|
198
|
+
else if (/\b(?:context_length_exceeded|context_window_exceeded|prompt_too_long|input_too_long)\b/.test(tags))
|
|
199
|
+
result.category = "context";
|
|
200
|
+
else if (/\b(?:insufficient_quota|quota_exceeded|usage_limit_exceeded|credits_exhausted)\b/.test(tags))
|
|
201
|
+
result.category = "quota";
|
|
202
|
+
else if (/\b(?:rate_limit_exceeded|too_many_requests|rate_limit_error)\b/.test(tags))
|
|
203
|
+
result.category = "rate_limit";
|
|
204
|
+
else if (result.status === 401 || result.status === 403 || /\b(?:authentication_error|permission_denied|unauthorized|forbidden|invalid_api_key)\b/.test(tags))
|
|
205
|
+
result.category = "auth";
|
|
206
|
+
else if (/\b(?:econnrefused|econnreset|enotfound|eai_again|etimedout|und_err_connect_timeout|cert_has_expired|unable_to_verify_leaf_signature|err_tls[a-z_]*)\b/.test(tags))
|
|
207
|
+
result.category = "network";
|
|
208
|
+
else if (/\b(?:ttft_timeout|first_token_timeout|response_header_timeout|und_err_headers_timeout)\b/.test(tags))
|
|
209
|
+
result.category = "ttft_timeout";
|
|
210
|
+
else if (/\b(?:total_timeout|execution_timeout|request_deadline_exceeded)\b/.test(tags))
|
|
211
|
+
result.category = "total_timeout";
|
|
212
|
+
else if (abortSignal || code === "abort_err")
|
|
213
|
+
result.category = "cancelled";
|
|
214
|
+
else if (candidates.some(c => c.compactionFailedBody && c.score >= (chosen?.score ?? -Infinity))
|
|
215
|
+
&& ![401, 403, 429].includes(result.status ?? 0) && (!code || neutralCode) && !param
|
|
216
|
+
&& (!type || /^(?:error|request_error|gateway_error|api_error|server_error|invalid_request_error|service_unavailable)$/.test(type)))
|
|
217
|
+
result.category = "compaction_failed";
|
|
218
|
+
// Fallback prose is classification evidence only. It cannot overwrite the
|
|
219
|
+
// concrete structural category above and is never serialized to audiences.
|
|
220
|
+
else if (/model not allowed by agents\.defaults\.modelpolicy\.allow/.test(text))
|
|
221
|
+
result.category = "model_policy";
|
|
222
|
+
else if (/\b(?:invalid model|model (?:is )?not (?:supported|found)|requested model is not supported|incompatible api)\b/.test(text))
|
|
223
|
+
result.category = "model";
|
|
224
|
+
else if (/\b(?:maximum context length|context window exceeded|prompt too (?:large|long))\b/.test(text))
|
|
225
|
+
result.category = "context";
|
|
226
|
+
else if (/\b(?:quota (?:exceeded|exhausted)|(?:exceeded|exhausted).{0,40}(?:usage quota|usage limit)|insufficient (?:quota|credits)|credits exhausted)\b/.test(text))
|
|
227
|
+
result.category = "quota";
|
|
228
|
+
else if (result.status === 429 || /\brate limit(?:ed| exceeded)?\b/.test(text))
|
|
229
|
+
result.category = "rate_limit";
|
|
230
|
+
else if (/\b(?:authentication (?:failed|rejected)|permission denied|unauthorized|forbidden)\b/.test(text))
|
|
231
|
+
result.category = "auth";
|
|
232
|
+
else if (/\b(?:dns (?:lookup|resolution) failed|tls (?:handshake|certificate)|ssl certificate|connection (?:refused|reset)|gateway disconnected|not connected and reconnect failed)\b/.test(text))
|
|
233
|
+
result.category = "network";
|
|
234
|
+
else if (/\b(?:ttft|first[- ]token|response[- ]header).{0,24}(?:timeout|timed out)\b/.test(text))
|
|
235
|
+
result.category = "ttft_timeout";
|
|
236
|
+
else if (/\b(?:total|execution|overall).{0,24}(?:timeout|timed out|deadline exceeded)\b/.test(text))
|
|
237
|
+
result.category = "total_timeout";
|
|
238
|
+
else if (/\b(?:rpc timeout|collector timeout|collectreply.*timeout|status lookup timeout)\b/.test(text)
|
|
239
|
+
|| /等待 agent 运行结果超时/.test(text))
|
|
240
|
+
result.category = "collector_timeout";
|
|
241
|
+
else if (!result.status && !/invalid|validation|schema|malformed/.test(tags + " " + text) && /\b(?:timeout|timed out|etimedout)\b|超时/.test(text))
|
|
242
|
+
result.category = "timeout";
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
/** Transparency (user directive 2026-10-09): the visible notice must always
|
|
246
|
+
* carry the original diagnostic so "unclassified" is still actionable. The
|
|
247
|
+
* excerpt is extracted from the error's diagnostic envelope fields and
|
|
248
|
+
* sanitized (no HTML, no URLs, no keys/tokens, capped) — never verbatim
|
|
249
|
+
* arbitrary provider prose. Kept out of classifyProviderError so the
|
|
250
|
+
* classification shape stays stable. */
|
|
251
|
+
export function rawErrorExcerpt(error) {
|
|
252
|
+
const texts = [];
|
|
253
|
+
const seen = new Set();
|
|
254
|
+
const visit = (value, depth = 0) => {
|
|
255
|
+
if (depth > 3 || texts.length > 16)
|
|
256
|
+
return;
|
|
257
|
+
if (typeof value === "string") {
|
|
258
|
+
if (value.length <= MAX_TEXT)
|
|
259
|
+
texts.push(value);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
263
|
+
return;
|
|
264
|
+
seen.add(value);
|
|
265
|
+
const obj = value;
|
|
266
|
+
for (const field of ENVELOPE_FIELDS) {
|
|
267
|
+
if (obj[field] !== undefined)
|
|
268
|
+
visit(obj[field], depth + 1);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
visit(error);
|
|
272
|
+
// Safety screen (the curated anti-echo contract keeps applying): prompt
|
|
273
|
+
// echoes, tool outputs, JSON/HTML bodies, PRIVATE-like markers and known
|
|
274
|
+
// compaction-copy fragments are attacker- or prompt-controlled content and
|
|
275
|
+
// are never eligible for the transparency excerpt. Real server diagnostics
|
|
276
|
+
// (status lines, provider error prose) pass through sanitized below.
|
|
277
|
+
const isEcho = (t) => /\b(?:user|your)\s+prompt\b|\btool\s+outputs?\b|\btool\s+results?\b|\bdocs\s+quote\b/i.test(t);
|
|
278
|
+
const containsCoreCopyFragment = /context is too large|auto-compaction could not|use \/compact|start a fresh session/i;
|
|
279
|
+
// Credential-shaped or malformed-ID content is never eligible for echo.
|
|
280
|
+
// Applied AFTER URL pre-redaction so URL-bearing diagnostics still show as
|
|
281
|
+
// "[url]" instead of being dropped entirely.
|
|
282
|
+
const credentialish = (t) => /api[_-]?key|secret|credential|password|passcode|ghp_[a-z0-9]|eyJ[a-z0-9]|sk-[a-z0-9]|private|\/\/|[{"]/i.test(t);
|
|
283
|
+
const preRedact = (t) => t.replace(/\s+/g, " ").replace(/https?:\/\/\S+/gi, "[url]");
|
|
284
|
+
const isUnsafeForEcho = (t) => isEcho(t) || credentialish(t) || containsCoreCopyFragment.test(t)
|
|
285
|
+
|| /^\s*[{\["]/.test(t) || /<(?:!doctype|html|body)\b/i.test(t);
|
|
286
|
+
const diagnosticTexts = texts.map(preRedact).filter(t => t && !isUnsafeForEcho(t));
|
|
287
|
+
const excerptSource = diagnosticTexts.filter(t => /(?:error|fail|exceed|disabled|timeout|filter|quota|unsupported|invalid|refused|terminated|abort|prohibited|denied|limit|not found|unknown)/i.test(t))
|
|
288
|
+
.sort((a, b) => b.length - a.length)[0]
|
|
289
|
+
|| [...diagnosticTexts].sort((a, b) => b.length - a.length)[0]
|
|
290
|
+
|| "";
|
|
291
|
+
if (!excerptSource)
|
|
292
|
+
return undefined;
|
|
293
|
+
const sanitized = excerptSource
|
|
294
|
+
.replace(/\b(?:sk|pk|rk)-[A-Za-z0-9_-]{8,}/g, "[key]")
|
|
295
|
+
.replace(/\b(?:bearer|authorization)\s+[A-Za-z0-9._-]{8,}/gi, "[auth]")
|
|
296
|
+
.replace(/[A-Za-z0-9+/]{32,}={0,2}/g, "[token]")
|
|
297
|
+
.trim()
|
|
298
|
+
.slice(0, 200);
|
|
299
|
+
return sanitized || undefined;
|
|
300
|
+
}
|
|
301
|
+
export function safeErrorSummary(error) {
|
|
302
|
+
const e = classifyProviderError(error);
|
|
303
|
+
return [e.category, e.safeMessage, e.status && `HTTP ${e.status}`, e.code && `code=${e.code}`,
|
|
304
|
+
e.type && `type=${e.type}`, e.param && `param=${e.param}`, e.requestId && `request_id=${e.requestId}`].filter(Boolean).join(" · ");
|
|
305
|
+
}
|
|
306
|
+
export function formatProviderError(error, botName, model, locale) {
|
|
307
|
+
const e = classifyProviderError(error);
|
|
308
|
+
// Transparency for the unclassified gap (user directive 2026-10-09): when
|
|
309
|
+
// no category explains the failure, carry the sanitized original text.
|
|
310
|
+
// Categorized errors keep their curated prose (anti-echo hardening) unless
|
|
311
|
+
// an exact safe message was allowlisted.
|
|
312
|
+
const rawExcerpt = e.category === "unknown" ? rawErrorExcerpt(error) : undefined;
|
|
313
|
+
const en = locale === "en";
|
|
314
|
+
const reasons = {
|
|
315
|
+
cancelled: ["本次运行收到停止或取消信号;不据此认定供应商故障。", "A stop/cancel signal was reported; provider failure is not established."],
|
|
316
|
+
model: ["当前模型不受供应商支持、不可用,或与所用 API 不兼容;供应商支持尚未恢复。", "The current model is unsupported, unavailable or incompatible with this API; provider support has not been restored."],
|
|
317
|
+
model_unavailable: ["所选模型已暂停或当前没有可用上游;这不是工具参数错误。", "The selected model is paused or currently has no usable upstream; this is not a tool-argument error."],
|
|
318
|
+
model_policy: ["当前模型被 OpenClaw 模型策略拒绝(modelPolicy.allow);没有自动切换模型。", "The current model was rejected by OpenClaw modelPolicy.allow; no automatic model switch occurred."],
|
|
319
|
+
tools: ["供应商明确拒绝了 tools/functions 的参数或 JSON Schema。", "The provider explicitly rejected tools/functions arguments or JSON Schema."],
|
|
320
|
+
context: ["请求超过模型上下文或输入长度限制;这不是供应商额度耗尽。", "The request exceeded the model context/input limit; this is not quota exhaustion."],
|
|
321
|
+
compaction_failed: ["整理/压缩未完成,本轮尚未生成回复;请检查摘要辅助模型或后台状态。", "Context preparation/compaction did not complete and this turn has not produced a reply; check the summarization helper model or background status."],
|
|
322
|
+
quota: ["模型/供应商额度已用尽。", "The model/provider quota is exhausted."],
|
|
323
|
+
rate_limit: ["供应商返回限流或额度限制(429);未能确认额度已耗尽。", "The provider reported a rate/quota limit (429); quota exhaustion is not confirmed."],
|
|
324
|
+
auth: ["供应商认证失败或当前账号无权访问。", "Provider authentication failed or the current account lacks access."],
|
|
325
|
+
network: ["连接、DNS 或 TLS 失败;不是已确认的模型响应超时。", "Connection, DNS or TLS failed; this is not a confirmed model response timeout."],
|
|
326
|
+
ttft_timeout: ["等待首个 token/响应头超时。", "Waiting for the first token/response headers timed out."],
|
|
327
|
+
total_timeout: ["请求或运行的总时限已到。", "The request/run total deadline was reached."],
|
|
328
|
+
collector_timeout: ["等待或 RPC 查询超时;不能据此认定供应商故障或底层运行已停止。", "A collector/RPC wait timed out; this does not establish provider failure or stopped execution."],
|
|
329
|
+
timeout: ["收到超时错误,但未能确认是网络、首 token、总时限还是等待层。", "A timeout was reported; its network, first-token, total or collector boundary is unconfirmed."],
|
|
330
|
+
unknown: ["请求未完成,原因尚未分类;不据此认定工具参数错误。", "The request did not complete and the cause is unclassified; a tool-argument failure is not established."],
|
|
331
|
+
};
|
|
332
|
+
const registration = e.category === "model_unavailable" && e.safeMessage === UNKNOWN_MODEL_NOTE;
|
|
333
|
+
const reason = registration
|
|
334
|
+
? (en ? "The selected model is not registered in the current model catalog or is unavailable; this is not a tool-argument error."
|
|
335
|
+
: "所选模型在当前模型目录中未注册或不可用;这不是工具参数错误。")
|
|
336
|
+
: reasons[e.category][en ? 1 : 0];
|
|
337
|
+
const metadata = [e.status && `HTTP ${e.status}`, e.code && `code=${e.code}`, e.type && `type=${e.type}`,
|
|
338
|
+
e.param && `param=${e.param}`, e.requestId && `request_id=${e.requestId}`].filter(Boolean).join(" · ");
|
|
339
|
+
// Names come from bridge configuration, not provider prose. Validate them too.
|
|
340
|
+
const safeName = /^[\p{L}\p{N} _.-]{1,64}$/u.test(botName) ? botName : "Agent";
|
|
341
|
+
const safeModel = isSafeModelId(model) ? model : "unknown";
|
|
342
|
+
const title = e.category === "cancelled" ? (en ? "run cancelled" : "本次运行已取消") : (en ? "did not finish this reply" : "这次没有完成回复");
|
|
343
|
+
return `${e.category === "cancelled" ? "⏹️" : "⚠️"} ${safeName} ${title}${en ? "." : "。"}\n${en ? "Model: " : "模型:"}${safeModel}\n${en ? "Reason: " : "原因:"}${reason}${e.safeMessage && !registration ? `\n${e.safeMessage}` : ""}${rawExcerpt && rawExcerpt !== e.safeMessage ? `\n${en ? "Raw error (excerpt): " : "原始错误(节选):"}${rawExcerpt}` : ""}${metadata ? `\n${metadata}` : ""}`;
|
|
344
|
+
}
|
|
@@ -9,6 +9,9 @@ export interface ErrorProvenance {
|
|
|
9
9
|
userStop?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export interface ProactiveMessageMeta {
|
|
12
|
+
/** Bridge-local Go result owner; never derived from an untrusted model body. */
|
|
13
|
+
goJobId?: number;
|
|
14
|
+
sessionId?: string;
|
|
12
15
|
sourceType?: string;
|
|
13
16
|
runId?: string;
|
|
14
17
|
sessionKey?: string;
|
package/dist/terminal-errors.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { safeErrorSummary } from "./provider-errors.js";
|
|
2
3
|
export const errorIdentity = (sessionKey, runId) => createHash("sha256").update(JSON.stringify([sessionKey, runId])).digest("hex");
|
|
3
4
|
export const genericFailure = (text) => text.trim() === "The agent run failed before producing a reply.";
|
|
4
5
|
// These are candidate locators, NOT business identities. Verify every component
|
|
@@ -43,7 +44,7 @@ export async function inspectError(p, rpc) {
|
|
|
43
44
|
if (p.source === "chat" && (p.state === "error" || (p.state === "final" && p.stopReason === "error")))
|
|
44
45
|
return {
|
|
45
46
|
state: "terminal", reason: "ordinary_gateway_error", terminalKey: `run-error:${errorIdentity(p.sessionKey, p.runId)}`,
|
|
46
|
-
text: `⚠️ Agent run failed${p.detail ? `: ${p.detail}` : ""}`,
|
|
47
|
+
text: `⚠️ Agent run failed${p.detail ? `: ${safeErrorSummary(p.detail)}` : ""}`,
|
|
47
48
|
};
|
|
48
49
|
try {
|
|
49
50
|
const wait = await rpc("agent.wait", { runId: p.runId, timeoutMs: 1 }, 2000);
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-lark-multi-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Multi-bot Lark/Feishu bridge for OpenClaw, with per-bot model routing and isolated sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "tsc && npm run build:steer-plugin",
|
|
7
|
+
"build": "tsc && npm run build:steer-plugin && npm run build:go-plugin",
|
|
8
8
|
"build:steer-plugin": "tsc -p plugins/lma-steer/tsconfig.json",
|
|
9
9
|
"start": "node dist/index.js",
|
|
10
10
|
"dev": "tsx src/index.ts",
|
|
11
11
|
"test": "vitest run --config tests/normal-flow-offline.config.ts",
|
|
12
12
|
"test:coverage": "vitest run --config tests/normal-flow-offline.config.ts --coverage",
|
|
13
13
|
"prepack": "npm run build",
|
|
14
|
-
"prepublishOnly": "npm test && npm pack --dry-run"
|
|
14
|
+
"prepublishOnly": "npm test && npm pack --dry-run",
|
|
15
|
+
"build:go-plugin": "tsc -p plugins/lma-go/tsconfig.json"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"@larksuiteoapi/node-sdk": "^1.73.3",
|
|
@@ -57,6 +58,10 @@
|
|
|
57
58
|
"plugins/lma-steer/openclaw.plugin.json",
|
|
58
59
|
"plugins/lma-steer/package.json",
|
|
59
60
|
"plugins/lma-steer/README.md",
|
|
61
|
+
"plugins/lma-go/dist",
|
|
62
|
+
"plugins/lma-go/openclaw.plugin.json",
|
|
63
|
+
"plugins/lma-go/package.json",
|
|
64
|
+
"plugins/lma-go/README.md",
|
|
60
65
|
"config.example.json",
|
|
61
66
|
"README.md",
|
|
62
67
|
"README.zh-CN.md",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# LMA Go independent runtime enforcement
|
|
2
|
+
|
|
3
|
+
Build with `npm run build:go-plugin`. Enable plugin id `lma-go` through the host's
|
|
4
|
+
supported plugin installation/configuration workflow (operator-owned).
|
|
5
|
+
|
|
6
|
+
The real before_tool_call hook denies **all** tools for explicitly bound dedicated
|
|
7
|
+
Checkers. Exact canonical keys/generations are persisted under the host state dir;
|
|
8
|
+
missing/replaced Checker generations remain denied on the bound key. Executor
|
|
9
|
+
native Goal tools and unrelated sessions are not restricted.
|
|
10
|
+
|
|
11
|
+
Public operator RPCs:
|
|
12
|
+
- `lma.go.capabilities` (operator.read): independent policy/task query capability;
|
|
13
|
+
explicitly no atomic snapshot and no native Goal authority.
|
|
14
|
+
- `lma.go.bind` (operator.admin): persistent idempotent dynamic Checker binding;
|
|
15
|
+
identity conflicts reject. Called before the first Checker admission.
|
|
16
|
+
- `lma.go.snapshot` (operator.read): canonical-key task views, explicit query
|
|
17
|
+
boundaries; the production client separately checks session generation, public
|
|
18
|
+
input/recovery/live-run state and LMA's durable queue/delivery ledger.
|
|
19
|
+
|
|
20
|
+
No static bindings or nativeGoalId are required. The bridge's independent SQLite
|
|
21
|
+
Go state owns objective, criteria, revision and completion. No Host changes needed.
|
|
22
|
+
See ../../docs/go-integration-boundaries.md for limits and activation procedure.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { checkTool } from "./policy.js";
|
|
5
|
+
import { GoPolicyRegistry } from "./registry.js";
|
|
6
|
+
export default definePluginEntry({
|
|
7
|
+
id: "lma-go", name: "LMA Go", description: "Exact-identity persisted Go tool enforcement",
|
|
8
|
+
register(api) {
|
|
9
|
+
const registry = new GoPolicyRegistry(join(resolveStateDir(), "lma-go", "bindings.json"), api.pluginConfig?.bindings ?? []);
|
|
10
|
+
api.on("before_tool_call", (event, ctx) => checkTool(registry.list(), event, ctx), { priority: 10000 });
|
|
11
|
+
api.registerGatewayMethod("lma.go.bind", ({ params, respond }) => {
|
|
12
|
+
try {
|
|
13
|
+
registry.bind(params.binding);
|
|
14
|
+
respond(true, { persisted: true, binding: params.binding });
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
respond(false, undefined, { code: "INVALID_REQUEST", message: e.message });
|
|
18
|
+
}
|
|
19
|
+
}, { scope: "operator.admin" });
|
|
20
|
+
api.registerGatewayMethod("lma.go.capabilities", ({ respond }) => {
|
|
21
|
+
respond(true, { mode: "independent", checkerToolsIsolated: true, taskQuery: true,
|
|
22
|
+
atomicSnapshot: false, nativeGoalAuthority: false });
|
|
23
|
+
}, { scope: "operator.read" });
|
|
24
|
+
api.registerGatewayMethod("lma.go.snapshot", ({ params, respond }) => {
|
|
25
|
+
if (typeof params.sessionKey !== "string" || typeof params.sessionId !== "string") {
|
|
26
|
+
respond(false, undefined, { code: "INVALID_REQUEST", message: "exact session identity required" });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const tasks = api.runtime.tasks.runs.bindSession({ sessionKey: params.sessionKey }).list();
|
|
30
|
+
respond(true, { sessionKey: params.sessionKey, requestedSessionId: params.sessionId, tasks,
|
|
31
|
+
queryAvailable: true, atomicSnapshot: false,
|
|
32
|
+
boundaries: ["tasks scoped to canonical key; adapter pins generation and run ids", "LMA checks its own queue and delivery ledger"] });
|
|
33
|
+
}, { scope: "operator.read" });
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function checkTool(bindings, event, ctx) {
|
|
2
|
+
for (const b of bindings) {
|
|
3
|
+
// Deny also on changed/missing generation: an old checker must not become unrestricted.
|
|
4
|
+
if (ctx.sessionKey === b.checkerSessionKey || ctx.sessionId === b.checkerSessionId)
|
|
5
|
+
return { block: true, blockReason: "LMA Go message-review Checker cannot invoke tools" };
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function parseBindings(value) {
|
|
9
|
+
if (!Array.isArray(value))
|
|
10
|
+
throw new Error("bindings must be an array");
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
return value.map((item) => {
|
|
13
|
+
if (!item || typeof item !== "object")
|
|
14
|
+
throw new Error("invalid binding");
|
|
15
|
+
for (const k of ["checkerSessionKey", "checkerSessionId", "executorSessionKey", "executorSessionId"])
|
|
16
|
+
if (typeof item[k] !== "string" || !item[k].trim())
|
|
17
|
+
throw new Error(`missing ${k}`);
|
|
18
|
+
if (!item.checkerSessionKey.startsWith("agent:") || !item.executorSessionKey.startsWith("agent:"))
|
|
19
|
+
throw new Error("canonical session keys required");
|
|
20
|
+
if (item.checkerSessionKey === item.executorSessionKey || item.checkerSessionId === item.executorSessionId || seen.has(item.checkerSessionKey))
|
|
21
|
+
throw new Error("ambiguous binding");
|
|
22
|
+
seen.add(item.checkerSessionKey);
|
|
23
|
+
return { ...item };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { parseBindings } from "./policy.js";
|
|
4
|
+
/** Atomic persisted restriction registry. Never auto-unbind on restart/stop. */
|
|
5
|
+
export class GoPolicyRegistry {
|
|
6
|
+
path;
|
|
7
|
+
bindings;
|
|
8
|
+
constructor(path, initial = []) {
|
|
9
|
+
this.path = path;
|
|
10
|
+
let value;
|
|
11
|
+
try {
|
|
12
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
if (e.code !== "ENOENT")
|
|
16
|
+
throw e;
|
|
17
|
+
value = initial;
|
|
18
|
+
}
|
|
19
|
+
this.bindings = parseBindings(value);
|
|
20
|
+
}
|
|
21
|
+
list() { return this.bindings.map(b => ({ ...b })); }
|
|
22
|
+
bind(value) {
|
|
23
|
+
const [binding] = parseBindings([value]);
|
|
24
|
+
const previous = this.bindings.find(b => b.checkerSessionKey === binding.checkerSessionKey);
|
|
25
|
+
if (previous) {
|
|
26
|
+
if (JSON.stringify(previous) !== JSON.stringify(binding))
|
|
27
|
+
throw new Error("Binding identity conflict");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const next = parseBindings([...this.bindings, binding]);
|
|
31
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
32
|
+
const temp = `${this.path}.tmp`;
|
|
33
|
+
writeFileSync(temp, JSON.stringify(next), { mode: 0o600 });
|
|
34
|
+
renameSync(temp, this.path);
|
|
35
|
+
this.bindings = next;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id": "lma-go", "name": "LMA Go", "description": "Independent LMA Go persisted Checker tool enforcement", "configSchema": {"type": "object", "additionalProperties": false, "properties": {"bindings": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["checkerSessionKey", "checkerSessionId", "executorSessionKey", "executorSessionId"], "properties": {"checkerSessionKey": {"type": "string"}, "checkerSessionId": {"type": "string"}, "executorSessionKey": {"type": "string"}, "executorSessionId": {"type": "string"}, "goalId": {"type": "string"}}}}}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"@hackerphysics/lma-go","version":"0.1.0","type":"module","openclaw":{"extensions":["./dist/index.js"]}}
|