@praxis-ai/praxis 0.1.1 → 0.1.2
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/dist/agentCore/index.d.ts +45 -6
- package/dist/agentCore/index.js +14 -2
- package/dist/applicationLayer/applicationContract.d.ts +2 -0
- package/dist/applicationLayer/applicationRuntime.d.ts +6 -1
- package/dist/applicationLayer/applicationRuntime.js +37 -3
- package/dist/applicationLayer/index.d.ts +1 -0
- package/dist/basetool/core/shellRun.js +6 -1
- package/dist/rax_packageManager/raxCli.js +42 -1
- package/dist/runtimeImplementation/praxisRuntimeKernel.d.ts +6 -0
- package/dist/runtimeImplementation/praxisRuntimeKernel.js +165 -14
- package/dist/runtimeImplementation/runtime.componentPlane/runtimeComponentRegistry.d.ts +1 -1
- package/dist/runtimeImplementation/runtime.componentPlane/runtimeComponentRegistry.js +2 -2
- package/dist/runtimeImplementation/runtime.dependencyPlane/dependencySourceRegistry.d.ts +1 -1
- package/dist/runtimeImplementation/runtime.dependencyPlane/dependencySourceRegistry.js +12 -0
- package/dist/runtimeImplementation/runtime.dependencyPlane/dependencyTypes.js +2 -0
- package/dist/runtimeImplementation/runtime.execEngine/baseToolExecutorPortFactory.d.ts +3 -0
- package/dist/runtimeImplementation/runtime.execEngine/baseToolExecutorPortFactory.js +45 -7
- package/dist/runtimeImplementation/runtime.execEngine/mcpRuntimeAdapter.js +56 -0
- package/dist/runtimeImplementation/runtime.mcpPlane/index.d.ts +114 -0
- package/dist/runtimeImplementation/runtime.mcpPlane/index.js +167 -0
- package/dist/runtimeImplementation/runtime.sandboxPlane/baseToolSandboxPlanner.js +0 -2
- package/dist/runtimeImplementation/runtime.sandboxPlane/raxcellSandboxProvider.d.ts +19 -0
- package/dist/runtimeImplementation/runtime.sandboxPlane/raxcellSandboxProvider.js +172 -0
- package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxCommandRunner.d.ts +13 -1
- package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxCommandRunner.js +230 -186
- package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxPolicyMiddleware.d.ts +175 -0
- package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxPolicyMiddleware.js +142 -0
- package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxRuntimeProvider.d.ts +9 -0
- package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxRuntimeProvider.js +115 -205
- package/dist/runtimeImplementation/runtimeAgentManifest.js +7 -3
- package/package.json +3 -1
- package/raxode-tui/dist/raxode-cli/backend/agents/codingAgent/agent.js +3 -3
- package/raxode-tui/dist/raxode-cli/backend/application/backendModuleInventory.js +3 -3
- package/raxode-tui/dist/raxode-cli/backend/application/localReadinessProbe.d.ts +1 -0
- package/raxode-tui/dist/raxode-cli/backend/application/localReadinessProbe.js +50 -4
- package/raxode-tui/dist/raxode-cli/backend/application/raxcellSandboxProvider.d.ts +12 -0
- package/raxode-tui/dist/raxode-cli/backend/application/raxcellSandboxProvider.js +58 -0
- package/raxode-tui/dist/raxode-cli/backend/application/runtimeReadiness.d.ts +1 -0
- package/raxode-tui/dist/raxode-cli/backend/application/runtimeReadiness.js +3 -1
- package/raxode-tui/dist/raxode-cli/backend/application/stdioApplicationServer.d.ts +2 -0
- package/raxode-tui/dist/raxode-cli/backend/application/stdioApplicationServer.js +7 -0
- package/raxode-tui/dist/raxode-cli/backend/directApplicationBackend.d.ts +2 -0
- package/raxode-tui/dist/raxode-cli/backend/directApplicationBackend.js +21 -1
- package/raxode-tui/dist/raxode-cli/backend/raxodeBackend.d.ts +1 -1
- package/raxode-tui/dist/raxode-cli/backend/raxodeBackend.js +8 -0
- package/raxode-tui/dist/raxode-cli/frontend/tui/cli/raxode-cli.js +19 -1
- package/raxode-tui/package.json +2 -1
- package/tsconfig.json +16 -1
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* 边界:工具 handler 不感知本文件;executor port 负责把真实进程执行交给本 runner。
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { mkdir, mkdtemp, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
8
|
-
import os from "node:os";
|
|
9
7
|
import path from "node:path";
|
|
8
|
+
import { createRaxcellSandboxProvider } from "./raxcellSandboxProvider.js";
|
|
9
|
+
import { resolveRaxcellBinaryPath } from "./sandboxRuntimeProvider.js";
|
|
10
|
+
import { runSandboxPolicyMiddleware, } from "./sandboxPolicyMiddleware.js";
|
|
10
11
|
import { createWorkspaceRollbackSandboxPlan, createWorkspaceRollbackSnapshot, finalizeWorkspaceRollbackSnapshot, restoreWorkspaceRollbackSnapshot, } from "./workspaceRollbackSandbox.js";
|
|
11
12
|
export const sandboxCommandRunnerDescriptor = {
|
|
12
13
|
surface: "runtime.sandboxPlane.sandboxCommandRunner",
|
|
@@ -31,8 +32,6 @@ function truncate(value, maxBytes) {
|
|
|
31
32
|
return output;
|
|
32
33
|
}
|
|
33
34
|
function defaultModeFor(profile) {
|
|
34
|
-
if (profile === "bapr")
|
|
35
|
-
return "none";
|
|
36
35
|
if (profile === "yolo")
|
|
37
36
|
return "workspace-rollback";
|
|
38
37
|
return "isolated";
|
|
@@ -80,191 +79,10 @@ function sandboxProviderUnavailable(input, providerFamily) {
|
|
|
80
79
|
const preparedStatus = input.preparedSandbox?.probe.status ?? "not-prepared";
|
|
81
80
|
return new Error(`${providerFamily} sandbox is not ready for isolated execution (${preparedStatus})`);
|
|
82
81
|
}
|
|
83
|
-
function secretMaskScript(filesystem) {
|
|
84
|
-
if (!filesystem.protectSecrets)
|
|
85
|
-
return "";
|
|
86
|
-
const patterns = filesystem.secretGlobs.map((glob) => `-name '${glob.replaceAll("'", "'\\''")}'`).join(" -o ");
|
|
87
|
-
return `find /workspace -maxdepth 3 -type f \\( ${patterns} \\) -print0 2>/dev/null | while IFS= read -r -d '' file; do :; done`;
|
|
88
|
-
}
|
|
89
|
-
function secretPatternMatches(name) {
|
|
90
|
-
return name === ".env" || name.startsWith(".env.");
|
|
91
|
-
}
|
|
92
|
-
async function collectSecretFiles(root, current = root, depth = 0, out = []) {
|
|
93
|
-
if (depth > 4)
|
|
94
|
-
return out;
|
|
95
|
-
let entries;
|
|
96
|
-
try {
|
|
97
|
-
entries = await readdir(current, { withFileTypes: true });
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
|
-
for (const entry of entries) {
|
|
103
|
-
const absolute = path.join(current, entry.name);
|
|
104
|
-
if (entry.isDirectory()) {
|
|
105
|
-
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === ".rax_workspace")
|
|
106
|
-
continue;
|
|
107
|
-
await collectSecretFiles(root, absolute, depth + 1, out);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (!entry.isFile() || !secretPatternMatches(entry.name))
|
|
111
|
-
continue;
|
|
112
|
-
try {
|
|
113
|
-
const info = await stat(absolute);
|
|
114
|
-
if (info.isFile())
|
|
115
|
-
out.push(path.relative(root, absolute).split(path.sep).join("/"));
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// Ignore files that disappeared between directory scan and stat.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
82
|
function isInsidePath(root, candidate) {
|
|
124
83
|
const relative = path.relative(root, candidate);
|
|
125
84
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
126
85
|
}
|
|
127
|
-
function sandboxPathForRoot(root, workspaceRoot) {
|
|
128
|
-
if (root === workspaceRoot)
|
|
129
|
-
return "/workspace";
|
|
130
|
-
if (isInsidePath(workspaceRoot, root)) {
|
|
131
|
-
return path.posix.join("/workspace", path.relative(workspaceRoot, root).split(path.sep).join("/"));
|
|
132
|
-
}
|
|
133
|
-
return root;
|
|
134
|
-
}
|
|
135
|
-
function sandboxCwdForHost(hostCwd, filesystem) {
|
|
136
|
-
const roots = [...new Set([
|
|
137
|
-
filesystem.workspaceRoot,
|
|
138
|
-
...filesystem.allowedReadRoots,
|
|
139
|
-
...filesystem.allowedWriteRoots,
|
|
140
|
-
])].sort((left, right) => right.length - left.length);
|
|
141
|
-
for (const root of roots) {
|
|
142
|
-
if (!isInsidePath(root, hostCwd))
|
|
143
|
-
continue;
|
|
144
|
-
const rootDest = sandboxPathForRoot(root, filesystem.workspaceRoot);
|
|
145
|
-
const relative = path.relative(root, hostCwd);
|
|
146
|
-
if (relative === "")
|
|
147
|
-
return rootDest;
|
|
148
|
-
return path.posix.join(rootDest, relative.split(path.sep).join("/"));
|
|
149
|
-
}
|
|
150
|
-
throw new Error(`sandbox cwd ${hostCwd} is outside sandbox filesystem roots`);
|
|
151
|
-
}
|
|
152
|
-
function sandboxPathParentDirs(target) {
|
|
153
|
-
const normalized = path.posix.resolve(target.split(path.sep).join(path.posix.sep));
|
|
154
|
-
if (normalized === "/workspace" || normalized.startsWith("/workspace/"))
|
|
155
|
-
return [];
|
|
156
|
-
const segments = normalized.split("/").filter(Boolean);
|
|
157
|
-
const dirs = [];
|
|
158
|
-
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
159
|
-
const dir = `/${segments.slice(0, index + 1).join("/")}`;
|
|
160
|
-
if (dir === "/tmp" || dir === "/dev" || dir === "/proc" || dir === "/usr" || dir === "/bin" || dir === "/lib" || dir === "/lib64" || dir === "/etc")
|
|
161
|
-
continue;
|
|
162
|
-
dirs.push(dir);
|
|
163
|
-
}
|
|
164
|
-
return dirs;
|
|
165
|
-
}
|
|
166
|
-
async function linuxBubblewrapPlan(input, base) {
|
|
167
|
-
if (process.platform !== "linux") {
|
|
168
|
-
return {
|
|
169
|
-
program: input.command,
|
|
170
|
-
args: input.args ?? [],
|
|
171
|
-
metadata: { ...base.metadata, providerUnavailable: "linux-bubblewrap is only executable on Linux" },
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const tempDir = await mkdtemp(path.join(os.tmpdir(), "praxis-bwrap-"));
|
|
175
|
-
const home = path.join(tempDir, "home");
|
|
176
|
-
const tmp = path.join(tempDir, "tmp");
|
|
177
|
-
await Promise.all([mkdir(home, { recursive: true }), mkdir(tmp, { recursive: true })]);
|
|
178
|
-
const command = [input.command, ...(input.args ?? [])];
|
|
179
|
-
const networkArgs = base.network === "allow" ? ["--share-net"] : ["--unshare-net"];
|
|
180
|
-
const sandboxCwd = sandboxCwdForHost(base.cwd, base.filesystem);
|
|
181
|
-
const mountArgs = [
|
|
182
|
-
"--unshare-pid",
|
|
183
|
-
"--unshare-ipc",
|
|
184
|
-
"--unshare-uts",
|
|
185
|
-
...networkArgs,
|
|
186
|
-
"--die-with-parent",
|
|
187
|
-
"--new-session",
|
|
188
|
-
"--ro-bind-try",
|
|
189
|
-
"/usr",
|
|
190
|
-
"/usr",
|
|
191
|
-
"--ro-bind-try",
|
|
192
|
-
"/bin",
|
|
193
|
-
"/bin",
|
|
194
|
-
"--ro-bind-try",
|
|
195
|
-
"/lib",
|
|
196
|
-
"/lib",
|
|
197
|
-
"--ro-bind-try",
|
|
198
|
-
"/lib64",
|
|
199
|
-
"/lib64",
|
|
200
|
-
"--ro-bind-try",
|
|
201
|
-
"/etc",
|
|
202
|
-
"/etc",
|
|
203
|
-
"--dev",
|
|
204
|
-
"/dev",
|
|
205
|
-
"--proc",
|
|
206
|
-
"/proc",
|
|
207
|
-
"--tmpfs",
|
|
208
|
-
"/tmp",
|
|
209
|
-
"--bind",
|
|
210
|
-
home,
|
|
211
|
-
"/sandbox-home",
|
|
212
|
-
"--setenv",
|
|
213
|
-
"HOME",
|
|
214
|
-
"/sandbox-home",
|
|
215
|
-
"--setenv",
|
|
216
|
-
"TMPDIR",
|
|
217
|
-
"/tmp",
|
|
218
|
-
"--setenv",
|
|
219
|
-
"PRAXIS_SANDBOX",
|
|
220
|
-
"linux-bubblewrap",
|
|
221
|
-
"--chdir",
|
|
222
|
-
sandboxCwd,
|
|
223
|
-
];
|
|
224
|
-
const workspaceMountFlag = base.filesystem.readonlyRoot ? "--ro-bind" : "--bind";
|
|
225
|
-
mountArgs.push(workspaceMountFlag, base.filesystem.workspaceRoot, "/workspace");
|
|
226
|
-
const mountedReadTargets = new Set(["/workspace"]);
|
|
227
|
-
const mountedWriteTargets = new Set();
|
|
228
|
-
const writableDestinations = new Set(base.filesystem.allowedWriteRoots.map((root) => sandboxPathForRoot(root, base.filesystem.workspaceRoot)));
|
|
229
|
-
for (const readRoot of base.filesystem.allowedReadRoots) {
|
|
230
|
-
const dest = sandboxPathForRoot(readRoot, base.filesystem.workspaceRoot);
|
|
231
|
-
if (writableDestinations.has(dest))
|
|
232
|
-
continue;
|
|
233
|
-
if (mountedReadTargets.has(dest) || mountedWriteTargets.has(dest))
|
|
234
|
-
continue;
|
|
235
|
-
if (dest !== "/workspace") {
|
|
236
|
-
for (const parent of sandboxPathParentDirs(dest))
|
|
237
|
-
mountArgs.push("--dir", parent);
|
|
238
|
-
mountArgs.push("--ro-bind-try", readRoot, dest);
|
|
239
|
-
}
|
|
240
|
-
mountedReadTargets.add(dest);
|
|
241
|
-
}
|
|
242
|
-
for (const writeRoot of base.filesystem.allowedWriteRoots) {
|
|
243
|
-
if (writeRoot === base.filesystem.workspaceRoot)
|
|
244
|
-
continue;
|
|
245
|
-
const dest = sandboxPathForRoot(writeRoot, base.filesystem.workspaceRoot);
|
|
246
|
-
if (mountedWriteTargets.has(dest))
|
|
247
|
-
continue;
|
|
248
|
-
for (const parent of sandboxPathParentDirs(dest))
|
|
249
|
-
mountArgs.push("--dir", parent);
|
|
250
|
-
mountArgs.push("--bind-try", writeRoot, dest);
|
|
251
|
-
mountedWriteTargets.add(dest);
|
|
252
|
-
}
|
|
253
|
-
if (base.filesystem.protectSecrets) {
|
|
254
|
-
const mask = path.join(tempDir, "secret-mask");
|
|
255
|
-
await writeFile(mask, "", { mode: 0o000 });
|
|
256
|
-
const secretFiles = await collectSecretFiles(base.filesystem.workspaceRoot);
|
|
257
|
-
for (const name of secretFiles)
|
|
258
|
-
mountArgs.push("--ro-bind-try", mask, path.posix.join("/workspace", name));
|
|
259
|
-
}
|
|
260
|
-
mountArgs.push("/usr/bin/env", ...command);
|
|
261
|
-
return {
|
|
262
|
-
program: "bwrap",
|
|
263
|
-
args: mountArgs,
|
|
264
|
-
cleanup: async () => { await rm(tempDir, { recursive: true, force: true }); },
|
|
265
|
-
metadata: { ...base.metadata, tempDir, secretMaskScript: secretMaskScript(base.filesystem) },
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
86
|
function seatbeltString(value) {
|
|
269
87
|
return value
|
|
270
88
|
.replace(/\\/gu, "\\\\")
|
|
@@ -352,7 +170,7 @@ export async function createSandboxCommandPlan(input) {
|
|
|
352
170
|
if (providerFamily === "linux-bubblewrap") {
|
|
353
171
|
if (input.preparedSandbox?.ready !== true)
|
|
354
172
|
throw sandboxProviderUnavailable(input, providerFamily);
|
|
355
|
-
plan = { ...base,
|
|
173
|
+
plan = { ...base, program: input.command, args: input.args ?? [] };
|
|
356
174
|
}
|
|
357
175
|
else if (providerFamily === "macos-containerization") {
|
|
358
176
|
if (input.preparedSandbox !== undefined && input.preparedSandbox.ready !== true)
|
|
@@ -412,6 +230,227 @@ function parseSandboxDenial(plan, stderr, exitCode) {
|
|
|
412
230
|
}
|
|
413
231
|
return undefined;
|
|
414
232
|
}
|
|
233
|
+
function providerFromOptions(options) {
|
|
234
|
+
if (options.sandboxProvider !== undefined)
|
|
235
|
+
return options.sandboxProvider;
|
|
236
|
+
const binaryPath = resolveRaxcellBinaryPath();
|
|
237
|
+
return binaryPath === undefined
|
|
238
|
+
? undefined
|
|
239
|
+
: createRaxcellSandboxProvider({ binaryPath });
|
|
240
|
+
}
|
|
241
|
+
function toProviderRunRequest(input, plan) {
|
|
242
|
+
return {
|
|
243
|
+
kind: "runtime.sandboxPlane.provider.runRequest",
|
|
244
|
+
action: {
|
|
245
|
+
actionId: input.invocationId,
|
|
246
|
+
runtimeId: input.runtimeId,
|
|
247
|
+
sessionId: input.sessionId,
|
|
248
|
+
toolId: input.toolId,
|
|
249
|
+
ownerRuntime: "praxis",
|
|
250
|
+
intentLabel: `${input.toolId} command`,
|
|
251
|
+
metadata: input.metadata ?? {},
|
|
252
|
+
},
|
|
253
|
+
command: {
|
|
254
|
+
argv: [input.command, ...(input.args ?? [])],
|
|
255
|
+
cwd: plan.cwd,
|
|
256
|
+
env: plan.env,
|
|
257
|
+
stdin: null,
|
|
258
|
+
},
|
|
259
|
+
policy: {
|
|
260
|
+
profile: input.policyProfile,
|
|
261
|
+
sandboxId: input.sandbox.sandboxId,
|
|
262
|
+
sandboxMode: plan.mode,
|
|
263
|
+
network: plan.network,
|
|
264
|
+
process: { spawn: true },
|
|
265
|
+
resources: {
|
|
266
|
+
timeoutMs: input.timeoutMs ?? input.sandbox.resourceLimits.timeoutMs,
|
|
267
|
+
maxOutputBytes: input.maxOutputBytes ?? input.sandbox.resourceLimits.maxOutputBytes,
|
|
268
|
+
maxProcesses: input.sandbox.resourceLimits.maxProcesses,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
filesystem: {
|
|
272
|
+
workspaceRoot: plan.filesystem.workspaceRoot,
|
|
273
|
+
read: plan.filesystem.allowedReadRoots,
|
|
274
|
+
write: plan.filesystem.allowedWriteRoots,
|
|
275
|
+
readonlyRoot: plan.filesystem.readonlyRoot,
|
|
276
|
+
protectSecrets: plan.filesystem.protectSecrets,
|
|
277
|
+
},
|
|
278
|
+
policyGrants: input.policyGrants ?? [],
|
|
279
|
+
fallback: { mode: "none" },
|
|
280
|
+
metadata: {
|
|
281
|
+
providerFamily: plan.providerFamily,
|
|
282
|
+
requestedProviderFamily: plan.requestedProviderFamily ?? "",
|
|
283
|
+
policyProfile: plan.policyProfile,
|
|
284
|
+
secretGlobs: plan.filesystem.secretGlobs,
|
|
285
|
+
protectSecrets: plan.filesystem.protectSecrets,
|
|
286
|
+
approvalAccepted: input.approval?.accepted === true,
|
|
287
|
+
approvalGrantedBy: input.approval?.grantedBy ?? null,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function grantAccessForGap(gap) {
|
|
292
|
+
const access = [...new Set((gap.required ?? [])
|
|
293
|
+
.map((value) => value.toLowerCase())
|
|
294
|
+
.filter((value) => value === "read" || value === "write"))];
|
|
295
|
+
return access.length > 0 ? access : ["read"];
|
|
296
|
+
}
|
|
297
|
+
function resolvePraxisDynamicShellPath(request, rawPath) {
|
|
298
|
+
const home = request.command.env.HOME ?? process.env.HOME;
|
|
299
|
+
if (home === undefined || home.trim().length === 0)
|
|
300
|
+
return undefined;
|
|
301
|
+
if (rawPath === "$HOME" || rawPath === "${HOME}" || rawPath === "~")
|
|
302
|
+
return path.resolve(home);
|
|
303
|
+
for (const prefix of ["$HOME/", "${HOME}/", "~/"]) {
|
|
304
|
+
if (rawPath.startsWith(prefix))
|
|
305
|
+
return path.resolve(home, rawPath.slice(prefix.length));
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
function replaceShellPathToken(value, rawPath, resolvedPath) {
|
|
310
|
+
return value.split(rawPath).join(resolvedPath);
|
|
311
|
+
}
|
|
312
|
+
function rewriteDynamicShellPathRequest(context) {
|
|
313
|
+
const rawPath = context.gap.path;
|
|
314
|
+
if (rawPath.trim().length === 0)
|
|
315
|
+
return undefined;
|
|
316
|
+
const resolvedPath = resolvePraxisDynamicShellPath(context.request, rawPath);
|
|
317
|
+
if (resolvedPath === undefined)
|
|
318
|
+
return undefined;
|
|
319
|
+
return {
|
|
320
|
+
...context.request,
|
|
321
|
+
command: {
|
|
322
|
+
...context.request.command,
|
|
323
|
+
argv: context.request.command.argv.map((value) => replaceShellPathToken(value, rawPath, resolvedPath)),
|
|
324
|
+
},
|
|
325
|
+
policyGrants: [
|
|
326
|
+
...context.request.policyGrants,
|
|
327
|
+
{
|
|
328
|
+
reason: context.gap.reason,
|
|
329
|
+
path: resolvedPath,
|
|
330
|
+
access: grantAccessForGap(context.gap),
|
|
331
|
+
grantedBy: typeof context.request.metadata.approvalGrantedBy === "string"
|
|
332
|
+
? context.request.metadata.approvalGrantedBy
|
|
333
|
+
: "praxis-human-approval",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function defaultEnvironmentGapDecision(context) {
|
|
339
|
+
const gap = context.environmentGap;
|
|
340
|
+
if (context.request.metadata.approvalAccepted === true && gap.reason === "path-outside-declared-roots") {
|
|
341
|
+
return {
|
|
342
|
+
type: "grant",
|
|
343
|
+
grants: [{
|
|
344
|
+
reason: gap.reason,
|
|
345
|
+
path: gap.path,
|
|
346
|
+
access: grantAccessForGap(gap),
|
|
347
|
+
grantedBy: typeof context.request.metadata.approvalGrantedBy === "string"
|
|
348
|
+
? context.request.metadata.approvalGrantedBy
|
|
349
|
+
: "praxis-human-approval",
|
|
350
|
+
}],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
if (context.request.metadata.approvalAccepted === true && gap.reason === "shell-dynamic-path-unresolved") {
|
|
354
|
+
const request = rewriteDynamicShellPathRequest({ request: context.request, gap });
|
|
355
|
+
if (request !== undefined) {
|
|
356
|
+
return { type: "rewrite", request, reason: "Praxis resolved approved dynamic shell path before sandbox lowering" };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (gap.reason !== "cwd-outside-declared-roots") {
|
|
360
|
+
return { type: "deny", reason: gap.publicSafeMessage };
|
|
361
|
+
}
|
|
362
|
+
const cwd = path.resolve(gap.path);
|
|
363
|
+
const writeRoot = context.request.filesystem.write.find((root) => isInsidePath(root, cwd));
|
|
364
|
+
if (writeRoot !== undefined) {
|
|
365
|
+
return {
|
|
366
|
+
type: "grant",
|
|
367
|
+
grants: [{ reason: gap.reason, path: gap.path, access: ["write"], grantedBy: "praxis-policy" }],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const readRoot = context.request.filesystem.read.find((root) => isInsidePath(root, cwd));
|
|
371
|
+
if (readRoot !== undefined) {
|
|
372
|
+
return {
|
|
373
|
+
type: "grant",
|
|
374
|
+
grants: [{ reason: gap.reason, path: gap.path, access: ["read"], grantedBy: "praxis-policy" }],
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return { type: "deny", reason: gap.publicSafeMessage };
|
|
378
|
+
}
|
|
379
|
+
async function runProviderSandboxCommand(input, plan, options) {
|
|
380
|
+
const provider = providerFromOptions(options);
|
|
381
|
+
if (provider === undefined) {
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
plan,
|
|
385
|
+
error: {
|
|
386
|
+
code: "SANDBOX_DENIED",
|
|
387
|
+
message: "Raxcell sandbox provider is not configured",
|
|
388
|
+
publicSafe: true,
|
|
389
|
+
denial: {
|
|
390
|
+
kind: "runtime.sandboxPlane.command.denial",
|
|
391
|
+
code: "SANDBOX_PROVIDER_UNAVAILABLE",
|
|
392
|
+
message: "Raxcell sandbox provider is not configured",
|
|
393
|
+
needsApproval: false,
|
|
394
|
+
publicSafe: true,
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
events: [...plan.events, "runtime.sandboxPlane.command.denied"],
|
|
398
|
+
metadata: { providerFamily: plan.providerFamily, providerUnavailable: "raxcell provider is not configured" },
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const middlewareResult = await runSandboxPolicyMiddleware({
|
|
402
|
+
provider,
|
|
403
|
+
request: toProviderRunRequest(input, plan),
|
|
404
|
+
decideEnvironmentGap: async ({ request, environmentGap }) => options.decideEnvironmentGap?.({ request, environmentGap }) ?? defaultEnvironmentGapDecision({ request, environmentGap }),
|
|
405
|
+
audit: options.audit,
|
|
406
|
+
});
|
|
407
|
+
if (!middlewareResult.ok) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
plan,
|
|
411
|
+
error: {
|
|
412
|
+
code: "SANDBOX_DENIED",
|
|
413
|
+
message: middlewareResult.error.message,
|
|
414
|
+
publicSafe: true,
|
|
415
|
+
denial: {
|
|
416
|
+
kind: "runtime.sandboxPlane.command.denial",
|
|
417
|
+
code: middlewareResult.error.code === "SANDBOX_PREPARE_FAILED" ? "SANDBOX_PROVIDER_UNAVAILABLE" : "SANDBOX_PERMISSION_DENIED",
|
|
418
|
+
message: middlewareResult.error.message,
|
|
419
|
+
needsApproval: false,
|
|
420
|
+
publicSafe: true,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
stdout: "",
|
|
424
|
+
stderr: "",
|
|
425
|
+
events: [...plan.events, ...middlewareResult.events, "runtime.sandboxPlane.command.denied"],
|
|
426
|
+
metadata: { providerFamily: plan.providerFamily, middlewareError: middlewareResult.error.code },
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
ok: true,
|
|
431
|
+
plan: {
|
|
432
|
+
...plan,
|
|
433
|
+
metadata: {
|
|
434
|
+
...plan.metadata,
|
|
435
|
+
providerPrepare: middlewareResult.prepared,
|
|
436
|
+
providerRequest: middlewareResult.request,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
exitCode: middlewareResult.result.exitCode ?? 0,
|
|
440
|
+
stdout: middlewareResult.result.stdout,
|
|
441
|
+
stderr: middlewareResult.result.stderr,
|
|
442
|
+
events: [...plan.events, ...middlewareResult.events, "runtime.sandboxPlane.command.completed"],
|
|
443
|
+
metadata: {
|
|
444
|
+
providerFamily: plan.providerFamily,
|
|
445
|
+
providerId: provider.providerId,
|
|
446
|
+
...(middlewareResult.result.timedOut ? { timedOut: true } : {}),
|
|
447
|
+
providerRun: {
|
|
448
|
+
denial: middlewareResult.result.denial ?? null,
|
|
449
|
+
filesystemLowering: middlewareResult.result.filesystemLowering ?? null,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
}
|
|
415
454
|
function spawnPlan(plan, input) {
|
|
416
455
|
return new Promise((resolve, reject) => {
|
|
417
456
|
const child = spawn(plan.program, [...plan.args], {
|
|
@@ -505,6 +544,11 @@ export async function runSandboxCommand(input, options = {}) {
|
|
|
505
544
|
metadata: { providerFamily: plan.providerFamily, providerUnavailable: "remote-worker adapter is not configured" },
|
|
506
545
|
};
|
|
507
546
|
}
|
|
547
|
+
if (plan.providerFamily === "linux-bubblewrap") {
|
|
548
|
+
const result = await runProviderSandboxCommand(input, plan, options);
|
|
549
|
+
await plan.cleanup?.();
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
508
552
|
const remote = plan.providerFamily === "remote-worker"
|
|
509
553
|
? await options.remoteWorker?.(input, plan)
|
|
510
554
|
: undefined;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { BaseToolPolicyProfile } from "../runtimeAgentManifest.js";
|
|
2
|
+
import type { SandboxCommandNetworkPolicy } from "./sandboxCommandRunner.js";
|
|
3
|
+
export type SandboxProviderFamily = "host-observed" | "workspace-policy" | "workspace-rollback" | "linux-bubblewrap" | "macos-containerization" | "windows-sandbox" | "remote-worker" | "external";
|
|
4
|
+
export type SandboxProviderPolicyGrant = {
|
|
5
|
+
reason: string;
|
|
6
|
+
path: string;
|
|
7
|
+
access?: readonly string[];
|
|
8
|
+
grantedBy?: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type SandboxProviderFilesystemLoweredRoot = {
|
|
11
|
+
path: string;
|
|
12
|
+
access: "read" | "write" | "runtime" | "scratch" | "runtime-link";
|
|
13
|
+
source: "declared" | "backend-runtime" | "policy-grant";
|
|
14
|
+
};
|
|
15
|
+
export type SandboxProviderFilesystemLoweringReport = {
|
|
16
|
+
declaredRoots: readonly SandboxProviderFilesystemLoweredRoot[];
|
|
17
|
+
runtimeRoots: readonly SandboxProviderFilesystemLoweredRoot[];
|
|
18
|
+
policyGrants: readonly SandboxProviderPolicyGrant[];
|
|
19
|
+
warnings: readonly {
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}[];
|
|
23
|
+
effects?: readonly {
|
|
24
|
+
path?: string;
|
|
25
|
+
pattern?: string;
|
|
26
|
+
rawToken: string;
|
|
27
|
+
access: "read" | "write" | "readwrite";
|
|
28
|
+
command: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
confidence: "high" | "medium" | "low";
|
|
31
|
+
warning?: string;
|
|
32
|
+
}[];
|
|
33
|
+
};
|
|
34
|
+
export type SandboxProviderBackendArtifact = {
|
|
35
|
+
backend: SandboxProviderFamily;
|
|
36
|
+
format: string;
|
|
37
|
+
arguments: readonly string[];
|
|
38
|
+
data: Readonly<Record<string, unknown>>;
|
|
39
|
+
warnings: readonly {
|
|
40
|
+
code: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}[];
|
|
43
|
+
};
|
|
44
|
+
export type SandboxProviderEnvironmentGap = {
|
|
45
|
+
reason: string;
|
|
46
|
+
path: string;
|
|
47
|
+
required?: readonly string[];
|
|
48
|
+
publicSafeMessage: string;
|
|
49
|
+
};
|
|
50
|
+
export type SandboxProviderDenial = {
|
|
51
|
+
code: string;
|
|
52
|
+
message: string;
|
|
53
|
+
publicSafe: true;
|
|
54
|
+
};
|
|
55
|
+
export type SandboxProviderRunRequest = {
|
|
56
|
+
kind: "runtime.sandboxPlane.provider.runRequest";
|
|
57
|
+
action: {
|
|
58
|
+
actionId: string;
|
|
59
|
+
runtimeId: string;
|
|
60
|
+
sessionId: string;
|
|
61
|
+
toolId: string;
|
|
62
|
+
ownerRuntime: "praxis" | string;
|
|
63
|
+
intentLabel: string;
|
|
64
|
+
metadata: Readonly<Record<string, unknown>>;
|
|
65
|
+
};
|
|
66
|
+
command: {
|
|
67
|
+
argv: readonly string[];
|
|
68
|
+
cwd: string;
|
|
69
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
70
|
+
stdin: string | null;
|
|
71
|
+
};
|
|
72
|
+
policy: {
|
|
73
|
+
profile: BaseToolPolicyProfile;
|
|
74
|
+
sandboxId: string;
|
|
75
|
+
sandboxMode: "none" | "workspace-rollback" | "isolated";
|
|
76
|
+
network: SandboxCommandNetworkPolicy;
|
|
77
|
+
process: Readonly<Record<string, unknown>>;
|
|
78
|
+
resources: Readonly<Record<string, unknown>>;
|
|
79
|
+
};
|
|
80
|
+
filesystem: {
|
|
81
|
+
workspaceRoot: string;
|
|
82
|
+
read: readonly string[];
|
|
83
|
+
write: readonly string[];
|
|
84
|
+
readonlyRoot: boolean;
|
|
85
|
+
protectSecrets: boolean;
|
|
86
|
+
};
|
|
87
|
+
policyGrants: readonly SandboxProviderPolicyGrant[];
|
|
88
|
+
fallback: {
|
|
89
|
+
mode: string;
|
|
90
|
+
};
|
|
91
|
+
metadata: Readonly<Record<string, unknown>>;
|
|
92
|
+
};
|
|
93
|
+
export type SandboxProviderPrepareRunResult = {
|
|
94
|
+
kind: "runtime.sandboxPlane.provider.prepareRunResult";
|
|
95
|
+
ok: boolean;
|
|
96
|
+
providerFamily: SandboxProviderFamily;
|
|
97
|
+
denial?: SandboxProviderDenial | null;
|
|
98
|
+
environmentGap?: SandboxProviderEnvironmentGap | null;
|
|
99
|
+
filesystemLowering?: SandboxProviderFilesystemLoweringReport | null;
|
|
100
|
+
backendArtifacts: readonly SandboxProviderBackendArtifact[];
|
|
101
|
+
metadata: Readonly<Record<string, unknown>>;
|
|
102
|
+
};
|
|
103
|
+
export type SandboxProviderRunResult = {
|
|
104
|
+
kind: "runtime.sandboxPlane.provider.runResult";
|
|
105
|
+
ok: boolean;
|
|
106
|
+
providerFamily: SandboxProviderFamily;
|
|
107
|
+
exitCode: number | null;
|
|
108
|
+
stdout: string;
|
|
109
|
+
stderr: string;
|
|
110
|
+
timedOut: boolean;
|
|
111
|
+
denial?: SandboxProviderDenial | null;
|
|
112
|
+
environmentGap?: SandboxProviderEnvironmentGap | null;
|
|
113
|
+
filesystemLowering?: SandboxProviderFilesystemLoweringReport | null;
|
|
114
|
+
metadata: Readonly<Record<string, unknown>>;
|
|
115
|
+
};
|
|
116
|
+
export type SandboxExecutionProviderPort = {
|
|
117
|
+
providerId: string;
|
|
118
|
+
providerFamily: SandboxProviderFamily;
|
|
119
|
+
prepareRun(request: SandboxProviderRunRequest): Promise<SandboxProviderPrepareRunResult>;
|
|
120
|
+
run(request: SandboxProviderRunRequest): Promise<SandboxProviderRunResult>;
|
|
121
|
+
};
|
|
122
|
+
export type SandboxPolicyMiddlewareEnvironmentGapDecision = {
|
|
123
|
+
type: "grant";
|
|
124
|
+
grants: readonly SandboxProviderPolicyGrant[];
|
|
125
|
+
} | {
|
|
126
|
+
type: "rewrite";
|
|
127
|
+
request: SandboxProviderRunRequest;
|
|
128
|
+
reason: string;
|
|
129
|
+
} | {
|
|
130
|
+
type: "deny";
|
|
131
|
+
reason: string;
|
|
132
|
+
};
|
|
133
|
+
export type SandboxPolicyMiddlewareResult = {
|
|
134
|
+
ok: true;
|
|
135
|
+
request: SandboxProviderRunRequest;
|
|
136
|
+
prepared: SandboxProviderPrepareRunResult;
|
|
137
|
+
result: SandboxProviderRunResult;
|
|
138
|
+
events: readonly string[];
|
|
139
|
+
} | {
|
|
140
|
+
ok: false;
|
|
141
|
+
request: SandboxProviderRunRequest;
|
|
142
|
+
prepared?: SandboxProviderPrepareRunResult;
|
|
143
|
+
error: {
|
|
144
|
+
code: "SANDBOX_PREPARE_FAILED" | "SANDBOX_DENIED" | "SANDBOX_RUN_FAILED";
|
|
145
|
+
message: string;
|
|
146
|
+
publicSafe: true;
|
|
147
|
+
denial?: SandboxProviderDenial | null;
|
|
148
|
+
};
|
|
149
|
+
events: readonly string[];
|
|
150
|
+
};
|
|
151
|
+
export type SandboxPolicyMiddlewareAuditEvent = {
|
|
152
|
+
type: string;
|
|
153
|
+
actionId: string;
|
|
154
|
+
sessionId: string;
|
|
155
|
+
toolId: string;
|
|
156
|
+
providerId: string;
|
|
157
|
+
providerFamily: SandboxProviderFamily;
|
|
158
|
+
payload: Readonly<Record<string, unknown>>;
|
|
159
|
+
};
|
|
160
|
+
export declare const sandboxPolicyMiddlewareDescriptor: {
|
|
161
|
+
readonly surface: "runtime.sandboxPlane.sandboxPolicyMiddleware";
|
|
162
|
+
readonly policyOwner: "praxis";
|
|
163
|
+
readonly providerRole: "environment-and-execution";
|
|
164
|
+
readonly publicSafe: true;
|
|
165
|
+
};
|
|
166
|
+
export declare function runSandboxPolicyMiddleware(input: {
|
|
167
|
+
provider: SandboxExecutionProviderPort;
|
|
168
|
+
request: SandboxProviderRunRequest;
|
|
169
|
+
decideEnvironmentGap?: (context: {
|
|
170
|
+
request: SandboxProviderRunRequest;
|
|
171
|
+
prepared: SandboxProviderPrepareRunResult;
|
|
172
|
+
environmentGap: SandboxProviderEnvironmentGap;
|
|
173
|
+
}) => Promise<SandboxPolicyMiddlewareEnvironmentGapDecision> | SandboxPolicyMiddlewareEnvironmentGapDecision;
|
|
174
|
+
audit?: (event: SandboxPolicyMiddlewareAuditEvent) => Promise<void> | void;
|
|
175
|
+
}): Promise<SandboxPolicyMiddlewareResult>;
|