import assert from 'node:assert/strict' import { promises as fs } from 'node:fs' import { execFileSync } from 'node:child_process' import { join, resolve } from 'node:path' import { configureSandbox } from '../src/main/sandbox/configuration' import { importSandboxToolchain } from '../src/main/sandbox/toolchains' import { executeSandboxCommand, sandboxStatus } from '../src/main/sandbox/service' await fs.mkdir(resolve('.tmp'), { recursive: true }) const fixture = await fs.mkdtemp(resolve('.tmp/sandbox-process-')), workspace = join(fixture, 'workspace') await fs.mkdir(workspace) const outside = join(fixture, 'outside.txt'); await fs.writeFile(outside, 'OUTSIDE_FIXTURE') configureSandbox({ stateRoot: join(fixture, 'state'), resourceDirectory: resolve('resources/sandbox/win32-x64') }) const checks: Array<{ label: string; pass: boolean }> = [] function check(label: string, condition: unknown): void { checks.push({ label, pass: !!condition }); assert.ok(condition, label); console.log(`PASS ${label}`) } const context = { workspaceRoot: workspace, runtimes: ['node'], timeoutMs: 10_000 } const source = String.raw` const assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path'),cp=require('node:child_process'); const labels=[];function check(label,fn){fn();labels.push(label);console.log('PROCESS_CHECK '+label)} const scratch=fs.mkdtempSync(path.join(os.tmpdir(),'node-runtime-')); try{ check('private-temp',()=>{fs.writeFileSync(path.join(scratch,'x.txt'),'private');assert.equal(fs.readFileSync(path.join(scratch,'x.txt'),'utf8'),'private');fs.unlinkSync(path.join(scratch,'x.txt'))}); check('binary-stdout-stderr',()=>{const r=cp.spawnSync(process.execPath,['-e','process.stdout.write(Buffer.from([0,255,1,2]));process.stderr.write("diagnostic")']);assert.equal(r.status,0);assert.deepEqual(r.stdout,Buffer.from([0,255,1,2]));assert.equal(r.stderr.toString(),'diagnostic')}); check('stdin',()=>{const r=cp.spawnSync(process.execPath,['-e','process.stdin.on("data",x=>process.stdout.write(x))'],{input:Buffer.from([0,255,19])});assert.equal(r.status,0);assert.deepEqual(r.stdout,Buffer.from([0,255,19]))}); check('cwd-and-env',()=>{const r=cp.spawnSync(process.execPath,['-e','process.stdout.write(process.cwd()+"|"+process.env.YCODE_FIXTURE)'],{cwd:scratch,env:{...process.env,YCODE_FIXTURE:'private-value'},encoding:'utf8'});assert.equal(r.status,0);assert.equal(r.stdout,scratch+'|private-value')}); check('exec-file',()=>assert.equal(cp.execFileSync(process.execPath,['-e','process.stdout.write("file-result")'],{encoding:'utf8'}),'file-result')); check('exec-shell',()=>assert.equal(cp.execSync('echo shell-result',{encoding:'utf8'}).trim(),'shell-result')); check('omitted-args-options',()=>assert.equal(cp.execFileSync(process.execPath,undefined,{input:'process.stdout.write("stdin-script")',encoding:'utf8'}),'stdin-script')); check('nonzero-status',()=>{assert.throws(()=>cp.execFileSync(process.execPath,['-e','process.stderr.write("expected-error");process.exit(7)'],{encoding:'utf8'}),e=>e.status===7&&e.stderr==='expected-error')}); check('missing-executable',()=>{const r=cp.spawnSync('definitely-missing-ycode-program.exe');assert.equal(r.error.code,'ENOENT');assert.equal(r.status,null)}); check('timeout',()=>{const r=cp.spawnSync(process.execPath,['-e','setTimeout(()=>console.log("late"),3000)'],{timeout:150,encoding:'utf8'});assert.equal(r.error.code,'ETIMEDOUT');assert.ok(!r.stdout.includes('late'))}); check('max-buffer',()=>{const r=cp.spawnSync(process.execPath,['-e','process.stdout.write("x".repeat(2048))'],{maxBuffer:128,encoding:'utf8'});assert.equal(r.error.code,'ENOBUFS');assert.equal(r.stdout.length,128)}); check('inherited-stdio',()=>assert.equal(cp.spawnSync(process.execPath,['-e','console.log("INHERITED_STDIO_PASS")'],{stdio:'inherit'}).status,0)); check('esm-exports',()=>{const r=cp.spawnSync(process.execPath,['--require',process.env.YCODE_SANDBOX_PROCESS,'--input-type=module','-e','import {execSync} from "node:child_process";console.log(execSync("echo ESM_CAPTURE_PASS",{encoding:"utf8"}).trim())'],{encoding:'utf8'});assert.equal(r.status,0);assert.ok(r.stdout.includes('ESM_CAPTURE_PASS'))}); check('adapter-readonly',()=>{assert.throws(()=>fs.appendFileSync(process.env.YCODE_SANDBOX_PROCESS,'x'),e=>['EACCES','EPERM'].includes(e.code))}); check('host-boundary',()=>{const r=cp.spawnSync(process.execPath,['-e',__HOST_PROBE__],{encoding:'utf8'});assert.equal(r.status,0);assert.equal(r.stdout,'HOST_DENIED')}); check('scratch-cleanup',()=>assert.deepEqual(fs.readdirSync(os.tmpdir()).filter(name=>name.startsWith('ycode-process-')),[])); console.log('PROCESS_LABELS '+JSON.stringify(labels)); }finally{fs.rmdirSync(scratch)} ` try { await importSandboxToolchain('node', await fs.realpath(process.execPath)) const hostProbe = `try{require('fs').readFileSync(${JSON.stringify(outside)});process.exit(9)}catch(e){if(!['EACCES','EPERM'].includes(e.code))throw e;process.stdout.write('HOST_DENIED')}` await fs.writeFile(join(workspace, 'process.cjs'), source.replace('__HOST_PROBE__', JSON.stringify(hostProbe))) const result = await executeSandboxCommand('node --require "%YCODE_SANDBOX_PROCESS%" process.cjs', context) assert.ok(result.ok, result.content) const labels = JSON.parse(result.data.stdout.split(/\r?\n/).find(line => line.startsWith('PROCESS_LABELS '))!.slice(15)) as string[] for (const label of labels) check(`actual AppContainer Node ${label}`, true) check('process adapter creates no host writeback or network authority', result.data.writeback === 'unchanged' && result.data.profileRemoved && result.data.driveRemoved) const pythonSource = execFileSync('where.exe', ['python'], { encoding: 'utf8', windowsHide: true }).trim().split(/\r?\n/)[0] await importSandboxToolchain('python', await fs.realpath(pythonSource)) const outsideDirectory = join(fixture, 'outside-directory') await fs.writeFile(join(workspace, 'temp.py'), [ 'import tempfile, pathlib, subprocess, sys, os, sitecustomize', 'with tempfile.TemporaryDirectory() as directory:', ' path = pathlib.Path(directory) / "data.txt"', ' path.write_text("private-python", encoding="utf-8")', ' assert path.read_text() == "private-python"', ' result = subprocess.run([sys.executable, "-c", "print(43)"], capture_output=True, text=True, timeout=3)', ' assert result.returncode == 0 and result.stdout.strip() == "43"', ' private = pathlib.Path(directory) / "mode700"', ' os.mkdir(private, 0o700)', ' (private / "child.txt").write_text("accessible")', ' assert (private / "child.txt").read_text() == "accessible"', `try: os.mkdir(${JSON.stringify(outsideDirectory)}, 0o700)`, 'except PermissionError: pass', 'else: raise AssertionError("outside directory unexpectedly created")', 'try: pathlib.Path(sitecustomize.__file__).write_text("tamper")', 'except PermissionError: pass', 'else: raise AssertionError("adapter unexpectedly writable")', 'print("PYTHON_TEMP_CAPTURE_PASS")', '' ].join('\n')) const python = await executeSandboxCommand('python temp.py', { ...context, runtimes: ['python'] }) assert.ok(python.ok, python.content) check('actual Python private tempfile and anonymous-pipe subprocess capture work', python.ok && python.data.stdout.includes('PYTHON_TEMP_CAPTURE_PASS') && python.data.writeback === 'unchanged') check('Python mode700 compatibility retains native host denial and read-only adapter', !await fs.stat(outsideDirectory).catch(() => null)) const components = join(fixture, 'components') await fs.mkdir(components) for (const name of ['ycode-sandbox.exe', 'manifest.json', 'node-bridge.cjs', 'node-process.cjs', 'python-sitecustomize.py']) await fs.copyFile(resolve('resources/sandbox/win32-x64', name), join(components, name)) configureSandbox({ stateRoot: join(fixture, 'state'), resourceDirectory: components }) assert.equal((await sandboxStatus()).state, 'installed') for (const name of ['node-bridge.cjs', 'node-process.cjs', 'python-sitecustomize.py']) { const original = await fs.readFile(join(components, name)) await fs.appendFile(join(components, name), 'tamper') assert.equal((await sandboxStatus()).state, 'unavailable') await assert.rejects(executeSandboxCommand('echo MUST_NOT_RUN', context), /摘要/) await fs.writeFile(join(components, name), original) } check('a tampered shipped adapter invalidates installed status and fails commands closed', true) console.log(`SANDBOX_PROCESS_PASS ${checks.length}/${checks.length}`) } finally { await fs.writeFile(join(fixture, 'report.json'), JSON.stringify({ fixture, checks, scope: 'Actual AppContainer Node explicit synchronous file-stdio adapter and Python temporary-directory/capture regression. Default Node pipes and asynchronous spawn/fork compatibility remain unverified/unsupported.' }, null, 2) + '\n') console.log(`SANDBOX_PROCESS_REPORT ${join(fixture, 'report.json')}`) }