chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,195 @@
import { spawn } from "node:child_process"
import fs from "node:fs"
import path from "path"
import { Effect } from "effect"
import { Shell } from "@/shell/shell"
import type { SandboxPolicy } from "./policy"
import { resolveWritableRoots } from "./path"
const SAFE_ENV_KEYS = [
"PATH",
"Path",
"SYSTEMROOT",
"SystemRoot",
"WINDIR",
"windir",
"SYSTEMDRIVE",
"SystemDrive",
"COMSPEC",
"ComSpec",
"PATHEXT",
"PATHExt",
"TEMP",
"TMP",
"USERPROFILE",
"HOME",
"LOCALAPPDATA",
"APPDATA",
"COMPUTERNAME",
"USERNAME",
"OS",
"PROCESSOR_ARCHITECTURE",
"LANG",
"TZ",
] as const
function buildHelperEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {}
for (const key of SAFE_ENV_KEYS) {
const value = baseEnv[key]
if (value !== undefined && value !== null) {
env[key] = String(value)
}
}
delete env.ELECTRON_RUN_AS_NODE
return env
}
function resolveHelperShell(shellPath: string): { cmd: string; args: string[] } {
const gitBash = Shell.gitbash()
if (gitBash && Shell.posix(shellPath)) {
return { cmd: gitBash, args: ["-c"] }
}
return {
cmd: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
args: ["-NoProfile", "-NonInteractive", "-Command"],
}
}
function resolveSandboxCwd(requestedCwd: string, policy: SandboxPolicy): string {
const winMode = policy.sandbox.mode ?? "workspace-write"
if (winMode !== "workspace-write") return path.resolve(requestedCwd)
const roots = resolveWritableRoots(policy.sandbox, policy.instanceDirectory)
const resolved = path.resolve(requestedCwd)
if (roots.some((root) => {
const rel = path.relative(root, resolved)
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
})) {
return resolved
}
return roots[0] ?? resolved
}
export type SandboxBashResult = {
exitCode: number
stdout: string
stderr: string
timedOut: boolean
}
export const runBashViaSandboxHelper = Effect.fn("Sandbox.runBashViaSandboxHelper")(function* (input: {
policy: SandboxPolicy
shell: string
command: string
cwd: string
timeout: number
}) {
const helperPath = input.policy.sandbox.helper_path
if (!helperPath || !fs.existsSync(helperPath)) {
return yield* Effect.fail(new Error(`Sandbox helper not found: ${helperPath ?? "(unset)"}`))
}
const winMode = input.policy.sandbox.mode ?? "workspace-write"
const networkEnabled = input.policy.sandbox.network_enabled ?? true
const writableRoots = resolveWritableRoots(input.policy.sandbox, input.policy.instanceDirectory)
const sandboxCwd = resolveSandboxCwd(input.cwd, input.policy)
const policyJson = {
type: winMode === "workspace-write" ? "workspace-write" : "read-only",
network_access: networkEnabled,
sandbox_mode: input.policy.sandbox.sandbox_mode,
...(writableRoots.length > 0 ? { writable_roots: writableRoots } : {}),
}
const shellWrap = resolveHelperShell(input.shell)
const helperArgs = [
"run",
"--mode",
winMode,
"--cwd",
sandboxCwd,
"--policy-json",
JSON.stringify(policyJson),
"--",
shellWrap.cmd,
...shellWrap.args,
input.command,
]
const env = buildHelperEnv(process.env)
const cappedTimeout = Math.min(input.timeout > 0 ? input.timeout : 120_000, 120_000)
return yield* Effect.tryPromise({
try: () =>
new Promise<SandboxBashResult>((resolve, reject) => {
const child = spawn(helperPath, helperArgs, {
cwd: sandboxCwd,
env,
windowsHide: true,
stdio: ["ignore", "pipe", "pipe"],
})
let stdout = ""
let stderr = ""
let timer: ReturnType<typeof setTimeout> | undefined
if (cappedTimeout > 0) {
timer = setTimeout(() => {
try {
child.kill("SIGKILL")
} catch {
// ignore
}
}, cappedTimeout)
}
child.stdout?.on("data", (chunk) => {
stdout += chunk.toString()
})
child.stderr?.on("data", (chunk) => {
stderr += chunk.toString()
})
child.on("error", (err) => {
if (timer) clearTimeout(timer)
reject(err)
})
child.on("close", (code, signal) => {
if (timer) clearTimeout(timer)
try {
const parsed = JSON.parse(stdout) as {
exit_code?: number
stdout?: string
stderr?: string
timed_out?: boolean
}
resolve({
exitCode: parsed.exit_code ?? code ?? 1,
stdout: parsed.stdout ?? "",
stderr: parsed.stderr ?? "",
timedOut: parsed.timed_out ?? signal === "SIGKILL",
})
} catch {
resolve({
exitCode: code ?? 1,
stdout,
stderr,
timedOut: signal === "SIGKILL",
})
}
})
}),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
})
})
export function shouldUseSandboxHelper(policy: SandboxPolicy | null): policy is SandboxPolicy {
return (
policy !== null &&
process.platform === "win32" &&
!!policy.sandbox.helper_path &&
fs.existsSync(policy.sandbox.helper_path)
)
}

