mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
Add experimental Sandpit agent security controls
Signed-off-by: Alex Rosenzweig <arosenzweig@squareup.com>
This commit is contained in:
53
scripts/fixtures/sandpit-acp-probe.cjs
Normal file
53
scripts/fixtures/sandpit-acp-probe.cjs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Synthetic ACP adapter: deterministic, no model credentials or relay needed.
|
||||
const fs = require('node:fs');
|
||||
const cp = require('node:child_process');
|
||||
const net = require('node:net');
|
||||
const readline = require('node:readline');
|
||||
const rootIndex = process.argv.indexOf("--root");
|
||||
const root = rootIndex >= 0 ? process.argv[rootIndex + 1] : process.env.BUZZ_SANDBOX_FIXTURE;
|
||||
function attempt(action) {
|
||||
try { action(); return 'allowed'; } catch (error) { return error.code; }
|
||||
}
|
||||
async function probe() {
|
||||
const childProcess = process.argv.includes('--probe') ? null : cp.spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], {stdio:'ignore'});
|
||||
const result = {
|
||||
descendant_pid: childProcess?.pid ?? null,
|
||||
read: attempt(() => fs.readFileSync(root + '/protected/secret')),
|
||||
write: attempt(() => fs.writeFileSync(root + '/protected/keep', 'changed')),
|
||||
allowed_write: attempt(() => fs.writeFileSync(root + '/work/result.txt', 'Work completed inside Buzz’s sandbox.\n')),
|
||||
outside_write: attempt(() => fs.writeFileSync(root + '/outside/escape', 'changed')),
|
||||
hardlink: attempt(() => fs.linkSync(root + '/protected/secret', root + '/work/alias')),
|
||||
symlink_read: attempt(() => fs.readFileSync(root + '/work/secret-link')),
|
||||
policy_tamper: process.env.SANDPIT_CONFIG ? attempt(() => fs.writeFileSync(process.env.SANDPIT_CONFIG, '[files]\n')) : 'unprotected',
|
||||
ambient_secret: process.env.BUZZ_SANDBOX_UNGRANTED_SECRET ? 'leaked' : 'absent',
|
||||
};
|
||||
const child = cp.spawnSync('/bin/sh', ['-c', 'exec /usr/bin/env -i "$1" -e "$2" "$3"', 'probe', process.execPath,
|
||||
`const fs=require('fs'); try {fs.readFileSync(process.argv[1]); console.log('allowed')} catch(e) {console.log(e.code)}`,
|
||||
root + '/protected/secret'], {encoding:'utf8'});
|
||||
result.empty_environment_read = child.stdout.trim();
|
||||
result.socket = await new Promise(resolve => {
|
||||
const s = net.connect(9, '127.0.0.1');
|
||||
s.setTimeout(1000, () => { s.destroy(); resolve('timeout'); });
|
||||
s.on('connect', () => { s.destroy(); resolve('allowed'); });
|
||||
s.on('error', e => resolve(e.code));
|
||||
});
|
||||
fs.writeFileSync(root + '/work/report.json', JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
if (process.argv.includes('--probe')) { probe().then(r => console.log(JSON.stringify(r))); }
|
||||
else {
|
||||
const send = value => process.stdout.write(JSON.stringify(value) + '\n');
|
||||
readline.createInterface({input:process.stdin}).on('line', async line => {
|
||||
const req = JSON.parse(line);
|
||||
let result;
|
||||
if (req.method === 'initialize') result = {protocolVersion:2, agentInfo:{name:'sandpit-probe', version:'1'}, agentCapabilities:{}};
|
||||
else if (req.method === 'session/new') result = {sessionId:'probe-session'};
|
||||
else if (req.method === 'session/prompt') {
|
||||
const report = await probe();
|
||||
send({jsonrpc:'2.0', method:'session/update', params:{sessionId:'probe-session', update:{sessionUpdate:'agent_message_chunk', content:{type:'text', text:JSON.stringify(report)}}}});
|
||||
result = {stopReason:'end_turn'};
|
||||
} else if (req.method === 'session/cancel') return;
|
||||
else result = {};
|
||||
if (req.id !== undefined) send({jsonrpc:'2.0', id:req.id, result});
|
||||
});
|
||||
}
|
||||
101
scripts/sandpit-demo.py
Normal file
101
scripts/sandpit-demo.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Safe, repeatable confinement demo through Buzz's production ACP launch seam."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
import time
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
NODE = subprocess.check_output([str(REPO / 'bin/node'), '-p', 'process.execPath'], text=True).strip()
|
||||
root = Path(tempfile.mkdtemp(prefix='buzz-sandpit-demo-')).resolve()
|
||||
for name in ['protected', 'work', 'outside']:
|
||||
(root / name).mkdir()
|
||||
(root / 'protected/secret').write_text('synthetic secret; no real credentials')
|
||||
(root / 'protected/keep').write_text('original')
|
||||
(root / 'work/secret-link').symlink_to(root / 'protected/secret')
|
||||
policy = {
|
||||
'schema_version': 1,
|
||||
'writable_roots': [str(root / 'work')],
|
||||
'denied_reads': [str(root / 'protected')],
|
||||
'denied_writes': [str(root / 'protected')],
|
||||
'network': {'mode': 'deny_all'},
|
||||
'environment': ['BUZZ_SANDBOX_FIXTURE'],
|
||||
}
|
||||
(root / 'policy.json').write_text(json.dumps(policy, indent=2))
|
||||
env = os.environ.copy()
|
||||
env['BUZZ_SANDBOX_FIXTURE'] = str(root)
|
||||
env['BUZZ_SANDBOX_UNGRANTED_SECRET'] = 'synthetic-ambient-secret'
|
||||
args = [str(REPO / 'target/debug/buzz-acp'), 'sandbox-chat', '--policy', str(root / 'policy.json'),
|
||||
'--agent-command', NODE, '--arg', str(REPO / 'scripts/fixtures/sandpit-acp-probe.cjs'),
|
||||
'--workspace', str(root / 'work'), '--prompt', 'Complete ordinary work and try the escape probes.']
|
||||
print('Buzz + Sandpit: real ACP launch, disposable synthetic files', flush=True)
|
||||
# Negative control: identical probes have access without the production guard.
|
||||
control = subprocess.run([NODE, str(REPO / 'scripts/fixtures/sandpit-acp-probe.cjs'), '--probe'],
|
||||
env={'PATH': os.environ.get('PATH', ''), 'BUZZ_SANDBOX_FIXTURE': str(root)},
|
||||
text=True, capture_output=True, timeout=10)
|
||||
assert control.returncode == 0, control.stderr
|
||||
control_report = json.loads((root / 'work/report.json').read_text())
|
||||
for field in ['read', 'write', 'outside_write', 'symlink_read', 'empty_environment_read']:
|
||||
assert control_report[field] == 'allowed', (field, control_report)
|
||||
(root / 'work/alias').unlink()
|
||||
(root / 'outside/escape').unlink()
|
||||
(root / 'protected/keep').write_text('original')
|
||||
print('PASS negative control: probes succeed without confinement')
|
||||
|
||||
result = subprocess.run(args, env=env, text=True, capture_output=True, timeout=90)
|
||||
print(result.stderr)
|
||||
if result.returncode:
|
||||
raise SystemExit(result.returncode)
|
||||
report = json.loads((root / 'work/report.json').read_text())
|
||||
for _ in range(30):
|
||||
try:
|
||||
os.kill(report['descendant_pid'], 0)
|
||||
except ProcessLookupError:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise AssertionError('Buzz shutdown stranded a descendant')
|
||||
print('PASS descendant stopped with Buzz')
|
||||
assert report['allowed_write'] == 'allowed', report
|
||||
assert report['ambient_secret'] == 'absent', report
|
||||
for field in ['read', 'write', 'outside_write', 'hardlink', 'symlink_read', 'policy_tamper', 'empty_environment_read', 'socket']:
|
||||
assert report[field] in ['EPERM', 'EACCES'], (field, report)
|
||||
print(f'PASS {field.replace("_", " ")} blocked')
|
||||
assert (root / 'protected/keep').read_text() == 'original'
|
||||
assert 'Protected' in result.stderr
|
||||
print('PASS ordinary workspace work completed')
|
||||
print('PASS ungranted environment secret removed')
|
||||
# A second process exercises the same frozen-input launch path afresh.
|
||||
second = subprocess.run(args[:-2], input='/restart\nRun the probes.\n/quit\n', env=env, text=True, capture_output=True, timeout=90)
|
||||
assert second.returncode == 0 and 'Restarted under the same immutable policy' in second.stderr, second.stderr
|
||||
print('PASS restart and reinitialize')
|
||||
# Fail closed before even launching an adapter.
|
||||
invalid = dict(policy, schema_version=999)
|
||||
(root / 'invalid.json').write_text(json.dumps(invalid))
|
||||
invalid_args = list(args)
|
||||
invalid_args[invalid_args.index('--policy')+1] = str(root / 'invalid.json')
|
||||
failed = subprocess.run(invalid_args, env=env, text=True, capture_output=True, timeout=20)
|
||||
assert failed.returncode != 0 and 'unsupported security schema' in failed.stderr
|
||||
print('PASS unsupported policy refused')
|
||||
# A separate executable directory must not fall back to PATH/Homebrew.
|
||||
with tempfile.TemporaryDirectory(prefix='buzz-missing-engine-') as isolated:
|
||||
isolated = Path(isolated)
|
||||
harness = isolated / 'buzz-acp'
|
||||
shutil.copy2(REPO / 'target/debug/buzz-acp', harness)
|
||||
isolated_args = [str(harness), *args[1:]]
|
||||
absent = subprocess.run(isolated_args, env=env, text=True, capture_output=True, timeout=20)
|
||||
assert absent.returncode != 0 and 'bundled buzz-sandpit is missing' in absent.stderr, absent.stderr
|
||||
print('PASS missing engine refused without fallback')
|
||||
engine = isolated / 'buzz-sandpit'
|
||||
engine.write_text('#!/bin/sh\nprintf incompatible\n')
|
||||
engine.chmod(0o700)
|
||||
incompatible = subprocess.run(isolated_args, env=env, text=True, capture_output=True, timeout=20)
|
||||
assert incompatible.returncode != 0 and 'incompatible bundled security engine' in incompatible.stderr, incompatible.stderr
|
||||
print('PASS incompatible engine refused without fallback')
|
||||
|
||||
print(f'\nTry/edit the policy: {root / "policy.json"}')
|
||||
print(f'Work product: {root / "work/result.txt"}')
|
||||
print(f'Probe report: {root / "work/report.json"}')
|
||||
19
scripts/sandpit-desktop-bundle.sh
Executable file
19
scripts/sandpit-desktop-bundle.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Repeatable local app bundle, with isolated app, keyring, and OAuth identities.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source bin/activate-hermit
|
||||
just sandpit-poc-build
|
||||
cargo build -p buzz-agent -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
|
||||
TARGET_TRIPLE=$(rustc -vV | sed -n 's|host: ||p')
|
||||
for executable in buzz-acp buzz-agent buzz-dev-mcp buzz git-credential-nostr; do
|
||||
cp "target/debug/$executable" "desktop/src-tauri/binaries/$executable-$TARGET_TRIPLE"
|
||||
done
|
||||
DEMO_CONFIG=$(mktemp /tmp/buzz-sandpit-bundle.XXXXXX)
|
||||
trap 'rm -f "$DEMO_CONFIG"' EXIT
|
||||
node desktop/scripts/demo-build-config.mjs 'Sandpit Demo' "$DEMO_CONFIG" 2026091500000001
|
||||
BUZZ_BUILD_DEMO_SLUG=sandpit-demo-2026091500000001 pnpm --dir desktop tauri build --debug --bundles app --config "$DEMO_CONFIG"
|
||||
DEMO_APP='desktop/src-tauri/target/debug/bundle/macos/Buzz Sandpit Demo.app'
|
||||
cp sandpit/target/release/sandpit "$DEMO_APP/Contents/MacOS/buzz-sandpit"
|
||||
codesign --force --deep --sign - "$DEMO_APP"
|
||||
printf 'Demo app: %s/%s\n' "$PWD" "$DEMO_APP"
|
||||
19
scripts/sandpit-desktop-demo.sh
Executable file
19
scripts/sandpit-desktop-demo.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local development preview with its own Desktop identity and keyring namespace.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source bin/activate-hermit
|
||||
cargo build --manifest-path sandpit/Cargo.toml --release -p sandpit-dylib
|
||||
cargo build --manifest-path sandpit/Cargo.toml --release --bin sandpit
|
||||
cargo build -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p buzz-cli -p git-credential-nostr
|
||||
cp sandpit/target/release/sandpit target/debug/buzz-sandpit
|
||||
TARGET_TRIPLE=$(rustc -vV | sed -n 's|host: ||p')
|
||||
for executable in buzz-acp buzz-agent buzz-dev-mcp buzz git-credential-nostr; do
|
||||
cp "target/debug/$executable" "desktop/src-tauri/binaries/$executable-$TARGET_TRIPLE"
|
||||
done
|
||||
# Development lookup uses workspace binaries, including the adjacent engine.
|
||||
export BUZZ_DEV_KEYRING_SERVICE=buzz-desktop-dev.sandpit
|
||||
export BUZZ_RELAY_URL="${BUZZ_RELAY_URL:-ws://localhost:3000}"
|
||||
unset BUZZ_PRIVATE_KEY BUZZ_SHARE_IDENTITY BUZZ_ACP_SECURITY_POLICY BUZZ_ACP_SECURITY_STATUS
|
||||
cd desktop
|
||||
exec ../bin/pnpm exec tauri dev --config '{"identifier":"xyz.block.buzz.app.dev.sandpit","productName":"Buzz Sandpit Demo","build":{"devUrl":"http://localhost:1437","beforeDevCommand":"exec ./node_modules/.bin/vite --port 1437 --strictPort"}}' "$@"
|
||||
72
scripts/sandpit-live-demo.py
Normal file
72
scripts/sandpit-live-demo.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Try a real native Buzz agent with existing Databricks sign-in. No relay required."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
from urllib.parse import urlparse
|
||||
|
||||
repo = Path(__file__).resolve().parents[1]
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--host', default=os.environ.get('DATABRICKS_HOST'))
|
||||
parser.add_argument('--model', default='databricks-gpt-5-5-pro')
|
||||
parser.add_argument('--interactive', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if not args.host:
|
||||
# Read only the workspace setting; auth remains in the native agent's cache.
|
||||
goose_config = Path.home() / '.config/goose/config.yaml'
|
||||
if goose_config.exists():
|
||||
for line in goose_config.read_text().splitlines():
|
||||
if line.startswith('DATABRICKS_HOST:'):
|
||||
args.host = line.split(':', 1)[1].strip().strip('"').strip("'")
|
||||
if not args.host:
|
||||
parser.error('provide --host with your Databricks workspace URL')
|
||||
host = urlparse(args.host if '://' in args.host else 'https://' + args.host).hostname
|
||||
if not host:
|
||||
parser.error('invalid workspace URL')
|
||||
root = Path(tempfile.mkdtemp(prefix='buzz-sandpit-live-', dir='/tmp')).resolve()
|
||||
for name in ['protected', 'work', 'outside']:
|
||||
(root / name).mkdir()
|
||||
(root / 'protected/secret').write_text('SYNTHETIC_SECRET')
|
||||
(root / 'protected/keep').write_text('original')
|
||||
(root / 'work/secret-link').symlink_to(root / 'protected/secret')
|
||||
(root / 'work/AGENTS.md').write_text('Only use these disposable fixtures. Do not inspect other directories.\n')
|
||||
node = subprocess.check_output([str(repo / 'bin/node'), '-p', 'process.execPath'], text=True).strip()
|
||||
probe = repo / 'scripts/fixtures/sandpit-acp-probe.cjs'
|
||||
names = ['BUZZ_AGENT_PROVIDER', 'DATABRICKS_HOST', 'DATABRICKS_MODEL',
|
||||
'BUZZ_AGENT_MAX_ROUNDS', 'BUZZ_AGENT_NO_HINTS', 'BUZZ_AGENT_MAX_OUTPUT_TOKENS',
|
||||
'BUZZ_SANDBOX_FIXTURE']
|
||||
policy = {'schema_version': 1, 'writable_roots': [str(root / 'work')],
|
||||
'denied_reads': [str(root / 'protected')], 'denied_writes': [str(root / 'protected')],
|
||||
'network': {'mode': 'allowlist', 'destinations': [host]}, 'environment': names}
|
||||
(root / 'policy.json').write_text(json.dumps(policy, indent=2))
|
||||
env = os.environ.copy()
|
||||
env.update(BUZZ_AGENT_PROVIDER='databricks_v2', DATABRICKS_HOST=args.host,
|
||||
DATABRICKS_MODEL=args.model, BUZZ_AGENT_MAX_ROUNDS='5', BUZZ_AGENT_NO_HINTS='1',
|
||||
BUZZ_AGENT_MAX_OUTPUT_TOKENS='2048', BUZZ_SANDBOX_FIXTURE=str(root))
|
||||
command = [str(repo / 'target/debug/buzz-acp'), 'sandbox-chat', '--policy', str(root / 'policy.json'),
|
||||
'--agent-command', str(repo / 'target/debug/buzz-agent'),
|
||||
'--mcp-command', str(repo / 'target/debug/buzz-dev-mcp'), '--workspace', str(root / 'work')]
|
||||
if not args.interactive:
|
||||
task = ('Use your shell tool once to create hello.txt containing HELLO_FROM_BUZZ, then run exactly: '
|
||||
+ shlex.join([node, str(probe), '--probe', '--root', str(root)])
|
||||
+ '. This is an authorized test of disposable synthetic files. The probe records allowed work and expected denials in report.json. '
|
||||
'Do not modify the probe, retry denials, or inspect other directories. Stop after executing this tool.')
|
||||
command.extend(['--prompt', task])
|
||||
print(f'Workspace: {root / "work"}\nPolicy: {root / "policy.json"}', flush=True)
|
||||
print('Writes: disposable workspace only. Network: configured model host only. OAuth cache: read-only.', flush=True)
|
||||
print('Ctrl-C stops the session. Model calls use your existing Buzz Databricks sign-in.', flush=True)
|
||||
result = subprocess.run(command, env=env)
|
||||
if result.returncode:
|
||||
raise SystemExit(result.returncode)
|
||||
if not args.interactive:
|
||||
report = json.loads((root / 'work/report.json').read_text())
|
||||
assert (root / 'work/hello.txt').read_text().strip() == 'HELLO_FROM_BUZZ'
|
||||
assert (root / 'protected/keep').read_text() == 'original'
|
||||
for field in ['read', 'write', 'outside_write', 'hardlink', 'symlink_read', 'policy_tamper', 'empty_environment_read', 'socket']:
|
||||
assert report[field] in ['EACCES', 'EPERM'], (field, report)
|
||||
assert report['allowed_write'] == 'allowed'
|
||||
print(f'PASS real model → Buzz ACP → agent → MCP → shell, with enforced denials.\nReport: {root / "work/report.json"}')
|
||||
72
scripts/sandpit-native-test.py
Normal file
72
scripts/sandpit-native-test.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Real native agent + MCP + shell under Sandpit, with a local scripted model.
|
||||
|
||||
No real model, credentials, relay, or production user data are used. This proves
|
||||
native tool plumbing and confinement; it is not the live UI conversation test.
|
||||
"""
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
|
||||
repo = Path(__file__).resolve().parents[1]
|
||||
root = Path(tempfile.mkdtemp(prefix='buzz-native-security-', dir='/tmp')).resolve()
|
||||
for name in ['home', 'work', 'protected', 'outside', 'control']:
|
||||
(root / name).mkdir()
|
||||
(root / 'protected/secret').write_text('synthetic')
|
||||
(root / 'protected/keep').write_text('original')
|
||||
(root / 'work/secret-link').symlink_to(root / 'protected/secret')
|
||||
node = subprocess.check_output([str(repo / 'bin/node'), '-p', 'process.execPath'], text=True).strip()
|
||||
probe_command = shlex.join([node, str(repo / 'scripts/fixtures/sandpit-acp-probe.cjs'), '--probe', '--root', str(root)])
|
||||
requests = []
|
||||
|
||||
class Model(BaseHTTPRequestHandler):
|
||||
def log_message(self, *_args):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
request = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
||||
requests.append(request)
|
||||
if len(requests) == 1:
|
||||
names = [tool['function']['name'] for tool in request['tools']]
|
||||
shell = next(name for name in names if name.endswith('__shell') or name == 'shell')
|
||||
message = {'role': 'assistant', 'content': None, 'tool_calls': [{'id': 'probe', 'type': 'function', 'function': {'name': shell, 'arguments': json.dumps({'command': probe_command, 'workdir': str(root / 'work')})}}]}
|
||||
finish = 'tool_calls'
|
||||
else:
|
||||
message = {'role': 'assistant', 'content': 'Scripted model: probe complete.'}
|
||||
finish = 'stop'
|
||||
body = json.dumps({'id': 'local-scripted-model', 'object': 'chat.completion', 'model': 'scripted-test', 'choices': [{'index': 0, 'message': message, 'finish_reason': finish}]}).encode()
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
server = ThreadingHTTPServer(('127.0.0.1', 0), Model)
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
values = {'BUZZ_AGENT_PROVIDER': 'openai', 'OPENAI_COMPAT_API_KEY': 'synthetic-test', 'OPENAI_COMPAT_MODEL': 'scripted-test', 'OPENAI_COMPAT_BASE_URL': f'http://127.0.0.1:{server.server_port}', 'BUZZ_AGENT_MAX_ROUNDS': '3', 'BUZZ_AGENT_NO_HINTS': '1', 'BUZZ_SANDBOX_FIXTURE': str(root)}
|
||||
policy = {'schema_version': 1, 'writable_roots': [str(root / 'work')], 'denied_reads': [str(root / 'protected')], 'denied_writes': [str(root / 'protected')], 'network': {'mode': 'allowlist', 'destinations': [f'localhost:{server.server_port}']}, 'environment': list(values)}
|
||||
source = root / 'control/policy.json'
|
||||
source.write_text(json.dumps(policy))
|
||||
status = root / 'control/protected'
|
||||
env = {'HOME': str(root / 'home'), 'PATH': os.environ['PATH'], 'TMPDIR': str(root), 'BUZZ_ACP_SECURITY_STATUS': str(status), **values}
|
||||
try:
|
||||
result = subprocess.run([str(repo / 'target/debug/buzz-acp'), 'sandbox-chat', '--policy', str(source), '--agent-command', str(repo / 'target/debug/buzz-agent'), '--mcp-command', str(repo / 'target/debug/buzz-dev-mcp'), '--workspace', str(root / 'work'), '--prompt', 'Execute the disposable probe once.'], env=env, text=True, capture_output=True, timeout=60)
|
||||
(root / 'transcript.txt').write_text(result.stdout + result.stderr)
|
||||
(root / 'model-requests.json').write_text(json.dumps(requests, indent=2))
|
||||
assert result.returncode == 0, result.stderr
|
||||
report = json.loads((root / 'work/report.json').read_text())
|
||||
assert len(requests) == 2, len(requests)
|
||||
assert len(status.read_text()) == 64
|
||||
assert report['allowed_write'] == 'allowed'
|
||||
for key in ['read', 'write', 'outside_write', 'hardlink', 'symlink_read', 'policy_tamper', 'empty_environment_read', 'socket']:
|
||||
assert report[key] in ['EACCES', 'EPERM'], (key, report)
|
||||
assert (root / 'protected/keep').read_text() == 'original'
|
||||
print(f'PASS native Buzz agent → local scripted model → MCP → real shell, with eight enforced denials.\nArtifacts: {root}')
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
Reference in New Issue
Block a user