v0.18.0: MCP entity-graph CRUD, lifecycle validation, curl -o /dev/null fix
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

- create_entity, set_entity_state, end_relationship MCP tools
- update_entity_attributes now triggers check derivation via EnsureEntityChecks
- shared db.EnsureEntityChecks + db.ValidateTransition hooks (HTTP + MCP parity)
- curl -o /dev/null now classified read_only (was config_mutation)
- db.ErrTransitionInvalid sentinel for HTTP error-type accuracy
- SOUL.md: capability escalation, self-grounding, exploration budget rules
- Runbook: oikos check lifecycle for agent self-knowledge
This commit is contained in:
2026-08-04 08:52:08 +02:00
parent 058f1afcdc
commit 20adb89650
14 changed files with 1098 additions and 157 deletions

View File

@@ -105,6 +105,15 @@ var curlLeadRe = regexp.MustCompile(`(?i)^curl\b`)
// When any of these appears, the curl command is no longer read-only.
var curlMutateRe = regexp.MustCompile(`(?i)(?:^|\s)-X\s+(?:post|put|delete|patch|connect|trace)\b|(?:^|\s)-(?:d|F|T|o)\b|(?:^|\s)--(?:data[-a-z]*|request|form|upload-file|output)\b`)
// curlDevNullOutRe matches curl output redirected to /dev/null in any of curl's
// argument forms (space, =, or attached). /dev/null is a no-op sink, so a GET
// that discards its body — the canonical reachability idiom
// `curl -o /dev/null -w '%{http_code}' URL` — is read-only. Output to any real
// path (-o /tmp/x) stays a potential mutation. Stripped before curlMutateRe so
// the remaining flags (-X, -d, ...) still classify correctly: a
// `curl -o /dev/null -X POST` stays config_mutation.
var curlDevNullOutRe = regexp.MustCompile(`(?i)(^|\s)-o\s*/dev/null(\s|$)|(^|\s)--output[=\s]\s*/dev/null(\s|$)`)
// redirectOutRe matches shell output redirection to a file (> or >> followed
// by a path), but excludes the file-descriptor merge form `>&<digit>` (e.g.
// `2>&1`) which only rearranges streams and writes nothing to disk. RE2 has
@@ -308,6 +317,10 @@ func curlIsReadOnly(curlCmd string) bool {
if !curlLeadRe.MatchString(curlCmd) {
return false
}
// -o /dev/null is a no-op sink: strip it before flag detection so the
// canonical GET-and-discard reachability probe stays read-only.
// A `curl -o /dev/null -X POST` still fails curlMutateRe after stripping.
curlCmd = curlDevNullOutRe.ReplaceAllString(curlCmd, " ")
if curlMutateRe.MatchString(curlCmd) {
return false
}

View File

@@ -102,6 +102,33 @@ func TestClassifyCommand_CurlPipeSh_ConfigMutation(t *testing.T) {
}
}
func TestClassifyCommand_CurlDevNull_ReadOnly(t *testing.T) {
// -o /dev/null is a no-op sink — the canonical GET-and-discard
// reachability idiom must stay read_only. Output to real paths stays
// config_mutation. POST/data flags after stripping still gate.
cases := []struct {
cmd string
cls string
}{
// read_only: GET with body discarded to /dev/null
{`curl -o /dev/null -w '%{http_code}' --connect-timeout 10 http://192.168.8.101:8123`, RiskReadOnly},
{`curl -sS -o /dev/null https://home.hubris.network`, RiskReadOnly},
{`curl --output /dev/null https://example.com`, RiskReadOnly},
{`curl -o /dev/null https://example.com`, RiskReadOnly},
{`curl -o/dev/null -w '%{http_code}' https://example.com`, RiskReadOnly},
// config_mutation: POST/data still caught after stripping devnull
{`curl -o /dev/null -X POST https://example.com`, RiskConfigMutation},
{`curl -o /dev/null -d '{"x":1}' https://example.com`, RiskConfigMutation},
// config_mutation: -o to real path stays config_mutation
{`curl -o /etc/caddy/Caddyfile http://example.com`, RiskConfigMutation},
}
for _, c := range cases {
if got := ClassifyCommand(c.cmd, ""); got != c.cls {
t.Errorf("ClassifyCommand(%q) = %q, want %q", c.cmd, got, c.cls)
}
}
}
func TestClassifyCommand_DefaultEscalatesToConfigMutation(t *testing.T) {
cases := []string{
"apt-get install -y nginx",