mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 14:18:12 +00:00
fix(cli): match managed prompt files by exact path
This commit is contained in:
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
buildSteeringArg,
|
buildSteeringArg,
|
||||||
|
hasManagedPromptFileArg,
|
||||||
PROMPT_FLAG_INLINE,
|
PROMPT_FLAG_INLINE,
|
||||||
PROMPT_FLAG_FILE,
|
|
||||||
} from '../prompt-injection-strategy';
|
} from '../prompt-injection-strategy';
|
||||||
|
|
||||||
const IMAGE_ANALYSIS_STEERING_PROMPT = {
|
const IMAGE_ANALYSIS_STEERING_PROMPT = {
|
||||||
@@ -41,9 +41,8 @@ function hasExactFlagValue(params: {
|
|||||||
args: string[];
|
args: string[];
|
||||||
flag: string;
|
flag: string;
|
||||||
expectedValue: string;
|
expectedValue: string;
|
||||||
allowPartiallyMatch?: boolean;
|
|
||||||
}): boolean {
|
}): boolean {
|
||||||
const { args, flag, expectedValue, allowPartiallyMatch } = params;
|
const { args, flag, expectedValue } = params;
|
||||||
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
const arg = args[index];
|
const arg = args[index];
|
||||||
@@ -55,10 +54,6 @@ function hasExactFlagValue(params: {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowPartiallyMatch && value?.includes(expectedValue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,12 +83,7 @@ function ensureImageAnalysisSteeringPrompt(args: string[]): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasExactFlagValue({
|
hasManagedPromptFileArg({ args: optionArgs, promptName: IMAGE_ANALYSIS_STEERING_PROMPT.name })
|
||||||
args: optionArgs,
|
|
||||||
flag: PROMPT_FLAG_FILE,
|
|
||||||
expectedValue: IMAGE_ANALYSIS_STEERING_PROMPT.name,
|
|
||||||
allowPartiallyMatch: true,
|
|
||||||
})
|
|
||||||
) {
|
) {
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,43 @@ export const PROMPT_FLAG_INLINE = '--append-system-prompt';
|
|||||||
/** `--append-system-prompt-file` — prompt read from file */
|
/** `--append-system-prompt-file` — prompt read from file */
|
||||||
export const PROMPT_FLAG_FILE = '--append-system-prompt-file';
|
export const PROMPT_FLAG_FILE = '--append-system-prompt-file';
|
||||||
|
|
||||||
|
function getManagedPromptsDir(): string {
|
||||||
|
return path.join(getCcsDir(), 'prompts');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getManagedPromptFileName(promptName: string): string {
|
||||||
|
return `${promptName}.txt`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getManagedPromptFilePath(promptName: string): string {
|
||||||
|
return path.join(getManagedPromptsDir(), getManagedPromptFileName(promptName));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasManagedPromptFileArg(params: { args: string[]; promptName: string }): boolean {
|
||||||
|
const expectedPath = path.resolve(getManagedPromptFilePath(params.promptName));
|
||||||
|
|
||||||
|
for (let index = 0; index < params.args.length; index += 1) {
|
||||||
|
const arg = params.args[index];
|
||||||
|
|
||||||
|
if (arg === PROMPT_FLAG_FILE) {
|
||||||
|
const filePath = params.args[index + 1];
|
||||||
|
if (filePath && path.resolve(filePath) === expectedPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
arg.startsWith(`${PROMPT_FLAG_FILE}=`) &&
|
||||||
|
path.resolve(arg.slice(PROMPT_FLAG_FILE.length + 1)) === expectedPath
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect which prompt injection mode to use based on user-provided args.
|
* Detect which prompt injection mode to use based on user-provided args.
|
||||||
*
|
*
|
||||||
@@ -56,9 +93,7 @@ export function buildFileSteeringArg(params: {
|
|||||||
promptFileName: string;
|
promptFileName: string;
|
||||||
promptContent: string;
|
promptContent: string;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const ccsDir = getCcsDir();
|
const promptsFolder = getManagedPromptsDir();
|
||||||
|
|
||||||
const promptsFolder = path.join(ccsDir, '/prompts');
|
|
||||||
|
|
||||||
if (!fs.existsSync(promptsFolder)) {
|
if (!fs.existsSync(promptsFolder)) {
|
||||||
fs.mkdirSync(promptsFolder, { recursive: true });
|
fs.mkdirSync(promptsFolder, { recursive: true });
|
||||||
@@ -83,7 +118,7 @@ export function buildSteeringArg(params: {
|
|||||||
|
|
||||||
if (mode === 'file') {
|
if (mode === 'file') {
|
||||||
return buildFileSteeringArg({
|
return buildFileSteeringArg({
|
||||||
promptFileName: `${params.promptName}.txt`,
|
promptFileName: getManagedPromptFileName(params.promptName),
|
||||||
promptContent: params.promptContent,
|
promptContent: params.promptContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
buildSteeringArg,
|
buildSteeringArg,
|
||||||
|
hasManagedPromptFileArg,
|
||||||
PROMPT_FLAG_INLINE,
|
PROMPT_FLAG_INLINE,
|
||||||
PROMPT_FLAG_FILE,
|
|
||||||
} from '../prompt-injection-strategy';
|
} from '../prompt-injection-strategy';
|
||||||
|
|
||||||
const NATIVE_WEBSEARCH_TOOL = 'WebSearch';
|
const NATIVE_WEBSEARCH_TOOL = 'WebSearch';
|
||||||
@@ -83,9 +83,8 @@ function hasExactFlagValue(params: {
|
|||||||
args: string[];
|
args: string[];
|
||||||
flag: string;
|
flag: string;
|
||||||
expectedValue: string;
|
expectedValue: string;
|
||||||
allowPartiallyMatch?: boolean;
|
|
||||||
}): boolean {
|
}): boolean {
|
||||||
const { args, flag, expectedValue, allowPartiallyMatch } = params;
|
const { args, flag, expectedValue } = params;
|
||||||
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
const arg = args[index];
|
const arg = args[index];
|
||||||
@@ -97,10 +96,6 @@ function hasExactFlagValue(params: {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowPartiallyMatch && value?.includes(expectedValue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,11 +164,9 @@ function ensureWebSearchSteeringPrompt(args: string[]): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
hasExactFlagValue({
|
hasManagedPromptFileArg({
|
||||||
args: optionArgs,
|
args: optionArgs,
|
||||||
flag: PROMPT_FLAG_FILE,
|
promptName: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name,
|
||||||
expectedValue: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name,
|
|
||||||
allowPartiallyMatch: true,
|
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import * as os from 'os';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir } from '../config-manager';
|
import { getCcsDir } from '../config-manager';
|
||||||
import { createLogger } from '../../services/logging';
|
import { createLogger } from '../../services/logging';
|
||||||
import { PROMPT_FLAG_INLINE, PROMPT_FLAG_FILE } from '../prompt-injection-strategy';
|
import { hasManagedPromptFileArg, PROMPT_FLAG_INLINE } from '../prompt-injection-strategy';
|
||||||
import { THIRD_PARTY_WEBSEARCH_STEERING_PROMPT } from './claude-tool-args';
|
import { THIRD_PARTY_WEBSEARCH_STEERING_PROMPT } from './claude-tool-args';
|
||||||
|
|
||||||
const TRACE_FILE_NAME = 'websearch-trace.jsonl';
|
const TRACE_FILE_NAME = 'websearch-trace.jsonl';
|
||||||
@@ -61,9 +61,8 @@ function hasExactFlagValue(params: {
|
|||||||
args: string[];
|
args: string[];
|
||||||
flag: string;
|
flag: string;
|
||||||
expectedValue: string;
|
expectedValue: string;
|
||||||
allowIncludesValue?: boolean;
|
|
||||||
}): boolean {
|
}): boolean {
|
||||||
const { args, flag, expectedValue, allowIncludesValue } = params;
|
const { args, flag, expectedValue } = params;
|
||||||
|
|
||||||
for (let index = 0; index < args.length; index += 1) {
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
const arg = args[index];
|
const arg = args[index];
|
||||||
@@ -75,10 +74,6 @@ function hasExactFlagValue(params: {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowIncludesValue && immediateFlagValue?.includes(expectedValue)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,14 +198,7 @@ function hasSteeringPromptInArgs(args: string[]): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (hasManagedPromptFileArg({ args, promptName: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name })) {
|
||||||
hasExactFlagValue({
|
|
||||||
args,
|
|
||||||
flag: PROMPT_FLAG_FILE,
|
|
||||||
expectedValue: THIRD_PARTY_WEBSEARCH_STEERING_PROMPT.name,
|
|
||||||
allowIncludesValue: true,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,4 +66,17 @@ describe('appendThirdPartyImageAnalysisToolArgs', () => {
|
|||||||
);
|
);
|
||||||
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not treat unrelated user prompt files as the managed CCS steering prompt', () => {
|
||||||
|
const result = appendThirdPartyImageAnalysisToolArgs([
|
||||||
|
'-p',
|
||||||
|
'describe',
|
||||||
|
'--append-system-prompt-file',
|
||||||
|
'/tmp/user-ccs-prompt-image-analysis-tool-notes.txt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filePaths = result.filter((arg, index) => result[index - 1] === '--append-system-prompt-file');
|
||||||
|
expect(filePaths).toContain('/tmp/user-ccs-prompt-image-analysis-tool-notes.txt');
|
||||||
|
expect(filePaths.some((filePath) => filePath.endsWith('/ccs-prompt-image-analysis-tool.txt'))).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,36 @@
|
|||||||
import { describe, expect, it } from 'bun:test';
|
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
import {
|
import {
|
||||||
detectPromptInjectionMode,
|
|
||||||
buildInlineSteeringArg,
|
buildInlineSteeringArg,
|
||||||
buildFileSteeringArg,
|
buildFileSteeringArg,
|
||||||
buildSteeringArg,
|
buildSteeringArg,
|
||||||
|
detectPromptInjectionMode,
|
||||||
|
getManagedPromptFilePath,
|
||||||
|
hasManagedPromptFileArg,
|
||||||
PROMPT_FLAG_INLINE,
|
PROMPT_FLAG_INLINE,
|
||||||
PROMPT_FLAG_FILE,
|
PROMPT_FLAG_FILE,
|
||||||
} from '../../../src/utils/prompt-injection-strategy';
|
} from '../../../src/utils/prompt-injection-strategy';
|
||||||
|
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
let tempHome: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-prompt-strategy-'));
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalCcsHome === undefined) {
|
||||||
|
delete process.env.CCS_HOME;
|
||||||
|
} else {
|
||||||
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
|
}
|
||||||
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
describe('detectPromptInjectionMode', () => {
|
describe('detectPromptInjectionMode', () => {
|
||||||
it('returns inline when no prompt flags present', () => {
|
it('returns inline when no prompt flags present', () => {
|
||||||
expect(detectPromptInjectionMode(['-p', 'hello'])).toBe('inline');
|
expect(detectPromptInjectionMode(['-p', 'hello'])).toBe('inline');
|
||||||
@@ -48,23 +71,47 @@ describe('buildInlineSteeringArg', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('buildFileSteeringArg', () => {
|
describe('buildFileSteeringArg', () => {
|
||||||
it('returns file flag and writes temp file', () => {
|
it('returns file flag and writes the prompt into the isolated CCS home', () => {
|
||||||
const result = buildFileSteeringArg({promptFileName: 'ccs-test-prompt.txt', promptContent: 'hello world' });
|
const result = buildFileSteeringArg({
|
||||||
|
promptFileName: 'ccs-test-prompt.txt',
|
||||||
|
promptContent: 'hello world',
|
||||||
|
});
|
||||||
|
|
||||||
expect(result[0]).toBe('--append-system-prompt-file');
|
expect(result[0]).toBe('--append-system-prompt-file');
|
||||||
expect(result[1]).toContain('ccs-test-prompt.txt');
|
expect(result[1]).toBe(path.join(tempHome, '.ccs', 'prompts', 'ccs-test-prompt.txt'));
|
||||||
|
expect(fs.readFileSync(result[1], 'utf8')).toBe('hello world');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasManagedPromptFileArg', () => {
|
||||||
|
it('returns true for the exact CCS-managed prompt path', () => {
|
||||||
|
expect(
|
||||||
|
hasManagedPromptFileArg({
|
||||||
|
args: [PROMPT_FLAG_FILE, getManagedPromptFilePath('ccs-test')],
|
||||||
|
promptName: 'ccs-test',
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for unrelated user files that only contain the prompt name', () => {
|
||||||
|
expect(
|
||||||
|
hasManagedPromptFileArg({
|
||||||
|
args: [PROMPT_FLAG_FILE, '/tmp/user-ccs-test-notes.txt'],
|
||||||
|
promptName: 'ccs-test',
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildSteeringArg', () => {
|
describe('buildSteeringArg', () => {
|
||||||
it('delegates to inline in inline mode', () => {
|
it('delegates to inline in inline mode', () => {
|
||||||
expect(buildSteeringArg({
|
expect(
|
||||||
args: [PROMPT_FLAG_INLINE],
|
buildSteeringArg({
|
||||||
promptName: 'ignored.txt',
|
args: [PROMPT_FLAG_INLINE],
|
||||||
promptContent: 'hello',
|
promptName: 'ignored.txt',
|
||||||
})).toEqual([
|
promptContent: 'hello',
|
||||||
'--append-system-prompt',
|
})
|
||||||
'hello',
|
).toEqual(['--append-system-prompt', 'hello']);
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('delegates to file in file mode', () => {
|
it('delegates to file in file mode', () => {
|
||||||
@@ -74,6 +121,6 @@ describe('buildSteeringArg', () => {
|
|||||||
promptContent: 'hello',
|
promptContent: 'hello',
|
||||||
});
|
});
|
||||||
expect(result[0]).toBe('--append-system-prompt-file');
|
expect(result[0]).toBe('--append-system-prompt-file');
|
||||||
expect(result[1]).toContain('ccs-test.txt');
|
expect(result[1]).toBe(getManagedPromptFilePath('ccs-test'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -157,4 +157,16 @@ describe('appendThirdPartyWebSearchToolArgs', () => {
|
|||||||
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
expect(fileFlags.length).toBeGreaterThanOrEqual(2);
|
||||||
expect(result).not.toContain('--append-system-prompt');
|
expect(result).not.toContain('--append-system-prompt');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not treat unrelated user prompt files as the managed CCS steering prompt', () => {
|
||||||
|
const result = appendThirdPartyWebSearchToolArgs([
|
||||||
|
'smoke',
|
||||||
|
'--append-system-prompt-file',
|
||||||
|
'/tmp/user-ccs-prompt-websearch-tool-notes.txt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const filePaths = result.filter((arg, index) => result[index - 1] === '--append-system-prompt-file');
|
||||||
|
expect(filePaths).toContain('/tmp/user-ccs-prompt-websearch-tool-notes.txt');
|
||||||
|
expect(filePaths.some((filePath) => filePath.endsWith('/ccs-prompt-websearch-tool.txt'))).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user