diff --git a/lex.toml b/lex.toml index 4262705..dfdfaa2 100644 --- a/lex.toml +++ b/lex.toml @@ -4,11 +4,13 @@ version = "0.1.0" lex = "0.9.7" [dependencies] -"lex-llm" = { path = "../lex-llm" } -"lex-agent" = { path = "../lex-agent" } -"lex-trail" = { path = "../lex-trail" } -"lex-spec" = { path = "../lex-spec" } -"lex-schema" = { path = "../lex-schema" } +lex-llm = { git = "https://github.com/alpibrusl/lex-llm" } +lex-agent = { git = "https://github.com/alpibrusl/lex-agent" } +lex-trail = { git = "https://github.com/alpibrusl/lex-trail" } +lex-spec = { git = "https://github.com/alpibrusl/lex-spec" } +lex-schema = { git = "https://github.com/alpibrusl/lex-schema" } +lex-os-manifest = { git = "https://github.com/alpibrusl/lex-os-manifest" } +lex-web = { git = "https://github.com/alpibrusl/lex-web" } [bin] name = "lex-code" diff --git a/src/agents/build.lex b/src/agents/build.lex index e0f8419..917350c 100644 --- a/src/agents/build.lex +++ b/src/agents/build.lex @@ -20,7 +20,7 @@ fn agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "build", goal: bpo.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } + let base := { name: "build", goal: bpo.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.build_permission()) } @@ -30,7 +30,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "build", goal: bpo.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: list.concat(tools.standard_tools(), tools.lex_cli_tools()), options: { temperature: None, top_p: None, max_steps: Some(10), max_tokens: None }, permission_spec: None } + let base := { name: "build", goal: bpo.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.build_permission()) } diff --git a/src/agents/explore.lex b/src/agents/explore.lex index be3994c..13fb5f2 100644 --- a/src/agents/explore.lex +++ b/src/agents/explore.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "explore", goal: ep.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.explore_permission()), options: { temperature: None, top_p: None, max_steps: Some(20), max_tokens: None }, permission_spec: None } + let base := { name: "explore", goal: ep.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.explore_permission()) } diff --git a/src/agents/plan.lex b/src/agents/plan.lex index d516ceb..8a74724 100644 --- a/src/agents/plan.lex +++ b/src/agents/plan.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "plan", goal: pp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "plan", goal: pp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.plan_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "plan", goal: pp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "plan", goal: pp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.plan_permission()) } diff --git a/src/agents/refactor.lex b/src/agents/refactor.lex index 1bd6654..f41f3c7 100644 --- a/src/agents/refactor.lex +++ b/src/agents/refactor.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "refactor", goal: rp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } + let base := { name: "refactor", goal: rp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.refactor_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "refactor", goal: rp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } + let base := { name: "refactor", goal: rp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.refactor_permission()) } diff --git a/src/agents/review.lex b/src/agents/review.lex index 8df137c..bfb7ed0 100644 --- a/src/agents/review.lex +++ b/src/agents/review.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "review", goal: rvp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } + let base := { name: "review", goal: rvp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.review_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "review", goal: rvp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } + let base := { name: "review", goal: rvp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.review_permission()) } diff --git a/src/agents/spec_agent.lex b/src/agents/spec_agent.lex index ffdf6f3..6c3ce1c 100644 --- a/src/agents/spec_agent.lex +++ b/src/agents/spec_agent.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "spec", goal: sp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "spec", goal: sp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.spec_permission()) } @@ -31,7 +31,7 @@ fn vllm_agent() -> [env] ag.AgentDef { } fn openai_agent() -> [env] ag.AgentDef { - let base := { name: "spec", goal: sp.system(), model: prov.gpt55(), provider: providers.openai(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "spec", goal: sp.system(), model: prov.gpt4o(), provider: providers.openai(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.spec_permission()) } diff --git a/src/agents/test_agent.lex b/src/agents/test_agent.lex index 610dd1f..50d793b 100644 --- a/src/agents/test_agent.lex +++ b/src/agents/test_agent.lex @@ -21,7 +21,7 @@ fn mistral_agent() -> [env] ag.AgentDef { } fn ollama_agent() -> [env] ag.AgentDef { - let base := { name: "test", goal: tp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: tools.tools_for_spec(rules.test_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + let base := { name: "test", goal: tp.system(), model: prov.ollama(providers.ollama_model()), provider: providers.ollama_local(), tools: [], options: { temperature: None, top_p: None, max_steps: Some(3), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.test_permission()) } diff --git a/src/permissions/manifests.lex b/src/permissions/manifests.lex new file mode 100644 index 0000000..94e847b --- /dev/null +++ b/src/permissions/manifests.lex @@ -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), + } +} diff --git a/src/permissions/rules.lex b/src/permissions/rules.lex index aa7c917..a420c36 100644 --- a/src/permissions/rules.lex +++ b/src/permissions/rules.lex @@ -53,7 +53,7 @@ fn spec_permission() -> sp.Spec { } fn refactor_permission() -> sp.Spec { - allow_tools("refactor_tools", list.concat(["read", "write", "edit", "grep", "glob", "bash", "lex_check", "lex_audit", "sigid_lookup", "effects_of", "lex_store_diff", "lex_store_apply", "lex_store_merge"], list.concat(vcs_read_names(), vcs_write_names()))) + allow_tools("refactor_tools", list.concat(["read", "write", "edit", "grep", "glob", "bash", "lex_check", "os_check", "lex_audit", "sigid_lookup", "effects_of", "lex_store_diff", "lex_store_apply", "lex_store_merge"], list.concat(vcs_read_names(), vcs_write_names()))) } fn test_permission() -> sp.Spec { diff --git a/src/tools/index.lex b/src/tools/index.lex index bf12599..43f44b7 100644 --- a/src/tools/index.lex +++ b/src/tools/index.lex @@ -14,6 +14,8 @@ import "./standard/todowrite" as todo_tool import "./lex_check" as check_tool +import "./os_check" as os_check_tool + import "./lex_audit" as audit_tool import "./lex_run" as run_tool @@ -99,7 +101,7 @@ fn vcs_tools() -> List[t.Tool] { } fn all_tools() -> List[t.Tool] { - list.concat([read_tool.tool(), write_tool.tool(), edit_tool.tool(), grep_tool.tool(), glob_tool.tool(), bash_tool.tool(), todo_tool.tool(), check_tool.tool(), audit_tool.tool(), run_tool.tool(), test_tool.tool(), spec_check_tool.tool(), spec_smt_tool.tool(), sigid_tool.tool(), attest_tool.tool(), effects_tool.tool(), store_diff_tool.tool(), store_apply_tool.tool(), store_merge_tool.tool(), guidelines_tool.tool()], vcs_tools()) + list.concat([read_tool.tool(), write_tool.tool(), edit_tool.tool(), grep_tool.tool(), glob_tool.tool(), bash_tool.tool(), todo_tool.tool(), check_tool.tool(), os_check_tool.tool(), audit_tool.tool(), run_tool.tool(), test_tool.tool(), spec_check_tool.tool(), spec_smt_tool.tool(), sigid_tool.tool(), attest_tool.tool(), effects_tool.tool(), store_diff_tool.tool(), store_apply_tool.tool(), store_merge_tool.tool(), guidelines_tool.tool()], vcs_tools()) } fn tools_for_spec(spec :: sp.Spec) -> List[t.Tool] { diff --git a/src/tools/os_check.lex b/src/tools/os_check.lex new file mode 100644 index 0000000..0ce3db0 --- /dev/null +++ b/src/tools/os_check.lex @@ -0,0 +1,122 @@ +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", [])] } +} + +fn network_effects() -> List[Str] { + ["net", "http", "mcp", "llm_cloud"] +} + +# 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" { + list.concat(network_effects(), ["proc", "fs_write"]) + } else { + if mode == "spec" { + list.concat(network_effects(), ["proc"]) + } else { + if mode == "test" or mode == "refactor" { + network_effects() + } 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) +} + diff --git a/src/web/index.html b/src/web/index.html index 249b56d..0a57a8e 100644 --- a/src/web/index.html +++ b/src/web/index.html @@ -24,7 +24,7 @@

lex-code