Repository navigation
Conversation
LibreChat skills upload files with relative paths like
"<skillName>/SKILL.md", but every layer flattened them to a basename,
breaking skill directory structure, relative references and causing
cross-skill basename collisions.
Fix flattening across all four layers:
- output.py: add `_is_safe_relative_path` + `sanitize_filepath`
(per-segment sanitize, preserves subdirs; falls back to basename for
unsafe/traversal/absolute/dot inputs). `sanitize_filename` (basename
flatten) retained for output filenames.
- api/files.py: `/upload` and `/upload/batch` use `sanitize_filepath`.
- docker/runner/files.go: Go's multipart parser strips directories per
RFC 7578, so HandleUpload now reads the relative path out-of-band from
a `path` form field (paired by index), MkdirAll's the parent, and
validates via validatePath (authoritative traversal guard; dotfile
rejection preserved). HandleList rewritten with WalkDir (recursive,
relative slash paths).
- manager.py / job_executor.py: send `data={"path": filename}` alongside
the file part so the runner can reconstruct the nested path.
- runner.py: mounted-filename set includes full relative path + basename;
generated-file filtering matches by relative-path-under-/mnt/data or
basename.
Tests: nested round-trip, traversal/absolute/dot fallback, per-segment
sanitize, recursive list, and a LibreChat nested-path upload contract.
Note: the Go runner is baked into all 12 language images, so this fix
requires rebuilding every language image.
Skill/agent bundles are uploaded with read_only=true but the flag was a placeholder: read-only inputs the sandbox touched surfaced as ghost download artifacts, and there was no way to distinguish inherited inputs from generated outputs. - models/files.py: FileInfo carries read_only (default False). - file.py / interfaces.py: store_uploaded_file persists read_only on file metadata; get_file_info reads it back (legacy files default False). - api/files.py: batch /upload threads its read_only form field to storage. - orchestrator.py: mounted file dicts carry read_only (explicit + auto-mount + cross-session consolidation); _build_response echoes read-only inputs in a new ExecResponse.inherited_files list, each FileRef marked inherited=true. - runner.py: _filter_generated_files excludes read-only inputs from the generated files list even when modified (suppresses ghost downloads), via a new _get_readonly_filenames expansion. inherited_files is deliberately separate from files[]: @librechat/agents 3.2.35 maps every files[] entry into a user download artifact and ignores the inherited flag, so emitting inherited entries there would re-create the ghosts. The separate field is forward-compatible for consumers that read it. Tests: read-only suppression in the runner, inherited_files assembly in the orchestrator, batch read_only persistence + default, updated consolidation contract.
Thread kind/id/version/read_only through the single-file /upload handler so it reaches identity parity with /upload/batch. read_only is persisted on the stored file; kind and id land on the session metadata. Existing direct-call unit tests updated to pass the new Form params explicitly, matching the batch test convention.
LibreChat's deleteFile issues DELETE /sessions/{session}/objects/{file}
first and only falls back to DELETE /files/{session}/{file} on failure.
Serving the objects path directly (delegating to delete_file) avoids the
extra 404 round-trip. kind/id/version query params are accepted for
parity with buildCodeEnvDownloadQuery.
* Build the runner image synchronously before other language images. * Prevents build failures due to missing runner image during subsequent builds. * Adds error handling for missing Dockerfile and build failures.
…ract) LibreChat 0.8.7 pins @librechat/agents ^3.2.46, whose CodeSessionFileSummary filters files[] by `file.inherited !== true`, and the host (callbacks.js / tools.js) already skips `if (file.inherited)` when creating user downloads and re-downloads. The canonical wire contract is therefore to echo read-only inputs INSIDE files[] flagged inherited:true — not in a separate field. Replace the separate ExecResponse.inherited_files field with inherited:true entries appended to files[] (orchestrator._build_response). Matches the SaaS code-interpreter reference and what final LibreChat + agents 3.2.46 consume. The runner still excludes read-only inputs from the generated list, so each read-only input appears exactly once (as an inherited entry).
… mode)
@librechat/agents 3.2.46 ships ProgrammaticToolCalling, which POSTs to
${baseURL}/exec/programmatic so the model can write Python that calls
LibreChat tools as async functions. KubeCodeRun execution pods have no
network egress (NetworkPolicy egress: []), so the live-callback bridge is
impossible here; this implements the stateless replay model used by the
reference code-interpreter service instead.
Each round the sandbox re-runs the user code with an injected history file
(_ptc_history.json: call_id -> prior result). A cached call returns
immediately; the first un-cached call is printed inside an execution-scoped
stdout sentinel and the process exits. The API relays that call to LibreChat
and continues via continuation_token + tool_results until the code completes.
- models/programmatic.py: request/response wire models (language/lang alias,
tool_calls {id,name,input}, tool_results {call_id,result,is_error}).
- services/programmatic/: constants (sentinels, reserved-filename guard),
harness (replay preamble + tool stubs + async wrap), sentinel parser,
Redis-backed continuation state, and the replay orchestration service.
- api/programmatic.py: POST /exec/programmatic (auth + user_id like /exec).
- orchestrator: internal extra_files (inject the history file) and
capture_state_override (PTC runs are stateless).
Bash PTC is rejected with a clear 400 for now (Python only). Forged/un-issued
tool_result call_ids are dropped; unregistered tool calls and empty pending
blocks return status=error to avoid tight retry loops.
There was a problem hiding this comment.
Pull request overview
This PR updates KubeCodeRun to match LibreChat v0.8.7’s wire contract more closely, closing several compatibility gaps around skill file paths, read-only/inherited inputs, upload/delete parity, and adding Programmatic Tool Calling via a replay-loop endpoint.
Changes:
- Add Programmatic Tool Calling (
POST /exec/programmatic) with a Python replay harness, sentinel parsing, and Redis-backed continuation state. - Preserve safe nested upload paths end-to-end (Python upload sanitization, K8s file copy, Go runner upload + recursive listing) and adjust generated-file filtering to recognize nested mounted/read-only inputs.
- Persist
read_onlyon stored files and echo read-only mounted inputs back asfiles[]entries withinherited: true; add a LibreChat-compatible delete alias route.
Reviewed changes
Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_programmatic_service.py | New unit tests covering PTC replay loop behavior, continuation, and validation cases. |
| tests/unit/test_programmatic_harness.py | Tests for harness generation, sentinel markers, and reserved filename handling. |
| tests/unit/test_output_processor.py | Adds coverage for nested-path-preserving sanitize_filepath. |
| tests/unit/test_orchestrator.py | Verifies read-only mounted inputs are echoed as inherited file refs. |
| tests/unit/test_librechat_contract.py | Contract tests for inherited file refs, upload identity/read_only threading, and PTC response shapes. |
| tests/unit/test_execution_runner.py | Tests nested mounted filename matching and read-only exclusion from generated artifacts. |
| tests/unit/test_cross_session_files.py | Updates mocks/calls for new upload fields and read_only in file info. |
| tests/unit/test_api_files.py | Adds coverage for the delete alias route and updates upload calls for new fields. |
| src/services/programmatic/state.py | Introduces Redis-backed continuation state model + merge logic for tool results. |
| src/services/programmatic/service.py | Implements the replay-loop orchestration over the existing ExecutionOrchestrator. |
| src/services/programmatic/sentinel.py | Adds stdout sentinel parsing to extract pending tool calls and clean user output. |
| src/services/programmatic/harness.py | Generates the Python replay harness/preamble and async wrapping for user code. |
| src/services/programmatic/constants.py | Defines PTC limits, sentinel building, reserved filename checks, and history constants. |
| src/services/programmatic/init.py | Exposes PTC service/state types for import. |
| src/services/orchestrator.py | Adds support for inline extra files + stateless override; threads read_only and emits inherited file refs. |
| src/services/kubernetes/manager.py | Sends nested paths out-of-band (path field) when uploading files to runner. |
| src/services/kubernetes/job_executor.py | Same out-of-band path field for job-mode runner uploads. |
| src/services/interfaces.py | Extends file storage interface to persist read_only. |
| src/services/file.py | Persists and reloads read_only from file metadata. |
| src/services/execution/runner.py | Enhances generated-file detection/filtering for nested paths + read-only exclusion. |
| src/services/execution/output.py | Adds sanitize_filepath to preserve safe nested upload paths segment-by-segment. |
| src/models/programmatic.py | Adds request/response models for the /exec/programmatic endpoint. |
| src/models/files.py | Adds read_only to FileInfo. |
| src/models/exec.py | Adds inherited flag to FileRef for LibreChat contract behavior. |
| src/main.py | Registers the new programmatic router. |
| src/api/programmatic.py | Adds /exec/programmatic endpoint with auth/user-id resolution and error mapping. |
| src/api/files.py | Adds upload identity/read_only fields, uses path-preserving sanitization, and adds delete alias route. |
| scripts/build-images.sh | Ensures the runner image is built first since other images depend on it. |
| docker/runner/files.go | Supports nested upload paths via path field and makes file listing recursive with relative paths. |
| docker/runner/files_test.go | Adds coverage for nested uploads, traversal rejection, and recursive listing. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… validation) - sentinel: match the marker line exactly (tolerating only a trailing CR for CRLF-captured stdout) instead of strip(), so user output that merely contains the marker padded by whitespace can't be mistaken for a sentinel or shift the byte-accurate stdout reconstruction. - service: validate tool names up front — reject empty/whitespace names and names with no identifier-safe characters (which would emit `async def ()` and surface as a confusing "completed" syntax error), and reject two distinct names that normalize to the same Python identifier. Returns a clear 400. - harness: expose normalize_python_function_name (was private) for reuse.
Contributor
Author
|
@aron-muon Any news here? |
aron-muon
added a commit
that referenced
this pull request
Sep 16, 2026
* feat(files): preserve nested skill file paths end-to-end
LibreChat skills upload files with relative paths like
"<skillName>/SKILL.md", but every layer flattened them to a basename,
breaking skill directory structure, relative references and causing
cross-skill basename collisions.
Fix flattening across all four layers:
- output.py: add `_is_safe_relative_path` + `sanitize_filepath`
(per-segment sanitize, preserves subdirs; falls back to basename for
unsafe/traversal/absolute/dot inputs). `sanitize_filename` (basename
flatten) retained for output filenames.
- api/files.py: `/upload` and `/upload/batch` use `sanitize_filepath`.
- docker/runner/files.go: Go's multipart parser strips directories per
RFC 7578, so HandleUpload now reads the relative path out-of-band from
a `path` form field (paired by index), MkdirAll's the parent, and
validates via validatePath (authoritative traversal guard; dotfile
rejection preserved). HandleList rewritten with WalkDir (recursive,
relative slash paths).
- manager.py / job_executor.py: send `data={"path": filename}` alongside
the file part so the runner can reconstruct the nested path.
- runner.py: mounted-filename set includes full relative path + basename;
generated-file filtering matches by relative-path-under-/mnt/data or
basename.
Tests: nested round-trip, traversal/absolute/dot fallback, per-segment
sanitize, recursive list, and a LibreChat nested-path upload contract.
Note: the Go runner is baked into all 12 language images, so this fix
requires rebuilding every language image.
* feat(files): enforce read_only inputs and echo inherited passthroughs
Skill/agent bundles are uploaded with read_only=true but the flag was a
placeholder: read-only inputs the sandbox touched surfaced as ghost
download artifacts, and there was no way to distinguish inherited inputs
from generated outputs.
- models/files.py: FileInfo carries read_only (default False).
- file.py / interfaces.py: store_uploaded_file persists read_only on file
metadata; get_file_info reads it back (legacy files default False).
- api/files.py: batch /upload threads its read_only form field to storage.
- orchestrator.py: mounted file dicts carry read_only (explicit + auto-mount
+ cross-session consolidation); _build_response echoes read-only inputs in
a new ExecResponse.inherited_files list, each FileRef marked inherited=true.
- runner.py: _filter_generated_files excludes read-only inputs from the
generated files list even when modified (suppresses ghost downloads),
via a new _get_readonly_filenames expansion.
inherited_files is deliberately separate from files[]: @librechat/agents
3.2.35 maps every files[] entry into a user download artifact and ignores
the inherited flag, so emitting inherited entries there would re-create the
ghosts. The separate field is forward-compatible for consumers that read it.
Tests: read-only suppression in the runner, inherited_files assembly in the
orchestrator, batch read_only persistence + default, updated consolidation
contract.
* feat(files): add identity form fields to single /upload
Thread kind/id/version/read_only through the single-file /upload handler
so it reaches identity parity with /upload/batch. read_only is persisted
on the stored file; kind and id land on the session metadata. Existing
direct-call unit tests updated to pass the new Form params explicitly,
matching the batch test convention.
* feat(files): add DELETE /sessions/{session}/objects/{file} alias
LibreChat's deleteFile issues DELETE /sessions/{session}/objects/{file}
first and only falls back to DELETE /files/{session}/{file} on failure.
Serving the objects path directly (delegating to delete_file) avoids the
extra 404 round-trip. kind/id/version query params are accepted for
parity with buildCodeEnvDownloadQuery.
* feat(build): ensure runner image builds first as dependency
* Build the runner image synchronously before other language images.
* Prevents build failures due to missing runner image during subsequent builds.
* Adds error handling for missing Dockerfile and build failures.
* refactor(files): echo inherited inputs inside files[] (canonical contract)
LibreChat 0.8.7 pins @librechat/agents ^3.2.46, whose CodeSessionFileSummary
filters files[] by `file.inherited !== true`, and the host
(callbacks.js / tools.js) already skips `if (file.inherited)` when creating
user downloads and re-downloads. The canonical wire contract is therefore to
echo read-only inputs INSIDE files[] flagged inherited:true — not in a
separate field.
Replace the separate ExecResponse.inherited_files field with inherited:true
entries appended to files[] (orchestrator._build_response). Matches the SaaS
code-interpreter reference and what final LibreChat + agents 3.2.46 consume.
The runner still excludes read-only inputs from the generated list, so each
read-only input appears exactly once (as an inherited entry).
* feat(exec): add /exec/programmatic (Programmatic Tool Calling, replay mode)
@librechat/agents 3.2.46 ships ProgrammaticToolCalling, which POSTs to
${baseURL}/exec/programmatic so the model can write Python that calls
LibreChat tools as async functions. KubeCodeRun execution pods have no
network egress (NetworkPolicy egress: []), so the live-callback bridge is
impossible here; this implements the stateless replay model used by the
reference code-interpreter service instead.
Each round the sandbox re-runs the user code with an injected history file
(_ptc_history.json: call_id -> prior result). A cached call returns
immediately; the first un-cached call is printed inside an execution-scoped
stdout sentinel and the process exits. The API relays that call to LibreChat
and continues via continuation_token + tool_results until the code completes.
- models/programmatic.py: request/response wire models (language/lang alias,
tool_calls {id,name,input}, tool_results {call_id,result,is_error}).
- services/programmatic/: constants (sentinels, reserved-filename guard),
harness (replay preamble + tool stubs + async wrap), sentinel parser,
Redis-backed continuation state, and the replay orchestration service.
- api/programmatic.py: POST /exec/programmatic (auth + user_id like /exec).
- orchestrator: internal extra_files (inject the history file) and
capture_state_override (PTC runs are stateless).
Bash PTC is rejected with a clear 400 for now (Python only). Forged/un-issued
tool_result call_ids are dropped; unregistered tool calls and empty pending
blocks return status=error to avoid tight retry loops.
* fix(programmatic): address PR review (exact sentinel match, tool-name validation)
- sentinel: match the marker line exactly (tolerating only a trailing CR for
CRLF-captured stdout) instead of strip(), so user output that merely contains
the marker padded by whitespace can't be mistaken for a sentinel or shift the
byte-accurate stdout reconstruction.
- service: validate tool names up front — reject empty/whitespace names and
names with no identifier-safe characters (which would emit `async def ()` and
surface as a confusing "completed" syntax error), and reject two distinct
names that normalize to the same Python identifier. Returns a clear 400.
- harness: expose normalize_python_function_name (was private) for reuse.
* fix(programmatic): make harness generation robust to hostile tool schemas and user code
Three harness-generation bugs, each reproduced before fixing:
- Parameter names from tool JSON schemas were interpolated raw into the
stub signature and input dict: a name like "user-name" generated
`async def search(user-name: str)` — a syntax error surfacing as a
confusing 'completed' response with a traceback. Property names are now
normalized with normalize_python_function_name (dispatch still sends the
original name, JSON-escaped, as the input key), and requests whose
parameter names have no identifier-safe characters or collide after
normalization get a clear 400.
- Tool descriptions ending in '"' (or containing quote runs) terminated
the generated docstring literal early. Every quote is now escaped, not
just literal triple-quote sequences.
- wrap_user_code_in_async re-indented every user line by 4 spaces, which
corrupts the *content* of multi-line string literals ('line1\nline2'
became 'line1\n line2'). User code is now embedded verbatim as a
string literal and compiled at runtime with PyCF_ALLOW_TOP_LEVEL_AWAIT,
preserving bytes exactly, keeping module-level scoping (global,
top-level def/class) and traceback line numbers, while still supporting
top-level await.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(programmatic): injected replay history must override same-named session files
_mount_files appended extra_files only when no mounted file shared the
name, so a session file named _ptc_history.json (uploadable via /upload —
the reserved-name check in ProgrammaticService only covers request.files)
silently shadowed the injected replay history. That hands replay control
to session content: forged tool results, or a stale history that re-emits
the same tool_call_required forever and burns the client's 20-round cap.
Extra files now replace same-named mounted entries. The history file is
runtime infrastructure; nothing a session contains may impersonate it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(programmatic): report truncated-sentinel runs as errors, not completed
The PTC sentinel is the last thing the harness prints, and pod stdout is
capped at 64KB by OutputProcessor.sanitize_output — so any run that
prints enough before its first tool call gets its sentinel cut off and
was previously misreported as status=completed with truncated stdout,
silently dropping the pending tool call.
When no sentinel is found and stdout carries the truncation notice, the
service now returns a structured status=error telling the model to print
less before calling tools, and drops the continuation state. The notice
string is promoted to OutputProcessor.TRUNCATION_NOTICE so the two sites
cannot drift.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(programmatic): harden the continuation contract against the real client behavior
Verified against @librechat/agents 3.2.46 source (ProgrammaticToolCalling
+ Bash variant) and LibreChat v0.8.7:
- Bind continuation tokens to the identity that started the execution.
A token presented under a different user_id now gets the same
'not found or expired' error as an unknown token, instead of resuming
someone else's replay state under the shared API key.
- Mid-loop failures (unknown/expired token, replay-call cap) now return
HTTP 200 with status='error'. The agents client's makeRequest throws on
any non-2xx with an unstructured 'CodeAPI request failed' message and
never reads the body's error field; only the 200 + status:'error' shape
takes its structured path. Up-front request validation (bad tool names,
missing code) keeps returning 400 - that path is developer-facing.
- Treat any body carrying continuation_token as a continuation. Requiring
tool_results too meant a token-only body was misread as an initial
request and rejected with a misleading 'Missing required field: code'.
- Raise the continuation-state sliding TTL from 600s to 3600s: the client
caps the loop at 20 round trips but has NO timeout between rounds -
each gap lasts as long as the host takes to run the requested tool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(runner): reject hidden path segments and make multi-field uploads deterministic
Nested upload paths reopened a hole the old basename-only flow had
closed: only the final segment was checked for a leading dot, so
'.git/config' or 'skill/.ssh/authorized_keys' could materialize hidden
directories inside /mnt/data. Every segment of the relative path is now
checked.
Also iterate multipart field names in sorted order when pairing files
with the out-of-band 'path' values: Go map iteration is randomized, so a
request using more than one field name could nondeterministically
associate paths with the wrong files. The control plane sends one field
per request today, but correctness must not depend on that.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* style: ruff format
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(programmatic): execute the generated harness end-to-end in a subprocess
Round 1 with empty history must emit the scoped sentinel for the first
tool call and preserve user prints; round 2 with the cached result must
run to completion; a cached is_error result must raise inside user code.
This runs the real generated Python (via PTC_HISTORY_PATH), so harness,
sentinel parser and replay semantics are proven together rather than
assumed from string assertions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Gil Assunção <gil.assuncao@parceiros.nos.pt>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
🎉 This PR is included in version 3.7.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings KubeCodeRun to full wire-contract compatibility with LibreChat v0.8.7
(
@librechat/agents@^3.2.46). Closes the four skills/file gaps (G/A/C/B), adoptsthe canonical
inheritedcontract, and adds Programmatic Tool Calling(
/exec/programmatic). A fifth gap (D) is intentionally deferred — see Deferred.Both auth paths are preserved unchanged: the model-facing
execute_code/programmatic tools keep using
X-API-Key, and host-side file/skill operationskeep using
BearerJWT (EdDSA,iss=librechat,aud=codeapi).What changed
c8fe1cdread_only; echo passthroughs asinherited:trueinsidefiles[](canonical)a7d91d7,502abef/uploadidentity parity (kind/id/version/read_only) with/upload/batch390360aDELETE /sessions/{session}/objects/{file}alias to avoid LibreChat's 404 fallback3c92153POST /exec/programmatic— Programmatic Tool Calling via the stateless replay modeld3409aaGap G — Preserve nested skill file paths
LibreChat skill bundles upload files whose multipart filename carries a relative
path (e.g.
skillName/SKILL.md). Previously these were flattened to thebasename at multiple layers, breaking the skill directory structure, relative
references, and
readSandboxFile('/mnt/data/skillName/SKILL.md'), and riskingcollisions. This required a multi-layer fix:
/upload+/upload/batch: preserve safe relative paths (per-segmentsanitize, mirroring LibreChat's
isSafeCodeEnvFilepath).pathfield on upload (os.MkdirAll,reusing the existing
validatePathtraversal guard — Go's multipart parserstrips directories per RFC 7578) and recurse on list.
Gap A — Enforce
read_only, echoinherited(canonical contract)read_onlypersists end-to-end (FileInfo.read_only,store_uploaded_file(read_only=...)); the executor excludes read-only mountedinputs from generated artifacts even when modified.
Gap C — Single
/uploadidentity parity/uploadnow acceptskind/id/version/read_onlyform fields like/upload/batch:read_onlyis persisted on the stored file;kind→ sessionmetadata["kind"],id→metadata["resource_id"].Gap B —
DELETE /sessions/{session}/objects/{file}aliasLibreChat's
deleteFileissues this path first and only falls back toDELETE /files/{session}/{file}on failure. The alias delegates to the samehandler;
kind/id/versionquery params are accepted for parity (notenforced).
Programmatic Tool Calling —
POST /exec/programmatic@librechat/agents@3.2.46shipsProgrammaticToolCalling, letting the modelwrite Python that calls LibreChat tools as async functions. KubeCodeRun
execution pods have no network egress (
NetworkPolicy egress: []), so thelive-callback bridge is impossible here — this implements the stateless replay
model from the reference
code-interpreterservice:{ code, tools, session_id?, files? }→ the sandbox runs harnessedcode (tool stubs + async wrap) with an injected
_ptc_history.json.sentinel and the process exits → API returns
{ status: "tool_call_required", tool_calls: [{id,name,input}], continuation_token }.{ continuation_token, tool_results };the result is merged into history and the code re-runs — until it completes
(
{ status: "completed", stdout, stderr, files }).Security: forged/un-issued
tool_resultcall_ids are dropped; unregisteredtool calls and empty pending blocks return
status:"error"; user files named_ptc_history.json(or traversal paths) are rejected. Bash PTC returns aclear 400 for now (Python only).
Deferred — Gap D (deterministic sessionKey bucketing)
Intentionally not implemented. It is an optimization (cross-user skill/agent
bucket sharing + cache hits), not a compatibility requirement — no LibreChat
v0.8.7 flow breaks without it. It would also deliberately reverse KubeCodeRun's
per-user isolation invariant (
test_upload_does_NOT_reuse_session_for_different_user)for skill/agent kinds, adding cross-user exposure for little reward in the
effectively single-tenant prod deployment. If pursued later, implement behind a
default-off flag (e.g.
CODEAPI_DETERMINISTIC_BUCKETS) with dedicated cross-userisolation tests and outputs always keyed
<ns>:user:<userId>.