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 not pinned to immutable commits/tags, and this repo has no lockfile. A future or compromised upstream commit can be pulled by lex pkg install/CI without any change in this repo, letting attacker-controlled dependency code run in builds and developer installs. Pin each git dependency to a reviewed revision or 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
117 changes: 117 additions & 0 deletions src/tools/os_check.lex
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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) -> List[Str] {
match jv.get_field(check_json, "data") {
None => [],
Some(data) => match jv.get_field(data, "required_effects") {
Some(JList(items)) => list.fold(items, [], fn (acc :: List[Str], j :: jv.Json) -> List[Str] {
match j {
JStr(s) => list.concat(acc, [s]),
_ => acc,
}
}),
_ => [],
},
}
}

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) => {
let required := extract_effects(parsed)
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 {
Ok(JStr(str.join([

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 is a critical policy-check bypass: the tool reports a refactor grant violation as Ok(...), so the dispatcher records a successful tool call even when forbidden effects were found. A refactor agent can add a [net] effect, call os_check on the edited file, receive GRANT VIOLATION, and still have the tool invocation complete successfully for any caller/automation that gates on tool errors. I opened follow-up PR #38 to make this path return Err(..., "grant_violation", ...) and to fail closed on malformed/missing required_effects.

"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