{"record":{"id":"087fb04cd5f21311","repo":"Tencent/WeKnora","slug":"sandbox-session-input-s-is-read-only","errorCode":null,"errorMessage":"sandbox: session input %s is read-only","messagePattern":"sandbox: session input (.+?) is read-only","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sandbox/session_manager.go","lineNumber":1031,"sourceCode":"\t)\n}\n\n// cleanSessionWorkspaceWritePath keeps model-authored writes inside the\n// session workspace and out of the attachment tree. Validation is lexical\n// (path.Clean plus prefix checks), matching cleanSessionWorkDir.\nfunc cleanSessionWorkspaceWritePath(filePath string) (string, error) {\n\tclean := path.Clean(strings.TrimSpace(filePath))\n\tif !path.IsAbs(clean) || clean == \".\" || clean == \"/\" {\n\t\treturn \"\", fmt.Errorf(\"sandbox: workspace write path %q must be an absolute file path\", filePath)\n\t}\n\tif clean == SessionWorkspaceRoot || clean == SessionOutputRoot || clean == SessionInputRoot {\n\t\treturn \"\", fmt.Errorf(\"sandbox: workspace write path %q is a directory, not a file\", filePath)\n\t}\n\tif !strings.HasPrefix(clean, SessionWorkspaceRoot+\"/\") {\n\t\treturn \"\", fmt.Errorf(\"sandbox: workspace write path %q is outside %s\", filePath, SessionWorkspaceRoot)\n\t}\n\tif strings.HasPrefix(clean, SessionInputRoot+\"/\") {\n\t\treturn \"\", fmt.Errorf(\"sandbox: session input %s is read-only\", SessionInputRoot)\n\t}\n\treturn clean, nil\n}\n\n// cleanSessionWorkDir keeps shell_exec inside directories we are willing to let\n// an agent work in. Ordinary sessions get /workspace only.\n//\n// Validation is lexical (path.Clean plus prefix checks): a symlink under an\n// allowed root that resolves elsewhere at execution time is not detected and\n// that is intentional. The only caller that passes allowSkillsRoot also passes\n// AsRoot and runs arbitrary install shell commands, so a symlink would grant\n// nothing those commands cannot already reach via cd or absolute paths. For\n// ordinary sessions the allowlist is unchanged and its lexical nature is\n// pre-existing. The allowlist stops casual wandering and makes intent\n// auditable; the real isolation boundary is the remote sandbox itself.\n//\n// allowSkillsRoot widens it to the skills image root for install/maintenance\n// sessions, so the installer agent can set work_dir to the skill directory and","sourceCodeStart":1013,"sourceCodeEnd":1049,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/sandbox/session_manager.go#L1013-L1049","documentation":"cleanSessionWorkspaceWritePath rejects any write target under /workspace/input (SessionInputRoot). The input root holds durable user attachments restored read-only into the session; the library throws this to prevent the agent or caller from overwriting user-supplied inputs.","triggerScenarios":"Calling WriteSessionWorkspaceFile with a path like \"/workspace/input/notes.txt\" — the path passes the /workspace prefix check but fails the read-only input-root check.","commonSituations":"Agent-generated code that writes results next to where it read attachments; scripts that treat /workspace/input like any other scratch directory; tools that default outputs to the same directory as their inputs.","solutions":["Write the output elsewhere under /workspace, e.g. /workspace/output/.","If you need a modified copy of an input, copy it to /workspace and edit the copy.","Update agent prompts/system instructions to state that /workspace/input is read-only."],"exampleFix":"// before\np := \"/workspace/input/report.txt\"\n// after\np := \"/workspace/output/report.txt\"","handlingStrategy":"validation","validationCode":"func writablePath(p string) bool {\n    c := filepath.Clean(p)\n    return strings.HasPrefix(c, \"/workspace/\") && !strings.HasPrefix(c, \"/workspace/input/\")\n}\nif !writablePath(p) { /* redirect output before calling */ }","typeGuard":"func isReadOnlyInputPath(p string) bool {\n    c := filepath.Clean(p)\n    return c == \"/workspace/input\" || strings.HasPrefix(c, \"/workspace/input/\")\n}","tryCatchPattern":"err := mgr.WriteSessionWorkspaceFile(ctx, s, path, data)\nif errors.Is(err, errReadOnly) || strings.Contains(err.Error(), \"read-only\") {\n    path = strings.Replace(path, \"/workspace/input/\", \"/workspace/output/\", 1)\n    err = mgr.WriteSessionWorkspaceFile(ctx, s, path, data)\n}","preventionTips":["Treat /workspace/input as immutable; always write results to /workspace/output.","State the read-only rule in agent system prompts.","Derive output paths from input paths programmatically instead of reusing them."],"tags":["sandbox","read-only-path","security"],"backgroundTag":"read-only-filesystem","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}