-
Notifications
You must be signed in to change notification settings - Fork 0
fix: fail closed on os_check grant violations #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # Per-mode trust manifests for lex-os integration. | ||
| # | ||
| # Each AgentMode maps to a Grant that constrains what the mode can do | ||
| # on the three trust dimensions: filesystem, network, exec. | ||
| # | ||
| # Grant levels (from lex-types): | ||
| # filesystem: None | ReadOnly | ReadWrite | Full | ||
| # network: None | Loopback | Allowlist | Full | ||
| # exec: None | Sandboxed | Full | ||
| # | ||
| # Manifests are written to /tmp at call time and passed to `lex-os check`. | ||
|
|
||
| import "std.str" as str | ||
|
|
||
| import "std.int" as int | ||
|
|
||
| import "std.io" as io | ||
|
|
||
| fn manifest_json(goal :: Str, filesystem :: Str, network :: Str, exec_level :: Str, floor :: Str, wall :: Int, cmds :: Int, money :: Int, api_calls :: Int) -> Str { | ||
| str.join([ | ||
| "{\"goal\":{\"description\":\"", goal, "\"},", | ||
| "\"grant\":{\"filesystem\":\"", filesystem, "\",\"network\":\"", network, "\",\"exec\":\"", exec_level, "\"},", | ||
| "\"budget\":{\"wall_clock_secs\":", int.to_str(wall), ",", | ||
| "\"max_commands\":", int.to_str(cmds), ",", | ||
| "\"max_money_cents\":", int.to_str(money), ",", | ||
| "\"max_api_calls\":", int.to_str(api_calls), "},", | ||
| "\"isolation_floor\":\"", floor, "\",\"egress\":[]}" | ||
| ], "") | ||
| } | ||
|
|
||
| fn explore_manifest_json() -> Str { | ||
| manifest_json("lex-code explore mode", "ReadOnly", "None", "None", "Namespace", 300, 200, 0, 50) | ||
| } | ||
|
|
||
| fn plan_manifest_json() -> Str { | ||
| manifest_json("lex-code plan mode", "ReadOnly", "None", "None", "Namespace", 300, 200, 0, 50) | ||
| } | ||
|
|
||
| fn review_manifest_json() -> Str { | ||
| manifest_json("lex-code review mode", "ReadOnly", "None", "None", "Namespace", 300, 200, 0, 50) | ||
| } | ||
|
|
||
| fn spec_manifest_json() -> Str { | ||
| manifest_json("lex-code spec mode", "ReadWrite", "None", "None", "Namespace", 300, 200, 0, 50) | ||
| } | ||
|
|
||
| fn test_manifest_json() -> Str { | ||
| manifest_json("lex-code test mode", "ReadWrite", "None", "Sandboxed", "Gvisor", 300, 200, 0, 50) | ||
| } | ||
|
|
||
| fn refactor_manifest_json() -> Str { | ||
| manifest_json("lex-code refactor mode", "ReadWrite", "None", "Sandboxed", "Gvisor", 300, 200, 0, 50) | ||
| } | ||
|
|
||
| fn build_manifest_json() -> Str { | ||
| manifest_json("lex-code build mode", "Full", "Allowlist", "Full", "MicroVm", 600, 500, 500, 100) | ||
| } | ||
|
|
||
| fn json_for_mode(mode :: Str) -> Str { | ||
| if mode == "explore" { | ||
| explore_manifest_json() | ||
| } else { | ||
| if mode == "plan" { | ||
| plan_manifest_json() | ||
| } else { | ||
| if mode == "review" { | ||
| review_manifest_json() | ||
| } else { | ||
| if mode == "spec" { | ||
| spec_manifest_json() | ||
| } else { | ||
| if mode == "test" { | ||
| test_manifest_json() | ||
| } else { | ||
| if mode == "refactor" { | ||
| refactor_manifest_json() | ||
| } else { | ||
| build_manifest_json() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn temp_path_for_mode(mode :: Str) -> Str { | ||
| str.concat("/tmp/lex-code-manifest-", str.concat(mode, ".json")) | ||
| } | ||
|
|
||
| fn write_manifest_for_mode(mode :: Str) -> [io] Result[Str, Str] { | ||
| let path := temp_path_for_mode(mode) | ||
| match io.write(path, json_for_mode(mode)) { | ||
| Ok(_) => Ok(path), | ||
| Err(e) => Err(e), | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import "std.proc" as proc | ||
|
|
||
| import "std.str" as str | ||
|
|
||
| import "std.list" as list | ||
|
|
||
| import "lex-llm/tool" as t | ||
|
|
||
| import "lex-schema/json_value" as jv | ||
|
|
||
| import "lex-schema/error" as e | ||
|
|
||
| import "lex-schema/schema" as s | ||
|
|
||
| import "./util" as util | ||
|
|
||
| fn params() -> s.ModelSchema { | ||
| { title: "OsCheckArgs", description: "Check a Lex file's declared effects against the refactor-mode trust grant", fields: [s.required_str("path", [])] } | ||
| } | ||
|
|
||
| # Effects the mode forbids, derived from the lex-os grant for that mode. | ||
| # explore / plan / review — ReadOnly FS, No Net, No Exec | ||
| # spec — ReadWrite FS, No Net, No Exec | ||
| # test / refactor — ReadWrite FS, No Net, Sandboxed Exec | ||
| # build — Full FS, Allowlist Net, Full Exec (nothing forbidden) | ||
| fn forbidden_for_mode(mode :: Str) -> List[Str] { | ||
| if mode == "explore" or mode == "plan" or mode == "review" { | ||
| ["net", "proc", "fs_write"] | ||
| } else { | ||
| if mode == "spec" { | ||
| ["net", "proc"] | ||
| } else { | ||
| if mode == "test" or mode == "refactor" { | ||
| ["net"] | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This only forbids the bare |
||
| } else { | ||
| [] | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn effect_in(effect :: Str, lst :: List[Str]) -> Bool { | ||
| list.fold(lst, false, fn (acc :: Bool, x :: Str) -> Bool { | ||
| acc or x == effect | ||
| }) | ||
| } | ||
|
|
||
| fn violations(required :: List[Str], forbidden :: List[Str]) -> List[Str] { | ||
| list.filter(required, fn (eff :: Str) -> Bool { | ||
| effect_in(eff, forbidden) | ||
| }) | ||
| } | ||
|
|
||
| fn extract_effects(check_json :: jv.Json) -> Result[List[Str], e.Errors] { | ||
| match jv.get_field(check_json, "data") { | ||
| None => Err(e.single("", "missing_field", "lex check JSON missing data.required_effects")), | ||
| Some(data) => match jv.get_field(data, "required_effects") { | ||
| Some(JList(items)) => Ok(list.fold(items, [], fn (acc :: List[Str], j :: jv.Json) -> List[Str] { | ||
| match j { | ||
| JStr(s) => list.concat(acc, [s]), | ||
| _ => acc, | ||
| } | ||
| })), | ||
| Some(_) => Err(e.single("", "invalid_field", "lex check JSON data.required_effects must be a list")), | ||
| None => Err(e.single("", "missing_field", "lex check JSON missing data.required_effects")), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| fn execute(args :: jv.Json) -> [net, io, proc] Result[jv.Json, e.Errors] { | ||
| let path := util.field_str_or(args, "path", ".") | ||
| let mode := "refactor" | ||
| match proc.spawn("lex", ["--output", "json", "check", path]) { | ||
| Err(msg) => Err(e.single("", "proc_error", msg)), | ||
| Ok(out) => if out.exit_code != 0 { | ||
| Err(e.single("", "lex_check_failed", str.concat(out.stdout, out.stderr))) | ||
| } else { | ||
| match jv.parse(out.stdout) { | ||
| Err(_) => Err(e.single("", "parse_error", "could not parse lex check output")), | ||
| Ok(parsed) => { | ||
| match extract_effects(parsed) { | ||
| Err(errs) => Err(errs), | ||
| Ok(required) => { | ||
| let forbidden := forbidden_for_mode(mode) | ||
| let violated := violations(required, forbidden) | ||
| if list.len(violated) == 0 { | ||
| Ok(JStr(str.concat("grant check passed [mode=", str.concat(mode, str.concat("] effects=", str.join(required, ",")))))) | ||
| } else { | ||
| Err(e.single("", "grant_violation", str.join(["GRANT VIOLATION [mode=", mode, "]\n", " forbidden effects used: ", str.join(violated, ", "), "\n", " all required effects: ", str.join(required, ", "), "\n", " grant allows: ", grant_summary_for_mode(mode)], ""))) | ||
| } | ||
| }, | ||
| } | ||
| }, | ||
| } | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| fn grant_summary_for_mode(mode :: Str) -> Str { | ||
| if mode == "explore" or mode == "plan" or mode == "review" { | ||
| "fs=read-only net=none exec=none" | ||
| } else { | ||
| if mode == "spec" { | ||
| "fs=read-write net=none exec=none" | ||
| } else { | ||
| if mode == "test" or mode == "refactor" { | ||
| "fs=read-write net=none exec=sandboxed" | ||
| } else { | ||
| "fs=full net=allowlist exec=full" | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn tool() -> t.Tool { | ||
| t.define("os_check", "Check a Lex file's declared effects against the refactor-mode trust grant (lex-os integration). Run after lex_check to catch grant violations — e.g. a refactor agent must not use net effects. The grant mode is fixed by the tool and cannot be overridden by model input. Returns GRANT VIOLATION with details if the file exceeds the refactor grant.", params(), execute) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Severity: Medium. These new Git dependencies are floating: there is no tag/rev pin here and no lockfile in the repo, so builds can resolve whatever is on each dependency repo's default branch. If one dependency repo or maintainer account is compromised, attacker-controlled package code can run inside
lex-code's process/tool boundary, including code that defines providers, tools, or permission-gate behavior. Pin each Git dependency to an immutable commit/tag and commit the resolver lockfile.