@olahulleberg/infer 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/cli.js +59 -3
package/README.md
CHANGED
|
@@ -95,6 +95,16 @@ The classifier uses the model you configure — no separate API key needed. If i
|
|
|
95
95
|
|
|
96
96
|
To set it up: run `infer config` and answer yes to the classifier prompt at the end.
|
|
97
97
|
|
|
98
|
+
## Sandbox
|
|
99
|
+
|
|
100
|
+
When a classifier is configured, auto-approved commands run inside a sandbox that makes the filesystem read-only. The command can read anything but cannot write, delete, or modify files.
|
|
101
|
+
|
|
102
|
+
**Linux:** requires [`bwrap`](https://github.com/containers/bubblewrap) (`sudo apt install bubblewrap` / `sudo pacman -S bubblewrap`)
|
|
103
|
+
|
|
104
|
+
**macOS:** uses `sandbox-exec`, which is built-in.
|
|
105
|
+
|
|
106
|
+
If the classifier is configured but no sandbox is detected, infer warns at startup and falls back to running auto-approved commands without isolation.
|
|
107
|
+
|
|
98
108
|
## Sessions
|
|
99
109
|
|
|
100
110
|
- Default: fresh session, clears previous
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import { completeSimple } from "@mariozechner/pi-ai";
|
|
|
24
24
|
import { homedir } from "os";
|
|
25
25
|
import { join, resolve } from "path";
|
|
26
26
|
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
|
|
27
|
-
import { spawn } from "child_process";
|
|
27
|
+
import { spawn, execFileSync } from "child_process";
|
|
28
28
|
|
|
29
29
|
const VALID_THINKING_LEVELS = new Set([
|
|
30
30
|
"off",
|
|
@@ -149,11 +149,26 @@ if (!prompt) {
|
|
|
149
149
|
if (!options.continue) {
|
|
150
150
|
clearSessions(sessionDir);
|
|
151
151
|
}
|
|
152
|
+
|
|
153
|
+
const classifierConfig = loadClassifierConfig(agentDir);
|
|
154
|
+
const sandboxBin = detectSandbox();
|
|
155
|
+
|
|
156
|
+
if (classifierConfig && !sandboxBin) {
|
|
157
|
+
process.stderr.write(
|
|
158
|
+
gray(
|
|
159
|
+
"Warning: classifier is active but no sandbox detected. Auto-approved commands run without isolation.\n" +
|
|
160
|
+
" Linux: install bubblewrap (bwrap) | macOS: sandbox-exec should be built-in\n",
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sandboxState = { next: false };
|
|
166
|
+
|
|
152
167
|
const resourceLoader = new DefaultResourceLoader({
|
|
153
168
|
cwd: process.cwd(),
|
|
154
169
|
agentDir,
|
|
155
170
|
settingsManager,
|
|
156
|
-
extensionFactories: [createBashApprovalExtension({ modelRegistry, classifierConfig
|
|
171
|
+
extensionFactories: [createBashApprovalExtension({ modelRegistry, classifierConfig, sandboxBin, sandboxState })],
|
|
157
172
|
});
|
|
158
173
|
|
|
159
174
|
await resourceLoader.reload();
|
|
@@ -178,6 +193,20 @@ const { session } = await createAgentSession({
|
|
|
178
193
|
thinkingLevel: options.thinking,
|
|
179
194
|
});
|
|
180
195
|
|
|
196
|
+
if (sandboxBin && classifierConfig) {
|
|
197
|
+
const bt = session.agent.state.tools.find((t) => t.name === "bash");
|
|
198
|
+
if (bt) {
|
|
199
|
+
const orig = bt.execute;
|
|
200
|
+
bt.execute = async (id, args, signal, progress) => {
|
|
201
|
+
if (sandboxState.next) {
|
|
202
|
+
sandboxState.next = false;
|
|
203
|
+
args = { ...args, command: wrapWithSandbox(args.command, sandboxBin) };
|
|
204
|
+
}
|
|
205
|
+
return orig(id, args, signal, progress);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
181
210
|
let lastAssistantText = "";
|
|
182
211
|
let printedToolLine = false;
|
|
183
212
|
|
|
@@ -627,6 +656,32 @@ function formatContext(value) {
|
|
|
627
656
|
return String(value);
|
|
628
657
|
}
|
|
629
658
|
|
|
659
|
+
function detectSandbox() {
|
|
660
|
+
try {
|
|
661
|
+
if (process.platform === "linux") {
|
|
662
|
+
execFileSync("which", ["bwrap"], { stdio: "ignore" });
|
|
663
|
+
return "bwrap";
|
|
664
|
+
}
|
|
665
|
+
if (process.platform === "darwin") {
|
|
666
|
+
execFileSync("which", ["sandbox-exec"], { stdio: "ignore" });
|
|
667
|
+
return "sandbox-exec";
|
|
668
|
+
}
|
|
669
|
+
} catch {}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function wrapWithSandbox(command, sandboxBin) {
|
|
674
|
+
const q = "'" + command.replace(/'/g, "'\\''") + "'";
|
|
675
|
+
if (sandboxBin === "bwrap") {
|
|
676
|
+
return `bwrap --ro-bind / / --dev-bind /dev /dev --proc /proc --tmpfs /tmp -- sh -c ${q}`;
|
|
677
|
+
}
|
|
678
|
+
if (sandboxBin === "sandbox-exec") {
|
|
679
|
+
const profile = "(version 1)(deny default)(allow file-read* file-map-executable process-exec process-fork signal sysctl-read mach-lookup)";
|
|
680
|
+
return `sandbox-exec -p '${profile}' sh -c ${q}`;
|
|
681
|
+
}
|
|
682
|
+
return command;
|
|
683
|
+
}
|
|
684
|
+
|
|
630
685
|
function loadClassifierConfig(agentDir) {
|
|
631
686
|
const file = join(agentDir, "classifier.json");
|
|
632
687
|
if (!existsSync(file)) return null;
|
|
@@ -652,7 +707,7 @@ function gray(text) {
|
|
|
652
707
|
return `${DIM}${text}${RESET}`;
|
|
653
708
|
}
|
|
654
709
|
|
|
655
|
-
function createBashApprovalExtension({ modelRegistry, classifierConfig }) {
|
|
710
|
+
function createBashApprovalExtension({ modelRegistry, classifierConfig, sandboxBin, sandboxState }) {
|
|
656
711
|
let allowAll = false;
|
|
657
712
|
|
|
658
713
|
return (pi) => {
|
|
@@ -677,6 +732,7 @@ function createBashApprovalExtension({ modelRegistry, classifierConfig }) {
|
|
|
677
732
|
const classification = await classifyCommand(command, { modelRegistry, classifierConfig });
|
|
678
733
|
if (classification.harmless) {
|
|
679
734
|
process.stdout.write(gray(`✓ ${classification.description}\n`));
|
|
735
|
+
if (sandboxBin) sandboxState.next = true;
|
|
680
736
|
return;
|
|
681
737
|
}
|
|
682
738
|
let decision;
|