@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.
- 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 +13 -1
- package/dist/applicationLayer/applicationRuntime.js +39 -3
- package/dist/applicationLayer/index.d.ts +2 -0
- package/dist/applicationLayer/index.js +1 -0
- package/dist/basetool/core/shellRun.js +6 -1
- package/dist/rax_packageManager/raxCli.js +42 -1
- package/dist/runtimeImplementation/praxisRuntimeKernel.d.ts +13 -0
- package/dist/runtimeImplementation/praxisRuntimeKernel.js +550 -15
- 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 +225 -0
- package/dist/runtimeImplementation/runtime.mcpPlane/index.js +549 -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
|
@@ -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
|
|
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,
|
|
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;
|