Files
zeekay 9539f5a380 code interpreter: a sandbox with a session lease, run as the signed-in user
execute_code no longer POSTs /v1/exec behind a shared service key. It leases a
Hanzo sandbox (cloud /v1/sandboxes) with the CALLER's own hanzo.id bearer, runs
the program there, and reports what it printed and what it wrote.

The session IS the sandbox. `session_id` is the sandbox id, the artifact
directory is its workdir, an expired session is a lease the reaper ended. There
is no session table here because there is no second object to keep in sync.

Why the key had to be deleted rather than fixed
-----------------------------------------------
A shared service key IS custom auth, and it is also why that surface could not
bill: it carries no tenant, so cloud could not tell whose org a run belonged to
— apps/exec's own header says so. With the user's bearer the run lands on the
account that ran it, because the account is the only thing the call carries.
It was never even wired: `EnvVar.CODE_API_KEY` does not exist in the shipped
@hanzochat/agents, so `authFields: [undefined]` at every call site and setting
the env var was a silent no-op. Deleted at all ~20 sites, plus the guard in
loadAuthValues that existed only to survive the `undefined`, plus the client's
key-entry path in the tool-auth contract (`fieldsMap[execute_code]` is now `[]`
and verifyToolAuth never consults the credential store for it).

What the tool does now
----------------------
One round trip. The program rides the exec channel's stdin, so the source is
never quoted into a command line. Its stdout/stderr are redirected to files, so
the command's own stdout carries only four framed lines — exit code, then base64
of out, err and the new-file list. That is unforgeable by construction: a
delimiter would let a program that printed the delimiter name files it never
wrote. Scratch lives in /tmp, never in /mnt/data, so the artifact scan cannot
mistake this tool's own source for a user's output.

The language menu is measured against the sandbox image rather than inherited:
`d`, `f90` and `r` are dropped (no compiler), `rb` added, `ts` runs on bun.

Files in and out ride the same three verbs: upload is POST /:id/fs, download is
GET /:id/fs, and "is this session still alive?" is GET /:id — one question where
there used to be a `lastModified` fetch and a 23-hour freshness rule that only
together approximated it.

PTC is declared and not executable: it is a different protocol (it suspends the
run and resumes from a continuation token) and cloud answers it 501, so offering
a tool that fails mid-run is worse than not offering one.

De-branding: user-facing prose first, wire second. The `LIBRECHAT_TEST_RUN_HOOK`
env var and the `@librechat/*` requires are renamed because BOTH ends live in
this repo. The `application/vnd.librechat.*` artifact MIMEs are NOT renamed —
they are identifiers on the wire, no user reads them, and a rename needs a
migration story it would not buy anything. NOTICE and LICENSE are untouched.
2026-08-06 19:40:02 -07:00

85 lines
3.6 KiB
JavaScript

const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { enrichWithSkillConfigurable } = require('@hanzochat/api');
const db = require('~/models');
/**
* Builds the `skillPrimedIdsByName` map passed through to
* `enrichWithSkillConfigurable`. Centralized here so the four CJS call
* sites (`initialize.js`, `responses.js` x2, `openai.js`) share one
* source of truth — if `ResolvedManualSkill` ever renames `_id` or
* gains new identifying fields, only this helper changes.
*
* Combines both manual (`$`-popover) primes AND always-apply primes so
* `read_file` can:
* - Relax the `disable-model-invocation: true` gate for either source
* (the body is already in context; blocking its own files would be
* nonsensical).
* - Pin same-name collision lookups to the exact `_id` the resolver
* primed (otherwise a newer same-name duplicate could shadow the
* body/file pair within a single turn).
*
* On the rare overlap (a name appears in both arrays because upstream
* dedup was skipped), manual wins — manual invocation is explicit user
* intent and carries the authoritative `_id` for this turn.
*
* Returns `undefined` (not `{}`) when both arrays are empty, so the
* downstream `enrichWithSkillConfigurable` cleanly omits the field from
* `mergedConfigurable` rather than threading an empty object.
*
* @param {Array<{ name: string, _id: { toString(): string } }> | undefined} manualSkillPrimes
* @param {Array<{ name: string, _id: { toString(): string } }> | undefined} alwaysApplySkillPrimes
* @returns {Record<string, string> | undefined}
*/
function buildSkillPrimedIdsByName(manualSkillPrimes, alwaysApplySkillPrimes) {
const manualCount = manualSkillPrimes?.length ?? 0;
const alwaysApplyCount = alwaysApplySkillPrimes?.length ?? 0;
if (manualCount === 0 && alwaysApplyCount === 0) {
return undefined;
}
const out = {};
/* Order matters on the edge case where the same name appears in both
lists: always-apply goes in first, then manual overwrites — manual
wins because it's explicit user intent for this turn. */
if (alwaysApplyCount > 0) {
for (const p of alwaysApplySkillPrimes) {
out[p.name] = p._id.toString();
}
}
if (manualCount > 0) {
for (const p of manualSkillPrimes) {
out[p.name] = p._id.toString();
}
}
return out;
}
/** Skill-related properties for ToolExecuteOptions (stable references, allocated once). */
const skillToolDeps = {
getSkillByName: db.getSkillByName,
listSkillFiles: db.listSkillFiles,
getStrategyFunctions,
updateSkillFileCodeEnvIds: db.updateSkillFileCodeEnvIds,
getSkillFileByPath: db.getSkillFileByPath,
updateSkillFileContent: db.updateSkillFileContent,
/* `batchUploadCodeEnvFiles`, `getSessionInfo`, `checkIfActive` and
* `readSandboxFile` used to be listed here and NONE of them existed: three were
* never exported by the modules they were destructured from and the fourth had a
* different signature than `primeSkillFiles` declares. They were `undefined` in
* this bag on every request. Listing a dependency that is not there is worse than
* not listing it — `primeSkillFiles` treats the optional ones as "skip the
* check", which is what it already did, and the required one throws at the point
* of use instead of at wiring time. Skill FILES therefore do not reach the code
* environment; skill bodies are unaffected. That is the honest state, and it is
* a separate piece of work from this one. */
};
function getSkillToolDeps() {
return skillToolDeps;
}
module.exports = {
getSkillToolDeps,
enrichWithSkillConfigurable,
buildSkillPrimedIdsByName,
};