View File

@@ -0,0 +1,18 @@
import { Log } from "@/util"
import type { Info as SandboxInfo } from "@/config/sandbox"
const log = Log.create({ service: "sandbox-env" })
/** Parse `QIMING_AGENT_SANDBOX_CONFIG` injected by qiming-agent (spawn / config merge). */
export function parseQimingAgentSandboxConfig(): SandboxInfo | undefined {
const raw = process.env.QIMING_AGENT_SANDBOX_CONFIG?.trim()
if (!raw) return undefined
try {
return JSON.parse(raw) as SandboxInfo
} catch (error) {
log.warn("invalid QIMING_AGENT_SANDBOX_CONFIG", {
error: error instanceof Error ? error.message : String(error),
})
return undefined
}
}

View File

@@ -0,0 +1,11 @@
export class SandboxPathError extends Error {
readonly filepath: string
readonly writableRoots: string[]
constructor(filepath: string, writableRoots: string[]) {
super(`Path outside writable roots: ${filepath}`)
this.name = "SandboxPathError"
this.filepath = filepath
this.writableRoots = writableRoots
}
}

View File

@@ -0,0 +1,7 @@
export * from "./path"
export * from "./error"
export * from "./env"
export * from "./policy"
export * from "./policy-sync"
export * from "./bash-helper"
export * from "./tool-guard"

View File

@@ -0,0 +1,88 @@
import path from "path"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import type { Info as SandboxInfo } from "@/config/sandbox"
export function isWithinRoot(candidate: string, root: string): boolean {
const resolvedCandidate = path.resolve(candidate)
const resolvedRoot = path.resolve(root)
const rel = path.relative(resolvedRoot, resolvedCandidate)
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
}
export function normalizeSandboxPath(filepath: string): string {
const resolved = path.resolve(filepath)
return process.platform === "win32" ? AppFileSystem.normalizePath(resolved) : resolved
}
export function collectTempDirs(): string[] {
const dirs: string[] = []
for (const key of ["TMP", "TEMP", "TMPDIR"] as const) {
const value = process.env[key]
if (value) dirs.push(path.resolve(value))
}
return dirs
}
export function collectCompatAppDataDirs(): string[] {
if (process.platform !== "win32") return []
const dirs: string[] = []
for (const key of ["APPDATA", "LOCALAPPDATA"] as const) {
const value = process.env[key]
if (value) dirs.push(path.resolve(value))
}
return dirs
}
/** Pick the most specific (deepest) directory when one path contains the other. */
export function pickNarrowestDirectory(...candidates: string[]): string {
const resolved = [...new Set(candidates.filter(Boolean).map((p) => path.resolve(p)))]
if (resolved.length === 0) return path.resolve(".")
let narrowest = resolved[0]!
for (const candidate of resolved.slice(1)) {
if (isWithinRoot(candidate, narrowest)) {
narrowest = candidate
} else if (!isWithinRoot(narrowest, candidate)) {
// Unrelated paths — keep the first candidate (HTTP ?directory= wins over stale DB).
continue
}
}
return narrowest
}
export function resolveWritableRoots(sandbox: SandboxInfo, instanceDirectory: string): string[] {
const mode = sandbox.sandbox_mode
if (mode === "permissive") return []
const roots = new Set<string>()
// Strict: only the session/instance directory, never config writable_roots or broad temp.
if (mode === "strict") {
roots.add(path.resolve(instanceDirectory))
} else {
const configured = sandbox.writable_roots?.map((p) => path.resolve(p)) ?? []
if (configured.length > 0) {
for (const root of configured) roots.add(root)
} else {
roots.add(path.resolve(instanceDirectory))
}
}
if (mode !== "strict") {
for (const temp of collectTempDirs()) {
roots.add(temp)
}
}
if (mode === "compat") {
for (const dir of collectCompatAppDataDirs()) {
roots.add(dir)
}
}
return [...roots]
}
export function isPathWritable(targetPath: string, roots: string[]): boolean {
if (roots.length === 0) return true
const normalized = normalizeSandboxPath(targetPath)
return roots.some((root) => isWithinRoot(normalized, root))
}

View File

@@ -0,0 +1,23 @@
import path from "path"
import { Instance } from "@/project/instance"
import type { SandboxPolicy } from "./policy"
import { parseQimingAgentSandboxConfig } from "./env"
import { resolveWritableRoots } from "./path"
/**
* Sync sandbox policy for bash helper routing (no Config/Session services).
* Uses `QIMING_AGENT_SANDBOX_CONFIG` + current `Instance.directory` (session cwd).
*/
export function getSandboxPolicySync(): SandboxPolicy | null {
const sandbox = parseQimingAgentSandboxConfig()
if (!sandbox || sandbox.sandbox_mode === "permissive") {
return null
}
const instanceDirectory = path.resolve(Instance.directory)
const writableRoots = resolveWritableRoots(sandbox, instanceDirectory)
return {
sandbox,
writableRoots,
instanceDirectory,
}
}

