@praxis-ai/praxis 0.1.1 → 0.1.3

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.
Files changed (49) hide show
  1. package/dist/agentCore/index.d.ts +45 -6
  2. package/dist/agentCore/index.js +14 -2
  3. package/dist/applicationLayer/applicationContract.d.ts +2 -0
  4. package/dist/applicationLayer/applicationRuntime.d.ts +13 -1
  5. package/dist/applicationLayer/applicationRuntime.js +39 -3
  6. package/dist/applicationLayer/index.d.ts +2 -0
  7. package/dist/applicationLayer/index.js +1 -0
  8. package/dist/basetool/core/shellRun.js +6 -1
  9. package/dist/rax_packageManager/raxCli.js +42 -1
  10. package/dist/runtimeImplementation/praxisRuntimeKernel.d.ts +13 -0
  11. package/dist/runtimeImplementation/praxisRuntimeKernel.js +550 -15
  12. package/dist/runtimeImplementation/runtime.componentPlane/runtimeComponentRegistry.d.ts +1 -1
  13. package/dist/runtimeImplementation/runtime.componentPlane/runtimeComponentRegistry.js +2 -2
  14. package/dist/runtimeImplementation/runtime.dependencyPlane/dependencySourceRegistry.d.ts +1 -1
  15. package/dist/runtimeImplementation/runtime.dependencyPlane/dependencySourceRegistry.js +12 -0
  16. package/dist/runtimeImplementation/runtime.dependencyPlane/dependencyTypes.js +2 -0
  17. package/dist/runtimeImplementation/runtime.execEngine/baseToolExecutorPortFactory.d.ts +3 -0
  18. package/dist/runtimeImplementation/runtime.execEngine/baseToolExecutorPortFactory.js +45 -7
  19. package/dist/runtimeImplementation/runtime.execEngine/mcpRuntimeAdapter.js +56 -0
  20. package/dist/runtimeImplementation/runtime.mcpPlane/index.d.ts +225 -0
  21. package/dist/runtimeImplementation/runtime.mcpPlane/index.js +549 -0
  22. package/dist/runtimeImplementation/runtime.sandboxPlane/baseToolSandboxPlanner.js +0 -2
  23. package/dist/runtimeImplementation/runtime.sandboxPlane/raxcellSandboxProvider.d.ts +19 -0
  24. package/dist/runtimeImplementation/runtime.sandboxPlane/raxcellSandboxProvider.js +172 -0
  25. package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxCommandRunner.d.ts +13 -1
  26. package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxCommandRunner.js +230 -186
  27. package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxPolicyMiddleware.d.ts +175 -0
  28. package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxPolicyMiddleware.js +142 -0
  29. package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxRuntimeProvider.d.ts +9 -0
  30. package/dist/runtimeImplementation/runtime.sandboxPlane/sandboxRuntimeProvider.js +115 -205
  31. package/dist/runtimeImplementation/runtimeAgentManifest.js +7 -3
  32. package/package.json +3 -1
  33. package/raxode-tui/dist/raxode-cli/backend/agents/codingAgent/agent.js +3 -3
  34. package/raxode-tui/dist/raxode-cli/backend/application/backendModuleInventory.js +3 -3
  35. package/raxode-tui/dist/raxode-cli/backend/application/localReadinessProbe.d.ts +1 -0
  36. package/raxode-tui/dist/raxode-cli/backend/application/localReadinessProbe.js +50 -4
  37. package/raxode-tui/dist/raxode-cli/backend/application/raxcellSandboxProvider.d.ts +12 -0
  38. package/raxode-tui/dist/raxode-cli/backend/application/raxcellSandboxProvider.js +58 -0
  39. package/raxode-tui/dist/raxode-cli/backend/application/runtimeReadiness.d.ts +1 -0
  40. package/raxode-tui/dist/raxode-cli/backend/application/runtimeReadiness.js +3 -1
  41. package/raxode-tui/dist/raxode-cli/backend/application/stdioApplicationServer.d.ts +2 -0
  42. package/raxode-tui/dist/raxode-cli/backend/application/stdioApplicationServer.js +7 -0
  43. package/raxode-tui/dist/raxode-cli/backend/directApplicationBackend.d.ts +2 -0
  44. package/raxode-tui/dist/raxode-cli/backend/directApplicationBackend.js +21 -1
  45. package/raxode-tui/dist/raxode-cli/backend/raxodeBackend.d.ts +1 -1
  46. package/raxode-tui/dist/raxode-cli/backend/raxodeBackend.js +8 -0
  47. package/raxode-tui/dist/raxode-cli/frontend/tui/cli/raxode-cli.js +19 -1
  48. package/raxode-tui/package.json +2 -1
  49. package/tsconfig.json +16 -1
