@intutic/gate 1.10.1

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/actions.d.ts +86 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +239 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/client.d.ts +94 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +207 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/dsh.d.ts +162 -0
  12. package/dist/dsh.d.ts.map +1 -0
  13. package/dist/dsh.js +189 -0
  14. package/dist/dsh.js.map +1 -0
  15. package/dist/errors.d.ts +41 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +53 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/eve.d.ts +425 -0
  20. package/dist/eve.d.ts.map +1 -0
  21. package/dist/eve.js +378 -0
  22. package/dist/eve.js.map +1 -0
  23. package/dist/gate.d.ts +113 -0
  24. package/dist/gate.d.ts.map +1 -0
  25. package/dist/gate.js +274 -0
  26. package/dist/gate.js.map +1 -0
  27. package/dist/harness.d.ts +482 -0
  28. package/dist/harness.d.ts.map +1 -0
  29. package/dist/harness.js +504 -0
  30. package/dist/harness.js.map +1 -0
  31. package/dist/headers.d.ts +26 -0
  32. package/dist/headers.d.ts.map +1 -0
  33. package/dist/headers.js +30 -0
  34. package/dist/headers.js.map +1 -0
  35. package/dist/imagecheck.d.ts +93 -0
  36. package/dist/imagecheck.d.ts.map +1 -0
  37. package/dist/imagecheck.js +286 -0
  38. package/dist/imagecheck.js.map +1 -0
  39. package/dist/index.d.ts +26 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +19 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/managedAgents.d.ts +375 -0
  44. package/dist/managedAgents.d.ts.map +1 -0
  45. package/dist/managedAgents.js +446 -0
  46. package/dist/managedAgents.js.map +1 -0
  47. package/dist/mastra.d.ts +175 -0
  48. package/dist/mastra.d.ts.map +1 -0
  49. package/dist/mastra.js +161 -0
  50. package/dist/mastra.js.map +1 -0
  51. package/dist/openai.d.ts +392 -0
  52. package/dist/openai.d.ts.map +1 -0
  53. package/dist/openai.js +658 -0
  54. package/dist/openai.js.map +1 -0
  55. package/dist/shlex.d.ts +16 -0
  56. package/dist/shlex.d.ts.map +1 -0
  57. package/dist/shlex.js +78 -0
  58. package/dist/shlex.js.map +1 -0
  59. package/dist/snapshot.d.ts +111 -0
  60. package/dist/snapshot.d.ts.map +1 -0
  61. package/dist/snapshot.js +243 -0
  62. package/dist/snapshot.js.map +1 -0
  63. package/dist/soprules.d.ts +88 -0
  64. package/dist/soprules.d.ts.map +1 -0
  65. package/dist/soprules.js +176 -0
  66. package/dist/soprules.js.map +1 -0
  67. package/dist/trueforge.d.ts +191 -0
  68. package/dist/trueforge.d.ts.map +1 -0
  69. package/dist/trueforge.js +184 -0
  70. package/dist/trueforge.js.map +1 -0
  71. package/dist/vercel.d.ts +174 -0
  72. package/dist/vercel.d.ts.map +1 -0
  73. package/dist/vercel.js +157 -0
  74. package/dist/vercel.js.map +1 -0
  75. package/dist/workflow.d.ts +209 -0
  76. package/dist/workflow.d.ts.map +1 -0
  77. package/dist/workflow.js +254 -0
  78. package/dist/workflow.js.map +1 -0
  79. package/dist/wrapTools.d.ts +68 -0
  80. package/dist/wrapTools.d.ts.map +1 -0
  81. package/dist/wrapTools.js +95 -0
  82. package/dist/wrapTools.js.map +1 -0
  83. package/package.json +123 -0
