siyuan-note/siyuan · error

invalid agent runtime turn state

Error message

invalid agent runtime turn state

What it means

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.

Source

Thrown at kernel/agent/runtime.go:219

		return nil, err
	}
	if runtime.SchemaVersion > 1 {
		return nil, fmt.Errorf("unsupported agent runtime schema version: %d", runtime.SchemaVersion)
	}
	if runtime.SessionID != "" && runtime.SessionID != sessionID {
		return nil, fmt.Errorf("agent runtime session id mismatch")
	}
	if runtime.Revision < 0 {
		return nil, fmt.Errorf("invalid agent runtime revision")
	}
	if runtime.ActiveTurn != nil {
		if runtime.ActiveTurn.TurnID == "" {
			return nil, fmt.Errorf("invalid agent runtime turn id")
		}
		switch runtime.ActiveTurn.State {
		case "running", "finished", "interrupted":
		default:
			return nil, fmt.Errorf("invalid agent runtime turn state")
		}
	}
	if runtime.SchemaVersion == 0 {
		runtime.SchemaVersion = 1
	}
	if runtime.SessionID == "" {
		runtime.SessionID = sessionID
	}
	return &runtime, nil
}

func writeRuntimeLocked(sessionID string, runtime *agentRuntime) error {
	if runtime == nil {
		return nil
	}
	// runtime 只能附着在已经存在的会话上,避免迟到的 checkpoint 复活已删除会话。
	if _, err := os.Stat(filepath.Join(sessionsDir(), sessionID, "session.json")); err != nil {
		return err

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Keep all storage keys relative to the plugin storage root and drop any leading '/' or '..' segments
  2. Sanitize user-supplied paths before use: strip '..' components or reject them
  3. For cross-plugin data, use siyuan.rpc or siyuan.event instead of filesystem traversal
  4. Normalize with a helper that returns null for escaping paths and prompt for a new one

Example fix

// before
await siyuan.storage.put(`../other-plugin/${name}`, data);

// after
const safeKey = name.split('/').filter(s => s && s !== '.' && s !== '..').join('/');
await siyuan.storage.put(safeKey, data);
Defensive patterns

Strategy: validation

Validate before calling

const safeStorageKey = (p) => {
  if (typeof p !== 'string') return null;
  const parts = p.split(/[\\/]+/).filter((s) => s && s !== '.' && s !== '..');
  return parts.length ? parts.join('/') : null;
};
const key = safeStorageKey(userPath);
if (key) await siyuan.storage.put(key, data); else throw new Error('invalid storage path');

Type guard

const isSafeStoragePath = (p) => typeof p === 'string' && p.length > 0 && !p.split(/[\\/]+/).includes('..') && !/^[a-zA-Z]:/.test(p) && !p.startsWith('\\\\');

Try / catch

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; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/469c7213e873dfab. Report an issue: GitHub.