@poolzin/pool-bot 2026.3.21 → 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.
Files changed (124) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/acp/bindings-store.js +209 -0
  3. package/dist/acp/control-plane/runtime-cache.js +54 -0
  4. package/dist/acp/control-plane/runtime-options.js +215 -0
  5. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  6. package/dist/acp/runtime/errors.js +47 -0
  7. package/dist/acp/runtime/registry.js +86 -0
  8. package/dist/acp/runtime/types.js +1 -0
  9. package/dist/acp/translator.js +97 -0
  10. package/dist/agents/failover-error.js +145 -47
  11. package/dist/browser/browser-profile-manager.js +319 -0
  12. package/dist/browser/cdp-proxy-bypass.js +129 -0
  13. package/dist/browser/cdp-timeouts.js +41 -0
  14. package/dist/browser/chrome-extension-validator.js +406 -0
  15. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  16. package/dist/browser/chrome-mcp.js +421 -0
  17. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  18. package/dist/browser/errors.js +67 -0
  19. package/dist/browser/form-fields.js +22 -0
  20. package/dist/browser/output-atomic.js +44 -0
  21. package/dist/browser/profile-capabilities.js +47 -0
  22. package/dist/browser/safe-filename.js +25 -0
  23. package/dist/browser/snapshot-roles.js +60 -0
  24. package/dist/build-info.json +3 -3
  25. package/dist/commands/security-owner-only.js +86 -0
  26. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  27. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  28. package/dist/control-ui/index.html +1 -1
  29. package/dist/cron/cron-filters.js +150 -0
  30. package/dist/gateway/device-pairing-security.js +197 -0
  31. package/dist/gateway/event-deduplication.js +167 -0
  32. package/dist/gateway/run-tracker.js +253 -0
  33. package/dist/gateway/server-methods/nodes.js +14 -0
  34. package/dist/gateway/websocket-preauth-security.js +188 -0
  35. package/dist/infra/errors.js +53 -13
  36. package/dist/infra/exec-approvals-security.js +217 -0
  37. package/dist/infra/security/command-analyzer.js +257 -0
  38. package/dist/plugins/loader.js +16 -8
  39. package/dist/security/external-content.js +51 -1
  40. package/dist/sessions/session-costs.js +228 -0
  41. package/dist/shared/param-key.js +16 -0
  42. package/dist/shared/poll-params.js +58 -0
  43. package/dist/shared/polls.js +55 -0
  44. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  45. package/docs/FEATURES.md +523 -0
  46. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  47. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  48. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  49. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  50. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  51. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  52. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  53. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  54. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  55. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  56. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  57. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  58. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  59. package/docs/PHASE-7-SUMMARY.md +144 -0
  60. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  61. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  62. package/docs/README.md +116 -0
  63. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  64. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  65. package/docs/channels/googlechat.md +235 -206
  66. package/docs/channels/irc.md +332 -0
  67. package/docs/channels/nostr.md +255 -168
  68. package/docs/components/command-palette.md +166 -0
  69. package/docs/components/login-gate.md +219 -0
  70. package/docs/getting-started/installation.md +191 -0
  71. package/docs/getting-started/introduction.md +120 -0
  72. package/docs/improvements/USAGE-GUIDE.md +359 -0
  73. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  74. package/docs/reference/deadcode-detection.md +72 -0
  75. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  76. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  77. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  78. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  79. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  80. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  81. package/extensions/googlechat/package.json +11 -28
  82. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  83. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  84. package/extensions/googlechat/src/index.ts +14 -0
  85. package/extensions/irc/node_modules/.bin/tsc +21 -0
  86. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  87. package/extensions/irc/node_modules/.bin/vitest +21 -0
  88. package/extensions/irc/package.json +16 -8
  89. package/extensions/irc/src/index.ts +14 -0
  90. package/extensions/irc/src/irc-channel.test.ts +43 -0
  91. package/extensions/irc/src/irc-channel.ts +191 -0
  92. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  93. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  94. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  95. package/extensions/keyed-async-queue/package.json +20 -0
  96. package/extensions/keyed-async-queue/src/index.ts +14 -0
  97. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  98. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  99. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  100. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  101. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  102. package/extensions/memory-core/package.json +11 -8
  103. package/extensions/memory-core/src/index.ts +14 -0
  104. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  105. package/extensions/memory-core/src/memory-manager.ts +186 -0
  106. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  107. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  108. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  109. package/extensions/nostr/package.json +15 -24
  110. package/extensions/nostr/src/index.ts +14 -0
  111. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  112. package/extensions/nostr/src/nostr-channel.ts +228 -0
  113. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  114. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  115. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  116. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  117. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  118. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  119. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  120. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  121. package/package.json +2 -1
  122. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  123. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  124. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
package/CHANGELOG.md CHANGED
@@ -1,3 +1,84 @@
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
+
55
+ ## v2026.3.22 (2026-03-13)
56
+
57
+ ### ✅ Testes Críticos Implementados
58
+ - **Skills Loader Integration Tests:** 20+ testes para validação de 339+ skills
59
+ - **Health Endpoints Tests:** 15+ testes para /health, /healthz, /ready, /readyz
60
+ - **Coverage:** Validação de estrutura, categorias, performance, edge cases
61
+
62
+ ### 📊 Skills Loader Tests
63
+ - ✅ Load 339+ skills from skills-openclaw
64
+ - ✅ Validate skill structure (name, description, category, filePath)
65
+ - ✅ Category mapping (development, ai-llm, productivity, etc.)
66
+ - ✅ Performance: <5s para carregar todas skills
67
+ - ✅ Parse individual skill: <100ms
68
+ - ✅ Edge cases: empty directory, concurrent parsing, special chars
69
+
70
+ ### 🏥 Health Endpoints Tests
71
+ - ✅ GET /health returns JSON with status, version, uptime
72
+ - ✅ GET /healthz returns "ok" text (<100ms)
73
+ - ✅ GET /ready returns detailed checks
74
+ - ✅ GET /readyz returns "ready" text
75
+ - ✅ Cache-Control headers
76
+ - ✅ Kubernetes/Docker compatible
77
+
78
+ ### 📝 Test Files Created
79
+ - `src/skills/openclaw-skill-loader.integration.test.ts` (280 linhas)
80
+ - `src/gateway/server-http-health.test.ts` (180 linhas)
81
+
1
82
  ## v2026.3.21 (2026-03-13)
2
83
 
3
84
  ### 🚀 New Features
@@ -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
+ }