@poolzin/pool-bot 2026.3.6 → 2026.3.9
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 +16 -0
- package/dist/.buildstamp +1 -1
- package/dist/agents/error-classifier.js +302 -0
- package/dist/agents/pi-tools.js +32 -2
- package/dist/agents/skills/security.js +217 -0
- package/dist/auto-reply/reply/get-reply.js +6 -0
- package/dist/auto-reply/reply/message-preprocess-hooks.js +17 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/banner.js +20 -1
- package/dist/cli/lazy-commands.example.js +113 -0
- package/dist/cli/lazy-commands.js +329 -0
- package/dist/cli/program/command-registry.js +13 -0
- package/dist/cli/program/register.skills.js +4 -0
- package/dist/cli/security-cli.js +211 -2
- package/dist/cli/tagline.js +7 -0
- package/dist/config/config.js +1 -0
- package/dist/config/secrets-integration.js +88 -0
- package/dist/config/types.cli.js +1 -0
- package/dist/config/types.security.js +33 -0
- package/dist/config/zod-schema.js +15 -0
- package/dist/config/zod-schema.providers-core.js +1 -0
- package/dist/config/zod-schema.security.js +113 -0
- package/dist/context-engine/index.js +33 -0
- package/dist/context-engine/legacy.js +181 -0
- package/dist/context-engine/registry.js +86 -0
- package/dist/context-engine/summarizing.js +293 -0
- package/dist/context-engine/types.js +7 -0
- package/dist/discord/monitor/message-handler.preflight.js +11 -2
- package/dist/gateway/http-common.js +6 -1
- package/dist/hooks/fire-and-forget.js +6 -0
- package/dist/hooks/internal-hooks.js +64 -19
- package/dist/hooks/message-hook-mappers.js +179 -0
- package/dist/infra/abort-pattern.js +106 -0
- package/dist/infra/retry.js +94 -0
- package/dist/secrets/index.js +28 -0
- package/dist/secrets/resolver.js +185 -0
- package/dist/secrets/runtime.js +142 -0
- package/dist/secrets/types.js +11 -0
- package/dist/security/capability-guards.js +89 -0
- package/dist/security/capability-manager.js +76 -0
- package/dist/security/capability.js +147 -0
- package/dist/security/dangerous-tools.js +80 -0
- package/dist/security/index.js +7 -0
- package/dist/security/middleware.js +105 -0
- package/dist/security/types.js +12 -0
- package/dist/skills/commands.js +351 -0
- package/dist/skills/index.js +167 -0
- package/dist/skills/loader.js +282 -0
- package/dist/skills/parser.js +461 -0
- package/dist/skills/registry.js +397 -0
- package/dist/skills/security.js +318 -0
- package/dist/skills/types.js +21 -0
- package/dist/slack/monitor/context.js +1 -0
- package/dist/slack/monitor/message-handler/dispatch.js +14 -1
- package/dist/slack/monitor/provider.js +2 -0
- package/dist/test-utils/index.js +219 -0
- package/dist/tui/index.js +595 -0
- package/docs/INTEGRATION_PLAN.md +475 -0
- package/docs/INTEGRATION_SUMMARY.md +215 -0
- package/docs/integrations/HEXSTRIKE_PLAN.md +796 -0
- package/docs/integrations/INTEGRATION_PLAN.md +424 -0
- package/docs/integrations/PAGE_AGENT_PLAN.md +370 -0
- package/docs/integrations/XYOPS_PLAN.md +978 -0
- package/docs/skills/IMPLEMENTATION_SUMMARY.md +145 -0
- package/docs/skills/SKILL.md +524 -0
- package/docs/skills.md +405 -0
- package/package.json +1 -1
- package/skills/example-skill/SKILL.md +195 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abort Pattern Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides memory-leak-free abort signal handling.
|
|
5
|
+
*
|
|
6
|
+
* CRITICAL FIX: Uses `.bind()` instead of closures to prevent memory leaks.
|
|
7
|
+
* Issue #7174: Closure-based abort handlers capture scope and leak memory.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // BAD: Captures closure scope (leaks memory)
|
|
12
|
+
* signal.addEventListener('abort', () => controller.abort());
|
|
13
|
+
*
|
|
14
|
+
* // GOOD: No closure capture
|
|
15
|
+
* signal.addEventListener('abort', relayAbort.bind(controller));
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Relay abort signal without closure capture
|
|
20
|
+
* Prevents memory leak by using bind instead of arrow function
|
|
21
|
+
*/
|
|
22
|
+
export function relayAbort() {
|
|
23
|
+
this.abort();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Create an abort relay that doesn't capture closure scope
|
|
27
|
+
*/
|
|
28
|
+
export function createAbortRelay(controller) {
|
|
29
|
+
return relayAbort.bind(controller);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Link an abort signal to a controller without memory leaks
|
|
33
|
+
*/
|
|
34
|
+
export function linkAbortSignal(source, target) {
|
|
35
|
+
const handler = createAbortRelay(target);
|
|
36
|
+
source.addEventListener('abort', handler, { once: true });
|
|
37
|
+
// Return cleanup function
|
|
38
|
+
return () => {
|
|
39
|
+
source.removeEventListener('abort', handler);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a timeout-based abort controller
|
|
44
|
+
*/
|
|
45
|
+
export function createTimeoutAbortController(timeoutMs) {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
if (timeoutMs > 0) {
|
|
48
|
+
const timeout = setTimeout(() => {
|
|
49
|
+
controller.abort();
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
return {
|
|
52
|
+
controller,
|
|
53
|
+
cleanup: () => clearTimeout(timeout),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
controller,
|
|
58
|
+
cleanup: () => { },
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Race multiple abort signals
|
|
63
|
+
*/
|
|
64
|
+
export function raceAbortSignals(signals, controller) {
|
|
65
|
+
const handlers = [];
|
|
66
|
+
let alreadyAborted = false;
|
|
67
|
+
for (const signal of signals) {
|
|
68
|
+
if (signal.aborted && !alreadyAborted) {
|
|
69
|
+
alreadyAborted = true;
|
|
70
|
+
controller.abort();
|
|
71
|
+
}
|
|
72
|
+
else if (!alreadyAborted) {
|
|
73
|
+
const handler = createAbortRelay(controller);
|
|
74
|
+
signal.addEventListener('abort', handler, { once: true });
|
|
75
|
+
handlers.push(() => signal.removeEventListener('abort', handler));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return () => {
|
|
79
|
+
for (const cleanup of handlers) {
|
|
80
|
+
cleanup();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fetch with timeout and proper abort handling
|
|
86
|
+
*/
|
|
87
|
+
export async function fetchWithTimeout(url, options = {}) {
|
|
88
|
+
const { timeoutMs = 30000, ...fetchOptions } = options;
|
|
89
|
+
const { controller, cleanup } = createTimeoutAbortController(timeoutMs);
|
|
90
|
+
// Link existing signal if provided
|
|
91
|
+
let unlink;
|
|
92
|
+
if (fetchOptions.signal) {
|
|
93
|
+
unlink = linkAbortSignal(fetchOptions.signal, controller);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
...fetchOptions,
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
return response;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
unlink?.();
|
|
104
|
+
cleanup();
|
|
105
|
+
}
|
|
106
|
+
}
|
package/dist/infra/retry.js
CHANGED
|
@@ -85,5 +85,99 @@ export async function retryAsync(fn, attemptsOrOptions = 3, initialDelayMs = 300
|
|
|
85
85
|
await sleep(delay);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
options.onExhausted?.(lastErr, maxAttempts);
|
|
88
89
|
throw lastErr ?? new Error("Retry failed");
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Execute a function with retry logic and return detailed result
|
|
93
|
+
*/
|
|
94
|
+
export async function retryWithResult(fn, options = {}) {
|
|
95
|
+
const resolved = resolveRetryConfig(DEFAULT_RETRY_CONFIG, options);
|
|
96
|
+
const maxAttempts = resolved.attempts;
|
|
97
|
+
const minDelayMs = resolved.minDelayMs;
|
|
98
|
+
const maxDelayMs = Number.isFinite(resolved.maxDelayMs) ? resolved.maxDelayMs : Number.POSITIVE_INFINITY;
|
|
99
|
+
const jitter = resolved.jitter;
|
|
100
|
+
const shouldRetry = options.shouldRetry ?? (() => true);
|
|
101
|
+
let lastErr;
|
|
102
|
+
let totalDelayMs = 0;
|
|
103
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
const result = await fn();
|
|
106
|
+
return { result, attempts: attempt, totalDelayMs };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
lastErr = err;
|
|
110
|
+
if (attempt >= maxAttempts || !shouldRetry(err, attempt)) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
const retryAfterMs = options.retryAfterMs?.(err);
|
|
114
|
+
const hasRetryAfter = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs);
|
|
115
|
+
const baseDelay = hasRetryAfter
|
|
116
|
+
? Math.max(retryAfterMs, minDelayMs)
|
|
117
|
+
: minDelayMs * 2 ** (attempt - 1);
|
|
118
|
+
let delay = Math.min(baseDelay, maxDelayMs);
|
|
119
|
+
delay = applyJitter(delay, jitter);
|
|
120
|
+
delay = Math.min(Math.max(delay, minDelayMs), maxDelayMs);
|
|
121
|
+
totalDelayMs += delay;
|
|
122
|
+
options.onRetry?.({
|
|
123
|
+
attempt,
|
|
124
|
+
maxAttempts,
|
|
125
|
+
delayMs: delay,
|
|
126
|
+
err,
|
|
127
|
+
label: options.label,
|
|
128
|
+
});
|
|
129
|
+
await sleep(delay);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
options.onExhausted?.(lastErr, maxAttempts);
|
|
133
|
+
throw lastErr ?? new Error("Retry failed");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if error is retryable (network errors, rate limits, etc.)
|
|
137
|
+
*/
|
|
138
|
+
export function isRetryableError(error) {
|
|
139
|
+
if (error instanceof Error) {
|
|
140
|
+
const message = error.message.toLowerCase();
|
|
141
|
+
// Network errors
|
|
142
|
+
if (message.includes("econnreset"))
|
|
143
|
+
return true;
|
|
144
|
+
if (message.includes("etimedout"))
|
|
145
|
+
return true;
|
|
146
|
+
if (message.includes("econnrefused"))
|
|
147
|
+
return true;
|
|
148
|
+
if (message.includes("enotfound"))
|
|
149
|
+
return true;
|
|
150
|
+
if (message.includes("network"))
|
|
151
|
+
return true;
|
|
152
|
+
if (message.includes("timeout"))
|
|
153
|
+
return true;
|
|
154
|
+
// HTTP status codes that are retryable
|
|
155
|
+
const statusMatch = error.message.match(/status\s+(\d+)/i);
|
|
156
|
+
if (statusMatch) {
|
|
157
|
+
const status = parseInt(statusMatch[1], 10);
|
|
158
|
+
// 429 = Too Many Requests, 502/503/504 = Gateway errors
|
|
159
|
+
if (status === 429 || status === 502 || status === 503 || status === 504) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Default retry configuration for API calls
|
|
168
|
+
*/
|
|
169
|
+
export const API_RETRY_CONFIG = {
|
|
170
|
+
attempts: 3,
|
|
171
|
+
minDelayMs: 1000,
|
|
172
|
+
maxDelayMs: 10000,
|
|
173
|
+
jitter: 0.1,
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* Default retry configuration for network calls
|
|
177
|
+
*/
|
|
178
|
+
export const NETWORK_RETRY_CONFIG = {
|
|
179
|
+
attempts: 5,
|
|
180
|
+
minDelayMs: 500,
|
|
181
|
+
maxDelayMs: 30000,
|
|
182
|
+
jitter: 0.1,
|
|
183
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Management System
|
|
3
|
+
*
|
|
4
|
+
* Secure secret resolution with support for:
|
|
5
|
+
* - Environment variables (env:default:KEY)
|
|
6
|
+
* - JSON files (file:/path/to/secrets.json:key)
|
|
7
|
+
* - Direct values (backward compatible)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { SecretsRuntime, resolveValue } from './secrets/index.js';
|
|
12
|
+
*
|
|
13
|
+
* // Create runtime
|
|
14
|
+
* const runtime = new SecretsRuntime({
|
|
15
|
+
* allowDirectValues: true,
|
|
16
|
+
* warnOnDirectValues: true
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Resolve API key
|
|
20
|
+
* runtime.resolve('openaiApiKey', 'env:default:OPENAI_API_KEY');
|
|
21
|
+
*
|
|
22
|
+
* // Get resolved value
|
|
23
|
+
* const apiKey = runtime.get('openaiApiKey');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export * from "./types.js";
|
|
27
|
+
export * from "./resolver.js";
|
|
28
|
+
export * from "./runtime.js";
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Parse a secret reference string
|
|
5
|
+
* Formats:
|
|
6
|
+
* - env:default:KEY - Environment variable
|
|
7
|
+
* - env:production:KEY - Environment variable from specific source
|
|
8
|
+
* - file:/path/to/secrets.json:KEY - JSON file
|
|
9
|
+
* - file:/path/to/secrets.json - Entire JSON file as secret
|
|
10
|
+
* - raw value - Direct value (backward compatibility)
|
|
11
|
+
*/
|
|
12
|
+
export function parseSecretRef(value) {
|
|
13
|
+
// Check if it's a reference
|
|
14
|
+
const envMatch = value.match(/^env:([^:]+):(.+)$/);
|
|
15
|
+
if (envMatch) {
|
|
16
|
+
return {
|
|
17
|
+
isRef: true,
|
|
18
|
+
ref: {
|
|
19
|
+
provider: "env",
|
|
20
|
+
source: envMatch[1],
|
|
21
|
+
key: envMatch[2],
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const fileMatch = value.match(/^file:([^:]+):(.+)$/);
|
|
26
|
+
if (fileMatch) {
|
|
27
|
+
return {
|
|
28
|
+
isRef: true,
|
|
29
|
+
ref: {
|
|
30
|
+
provider: "file",
|
|
31
|
+
source: fileMatch[1],
|
|
32
|
+
key: fileMatch[2],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const fileOnlyMatch = value.match(/^file:(.+)$/);
|
|
37
|
+
if (fileOnlyMatch) {
|
|
38
|
+
return {
|
|
39
|
+
isRef: true,
|
|
40
|
+
ref: {
|
|
41
|
+
provider: "file",
|
|
42
|
+
source: fileOnlyMatch[1],
|
|
43
|
+
key: "", // Entire file
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Not a reference - treat as direct value
|
|
48
|
+
return {
|
|
49
|
+
isRef: false,
|
|
50
|
+
rawValue: value,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a secret reference to its value
|
|
55
|
+
*/
|
|
56
|
+
export function resolveSecret(ref, options = {}) {
|
|
57
|
+
const warnings = [];
|
|
58
|
+
switch (ref.provider) {
|
|
59
|
+
case "env":
|
|
60
|
+
return resolveEnvSecret(ref, options, warnings);
|
|
61
|
+
case "file":
|
|
62
|
+
return resolveFileSecret(ref, options, warnings);
|
|
63
|
+
case "exec":
|
|
64
|
+
return resolveExecSecret(ref, options, warnings);
|
|
65
|
+
default:
|
|
66
|
+
return {
|
|
67
|
+
value: undefined,
|
|
68
|
+
resolved: false,
|
|
69
|
+
warnings: [`Unknown provider: ${String(ref.provider)}`],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function resolveEnvSecret(ref, options, warnings) {
|
|
74
|
+
const value = process.env[ref.key];
|
|
75
|
+
if (value === undefined) {
|
|
76
|
+
if (!options.allowMissing) {
|
|
77
|
+
warnings.push(`Environment variable ${ref.key} is not set`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
value: undefined,
|
|
81
|
+
resolved: false,
|
|
82
|
+
source: `env:${ref.source}:${ref.key}`,
|
|
83
|
+
warnings,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
value,
|
|
88
|
+
resolved: true,
|
|
89
|
+
source: `env:${ref.source}:${ref.key}`,
|
|
90
|
+
warnings,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function resolveFileSecret(ref, options, warnings) {
|
|
94
|
+
try {
|
|
95
|
+
const filePath = resolvePath(ref.source);
|
|
96
|
+
// Path traversal protection
|
|
97
|
+
if (options.basePath && !filePath.startsWith(resolvePath(options.basePath))) {
|
|
98
|
+
warnings.push(`Secret file path outside allowed directory: ${ref.source}`);
|
|
99
|
+
return {
|
|
100
|
+
value: undefined,
|
|
101
|
+
resolved: false,
|
|
102
|
+
source: `file:${ref.source}`,
|
|
103
|
+
warnings,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const content = readFileSync(filePath, "utf-8");
|
|
107
|
+
// If key is empty, return entire file content
|
|
108
|
+
if (!ref.key) {
|
|
109
|
+
return {
|
|
110
|
+
value: content.trim(),
|
|
111
|
+
resolved: true,
|
|
112
|
+
source: `file:${ref.source}`,
|
|
113
|
+
warnings,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Try to parse as JSON
|
|
117
|
+
try {
|
|
118
|
+
const json = JSON.parse(content);
|
|
119
|
+
if (typeof json === "object" && json !== null) {
|
|
120
|
+
if (ref.key in json) {
|
|
121
|
+
const value = String(json[ref.key]);
|
|
122
|
+
return {
|
|
123
|
+
value,
|
|
124
|
+
resolved: true,
|
|
125
|
+
source: `file:${ref.source}:${ref.key}`,
|
|
126
|
+
warnings,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
warnings.push(`Key "${ref.key}" not found in JSON file ${ref.source}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Not valid JSON, treat as plain text
|
|
134
|
+
warnings.push(`File ${ref.source} is not valid JSON`);
|
|
135
|
+
}
|
|
136
|
+
if (!options.allowMissing) {
|
|
137
|
+
warnings.push(`Could not resolve secret from file: ${ref.source}:${ref.key}`);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
value: undefined,
|
|
141
|
+
resolved: false,
|
|
142
|
+
source: `file:${ref.source}:${ref.key}`,
|
|
143
|
+
warnings,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
warnings.push(`Failed to read file ${ref.source}: ${message}`);
|
|
149
|
+
return {
|
|
150
|
+
value: undefined,
|
|
151
|
+
resolved: false,
|
|
152
|
+
source: `file:${ref.source}:${ref.key}`,
|
|
153
|
+
warnings,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function resolveExecSecret(ref, _options, warnings) {
|
|
158
|
+
// Exec provider not yet implemented
|
|
159
|
+
warnings.push("Exec provider is not yet implemented");
|
|
160
|
+
return {
|
|
161
|
+
value: undefined,
|
|
162
|
+
resolved: false,
|
|
163
|
+
source: `exec:${ref.source}:${ref.key}`,
|
|
164
|
+
warnings,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve a value that may be a secret reference or direct value
|
|
169
|
+
*/
|
|
170
|
+
export function resolveValue(value, options = {}) {
|
|
171
|
+
const parsed = parseSecretRef(value);
|
|
172
|
+
if (parsed.isRef && parsed.ref) {
|
|
173
|
+
return resolveSecret(parsed.ref, options);
|
|
174
|
+
}
|
|
175
|
+
// Direct value
|
|
176
|
+
const warnings = [];
|
|
177
|
+
if (options.warnOnDirectValues && parsed.rawValue !== undefined) {
|
|
178
|
+
warnings.push("Using direct secret value. Consider using env:default:KEY or file:/path:key format for better security");
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
value: parsed.rawValue,
|
|
182
|
+
resolved: parsed.rawValue !== undefined,
|
|
183
|
+
warnings,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { resolveValue } from "./resolver.js";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime snapshot for resolved secrets
|
|
4
|
+
*
|
|
5
|
+
* This class manages a snapshot of resolved secrets that is created
|
|
6
|
+
* at startup and remains immutable during runtime for security.
|
|
7
|
+
*/
|
|
8
|
+
export class SecretsRuntime {
|
|
9
|
+
snapshot;
|
|
10
|
+
config;
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = {
|
|
13
|
+
allowDirectValues: true,
|
|
14
|
+
warnOnDirectValues: true,
|
|
15
|
+
failOnUnresolved: false,
|
|
16
|
+
...config,
|
|
17
|
+
};
|
|
18
|
+
this.snapshot = {
|
|
19
|
+
secrets: new Map(),
|
|
20
|
+
unresolved: [],
|
|
21
|
+
warnings: [],
|
|
22
|
+
timestamp: new Date(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a single secret and add to snapshot
|
|
27
|
+
*/
|
|
28
|
+
resolve(key, value) {
|
|
29
|
+
const result = resolveValue(value, {
|
|
30
|
+
allowDirectValues: this.config.allowDirectValues,
|
|
31
|
+
warnOnDirectValues: this.config.warnOnDirectValues,
|
|
32
|
+
allowMissing: !this.config.failOnUnresolved,
|
|
33
|
+
});
|
|
34
|
+
if (result.resolved && result.value !== undefined) {
|
|
35
|
+
this.snapshot.secrets.set(key, {
|
|
36
|
+
value: result.value,
|
|
37
|
+
source: result.source || "direct",
|
|
38
|
+
warnings: result.warnings,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
this.snapshot.unresolved.push(key);
|
|
43
|
+
}
|
|
44
|
+
if (result.warnings.length > 0) {
|
|
45
|
+
this.snapshot.warnings.push(...result.warnings);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve multiple secrets from an object
|
|
51
|
+
*/
|
|
52
|
+
resolveObject(obj) {
|
|
53
|
+
const resolved = {};
|
|
54
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
55
|
+
const result = this.resolve(key, value);
|
|
56
|
+
if (result.value !== undefined) {
|
|
57
|
+
resolved[key] = result.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get a resolved secret value
|
|
64
|
+
*/
|
|
65
|
+
get(key) {
|
|
66
|
+
return this.snapshot.secrets.get(key)?.value;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a secret is resolved
|
|
70
|
+
*/
|
|
71
|
+
has(key) {
|
|
72
|
+
return this.snapshot.secrets.has(key);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get full snapshot info for a secret
|
|
76
|
+
*/
|
|
77
|
+
getInfo(key) {
|
|
78
|
+
return this.snapshot.secrets.get(key);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get all unresolved keys
|
|
82
|
+
*/
|
|
83
|
+
getUnresolved() {
|
|
84
|
+
return [...this.snapshot.unresolved];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get all warnings
|
|
88
|
+
*/
|
|
89
|
+
getWarnings() {
|
|
90
|
+
return [...this.snapshot.warnings];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get snapshot timestamp
|
|
94
|
+
*/
|
|
95
|
+
getTimestamp() {
|
|
96
|
+
return this.snapshot.timestamp;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Create an immutable snapshot copy
|
|
100
|
+
*/
|
|
101
|
+
createSnapshot() {
|
|
102
|
+
return {
|
|
103
|
+
secrets: new Map(this.snapshot.secrets),
|
|
104
|
+
unresolved: [...this.snapshot.unresolved],
|
|
105
|
+
warnings: [...this.snapshot.warnings],
|
|
106
|
+
timestamp: new Date(this.snapshot.timestamp),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if there are any unresolved required secrets
|
|
111
|
+
*/
|
|
112
|
+
hasUnresolved() {
|
|
113
|
+
return this.snapshot.unresolved.length > 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get summary of the runtime state
|
|
117
|
+
*/
|
|
118
|
+
getSummary() {
|
|
119
|
+
return {
|
|
120
|
+
resolved: this.snapshot.secrets.size,
|
|
121
|
+
unresolved: this.snapshot.unresolved.length,
|
|
122
|
+
warnings: this.snapshot.warnings.length,
|
|
123
|
+
timestamp: this.snapshot.timestamp,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Global runtime instance (lazy initialized)
|
|
129
|
+
*/
|
|
130
|
+
let globalRuntime = null;
|
|
131
|
+
export function getGlobalRuntime() {
|
|
132
|
+
if (!globalRuntime) {
|
|
133
|
+
globalRuntime = new SecretsRuntime();
|
|
134
|
+
}
|
|
135
|
+
return globalRuntime;
|
|
136
|
+
}
|
|
137
|
+
export function setGlobalRuntime(runtime) {
|
|
138
|
+
globalRuntime = runtime;
|
|
139
|
+
}
|
|
140
|
+
export function resetGlobalRuntime() {
|
|
141
|
+
globalRuntime = null;
|
|
142
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Management System for PoolBot
|
|
3
|
+
*
|
|
4
|
+
* Provides secure secret resolution with support for:
|
|
5
|
+
* - Environment variables (env:default:KEY)
|
|
6
|
+
* - JSON files (file:/path/to/secrets.json)
|
|
7
|
+
* - Direct values (for backward compatibility)
|
|
8
|
+
*
|
|
9
|
+
* Based on OpenClaw patterns but adapted for PoolBot architecture.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getCapabilityManager } from "./capability-manager.js";
|
|
2
|
+
/** Error thrown when a capability check fails. */
|
|
3
|
+
export class CapabilityError extends Error {
|
|
4
|
+
agentId;
|
|
5
|
+
required;
|
|
6
|
+
constructor(message, agentId, required) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.agentId = agentId;
|
|
9
|
+
this.required = required;
|
|
10
|
+
this.name = "CapabilityError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Guard function that throws if capability is denied. */
|
|
14
|
+
export function requireCapability(agentId, required) {
|
|
15
|
+
const manager = getCapabilityManager();
|
|
16
|
+
const check = manager.check(agentId, required);
|
|
17
|
+
if (!check.granted) {
|
|
18
|
+
throw new CapabilityError(check.reason, agentId, required);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Async guard that returns a promise rejection if denied. */
|
|
22
|
+
export async function withCapability(agentId, required, fn) {
|
|
23
|
+
requireCapability(agentId, required);
|
|
24
|
+
return await fn();
|
|
25
|
+
}
|
|
26
|
+
/** Helper to check file read capability. */
|
|
27
|
+
export function checkFileRead(agentId, path) {
|
|
28
|
+
return getCapabilityManager().check(agentId, {
|
|
29
|
+
type: "file:read",
|
|
30
|
+
pattern: path,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Helper to check file write capability. */
|
|
34
|
+
export function checkFileWrite(agentId, path) {
|
|
35
|
+
return getCapabilityManager().check(agentId, {
|
|
36
|
+
type: "file:write",
|
|
37
|
+
pattern: path,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Helper to check tool invocation capability. */
|
|
41
|
+
export function checkToolInvoke(agentId, toolId) {
|
|
42
|
+
return getCapabilityManager().check(agentId, {
|
|
43
|
+
type: "tool:invoke",
|
|
44
|
+
toolId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Helper to check network connection capability. */
|
|
48
|
+
export function checkNetConnect(agentId, host) {
|
|
49
|
+
return getCapabilityManager().check(agentId, {
|
|
50
|
+
type: "net:connect",
|
|
51
|
+
pattern: host,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Helper to check shell execution capability. */
|
|
55
|
+
export function checkShellExec(agentId, command) {
|
|
56
|
+
return getCapabilityManager().check(agentId, {
|
|
57
|
+
type: "shell:exec",
|
|
58
|
+
pattern: command,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** Helper to check LLM query capability. */
|
|
62
|
+
export function checkLlmQuery(agentId, model) {
|
|
63
|
+
return getCapabilityManager().check(agentId, {
|
|
64
|
+
type: "llm:query",
|
|
65
|
+
pattern: model,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** Helper to check agent spawn capability. */
|
|
69
|
+
export function checkAgentSpawn(agentId) {
|
|
70
|
+
return getCapabilityManager().check(agentId, { type: "agent:spawn" });
|
|
71
|
+
}
|
|
72
|
+
/** Helper to check memory read capability. */
|
|
73
|
+
export function checkMemoryRead(agentId, scope) {
|
|
74
|
+
return getCapabilityManager().check(agentId, {
|
|
75
|
+
type: "memory:read",
|
|
76
|
+
scope,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Helper to check memory write capability. */
|
|
80
|
+
export function checkMemoryWrite(agentId, scope) {
|
|
81
|
+
return getCapabilityManager().check(agentId, {
|
|
82
|
+
type: "memory:write",
|
|
83
|
+
scope,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Helper to check gateway admin capability. */
|
|
87
|
+
export function checkGatewayAdmin(agentId) {
|
|
88
|
+
return getCapabilityManager().check(agentId, { type: "gateway:admin" });
|
|
89
|
+
}
|