Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/code/src/native-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events';
import test from 'node:test';
import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { dirname, join } from 'node:path';
import type { ChildProcess, ForkOptions } from 'node:child_process';
import {
NativeProcessWorkspaceCommandSandbox,
Expand Down Expand Up @@ -199,6 +199,7 @@ test('programmatic executor resolves and scopes credentials to its command', asy
{
workspaceRoot: tmpdir(),
programmaticFileUpstream: 'http://127.0.0.1:3190',
environment: { PATH: '/sandbox-only' },
maskedEnvironment: {
variables: [{ name: 'TOKEN', injectHosts: ['github.com'] }],
async resolve() {
Expand All @@ -225,9 +226,18 @@ test('programmatic executor resolves and scopes credentials to its command', asy
JSON.stringify(fake.options).includes('per-programmatic-secret'),
false,
);
const message = fake.messages.find(
const message = fake.messages.find(
candidate => candidate.type === 'programmatic',
)!;
const prepareMessage = fake.messages.find(
candidate => candidate.type === 'prepare',
)!;
assert.equal(typeof prepareMessage.options.jqPath, 'string');
assert.equal(prepareMessage.options.jqPath.startsWith('/'), true);
assert.equal(
'/sandbox-only'.split(':').includes(dirname(prepareMessage.options.jqPath)),
false,
);
assert.deepEqual(message.credentials, { TOKEN: 'per-programmatic-secret' });
assert.equal(
message.wrappedCommand,
Expand Down
11 changes: 6 additions & 5 deletions packages/code/src/native-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export async function trustedProgrammaticExecutable(candidate: string, workspace

async function resolveProgrammaticShell(
options: NativeProcessSandboxOptions,
): Promise<string> {
): Promise<{ shellPath: string; jqPath: string }> {
const environment = options.environment ?? process.env;
const shellPath =
options.shellPath != null
Expand Down Expand Up @@ -99,7 +99,7 @@ async function resolveProgrammaticShell(
'COMMAND_UNAVAILABLE',
);
}
return shellPath;
return { shellPath, jqPath };
}

/** Only OS discovery and conventional proxy settings cross into the executor.
Expand Down Expand Up @@ -211,9 +211,9 @@ export class NativeProcessWorkspaceCommandSandbox implements WorkspaceCommandSan
}

private async start(): Promise<void> {
const programmaticShellPath = this.options.programmaticFileUpstream
const programmaticExecutables = this.options.programmaticFileUpstream
? await resolveProgrammaticShell(this.options)
: this.options.shellPath;
: undefined;
const child = this.forkExecutor(
new URL('./native-process-child.js', import.meta.url),
[],
Expand Down Expand Up @@ -299,7 +299,8 @@ export class NativeProcessWorkspaceCommandSandbox implements WorkspaceCommandSan
protectedPaths,
allowedDomains,
homeDirectory,
shellPath: programmaticShellPath ?? shellPath,
shellPath: programmaticExecutables?.shellPath ?? shellPath,
jqPath: programmaticExecutables?.jqPath,
programmaticFileUpstream,
variables: this.options.maskedEnvironment?.variables,
},
Expand Down
5 changes: 5 additions & 0 deletions packages/code/src/native-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export interface NativeSrtWorkspaceCommandSandboxOptions {
platform?: NodeJS.Platform;
/** Trusted shell path used by SRT on POSIX hosts. */
shellPath?: string;
/** Trusted jq path used by generated programmatic scripts. */
jqPath?: string;
/** Host-owned credentials exposed only as SRT sentinels inside the sandbox. */
maskedEnvironment?: {
variables: Array<{
Expand Down Expand Up @@ -702,6 +704,9 @@ export class NativeSrtWorkspaceCommandSandbox implements WorkspaceCommandSandbox
'_ptc_pending_result.json',
),
LIBRECHAT_CODE_BASH_PATH: this.options.shellPath ?? '/bin/bash',
...(this.options.jqPath
? { LIBRECHAT_CODE_JQ_PATH: this.options.jqPath }
: {}),
PTC_HISTORY_PATH: join(
canonicalDataDirectory,
'_ptc_history.json',
Expand Down
8 changes: 8 additions & 0 deletions service/src/preamble-bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ printf '%s\\n' "$_PTC_PENDING_FILE" "$_PTC_ERROR_FILE" "$_PTC_COUNTER_FILE"
rmSync(dir, { recursive: true, force: true });
}
});

test('uses the trusted jq path instead of resolving jq through PATH', () => {
const preamble = generateBashReplayPreamble({ executionId, tools });
expect(preamble).toContain(
'_PTC_JQ_PATH="${LIBRECHAT_CODE_JQ_PATH:-jq}"',
);
expect(preamble).not.toMatch(/(^|[|;(]\s*)jq\s/m);
});
});

describe('generateBashReplayPreamble - command substitution pending emission', () => {
Expand Down
27 changes: 14 additions & 13 deletions service/src/preamble-bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ _PTC_SENTINEL_START="${scopedStart}"
_PTC_SENTINEL_END="${scopedEnd}"
_PTC_HISTORY_PATH="\${PTC_HISTORY_PATH:-${PTC_HISTORY_SANDBOX_PATH}}"
_PTC_CONTROL_PATH="\${LIBRECHAT_CODE_CONTROL_PATH:-}"
_PTC_JQ_PATH="\${LIBRECHAT_CODE_JQ_PATH:-jq}"
_PTC_RUNTIME_DIR="\${TMPDIR:-/tmp}"
_ptc_mktemp() {
mktemp "\${_PTC_RUNTIME_DIR%/}/$1.XXXXXX"
Expand Down Expand Up @@ -222,7 +223,7 @@ _ptc_sha256() {

_ptc_hash_input() {
local _ptc_canonical
_ptc_canonical=$(printf '%s' "$1" | jq -cS . 2>/dev/null) || return 1
_ptc_canonical=$(printf '%s' "$1" | "$_PTC_JQ_PATH" -cS . 2>/dev/null) || return 1
printf '%s' "$_ptc_canonical" | _ptc_sha256
}

Expand Down Expand Up @@ -369,7 +370,7 @@ _ptc_maybe_emit_pending() {
return 0
fi
local _ptc_payload
if ! _ptc_payload=$(jq -c -s '{pending:.}' "$_PTC_PENDING_FILE" 2>/dev/null); then
if ! _ptc_payload=$("$_PTC_JQ_PATH" -c -s '{pending:.}' "$_PTC_PENDING_FILE" 2>/dev/null); then
printf 'failed to serialize pending PTC tool calls\\n' >&2
_ptc_cleanup_tempfiles
trap - DEBUG EXIT
Expand Down Expand Up @@ -480,7 +481,7 @@ _ptc_history_matches_by_signature() {
return 0
fi
# Path, not inline: large input can exceed ARG_MAX via --argjson.
jq -c \\
"$_PTC_JQ_PATH" -c \\
--arg nm "$_ptc_name" \\
--arg site "$_ptc_call_site" \\
--arg hash "$_ptc_input_hash" \\
Expand All @@ -504,7 +505,7 @@ _ptc_first_unconsumed_history_match() {
local _ptc_key
while IFS= read -r _ptc_match; do
[ -n "$_ptc_match" ] || continue
_ptc_key=$(printf '%s' "$_ptc_match" | jq -r '.key // empty' 2>/dev/null)
_ptc_key=$(printf '%s' "$_ptc_match" | "$_PTC_JQ_PATH" -r '.key // empty' 2>/dev/null)
if [ -n "$_ptc_key" ] && ! grep -Fxq "$_ptc_key" "$_PTC_CONSUMED_FILE" 2>/dev/null; then
printf '%s' "$_ptc_match"
return 0
Expand All @@ -516,15 +517,15 @@ _ptc_first_unconsumed_history_match() {
_ptc_print_history_entry() {
local _ptc_entry="$1"
local _ptc_is_err
_ptc_is_err=$(printf '%s' "$_ptc_entry" | jq -r 'if type == "object" then (.is_error // false) else false end' 2>/dev/null)
_ptc_is_err=$(printf '%s' "$_ptc_entry" | "$_PTC_JQ_PATH" -r 'if type == "object" then (.is_error // false) else false end' 2>/dev/null)
if [ "$_ptc_is_err" = "true" ]; then
local _ptc_msg
_ptc_msg=$(printf '%s' "$_ptc_entry" | jq -r '.error_message // "tool execution failed"' 2>/dev/null)
_ptc_msg=$(printf '%s' "$_ptc_entry" | "$_PTC_JQ_PATH" -r '.error_message // "tool execution failed"' 2>/dev/null)
_ptc_write_error "$_ptc_msg"
exit 1
fi
local _ptc_result
_ptc_result=$(printf '%s' "$_ptc_entry" | jq -c 'if type == "object" and has("result") then .result else . end' 2>/dev/null || printf 'null')
_ptc_result=$(printf '%s' "$_ptc_entry" | "$_PTC_JQ_PATH" -c 'if type == "object" and has("result") then .result else . end' 2>/dev/null || printf 'null')
printf '%s' "$_ptc_result"
return 0
}
Expand All @@ -535,7 +536,7 @@ _ptc_history_entry_matches_current_call() {
local _ptc_input_file="$3"
local _ptc_input_hash="$4"
# Path, same ARG_MAX reason as above.
printf '%s' "$_ptc_entry" | jq -e \\
printf '%s' "$_ptc_entry" | "$_PTC_JQ_PATH" -e \\
--arg nm "$_ptc_name" \\
--arg hash "$_ptc_input_hash" \\
--slurpfile inp_arr "$_ptc_input_file" \\
Expand All @@ -555,7 +556,7 @@ _ptc_call_tool() {
local _ptc_call_site="\${BASH_LINENO[1]:-\${BASH_LINENO[0]:-0}}"

# Reject extra trailing JSON values instead of silently dropping them.
if ! printf '%s' "$_ptc_input" | jq -e -n '[inputs] as $docs | ($docs | length) == 1 and ($docs[0] | type) == "object"' >/dev/null 2>&1; then
if ! printf '%s' "$_ptc_input" | "$_PTC_JQ_PATH" -e -n '[inputs] as $docs | ($docs | length) == 1 and ($docs[0] | type) == "object"' >/dev/null 2>&1; then
_ptc_write_error "tool input for $_ptc_name must be a single JSON object, got: $_ptc_input"
exit 1
fi
Expand Down Expand Up @@ -583,8 +584,8 @@ _ptc_call_tool() {
if [ -n "$_ptc_match" ] && [ "$_ptc_match" != "null" ]; then
local _ptc_matched_call_id
local _ptc_matched_entry
_ptc_matched_call_id=$(printf '%s' "$_ptc_match" | jq -r '.key' 2>/dev/null)
_ptc_matched_entry=$(printf '%s' "$_ptc_match" | jq -c '.value' 2>/dev/null)
_ptc_matched_call_id=$(printf '%s' "$_ptc_match" | "$_PTC_JQ_PATH" -r '.key' 2>/dev/null)
_ptc_matched_entry=$(printf '%s' "$_ptc_match" | "$_PTC_JQ_PATH" -c '.value' 2>/dev/null)
printf '%s\\n' "$_ptc_matched_call_id" >> "$_PTC_CONSUMED_FILE"
_ptc_mark_counter_at_least "$_ptc_matched_call_id"
_ptc_release_lock
Expand All @@ -598,7 +599,7 @@ _ptc_call_tool() {
while :; do
_ptc_call_id=$(_ptc_next_call_id)
if [ -r "$_PTC_HISTORY_PATH" ]; then
_ptc_entry=$(jq -c --arg id "$_ptc_call_id" '.[$id] // empty' "$_PTC_HISTORY_PATH" 2>/dev/null || printf '')
_ptc_entry=$("$_PTC_JQ_PATH" -c --arg id "$_ptc_call_id" '.[$id] // empty' "$_PTC_HISTORY_PATH" 2>/dev/null || printf '')
else
_ptc_entry=""
fi
Expand All @@ -614,7 +615,7 @@ _ptc_call_tool() {
fi
done

if ! printf '%s' "$_ptc_input" | jq -c -n \\
if ! printf '%s' "$_ptc_input" | "$_PTC_JQ_PATH" -c -n \\
--arg cid "$_ptc_call_id" \\
--arg nm "$_ptc_name" \\
--arg hash "$_ptc_input_hash" \\
Expand Down