Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions lex.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Comment on lines +7 to +13

Copy link
Copy Markdown
Author

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.


[bin]
name = "lex-code"
Expand Down
4 changes: 2 additions & 2 deletions src/agents/build.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -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())
}

Expand Down
2 changes: 1 addition & 1 deletion src/agents/explore.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
4 changes: 2 additions & 2 deletions src/agents/plan.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -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())
}

Expand Down
4 changes: 2 additions & 2 deletions src/agents/refactor.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -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())
}

Expand Down
4 changes: 2 additions & 2 deletions src/agents/review.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -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())
}

Expand Down
4 changes: 2 additions & 2 deletions src/agents/spec_agent.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -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())
}

Expand Down
2 changes: 1 addition & 1 deletion src/agents/test_agent.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
97 changes: 97 additions & 0 deletions src/permissions/manifests.lex
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),
}
}
2 changes: 1 addition & 1 deletion src/permissions/rules.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/tools/index.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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] {
Expand Down
118 changes: 118 additions & 0 deletions src/tools/os_check.lex
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"]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only forbids the bare net effect for refactor/test, but Lex's trust lattice also classifies mcp and llm_cloud as network egress effects. A refactor-mode generated file that declares [llm_cloud] or [mcp] will appear in lex check --output json as that effect name, miss this exact-match deny list, and pass despite the net=none grant. I opened #39 to reject the full network-effect set.

} 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)
}

2 changes: 1 addition & 1 deletion src/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ <h1>lex-code</h1>
</label>
<label>Provider
<select id="provider-select">
<option value="anthropic">Anthropic</option>
<option value="anthropic" selected>Anthropic</option>
<option value="openai">OpenAI</option>
<option value="mistral">Mistral</option>
<option value="google">Google</option>
Expand Down
Loading