my harness for niri
1import path from "path"
2
3const configuredContainerName = (process.env.NIRI_CONTAINER ?? "").trim()
4const configuredContainerUser = (process.env.NIRI_USER ?? "").trim()
5
6/** Whether command execution should go through Docker instead of a local shell. */
7export const USE_DOCKER_SHELL = configuredContainerName.length > 0 && configuredContainerUser.length > 0
8/** Docker container name used for command execution. */
9export const CONTAINER_NAME = configuredContainerName || "niri"
10/** Linux user inside the container used for command execution. */
11export const CONTAINER_USER = configuredContainerUser || "niri"
12/** Repository fallback home used when a local OS home is unavailable. */
13export const REPO_HOME_DIR = path.resolve(process.cwd(), "home")
14/** Harness home for soul, memories, and local databases. */
15export const HOME_DIR = USE_DOCKER_SHELL ? REPO_HOME_DIR : path.resolve(process.env.HOME ?? REPO_HOME_DIR)
16
17/** Absolute image root allowed for `image_tool` operations. */
18export const IMAGE_ROOT = (() => {
19 const configured = (process.env.IMAGE_ROOT ?? "").trim()
20 const root = configured || (USE_DOCKER_SHELL ? `/home/${CONTAINER_USER}/images` : path.resolve(process.cwd(), "home", "images"))
21 if (!root.startsWith("/")) {
22 throw new Error(`IMAGE_ROOT must be an absolute path, got: ${root}`)
23 }
24 return path.posix.normalize(root)
25})()
26
27const DEFAULT_IMAGE_MAX_BYTES = 150_000
28/** Maximum image size (bytes) accepted by `image_tool`. */
29export const IMAGE_MAX_BYTES = (() => {
30 const parsed = parseInt(process.env.IMAGE_TOOL_MAX_BYTES ?? `${DEFAULT_IMAGE_MAX_BYTES}`, 10)
31 if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_IMAGE_MAX_BYTES
32 return parsed
33})()
34
35/** Default max lines returned by shell enough for most output without flooding context. */
36const DEFAULT_MAX_LINES = 150
37
38/**
39 * Commands that routinely produce thousands of lines of noise.
40 * When matched, the default cap is tightened to VERBOSE_MAX_LINES.
41 */
42const VERBOSE_PATTERNS: RegExp[] = [
43 /\bapt(-get)?\s+(install|upgrade|update|dist-upgrade|autoremove)\b/,
44 /\bpip3?\s+install\b/,
45 /\bnpm\s+(install|ci|i)\b/,
46 /\byarn\s+(install|add)\b/,
47 /\bcargo\s+(build|install|fetch|update)\b/,
48 /\bgo\s+(get|install|build|mod\s+download)\b/,
49 /\bdpkg\b/,
50 /\bsnap\s+install\b/,
51]
52
53const VERBOSE_MAX_LINES = 40
54/** Default timeout for shell command execution in milliseconds. */
55export const DEFAULT_COMMAND_TIMEOUT_MS = 30_000
56/** Default timeout for file operations in milliseconds. */
57export const DEFAULT_FILE_TIMEOUT_MS = 120_000
58/** Upper bound for all tool timeouts in milliseconds. */
59export const MAX_TIMEOUT_MS = 10 * 60_000
60
61/**
62 * Returns the effective image root path exposed to model/tool descriptions.
63 *
64 * @returns The normalized absolute image root path.
65 */
66export function imageRootForModelInput(): string {
67 return IMAGE_ROOT
68}
69
70/**
71 * Resolves the effective output line cap for a shell command.
72 *
73 * @param command - Command text used to detect verbose command patterns.
74 * @param requested - Optional explicit line cap override.
75 * @returns Effective max line count (`0` means no truncation).
76 */
77export function resolveMaxLines(command: string, requested?: number): number {
78 if (requested !== undefined) return requested
79 return VERBOSE_PATTERNS.some((p) => p.test(command)) ? VERBOSE_MAX_LINES : DEFAULT_MAX_LINES
80}
81
82/**
83 * Normalizes a timeout value by applying defaults, integer coercion, and hard cap.
84 *
85 * @param requested - User-provided timeout value.
86 * @param fallback - Default timeout to use when the provided value is invalid.
87 * @returns A bounded timeout in milliseconds.
88 */
89export function normalizeTimeoutMs(requested: number | undefined, fallback: number): number {
90 const numeric = Number(requested)
91 if (!Number.isFinite(numeric) || numeric <= 0) return fallback
92 return Math.min(Math.trunc(numeric), MAX_TIMEOUT_MS)
93}