@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.
- package/CHANGELOG.md +35 -8
- package/README.md +15 -4
- package/dist/openai-agents-harness.d.ts +21 -11
- package/dist/openai-agents-harness.js +164 -78
- package/dist/openai-agents-harness.js.map +1 -1
- package/dist/openai-approval-coordinator.d.ts +66 -18
- package/dist/openai-approval-coordinator.js +127 -28
- package/dist/openai-approval-coordinator.js.map +1 -1
- package/dist/openai-built-in-policies.d.ts +4 -3
- package/dist/openai-built-in-policies.js +4 -3
- package/dist/openai-built-in-policies.js.map +1 -1
- package/dist/openai-event-adapter.d.ts +8 -3
- package/dist/openai-event-adapter.js +9 -4
- package/dist/openai-event-adapter.js.map +1 -1
- package/dist/openai-mcp-state.d.ts +22 -7
- package/dist/openai-message-mapper.d.ts +58 -9
- package/dist/openai-message-mapper.js +137 -12
- package/dist/openai-message-mapper.js.map +1 -1
- package/dist/openai-skill-tools.d.ts +36 -0
- package/dist/openai-skill-tools.js +209 -0
- package/dist/openai-skill-tools.js.map +1 -0
- package/dist/openai-tool-redaction.d.ts +44 -10
- package/dist/openai-tool-redaction.js +77 -29
- package/dist/openai-tool-redaction.js.map +1 -1
- package/dist/rule-composer.d.ts +35 -0
- package/dist/rule-composer.js +88 -0
- package/dist/rule-composer.js.map +1 -0
- package/dist/skill-loader.d.ts +76 -0
- package/dist/skill-loader.js +180 -0
- package/dist/skill-loader.js.map +1 -0
- package/dist/test/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -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)
|
|
40
|
-
*
|
|
41
|
-
* `
|
|
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
|
|
44
|
-
*
|
|
45
|
-
* the
|
|
46
|
-
* {@link RedactionContext}
|
|
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
|
|
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)
|
|
35
|
-
*
|
|
36
|
-
* `
|
|
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
|
|
39
|
-
*
|
|
40
|
-
* the
|
|
41
|
-
* {@link RedactionContext}
|
|
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
|
|
75
|
+
export async function buildManagedMcpTools(servers, ctx) {
|
|
51
76
|
const tools = await getAllMcpTools({ mcpServers: servers });
|
|
52
|
-
return tools.map((tool) => (tool.type === 'function' ?
|
|
77
|
+
return tools.map((tool) => (tool.type === 'function' ? manageFunctionTool(tool, ctx) : tool));
|
|
53
78
|
}
|
|
54
|
-
/**
|
|
55
|
-
function
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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;
|
|
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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"}
|