View File

@@ -0,0 +1,136 @@
import path from "path"
import { Effect } from "effect"
import { Config } from "@/config"
import type { Info as SandboxInfo } from "@/config/sandbox"
import { InstanceState } from "@/effect"
import { Log } from "@/util"
import { Session } from "@/session"
import type { SessionID } from "@/session/schema"
import { resolveWritableRoots, normalizeSandboxPath, isPathWritable, pickNarrowestDirectory } from "./path"
import { SandboxPathError } from "./error"
import { parseQimingAgentSandboxConfig } from "./env"
const log = Log.create({ service: "sandbox-policy" })
export type SandboxPolicy = {
sandbox: SandboxInfo
writableRoots: string[]
instanceDirectory: string
}
export const getSandboxPolicy = Effect.fn("Sandbox.getSandboxPolicy")(function* (sessionID?: SessionID) {
const cfg = yield* Config.Service.use((svc) => svc.get())
const sandbox = parseQimingAgentSandboxConfig() ?? cfg.sandbox
if (!sandbox || sandbox.sandbox_mode === "permissive") {
return null
}
const ins = yield* InstanceState.context
const instanceDir = path.resolve(ins.directory)
let instanceDirectory = instanceDir
if (sessionID && sandbox.sandbox_mode === "strict") {
const sessions = yield* Session.Service
const session = yield* sessions.get(sessionID)
const sessionDir = path.resolve(session.directory)
instanceDirectory = pickNarrowestDirectory(instanceDir, sessionDir)
} else if (sessionID) {
const sessions = yield* Session.Service
const session = yield* sessions.get(sessionID)
instanceDirectory = path.resolve(session.directory)
}
const writableRoots = resolveWritableRoots(sandbox, instanceDirectory)
return {
sandbox,
writableRoots,
instanceDirectory,
} satisfies SandboxPolicy
})
export const assertSandboxWritable = Effect.fn("Sandbox.assertSandboxWritable")(function* (
targetPath: string,
sessionID?: SessionID,
) {
const policy = yield* getSandboxPolicy(sessionID)
if (!policy) return
const resolved =
path.isAbsolute(targetPath) || (process.platform === "win32" && /^[a-zA-Z]:[\\/]/.test(targetPath))
? normalizeSandboxPath(targetPath)
: normalizeSandboxPath(path.join(policy.instanceDirectory, targetPath))
if (!isPathWritable(resolved, policy.writableRoots)) {
log.warn("sandbox write blocked", {
target: resolved,
sessionID,
instanceDirectory: policy.instanceDirectory,
writableRoots: policy.writableRoots,
sandbox_mode: policy.sandbox.sandbox_mode,
})
return yield* Effect.fail(new SandboxPathError(resolved, policy.writableRoots))
}
})

View File

@@ -0,0 +1,42 @@
import path from "path"
import { Effect } from "effect"
import type { Info as SandboxInfo } from "@/config/sandbox"
import { Instance } from "@/project/instance"
import { SandboxPathError } from "./error"
import { parseQimingAgentSandboxConfig } from "./env"
import { isPathWritable, normalizeSandboxPath, resolveWritableRoots } from "./path"
function resolveTargetPath(targetPath: string, instanceDirectory: string): string {
if (
path.isAbsolute(targetPath) ||
(process.platform === "win32" && /^[a-zA-Z]:[\\/]/.test(targetPath))
) {
return normalizeSandboxPath(targetPath)
}
return normalizeSandboxPath(path.join(instanceDirectory, targetPath))
}
/**
* Synchronous write guard for built-in tools (edit/write/apply_patch).
* Uses `QIMING_AGENT_SANDBOX_CONFIG` + `Instance.directory` (session cwd) — no Effect services.
*/
export function assertSandboxWritableInTool(targetPath: string, sandbox?: SandboxInfo): void {
const active = sandbox ?? parseQimingAgentSandboxConfig()
if (!active || active.sandbox_mode === "permissive") return
const instanceDirectory = path.resolve(Instance.directory)
const writableRoots = resolveWritableRoots(active, instanceDirectory)
const resolved = resolveTargetPath(targetPath, instanceDirectory)
if (!isPathWritable(resolved, writableRoots)) {
throw new SandboxPathError(resolved, writableRoots)
}
}
/** Effect wrapper for built-in tool `execute` (maps to `Error`, no extra services). */
export function assertSandboxWritableEffect(targetPath: string) {
return Effect.try({
try: () => assertSandboxWritableInTool(targetPath),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
})
}