@@ -0,0 +1,172 @@
1
+ /*
2
+ * 文件定位:Agent 运行态实现层 / Raxcell 官方沙箱 provider。
3
+ * 核心目的:把 Praxis sandbox provider-neutral request 映射到 Raxcell prepareRun/run 协议。
4
+ * 边界:Raxcell 只提供环境事实与执行;策略、审批、fallback 决策仍归 Praxis middleware。
5
+ */
6
+ import { RaxcellClient } from "@praxis-ai/raxcell";
7
+ export const raxcellSandboxProviderDescriptor = {
8
+ surface: "runtime.sandboxPlane.raxcellSandboxProvider",
9
+ providerFamily: "linux-bubblewrap",
10
+ policyOwner: "praxis",
11
+ role: "environment-and-execution",
12
+ };
13
+ function clientFromOptions(options) {
14
+ if ("client" in options)
15
+ return options.client;
16
+ return new RaxcellClient({ binaryPath: options.binaryPath });
17
+ }
18
+ function cleanEnv(env) {
19
+ const output = {};
20
+ for (const [key, value] of Object.entries(env)) {
21
+ if (value !== undefined)
22
+ output[key] = value;
23
+ }
24
+ return output;
25
+ }
26
+ function networkForRaxcell(value) {
27
+ if (value === "allow" || value === "deny")
28
+ return value;
29
+ return "deny";
30
+ }
31
+ function grantForRaxcell(grant) {
32
+ return {
33
+ reason: grant.reason,
34
+ path: grant.path,
35
+ access: grant.access === undefined ? undefined : [...grant.access],
36
+ grantedBy: grant.grantedBy ?? null,
37
+ };
38
+ }
39
+ export function mapSandboxProviderRequestToRaxcell(request) {
40
+ return {
41
+ kind: "raxcell.run.v1",
42
+ backendPreference: ["linux-bubblewrap"],
43
+ policyGrants: request.policyGrants.map(grantForRaxcell),
44
+ action: {
45
+ actionId: request.action.actionId,
46
+ ownerRuntime: request.action.ownerRuntime,
47
+ intentLabel: request.action.intentLabel,
48
+ metadata: {
49
+ runtimeId: request.action.runtimeId,
50
+ sessionId: request.action.sessionId,
51
+ toolId: request.action.toolId,
52
+ policyProfile: request.policy.profile,
53
+ sandboxId: request.policy.sandboxId,
54
+ sandboxMode: request.policy.sandboxMode,
55
+ ...request.action.metadata,
56
+ ...request.metadata,
57
+ },
58
+ },
59
+ command: {
60
+ argv: [...request.command.argv],
61
+ cwd: request.command.cwd,
62
+ env: cleanEnv(request.command.env),
63
+ stdin: request.command.stdin,
64
+ },
65
+ enforcement: {
66
+ profile: request.policy.profile,
67
+ filesystem: {
68
+ read: [...request.filesystem.read],
69
+ write: [...request.filesystem.write],
70
+ },
71
+ network: networkForRaxcell(request.policy.network),
72
+ process: request.policy.process,
73
+ resources: request.policy.resources,
74
+ },
75
+ fallback: request.fallback,
76
+ };
77
+ }
78
+ function providerFamily(value) {
79
+ if (value === "linux-bubblewrap")
80
+ return "linux-bubblewrap";
81
+ if (value === "host-observed")
82
+ return "host-observed";
83
+ if (value === "external")
84
+ return "external";
85
+ return "external";
86
+ }
87
+ function denial(value) {
88
+ if (value === null || value === undefined)
89
+ return null;
90
+ return {
91
+ code: value.code,
92
+ message: value.message,
93
+ publicSafe: true,
94
+ };
95
+ }
96
+ function environmentGap(value) {
97
+ if (value === null || value === undefined)
98
+ return null;
99
+ return {
100
+ reason: value.reason,
101
+ path: value.path ?? "",
102
+ required: value.required,
103
+ publicSafeMessage: value.publicSafeMessage,
104
+ };
105
+ }
106
+ function filesystemLowering(value) {
107
+ if (value === null || value === undefined)
108
+ return null;
109
+ return {
110
+ declaredRoots: value.declaredRoots,
111
+ runtimeRoots: value.runtimeRoots,
112
+ policyGrants: value.policyGrants,
113
+ warnings: value.warnings,
114
+ effects: value.effects,
115
+ };
116
+ }
117
+ function backendArtifacts(value) {
118
+ return value.map((artifact) => ({
119
+ backend: providerFamily(artifact.backend),
120
+ format: artifact.format,
121
+ arguments: artifact.arguments,
122
+ data: artifact.data,
123
+ warnings: artifact.warnings,
124
+ }));
125
+ }
126
+ function prepareResult(value) {
127
+ return {
128
+ kind: "runtime.sandboxPlane.provider.prepareRunResult",
129
+ ok: value.ok,
130
+ providerFamily: providerFamily(value.backend),
131
+ denial: denial(value.denial),
132
+ environmentGap: environmentGap(value.environmentGap ?? value.policyDecision),
133
+ filesystemLowering: filesystemLowering(value.filesystemLowering),
134
+ backendArtifacts: backendArtifacts(value.backendArtifacts),
135
+ metadata: {
136
+ raxcellKind: value.kind,
137
+ capabilityReport: value.capabilityReport,
138
+ },
139
+ };
140
+ }
141
+ function runResult(value) {
142
+ return {
143
+ kind: "runtime.sandboxPlane.provider.runResult",
144
+ ok: value.ok,
145
+ providerFamily: providerFamily(value.backend),
146
+ exitCode: value.exitCode,
147
+ stdout: value.stdout,
148
+ stderr: value.stderr,
149
+ timedOut: value.timedOut,
150
+ denial: denial(value.denial),
151
+ environmentGap: environmentGap(value.environmentGap ?? value.policyDecision),
152
+ filesystemLowering: filesystemLowering(value.filesystemLowering),
153
+ metadata: {
154
+ raxcellKind: value.kind,
155
+ fallback: value.fallback,
156
+ capabilityReport: value.capabilityReport,
157
+ },
158
+ };
159
+ }
160
+ export function createRaxcellSandboxProvider(options) {
161
+ const client = clientFromOptions(options);
162
+ return {
163
+ providerId: options.providerId ?? "raxcell",
164
+ providerFamily: "linux-bubblewrap",
165
+ async prepareRun(request) {
166
+ return prepareResult(await client.prepareRun(mapSandboxProviderRequestToRaxcell(request)));
167
+ },
168
+ async run(request) {
169
+ return runResult(await client.run(mapSandboxProviderRequestToRaxcell(request)));
170
+ },
171
+ };
172
+ }
@@ -1,5 +1,6 @@
1
1
  import type { BaseToolPolicyProfile, SandboxSpec } from "../runtimeAgentManifest.js";
