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..45595b5 --- /dev/null +++ b/src/tools/os_check.lex @@ -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"] + } 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