@fractalcode/bridge 0.1.0
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/LICENSE +201 -0
- package/NOTICE +11 -0
- package/README.md +69 -0
- package/dist/src/cli.d.ts +16 -0
- package/dist/src/cli.js +241 -0
- package/dist/src/compiler/index.d.ts +26 -0
- package/dist/src/compiler/index.js +137 -0
- package/dist/src/compiler/mappings.d.ts +31 -0
- package/dist/src/compiler/mappings.js +316 -0
- package/dist/src/compiler/serialize.d.ts +23 -0
- package/dist/src/compiler/serialize.js +119 -0
- package/dist/src/compiler/types.d.ts +83 -0
- package/dist/src/compiler/types.js +9 -0
- package/dist/src/prover/index.d.ts +63 -0
- package/dist/src/prover/index.js +185 -0
- package/package.json +40 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fractal-openshell-bridge — type definitions
|
|
3
|
+
*
|
|
4
|
+
* Input: Fractal `.channels.json` sidecar shape (plain JSON, no Fractal dependency).
|
|
5
|
+
* Output: OpenShell `PolicyFile` shape (mirrors the serde structs in
|
|
6
|
+
* openshell-policy/src/lib.rs).
|
|
7
|
+
*/
|
|
8
|
+
/** One typed channel declaration as written by Fractal at finalize time. */
|
|
9
|
+
export interface TypedChannelConfig {
|
|
10
|
+
/** Cell's handle for the channel, e.g. "file", "llm", "tests". */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Channel implementation kind, e.g. "file", "http", "anthropic". */
|
|
13
|
+
kind: string;
|
|
14
|
+
/** Frozen scope object; shape depends on `kind`. */
|
|
15
|
+
scope: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface FilesystemDef {
|
|
18
|
+
include_workdir: boolean;
|
|
19
|
+
read_only: string[];
|
|
20
|
+
read_write: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface LandlockDef {
|
|
23
|
+
compatibility: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ProcessDef {
|
|
26
|
+
run_as_user: string;
|
|
27
|
+
run_as_group: string;
|
|
28
|
+
}
|
|
29
|
+
export interface L7AllowDef {
|
|
30
|
+
method: string;
|
|
31
|
+
path?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface L7RuleDef {
|
|
34
|
+
allow: L7AllowDef;
|
|
35
|
+
}
|
|
36
|
+
export interface NetworkEndpointDef {
|
|
37
|
+
host: string;
|
|
38
|
+
port?: number;
|
|
39
|
+
ports?: number[];
|
|
40
|
+
protocol: string;
|
|
41
|
+
tls?: string;
|
|
42
|
+
enforcement: string;
|
|
43
|
+
access?: string;
|
|
44
|
+
rules?: L7RuleDef[];
|
|
45
|
+
}
|
|
46
|
+
export interface NetworkBinaryDef {
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
export interface NetworkPolicyRuleDef {
|
|
50
|
+
name: string;
|
|
51
|
+
endpoints: NetworkEndpointDef[];
|
|
52
|
+
binaries: NetworkBinaryDef[];
|
|
53
|
+
}
|
|
54
|
+
export interface PolicyFile {
|
|
55
|
+
version: 1;
|
|
56
|
+
filesystem_policy?: FilesystemDef;
|
|
57
|
+
landlock?: LandlockDef;
|
|
58
|
+
process?: ProcessDef;
|
|
59
|
+
network_policies: Record<string, NetworkPolicyRuleDef>;
|
|
60
|
+
}
|
|
61
|
+
export interface AuditLog {
|
|
62
|
+
/** Epoch milliseconds — supplied by the caller, NOT an input to policyHash. */
|
|
63
|
+
timestamp: number;
|
|
64
|
+
channelsProcessed: number;
|
|
65
|
+
skippedChannels: string[];
|
|
66
|
+
/** Human-readable notes about mapping limitations (e.g. protectedPaths carve-outs). */
|
|
67
|
+
anomaliesDetected: string[];
|
|
68
|
+
}
|
|
69
|
+
export interface CompilationResultOk {
|
|
70
|
+
success: true;
|
|
71
|
+
policyYaml: string;
|
|
72
|
+
/** SHA-256 of policyYaml only. Deterministic across runs for identical input. */
|
|
73
|
+
policyHash: string;
|
|
74
|
+
auditLog: AuditLog;
|
|
75
|
+
}
|
|
76
|
+
export interface CompilationResultErr {
|
|
77
|
+
success: false;
|
|
78
|
+
/** Stable machine-readable code, e.g. "input-too-large", "parse-depth-exceeded". */
|
|
79
|
+
code: string;
|
|
80
|
+
/** Human-readable error message. No stack trace (caller logs that separately). */
|
|
81
|
+
error: string;
|
|
82
|
+
}
|
|
83
|
+
export type CompilationResult = CompilationResultOk | CompilationResultErr;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* fractal-openshell-bridge — type definitions
|
|
4
|
+
*
|
|
5
|
+
* Input: Fractal `.channels.json` sidecar shape (plain JSON, no Fractal dependency).
|
|
6
|
+
* Output: OpenShell `PolicyFile` shape (mirrors the serde structs in
|
|
7
|
+
* openshell-policy/src/lib.rs).
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fractal-openshell-bridge — openshell-prover subprocess wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Invokes the prover with `shell: false` and an arg array — zero sidecar-derived
|
|
5
|
+
* data reaches a shell context (red-team #4). Exit-code handling is explicit
|
|
6
|
+
* (red-team #1, corrected):
|
|
7
|
+
* - flag NOT set → not-enabled (no-op)
|
|
8
|
+
* - binary absent → fail-closed (the operator declared the constraint;
|
|
9
|
+
* an attacker who deletes the binary or mutates $PATH
|
|
10
|
+
* must not silently disable the gate)
|
|
11
|
+
* - exit 0 → pass
|
|
12
|
+
* - exit 1 → fail (critical/high findings)
|
|
13
|
+
* - exit 2 → input error (bad YAML / missing files)
|
|
14
|
+
* - any other non-zero → crash → fail-closed (a crashing prover is not passing)
|
|
15
|
+
*
|
|
16
|
+
* The spawn function is injectable so tests can exercise every exit path without
|
|
17
|
+
* requiring openshell-prover to be installed.
|
|
18
|
+
*/
|
|
19
|
+
export interface ProverArgs {
|
|
20
|
+
policyPath: string;
|
|
21
|
+
credentialsPath: string;
|
|
22
|
+
registryPath?: string;
|
|
23
|
+
acceptedRisksPath?: string;
|
|
24
|
+
compact?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type ProverOutcome = "not-enabled" | "pass" | "fail" | "input-error" | "binary-absent-fail-closed" | "crash";
|
|
27
|
+
export interface ProverResult {
|
|
28
|
+
outcome: ProverOutcome;
|
|
29
|
+
/** Raw exit code from the prover, or null if it didn't exit normally. */
|
|
30
|
+
exitCode: number | null;
|
|
31
|
+
stdout: string;
|
|
32
|
+
stderr: string;
|
|
33
|
+
/** Parsed finding lines (compact mode), when present. */
|
|
34
|
+
findings: string[];
|
|
35
|
+
/** Human-readable detail for non-pass outcomes. */
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Default invocation: `openshell prove --policy ...`. Override for a standalone binary. */
|
|
39
|
+
export declare const DEFAULT_PROVER_COMMAND: string[];
|
|
40
|
+
export interface SpawnOutput {
|
|
41
|
+
code: number | null;
|
|
42
|
+
stdout: string;
|
|
43
|
+
stderr: string;
|
|
44
|
+
spawnError?: NodeJS.ErrnoException;
|
|
45
|
+
}
|
|
46
|
+
/** Injectable spawn implementation. */
|
|
47
|
+
export type SpawnFn = (command: string[], args: string[]) => Promise<SpawnOutput>;
|
|
48
|
+
/** Production spawn using child_process.spawn with shell:false. */
|
|
49
|
+
export declare const defaultSpawn: SpawnFn;
|
|
50
|
+
/**
|
|
51
|
+
* Run the openshell-prover against a compiled policy + credentials.
|
|
52
|
+
*
|
|
53
|
+
* @param args Policy/credentials paths + options.
|
|
54
|
+
* @param opts flagEnabled (operator declared the constraint), injectable
|
|
55
|
+
* spawn fn and prover command for testing.
|
|
56
|
+
*/
|
|
57
|
+
export declare function runProver(args: ProverArgs, opts: {
|
|
58
|
+
flagEnabled: boolean;
|
|
59
|
+
spawnFn?: SpawnFn;
|
|
60
|
+
proverCommand?: string[];
|
|
61
|
+
}): Promise<ProverResult>;
|
|
62
|
+
/** True if the prover result blocks finalization. */
|
|
63
|
+
export declare function blocksFinalization(r: ProverResult): boolean;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* fractal-openshell-bridge — openshell-prover subprocess wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Invokes the prover with `shell: false` and an arg array — zero sidecar-derived
|
|
6
|
+
* data reaches a shell context (red-team #4). Exit-code handling is explicit
|
|
7
|
+
* (red-team #1, corrected):
|
|
8
|
+
* - flag NOT set → not-enabled (no-op)
|
|
9
|
+
* - binary absent → fail-closed (the operator declared the constraint;
|
|
10
|
+
* an attacker who deletes the binary or mutates $PATH
|
|
11
|
+
* must not silently disable the gate)
|
|
12
|
+
* - exit 0 → pass
|
|
13
|
+
* - exit 1 → fail (critical/high findings)
|
|
14
|
+
* - exit 2 → input error (bad YAML / missing files)
|
|
15
|
+
* - any other non-zero → crash → fail-closed (a crashing prover is not passing)
|
|
16
|
+
*
|
|
17
|
+
* The spawn function is injectable so tests can exercise every exit path without
|
|
18
|
+
* requiring openshell-prover to be installed.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.defaultSpawn = exports.DEFAULT_PROVER_COMMAND = void 0;
|
|
22
|
+
exports.runProver = runProver;
|
|
23
|
+
exports.blocksFinalization = blocksFinalization;
|
|
24
|
+
const child_process_1 = require("child_process");
|
|
25
|
+
/** Default invocation: `openshell prove --policy ...`. Override for a standalone binary. */
|
|
26
|
+
exports.DEFAULT_PROVER_COMMAND = ["openshell", "prove"];
|
|
27
|
+
/** Production spawn using child_process.spawn with shell:false. */
|
|
28
|
+
const defaultSpawn = (command, args) => new Promise((resolve) => {
|
|
29
|
+
const child = (0, child_process_1.spawn)(command[0], [...command.slice(1), ...args], {
|
|
30
|
+
shell: false,
|
|
31
|
+
});
|
|
32
|
+
let stdout = "";
|
|
33
|
+
let stderr = "";
|
|
34
|
+
child.stdout?.on("data", (d) => {
|
|
35
|
+
stdout += d.toString();
|
|
36
|
+
});
|
|
37
|
+
child.stderr?.on("data", (d) => {
|
|
38
|
+
stderr += d.toString();
|
|
39
|
+
});
|
|
40
|
+
child.on("error", (err) => {
|
|
41
|
+
resolve({ code: null, stdout, stderr, spawnError: err });
|
|
42
|
+
});
|
|
43
|
+
child.on("close", (code) => {
|
|
44
|
+
resolve({ code: code ?? null, stdout, stderr });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
exports.defaultSpawn = defaultSpawn;
|
|
48
|
+
/** Validate that a path looks like a real path (no shell metacharacters). */
|
|
49
|
+
function validatePath(p, label) {
|
|
50
|
+
if (typeof p !== "string" || p.length === 0)
|
|
51
|
+
return `${label} is required`;
|
|
52
|
+
// Reject shell metacharacters defensively — even though shell:false, this
|
|
53
|
+
// prevents a malformed path from being passed as an argv element that could
|
|
54
|
+
// confuse the prover's own arg parsing.
|
|
55
|
+
if (/[;<>&|`$()]/.test(p))
|
|
56
|
+
return `${label} contains forbidden characters`;
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Run the openshell-prover against a compiled policy + credentials.
|
|
61
|
+
*
|
|
62
|
+
* @param args Policy/credentials paths + options.
|
|
63
|
+
* @param opts flagEnabled (operator declared the constraint), injectable
|
|
64
|
+
* spawn fn and prover command for testing.
|
|
65
|
+
*/
|
|
66
|
+
async function runProver(args, opts) {
|
|
67
|
+
if (!opts.flagEnabled) {
|
|
68
|
+
return {
|
|
69
|
+
outcome: "not-enabled",
|
|
70
|
+
exitCode: null,
|
|
71
|
+
stdout: "",
|
|
72
|
+
stderr: "",
|
|
73
|
+
findings: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
for (const [label, p] of [
|
|
77
|
+
["policyPath", args.policyPath],
|
|
78
|
+
["credentialsPath", args.credentialsPath],
|
|
79
|
+
]) {
|
|
80
|
+
const err = validatePath(p, label);
|
|
81
|
+
if (err) {
|
|
82
|
+
return {
|
|
83
|
+
outcome: "input-error",
|
|
84
|
+
exitCode: null,
|
|
85
|
+
stdout: "",
|
|
86
|
+
stderr: "",
|
|
87
|
+
findings: [],
|
|
88
|
+
error: err,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const argv = ["--policy", args.policyPath, "--credentials", args.credentialsPath];
|
|
93
|
+
if (args.registryPath)
|
|
94
|
+
argv.push("--registry", args.registryPath);
|
|
95
|
+
if (args.acceptedRisksPath)
|
|
96
|
+
argv.push("--accepted-risks", args.acceptedRisksPath);
|
|
97
|
+
if (args.compact)
|
|
98
|
+
argv.push("--compact");
|
|
99
|
+
const command = opts.proverCommand ?? exports.DEFAULT_PROVER_COMMAND;
|
|
100
|
+
const spawnFn = opts.spawnFn ?? exports.defaultSpawn;
|
|
101
|
+
let out;
|
|
102
|
+
try {
|
|
103
|
+
out = await spawnFn(command, argv);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return {
|
|
107
|
+
outcome: "crash",
|
|
108
|
+
exitCode: null,
|
|
109
|
+
stdout: "",
|
|
110
|
+
stderr: "",
|
|
111
|
+
findings: [],
|
|
112
|
+
error: `prover invocation threw: ${err.message}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Binary absent (ENOENT) → fail-closed because flag is set.
|
|
116
|
+
if (out.spawnError && out.spawnError.code === "ENOENT") {
|
|
117
|
+
return {
|
|
118
|
+
outcome: "binary-absent-fail-closed",
|
|
119
|
+
exitCode: null,
|
|
120
|
+
stdout: out.stdout,
|
|
121
|
+
stderr: out.stderr,
|
|
122
|
+
findings: [],
|
|
123
|
+
error: `openshell-prover binary not found (ENOENT). The gate flag was set, so finalization is blocked — install openshell-prover or unset the flag. stderr: ${out.stderr}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Any other spawn error → fail-closed (crash).
|
|
127
|
+
if (out.spawnError) {
|
|
128
|
+
return {
|
|
129
|
+
outcome: "crash",
|
|
130
|
+
exitCode: null,
|
|
131
|
+
stdout: out.stdout,
|
|
132
|
+
stderr: out.stderr,
|
|
133
|
+
findings: [],
|
|
134
|
+
error: `prover failed to spawn: ${out.spawnError.message}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const code = out.code;
|
|
138
|
+
const findings = parseFindings(out.stdout);
|
|
139
|
+
if (code === 0) {
|
|
140
|
+
return { outcome: "pass", exitCode: code, stdout: out.stdout, stderr: out.stderr, findings };
|
|
141
|
+
}
|
|
142
|
+
if (code === 1) {
|
|
143
|
+
return {
|
|
144
|
+
outcome: "fail",
|
|
145
|
+
exitCode: code,
|
|
146
|
+
stdout: out.stdout,
|
|
147
|
+
stderr: out.stderr,
|
|
148
|
+
findings,
|
|
149
|
+
error: `prover reported critical/high findings (${findings.length} finding path(s))`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (code === 2) {
|
|
153
|
+
return {
|
|
154
|
+
outcome: "input-error",
|
|
155
|
+
exitCode: code,
|
|
156
|
+
stdout: out.stdout,
|
|
157
|
+
stderr: out.stderr,
|
|
158
|
+
findings,
|
|
159
|
+
error: `prover input error: ${out.stderr.trim() || out.stdout.trim()}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// Any other non-zero (or null) → crash → fail-closed.
|
|
163
|
+
return {
|
|
164
|
+
outcome: "crash",
|
|
165
|
+
exitCode: code,
|
|
166
|
+
stdout: out.stdout,
|
|
167
|
+
stderr: out.stderr,
|
|
168
|
+
findings,
|
|
169
|
+
error: `prover exited with unexpected code ${code} (crash). A crashing prover is not a passing prover. stderr: ${out.stderr.trim()}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Parse compact-mode finding lines from prover stdout. */
|
|
173
|
+
function parseFindings(stdout) {
|
|
174
|
+
return stdout
|
|
175
|
+
.split("\n")
|
|
176
|
+
.map((l) => l.trim())
|
|
177
|
+
.filter((l) => l.length > 0 && /•|REVIEW|link_local|l7_bypass|credential_reach|capability_expansion/.test(l));
|
|
178
|
+
}
|
|
179
|
+
/** True if the prover result blocks finalization. */
|
|
180
|
+
function blocksFinalization(r) {
|
|
181
|
+
return (r.outcome === "fail" ||
|
|
182
|
+
r.outcome === "input-error" ||
|
|
183
|
+
r.outcome === "binary-absent-fail-closed" ||
|
|
184
|
+
r.outcome === "crash");
|
|
185
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fractalcode/bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compiles Fractal Code channel sidecars into OpenShell sandbox policy YAML, and wires the OpenShell Z3 prover as a finalize-time gate. Standalone — zero NVIDIA code, zero Fractal-internal dependency.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"main": "dist/src/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"fractal-openshell-bridge": "dist/src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"NOTICE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"demo": "node examples/dual-gate/demo.js",
|
|
20
|
+
"clean": "rm -rf dist"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ai-agents",
|
|
24
|
+
"agent-security",
|
|
25
|
+
"sandbox",
|
|
26
|
+
"openshell",
|
|
27
|
+
"fractal-code",
|
|
28
|
+
"policy",
|
|
29
|
+
"defense-in-depth"
|
|
30
|
+
],
|
|
31
|
+
"author": "Angelo Regalbuto / Regal Health & Retirement LLC",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.3.0",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|