v0.18.0: MCP entity-graph CRUD, lifecycle validation, curl -o /dev/null fix
- 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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user