Plugin prompt contributions declared a `condition` field in the SDK, but the host never evaluated it, so gated prompt content always rendered unconditionally. - Add packages/core/src/plugin-prompt-condition.ts implementing a minimal, non-eval `settings["key"] === "value"` / `!==` condition grammar - Wire condition evaluation into plugin-runner.ts / agent-instructions.ts / executor.ts / reviewer.ts / triage.ts / agent-heartbeat.ts so prompt contributions are filtered by effective plugin settings at each call site - Extend plugin-types.ts and core index.ts/index.gate.ts to expose the new evaluator and condition typing - Document the condition grammar in docs/PLUGIN_AUTHORING.md - Add regression tests covering the evaluator and its wiring through plugin-runner and agent-instructions - Add changeset (@runfusion/fusion minor, feature) describing the new gating behavior Files changed: .changeset/fn-7776-prompt-condition.md | 7 ++ docs/PLUGIN_AUTHORING.md | 13 ++- .../src/__tests__/plugin-prompt-condition.test.ts | 90 ++++++++++++++++++++ packages/core/src/index.gate.ts | 6 ++ packages/core/src/index.ts | 6 ++ packages/core/src/plugin-prompt-condition.ts | 51 +++++++++++ packages/core/src/plugin-types.ts | 10 ++- .../src/__tests__/agent-instructions.test.ts | 33 ++++++-- .../engine/src/__tests__/plugin-runner.test.ts | 98 +++++++++++++++++++++- packages/engine/src/agent-heartbeat.ts | 2 +- packages/engine/src/agent-instructions.ts | 6 +- packages/engine/src/executor.ts | 28 +++++-- packages/engine/src/plugin-runner.ts | 64 ++++++++++++-- packages/engine/src/reviewer.ts | 2 +- packages/engine/src/triage.ts | 2 +- packages/plugin-sdk/src/index.ts | 4 + 16 files changed, 385 insertions(+), 37 deletions(-) Fusion-Task-Id: FN-7776 Fusion-Task-Lineage: ba8dcd52-260a-4166-a712-f3dd39b81b15 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import type { PluginSettingSchema } from "./plugin-types.js";
|
|
|
|
export interface PromptConditionEvaluationResult {
|
|
included: boolean;
|
|
reason?: string;
|
|
}
|
|
|
|
const PROMPT_CONDITION_PATTERN = /^\s*settings\s*\[\s*(["'])([^"'\\]+)\1\s*\]\s*(===|!==)\s*(["'])([^"'\\]*)\4\s*$/;
|
|
|
|
/**
|
|
* FNXC:PluginPrompt 2026-07-10-00:00:
|
|
* Plugin prompt conditions are plugin-authored strings that gate prompt injection, so the host must treat them as data.
|
|
* Evaluate only the documented single-comparison grammar with a deterministic regex; never use eval, Function, vm, or executable interpolation.
|
|
*/
|
|
export function evaluatePromptConditionDetailed(
|
|
condition: string | undefined,
|
|
settings: Record<string, unknown>,
|
|
): PromptConditionEvaluationResult {
|
|
if (condition === undefined || condition.trim() === "") {
|
|
return { included: true };
|
|
}
|
|
|
|
const match = PROMPT_CONDITION_PATTERN.exec(condition);
|
|
if (!match) {
|
|
return { included: false, reason: "unsupported prompt contribution condition grammar" };
|
|
}
|
|
|
|
const [, , key, operator, , expected] = match;
|
|
const actual = settings[key];
|
|
const equals = typeof actual === "string" && actual === expected;
|
|
return { included: operator === "===" ? equals : !equals };
|
|
}
|
|
|
|
export function evaluatePromptCondition(condition: string | undefined, settings: Record<string, unknown>): boolean {
|
|
return evaluatePromptConditionDetailed(condition, settings).included;
|
|
}
|
|
|
|
export function resolveEffectivePluginSettings(
|
|
stored: Record<string, unknown> | undefined,
|
|
schema?: Record<string, PluginSettingSchema>,
|
|
): Record<string, unknown> {
|
|
const effective: Record<string, unknown> = {};
|
|
|
|
for (const [key, setting] of Object.entries(schema ?? {})) {
|
|
if (Object.prototype.hasOwnProperty.call(setting, "defaultValue")) {
|
|
effective[key] = setting.defaultValue;
|
|
}
|
|
}
|
|
|
|
return { ...effective, ...(stored ?? {}) };
|
|
}
|