Merge pull request #203 from BaaRai/feat/untrusted-boundary-default-on

Untrusted boundary ON by default (opt-out via ARTEX_UNTRUSTED_BOUNDARY=0)
This commit is contained in:
BaaRai
2026-11-12 14:53:27 +01:00
committed by GitHub
9 changed files with 33 additions and 23 deletions
+5 -4
View File
@@ -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.
+2
View File
@@ -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)
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+8 -8
View File
@@ -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
}
+12 -7
View File
@@ -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")
}
}
+1 -1
View File
@@ -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)
+2
View File
@@ -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: