This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

idk this might fix sudo

+113 -11
+6 -6
package.json
··· 9 9 "apps/*" 10 10 ], 11 11 "scripts": { 12 - "start": "dotenv -- tsx src/index.ts", 13 - "start:local": "NIRI_ENV=local dotenv -- tsx src/index.ts", 14 - "dev": "dotenv -- tsx watch src/index.ts", 15 - "dev:local": "NIRI_ENV=local dotenv -- tsx watch src/index.ts", 12 + "start": "dotenv -o -- tsx src/index.ts", 13 + "start:local": "NIRI_ENV=local dotenv -o -- tsx src/index.ts", 14 + "dev": "dotenv -o -- tsx watch src/index.ts", 15 + "dev:local": "NIRI_ENV=local dotenv -o -- tsx watch src/index.ts", 16 16 "test": "node --import tsx --test src/**/*.test.ts", 17 - "chat": "dotenv -- tsx scripts/chat.ts", 18 - "chat:local": "NIRI_ENV=local dotenv -- tsx scripts/chat.ts", 17 + "chat": "dotenv -o -- tsx scripts/chat.ts", 18 + "chat:local": "NIRI_ENV=local dotenv -o -- tsx scripts/chat.ts", 19 19 "dev:web": "npm run dev --workspace @niri/web", 20 20 "build:web": "npm run build --workspace @niri/web", 21 21 "preview:web": "npm run preview --workspace @niri/web"
+45
src/container/shell.test.ts
··· 1 + import test from "node:test" 2 + import assert from "node:assert/strict" 3 + import fs from "node:fs/promises" 4 + import os from "node:os" 5 + import path from "node:path" 6 + import { closeBash } from "./shell.js" 7 + import { runCommand } from "./tools.js" 8 + 9 + test.afterEach(() => { 10 + closeBash() 11 + }) 12 + 13 + test("sudo commands run as one-off commands with stdin closed", async () => { 14 + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "niri-fake-sudo-")) 15 + const fakeSudo = path.join(dir, "sudo") 16 + await fs.writeFile( 17 + fakeSudo, 18 + [ 19 + "#!/bin/sh", 20 + "if [ -t 0 ]; then", 21 + " echo stdin_tty", 22 + "else", 23 + " echo stdin_not_tty", 24 + "fi", 25 + 'exec "$@"', 26 + "", 27 + ].join("\n"), 28 + { mode: 0o755 }, 29 + ) 30 + 31 + const oldPath = process.env.PATH 32 + process.env.PATH = `${dir}${path.delimiter}${oldPath ?? ""}` 33 + try { 34 + const output = await runCommand("echo before; sudo printf ok; echo after", 0, 5_000) 35 + 36 + assert.equal(output, "before\nstdin_not_tty\nokafter") 37 + } finally { 38 + if (oldPath === undefined) { 39 + delete process.env.PATH 40 + } else { 41 + process.env.PATH = oldPath 42 + } 43 + await fs.rm(dir, { recursive: true, force: true }) 44 + } 45 + })
+50
src/container/shell.ts
··· 1 1 import * as pty from "node-pty" 2 2 import { randomBytes } from "crypto" 3 + import { spawn } from "child_process" 3 4 import { 4 5 CONTAINER_NAME, 5 6 CONTAINER_USER, ··· 218 219 }, timeoutMs) 219 220 }) 220 221 } 222 + 223 + /** 224 + * Runs a command as a one-off child process instead of through the persistent 225 + * PTY shell. This follows Codex's shell-tool execution model for commands 226 + * that should not inherit interactive stdin, notably sudo. 227 + */ 228 + export async function runOneOff(command: string, cwd: string, options: RunRawOptions = {}): Promise<string> { 229 + const timeoutMs = normalizeTimeoutMs(options.timeoutMs, DEFAULT_COMMAND_TIMEOUT_MS) 230 + const args = USE_DOCKER_SHELL 231 + ? ["exec", "-i", "-u", CONTAINER_USER, "-w", cwd, CONTAINER_NAME, "bash", "-c", command] 232 + : ["-c", command] 233 + const program = USE_DOCKER_SHELL ? "docker" : "bash" 234 + 235 + return new Promise((resolve, reject) => { 236 + let raw = "" 237 + let settled = false 238 + const child = spawn(program, args, { 239 + cwd: USE_DOCKER_SHELL ? undefined : cwd, 240 + env: process.env, 241 + stdio: ["ignore", "pipe", "pipe"], 242 + }) 243 + 244 + child.stdout.on("data", (chunk) => { 245 + raw += chunk.toString("utf8") 246 + }) 247 + child.stderr.on("data", (chunk) => { 248 + raw += chunk.toString("utf8") 249 + }) 250 + child.on("error", (err) => { 251 + if (settled) return 252 + settled = true 253 + clearTimeout(timer) 254 + reject(err) 255 + }) 256 + child.on("close", () => { 257 + if (settled) return 258 + settled = true 259 + clearTimeout(timer) 260 + resolve(cleanOutput(raw).trimEnd()) 261 + }) 262 + 263 + const timer = setTimeout(() => { 264 + if (settled) return 265 + settled = true 266 + child.kill("SIGKILL") 267 + reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`)) 268 + }, timeoutMs) 269 + }) 270 + }
+12 -5
src/container/tools.ts
··· 10 10 normalizeTimeoutMs, 11 11 resolveMaxLines, 12 12 } from "./config.js" 13 - import { currentWorkingDirectory, runRaw } from "./shell.js" 13 + import { currentWorkingDirectory, runOneOff, runRaw } from "./shell.js" 14 14 import type { EditResult, ImageToolPayload, ModelImageInput } from "./types.js" 15 + 16 + function invokesSudo(command: string): boolean { 17 + return /(^|[;&|({]\s*)sudo(\s|$)/.test(String(command ?? "")) 18 + } 15 19 16 20 function shouldRedirectStdinToDevNullByDefault(command: string): boolean { 17 21 // This runner executes in a real PTY, but we still want to avoid accidentally ··· 76 80 */ 77 81 export async function runCommand(command: string, maxLines?: number, timeoutMs?: number): Promise<string> { 78 82 const cap = resolveMaxLines(command, maxLines) 79 - const raw = await runRaw(command, { 80 - timeoutMs: normalizeTimeoutMs(timeoutMs, DEFAULT_COMMAND_TIMEOUT_MS), 81 - redirectStdinToDevNull: shouldRedirectStdinToDevNullByDefault(command), 82 - }) 83 + const opTimeoutMs = normalizeTimeoutMs(timeoutMs, DEFAULT_COMMAND_TIMEOUT_MS) 84 + const raw = invokesSudo(command) 85 + ? await runOneOff(command, await currentWorkingDirectory(opTimeoutMs), { timeoutMs: opTimeoutMs }) 86 + : await runRaw(command, { 87 + timeoutMs: opTimeoutMs, 88 + redirectStdinToDevNull: shouldRedirectStdinToDevNullByDefault(command), 89 + }) 83 90 84 91 if (cap === 0) return raw 85 92