Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 071c0eeb9f |
+25
-10
@@ -91,29 +91,44 @@ jobs:
|
||||
echo "El SDK ready"
|
||||
/opt/el/dist/platform/elc --version || true
|
||||
|
||||
- name: Generate ELP master declarations header
|
||||
run: |
|
||||
{
|
||||
printf '/* Auto-generated C forward declarations for ELP cross-module calls */\n'
|
||||
printf '#pragma once\n'
|
||||
printf '#include "el_runtime.h"\n'
|
||||
printf '\n'
|
||||
grep -h -E '^(el_val_t|void|int|char\*|const char\*)[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(' dist/*.c 2>/dev/null \
|
||||
| grep ';$' | sort -u
|
||||
} > dist/elp-c-decls.h
|
||||
echo "Generated elp-c-decls.h with $(grep -c ';' dist/elp-c-decls.h 2>/dev/null || echo 0) declarations"
|
||||
|
||||
- name: Build neuron soul binary
|
||||
run: |
|
||||
ELB=/opt/el/dist/bin/elb
|
||||
ELC=/opt/el/dist/platform/elc
|
||||
RUNTIME=/opt/el/runtime
|
||||
|
||||
# Compile all El modules to C via elb.
|
||||
# elb fails at link on Linux (GNU ld rejects duplicate strong symbols that
|
||||
# macOS ld accepts silently) — that's expected and captured with || true.
|
||||
# The important output is dist/soul.c: the El compiler inlines all imported
|
||||
# modules into the entry-point file, so soul.c is a self-contained
|
||||
# translation unit. We never link the other dist/*.c files — they contain
|
||||
# the same symbols inlined again, plus capability-violation #error guards
|
||||
# that fire when compiled outside the cgi entrypoint.
|
||||
# Compile all El modules to C.
|
||||
# This step will fail at link on Linux: the El compiler inlines imported
|
||||
# modules into each module's .c file, producing duplicate strong symbol
|
||||
# definitions. GNU ld rejects these; macOS ld accepts them silently.
|
||||
# We capture the link failure and re-link manually below.
|
||||
$ELB --elc=$ELC --runtime=$RUNTIME/el_runtime.c || true
|
||||
|
||||
# Link only soul.c + the runtime. No --allow-multiple-definition needed.
|
||||
# Re-link with soul.c listed first so its real main() (from the cgi block)
|
||||
# wins over the stub main()s generated in every other module.
|
||||
# --allow-multiple-definition tells GNU ld to pick the first definition
|
||||
# for each duplicate symbol — safe here because all duplicates are identical
|
||||
# (same El source compiled independently into multiple .c files).
|
||||
mkdir -p dist
|
||||
OTHER_C=$(ls dist/*.c | grep -v '/soul\.c$' | sort | tr '\n' ' ')
|
||||
cc -O2 -DHAVE_CURL \
|
||||
-I$RUNTIME \
|
||||
dist/soul.c \
|
||||
dist/soul.c $OTHER_C \
|
||||
$RUNTIME/el_runtime.c \
|
||||
-lssl -lcrypto -lcurl -lpthread -lm \
|
||||
-Wl,--allow-multiple-definition \
|
||||
-o dist/neuron
|
||||
|
||||
ls -lh dist/neuron
|
||||
|
||||
@@ -377,16 +377,78 @@ fn call_neuron_mcp(tool_name: String, args: String) -> String {
|
||||
return json_safe(result)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Agent workspace scope (defense-in-depth, NOT a hard security boundary).
|
||||
//
|
||||
// When a workspace root is configured (state key "agent_workspace_root", else
|
||||
// env NEURON_AGENT_ROOT), the path-based tools (read_file, write_file,
|
||||
// list_files, grep) are confined to that subtree by a lexical check, and
|
||||
// run_command runs with its cwd set to the root. With no root set, behavior is
|
||||
// unchanged (unscoped) for backward compatibility.
|
||||
//
|
||||
// LIMITATION — FLAGGED FOR WILL'S REVIEW: this is a lexical guard. It does not
|
||||
// resolve symlinks and cannot stop an arbitrary shell command from cd-ing out
|
||||
// of the root. Real confinement needs runtime support (cwd-locked exec /
|
||||
// sandbox-exec / chroot) in el_runtime.c. This raises the floor; it is not a
|
||||
// boundary. The default-allow-when-unset policy and the "cd <root> && (...)"
|
||||
// wrapping are deliberate choices to confirm against the intended design.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn agent_workspace_root() -> String {
|
||||
let s: String = state_get("agent_workspace_root")
|
||||
if !str_eq(s, "") {
|
||||
return s
|
||||
}
|
||||
return env("NEURON_AGENT_ROOT")
|
||||
}
|
||||
|
||||
// Allow if path stays under root. Empty root = no sandbox = allow. Rejects
|
||||
// parent traversal and ~ expansion; absolute paths must live under root.
|
||||
fn path_within_root(path: String, root: String) -> Bool {
|
||||
if str_eq(root, "") {
|
||||
return true
|
||||
}
|
||||
if str_contains(path, "..") {
|
||||
return false
|
||||
}
|
||||
if str_starts_with(path, "~") {
|
||||
return false
|
||||
}
|
||||
if str_starts_with(path, "/") {
|
||||
return str_starts_with(path, root)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Resolve a relative tool path against the root so it lands inside the subtree.
|
||||
fn resolve_in_root(path: String, root: String) -> String {
|
||||
if str_eq(root, "") {
|
||||
return path
|
||||
}
|
||||
if str_starts_with(path, "/") {
|
||||
return path
|
||||
}
|
||||
return root + "/" + path
|
||||
}
|
||||
|
||||
fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
if str_eq(tool_name, "read_file") {
|
||||
let path: String = json_get(tool_input, "path")
|
||||
let content: String = fs_read(path)
|
||||
let root: String = agent_workspace_root()
|
||||
if !path_within_root(path, root) {
|
||||
return json_safe("denied: path is outside the agent workspace root")
|
||||
}
|
||||
let content: String = fs_read(resolve_in_root(path, root))
|
||||
return json_safe(content)
|
||||
}
|
||||
if str_eq(tool_name, "write_file") {
|
||||
let path: String = json_get(tool_input, "path")
|
||||
let content: String = json_get(tool_input, "content")
|
||||
fs_write(path, content)
|
||||
let root: String = agent_workspace_root()
|
||||
if !path_within_root(path, root) {
|
||||
return json_safe("denied: path is outside the agent workspace root")
|
||||
}
|
||||
fs_write(resolve_in_root(path, root), content)
|
||||
return json_safe("{\"ok\":true}")
|
||||
}
|
||||
if str_eq(tool_name, "web_get") {
|
||||
@@ -401,7 +463,9 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
}
|
||||
if str_eq(tool_name, "run_command") {
|
||||
let cmd: String = json_get(tool_input, "command")
|
||||
let result: String = exec_capture(cmd)
|
||||
let root: String = agent_workspace_root()
|
||||
let scoped: String = if str_eq(root, "") { cmd } else { "cd " + root + " && ( " + cmd + " )" }
|
||||
let result: String = exec_capture(scoped)
|
||||
return json_safe(result)
|
||||
}
|
||||
// MCP connector tools (namespaced mcp__<server>__<tool>) are routed through
|
||||
@@ -421,13 +485,21 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
||||
}
|
||||
if str_eq(tool_name, "list_files") {
|
||||
let path: String = json_get(tool_input, "path")
|
||||
let result: String = exec_capture("ls -la " + path + " 2>&1")
|
||||
let root: String = agent_workspace_root()
|
||||
if !path_within_root(path, root) {
|
||||
return json_safe("denied: path is outside the agent workspace root")
|
||||
}
|
||||
let result: String = exec_capture("ls -la " + resolve_in_root(path, root) + " 2>&1")
|
||||
return json_safe(result)
|
||||
}
|
||||
if str_eq(tool_name, "grep") {
|
||||
let pattern: String = json_get(tool_input, "pattern")
|
||||
let path: String = json_get(tool_input, "path")
|
||||
let result: String = exec_capture("grep -rn \"" + pattern + "\" " + path + " 2>&1 | head -50")
|
||||
let root: String = agent_workspace_root()
|
||||
if !path_within_root(path, root) {
|
||||
return json_safe("denied: path is outside the agent workspace root")
|
||||
}
|
||||
let result: String = exec_capture("grep -rn \"" + pattern + "\" " + resolve_in_root(path, root) + " 2>&1 | head -50")
|
||||
return json_safe(result)
|
||||
}
|
||||
if str_eq(tool_name, "edit_file") {
|
||||
|
||||
Reference in New Issue
Block a user