From 8c3bae41f64aa72d673285ea268bb938eb4fccd4 Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:46:44 +0100 Subject: [PATCH] Untrusted boundary ON by default (opt-out via ARTEX_UNTRUSTED_BOUNDARY=0) ARTEX_UNTRUSTED_BOUNDARY unset or empty now enables the boundary. 0, false, off and no (case-insensitive, trimmed) disable it; any other value keeps it on. Docs state the default, the opt-out and the measured cost (about 154 bytes per tool result plus one system note). --- .env.example | 9 +++++---- README.md | 2 ++ agent/chat.go | 2 +- agent/mainagent.go | 2 +- agent/planner.go | 2 +- agent/untrusted_boundary.go | 16 ++++++++-------- agent/untrusted_boundary_test.go | 19 ++++++++++++------- agent/worker.go | 2 +- docker-compose.yml | 2 ++ 9 files changed, 33 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index eef152c..8a556a2 100644 --- a/.env.example +++ b/.env.example @@ -18,10 +18,11 @@ ARTEX_LLM_MODEL= ARTEX_LLM_BASE_URL= ARTEX_LLM_PROXY= -# Untrusted-output boundary (optional). ARTEX_UNTRUSTED_BOUNDARY=1 frames every tool -# output between nonce-tagged markers so the model treats it as data, not instructions. -# Off by default; test it against a local target before enabling it in production. -# ARTEX_UNTRUSTED_BOUNDARY=1 +# Untrusted-output boundary. Frames every tool output between nonce-tagged markers so +# the model treats it as data, not instructions. On by default (unset or empty = on); +# set ARTEX_UNTRUSTED_BOUNDARY=0 (or false/off/no) to disable it. Measured cost: about +# 154 bytes per tool result plus one system note. +# ARTEX_UNTRUSTED_BOUNDARY=0 # Update banner (optional). ARTEX_UPDATE_CHECK=1 lets the backend check the public GitHub # releases API for a newer ARTEX release at most once per 24 h and show a dismissible banner. diff --git a/README.md b/README.md index de8b47b..ad5d048 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,8 @@ Optional: `ARTEX_LLM_PROVIDER` (anthropic | openai | openai-responses | ollama) **Concurrency**: the number of work agents per task is configured in "System Settings" (default 3). +**Untrusted-output boundary (on by default)**: tool results for the worker, planner, chat and main agent are framed between nonce-tagged markers so the model treats them as data, not instructions. Unset or empty, the boundary is on; set `ARTEX_UNTRUSTED_BOUNDARY=0` (or `false` / `off` / `no`) to disable it. Measured cost: about 154 bytes per tool result plus one system note. + **Common flags**: `./start.sh -addr :8787 -proxy :8788` (`-addr` frontend+API, `-proxy` traffic recording proxy). The startup script passes arguments through to `artex` verbatim. ### Reverse proxy deployment (HTTPS / only exposing 443) diff --git a/agent/chat.go b/agent/chat.go index 0a574a2..412a821 100644 --- a/agent/chat.go +++ b/agent/chat.go @@ -163,7 +163,7 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin NonStreaming: c.nonStreaming(), // when this profile selects non-streaming, use Provider.Complete MaxTokens: c.maxTokens(), // 0 = send no cap, the server's default decides - // Opt-in via ARTEX_UNTRUSTED_BOUNDARY (default off): nonce-frame tool output as untrusted data. + // On by default; ARTEX_UNTRUSTED_BOUNDARY=0 opts out: nonce-frame tool output as untrusted data. UntrustedOutputBoundary: untrustedBoundaryEnabled(), } if c.guard != nil { diff --git a/agent/mainagent.go b/agent/mainagent.go index d6e7443..dd7a373 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -163,7 +163,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db. NonStreaming: m.nonStreaming(), // when this profile selects non-streaming, use Provider.Complete MaxTokens: m.maxTokens(), // 0 = send no cap, the server's default decides - // Opt-in via ARTEX_UNTRUSTED_BOUNDARY (default off): nonce-frame tool output as untrusted data. + // On by default; ARTEX_UNTRUSTED_BOUNDARY=0 opts out: nonce-frame tool output as untrusted data. UntrustedOutputBoundary: untrustedBoundaryEnabled(), } if hooks != nil { // typed-nil guard: only set when concrete (avoids harness panic) diff --git a/agent/planner.go b/agent/planner.go index 7085c55..e422fad 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -541,7 +541,7 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts NonStreaming: p.nonStreaming(), // when this profile selects non-streaming, use Provider.Complete MaxTokens: p.maxTokens(), // 0 = send no cap; the server default decides - // Opt-in via ARTEX_UNTRUSTED_BOUNDARY (default off): nonce-frame tool output as untrusted data. + // On by default; ARTEX_UNTRUSTED_BOUNDARY=0 opts out: nonce-frame tool output as untrusted data. UntrustedOutputBoundary: untrustedBoundaryEnabled(), } if hooks != nil { // typed-nil guard: only set when concrete (avoids harness panic) diff --git a/agent/untrusted_boundary.go b/agent/untrusted_boundary.go index cf20e93..47141b9 100644 --- a/agent/untrusted_boundary.go +++ b/agent/untrusted_boundary.go @@ -5,15 +5,15 @@ import ( "strings" ) -// untrustedBoundaryEnabled reports whether ARTEX_UNTRUSTED_BOUNDARY opts in to -// Norma's untrusted-output boundary (agentcore.Options.UntrustedOutputBoundary: -// tool results framed between nonce-tagged markers). Only "1", "true" and "on" -// (case- and space-insensitive) enable it; unset, empty or anything else keeps -// it off. +// untrustedBoundaryEnabled reports whether Norma's untrusted-output boundary +// (agentcore.Options.UntrustedOutputBoundary: tool results framed between +// nonce-tagged markers) is on. It is on by default: unset, empty or any other +// value keeps it on; only ARTEX_UNTRUSTED_BOUNDARY set to "0", "false", "off" +// or "no" (case- and space-insensitive) turns it off. func untrustedBoundaryEnabled() bool { switch strings.ToLower(strings.TrimSpace(os.Getenv("ARTEX_UNTRUSTED_BOUNDARY"))) { - case "1", "true", "on": - return true + case "0", "false", "off", "no": + return false } - return false + return true } diff --git a/agent/untrusted_boundary_test.go b/agent/untrusted_boundary_test.go index c396eb6..5e7137e 100644 --- a/agent/untrusted_boundary_test.go +++ b/agent/untrusted_boundary_test.go @@ -11,15 +11,20 @@ func TestUntrustedBoundaryEnabled(t *testing.T) { env string want bool }{ - {"", false}, + {"", true}, {"0", false}, {"false", false}, - {"yes", false}, + {"off", false}, + {"no", false}, + {" FALSE ", false}, + {"Off", false}, + {"NO", false}, {"1", true}, {"true", true}, {"on", true}, + {"yes", true}, {" TRUE ", true}, - {"On", true}, + {"anything", true}, } { t.Setenv("ARTEX_UNTRUSTED_BOUNDARY", tc.env) if got := untrustedBoundaryEnabled(); got != tc.want { @@ -28,11 +33,11 @@ func TestUntrustedBoundaryEnabled(t *testing.T) { } } -func TestUntrustedBoundaryDisabledWhenUnset(t *testing.T) { - t.Setenv("ARTEX_UNTRUSTED_BOUNDARY", "1") // registers restoration of the original value +func TestUntrustedBoundaryEnabledWhenUnset(t *testing.T) { + t.Setenv("ARTEX_UNTRUSTED_BOUNDARY", "0") // registers restoration of the original value os.Unsetenv("ARTEX_UNTRUSTED_BOUNDARY") - if untrustedBoundaryEnabled() { - t.Fatal("ARTEX_UNTRUSTED_BOUNDARY unset: got true, want false") + if !untrustedBoundaryEnabled() { + t.Fatal("ARTEX_UNTRUSTED_BOUNDARY unset: got false, want true") } } diff --git a/agent/worker.go b/agent/worker.go index 093c3cc..66f13de 100644 --- a/agent/worker.go +++ b/agent/worker.go @@ -524,7 +524,7 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db. NonStreaming: w.nonStreaming(), // when this profile selects non-streaming, use Provider.Complete MaxTokens: w.maxTokens(), // 0 = send no cap; the server default decides - // Opt-in via ARTEX_UNTRUSTED_BOUNDARY (default off): nonce-frame tool output as untrusted data. + // On by default; ARTEX_UNTRUSTED_BOUNDARY=0 opts out: nonce-frame tool output as untrusted data. UntrustedOutputBoundary: untrustedBoundaryEnabled(), } if hooks != nil { // typed-nil guard: only set when concrete (avoids harness panic) diff --git a/docker-compose.yml b/docker-compose.yml index 8fd5884..c8da3e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,8 @@ services: ARTEX_LLM_PROXY: ${ARTEX_LLM_PROXY:-} ARTEX_JWT_KEY_DIR: /app/keys ARTEX_TRUSTED_PROXIES: ${ARTEX_TRUSTED_PROXIES:-} + # Untrusted-output boundary: on by default (empty = on); ARTEX_UNTRUSTED_BOUNDARY=0 in .env + # disables it. Cost: about 154 bytes per tool result plus one system note. ARTEX_UNTRUSTED_BOUNDARY: ${ARTEX_UNTRUSTED_BOUNDARY:-} ARTEX_UPDATE_CHECK: ${ARTEX_UPDATE_CHECK:-} ports: