@salesforce/sfdx-agent-harness-openai 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import { readFile } from 'node:fs/promises';
6
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
7
+ import { tool } from '@openai/agents';
8
+ import { getErrorMessage } from '@salesforce/agentic-common';
9
+ /**
10
+ * Build the `@openai/agents` function tools that expose an agent's configured
11
+ * skills to the model. Returns an empty array when the agent declares no skills
12
+ * so `buildAgent` adds nothing to the tool surface for a skill-less agent.
13
+ *
14
+ * Two tools:
15
+ *
16
+ * - **`load_skill({ skill })`** — returns the requested skill's `SKILL.md` body,
17
+ * prefixed with a `<skill-location>` anchor carrying the skill's directory.
18
+ * The tool description enumerates the catalog (one `- name: description` line
19
+ * per skill, alphabetical) so the model can scan-and-pick. Eager shape: the
20
+ * descriptor rides in the prompt at rest, but the body loads on demand.
21
+ * - **`read_skill_file({ skill, path })`** — reads a file the loaded `SKILL.md`
22
+ * references relative to its own directory (`references/...`, `assets/...`).
23
+ * This is the OpenAI harness's answer to multi-file skill reachability
24
+ * (W-23231661 / #626): Claude reaches siblings via the subprocess's built-in
25
+ * `Read` tool plus a granted directory, and Mastra via a `Workspace`
26
+ * filesystem with allowed paths — neither affordance exists here (the
27
+ * `@openai/agents` run loop has no built-in file reader and runs in-process),
28
+ * so the harness supplies the read affordance itself, scoped to the named
29
+ * skill's directory.
30
+ *
31
+ * Both tools are `needsApproval: false`. Skills read the consumer's OWN
32
+ * configured paths — a consumer-supplied capability, the same category as
33
+ * consumer-executed tools (`AgentConfig.tools`), never approval-gated. They
34
+ * carry no `serverName`, so they are not `skill_bridge`-identity tools and the
35
+ * SDK's `mcp:skill_bridge:*` auto-allow rules don't apply — harmless, because a
36
+ * `needsApproval: false` tool can never raise an interruption to gate.
37
+ *
38
+ * Called fresh inside `buildAgent` on every `stream()` reading `state.skillMap`
39
+ * live — never cached on state — so a mid-turn `updateAgent` that changes the
40
+ * configured skills lands on the next turn.
41
+ */
42
+ export function buildSkillTools(skillMap) {
43
+ if (skillMap.size === 0)
44
+ return [];
45
+ return [buildLoadSkillTool(skillMap), buildReadSkillFileTool(skillMap)];
46
+ }
47
+ const LOAD_SKILL_TOOL_NAME = 'load_skill';
48
+ const READ_SKILL_FILE_TOOL_NAME = 'read_skill_file';
49
+ function buildLoadSkillTool(skillMap) {
50
+ const execute = async (input) => {
51
+ const requested = readStringArg(input, 'skill');
52
+ if (requested.length === 0)
53
+ return errorPayload("Missing required 'skill' field.", skillMap);
54
+ const entry = skillMap.get(requested);
55
+ if (entry === undefined)
56
+ return errorPayload(`Unknown skill "${requested}".`, skillMap);
57
+ let body;
58
+ try {
59
+ body = await readFile(entry.path, 'utf8');
60
+ }
61
+ catch (err) {
62
+ return errorPayload(`Failed to read skill "${requested}": ${getErrorMessage(err)}`);
63
+ }
64
+ return prependSkillLocation(body, entry.path);
65
+ };
66
+ return tool({
67
+ name: LOAD_SKILL_TOOL_NAME,
68
+ description: buildLoadSkillDescription(skillMap),
69
+ parameters: {
70
+ type: 'object',
71
+ properties: {
72
+ skill: { type: 'string', description: 'Exact name of the skill to load (required).' },
73
+ },
74
+ required: ['skill'],
75
+ additionalProperties: false,
76
+ },
77
+ strict: false,
78
+ needsApproval: false,
79
+ execute,
80
+ // A JSON-schema `parameters` + `strict: false` selects the non-strict
81
+ // tool branch (the harness does not import zod). The options are cast as
82
+ // one object because a hand-written JSON schema won't structurally match
83
+ // the SDK's `JsonObjectSchemaNonStrict` generic and the execute arg is
84
+ // `unknown` on the non-strict branch — same pattern as `mapConsumerTools`.
85
+ });
86
+ }
87
+ function buildReadSkillFileTool(skillMap) {
88
+ const execute = async (input) => {
89
+ const skillName = readStringArg(input, 'skill');
90
+ const requestedPath = readStringArg(input, 'path');
91
+ if (skillName.length === 0)
92
+ return errorPayload("Missing required 'skill' field.", skillMap);
93
+ if (requestedPath.length === 0)
94
+ return errorPayload("Missing required 'path' field.");
95
+ const entry = skillMap.get(skillName);
96
+ if (entry === undefined)
97
+ return errorPayload(`Unknown skill "${skillName}".`, skillMap);
98
+ const skillDir = dirname(entry.path);
99
+ const resolved = resolve(skillDir, requestedPath);
100
+ // Containment: the resolved path must be the skill directory itself or a
101
+ // descendant of it. `relative(skillDir, resolved)` escapes the directory
102
+ // iff it starts with `..` or is absolute (a different drive on Windows),
103
+ // which rejects `../`, an absolute `requestedPath`, and any traversal that
104
+ // climbs out. Symlink-escape hardening (realpath) is a deliberate
105
+ // non-goal: the files are consumer-configured, not adversarial input, and
106
+ // the harness runs in-process with no sandbox to breach.
107
+ const rel = relative(skillDir, resolved);
108
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
109
+ return errorPayload(`Path "${requestedPath}" escapes skill "${skillName}"'s directory. ` +
110
+ 'Reference files inside the skill directory only (e.g. `references/format.md`).');
111
+ }
112
+ try {
113
+ return await readFile(resolved, 'utf8');
114
+ }
115
+ catch (err) {
116
+ return errorPayload(`Failed to read "${requestedPath}" for skill "${skillName}": ${getErrorMessage(err)}`);
117
+ }
118
+ };
119
+ return tool({
120
+ name: READ_SKILL_FILE_TOOL_NAME,
121
+ description: 'Read a file that a loaded skill references relative to its own directory (e.g. `references/format.md`, ' +
122
+ '`assets/template.txt`). Pass the skill name and the relative path exactly as the skill body wrote it, ' +
123
+ 'resolved against the skill directory the `load_skill` `<skill-location>` anchor reported. Use this after ' +
124
+ '`load_skill` whenever the loaded skill instructs you to read a referenced file before answering.',
125
+ parameters: {
126
+ type: 'object',
127
+ properties: {
128
+ skill: {
129
+ type: 'string',
130
+ description: 'Exact name of the skill whose directory the path is relative to.',
131
+ },
132
+ path: {
133
+ type: 'string',
134
+ description: 'Path to the file, relative to the skill directory (e.g. `references/format.md`).',
135
+ },
136
+ },
137
+ required: ['skill', 'path'],
138
+ additionalProperties: false,
139
+ },
140
+ strict: false,
141
+ needsApproval: false,
142
+ execute,
143
+ });
144
+ }
145
+ function buildLoadSkillDescription(skillMap) {
146
+ // Keep the catalog deterministic — alphabetical names regardless of
147
+ // insertion order means a consumer who reorders `config.skills` doesn't
148
+ // churn the model's prompt.
149
+ const names = [...skillMap.keys()].sort();
150
+ const lines = [
151
+ "Load a project-specific skill's full instructions on demand. Returns the SKILL.md body, prefixed with the " +
152
+ "skill's on-disk location so you can resolve any relative file references it mentions (e.g. " +
153
+ '`references/...`) via the `read_skill_file` tool. Pick the relevant skill from the catalog below before ' +
154
+ 'answering questions that depend on project conventions, internal procedures, or company-specific ' +
155
+ 'protocols.',
156
+ '',
157
+ 'Available skills:',
158
+ ];
159
+ for (const name of names) {
160
+ const description = skillMap.get(name)?.description ?? '';
161
+ lines.push(description.length > 0 ? `- ${name}: ${description}` : `- ${name}`);
162
+ }
163
+ return lines.join('\n');
164
+ }
165
+ /**
166
+ * Prefix the SKILL.md body with an absolute-path anchor so the model can resolve
167
+ * relative references inside a multi-file skill (W-23231661 / #626). The
168
+ * `load_skill` result on its own gives the model no way to turn a relative
169
+ * mention like `references/api-guide.md` into a read the harness can serve —
170
+ * configured skills routinely live outside the project root. Surfacing the
171
+ * skill's directory plus naming the `read_skill_file` tool gives the model the
172
+ * exact call to make. Mirrors the Claude harness's `prependSkillLocation`, with
173
+ * the tool name adapted to this harness's sibling-reader (Claude points the
174
+ * model at its built-in `Read`; here the model must call `read_skill_file`).
175
+ *
176
+ * The preamble is a tag-delimited (`<skill-location>…</skill-location>`) block
177
+ * ahead of the verbatim body so it reads as harness-provided metadata rather
178
+ * than part of the skill author's instructions.
179
+ */
180
+ function prependSkillLocation(body, skillMdPath) {
181
+ const skillDir = dirname(skillMdPath);
182
+ const preamble = [
183
+ '<skill-location>',
184
+ `This skill's files live in: ${skillDir}`,
185
+ `Its SKILL.md is at: ${skillMdPath}`,
186
+ 'To read any file this skill references by a relative path (e.g. `references/...`, `assets/...`), call the',
187
+ "`read_skill_file` tool with this skill's name and that relative path. Do NOT guess file contents — read them.",
188
+ '</skill-location>',
189
+ '',
190
+ ].join('\n');
191
+ return `${preamble}${body}`;
192
+ }
193
+ /** Read a required string field off the tool's parsed (non-strict) input. */
194
+ function readStringArg(input, field) {
195
+ const value = input?.[field];
196
+ return typeof value === 'string' ? value.trim() : '';
197
+ }
198
+ /**
199
+ * Shape a recoverable tool error as a JSON string the model reads and can act
200
+ * on. `load_skill`'s errors include the available skill names so the model can
201
+ * retry with a valid one; `read_skill_file`'s path errors omit them.
202
+ */
203
+ function errorPayload(message, skillMap) {
204
+ const payload = { error: message };
205
+ if (skillMap !== undefined)
206
+ payload.availableSkills = [...skillMap.keys()].sort();
207
+ return JSON.stringify(payload);
208
+ }
209
+ //# sourceMappingURL=openai-skill-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai-skill-tools.js","sourceRoot":"","sources":["../src/openai-skill-tools.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { type MCPServer, type Tool } from '@openai/agents';
2
- import type { ToolResultRedactor } from '@salesforce/sfdx-agent-sdk';
2
+ import type { Decision, ToolInvocation, ToolResultRedactor } from '@salesforce/sfdx-agent-sdk';
3
3
  import type { McpCatalogEntry } from './openai-mcp-state.js';
4
4
  /**
5
5
  * Everything a redaction seam needs to call the consumer's
@@ -16,6 +16,33 @@ export type RedactionContext = {
16
16
  /** Bare-tool-name → `{ serverName, annotations }`; supplies `serverName` for MCP tools. */
17
17
  readonly mcpCatalog: ReadonlyMap<string, McpCatalogEntry>;
18
18
  };
19
+ /**
20
+ * The policy decider the coordinator consults per interruption, plus the MCP
21
+ * catalog needed to lift a bare tool name to the full {@link ToolInvocation}
22
+ * (`serverName` + `annotations`) the `mcp` / `mcp-annotation` matchers require.
23
+ * Built per-turn in `stream()` when the tool-approval gate is active AND the
24
+ * agent has MCP servers, and threaded into `buildManagedMcpTools` so each
25
+ * materialized MCP tool carries a `needsApproval` predicate. `undefined` when
26
+ * gating is off — MCP tools then carry no predicate and never suspend.
27
+ */
28
+ export type McpApprovalContext = {
29
+ /** Resolves a tool invocation to `allow` / `deny` / `require-approval`. */
30
+ readonly policy: (invocation: ToolInvocation) => Decision;
31
+ /** Bare-tool-name → `{ serverName, annotations }` — the enrichment source. */
32
+ readonly mcpCatalog: ReadonlyMap<string, McpCatalogEntry>;
33
+ };
34
+ /**
35
+ * Lift a tool's **display name** (the `@openai/agents`-sanitized name carried on
36
+ * interruptions and materialized tools) to the {@link ToolInvocation} the policy
37
+ * resolver matches on. The catalog (keyed by display name) supplies `serverName`
38
+ * (required for the `mcp` / `mcp-annotation` matchers — a `builtin` matcher only
39
+ * fires when `serverName` is absent), the ORIGINAL un-sanitized `toolName` (so a
40
+ * rule pinning `{ toolName: 'get-sum' }` matches even though the display name is
41
+ * `get_sum`), and any declared `annotations`. A name missing from the catalog
42
+ * (a consumer / built-in tool) yields a bare `{ toolName }` from the display
43
+ * name, matching the event-adapter enrichment contract (`serverName` absent).
44
+ */
45
+ export declare function toToolInvocation(displayName: string, mcpCatalog: ReadonlyMap<string, McpCatalogEntry>): ToolInvocation;
19
46
  /**
20
47
  * Run the consumer's redactor over one tool result and return the value the
21
48
  * model should see. `{ output }` replaces; `undefined` (or a malformed return)
@@ -36,15 +63,19 @@ export declare function applyRedaction(ctx: RedactionContext, args: {
36
63
  /**
37
64
  * Materialize an agent's MCP tools as function tools (via {@link getAllMcpTools},
38
65
  * the same call the SDK makes internally for a native `Agent({ mcpServers })`
39
- * attach) and wrap each one's `invoke` with a redaction shim. Used only when the
40
- * agent has an `onToolResult` hook — otherwise the harness keeps the native
41
- * `mcpServers` attach unchanged (the #541-proven path).
66
+ * attach) so the harness can attach per-tool hooks the native attach doesn't
67
+ * expose: a **redaction** shim on each tool's `invoke` (when `redaction` is set)
68
+ * and a **`needsApproval` predicate** driven by the tool-approval policy (when
69
+ * `approval` is set). Used whenever EITHER hook is active; otherwise the harness
70
+ * keeps the native `mcpServers` attach unchanged (the #541-proven path).
42
71
  *
43
- * The `@openai/agents` MCP attach exposes no per-result rewrite hook, so owning
44
- * the function-tool `invoke` is the only model-visible seam that also carries
45
- * the `toolCallId` (`details.toolCall.callId`). The shim closure-captures the
46
- * {@link RedactionContext}, so `threadId` / `agentId` need no `RunContext`
47
- * plumbing.
72
+ * The `@openai/agents` MCP attach exposes neither a per-result rewrite hook nor
73
+ * a per-tool `needsApproval` seam, so owning the materialized function tool is
74
+ * the only place to attach both. The redaction shim closure-captures the
75
+ * {@link RedactionContext} (so `threadId` / `agentId` need no `RunContext`
76
+ * plumbing) and reads `toolCallId` from `details.toolCall.callId`; the approval
77
+ * predicate closure-captures the {@link McpApprovalContext} and resolves the
78
+ * live policy per call.
48
79
  *
49
80
  * **#541 is preserved:** `getAllMcpTools` calls `listTools()` per server, which
50
81
  * returns the warm instance cache (`cacheToolsList: true`) with no network
@@ -52,4 +83,7 @@ export declare function applyRedaction(ctx: RedactionContext, args: {
52
83
  * model-visible string output, not the SDK's internal error flag, so MCP
53
84
  * `isError` is best-effort (duck-typed) — matching Claude's `PostToolUse`.
54
85
  */
55
- export declare function buildRedactedMcpTools(servers: MCPServer[], ctx: RedactionContext): Promise<Tool[]>;
86
+ export declare function buildManagedMcpTools(servers: MCPServer[], ctx: {
87
+ redaction?: RedactionContext;
88
+ approval?: McpApprovalContext;
89
+ }): Promise<Tool[]>;
@@ -3,6 +3,27 @@
3
3
  * See LICENSE.txt for license terms.
4
4
  */
5
5
  import { getAllMcpTools } from '@openai/agents';
6
+ /**
7
+ * Lift a tool's **display name** (the `@openai/agents`-sanitized name carried on
8
+ * interruptions and materialized tools) to the {@link ToolInvocation} the policy
9
+ * resolver matches on. The catalog (keyed by display name) supplies `serverName`
10
+ * (required for the `mcp` / `mcp-annotation` matchers — a `builtin` matcher only
11
+ * fires when `serverName` is absent), the ORIGINAL un-sanitized `toolName` (so a
12
+ * rule pinning `{ toolName: 'get-sum' }` matches even though the display name is
13
+ * `get_sum`), and any declared `annotations`. A name missing from the catalog
14
+ * (a consumer / built-in tool) yields a bare `{ toolName }` from the display
15
+ * name, matching the event-adapter enrichment contract (`serverName` absent).
16
+ */
17
+ export function toToolInvocation(displayName, mcpCatalog) {
18
+ const entry = mcpCatalog.get(displayName);
19
+ if (entry === undefined)
20
+ return { toolName: displayName };
21
+ return {
22
+ toolName: entry.toolName,
23
+ serverName: entry.serverName,
24
+ ...(entry.annotations !== undefined ? { annotations: entry.annotations } : {}),
25
+ };
26
+ }
6
27
  /**
7
28
  * Run the consumer's redactor over one tool result and return the value the
8
29
  * model should see. `{ output }` replaces; `undefined` (or a malformed return)
@@ -31,15 +52,19 @@ export async function applyRedaction(ctx, args) {
31
52
  /**
32
53
  * Materialize an agent's MCP tools as function tools (via {@link getAllMcpTools},
33
54
  * the same call the SDK makes internally for a native `Agent({ mcpServers })`
34
- * attach) and wrap each one's `invoke` with a redaction shim. Used only when the
35
- * agent has an `onToolResult` hook — otherwise the harness keeps the native
36
- * `mcpServers` attach unchanged (the #541-proven path).
55
+ * attach) so the harness can attach per-tool hooks the native attach doesn't
56
+ * expose: a **redaction** shim on each tool's `invoke` (when `redaction` is set)
57
+ * and a **`needsApproval` predicate** driven by the tool-approval policy (when
58
+ * `approval` is set). Used whenever EITHER hook is active; otherwise the harness
59
+ * keeps the native `mcpServers` attach unchanged (the #541-proven path).
37
60
  *
38
- * The `@openai/agents` MCP attach exposes no per-result rewrite hook, so owning
39
- * the function-tool `invoke` is the only model-visible seam that also carries
40
- * the `toolCallId` (`details.toolCall.callId`). The shim closure-captures the
41
- * {@link RedactionContext}, so `threadId` / `agentId` need no `RunContext`
42
- * plumbing.
61
+ * The `@openai/agents` MCP attach exposes neither a per-result rewrite hook nor
62
+ * a per-tool `needsApproval` seam, so owning the materialized function tool is
63
+ * the only place to attach both. The redaction shim closure-captures the
64
+ * {@link RedactionContext} (so `threadId` / `agentId` need no `RunContext`
65
+ * plumbing) and reads `toolCallId` from `details.toolCall.callId`; the approval
66
+ * predicate closure-captures the {@link McpApprovalContext} and resolves the
67
+ * live policy per call.
43
68
  *
44
69
  * **#541 is preserved:** `getAllMcpTools` calls `listTools()` per server, which
45
70
  * returns the warm instance cache (`cacheToolsList: true`) with no network
@@ -47,32 +72,55 @@ export async function applyRedaction(ctx, args) {
47
72
  * model-visible string output, not the SDK's internal error flag, so MCP
48
73
  * `isError` is best-effort (duck-typed) — matching Claude's `PostToolUse`.
49
74
  */
50
- export async function buildRedactedMcpTools(servers, ctx) {
75
+ export async function buildManagedMcpTools(servers, ctx) {
51
76
  const tools = await getAllMcpTools({ mcpServers: servers });
52
- return tools.map((tool) => (tool.type === 'function' ? wrapFunctionToolInvoke(tool, ctx) : tool));
77
+ return tools.map((tool) => (tool.type === 'function' ? manageFunctionTool(tool, ctx) : tool));
53
78
  }
54
- /** Wrap a function tool's `invoke` so its result passes through the redactor before the model sees it. */
55
- function wrapFunctionToolInvoke(tool, ctx) {
79
+ /**
80
+ * Rebuild a materialized MCP function tool with the active per-tool hooks:
81
+ * wrap `invoke` with the redactor (when set) and stamp a `needsApproval`
82
+ * predicate from the policy (when set). The single site that rebuilds the
83
+ * tool descriptor before it attaches to the turn's `Agent`.
84
+ */
85
+ function manageFunctionTool(tool, ctx) {
56
86
  const originalInvoke = tool.invoke.bind(tool);
87
+ const redaction = ctx.redaction;
88
+ const approval = ctx.approval;
57
89
  return {
58
90
  ...tool,
59
- invoke: async (runContext, input, details) => {
60
- const output = await originalInvoke(runContext, input, details);
61
- const toolCallId = details?.toolCall?.callId;
62
- // Without a callId the redaction input can't be faithfully attributed;
63
- // still redact (the redactor keys on toolName/output), passing an empty
64
- // id rather than dropping the hook.
65
- const redacted = await applyRedaction(ctx, {
66
- toolCallId: toolCallId ?? '',
67
- toolName: tool.name,
68
- output,
69
- isError: isErrorOutput(output),
70
- });
71
- // `invoke` must return `string | Result`; a redactor may return any
72
- // shape (the consumer owns matching the tool's expected shape, per the
73
- // ToolResultRedactor contract — the harness does not validate it).
74
- return redacted;
75
- },
91
+ // Gate the tool when the policy resolves its invocation to anything but
92
+ // `allow`. `require-approval` / `deny` return `true` so the run loop
93
+ // raises a `RunToolApprovalItem` interruption the coordinator settles
94
+ // (deny is auto-rejected there, so it never prompts the consumer);
95
+ // `allow` returns `false` so the tool executes inline with no round-trip.
96
+ // Without an approval context the tool keeps `getAllMcpTools`' default
97
+ // (never gated).
98
+ ...(approval !== undefined
99
+ ? {
100
+ needsApproval: async () => approval.policy(toToolInvocation(tool.name, approval.mcpCatalog)) !== 'allow',
101
+ }
102
+ : {}),
103
+ invoke: redaction === undefined
104
+ ? originalInvoke
105
+ : async (runContext, input, details) => {
106
+ const output = await originalInvoke(runContext, input, details);
107
+ const toolCallId = details?.toolCall?.callId;
108
+ // Without a callId the redaction input can't be faithfully
109
+ // attributed; still redact (the redactor keys on
110
+ // toolName/output), passing an empty id rather than
111
+ // dropping the hook.
112
+ const redacted = await applyRedaction(redaction, {
113
+ toolCallId: toolCallId ?? '',
114
+ toolName: tool.name,
115
+ output,
116
+ isError: isErrorOutput(output),
117
+ });
118
+ // `invoke` must return `string | Result`; a redactor may
119
+ // return any shape (the consumer owns matching the tool's
120
+ // expected shape, per the ToolResultRedactor contract — the
121
+ // harness does not validate it).
122
+ return redacted;
123
+ },
76
124
  };
77
125
  }
78
126
  /** Best-effort MCP error detection: the model-visible output carries no structured flag, so duck-type it. */
@@ -1 +1 @@
1
- {"version":3,"file":"openai-tool-redaction.js","sourceRoot":"","sources":["../src/openai-tool-redaction.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAA6B,MAAM,gBAAgB,CAAC;AAoB3E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAChC,GAAqB,EACrB,IAAiF;IAEjF,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC;IACjE,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC;QAC9B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,EAAE,IAAI,CAAC,OAAO;KACxB,CAAC,CAAC;IACH,2EAA2E;IAC3E,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC;AACpG,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,OAAoB,EAAE,GAAqB;IACnF,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5D,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACtG,CAAC;AAED,0GAA0G;AAC1G,SAAS,sBAAsB,CAAC,IAAyC,EAAE,GAAqB;IAC5F,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC9C,OAAO;QACH,GAAG,IAAI;QACP,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;YACzC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YAChE,MAAM,UAAU,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC;YAC7C,uEAAuE;YACvE,wEAAwE;YACxE,oCAAoC;YACpC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE;gBACvC,UAAU,EAAE,UAAU,IAAI,EAAE;gBAC5B,QAAQ,EAAE,IAAI,CAAC,IAAI;gBACnB,MAAM;gBACN,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC;aACjC,CAAC,CAAC;YACH,oEAAoE;YACpE,uEAAuE;YACvE,mEAAmE;YACnE,OAAO,QAAkB,CAAC;QAC9B,CAAC;KACJ,CAAC;AACN,CAAC;AAED,6GAA6G;AAC7G,SAAS,aAAa,CAAC,MAAe;IAClC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAK,MAAgC,CAAC,OAAO,KAAK,IAAI,CAAC;AAC/G,CAAC"}
1
+ {"version":3,"file":"openai-tool-redaction.js","sourceRoot":"","sources":["../src/openai-tool-redaction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Loads `config.rules` and concatenates the rule-file bodies into a single
3
+ * string. Mirrors the Mastra and Claude loaders (same fs walk, frontmatter
4
+ * strip, alphabetical sort, blank-line join), so the returned string is
5
+ * byte-identical to the rules portion of Mastra's composed `instructions` and
6
+ * the Claude harness's `rulesAppend`. The cross-harness `AgentConfig.rules`
7
+ * e2e (#493) compares the composed system-prompt body across harnesses, so
8
+ * this loader MUST stay in lockstep with the sibling copies.
9
+ *
10
+ * Each entry in `rulePaths` is `stat`ed:
11
+ * - File entry (`.md`): read directly.
12
+ * - Directory entry: scanned one level deep for `*.md` files (alphabetical,
13
+ * subdirectories ignored, non-`.md` skipped).
14
+ *
15
+ * For each loaded file, optional YAML frontmatter is stripped via
16
+ * `splitFrontmatterAndBody` and the trimmed body is concatenated verbatim,
17
+ * separated by blank lines — no envelope, no per-rule heading. Returns the
18
+ * empty string when no rules contribute content. A missing or unreadable
19
+ * rule entry rejects.
20
+ *
21
+ * The result is stored on `OpenAIAgentState.rulesAppend` and folded onto
22
+ * `config.instructions` in `buildAgent` when the per-turn `Agent` is
23
+ * constructed — see ARCHITECTURE.md → "Rule Composition". A cross-harness
24
+ * import of the sibling loaders is lint-blocked by `harnessIsolationPack`, so
25
+ * this is a deliberate copy (the same posture as `mcp-error-classifier.ts`).
26
+ */
27
+ export declare function composeRulesAppend(rulePaths: readonly string[] | undefined): Promise<string>;
28
+ /**
29
+ * Fold the composed rule bodies onto the base instructions, matching Mastra's
30
+ * `composeInstructionsWithRules` join (`base` + blank line + `rulesBlock`) so
31
+ * the effective system prompt is byte-identical across harnesses. Returns the
32
+ * base unchanged when there are no rules, and the rules block alone when there
33
+ * are no base instructions.
34
+ */
35
+ export declare function composeSystemPrompt(instructions: string | undefined, rulesAppend: string): string;
@@ -0,0 +1,88 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import { getErrorMessage, splitFrontmatterAndBody } from '@salesforce/agentic-common';
8
+ /**
9
+ * Loads `config.rules` and concatenates the rule-file bodies into a single
10
+ * string. Mirrors the Mastra and Claude loaders (same fs walk, frontmatter
11
+ * strip, alphabetical sort, blank-line join), so the returned string is
12
+ * byte-identical to the rules portion of Mastra's composed `instructions` and
13
+ * the Claude harness's `rulesAppend`. The cross-harness `AgentConfig.rules`
14
+ * e2e (#493) compares the composed system-prompt body across harnesses, so
15
+ * this loader MUST stay in lockstep with the sibling copies.
16
+ *
17
+ * Each entry in `rulePaths` is `stat`ed:
18
+ * - File entry (`.md`): read directly.
19
+ * - Directory entry: scanned one level deep for `*.md` files (alphabetical,
20
+ * subdirectories ignored, non-`.md` skipped).
21
+ *
22
+ * For each loaded file, optional YAML frontmatter is stripped via
23
+ * `splitFrontmatterAndBody` and the trimmed body is concatenated verbatim,
24
+ * separated by blank lines — no envelope, no per-rule heading. Returns the
25
+ * empty string when no rules contribute content. A missing or unreadable
26
+ * rule entry rejects.
27
+ *
28
+ * The result is stored on `OpenAIAgentState.rulesAppend` and folded onto
29
+ * `config.instructions` in `buildAgent` when the per-turn `Agent` is
30
+ * constructed — see ARCHITECTURE.md → "Rule Composition". A cross-harness
31
+ * import of the sibling loaders is lint-blocked by `harnessIsolationPack`, so
32
+ * this is a deliberate copy (the same posture as `mcp-error-classifier.ts`).
33
+ */
34
+ export async function composeRulesAppend(rulePaths) {
35
+ if (!rulePaths || rulePaths.length === 0)
36
+ return '';
37
+ const sections = [];
38
+ for (const entry of rulePaths) {
39
+ let stats;
40
+ try {
41
+ stats = await fs.stat(entry);
42
+ }
43
+ catch (err) {
44
+ throw new Error(`Failed to load rules from "${entry}": ${getErrorMessage(err)}`, { cause: err });
45
+ }
46
+ const filesToRead = stats.isDirectory() ? await listMarkdownFilesInDirectory(entry) : [entry];
47
+ for (const ruleFile of filesToRead) {
48
+ let raw;
49
+ try {
50
+ raw = await fs.readFile(ruleFile, 'utf8');
51
+ }
52
+ catch (err) {
53
+ throw new Error(`Failed to load rule from "${ruleFile}": ${getErrorMessage(err)}`, { cause: err });
54
+ }
55
+ const { body } = splitFrontmatterAndBody(raw);
56
+ const trimmed = body.trim();
57
+ if (trimmed.length === 0)
58
+ continue;
59
+ sections.push(trimmed);
60
+ }
61
+ }
62
+ return sections.join('\n\n');
63
+ }
64
+ /**
65
+ * Fold the composed rule bodies onto the base instructions, matching Mastra's
66
+ * `composeInstructionsWithRules` join (`base` + blank line + `rulesBlock`) so
67
+ * the effective system prompt is byte-identical across harnesses. Returns the
68
+ * base unchanged when there are no rules, and the rules block alone when there
69
+ * are no base instructions.
70
+ */
71
+ export function composeSystemPrompt(instructions, rulesAppend) {
72
+ const base = instructions ?? '';
73
+ if (rulesAppend.length === 0)
74
+ return base;
75
+ return base.length > 0 ? `${base}\n\n${rulesAppend}` : rulesAppend;
76
+ }
77
+ async function listMarkdownFilesInDirectory(dir) {
78
+ let names;
79
+ try {
80
+ const entries = await fs.readdir(dir, { withFileTypes: true });
81
+ names = entries.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md')).map((e) => e.name);
82
+ }
83
+ catch (err) {
84
+ throw new Error(`Failed to load rules from "${dir}": ${getErrorMessage(err)}`, { cause: err });
85
+ }
86
+ return names.sort().map((name) => path.join(dir, name));
87
+ }
88
+ //# sourceMappingURL=rule-composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-composer.js","sourceRoot":"","sources":["../src/rule-composer.ts"],"names":[],"mappings":"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"}