Log resolved adapter command in run metadata
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
c114ff4dc6
commit
cadfcd1bc6
11 changed files with 274 additions and 21 deletions
|
|
@ -201,6 +201,33 @@ export function redactEnvForLogs(env: Record<string, string>): Record<string, st
|
||||||
return redacted;
|
return redacted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInvocationEnvForLogs(
|
||||||
|
env: Record<string, string>,
|
||||||
|
options: {
|
||||||
|
runtimeEnv?: NodeJS.ProcessEnv | Record<string, string>;
|
||||||
|
includeRuntimeKeys?: string[];
|
||||||
|
resolvedCommand?: string | null;
|
||||||
|
resolvedCommandEnvKey?: string;
|
||||||
|
} = {},
|
||||||
|
): Record<string, string> {
|
||||||
|
const merged: Record<string, string> = { ...env };
|
||||||
|
const runtimeEnv = options.runtimeEnv ?? {};
|
||||||
|
|
||||||
|
for (const key of options.includeRuntimeKeys ?? []) {
|
||||||
|
if (key in merged) continue;
|
||||||
|
const value = runtimeEnv[key];
|
||||||
|
if (typeof value !== "string" || value.length === 0) continue;
|
||||||
|
merged[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedCommand = options.resolvedCommand?.trim();
|
||||||
|
if (resolvedCommand) {
|
||||||
|
merged[options.resolvedCommandEnvKey ?? "PAPERCLIP_RESOLVED_COMMAND"] = resolvedCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redactEnvForLogs(merged);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildPaperclipEnv(agent: { id: string; companyId: string }): Record<string, string> {
|
export function buildPaperclipEnv(agent: { id: string; companyId: string }): Record<string, string> {
|
||||||
const resolveHostForUrl = (rawHost: string): string => {
|
const resolveHostForUrl = (rawHost: string): string => {
|
||||||
const host = rawHost.trim();
|
const host = rawHost.trim();
|
||||||
|
|
@ -269,6 +296,10 @@ async function resolveCommandPath(command: string, cwd: string, env: NodeJS.Proc
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveCommandForLogs(command: string, cwd: string, env: NodeJS.ProcessEnv): Promise<string> {
|
||||||
|
return (await resolveCommandPath(command, cwd, env)) ?? command;
|
||||||
|
}
|
||||||
|
|
||||||
function quoteForCmd(arg: string) {
|
function quoteForCmd(arg: string) {
|
||||||
if (!arg.length) return '""';
|
if (!arg.length) return '""';
|
||||||
const escaped = arg.replace(/"/g, '""');
|
const escaped = arg.replace(/"/g, '""');
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ import {
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
readPaperclipRuntimeSkillEntries,
|
readPaperclipRuntimeSkillEntries,
|
||||||
joinPromptSections,
|
joinPromptSections,
|
||||||
redactEnvForLogs,
|
buildInvocationEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
|
resolveCommandForLogs,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
|
|
@ -68,11 +69,13 @@ interface ClaudeExecutionInput {
|
||||||
|
|
||||||
interface ClaudeRuntimeConfig {
|
interface ClaudeRuntimeConfig {
|
||||||
command: string;
|
command: string;
|
||||||
|
resolvedCommand: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
workspaceId: string | null;
|
workspaceId: string | null;
|
||||||
workspaceRepoUrl: string | null;
|
workspaceRepoUrl: string | null;
|
||||||
workspaceRepoRef: string | null;
|
workspaceRepoRef: string | null;
|
||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
|
loggedEnv: Record<string, string>;
|
||||||
timeoutSec: number;
|
timeoutSec: number;
|
||||||
graceSec: number;
|
graceSec: number;
|
||||||
extraArgs: string[];
|
extraArgs: string[];
|
||||||
|
|
@ -236,6 +239,12 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||||
|
|
||||||
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME", "CLAUDE_CONFIG_DIR"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
|
@ -247,11 +256,13 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command,
|
command,
|
||||||
|
resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceRepoUrl,
|
workspaceRepoUrl,
|
||||||
workspaceRepoRef,
|
workspaceRepoRef,
|
||||||
env,
|
env,
|
||||||
|
loggedEnv,
|
||||||
timeoutSec,
|
timeoutSec,
|
||||||
graceSec,
|
graceSec,
|
||||||
extraArgs,
|
extraArgs,
|
||||||
|
|
@ -324,11 +335,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
command,
|
command,
|
||||||
|
resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
workspaceRepoUrl,
|
workspaceRepoUrl,
|
||||||
workspaceRepoRef,
|
workspaceRepoRef,
|
||||||
env,
|
env,
|
||||||
|
loggedEnv,
|
||||||
timeoutSec,
|
timeoutSec,
|
||||||
graceSec,
|
graceSec,
|
||||||
extraArgs,
|
extraArgs,
|
||||||
|
|
@ -440,11 +453,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "claude_local",
|
adapterType: "claude_local",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandArgs: args,
|
commandArgs: args,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
env: redactEnvForLogs(env),
|
env: loggedEnv,
|
||||||
prompt,
|
prompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ import {
|
||||||
asStringArray,
|
asStringArray,
|
||||||
parseObject,
|
parseObject,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
redactEnvForLogs,
|
buildInvocationEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
ensurePaperclipSkillSymlink,
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
readPaperclipRuntimeSkillEntries,
|
readPaperclipRuntimeSkillEntries,
|
||||||
|
resolveCommandForLogs,
|
||||||
resolvePaperclipDesiredSkillNames,
|
resolvePaperclipDesiredSkillNames,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
joinPromptSections,
|
joinPromptSections,
|
||||||
|
|
@ -383,6 +384,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
const billingType = resolveCodexBillingType(effectiveEnv);
|
const billingType = resolveCodexBillingType(effectiveEnv);
|
||||||
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
|
@ -490,14 +497,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "codex_local",
|
adapterType: "codex_local",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
commandArgs: args.map((value, idx) => {
|
commandArgs: args.map((value, idx) => {
|
||||||
if (idx === args.length - 1 && value !== "-") return `<prompt ${prompt.length} chars>`;
|
if (idx === args.length - 1 && value !== "-") return `<prompt ${prompt.length} chars>`;
|
||||||
return value;
|
return value;
|
||||||
}),
|
}),
|
||||||
env: redactEnvForLogs(env),
|
env: loggedEnv,
|
||||||
prompt,
|
prompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,13 @@ import {
|
||||||
asStringArray,
|
asStringArray,
|
||||||
parseObject,
|
parseObject,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
redactEnvForLogs,
|
buildInvocationEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
ensurePaperclipSkillSymlink,
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
readPaperclipRuntimeSkillEntries,
|
readPaperclipRuntimeSkillEntries,
|
||||||
|
resolveCommandForLogs,
|
||||||
resolvePaperclipDesiredSkillNames,
|
resolvePaperclipDesiredSkillNames,
|
||||||
removeMaintainerOnlySkillSymlinks,
|
removeMaintainerOnlySkillSymlinks,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
|
|
@ -271,6 +272,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
const billingType = resolveCursorBillingType(effectiveEnv);
|
const billingType = resolveCursorBillingType(effectiveEnv);
|
||||||
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
|
@ -383,11 +390,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "cursor",
|
adapterType: "cursor",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
commandArgs: args,
|
commandArgs: args,
|
||||||
env: redactEnvForLogs(env),
|
env: loggedEnv,
|
||||||
prompt,
|
prompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,17 @@ import {
|
||||||
asString,
|
asString,
|
||||||
asStringArray,
|
asStringArray,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
|
buildInvocationEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
ensurePaperclipSkillSymlink,
|
ensurePaperclipSkillSymlink,
|
||||||
joinPromptSections,
|
joinPromptSections,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
readPaperclipRuntimeSkillEntries,
|
readPaperclipRuntimeSkillEntries,
|
||||||
|
resolveCommandForLogs,
|
||||||
resolvePaperclipDesiredSkillNames,
|
resolvePaperclipDesiredSkillNames,
|
||||||
removeMaintainerOnlySkillSymlinks,
|
removeMaintainerOnlySkillSymlinks,
|
||||||
parseObject,
|
parseObject,
|
||||||
redactEnvForLogs,
|
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
|
|
@ -220,6 +221,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
const billingType = resolveGeminiBillingType(effectiveEnv);
|
const billingType = resolveGeminiBillingType(effectiveEnv);
|
||||||
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
const runtimeEnv = ensurePathInEnv(effectiveEnv);
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
const graceSec = asNumber(config.graceSec, 20);
|
const graceSec = asNumber(config.graceSec, 20);
|
||||||
|
|
@ -333,13 +340,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "gemini_local",
|
adapterType: "gemini_local",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
commandArgs: args.map((value, index) => (
|
commandArgs: args.map((value, index) => (
|
||||||
index === args.length - 1 ? `<prompt ${prompt.length} chars>` : value
|
index === args.length - 1 ? `<prompt ${prompt.length} chars>` : value
|
||||||
)),
|
)),
|
||||||
env: redactEnvForLogs(env),
|
env: loggedEnv,
|
||||||
prompt,
|
prompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ import {
|
||||||
parseObject,
|
parseObject,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
joinPromptSections,
|
joinPromptSections,
|
||||||
redactEnvForLogs,
|
buildInvocationEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
ensurePaperclipSkillSymlink,
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
|
resolveCommandForLogs,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
readPaperclipRuntimeSkillEntries,
|
readPaperclipRuntimeSkillEntries,
|
||||||
|
|
@ -186,6 +187,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(preparedRuntimeConfig.env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
await ensureOpenCodeModelConfiguredAndAvailable({
|
await ensureOpenCodeModelConfiguredAndAvailable({
|
||||||
model,
|
model,
|
||||||
|
|
@ -298,11 +305,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "opencode_local",
|
adapterType: "opencode_local",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
commandArgs: [...args, `<stdin prompt ${prompt.length} chars>`],
|
commandArgs: [...args, `<stdin prompt ${prompt.length} chars>`],
|
||||||
env: redactEnvForLogs(preparedRuntimeConfig.env),
|
env: loggedEnv,
|
||||||
prompt,
|
prompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,13 @@ import {
|
||||||
parseObject,
|
parseObject,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
joinPromptSections,
|
joinPromptSections,
|
||||||
redactEnvForLogs,
|
buildInvocationEnvForLogs,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
ensurePaperclipSkillSymlink,
|
ensurePaperclipSkillSymlink,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
readPaperclipRuntimeSkillEntries,
|
readPaperclipRuntimeSkillEntries,
|
||||||
|
resolveCommandForLogs,
|
||||||
resolvePaperclipDesiredSkillNames,
|
resolvePaperclipDesiredSkillNames,
|
||||||
removeMaintainerOnlySkillSymlinks,
|
removeMaintainerOnlySkillSymlinks,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
|
|
@ -204,6 +205,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
await ensureCommandResolvable(command, cwd, runtimeEnv);
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
// Validate model is available before execution
|
// Validate model is available before execution
|
||||||
await ensurePiModelConfiguredAndAvailable({
|
await ensurePiModelConfiguredAndAvailable({
|
||||||
|
|
@ -356,11 +363,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "pi_local",
|
adapterType: "pi_local",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandNotes,
|
commandNotes,
|
||||||
commandArgs: args,
|
commandArgs: args,
|
||||||
env: redactEnvForLogs(env),
|
env: loggedEnv,
|
||||||
prompt: userPrompt,
|
prompt: userPrompt,
|
||||||
promptMetrics,
|
promptMetrics,
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
99
server/src/__tests__/claude-local-execute.test.ts
Normal file
99
server/src/__tests__/claude-local-execute.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { execute } from "@paperclipai/adapter-claude-local/server";
|
||||||
|
|
||||||
|
async function writeFakeClaudeCommand(commandPath: string): Promise<void> {
|
||||||
|
const script = `#!/usr/bin/env node
|
||||||
|
const fs = require("node:fs");
|
||||||
|
|
||||||
|
const capturePath = process.env.PAPERCLIP_TEST_CAPTURE_PATH;
|
||||||
|
const payload = {
|
||||||
|
argv: process.argv.slice(2),
|
||||||
|
prompt: fs.readFileSync(0, "utf8"),
|
||||||
|
claudeConfigDir: process.env.CLAUDE_CONFIG_DIR || null,
|
||||||
|
};
|
||||||
|
if (capturePath) {
|
||||||
|
fs.writeFileSync(capturePath, JSON.stringify(payload), "utf8");
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-1", model: "claude-sonnet" }));
|
||||||
|
console.log(JSON.stringify({ type: "assistant", session_id: "claude-session-1", message: { content: [{ type: "text", text: "hello" }] } }));
|
||||||
|
console.log(JSON.stringify({ type: "result", session_id: "claude-session-1", result: "hello", usage: { input_tokens: 1, cache_read_input_tokens: 0, output_tokens: 1 } }));
|
||||||
|
`;
|
||||||
|
await fs.writeFile(commandPath, script, "utf8");
|
||||||
|
await fs.chmod(commandPath, 0o755);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("claude execute", () => {
|
||||||
|
it("logs HOME, CLAUDE_CONFIG_DIR, and the resolved executable path in invocation metadata", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-execute-meta-"));
|
||||||
|
const workspace = path.join(root, "workspace");
|
||||||
|
const binDir = path.join(root, "bin");
|
||||||
|
const commandPath = path.join(binDir, "claude");
|
||||||
|
const capturePath = path.join(root, "capture.json");
|
||||||
|
const claudeConfigDir = path.join(root, "claude-config");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.mkdir(binDir, { recursive: true });
|
||||||
|
await fs.mkdir(claudeConfigDir, { recursive: true });
|
||||||
|
await writeFakeClaudeCommand(commandPath);
|
||||||
|
|
||||||
|
const previousHome = process.env.HOME;
|
||||||
|
const previousPath = process.env.PATH;
|
||||||
|
const previousClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
process.env.HOME = root;
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH ?? ""}`;
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = claudeConfigDir;
|
||||||
|
|
||||||
|
let loggedCommand: string | null = null;
|
||||||
|
let loggedEnv: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const result = await execute({
|
||||||
|
runId: "run-meta",
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
name: "Claude Coder",
|
||||||
|
adapterType: "claude_local",
|
||||||
|
adapterConfig: {},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
sessionId: null,
|
||||||
|
sessionParams: null,
|
||||||
|
sessionDisplayId: null,
|
||||||
|
taskKey: null,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
command: "claude",
|
||||||
|
cwd: workspace,
|
||||||
|
env: {
|
||||||
|
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
||||||
|
},
|
||||||
|
promptTemplate: "Follow the paperclip heartbeat.",
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
authToken: "run-jwt-token",
|
||||||
|
onLog: async () => {},
|
||||||
|
onMeta: async (meta) => {
|
||||||
|
loggedCommand = meta.command;
|
||||||
|
loggedEnv = meta.env ?? {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.errorMessage).toBeNull();
|
||||||
|
expect(loggedCommand).toBe(commandPath);
|
||||||
|
expect(loggedEnv.HOME).toBe(root);
|
||||||
|
expect(loggedEnv.CLAUDE_CONFIG_DIR).toBe(claudeConfigDir);
|
||||||
|
expect(loggedEnv.PAPERCLIP_RESOLVED_COMMAND).toBe(commandPath);
|
||||||
|
} finally {
|
||||||
|
if (previousHome === undefined) delete process.env.HOME;
|
||||||
|
else process.env.HOME = previousHome;
|
||||||
|
if (previousPath === undefined) delete process.env.PATH;
|
||||||
|
else process.env.PATH = previousPath;
|
||||||
|
if (previousClaudeConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
else process.env.CLAUDE_CONFIG_DIR = previousClaudeConfigDir;
|
||||||
|
await fs.rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -195,6 +195,70 @@ describe("codex execute", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("logs HOME and the resolved executable path in invocation metadata", async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-meta-"));
|
||||||
|
const workspace = path.join(root, "workspace");
|
||||||
|
const binDir = path.join(root, "bin");
|
||||||
|
const commandPath = path.join(binDir, "codex");
|
||||||
|
const capturePath = path.join(root, "capture.json");
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
await fs.mkdir(binDir, { recursive: true });
|
||||||
|
await writeFakeCodexCommand(commandPath);
|
||||||
|
|
||||||
|
const previousHome = process.env.HOME;
|
||||||
|
const previousPath = process.env.PATH;
|
||||||
|
process.env.HOME = root;
|
||||||
|
process.env.PATH = `${binDir}${path.delimiter}${process.env.PATH ?? ""}`;
|
||||||
|
|
||||||
|
let loggedCommand: string | null = null;
|
||||||
|
let loggedEnv: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const result = await execute({
|
||||||
|
runId: "run-meta",
|
||||||
|
agent: {
|
||||||
|
id: "agent-1",
|
||||||
|
companyId: "company-1",
|
||||||
|
name: "Codex Coder",
|
||||||
|
adapterType: "codex_local",
|
||||||
|
adapterConfig: {},
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
sessionId: null,
|
||||||
|
sessionParams: null,
|
||||||
|
sessionDisplayId: null,
|
||||||
|
taskKey: null,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
command: "codex",
|
||||||
|
cwd: workspace,
|
||||||
|
env: {
|
||||||
|
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
||||||
|
},
|
||||||
|
promptTemplate: "Follow the paperclip heartbeat.",
|
||||||
|
},
|
||||||
|
context: {},
|
||||||
|
authToken: "run-jwt-token",
|
||||||
|
onLog: async () => {},
|
||||||
|
onMeta: async (meta) => {
|
||||||
|
loggedCommand = meta.command;
|
||||||
|
loggedEnv = meta.env ?? {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.errorMessage).toBeNull();
|
||||||
|
expect(loggedCommand).toBe(commandPath);
|
||||||
|
expect(loggedEnv.HOME).toBe(root);
|
||||||
|
expect(loggedEnv.PAPERCLIP_RESOLVED_COMMAND).toBe(commandPath);
|
||||||
|
} finally {
|
||||||
|
if (previousHome === undefined) delete process.env.HOME;
|
||||||
|
else process.env.HOME = previousHome;
|
||||||
|
if (previousPath === undefined) delete process.env.PATH;
|
||||||
|
else process.env.PATH = previousPath;
|
||||||
|
await fs.rm(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("uses a worktree-isolated CODEX_HOME while preserving shared auth and config", async () => {
|
it("uses a worktree-isolated CODEX_HOME while preserving shared auth and config", async () => {
|
||||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-"));
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-"));
|
||||||
const workspace = path.join(root, "workspace");
|
const workspace = path.join(root, "workspace");
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import {
|
||||||
asStringArray,
|
asStringArray,
|
||||||
parseObject,
|
parseObject,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
redactEnvForLogs,
|
buildInvocationEnvForLogs,
|
||||||
|
ensurePathInEnv,
|
||||||
|
resolveCommandForLogs,
|
||||||
runChildProcess,
|
runChildProcess,
|
||||||
} from "../utils.js";
|
} from "../utils.js";
|
||||||
|
|
||||||
|
|
@ -21,6 +23,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
for (const [k, v] of Object.entries(envConfig)) {
|
for (const [k, v] of Object.entries(envConfig)) {
|
||||||
if (typeof v === "string") env[k] = v;
|
if (typeof v === "string") env[k] = v;
|
||||||
}
|
}
|
||||||
|
const runtimeEnv = ensurePathInEnv({ ...process.env, ...env });
|
||||||
|
const resolvedCommand = await resolveCommandForLogs(command, cwd, runtimeEnv);
|
||||||
|
const loggedEnv = buildInvocationEnvForLogs(env, {
|
||||||
|
runtimeEnv,
|
||||||
|
includeRuntimeKeys: ["HOME"],
|
||||||
|
resolvedCommand,
|
||||||
|
});
|
||||||
|
|
||||||
const timeoutSec = asNumber(config.timeoutSec, 0);
|
const timeoutSec = asNumber(config.timeoutSec, 0);
|
||||||
const graceSec = asNumber(config.graceSec, 15);
|
const graceSec = asNumber(config.graceSec, 15);
|
||||||
|
|
@ -28,10 +37,10 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||||
if (onMeta) {
|
if (onMeta) {
|
||||||
await onMeta({
|
await onMeta({
|
||||||
adapterType: "process",
|
adapterType: "process",
|
||||||
command,
|
command: resolvedCommand,
|
||||||
cwd,
|
cwd,
|
||||||
commandArgs: args,
|
commandArgs: args,
|
||||||
env: redactEnvForLogs(env),
|
env: loggedEnv,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,13 @@ export {
|
||||||
resolvePathValue,
|
resolvePathValue,
|
||||||
renderTemplate,
|
renderTemplate,
|
||||||
redactEnvForLogs,
|
redactEnvForLogs,
|
||||||
|
buildInvocationEnvForLogs,
|
||||||
buildPaperclipEnv,
|
buildPaperclipEnv,
|
||||||
defaultPathForPlatform,
|
defaultPathForPlatform,
|
||||||
ensurePathInEnv,
|
ensurePathInEnv,
|
||||||
ensureAbsoluteDirectory,
|
ensureAbsoluteDirectory,
|
||||||
ensureCommandResolvable,
|
ensureCommandResolvable,
|
||||||
|
resolveCommandForLogs,
|
||||||
} from "@paperclipai/adapter-utils/server-utils";
|
} from "@paperclipai/adapter-utils/server-utils";
|
||||||
|
|
||||||
// Re-export runChildProcess with the server's pino logger wired in.
|
// Re-export runChildProcess with the server's pino logger wired in.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue