atabey-shared 0.0.23 → 0.0.25

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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Polly-style resilience primitives — no external deps.
3
+ *
4
+ * - withRetry: exponential backoff + jitter for transient failures
5
+ * - CircuitBreaker: closed → open → half-open state machine
6
+ * - withResilience: compose retry + circuit breaker
7
+ *
8
+ * Typical use: LLM provider rate limits, timeouts, 5xx, network blips.
9
+ */
10
+ export class CircuitOpenError extends Error {
11
+ circuit;
12
+ retryAfterMs;
13
+ constructor(circuit, retryAfterMs) {
14
+ super(`[CircuitBreaker:${circuit}] Circuit is OPEN — rejecting call. Retry after ~${retryAfterMs}ms`);
15
+ this.name = "CircuitOpenError";
16
+ this.circuit = circuit;
17
+ this.retryAfterMs = retryAfterMs;
18
+ }
19
+ }
20
+ export class CircuitBreaker {
21
+ state = "closed";
22
+ failures = 0;
23
+ successes = 0;
24
+ openedAt = 0;
25
+ failureThreshold;
26
+ successThreshold;
27
+ openMs;
28
+ name;
29
+ constructor(options = {}) {
30
+ this.failureThreshold = options.failureThreshold ?? 5;
31
+ this.successThreshold = options.successThreshold ?? 2;
32
+ this.openMs = options.openMs ?? 30_000;
33
+ this.name = options.name ?? "default";
34
+ }
35
+ getState() {
36
+ this.maybeTransitionToHalfOpen();
37
+ return this.state;
38
+ }
39
+ getStats() {
40
+ return {
41
+ state: this.getState(),
42
+ failures: this.failures,
43
+ successes: this.successes,
44
+ name: this.name,
45
+ };
46
+ }
47
+ /** Manual reset (e.g. after operator intervention). */
48
+ reset() {
49
+ this.state = "closed";
50
+ this.failures = 0;
51
+ this.successes = 0;
52
+ this.openedAt = 0;
53
+ }
54
+ async exec(fn) {
55
+ this.maybeTransitionToHalfOpen();
56
+ if (this.state === "open") {
57
+ const retryAfterMs = Math.max(0, this.openMs - (Date.now() - this.openedAt));
58
+ throw new CircuitOpenError(this.name, retryAfterMs);
59
+ }
60
+ try {
61
+ const result = await fn();
62
+ this.onSuccess();
63
+ return result;
64
+ }
65
+ catch (err) {
66
+ this.onFailure();
67
+ throw err;
68
+ }
69
+ }
70
+ maybeTransitionToHalfOpen() {
71
+ if (this.state === "open" && Date.now() - this.openedAt >= this.openMs) {
72
+ this.state = "half-open";
73
+ this.successes = 0;
74
+ }
75
+ }
76
+ onSuccess() {
77
+ if (this.state === "half-open") {
78
+ this.successes++;
79
+ if (this.successes >= this.successThreshold) {
80
+ this.state = "closed";
81
+ this.failures = 0;
82
+ this.successes = 0;
83
+ }
84
+ return;
85
+ }
86
+ // closed: decay failure count on success
87
+ this.failures = 0;
88
+ }
89
+ onFailure() {
90
+ this.failures++;
91
+ this.successes = 0;
92
+ if (this.state === "half-open") {
93
+ this.trip();
94
+ return;
95
+ }
96
+ if (this.failures >= this.failureThreshold) {
97
+ this.trip();
98
+ }
99
+ }
100
+ trip() {
101
+ this.state = "open";
102
+ this.openedAt = Date.now();
103
+ }
104
+ }
105
+ export function computeBackoffDelay(attempt, options = {}) {
106
+ const base = options.baseDelayMs ?? 100;
107
+ const max = options.maxDelayMs ?? 10_000;
108
+ const factor = options.factor ?? 2;
109
+ const jitter = options.jitter !== false;
110
+ // attempt is 1-based for first retry after failure → attempt 1 => base * factor^0
111
+ const exp = Math.max(0, attempt - 1);
112
+ let delay = Math.min(max, base * Math.pow(factor, exp));
113
+ if (jitter) {
114
+ delay = Math.floor(Math.random() * delay);
115
+ }
116
+ return delay;
117
+ }
118
+ export async function withRetry(fn, options = {}) {
119
+ const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
120
+ const retryIf = options.retryIf ?? (() => true);
121
+ let lastError;
122
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
123
+ if (options.signal?.aborted) {
124
+ throw options.signal.reason ?? new Error("Aborted");
125
+ }
126
+ try {
127
+ return await fn(attempt);
128
+ }
129
+ catch (err) {
130
+ lastError = err;
131
+ const isLast = attempt >= maxAttempts;
132
+ if (isLast || !retryIf(err, attempt)) {
133
+ throw err;
134
+ }
135
+ const delay = computeBackoffDelay(attempt, options);
136
+ options.onRetry?.(err, attempt, delay);
137
+ await sleep(delay, options.signal);
138
+ }
139
+ }
140
+ throw lastError;
141
+ }
142
+ /**
143
+ * Execute `fn` through optional circuit breaker + retry with exponential backoff.
144
+ */
145
+ export async function withResilience(fn, options = {}) {
146
+ const breaker = options.circuitBreaker;
147
+ const skipOpen = options.skipRetryOnOpenCircuit !== false;
148
+ const retryOpts = {
149
+ ...options.retry,
150
+ retryIf: (err, attempt) => {
151
+ if (skipOpen && err instanceof CircuitOpenError)
152
+ return false;
153
+ return options.retry?.retryIf ? options.retry.retryIf(err, attempt) : true;
154
+ },
155
+ };
156
+ return withRetry(async (attempt) => {
157
+ if (breaker) {
158
+ return breaker.exec(() => fn(attempt));
159
+ }
160
+ return fn(attempt);
161
+ }, retryOpts);
162
+ }
163
+ /** Heuristic: retry rate limits, timeouts, and common transient network errors. */
164
+ export function isTransientError(error) {
165
+ if (error instanceof CircuitOpenError)
166
+ return false;
167
+ if (!error)
168
+ return false;
169
+ const msg = error instanceof Error ? error.message : String(error);
170
+ const lower = msg.toLowerCase();
171
+ const code = error.code;
172
+ const status = error.status;
173
+ if (status === 429 || status === 502 || status === 503 || status === 504)
174
+ return true;
175
+ if (code === "ETIMEDOUT" || code === "ECONNRESET" || code === "EAI_AGAIN" || code === "ENOTFOUND") {
176
+ return true;
177
+ }
178
+ if (lower.includes("rate limit") ||
179
+ lower.includes("too many requests") ||
180
+ lower.includes("timeout") ||
181
+ lower.includes("temporar") ||
182
+ lower.includes("econnreset") ||
183
+ lower.includes("socket hang up")) {
184
+ return true;
185
+ }
186
+ return false;
187
+ }
188
+ /** Shared breakers for common services (lazy). */
189
+ const breakers = new Map();
190
+ export function getCircuitBreaker(name, options) {
191
+ let b = breakers.get(name);
192
+ if (!b) {
193
+ b = new CircuitBreaker({ name, ...options });
194
+ breakers.set(name, b);
195
+ }
196
+ return b;
197
+ }
198
+ export function resetAllCircuitBreakers() {
199
+ for (const b of breakers.values())
200
+ b.reset();
201
+ breakers.clear();
202
+ }
203
+ function sleep(ms, signal) {
204
+ return new Promise((resolve, reject) => {
205
+ if (signal?.aborted) {
206
+ reject(signal.reason ?? new Error("Aborted"));
207
+ return;
208
+ }
209
+ const timer = setTimeout(resolve, ms);
210
+ const onAbort = () => {
211
+ clearTimeout(timer);
212
+ reject(signal?.reason ?? new Error("Aborted"));
213
+ };
214
+ signal?.addEventListener("abort", onAbort, { once: true });
215
+ });
216
+ }
217
+ //# sourceMappingURL=resilience.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resilience.js","sourceRoot":"","sources":["../src/resilience.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Sandboxed filesystem mutations — Phase 1.2.
3
+ *
4
+ * - Always holds AtomicFileLock (try/finally).
5
+ * - When runtime=container: write via container stdin → /workspace/<path>
6
+ * - When runtime=uid|none: write on host under projectRoot (safePath caller responsibility)
7
+ * - ATABEY_SANDBOX_REQUIRED=true rejects writes if isolation is none
8
+ */
9
+ import { type SandboxRuntimeConfig } from "./sandbox-runtime.js";
10
+ export interface SandboxFsWriteResult {
11
+ absolutePath: string;
12
+ relativePath: string;
13
+ runtime: SandboxRuntimeConfig["effectiveMode"];
14
+ isolationLabel: string;
15
+ bytes: number;
16
+ }
17
+ /**
18
+ * Normalize user path to a project-relative POSIX path.
19
+ * Absolute paths are accepted only if they resolve inside projectRoot.
20
+ */
21
+ export declare function assertSafeRelativePath(userPath: string, projectRoot?: string): string;
22
+ /**
23
+ * Write file content under project root using the active sandbox runtime.
24
+ */
25
+ export declare function writeFileInSandbox(projectRoot: string, userPath: string, content: string, options?: {
26
+ ownerId?: string;
27
+ timeoutMs?: number;
28
+ }): Promise<SandboxFsWriteResult>;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Sandboxed filesystem mutations — Phase 1.2.
3
+ *
4
+ * - Always holds AtomicFileLock (try/finally).
5
+ * - When runtime=container: write via container stdin → /workspace/<path>
6
+ * - When runtime=uid|none: write on host under projectRoot (safePath caller responsibility)
7
+ * - ATABEY_SANDBOX_REQUIRED=true rejects writes if isolation is none
8
+ */
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { AtomicFileLock } from "./file-lock.js";
12
+ import { resolveSandboxRuntimeConfig, runInSandbox, SandboxRequiredError, } from "./sandbox-runtime.js";
13
+ /**
14
+ * Normalize user path to a project-relative POSIX path.
15
+ * Absolute paths are accepted only if they resolve inside projectRoot.
16
+ */
17
+ export function assertSafeRelativePath(userPath, projectRoot) {
18
+ if (!userPath || !userPath.trim()) {
19
+ throw new Error("[SANDBOX_FS] Empty path is not allowed");
20
+ }
21
+ const trimmed = userPath.trim();
22
+ if (trimmed.includes("\0")) {
23
+ throw new Error("[SANDBOX_FS] Illegal null byte in path");
24
+ }
25
+ let rel = trimmed.replace(/\\/g, "/");
26
+ // Absolute path → must live under projectRoot
27
+ if (path.isAbsolute(trimmed) || rel.startsWith("/")) {
28
+ if (!projectRoot) {
29
+ throw new Error(`[SANDBOX_FS] Absolute paths require projectRoot: "${userPath}"`);
30
+ }
31
+ const root = path.resolve(projectRoot);
32
+ const abs = path.resolve(trimmed);
33
+ if (!abs.startsWith(root + path.sep) && abs !== root) {
34
+ throw new Error(`[SANDBOX_FS] Absolute path escapes project root: "${userPath}"`);
35
+ }
36
+ rel = path.relative(root, abs).replace(/\\/g, "/");
37
+ if (!rel || rel === ".") {
38
+ throw new Error("[SANDBOX_FS] Path resolves to project root; refuse write");
39
+ }
40
+ if (rel.startsWith("..")) {
41
+ throw new Error(`[SANDBOX_FS] Path traversal denied: "${userPath}"`);
42
+ }
43
+ return rel;
44
+ }
45
+ // Relative: reject .. segments
46
+ const parts = [];
47
+ for (const seg of rel.split("/")) {
48
+ if (!seg || seg === ".")
49
+ continue;
50
+ if (seg === "..") {
51
+ throw new Error(`[SANDBOX_FS] Path traversal denied: "${userPath}"`);
52
+ }
53
+ parts.push(seg);
54
+ }
55
+ if (parts.length === 0) {
56
+ throw new Error("[SANDBOX_FS] Path resolves to project root; refuse write");
57
+ }
58
+ return parts.join("/");
59
+ }
60
+ function shellSingleQuote(s) {
61
+ // Safe for sh single-quoted strings
62
+ return `'${s.replace(/'/g, `'\\''`)}'`;
63
+ }
64
+ function createLock(projectRoot) {
65
+ return new AtomicFileLock({
66
+ projectRoot,
67
+ locksDir: path.join(projectRoot, ".atabey", "locks"),
68
+ ttlMs: 5 * 60 * 1000,
69
+ timeoutMs: 15_000,
70
+ });
71
+ }
72
+ function hostWriteAtomic(absolutePath, content) {
73
+ const dir = path.dirname(absolutePath);
74
+ if (!fs.existsSync(dir)) {
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ }
77
+ // Write exact bytes (callers decide trailing newline — patch/replace need fidelity)
78
+ const tempPath = `${absolutePath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
79
+ try {
80
+ fs.writeFileSync(tempPath, content, "utf8");
81
+ fs.renameSync(tempPath, absolutePath);
82
+ }
83
+ catch (err) {
84
+ try {
85
+ if (fs.existsSync(tempPath))
86
+ fs.unlinkSync(tempPath);
87
+ }
88
+ catch { /* ignore */ }
89
+ throw err;
90
+ }
91
+ }
92
+ /**
93
+ * Write file content under project root using the active sandbox runtime.
94
+ */
95
+ export async function writeFileInSandbox(projectRoot, userPath, content, options) {
96
+ const root = path.resolve(projectRoot);
97
+ const relativePath = assertSafeRelativePath(userPath, root);
98
+ const absolutePath = path.resolve(root, relativePath);
99
+ // Belt-and-suspenders: stay inside root
100
+ if (!absolutePath.startsWith(root + path.sep) && absolutePath !== root) {
101
+ throw new Error(`[SANDBOX_FS] Resolved path escapes project root: "${userPath}"`);
102
+ }
103
+ const cfg = resolveSandboxRuntimeConfig(root, options?.timeoutMs);
104
+ if (cfg.required && cfg.effectiveMode === "none") {
105
+ throw new SandboxRequiredError("[SANDBOX_REQUIRED] File write blocked: no sandbox isolation available. " +
106
+ "Set ATABEY_SANDBOX_RUNTIME=container (Podman/Docker) or uid, or disable ATABEY_SANDBOX_REQUIRED.");
107
+ }
108
+ const owner = options?.ownerId || process.env.ATABEY_ACTIVE_AGENT || "mcp-writer";
109
+ const lock = createLock(root);
110
+ const posixRel = relativePath.split(path.sep).join("/");
111
+ return lock.withLock(relativePath, owner, async () => {
112
+ if (cfg.effectiveMode === "container" && cfg.engine !== "none") {
113
+ const dir = path.posix.dirname(posixRel);
114
+ const script = dir === "."
115
+ ? `cat > ${shellSingleQuote(posixRel)}`
116
+ : `mkdir -p ${shellSingleQuote(dir)} && cat > ${shellSingleQuote(posixRel)}`;
117
+ const result = await runInSandbox({
118
+ command: "sh",
119
+ args: ["-c", script],
120
+ projectRoot: root,
121
+ timeoutMs: options?.timeoutMs ?? cfg.timeoutMs,
122
+ stdin: content,
123
+ });
124
+ if (result.code !== 0) {
125
+ throw new Error(`[SANDBOX_FS] Container write failed (code ${result.code}): ${result.stderr || result.stdout}`.trim());
126
+ }
127
+ return {
128
+ absolutePath,
129
+ relativePath,
130
+ runtime: "container",
131
+ isolationLabel: result.isolationLabel,
132
+ bytes: Buffer.byteLength(content, "utf8"),
133
+ };
134
+ }
135
+ // uid | none — host write confined to project root
136
+ hostWriteAtomic(absolutePath, content);
137
+ return {
138
+ absolutePath,
139
+ relativePath,
140
+ runtime: cfg.effectiveMode,
141
+ isolationLabel: cfg.effectiveMode === "uid"
142
+ ? "uid host write (project-bound)"
143
+ : "host write (project-bound)",
144
+ bytes: Buffer.byteLength(content, "utf8"),
145
+ };
146
+ });
147
+ }
148
+ //# sourceMappingURL=sandbox-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-fs.js","sourceRoot":"","sources":["../src/sandbox-fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Sandbox Runtime — Phase 1.1 execution isolation for agent shell commands.
3
+ *
4
+ * Modes (ATABEY_SANDBOX_RUNTIME):
5
+ * none — host spawn (legacy; allow-list only)
6
+ * uid — host spawn with uid/gid drop (see sandbox.ts)
7
+ * container — Docker/Podman run with network=none, resource limits, project mount
8
+ * auto — container if engine available, else uid if configured, else none
9
+ *
10
+ * Enterprise hardening:
11
+ * ATABEY_SANDBOX_REQUIRED=true → deny execution when isolation is effectively "none"
12
+ *
13
+ * Container defaults:
14
+ * ATABEY_SANDBOX_ENGINE=podman|docker|auto (prefer podman, then docker)
15
+ * ATABEY_SANDBOX_IMAGE=node:20-bookworm-slim
16
+ * ATABEY_SANDBOX_MEMORY=512m
17
+ * ATABEY_SANDBOX_CPUS=1
18
+ * ATABEY_SANDBOX_PIDS=256
19
+ * ATABEY_SANDBOX_NETWORK=none
20
+ */
21
+ import { resolveSandboxIdentity } from "./sandbox.js";
22
+ export type SandboxRuntimeMode = "none" | "uid" | "container" | "auto";
23
+ export type SandboxEngine = "podman" | "docker" | "none";
24
+ export interface SandboxRuntimeConfig {
25
+ mode: SandboxRuntimeMode;
26
+ /** Resolved effective mode after auto detection. */
27
+ effectiveMode: "none" | "uid" | "container";
28
+ engine: SandboxEngine;
29
+ image: string;
30
+ memory: string;
31
+ cpus: string;
32
+ pidsLimit: number;
33
+ network: string;
34
+ required: boolean;
35
+ projectRoot: string;
36
+ timeoutMs: number;
37
+ }
38
+ export interface RunInSandboxRequest {
39
+ command: string;
40
+ args: string[];
41
+ projectRoot: string;
42
+ timeoutMs?: number;
43
+ env?: NodeJS.ProcessEnv;
44
+ /** Optional stdin payload (e.g. file content for container-mediated writes). */
45
+ stdin?: string | Buffer;
46
+ }
47
+ export interface RunInSandboxResult {
48
+ code: number | null;
49
+ stdout: string;
50
+ stderr: string;
51
+ runtime: SandboxRuntimeConfig["effectiveMode"];
52
+ engine: SandboxEngine;
53
+ /** Human-readable isolation description for logs/errors. */
54
+ isolationLabel: string;
55
+ }
56
+ export declare class SandboxRequiredError extends Error {
57
+ constructor(message: string);
58
+ }
59
+ /**
60
+ * Detect available container engine (cached).
61
+ */
62
+ export declare function detectContainerEngine(): SandboxEngine;
63
+ /** Test helper */
64
+ export declare function clearSandboxRuntimeCache(): void;
65
+ /**
66
+ * Resolve full runtime configuration for a project.
67
+ */
68
+ export declare function resolveSandboxRuntimeConfig(projectRoot: string, timeoutMs?: number): SandboxRuntimeConfig;
69
+ /**
70
+ * Build docker/podman argv for an isolated run.
71
+ * Project is mounted at /workspace; command runs with -w /workspace.
72
+ */
73
+ export declare function buildContainerArgs(cfg: SandboxRuntimeConfig, command: string, args: string[]): string[];
74
+ /**
75
+ * Run a command under the configured sandbox runtime.
76
+ */
77
+ export declare function runInSandbox(req: RunInSandboxRequest): Promise<RunInSandboxResult>;
78
+ /**
79
+ * Diagnostic snapshot for dashboard / CLI.
80
+ */
81
+ export declare function getSandboxRuntimeStatus(projectRoot?: string): {
82
+ config: SandboxRuntimeConfig;
83
+ isolationLabel: string;
84
+ identity: ReturnType<typeof resolveSandboxIdentity>;
85
+ };