@poolzin/pool-bot 2026.3.22 → 2026.3.23
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/CHANGELOG.md +54 -0
- package/dist/acp/bindings-store.js +209 -0
- package/dist/acp/control-plane/runtime-cache.js +54 -0
- package/dist/acp/control-plane/runtime-options.js +215 -0
- package/dist/acp/control-plane/session-actor-queue.js +36 -0
- package/dist/acp/runtime/errors.js +47 -0
- package/dist/acp/runtime/registry.js +86 -0
- package/dist/acp/runtime/types.js +1 -0
- package/dist/acp/translator.js +97 -0
- package/dist/agents/failover-error.js +145 -47
- package/dist/browser/browser-profile-manager.js +319 -0
- package/dist/browser/cdp-proxy-bypass.js +129 -0
- package/dist/browser/cdp-timeouts.js +41 -0
- package/dist/browser/chrome-extension-validator.js +406 -0
- package/dist/browser/chrome-mcp-snapshot.js +222 -0
- package/dist/browser/chrome-mcp.js +421 -0
- package/dist/browser/chrome-mcp.snapshot.js +133 -0
- package/dist/browser/errors.js +67 -0
- package/dist/browser/form-fields.js +22 -0
- package/dist/browser/output-atomic.js +44 -0
- package/dist/browser/profile-capabilities.js +47 -0
- package/dist/browser/safe-filename.js +25 -0
- package/dist/browser/snapshot-roles.js +60 -0
- package/dist/build-info.json +3 -3
- package/dist/commands/security-owner-only.js +86 -0
- package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
- package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/cron-filters.js +150 -0
- package/dist/gateway/device-pairing-security.js +197 -0
- package/dist/gateway/event-deduplication.js +167 -0
- package/dist/gateway/run-tracker.js +253 -0
- package/dist/gateway/server-methods/nodes.js +14 -0
- package/dist/gateway/websocket-preauth-security.js +188 -0
- package/dist/infra/errors.js +53 -13
- package/dist/infra/exec-approvals-security.js +217 -0
- package/dist/infra/security/command-analyzer.js +257 -0
- package/dist/plugins/loader.js +16 -8
- package/dist/security/external-content.js +51 -1
- package/dist/sessions/session-costs.js +228 -0
- package/dist/shared/param-key.js +16 -0
- package/dist/shared/poll-params.js +58 -0
- package/dist/shared/polls.js +55 -0
- package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
- package/docs/FEATURES.md +523 -0
- package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
- package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
- package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
- package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
- package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
- package/docs/MIKRODASH-ANALYSIS.md +412 -0
- package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
- package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
- package/docs/PHASE-7-SUMMARY.md +144 -0
- package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
- package/docs/PROJECT-FINAL-STATUS.md +237 -0
- package/docs/README.md +116 -0
- package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
- package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
- package/docs/channels/googlechat.md +235 -206
- package/docs/channels/irc.md +332 -0
- package/docs/channels/nostr.md +255 -168
- package/docs/components/command-palette.md +166 -0
- package/docs/components/login-gate.md +219 -0
- package/docs/getting-started/installation.md +191 -0
- package/docs/getting-started/introduction.md +120 -0
- package/docs/improvements/USAGE-GUIDE.md +359 -0
- package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
- package/docs/reference/deadcode-detection.md +72 -0
- package/extensions/acpx/node_modules/.bin/acpx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +4 -4
- package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
- package/extensions/googlechat/node_modules/.bin/tsc +21 -0
- package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
- package/extensions/googlechat/node_modules/.bin/vitest +21 -0
- package/extensions/googlechat/package.json +11 -28
- package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
- package/extensions/googlechat/src/googlechat-channel.ts +120 -0
- package/extensions/googlechat/src/index.ts +14 -0
- package/extensions/irc/node_modules/.bin/tsc +21 -0
- package/extensions/irc/node_modules/.bin/tsserver +21 -0
- package/extensions/irc/node_modules/.bin/vitest +21 -0
- package/extensions/irc/package.json +16 -8
- package/extensions/irc/src/index.ts +14 -0
- package/extensions/irc/src/irc-channel.test.ts +43 -0
- package/extensions/irc/src/irc-channel.ts +191 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
- package/extensions/keyed-async-queue/package.json +20 -0
- package/extensions/keyed-async-queue/src/index.ts +14 -0
- package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
- package/extensions/keyed-async-queue/src/queue.ts +200 -0
- package/extensions/memory-core/node_modules/.bin/tsc +21 -0
- package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
- package/extensions/memory-core/node_modules/.bin/vitest +21 -0
- package/extensions/memory-core/package.json +11 -8
- package/extensions/memory-core/src/index.ts +14 -0
- package/extensions/memory-core/src/memory-manager.test.ts +124 -0
- package/extensions/memory-core/src/memory-manager.ts +186 -0
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/nostr/node_modules/.bin/vitest +21 -0
- package/extensions/nostr/package.json +15 -24
- package/extensions/nostr/src/index.ts +14 -0
- package/extensions/nostr/src/nostr-channel.test.ts +55 -0
- package/extensions/nostr/src/nostr-channel.ts +228 -0
- package/extensions/page-agent/node_modules/.bin/vitest +2 -2
- package/extensions/test-utils/node_modules/.bin/jiti +21 -0
- package/extensions/test-utils/node_modules/.bin/playwright +21 -0
- package/extensions/test-utils/node_modules/.bin/tsx +21 -0
- package/extensions/test-utils/node_modules/.bin/vite +21 -0
- package/extensions/test-utils/node_modules/.bin/vitest +21 -0
- package/extensions/test-utils/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +2 -2
- package/package.json +2 -1
- package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
- package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
- package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,57 @@
|
|
|
1
|
+
## v2026.3.23 (2026-03-16)
|
|
2
|
+
|
|
3
|
+
### 🎯 OpenClaw Integration - Melhorias Importadas
|
|
4
|
+
|
|
5
|
+
#### Sistema de Polls/Enquetes
|
|
6
|
+
- **polls.ts:** Criação e normalização de enquetes multi-canal
|
|
7
|
+
- **poll-params.ts:** Definições de parâmetros para polls
|
|
8
|
+
- **param-key.ts:** Utilitários para leitura de parâmetros
|
|
9
|
+
- **Suporte:** Telegram, Discord e outros canais
|
|
10
|
+
- **Testes:** 15 testes unitários passando
|
|
11
|
+
|
|
12
|
+
#### Error Handling Avançado
|
|
13
|
+
- Validação de que Pool Bot já possui sistema superior ao OpenClaw
|
|
14
|
+
- 840 linhas de error handling vs 330 do OpenClaw
|
|
15
|
+
- Classificação detalhada de erros
|
|
16
|
+
- Redaction de dados sensíveis
|
|
17
|
+
|
|
18
|
+
### 📊 Melhorias de Core (v2026.3.22)
|
|
19
|
+
|
|
20
|
+
#### Event Deduplication Layer
|
|
21
|
+
- Previne processamento duplicado de eventos
|
|
22
|
+
- LRU-style tracking com TTL configurável
|
|
23
|
+
- ~100KB overhead para 10k eventos
|
|
24
|
+
- 14 testes unitários
|
|
25
|
+
|
|
26
|
+
#### Run Tracker com AbortController
|
|
27
|
+
- Cancelamento limpo de operações longas
|
|
28
|
+
- TransformStream buffering para streaming real-time
|
|
29
|
+
- Estatísticas de runs ativas e duração média
|
|
30
|
+
- 18 testes unitários
|
|
31
|
+
|
|
32
|
+
#### Command Analyzer & Security
|
|
33
|
+
- 40+ padrões perigosos detectados
|
|
34
|
+
- Shell wrapper detection (10 patterns)
|
|
35
|
+
- PATH traversal prevention
|
|
36
|
+
- Command injection detection
|
|
37
|
+
- 36 testes unitários
|
|
38
|
+
|
|
39
|
+
#### Browser Profile Manager
|
|
40
|
+
- Múltiplos perfis Chrome isolados
|
|
41
|
+
- Cookies e storage isolados por perfil
|
|
42
|
+
- Suporte a proxy por perfil
|
|
43
|
+
- Integrado com chrome-mcp.ts
|
|
44
|
+
|
|
45
|
+
### 📝 Documentation
|
|
46
|
+
- **IMPLEMENTATIONS_COMPLETE.md:** Resumo completo das implementações
|
|
47
|
+
- **OPENCLAW_GAP_ANALYSIS.md:** Análise de gaps vs OpenClaw
|
|
48
|
+
- **docs/improvements/USAGE-GUIDE.md:** Guia de uso de cada componente
|
|
49
|
+
|
|
50
|
+
### 🧪 Test Coverage
|
|
51
|
+
- 68 testes para componentes core
|
|
52
|
+
- 15 testes para sistema de polls
|
|
53
|
+
- 90%+ coverage nos novos componentes
|
|
54
|
+
|
|
1
55
|
## v2026.3.22 (2026-03-13)
|
|
2
56
|
|
|
3
57
|
### ✅ Testes Críticos Implementados
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP Persistent Bindings Store
|
|
3
|
+
* Persists tool bindings across sessions with validation
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Persistent store for ACP tool bindings
|
|
9
|
+
*/
|
|
10
|
+
export class BindingsStore {
|
|
11
|
+
dataDir;
|
|
12
|
+
bindingsFile;
|
|
13
|
+
cache = new Map();
|
|
14
|
+
validateOnLoad;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.dataDir = options.dataDir;
|
|
17
|
+
this.bindingsFile = path.join(this.dataDir, "bindings.json");
|
|
18
|
+
this.validateOnLoad = options.validateOnLoad ?? true;
|
|
19
|
+
// Ensure data directory exists
|
|
20
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
21
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load bindings from disk
|
|
26
|
+
*/
|
|
27
|
+
async load() {
|
|
28
|
+
const errors = [];
|
|
29
|
+
const warnings = [];
|
|
30
|
+
if (!fs.existsSync(this.bindingsFile)) {
|
|
31
|
+
return { valid: true, errors, warnings };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const content = fs.readFileSync(this.bindingsFile, "utf-8");
|
|
35
|
+
const data = JSON.parse(content);
|
|
36
|
+
// Clear cache
|
|
37
|
+
this.cache.clear();
|
|
38
|
+
// Load and validate each binding
|
|
39
|
+
for (const binding of data) {
|
|
40
|
+
if (this.validateOnLoad) {
|
|
41
|
+
const validation = this.validateBinding(binding);
|
|
42
|
+
errors.push(...validation.errors.map((e) => `Binding ${binding.id}: ${e}`));
|
|
43
|
+
warnings.push(...validation.warnings.map((w) => `Binding ${binding.id}: ${w}`));
|
|
44
|
+
if (!validation.valid) {
|
|
45
|
+
continue; // Skip invalid bindings
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Check expiration
|
|
49
|
+
if (binding.expiresAt && binding.expiresAt < Date.now()) {
|
|
50
|
+
warnings.push(`Binding ${binding.id}: expired`);
|
|
51
|
+
continue; // Skip expired bindings
|
|
52
|
+
}
|
|
53
|
+
this.cache.set(binding.id, binding);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
valid: errors.length === 0,
|
|
57
|
+
errors,
|
|
58
|
+
warnings,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
errors.push(`Failed to load bindings: ${e.message}`);
|
|
63
|
+
return { valid: false, errors, warnings };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Save bindings to disk
|
|
68
|
+
*/
|
|
69
|
+
async save() {
|
|
70
|
+
const bindings = Array.from(this.cache.values());
|
|
71
|
+
const content = JSON.stringify(bindings, null, 2);
|
|
72
|
+
fs.writeFileSync(this.bindingsFile, content, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Add a new binding
|
|
76
|
+
*/
|
|
77
|
+
addBinding(binding) {
|
|
78
|
+
this.cache.set(binding.id, binding);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get a binding by ID
|
|
82
|
+
*/
|
|
83
|
+
getBinding(id) {
|
|
84
|
+
return this.cache.get(id);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Remove a binding by ID
|
|
88
|
+
*/
|
|
89
|
+
removeBinding(id) {
|
|
90
|
+
return this.cache.delete(id);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get all bindings for a session
|
|
94
|
+
*/
|
|
95
|
+
getSessionBindings(sessionId) {
|
|
96
|
+
return Array.from(this.cache.values()).filter((b) => b.scope === "session" && b.sessionId === sessionId);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get all bindings for an agent
|
|
100
|
+
*/
|
|
101
|
+
getAgentBindings(agentId) {
|
|
102
|
+
return Array.from(this.cache.values()).filter((b) => b.scope === "agent" && b.agentId === agentId);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get all global bindings
|
|
106
|
+
*/
|
|
107
|
+
getGlobalBindings() {
|
|
108
|
+
return Array.from(this.cache.values()).filter((b) => b.scope === "global");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get all bindings (optionally filtered by scope)
|
|
112
|
+
*/
|
|
113
|
+
getAllBindings(scope) {
|
|
114
|
+
const all = Array.from(this.cache.values());
|
|
115
|
+
if (scope) {
|
|
116
|
+
return all.filter((b) => b.scope === scope);
|
|
117
|
+
}
|
|
118
|
+
return all;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Clear expired bindings
|
|
122
|
+
*/
|
|
123
|
+
clearExpired() {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
let cleared = 0;
|
|
126
|
+
for (const [id, binding] of this.cache.entries()) {
|
|
127
|
+
if (binding.expiresAt && binding.expiresAt < now) {
|
|
128
|
+
this.cache.delete(id);
|
|
129
|
+
cleared++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return cleared;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Clear all bindings
|
|
136
|
+
*/
|
|
137
|
+
clearAll() {
|
|
138
|
+
this.cache.clear();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Validate a single binding
|
|
142
|
+
*/
|
|
143
|
+
validateBinding(binding) {
|
|
144
|
+
const errors = [];
|
|
145
|
+
const warnings = [];
|
|
146
|
+
// Required fields
|
|
147
|
+
if (!binding.id) {
|
|
148
|
+
errors.push("Missing required field: id");
|
|
149
|
+
}
|
|
150
|
+
if (!binding.name) {
|
|
151
|
+
errors.push("Missing required field: name");
|
|
152
|
+
}
|
|
153
|
+
if (!binding.scope) {
|
|
154
|
+
errors.push("Missing required field: scope");
|
|
155
|
+
}
|
|
156
|
+
else if (!["session", "global", "agent"].includes(binding.scope)) {
|
|
157
|
+
errors.push(`Invalid scope: ${binding.scope}`);
|
|
158
|
+
}
|
|
159
|
+
// Scope-specific validation
|
|
160
|
+
if (binding.scope === "session" && !binding.sessionId) {
|
|
161
|
+
errors.push("Session scope requires sessionId");
|
|
162
|
+
}
|
|
163
|
+
if (binding.scope === "agent" && !binding.agentId) {
|
|
164
|
+
errors.push("Agent scope requires agentId");
|
|
165
|
+
}
|
|
166
|
+
// Timestamp validation
|
|
167
|
+
if (!binding.createdAt) {
|
|
168
|
+
errors.push("Missing required field: createdAt");
|
|
169
|
+
}
|
|
170
|
+
if (binding.expiresAt && binding.expiresAt <= binding.createdAt) {
|
|
171
|
+
errors.push("expiresAt must be after createdAt");
|
|
172
|
+
}
|
|
173
|
+
// Warnings
|
|
174
|
+
if (binding.expiresAt && binding.expiresAt - binding.createdAt > 86400000 * 30) {
|
|
175
|
+
warnings.push("Binding expires in more than 30 days");
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
valid: errors.length === 0,
|
|
179
|
+
errors,
|
|
180
|
+
warnings,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get store statistics
|
|
185
|
+
*/
|
|
186
|
+
getStats() {
|
|
187
|
+
const all = Array.from(this.cache.values());
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
return {
|
|
190
|
+
total: all.length,
|
|
191
|
+
byScope: {
|
|
192
|
+
session: all.filter((b) => b.scope === "session").length,
|
|
193
|
+
global: all.filter((b) => b.scope === "global").length,
|
|
194
|
+
agent: all.filter((b) => b.scope === "agent").length,
|
|
195
|
+
},
|
|
196
|
+
expired: all.filter((b) => b.expiresAt && b.expiresAt < now).length,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Create a default bindings store
|
|
202
|
+
*/
|
|
203
|
+
export function createBindingsStore(dataDir) {
|
|
204
|
+
const dir = dataDir || path.join(process.cwd(), ".poolbot", "bindings");
|
|
205
|
+
return new BindingsStore({
|
|
206
|
+
dataDir: dir,
|
|
207
|
+
validateOnLoad: true,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class RuntimeCache {
|
|
2
|
+
cache = new Map();
|
|
3
|
+
size() {
|
|
4
|
+
return this.cache.size;
|
|
5
|
+
}
|
|
6
|
+
has(actorKey) {
|
|
7
|
+
return this.cache.has(actorKey);
|
|
8
|
+
}
|
|
9
|
+
get(actorKey, params = {}) {
|
|
10
|
+
const entry = this.cache.get(actorKey);
|
|
11
|
+
if (!entry) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (params.touch !== false) {
|
|
15
|
+
entry.lastTouchedAt = params.now ?? Date.now();
|
|
16
|
+
}
|
|
17
|
+
return entry.state;
|
|
18
|
+
}
|
|
19
|
+
peek(actorKey) {
|
|
20
|
+
return this.get(actorKey, { touch: false });
|
|
21
|
+
}
|
|
22
|
+
getLastTouchedAt(actorKey) {
|
|
23
|
+
return this.cache.get(actorKey)?.lastTouchedAt ?? null;
|
|
24
|
+
}
|
|
25
|
+
set(actorKey, state, params = {}) {
|
|
26
|
+
this.cache.set(actorKey, {
|
|
27
|
+
state,
|
|
28
|
+
lastTouchedAt: params.now ?? Date.now(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
clear(actorKey) {
|
|
32
|
+
this.cache.delete(actorKey);
|
|
33
|
+
}
|
|
34
|
+
snapshot(params = {}) {
|
|
35
|
+
const now = params.now ?? Date.now();
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (const [actorKey, entry] of this.cache.entries()) {
|
|
38
|
+
entries.push({
|
|
39
|
+
actorKey,
|
|
40
|
+
state: entry.state,
|
|
41
|
+
lastTouchedAt: entry.lastTouchedAt,
|
|
42
|
+
idleMs: Math.max(0, now - entry.lastTouchedAt),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
collectIdleCandidates(params) {
|
|
48
|
+
if (!Number.isFinite(params.maxIdleMs) || params.maxIdleMs <= 0) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const now = params.now ?? Date.now();
|
|
52
|
+
return this.snapshot({ now }).filter((entry) => entry.idleMs >= params.maxIdleMs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { AcpRuntimeError } from "../runtime/errors.js";
|
|
3
|
+
const MAX_RUNTIME_MODE_LENGTH = 64;
|
|
4
|
+
const MAX_MODEL_LENGTH = 200;
|
|
5
|
+
const MAX_PERMISSION_PROFILE_LENGTH = 80;
|
|
6
|
+
const MAX_CWD_LENGTH = 4096;
|
|
7
|
+
const MIN_TIMEOUT_SECONDS = 1;
|
|
8
|
+
const MAX_TIMEOUT_SECONDS = 24 * 60 * 60;
|
|
9
|
+
const MAX_BACKEND_OPTION_KEY_LENGTH = 64;
|
|
10
|
+
const MAX_BACKEND_OPTION_VALUE_LENGTH = 512;
|
|
11
|
+
const MAX_BACKEND_EXTRAS = 32;
|
|
12
|
+
const SAFE_OPTION_KEY_RE = /^[a-z0-9][a-z0-9._:-]*$/i;
|
|
13
|
+
function failInvalidOption(message) {
|
|
14
|
+
throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", message);
|
|
15
|
+
}
|
|
16
|
+
function validateNoControlChars(value, field) {
|
|
17
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
18
|
+
const code = value.charCodeAt(i);
|
|
19
|
+
if (code < 32 || code === 127) {
|
|
20
|
+
failInvalidOption(`${field} must not include control characters.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function validateBoundedText(params) {
|
|
26
|
+
const normalized = normalizeText(params.value);
|
|
27
|
+
if (!normalized) {
|
|
28
|
+
failInvalidOption(`${params.field} must not be empty.`);
|
|
29
|
+
}
|
|
30
|
+
if (normalized.length > params.maxLength) {
|
|
31
|
+
failInvalidOption(`${params.field} must be at most ${params.maxLength} characters.`);
|
|
32
|
+
}
|
|
33
|
+
return validateNoControlChars(normalized, params.field);
|
|
34
|
+
}
|
|
35
|
+
function validateBackendOptionKey(rawKey) {
|
|
36
|
+
const key = validateBoundedText({
|
|
37
|
+
value: rawKey,
|
|
38
|
+
field: "ACP config key",
|
|
39
|
+
maxLength: MAX_BACKEND_OPTION_KEY_LENGTH,
|
|
40
|
+
});
|
|
41
|
+
if (!SAFE_OPTION_KEY_RE.test(key)) {
|
|
42
|
+
failInvalidOption("ACP config key must use letters, numbers, dots, colons, underscores, or dashes.");
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
function validateBackendOptionValue(rawValue) {
|
|
47
|
+
return validateBoundedText({
|
|
48
|
+
value: rawValue,
|
|
49
|
+
field: "ACP config value",
|
|
50
|
+
maxLength: MAX_BACKEND_OPTION_VALUE_LENGTH,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function validateRuntimeModeInput(rawMode) {
|
|
54
|
+
return validateBoundedText({
|
|
55
|
+
value: rawMode,
|
|
56
|
+
field: "Runtime mode",
|
|
57
|
+
maxLength: MAX_RUNTIME_MODE_LENGTH,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function validateRuntimeModelInput(rawModel) {
|
|
61
|
+
return validateBoundedText({
|
|
62
|
+
value: rawModel,
|
|
63
|
+
field: "Model id",
|
|
64
|
+
maxLength: MAX_MODEL_LENGTH,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function validateRuntimePermissionProfileInput(rawProfile) {
|
|
68
|
+
return validateBoundedText({
|
|
69
|
+
value: rawProfile,
|
|
70
|
+
field: "Permission profile",
|
|
71
|
+
maxLength: MAX_PERMISSION_PROFILE_LENGTH,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function validateRuntimeCwdInput(rawCwd) {
|
|
75
|
+
const cwd = validateBoundedText({
|
|
76
|
+
value: rawCwd,
|
|
77
|
+
field: "Working directory",
|
|
78
|
+
maxLength: MAX_CWD_LENGTH,
|
|
79
|
+
});
|
|
80
|
+
if (!isAbsolute(cwd)) {
|
|
81
|
+
failInvalidOption(`Working directory must be an absolute path. Received "${cwd}".`);
|
|
82
|
+
}
|
|
83
|
+
return cwd;
|
|
84
|
+
}
|
|
85
|
+
export function validateRuntimeTimeoutSecondsInput(rawTimeout) {
|
|
86
|
+
if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout)) {
|
|
87
|
+
failInvalidOption("Timeout must be a positive integer in seconds.");
|
|
88
|
+
}
|
|
89
|
+
const timeout = Math.round(rawTimeout);
|
|
90
|
+
if (timeout < MIN_TIMEOUT_SECONDS || timeout > MAX_TIMEOUT_SECONDS) {
|
|
91
|
+
failInvalidOption(`Timeout must be between ${MIN_TIMEOUT_SECONDS} and ${MAX_TIMEOUT_SECONDS} seconds.`);
|
|
92
|
+
}
|
|
93
|
+
return timeout;
|
|
94
|
+
}
|
|
95
|
+
export function validateRuntimeConfigOptionInput(rawKey, rawValue) {
|
|
96
|
+
return {
|
|
97
|
+
key: validateBackendOptionKey(rawKey),
|
|
98
|
+
value: validateBackendOptionValue(rawValue),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function validateRuntimeOptionPatch(patch) {
|
|
102
|
+
if (!patch) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
const rawPatch = patch;
|
|
106
|
+
const allowedKeys = new Set([
|
|
107
|
+
"runtimeMode",
|
|
108
|
+
"model",
|
|
109
|
+
"cwd",
|
|
110
|
+
"permissionProfile",
|
|
111
|
+
"timeoutSeconds",
|
|
112
|
+
"backendExtras",
|
|
113
|
+
]);
|
|
114
|
+
for (const key of Object.keys(rawPatch)) {
|
|
115
|
+
if (!allowedKeys.has(key)) {
|
|
116
|
+
failInvalidOption(`Unknown runtime option "${key}".`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const next = {};
|
|
120
|
+
if (Object.hasOwn(rawPatch, "runtimeMode")) {
|
|
121
|
+
next.runtimeMode =
|
|
122
|
+
rawPatch.runtimeMode === undefined
|
|
123
|
+
? undefined
|
|
124
|
+
: validateRuntimeModeInput(rawPatch.runtimeMode);
|
|
125
|
+
}
|
|
126
|
+
if (Object.hasOwn(rawPatch, "model")) {
|
|
127
|
+
next.model =
|
|
128
|
+
rawPatch.model === undefined ? undefined : validateRuntimeModelInput(rawPatch.model);
|
|
129
|
+
}
|
|
130
|
+
if (Object.hasOwn(rawPatch, "cwd")) {
|
|
131
|
+
next.cwd = rawPatch.cwd === undefined ? undefined : validateRuntimeCwdInput(rawPatch.cwd);
|
|
132
|
+
}
|
|
133
|
+
if (Object.hasOwn(rawPatch, "permissionProfile")) {
|
|
134
|
+
next.permissionProfile =
|
|
135
|
+
rawPatch.permissionProfile === undefined
|
|
136
|
+
? undefined
|
|
137
|
+
: validateRuntimePermissionProfileInput(rawPatch.permissionProfile);
|
|
138
|
+
}
|
|
139
|
+
if (Object.hasOwn(rawPatch, "timeoutSeconds")) {
|
|
140
|
+
next.timeoutSeconds =
|
|
141
|
+
rawPatch.timeoutSeconds === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: validateRuntimeTimeoutSecondsInput(rawPatch.timeoutSeconds);
|
|
144
|
+
}
|
|
145
|
+
if (Object.hasOwn(rawPatch, "backendExtras")) {
|
|
146
|
+
const rawExtras = rawPatch.backendExtras;
|
|
147
|
+
if (rawExtras === undefined) {
|
|
148
|
+
next.backendExtras = undefined;
|
|
149
|
+
}
|
|
150
|
+
else if (!rawExtras || typeof rawExtras !== "object" || Array.isArray(rawExtras)) {
|
|
151
|
+
failInvalidOption("Backend extras must be a key/value object.");
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const entries = Object.entries(rawExtras);
|
|
155
|
+
if (entries.length > MAX_BACKEND_EXTRAS) {
|
|
156
|
+
failInvalidOption(`Backend extras must include at most ${MAX_BACKEND_EXTRAS} entries.`);
|
|
157
|
+
}
|
|
158
|
+
const extras = {};
|
|
159
|
+
for (const [entryKey, entryValue] of entries) {
|
|
160
|
+
const { key, value } = validateRuntimeConfigOptionInput(entryKey, entryValue);
|
|
161
|
+
extras[key] = value;
|
|
162
|
+
}
|
|
163
|
+
next.backendExtras = Object.keys(extras).length > 0 ? extras : undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
export function normalizeText(value) {
|
|
169
|
+
if (typeof value !== "string") {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const trimmed = value.trim();
|
|
173
|
+
return trimmed || undefined;
|
|
174
|
+
}
|
|
175
|
+
export function normalizeRuntimeOptions(options) {
|
|
176
|
+
const runtimeMode = normalizeText(options?.runtimeMode);
|
|
177
|
+
const model = normalizeText(options?.model);
|
|
178
|
+
const cwd = normalizeText(options?.cwd);
|
|
179
|
+
const permissionProfile = normalizeText(options?.permissionProfile);
|
|
180
|
+
let timeoutSeconds;
|
|
181
|
+
if (typeof options?.timeoutSeconds === "number" && Number.isFinite(options.timeoutSeconds)) {
|
|
182
|
+
const rounded = Math.round(options.timeoutSeconds);
|
|
183
|
+
if (rounded > 0) {
|
|
184
|
+
timeoutSeconds = rounded;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const backendExtrasEntries = Object.entries(options?.backendExtras ?? {})
|
|
188
|
+
.map(([key, value]) => [normalizeText(key), normalizeText(value)])
|
|
189
|
+
.filter(([key, value]) => Boolean(key && value));
|
|
190
|
+
const backendExtras = backendExtrasEntries.length > 0 ? Object.fromEntries(backendExtrasEntries) : undefined;
|
|
191
|
+
return {
|
|
192
|
+
...(runtimeMode ? { runtimeMode } : {}),
|
|
193
|
+
...(model ? { model } : {}),
|
|
194
|
+
...(cwd ? { cwd } : {}),
|
|
195
|
+
...(permissionProfile ? { permissionProfile } : {}),
|
|
196
|
+
...(typeof timeoutSeconds === "number" ? { timeoutSeconds } : {}),
|
|
197
|
+
...(backendExtras ? { backendExtras } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function mergeRuntimeOptions(params) {
|
|
201
|
+
const current = normalizeRuntimeOptions(params.current);
|
|
202
|
+
const patch = normalizeRuntimeOptions(validateRuntimeOptionPatch(params.patch));
|
|
203
|
+
const mergedExtras = {
|
|
204
|
+
...current.backendExtras,
|
|
205
|
+
...patch.backendExtras,
|
|
206
|
+
};
|
|
207
|
+
return normalizeRuntimeOptions({
|
|
208
|
+
...current,
|
|
209
|
+
...patch,
|
|
210
|
+
...(Object.keys(mergedExtras).length > 0 ? { backendExtras: mergedExtras } : {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export function runtimeOptionsEqual(a, b) {
|
|
214
|
+
return JSON.stringify(normalizeRuntimeOptions(a)) === JSON.stringify(normalizeRuntimeOptions(b));
|
|
215
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { KeyedAsyncQueue } from "../../plugin-sdk/keyed-async-queue.js";
|
|
2
|
+
export class SessionActorQueue {
|
|
3
|
+
queue = new KeyedAsyncQueue();
|
|
4
|
+
pendingBySession = new Map();
|
|
5
|
+
tailMap = new Map();
|
|
6
|
+
getTailMapForTesting() {
|
|
7
|
+
return this.tailMap;
|
|
8
|
+
}
|
|
9
|
+
getTotalPendingCount() {
|
|
10
|
+
let total = 0;
|
|
11
|
+
for (const count of this.pendingBySession.values()) {
|
|
12
|
+
total += count;
|
|
13
|
+
}
|
|
14
|
+
return total;
|
|
15
|
+
}
|
|
16
|
+
getPendingCountForSession(actorKey) {
|
|
17
|
+
return this.pendingBySession.get(actorKey) ?? 0;
|
|
18
|
+
}
|
|
19
|
+
async run(actorKey, op) {
|
|
20
|
+
this.pendingBySession.set(actorKey, (this.pendingBySession.get(actorKey) ?? 0) + 1);
|
|
21
|
+
await this.queue.acquire(actorKey);
|
|
22
|
+
try {
|
|
23
|
+
return await op();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
this.queue.release(actorKey);
|
|
27
|
+
const pending = (this.pendingBySession.get(actorKey) ?? 1) - 1;
|
|
28
|
+
if (pending <= 0) {
|
|
29
|
+
this.pendingBySession.delete(actorKey);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.pendingBySession.set(actorKey, pending);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const ACP_ERROR_CODES = [
|
|
2
|
+
"ACP_BACKEND_MISSING",
|
|
3
|
+
"ACP_BACKEND_UNAVAILABLE",
|
|
4
|
+
"ACP_BACKEND_UNSUPPORTED_CONTROL",
|
|
5
|
+
"ACP_DISPATCH_DISABLED",
|
|
6
|
+
"ACP_INVALID_RUNTIME_OPTION",
|
|
7
|
+
"ACP_SESSION_INIT_FAILED",
|
|
8
|
+
"ACP_TURN_FAILED",
|
|
9
|
+
];
|
|
10
|
+
export class AcpRuntimeError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
cause;
|
|
13
|
+
constructor(code, message, options) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AcpRuntimeError";
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.cause = options?.cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isAcpRuntimeError(value) {
|
|
21
|
+
return value instanceof AcpRuntimeError;
|
|
22
|
+
}
|
|
23
|
+
export function toAcpRuntimeError(params) {
|
|
24
|
+
if (params.error instanceof AcpRuntimeError) {
|
|
25
|
+
return params.error;
|
|
26
|
+
}
|
|
27
|
+
if (params.error instanceof Error) {
|
|
28
|
+
return new AcpRuntimeError(params.fallbackCode, params.error.message, {
|
|
29
|
+
cause: params.error,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return new AcpRuntimeError(params.fallbackCode, params.fallbackMessage, {
|
|
33
|
+
cause: params.error,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function withAcpRuntimeErrorBoundary(params) {
|
|
37
|
+
try {
|
|
38
|
+
return await params.run();
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw toAcpRuntimeError({
|
|
42
|
+
error,
|
|
43
|
+
fallbackCode: params.fallbackCode,
|
|
44
|
+
fallbackMessage: params.fallbackMessage,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|