package/dist/openai.js ADDED
@@ -0,0 +1,658 @@
1
+ /**
2
+ * OpenAI Agents SDK (TypeScript) adapter: `intuticToolGuardrail()`,
3
+ * `wrapTools()`, `wrapAgent()`, `installOpenAiGate()`.
4
+ *
5
+ * `@intutic/gate/openai` — a framework-specific specialization of the same
6
+ * idea `wrapTools.ts` implements generically (see that file's module doc):
7
+ * wrap the framework's own veto point around this package's `Gate.guard()`.
8
+ * The TypeScript twin of the Python SDK's
9
+ * `intutic_clawde.gate.adapters.openai_agents` (`intutic_tool_guardrail`).
10
+ *
11
+ * ## The veto mechanism — CONFIRMED against a real install
12
+ *
13
+ * `@openai/agents@0.16.1` was installed as a devDependency of this package
14
+ * and its shipped `.d.ts`/compiled source read directly (not inferred from
15
+ * docs alone). `@openai/agents` is a meta-package (`export * from
16
+ * '@openai/agents-core'` + `'@openai/agents-openai'` + `export * as
17
+ * realtime`); the relevant surface lives in `@openai/agents-core`:
18
+ *
19
+ * The SDK's documented per-tool veto point is a **tool input guardrail**
20
+ * (`dist/toolGuardrail.d.ts`, present since `@openai/agents-core` 0.3.8,
21
+ * 2026-01-14):
22
+ *
23
+ * ```ts
24
+ * interface ToolInputGuardrailDefinition<TContext> {
25
+ * type: 'tool_input'
26
+ * name: string
27
+ * run: (data: { context: RunContext; agent: Agent; toolCall: protocol.FunctionCallItem })
28
+ * => Promise<ToolGuardrailFunctionOutput> // { behavior, outputInfo? }
29
+ * }
30
+ * // behavior: {type:'allow'} | {type:'rejectContent'; message} | {type:'throwException'}
31
+ * ```
32
+ *
33
+ * The runner (`dist/runner/toolExecution.js`) runs every guardrail in
34
+ * `FunctionTool.inputGuardrails` BEFORE `invoke` — on `rejectContent` the
35
+ * tool body never runs and `message` becomes the model-visible tool output
36
+ * (`resolveFunctionFailureOutput`). `protocol.FunctionCallItem.arguments` is
37
+ * the RAW, unparsed JSON arguments string (`z.ZodString`), same as the Python
38
+ * SDK's `ToolContext.tool_arguments` — hence {@link toolInputFromArguments}.
39
+ *
40
+ * Guardrail attachment is PER-TOOL: there is no run-level tool guardrail.
41
+ * That is what {@link wrapTools} and {@link wrapAgent} are for.
42
+ *
43
+ * Ordering caveat (documented, verified in `toolExecution.js`): for a tool
44
+ * whose `needsApproval` resolves true, input guardrails run AFTER the
45
+ * human-approval flow by default. Opt in to guardrails-before-approval with
46
+ * the run config `toolExecution: { preApprovalInputGuardrails: true }`
47
+ * (available since `@openai/agents-core` 0.11.8;
48
+ * `dist/runner/runConfig.d.ts`).
49
+ *
50
+ * ## The MCP materialization gotcha — why `wrapAgent` patches `getAllTools`
51
+ *
52
+ * Tools contributed by `agent.mcpServers` do NOT live in `agent.tools`: they
53
+ * are materialized per run by `agent.getAllTools()` → `getMcpTools()` →
54
+ * `mcpToFunctionTool()` (`dist/mcp.js`), which builds them with the same
55
+ * `tool()` factory — same guardrail path — but attaches NO guardrails of its
56
+ * own. Mapping over `agent.tools` alone therefore silently misses every
57
+ * MCP-sourced tool. {@link wrapAgent} closes that hole by patching the
58
+ * agent INSTANCE's `getAllTools` to inject the Intutic guardrail into every
59
+ * `FunctionTool` it returns, MCP-derived ones included (the runner resolves
60
+ * tools exclusively through `executionAgent.getAllTools(...)` —
61
+ * `dist/runner/modelPreparation.js`). Injection is idempotent (keyed on
62
+ * {@link GUARDRAIL_NAME}), so tools that are both in `agent.tools` and in the
63
+ * `getAllTools` result are gated exactly once.
64
+ *
65
+ * ## Non-function tools — what is and is not gateable
66
+ *
67
+ * * **hostedMcpTool** entries execute at OpenAI's side, but expose a
68
+ * client-visible approval loop: `providerData.require_approval` /
69
+ * `providerData.on_approval` (snake_case POST-construction — the
70
+ * camelCase `requireApproval`/`onApproval` exist only as `hostedMcpTool()`
71
+ * factory options; the runner reads only the snake_case fields,
72
+ * `dist/runner/mcpApprovals.js`). `wrapTools` rewrites these entries to
73
+ * `require_approval: 'always'` plus an Intutic `on_approval` that guards
74
+ * each call and composes any pre-existing handler. NOTE
75
+ * `hostedMcpTool()`'s own default is `require_approval: 'never'` — an
76
+ * unwrapped hosted MCP tool is entirely ungated.
77
+ * * **shellTool / applyPatchTool** (local) carry `needsApproval` +
78
+ * `onApproval` auto-resolvers. `wrapTools` forces `needsApproval` to
79
+ * always-true and installs a composed Intutic `onApproval`: gate refusal
80
+ * → reject (with the BLOCKED message); gate allow → the tool's ORIGINAL
81
+ * approval policy is replayed (original `needsApproval` false → approve;
82
+ * original `onApproval` present → delegated to). When the original
83
+ * policy demanded a human and no original `onApproval` exists, the
84
+ * decision is deliberately left undecided so the interruption still
85
+ * surfaces — see {@link OpenAiApprovalDecision} for the verified
86
+ * mechanism this relies on.
87
+ * * **computerTool** has NO `onApproval` — only a per-action
88
+ * `needsApproval` predicate (plus `onSafetyCheck`). `wrapTools` (and
89
+ * {@link intuticComputerNeedsApproval}) can therefore only turn a gate
90
+ * refusal into `needsApproval → true`, which surfaces as a pending
91
+ * interruption in `result.interruptions` that YOUR code must
92
+ * `state.reject(...)`. Stated honestly: a caller who `state.approve()`s
93
+ * that interruption overrides the gate — the SDK gives this adapter no
94
+ * reject-with-message hook for computer actions. (The refusal is still
95
+ * emitted to the control plane as `tool_blocked` telemetry either way.)
96
+ * * **Hosted tools** (`webSearchTool`, `fileSearchTool`,
97
+ * `codeInterpreterTool`, `imageGenerationTool`, ...) are bare
98
+ * `HostedTool` objects executed server-side at OpenAI: NO client-side
99
+ * guardrail, approval hook, or wrapper ever sees them. The
100
+ * hosted-environment variant of `shellTool` (`environment.type:
101
+ * 'container_*'`) is in the same bucket — its factory options type
102
+ * `needsApproval`/`onApproval` as `never`. `wrapTools` passes all of
103
+ * these through UNCHANGED rather than pretending they are gated.
104
+ *
105
+ * ## Realtime
106
+ *
107
+ * `@openai/agents-realtime` honours `FunctionTool.inputGuardrails` through
108
+ * the same `runToolInputGuardrails` helper (verified in its shipped
109
+ * `realtimeSession.js`), so a guardrail injected by this module also gates
110
+ * voice-agent tool calls.
111
+ *
112
+ * ## LLM egress and the tracing DLP leak
113
+ *
114
+ * Routing chat traffic through the Intutic proxy is env-only for this SDK:
115
+ * the underlying `openai` client (7.5.0, `client.js`) reads
116
+ * `OPENAI_BASE_URL` whenever no explicit `baseURL`/client is passed to
117
+ * `OpenAIProvider` — so a sourced `.env.intutic` governs egress with zero
118
+ * code, unlike the Vercel AI SDK (see vercel.ts's module doc). The default
119
+ * transport is the HTTP Responses API, whose wire shape the Intutic proxy
120
+ * already parses.
121
+ *
122
+ * BUT: `@openai/agents-openai`'s tracing exporter POSTs trace spans —
123
+ * including tool inputs/outputs — to a HARDCODED
124
+ * `https://api.openai.com/v1/traces/ingest` (`openaiTracingExporter.js`),
125
+ * bypassing `OPENAI_BASE_URL` entirely, and tracing is ON by default. That
126
+ * is a real DLP exfiltration path around the proxy.
127
+ * {@link suppressAgentsTracingExport} (called by {@link installOpenAiGate}
128
+ * unless opted out) sets the SDK's own kill-switch env
129
+ * `OPENAI_AGENTS_DISABLE_TRACING=1` (read lazily per run —
130
+ * `@openai/agents-core`'s `config.js`). Callers who need traces should
131
+ * instead re-point the exporter endpoint in code
132
+ * (`new OpenAITracingExporter({ endpoint })` + `setTraceProcessors`) at an
133
+ * approved collector and opt out with `{ tracingExport: 'keep' }`.
134
+ *
135
+ * ## What this module does NOT import
136
+ *
137
+ * Deliberately no runtime dependency on `@openai/agents`/
138
+ * `@openai/agents-core` — the `OpenAi*` types below are narrow structural
139
+ * copies of only the shapes this adapter touches, same policy as mastra.ts /
140
+ * vercel.ts / dsh.ts (see dsh.ts's module doc for the full rationale). The
141
+ * real packages are devDependencies used by `__tests__/openai.test.ts` to
142
+ * type-check these structural types against the real shipped ones and to
143
+ * drive the real runner machinery end to end.
144
+ *
145
+ * @module
146
+ */
147
+ import { randomUUID } from 'node:crypto';
148
+ import { Gate, active as activeGate, install } from './gate.js';
149
+ import { GateClient } from './client.js';
150
+ import { IntuticGateRefusal } from './errors.js';
151
+ /** The `name` every guardrail this module builds carries. Also the
152
+ * idempotency key: a `FunctionTool` whose `inputGuardrails` already contains
153
+ * a guardrail with this name is never double-wrapped. */
154
+ export const GUARDRAIL_NAME = 'intutic';
155
+ const WRAPPED = Symbol.for('intutic.gate.openai.wrapped');
156
+ const AGENT_WRAPPED = Symbol.for('intutic.gate.openai.agentWrapped');
157
+ // ------------------------------------------------------------------ helpers
158
+ function resolveGate(explicit, what) {
159
+ const g = explicit ?? activeGate();
160
+ if (g === null) {
161
+ throw new Error('No gate configured: call install(new Gate(...)) from @intutic/gate, or pass ' +
162
+ `{ gate } to ${what}. Refusing to run the tool unguarded.`);
163
+ }
164
+ return g;
165
+ }
166
+ /**
167
+ * Parse the raw JSON `arguments` string into the object `Gate.guard()`
168
+ * evaluates. Port of the Python adapter's `_tool_input_of` — same fallback
169
+ * keys (`raw`, `value`): a call the gate cannot parse should still reach a
170
+ * rule, not silently skip evaluation.
171
+ */
172
+ export function toolInputFromArguments(raw) {
173
+ if (!raw)
174
+ return {};
175
+ let parsed;
176
+ try {
177
+ parsed = JSON.parse(raw);
178
+ }
179
+ catch {
180
+ return { raw };
181
+ }
182
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
183
+ return parsed;
184
+ }
185
+ return { value: parsed };
186
+ }
187
+ function crashMessage(err) {
188
+ const detail = err instanceof Error ? err.message : String(err);
189
+ return `[Intutic Governance] BLOCKED: gate crashed (${detail}) — failing closed rather than allowing an unevaluated call.`;
190
+ }
191
+ /** Refusal → rejection decision; anything else → fail-closed rejection.
192
+ * Same posture as dsh.ts's `createPreExecuteListener` (see that module's
193
+ * doc for why every adapter takes the closed direction on failure). */
194
+ function errorToDecision(err) {
195
+ if (err instanceof IntuticGateRefusal) {
196
+ return { approve: false, reason: err.message };
197
+ }
198
+ return { approve: false, reason: crashMessage(err) };
199
+ }
200
+ // -------------------------------------------------------- primary guardrail
201
+ /**
202
+ * Builds the Intutic tool input guardrail — `{ type: 'tool_input', name:
203
+ * 'intutic', run }` — attachable to any `FunctionTool`, either at
204
+ * construction:
205
+ *
206
+ * ```ts
207
+ * import { tool } from '@openai/agents'
208
+ * import { Gate, install } from '@intutic/gate'
209
+ * import { intuticToolGuardrail } from '@intutic/gate/openai'
210
+ *
211
+ * install(new Gate({ workspaceId: process.env.INTUTIC_WORKSPACE_ID }))
212
+ *
213
+ * const shell = tool({
214
+ * name: 'shell',
215
+ * description: 'Run a shell command',
216
+ * parameters: z.object({ command: z.string() }),
217
+ * inputGuardrails: [intuticToolGuardrail()],
218
+ * execute: async ({ command }) => run(command),
219
+ * })
220
+ * ```
221
+ *
222
+ * or after the fact via {@link wrapTools}/{@link wrapAgent}. On refusal the
223
+ * guardrail resolves `{ behavior: { type: 'rejectContent', message } }` —
224
+ * the runner skips the tool body and hands `message` (the
225
+ * `[Intutic Governance] BLOCKED: ...` text) back to the model in place of a
226
+ * tool result. On an unexpected gate crash it rejects too (fail closed). On
227
+ * allow it resolves `{ behavior: { type: 'allow' } }`.
228
+ *
229
+ * Requires a `Gate` — either passed via `{ gate }`, or installed
230
+ * process-wide with `install(new Gate(...))`. A call with neither throws
231
+ * out of `run` (which the runner escalates into a run-fatal `ToolCallError`)
232
+ * rather than running the tool unguarded — same posture as
233
+ * `wrapTool`/`intuticHooks`/`intuticToolApproval`.
234
+ */
235
+ export function intuticToolGuardrail(opts = {}) {
236
+ return {
237
+ type: 'tool_input',
238
+ name: GUARDRAIL_NAME,
239
+ run: async (data) => {
240
+ const g = resolveGate(opts.gate, 'intuticToolGuardrail()');
241
+ try {
242
+ await g.guard(data.toolCall.name, toolInputFromArguments(data.toolCall.arguments));
243
+ }
244
+ catch (err) {
245
+ if (err instanceof IntuticGateRefusal) {
246
+ return {
247
+ behavior: { type: 'rejectContent', message: err.message },
248
+ outputInfo: { code: err.code, incidentId: err.incidentId },
249
+ };
250
+ }
251
+ return { behavior: { type: 'rejectContent', message: crashMessage(err) } };
252
+ }
253
+ return { behavior: { type: 'allow' } };
254
+ },
255
+ };
256
+ }
257
+ // ------------------------------------------------------- per-type wrapping
258
+ function wrapFunctionTool(tool, opts) {
259
+ const existing = tool.inputGuardrails ?? [];
260
+ if (existing.some((gr) => gr.name === GUARDRAIL_NAME))
261
+ return tool;
262
+ // Intutic FIRST: a governance refusal should not depend on (or pay the
263
+ // cost of) whatever caller-authored guardrails follow it.
264
+ return { ...tool, inputGuardrails: [intuticToolGuardrail(opts), ...existing] };
265
+ }
266
+ function isHostedMcp(tool) {
267
+ if (tool.name === 'hosted_mcp')
268
+ return true;
269
+ const pd = tool.providerData;
270
+ return pd?.type === 'mcp';
271
+ }
272
+ /** Whether the tool's ORIGINAL `require_approval` demanded approval for
273
+ * `toolName`. Interpretation of the filter object form: a tool listed under
274
+ * `never` does not need approval; everything else does (matching the
275
+ * conservative reading of OpenAI's approval filter — unlisted tools require
276
+ * approval unless the whole setting is 'never'). */
277
+ function originallyDemandedApproval(requireApproval, toolName) {
278
+ if (requireApproval === undefined || requireApproval === 'never')
279
+ return false;
280
+ if (requireApproval === 'always')
281
+ return true;
282
+ if (requireApproval !== null && typeof requireApproval === 'object') {
283
+ const filter = requireApproval;
284
+ const neverNames = filter.never?.tool_names ?? filter.never?.toolNames ?? [];
285
+ if (neverNames.includes(toolName))
286
+ return false;
287
+ return true;
288
+ }
289
+ return false;
290
+ }
291
+ function hostedMcpApprovalDetails(item) {
292
+ const rawItem = item?.rawItem;
293
+ if (!rawItem || rawItem.type !== 'hosted_tool_call')
294
+ return null;
295
+ const pd = rawItem.providerData ?? {};
296
+ const toolName = typeof pd.name === 'string' && pd.name.length > 0
297
+ ? pd.name
298
+ : typeof rawItem.name === 'string' && rawItem.name !== 'mcp_approval_request'
299
+ ? rawItem.name
300
+ : null;
301
+ if (toolName === null)
302
+ return null;
303
+ return {
304
+ toolName,
305
+ ...(typeof pd.arguments === 'string' ? { rawArguments: pd.arguments } : {}),
306
+ };
307
+ }
308
+ function wrapHostedMcpTool(tool, opts) {
309
+ const marked = tool;
310
+ if (marked[WRAPPED] === true)
311
+ return tool;
312
+ const pd = { ...(tool.providerData ?? {}) };
313
+ const originalOnApproval = pd.on_approval ?? pd.onApproval;
314
+ const originalRequireApproval = pd.require_approval ?? pd.requireApproval;
315
+ pd.require_approval = 'always';
316
+ pd.on_approval = async (context, item) => {
317
+ const g = resolveGate(opts.gate, 'wrapTools()');
318
+ const details = hostedMcpApprovalDetails(item);
319
+ if (details === null) {
320
+ // Nothing recognisable to evaluate — refuse rather than approve a call
321
+ // the gate never saw.
322
+ return {
323
+ approve: false,
324
+ reason: '[Intutic Governance] BLOCKED: could not extract the MCP tool call from the approval ' +
325
+ 'request — failing closed rather than approving an unevaluated call.',
326
+ };
327
+ }
328
+ try {
329
+ await g.guard(details.toolName, toolInputFromArguments(details.rawArguments));
330
+ }
331
+ catch (err) {
332
+ return errorToDecision(err);
333
+ }
334
+ if (originalOnApproval)
335
+ return originalOnApproval(context, item);
336
+ if (originallyDemandedApproval(originalRequireApproval, details.toolName)) {
337
+ // The caller's original config demanded a human for this tool but gave
338
+ // the runner no resolver. The hosted-MCP approval loop consumes
339
+ // decisions with a truthy check (see OpenAiApprovalDecision's doc), so
340
+ // there is no "leave it pending" here: per this product's own posture
341
+ // (gate.ts, SOP_RULE_APPROVAL), an approval that cannot be granted is a
342
+ // block. Keep your own onApproval on the hostedMcpTool — this wrapper
343
+ // composes with it — to retain a human flow.
344
+ return {
345
+ approve: false,
346
+ reason: '[Intutic Governance] BLOCKED: this MCP tool call requires human approval ' +
347
+ '(require_approval) and the tool has no onApproval resolver; an approval that ' +
348
+ 'cannot be granted in an unattended run is a block.',
349
+ };
350
+ }
351
+ return { approve: true };
352
+ };
353
+ delete pd.requireApproval;
354
+ delete pd.onApproval;
355
+ const wrapped = { ...tool, providerData: pd };
356
+ Object.defineProperty(wrapped, WRAPPED, { value: true, enumerable: false });
357
+ return wrapped;
358
+ }
359
+ /** Shared shell/apply_patch wrapping: force every action through the
360
+ * approval path, gate it there, then replay the tool's original policy.
361
+ *
362
+ * Fall-through, stated because it is easy to miss (TD-406): this relies on
363
+ * `needsApproval`/`onApproval` being the SDK's veto point for these two
364
+ * hosted tools. A future `@openai/agents` that executes a shell or
365
+ * apply_patch action without consulting `onApproval` would bypass the gate
366
+ * with no error here — which is why `package.json` pins the exact version
367
+ * this was verified against, and a bump must re-run the openai adapter
368
+ * tests (`__tests__/openai.test.ts`) before landing. */
369
+ function wrapApprovalGatedTool(tool, opts, extract) {
370
+ const marked = tool;
371
+ if (marked[WRAPPED] === true)
372
+ return tool;
373
+ const originalNeedsApproval = tool.needsApproval;
374
+ const originalOnApproval = tool.onApproval;
375
+ const onApproval = async (runContext, item) => {
376
+ const g = resolveGate(opts.gate, 'wrapTools()');
377
+ const details = extract(item?.rawItem);
378
+ if (details === null) {
379
+ return {
380
+ approve: false,
381
+ reason: `[Intutic Governance] BLOCKED: could not extract the ${tool.type} action from the ` +
382
+ 'approval item — failing closed rather than approving an unevaluated call.',
383
+ };
384
+ }
385
+ try {
386
+ for (const toolInput of details.toolInputs) {
387
+ await g.guard(tool.name, toolInput);
388
+ }
389
+ }
390
+ catch (err) {
391
+ return errorToDecision(err);
392
+ }
393
+ // Gate allows. Replay the tool's ORIGINAL approval policy so wrapping is
394
+ // behaviour-preserving beyond the gate itself.
395
+ const originallyNeeded = typeof originalNeedsApproval === 'function'
396
+ ? await originalNeedsApproval(runContext, details.action, details.callId)
397
+ : Boolean(originalNeedsApproval);
398
+ if (!originallyNeeded)
399
+ return { approve: true };
400
+ if (originalOnApproval)
401
+ return originalOnApproval(runContext, item);
402
+ // Original policy: interruption for a human. Leave the decision unmade —
403
+ // the runner's `=== true`/`=== false` checks fall through to "pending"
404
+ // and the interruption surfaces exactly as before wrapping. See
405
+ // OpenAiApprovalDecision's doc for the verification of this mechanism.
406
+ return {};
407
+ };
408
+ const wrapped = { ...tool, needsApproval: async () => true, onApproval };
409
+ Object.defineProperty(wrapped, WRAPPED, { value: true, enumerable: false });
410
+ return wrapped;
411
+ }
412
+ function shellDetails(rawItem) {
413
+ const raw = rawItem;
414
+ if (!raw || raw.type !== 'shell_call')
415
+ return null;
416
+ const commands = Array.isArray(raw.action?.commands) ? raw.action.commands : null;
417
+ if (commands === null)
418
+ return null;
419
+ return {
420
+ // One guard() per command: a snapshot/SOP rule written against `command`
421
+ // must see each command on its own, not a joined blob.
422
+ toolInputs: commands.map((command) => ({ command: String(command) })),
423
+ action: raw.action,
424
+ ...(typeof raw.callId === 'string' ? { callId: raw.callId } : {}),
425
+ };
426
+ }
427
+ function applyPatchDetails(rawItem) {
428
+ const raw = rawItem;
429
+ if (!raw || raw.type !== 'apply_patch_call' || !raw.operation)
430
+ return null;
431
+ const op = raw.operation;
432
+ if (typeof op.path !== 'string')
433
+ return null;
434
+ const toolInput = {
435
+ path: op.path,
436
+ operation: typeof op.type === 'string' ? op.type : 'unknown',
437
+ // `content` so gate.ts's authoring-time infra check sees the diff body.
438
+ ...(typeof op.diff === 'string' ? { content: op.diff } : {}),
439
+ ...(typeof op.moveTo === 'string' ? { move_to: op.moveTo } : {}),
440
+ };
441
+ return {
442
+ toolInputs: [toolInput],
443
+ action: op,
444
+ ...(typeof raw.callId === 'string' ? { callId: raw.callId } : {}),
445
+ };
446
+ }
447
+ /**
448
+ * Gate-driven `needsApproval` predicate for `computerTool` — the ONLY
449
+ * pre-execution hook that tool exposes (no guardrails, no `onApproval`):
450
+ *
451
+ * ```ts
452
+ * computerTool({ computer, needsApproval: intuticComputerNeedsApproval() })
453
+ * ```
454
+ *
455
+ * A gate-refused (or gate-crashed — fail closed) action resolves `true`,
456
+ * which the runner turns into a pending interruption in
457
+ * `result.interruptions`; your interruption handler must `state.reject(...)`
458
+ * it. This is honestly weaker than the guardrail path: the SDK offers no way
459
+ * for this adapter to auto-reject a computer action with a message, and a
460
+ * handler that `state.approve()`s the interruption overrides the gate. An
461
+ * allowed action defers to `fallback` (the predicate the tool would
462
+ * otherwise have used; defaults to no approval).
463
+ */
464
+ export function intuticComputerNeedsApproval(opts = {}) {
465
+ const toolName = opts.toolName ?? 'computer_use_preview';
466
+ return async (runContext, action, callId) => {
467
+ const g = resolveGate(opts.gate, 'intuticComputerNeedsApproval()');
468
+ const toolInput = action !== null && typeof action === 'object' && !Array.isArray(action)
469
+ ? action
470
+ : { value: action };
471
+ try {
472
+ await g.guard(toolName, toolInput);
473
+ }
474
+ catch {
475
+ // Refusal OR crash: force the approval interruption (fail closed).
476
+ // The refusal itself was already emitted as tool_blocked telemetry
477
+ // inside guard().
478
+ return true;
479
+ }
480
+ if (opts.fallback) {
481
+ return opts.fallback(runContext, action, callId);
482
+ }
483
+ return false;
484
+ };
485
+ }
486
+ function wrapComputerTool(tool, opts) {
487
+ const marked = tool;
488
+ if (marked[WRAPPED] === true)
489
+ return tool;
490
+ const wrapped = {
491
+ ...tool,
492
+ needsApproval: intuticComputerNeedsApproval({
493
+ ...opts,
494
+ toolName: tool.name,
495
+ fallback: typeof tool.needsApproval === 'function' ? tool.needsApproval : undefined,
496
+ }),
497
+ };
498
+ Object.defineProperty(wrapped, WRAPPED, { value: true, enumerable: false });
499
+ return wrapped;
500
+ }
501
+ // -------------------------------------------------------------- public API
502
+ function wrapOneTool(tool, opts) {
503
+ const t = tool;
504
+ switch (t.type) {
505
+ case 'function':
506
+ return wrapFunctionTool(tool, opts);
507
+ case 'hosted_tool': {
508
+ const hosted = tool;
509
+ // Non-MCP hosted tools (webSearch, fileSearch, codeInterpreter, image
510
+ // generation, ...) execute server-side at OpenAI with no client-side
511
+ // hook — pass through UNCHANGED, documented in the module doc.
512
+ return isHostedMcp(hosted) ? wrapHostedMcpTool(hosted, opts) : tool;
513
+ }
514
+ case 'shell': {
515
+ // Hosted-environment shell (no local `shell` impl) is server-side —
516
+ // its factory types needsApproval/onApproval as `never`. Pass through.
517
+ if (t.shell === undefined)
518
+ return tool;
519
+ return wrapApprovalGatedTool(tool, opts, shellDetails);
520
+ }
521
+ case 'apply_patch':
522
+ return wrapApprovalGatedTool(tool, opts, applyPatchDetails);
523
+ case 'computer':
524
+ return wrapComputerTool(tool, opts);
525
+ default:
526
+ return tool;
527
+ }
528
+ }
529
+ /**
530
+ * Wrap a list of OpenAI Agents SDK tools so `Gate.guard()` runs before each
531
+ * one executes — see the module doc for exactly what each tool `type` gets
532
+ * (and what hosted tools cannot get). Returns NEW tool objects (shallow
533
+ * copies); the ones passed in are not mutated. Wrapping twice is a no-op.
534
+ *
535
+ * NOTE: this covers only the tools you hand it. Tools materialized from
536
+ * `agent.mcpServers` do not exist yet at wrapping time — use
537
+ * {@link wrapAgent} for an agent with MCP servers.
538
+ */
539
+ export function wrapTools(tools, opts = {}) {
540
+ return tools.map((t) => wrapOneTool(t, opts));
541
+ }
542
+ /**
543
+ * Gate every tool an agent can reach, MCP-sourced tools included.
544
+ *
545
+ * ```ts
546
+ * import { Agent, run } from '@openai/agents'
547
+ * import { installOpenAiGate, wrapAgent } from '@intutic/gate/openai'
548
+ *
549
+ * installOpenAiGate({ workspaceId: process.env.INTUTIC_WORKSPACE_ID })
550
+ *
551
+ * const agent = wrapAgent(new Agent({
552
+ * name: 'ops',
553
+ * tools: [shell],
554
+ * mcpServers: [filesystemServer],
555
+ * }))
556
+ * const result = await run(agent, prompt)
557
+ * ```
558
+ *
559
+ * Two things happen, both idempotent:
560
+ *
561
+ * 1. `agent.tools` is replaced with {@link wrapTools}' wrapped copies.
562
+ * 2. The agent INSTANCE's `getAllTools` — the materializer the runner
563
+ * actually consumes each turn, and the ONLY place `mcpServers`-derived
564
+ * tools ever appear (see module doc) — is patched to wrap everything it
565
+ * returns.
566
+ *
567
+ * MUTATES (and returns) the agent passed in.
568
+ *
569
+ * Scope, stated plainly: this wraps ONE agent. Agents reached via handoffs
570
+ * are separate objects — wrap each of them too.
571
+ */
572
+ export function wrapAgent(agent, opts = {}) {
573
+ const marked = agent;
574
+ if (marked[AGENT_WRAPPED] === true)
575
+ return agent;
576
+ agent.tools = wrapTools(agent.tools, opts);
577
+ const originalGetAllTools = agent.getAllTools.bind(agent);
578
+ agent.getAllTools = async (...args) => wrapTools(await originalGetAllTools(...args), opts);
579
+ Object.defineProperty(marked, AGENT_WRAPPED, { value: true, enumerable: false });
580
+ return agent;
581
+ }
582
+ // ------------------------------------------------------------ installation
583
+ /**
584
+ * Close the tracing DLP leak: set the SDK's own kill-switch env
585
+ * (`OPENAI_AGENTS_DISABLE_TRACING=1`) so the default exporter never POSTs
586
+ * trace spans — which carry tool inputs/outputs — to its HARDCODED
587
+ * `https://api.openai.com/v1/traces/ingest` endpoint (which ignores
588
+ * `OPENAI_BASE_URL`, i.e. bypasses the Intutic proxy). The SDK reads the env
589
+ * lazily per run, so calling this any time before `run(...)` is effective.
590
+ *
591
+ * To keep traces instead, re-point the exporter at an approved collector in
592
+ * your own code — `setTraceProcessors([new BatchTraceProcessor(new
593
+ * OpenAITracingExporter({ endpoint }))])` — and pass
594
+ * `{ tracingExport: 'keep' }` to {@link installOpenAiGate}.
595
+ */
596
+ export function suppressAgentsTracingExport() {
597
+ process.env.OPENAI_AGENTS_DISABLE_TRACING = '1';
598
+ }
599
+ /**
600
+ * One-call setup: build a {@link Gate} (with a `GateClient` from the
601
+ * environment when credentials allow — degrading to a client-less gate, Tier
602
+ * B/A3 inactive, rather than throwing out of setup, same posture as dsh.ts's
603
+ * `apply()`), `install()` it process-wide so every guardrail/wrapper in this
604
+ * module picks it up, and close the tracing leak (unless `tracingExport:
605
+ * 'keep'`). Returns the gate.
606
+ *
607
+ * Session id: one per PROCESS (`sessionId` option, else
608
+ * `INTUTIC_SESSION_ID`, else a random UUID) — the SDK has no single "current
609
+ * session" readable at install time; see dsh.ts's `resolveSessionId` for the
610
+ * full rationale this mirrors.
611
+ */
612
+ /**
613
+ * Where to point `@openai/agents`' tracing exporter so traces stay governed
614
+ * (TD-405): the control plane's ingest route, authenticated with the same
615
+ * API key the gate uses. Pass the result to the SDK's own exporter —
616
+ *
617
+ * ```ts
618
+ * import { setTraceProcessors, BatchTraceProcessor, OpenAITracingExporter } from '@openai/agents'
619
+ * setTraceProcessors([new BatchTraceProcessor(new OpenAITracingExporter(intuticTracingExporterOptions()))])
620
+ * ```
621
+ *
622
+ * — and pass `{ tracingExport: 'intutic' }` to {@link installOpenAiGate}. No
623
+ * `@openai/agents` import here, so this module stays loadable without the SDK.
624
+ * Generation spans become traces in the ledger; tool spans are counted, not
625
+ * stored (the route's own doc says why).
626
+ */
627
+ export function intuticTracingExporterOptions(env = process.env) {
628
+ // Trailing slashes stripped with a loop, not `/\/+$/`: CodeQL flags that
629
+ // regex as polynomial on a long run of slashes, and the input is operator
630
+ // config read once, so a loop is both safe and obviously linear.
631
+ let base = env.INTUTIC_CONTROL_PLANE_URL ?? '';
632
+ while (base.endsWith('/'))
633
+ base = base.slice(0, -1);
634
+ const apiKey = env.INTUTIC_API_KEY ?? '';
635
+ if (!base || !apiKey) {
636
+ throw new Error('intuticTracingExporterOptions: INTUTIC_CONTROL_PLANE_URL and INTUTIC_API_KEY must be set to route traces to Intutic');
637
+ }
638
+ return { endpoint: `${base}/api/v1/integrations/openai-agents/traces/ingest`, apiKey };
639
+ }
640
+ export function installOpenAiGate(config = {}) {
641
+ if ((config.tracingExport ?? 'disable') === 'disable') {
642
+ suppressAgentsTracingExport();
643
+ }
644
+ let client;
645
+ try {
646
+ client = GateClient.fromEnv({
647
+ harness: 'openai-agents',
648
+ sessionId: config.sessionId || process.env.INTUTIC_SESSION_ID || randomUUID(),
649
+ });
650
+ }
651
+ catch {
652
+ client = null;
653
+ }
654
+ const gate = new Gate(config, client);
655
+ install(gate);
656
+ return gate;
657
+ }
658
+ //# sourceMappingURL=openai.js.map