2
- import type { SandboxRuntimePrepareResult } from "./sandboxRuntimeProvider.js";
2
+ import { type SandboxRuntimePrepareResult } from "./sandboxRuntimeProvider.js";
3
+ import { type SandboxExecutionProviderPort, type SandboxPolicyMiddlewareAuditEvent, type SandboxPolicyMiddlewareEnvironmentGapDecision, type SandboxProviderEnvironmentGap, type SandboxProviderPolicyGrant, type SandboxProviderRunRequest } from "./sandboxPolicyMiddleware.js";
3
4
  import { type WorkspaceRollbackFinalizeResult, type WorkspaceRollbackSnapshot } from "./workspaceRollbackSandbox.js";
4
5
  export type SandboxCommandProviderFamily = "host-observed" | "workspace-policy" | "workspace-rollback" | "linux-bubblewrap" | "macos-containerization" | "windows-sandbox" | "remote-worker";
5
6
  export type SandboxCommandNetworkPolicy = "deny" | "allow" | "approval" | "provider-policy";
@@ -28,6 +29,11 @@ export type SandboxCommandRequest = {
28
29
  sandboxMode?: "none" | "workspace-rollback" | "isolated";
29
30
  filesystem?: Partial<SandboxCommandFilesystemPolicy>;
30
31
  network?: SandboxCommandNetworkPolicy;
32
+ policyGrants?: readonly SandboxProviderPolicyGrant[];
33
+ approval?: {
34
+ accepted: boolean;
35
+ grantedBy?: string;
36
+ };
31
37
  metadata?: Readonly<Record<string, unknown>>;
32
38
  };
33
39
  export type SandboxCommandPlan = {
@@ -95,6 +101,12 @@ export type SandboxRemoteWorkerAdapter = (request: SandboxCommandRequest, plan:
95
101
  }>;
96
102
  export type SandboxCommandRunnerOptions = {
97
103
  remoteWorker?: SandboxRemoteWorkerAdapter;
104
+ sandboxProvider?: SandboxExecutionProviderPort;
105
+ decideEnvironmentGap?: (context: {
106
+ request: SandboxProviderRunRequest;
107
+ environmentGap: SandboxProviderEnvironmentGap;
108
+ }) => Promise<SandboxPolicyMiddlewareEnvironmentGapDecision> | SandboxPolicyMiddlewareEnvironmentGapDecision;
109
+ audit?: (event: SandboxPolicyMiddlewareAuditEvent) => Promise<void> | void;
98
110
  };
99
111
  export declare const sandboxCommandRunnerDescriptor: {
100
112
  readonly surface: "runtime.sandboxPlane.sandboxCommandRunner";
@@ -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, ...(await linuxBubblewrapPlan(input, 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;