{"record":{"id":"469c7213e873dfab","repo":"siyuan-note/siyuan","slug":"invalid-agent-runtime-turn-state","errorCode":null,"errorMessage":"invalid agent runtime turn state","messagePattern":"invalid agent runtime turn state","errorType":"http","errorClass":null,"httpStatus":200,"severity":"error","filePath":"kernel/agent/runtime.go","lineNumber":219,"sourceCode":"\t\treturn nil, err\n\t}\n\tif runtime.SchemaVersion > 1 {\n\t\treturn nil, fmt.Errorf(\"unsupported agent runtime schema version: %d\", runtime.SchemaVersion)\n\t}\n\tif runtime.SessionID != \"\" && runtime.SessionID != sessionID {\n\t\treturn nil, fmt.Errorf(\"agent runtime session id mismatch\")\n\t}\n\tif runtime.Revision < 0 {\n\t\treturn nil, fmt.Errorf(\"invalid agent runtime revision\")\n\t}\n\tif runtime.ActiveTurn != nil {\n\t\tif runtime.ActiveTurn.TurnID == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"invalid agent runtime turn id\")\n\t\t}\n\t\tswitch runtime.ActiveTurn.State {\n\t\tcase \"running\", \"finished\", \"interrupted\":\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"invalid agent runtime turn state\")\n\t\t}\n\t}\n\tif runtime.SchemaVersion == 0 {\n\t\truntime.SchemaVersion = 1\n\t}\n\tif runtime.SessionID == \"\" {\n\t\truntime.SessionID = sessionID\n\t}\n\treturn &runtime, nil\n}\n\nfunc writeRuntimeLocked(sessionID string, runtime *agentRuntime) error {\n\tif runtime == nil {\n\t\treturn nil\n\t}\n\t// runtime 只能附着在已经存在的会话上，避免迟到的 checkpoint 复活已删除会话。\n\tif _, err := os.Stat(filepath.Join(sessionsDir(), sessionID, \"session.json\")); err != nil {\n\t\treturn err","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/agent/runtime.go#L201-L237","documentation":"Rejected by every siyuan.storage.* method (get/put/remove/list/watcher.add) when the resolved absolute path escapes the plugin's private storage directory (kernel/plugin/api_storage.go:43). resolvePath joins and cleans the input, then verifies the result is the storage dir itself or a child of it; '../' segments or absolute paths that clean to somewhere else fail this check. This is a deliberate sandbox escape prevention, so no error detail beyond the message is provided.","triggerScenarios":"put('../shared/config.json', data) trying to reach another plugin's storage; get('/etc/passwd') or any absolute path that is not under storageDir; watcher.add('..') ; paths like 'a/../../b' that clean outside the root after filepath.Clean collapses them.","commonSituations":"Sharing data between two plugins via relative ../ paths instead of a proper channel (rpc/event); user-typed paths pasted into plugin settings; path building with template strings like `${userInput}/notes` where userInput contains '..'; Windows drive-letter or UNC paths.","solutions":["Keep all storage keys relative to the plugin storage root and drop any leading '/' or '..' segments","Sanitize user-supplied paths before use: strip '..' components or reject them","For cross-plugin data, use siyuan.rpc or siyuan.event instead of filesystem traversal","Normalize with a helper that returns null for escaping paths and prompt for a new one"],"exampleFix":"// before\nawait siyuan.storage.put(`../other-plugin/${name}`, data);\n\n// after\nconst safeKey = name.split('/').filter(s => s && s !== '.' && s !== '..').join('/');\nawait siyuan.storage.put(safeKey, data);","handlingStrategy":"validation","validationCode":"const safeStorageKey = (p) => {\n  if (typeof p !== 'string') return null;\n  const parts = p.split(/[\\\\/]+/).filter((s) => s && s !== '.' && s !== '..');\n  return parts.length ? parts.join('/') : null;\n};\nconst key = safeStorageKey(userPath);\nif (key) await siyuan.storage.put(key, data); else throw new Error('invalid storage path');","typeGuard":"const isSafeStoragePath = (p) => typeof p === 'string' && p.length > 0 && !p.split(/[\\\\/]+/).includes('..') && !/^[a-zA-Z]:/.test(p) && !p.startsWith('\\\\\\\\');","tryCatchPattern":"try { await siyuan.storage.put(key, data); } catch (e) { if (/path traversal/.test(e.message)) throw new Error(`rejected unsafe storage path: ${key}`); else throw e; }","preventionTips":["Treat storage keys as relative identifiers, never filesystem paths","Sanitize user input by stripping '..' segments before any storage call","Use siyuan.rpc/siyuan.event for cross-plugin data instead of ../ paths","Never feed absolute OS paths into siyuan.storage"],"tags":["storage","plugin-api","path-traversal","security","sandbox","siyuan"],"backgroundTag":"path-traversal-blocked","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","schemaVersion":2},"datasetVersion":"2026-08-24T22:17:12.610Z"}