@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.
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/actions.d.ts +86 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +239 -0
- package/dist/actions.js.map +1 -0
- package/dist/client.d.ts +94 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +207 -0
- package/dist/client.js.map +1 -0
- package/dist/dsh.d.ts +162 -0
- package/dist/dsh.d.ts.map +1 -0
- package/dist/dsh.js +189 -0
- package/dist/dsh.js.map +1 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/eve.d.ts +425 -0
- package/dist/eve.d.ts.map +1 -0
- package/dist/eve.js +378 -0
- package/dist/eve.js.map +1 -0
- package/dist/gate.d.ts +113 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +274 -0
- package/dist/gate.js.map +1 -0
- package/dist/harness.d.ts +482 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +504 -0
- package/dist/harness.js.map +1 -0
- package/dist/headers.d.ts +26 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +30 -0
- package/dist/headers.js.map +1 -0
- package/dist/imagecheck.d.ts +93 -0
- package/dist/imagecheck.d.ts.map +1 -0
- package/dist/imagecheck.js +286 -0
- package/dist/imagecheck.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/managedAgents.d.ts +375 -0
- package/dist/managedAgents.d.ts.map +1 -0
- package/dist/managedAgents.js +446 -0
- package/dist/managedAgents.js.map +1 -0
- package/dist/mastra.d.ts +175 -0
- package/dist/mastra.d.ts.map +1 -0
- package/dist/mastra.js +161 -0
- package/dist/mastra.js.map +1 -0
- package/dist/openai.d.ts +392 -0
- package/dist/openai.d.ts.map +1 -0
- package/dist/openai.js +658 -0
- package/dist/openai.js.map +1 -0
- package/dist/shlex.d.ts +16 -0
- package/dist/shlex.d.ts.map +1 -0
- package/dist/shlex.js +78 -0
- package/dist/shlex.js.map +1 -0
- package/dist/snapshot.d.ts +111 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +243 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/soprules.d.ts +88 -0
- package/dist/soprules.d.ts.map +1 -0
- package/dist/soprules.js +176 -0
- package/dist/soprules.js.map +1 -0
- package/dist/trueforge.d.ts +191 -0
- package/dist/trueforge.d.ts.map +1 -0
- package/dist/trueforge.js +184 -0
- package/dist/trueforge.js.map +1 -0
- package/dist/vercel.d.ts +174 -0
- package/dist/vercel.d.ts.map +1 -0
- package/dist/vercel.js +157 -0
- package/dist/vercel.js.map +1 -0
- package/dist/workflow.d.ts +209 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +254 -0
- package/dist/workflow.js.map +1 -0
- package/dist/wrapTools.d.ts +68 -0
- package/dist/wrapTools.d.ts.map +1 -0
- package/dist/wrapTools.js +95 -0
- package/dist/wrapTools.js.map +1 -0
- package/package.json +123 -0
package/dist/harness.js
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ai-sdk/harness` adapter: `intuticApprovalResponder()`, `intuticSubmitApprovals()`,
|
|
3
|
+
* `intuticStaticApprovals()`, `recommendedHarnessSettings()`, and
|
|
4
|
+
* `intuticSandboxBootstrap()`.
|
|
5
|
+
*
|
|
6
|
+
* `@intutic/gate/harness` — the same idea `vercel.ts` implements for the base
|
|
7
|
+
* `ai` package (wrap the framework's own veto point around this package's
|
|
8
|
+
* `Gate.guard()`), reshaped for Vercel's sandboxed coding-agent runtime,
|
|
9
|
+
* whose veto surface is materially different from `generateText`'s.
|
|
10
|
+
*
|
|
11
|
+
* ## Confirmed against a real install
|
|
12
|
+
*
|
|
13
|
+
* `@ai-sdk/harness@1.0.75` (plus `@ai-sdk/harness-claude-code@1.0.78` and
|
|
14
|
+
* `@ai-sdk/harness-grok-build@1.0.12`) were installed and their shipped
|
|
15
|
+
* `.d.ts`/compiled source read directly (not inferred from docs alone):
|
|
16
|
+
*
|
|
17
|
+
* 1. **The `toolApproval` setting is STATIC.** `HarnessAgentSettings.toolApproval`
|
|
18
|
+
* is `HarnessAgentToolApprovalConfiguration = Readonly<Record<string,
|
|
19
|
+
* ToolApprovalStatus>>` (`dist/agent/index.d.ts`), whose own doc comment says
|
|
20
|
+
* it mirrors "AI SDK `toolApproval` object configuration for host-executed
|
|
21
|
+
* tools, **without callback support**". `vercel.ts`'s callback-shaped
|
|
22
|
+
* `intuticToolApproval()` therefore does NOT apply here — there is no place
|
|
23
|
+
* to hang a function. Per-call gating instead goes through the approval
|
|
24
|
+
* FLOW: mark tools `'user-approval'` (see {@link intuticStaticApprovals}),
|
|
25
|
+
* let the turn pause, and answer each pending approval with the gate
|
|
26
|
+
* (see {@link intuticApprovalResponder}).
|
|
27
|
+
* 2. **The portable answer path is continuation-based.**
|
|
28
|
+
* `HarnessAgentToolApprovalContinuation` carries `{ approvalResponse,
|
|
29
|
+
* toolCall }`; `continueTurn`/`continueGenerate`/`continueStream` accept
|
|
30
|
+
* `toolApprovalContinuations` (dist/agent/index.d.ts). The adapter-session
|
|
31
|
+
* method `session.submitToolApproval?({approvalId, approved, reason})` is
|
|
32
|
+
* OPTIONAL (`dist/index.d.ts` — `HarnessV1PromptControl`), and support
|
|
33
|
+
* genuinely varies per adapter: `@ai-sdk/harness-claude-code@1.0.78`
|
|
34
|
+
* implements it; `@ai-sdk/harness-grok-build@1.0.12` does not (zero
|
|
35
|
+
* occurrences in its shipped dist). This module therefore builds the
|
|
36
|
+
* responder on the continuation path and treats `submitToolApproval` as an
|
|
37
|
+
* optimization when present ({@link intuticSubmitApprovals}).
|
|
38
|
+
* 3. **Built-in sandbox tools are NOT governed by `toolApproval` at all.**
|
|
39
|
+
* They are governed only by `permissionMode: 'allow-reads' | 'allow-edits'
|
|
40
|
+
* | 'allow-all'`, which **defaults to `'allow-all'`** — the shipped doc
|
|
41
|
+
* comment says so verbatim: "Defaults to `'allow-all'`, preserving the
|
|
42
|
+
* existing bypass-permissions behavior unless users opt in." A HarnessAgent
|
|
43
|
+
* with default settings runs read/write/edit/bash/glob/grep in the sandbox
|
|
44
|
+
* with no approval step this gate could hook.
|
|
45
|
+
* {@link recommendedHarnessSettings} exists to steer callers off that
|
|
46
|
+
* default.
|
|
47
|
+
* 4. **Execution is server-side, in a sandbox.** The agent's tools run inside
|
|
48
|
+
* a microVM (`@ai-sdk/sandbox-vercel` and friends), NOT on the machine
|
|
49
|
+
* where the Intutic proxy runs — the laptop proxy cannot see sandbox
|
|
50
|
+
* egress. The only egress control available at this layer is the sandbox's
|
|
51
|
+
* own `HarnessV1NetworkPolicy` (`{mode:'allow-all'|'deny-all'} | {mode:
|
|
52
|
+
* 'custom', allowedHosts/allowedCIDRs/deniedCIDRs}`), applied via the
|
|
53
|
+
* sandbox session's optional `setNetworkPolicy?.()`. That is coarse
|
|
54
|
+
* host-level filtering, not Intutic DLP — stated plainly in
|
|
55
|
+
* `apps/docs/integrations/ai-sdk-harness.md` rather than oversold.
|
|
56
|
+
*
|
|
57
|
+
* ## What this module does NOT import
|
|
58
|
+
*
|
|
59
|
+
* Deliberately no runtime dependency on `@ai-sdk/harness` — the structural
|
|
60
|
+
* types below capture only the shapes this adapter touches, matching the
|
|
61
|
+
* convention `vercel.ts`/`mastra.ts`/`dsh.ts` established. The real package
|
|
62
|
+
* is a devDependency used by `__tests__/harness.test.ts` to (a) structurally
|
|
63
|
+
* check these types against the shipped ones and (b) run this responder's
|
|
64
|
+
* output through the REAL `collectHarnessAgentToolApprovalContinuations`
|
|
65
|
+
* machinery.
|
|
66
|
+
*
|
|
67
|
+
* @module
|
|
68
|
+
*/
|
|
69
|
+
import { createHash } from 'node:crypto';
|
|
70
|
+
import { active as activeGate } from './gate.js';
|
|
71
|
+
import { IntuticGateRefusal } from './errors.js';
|
|
72
|
+
/**
|
|
73
|
+
* Render a pending approval's `input` as the `tool_input` object
|
|
74
|
+
* {@link Gate.guard} evaluates.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors `dsh.ts`'s `renderDshToolInput`, plus one harness-specific case: a
|
|
77
|
+
* `HarnessAgentPendingToolApproval` carries its input as a JSON **string**,
|
|
78
|
+
* so strings are parsed first. A string that does not parse (or parses to a
|
|
79
|
+
* non-object) falls back to `{ args: [...] }` so the gate still has
|
|
80
|
+
* something to evaluate rather than throwing on a shape it does not
|
|
81
|
+
* recognise.
|
|
82
|
+
*/
|
|
83
|
+
export function renderHarnessToolInput(input) {
|
|
84
|
+
let value = input;
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
try {
|
|
87
|
+
value = JSON.parse(value);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { args: [input] };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
return { args: [value] };
|
|
97
|
+
}
|
|
98
|
+
/** Parsed-if-possible view of a request's input, used on the continuation's
|
|
99
|
+
* `toolCall.input` so the shape matches what the tool call actually carried
|
|
100
|
+
* (a pending-approval row serializes it to a JSON string in transit). */
|
|
101
|
+
function parsedInput(input) {
|
|
102
|
+
if (typeof input !== 'string')
|
|
103
|
+
return input;
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(input);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return input;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build an approval responder: a function that evaluates each pending
|
|
113
|
+
* tool-approval request via `Gate.guard()` and produces the
|
|
114
|
+
* `toolApprovalContinuations` array for the continue-turn call —
|
|
115
|
+
* `agent.continueGenerate({ session, toolApprovalContinuations })` /
|
|
116
|
+
* `continueStream(...)`:
|
|
117
|
+
*
|
|
118
|
+
* ```ts
|
|
119
|
+
* import { HarnessAgent } from '@ai-sdk/harness/agent'
|
|
120
|
+
* import { Gate, install } from '@intutic/gate'
|
|
121
|
+
* import {
|
|
122
|
+
* intuticApprovalResponder,
|
|
123
|
+
* intuticStaticApprovals,
|
|
124
|
+
* recommendedHarnessSettings,
|
|
125
|
+
* } from '@intutic/gate/harness'
|
|
126
|
+
*
|
|
127
|
+
* install(new Gate({ workspaceId: process.env.INTUTIC_WORKSPACE_ID }))
|
|
128
|
+
*
|
|
129
|
+
* const agent = new HarnessAgent({
|
|
130
|
+
* harness,
|
|
131
|
+
* sandbox,
|
|
132
|
+
* tools,
|
|
133
|
+
* ...recommendedHarnessSettings(), // permissionMode: 'allow-edits'
|
|
134
|
+
* toolApproval: intuticStaticApprovals(tools), // every custom tool -> 'user-approval'
|
|
135
|
+
* })
|
|
136
|
+
*
|
|
137
|
+
* const respond = intuticApprovalResponder()
|
|
138
|
+
* // ...run a turn; when it pauses awaiting approval, collect the pending
|
|
139
|
+
* // tool-approval requests (stream parts or suspended-session rows), then:
|
|
140
|
+
* const toolApprovalContinuations = await respond(pendingApprovals)
|
|
141
|
+
* await agent.continueGenerate({ session, toolApprovalContinuations })
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* Decision mapping, per request:
|
|
145
|
+
*
|
|
146
|
+
* - gate allows -> `approved: true`
|
|
147
|
+
* - `IntuticGateRefusal` -> `approved: false`, `reason` = the
|
|
148
|
+
* `[Intutic Governance] BLOCKED: ...` message (the runtime submits an
|
|
149
|
+
* `execution-denied` result to the model in place of a tool result)
|
|
150
|
+
* - any OTHER throw -> `approved: false` with a "gate crashed"
|
|
151
|
+
* reason — **fail closed**, same posture as `dsh.ts`'s listener: this
|
|
152
|
+
* responder's output is a decision list consumed by a paused turn, and a
|
|
153
|
+
* deny is deliverable and auditable where an escaped throw could leave
|
|
154
|
+
* the turn suspended with no answer at all.
|
|
155
|
+
*
|
|
156
|
+
* Requires a `Gate` — either passed via `{ gate }`, or installed process-wide
|
|
157
|
+
* with `install(new Gate(...))`. A call with neither rejects before
|
|
158
|
+
* evaluating anything, refusing to answer approvals unguarded.
|
|
159
|
+
*/
|
|
160
|
+
export function intuticApprovalResponder(opts = {}) {
|
|
161
|
+
return async (requests) => {
|
|
162
|
+
const g = opts.gate ?? activeGate();
|
|
163
|
+
if (g === null) {
|
|
164
|
+
throw new Error('No gate configured: call install(new Gate(...)) from @intutic/gate, or pass ' +
|
|
165
|
+
'{ gate } to intuticApprovalResponder(). Refusing to answer tool approvals unguarded.');
|
|
166
|
+
}
|
|
167
|
+
const continuations = [];
|
|
168
|
+
for (const req of requests) {
|
|
169
|
+
let approved = true;
|
|
170
|
+
let reason;
|
|
171
|
+
try {
|
|
172
|
+
await g.guard(req.toolName, renderHarnessToolInput(req.input));
|
|
173
|
+
}
|
|
174
|
+
catch (exc) {
|
|
175
|
+
approved = false;
|
|
176
|
+
if (exc instanceof IntuticGateRefusal) {
|
|
177
|
+
reason = exc.message;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const detail = exc instanceof Error ? exc.message : String(exc);
|
|
181
|
+
reason =
|
|
182
|
+
`[Intutic Governance] BLOCKED: gate crashed (${detail}) — failing closed rather ` +
|
|
183
|
+
'than approving an unevaluated call.';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
continuations.push({
|
|
187
|
+
approvalResponse: {
|
|
188
|
+
type: 'tool-approval-response',
|
|
189
|
+
approvalId: req.approvalId,
|
|
190
|
+
approved,
|
|
191
|
+
...(reason !== undefined ? { reason } : {}),
|
|
192
|
+
...(req.providerExecuted !== undefined ? { providerExecuted: req.providerExecuted } : {}),
|
|
193
|
+
},
|
|
194
|
+
toolCall: {
|
|
195
|
+
type: 'tool-call',
|
|
196
|
+
toolCallId: req.toolCallId,
|
|
197
|
+
toolName: req.toolName,
|
|
198
|
+
input: parsedInput(req.input),
|
|
199
|
+
...(req.providerExecuted !== undefined ? { providerExecuted: req.providerExecuted } : {}),
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return continuations;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Evaluate pending approvals and deliver the decisions through
|
|
208
|
+
* `session.submitToolApproval` when the adapter supports it.
|
|
209
|
+
*
|
|
210
|
+
* `submitToolApproval` is an OPTIONAL adapter-session method and per-adapter
|
|
211
|
+
* support varies (`harness-claude-code` implements it, `harness-grok-build`
|
|
212
|
+
* does not — see module doc point 2). This helper treats it as an
|
|
213
|
+
* optimization: when present, every decision is submitted directly (the turn
|
|
214
|
+
* resumes in place); when absent, nothing is submitted and the caller passes
|
|
215
|
+
* the returned `continuations` to `continueGenerate`/`continueStream` — the
|
|
216
|
+
* portable path — instead. Either way the SAME evaluated decisions are
|
|
217
|
+
* returned, so the caller never re-runs the gate for one delivery mechanism
|
|
218
|
+
* vs. the other.
|
|
219
|
+
*/
|
|
220
|
+
export async function intuticSubmitApprovals(session, requests, opts = {}) {
|
|
221
|
+
const continuations = await intuticApprovalResponder(opts)(requests);
|
|
222
|
+
const submit = session.submitToolApproval?.bind(session);
|
|
223
|
+
if (submit === undefined) {
|
|
224
|
+
return { continuations, submitted: false };
|
|
225
|
+
}
|
|
226
|
+
for (const c of continuations) {
|
|
227
|
+
await submit({
|
|
228
|
+
approvalId: c.approvalResponse.approvalId,
|
|
229
|
+
approved: c.approvalResponse.approved,
|
|
230
|
+
...(c.approvalResponse.reason !== undefined ? { reason: c.approvalResponse.reason } : {}),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return { continuations, submitted: true };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Build the static `toolApproval` record for `HarnessAgentSettings`.
|
|
237
|
+
*
|
|
238
|
+
* The record shape is `Readonly<Record<string, ToolApprovalStatus>>` — STATIC
|
|
239
|
+
* per-tool statuses "without callback support" (the shipped doc comment's own
|
|
240
|
+
* words), so a per-call gate function cannot be expressed here at all. What a
|
|
241
|
+
* static record CAN express is deny-by-default **routing**: every listed tool
|
|
242
|
+
* is marked `'user-approval'`, which pauses the turn on every call to it, and
|
|
243
|
+
* {@link intuticApprovalResponder} then answers each pause with a real
|
|
244
|
+
* per-call `Gate.guard()` verdict. `'denied'` is deliberately NOT the default
|
|
245
|
+
* here — it would refuse every call to the tool unconditionally, without the
|
|
246
|
+
* gate ever seeing the arguments; use `{ deny: [...] }` for tools where that
|
|
247
|
+
* is actually what you want.
|
|
248
|
+
*
|
|
249
|
+
* Two scope limits, stated plainly:
|
|
250
|
+
*
|
|
251
|
+
* - This covers CUSTOM host-executed tools only. Built-in sandbox tools
|
|
252
|
+
* (read/write/edit/bash/...) never consult `toolApproval` — they are
|
|
253
|
+
* governed solely by `permissionMode`, which defaults to `'allow-all'`.
|
|
254
|
+
* See {@link recommendedHarnessSettings}.
|
|
255
|
+
* - A tool absent from this record has status `undefined` =
|
|
256
|
+
* "not-applicable" and runs WITHOUT any approval pause. Pass the same
|
|
257
|
+
* `tools` record you give the agent (or every tool name), not a subset.
|
|
258
|
+
*/
|
|
259
|
+
export function intuticStaticApprovals(tools, opts = {}) {
|
|
260
|
+
const names = Array.isArray(tools) ? tools : Object.keys(tools);
|
|
261
|
+
const denied = new Set(opts.deny ?? []);
|
|
262
|
+
const record = {};
|
|
263
|
+
for (const name of names) {
|
|
264
|
+
record[name] = denied.has(name) ? 'denied' : 'user-approval';
|
|
265
|
+
}
|
|
266
|
+
return record;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The settings posture this integration recommends, as a value callers can
|
|
270
|
+
* spread/apply rather than prose they have to transcribe:
|
|
271
|
+
*
|
|
272
|
+
* - `permissionMode: 'allow-edits'` (or `'allow-reads'`) — because the
|
|
273
|
+
* framework's own default is `'allow-all'`, under which built-in sandbox
|
|
274
|
+
* tools (including `bash`) run with no approval surface this gate can
|
|
275
|
+
* reach. There is no per-call gate for built-ins at ANY permissionMode;
|
|
276
|
+
* the mode itself is the entire control, which is why steering it off
|
|
277
|
+
* `'allow-all'` matters.
|
|
278
|
+
* - a `networkPolicy` for the sandbox — `{ mode: 'deny-all' }` by default,
|
|
279
|
+
* or a `custom` allow-list (with the cloud-metadata CIDR denied) when
|
|
280
|
+
* `allowedHosts` is given. This is the honest egress-governance story for
|
|
281
|
+
* server-side sandbox execution: the Intutic proxy on the developer's
|
|
282
|
+
* machine never sees sandbox traffic, and host-level filtering is what
|
|
283
|
+
* the sandbox layer actually offers. It is coarse — no DLP, no
|
|
284
|
+
* per-request policy — and the integration docs say so.
|
|
285
|
+
*/
|
|
286
|
+
export function recommendedHarnessSettings(opts = {}) {
|
|
287
|
+
const networkPolicy = opts.allowedHosts !== undefined && opts.allowedHosts.length > 0
|
|
288
|
+
? {
|
|
289
|
+
mode: 'custom',
|
|
290
|
+
allowedHosts: opts.allowedHosts,
|
|
291
|
+
// Cloud metadata endpoint — deny it even when the caller's allow
|
|
292
|
+
// list is broad; deniedCIDRs wins over allows in the real policy.
|
|
293
|
+
deniedCIDRs: ['169.254.169.254/32'],
|
|
294
|
+
}
|
|
295
|
+
: { mode: 'deny-all' };
|
|
296
|
+
return {
|
|
297
|
+
permissionMode: opts.permissionMode ?? 'allow-edits',
|
|
298
|
+
networkPolicy,
|
|
299
|
+
...(opts.filterBash === false ? {} : { inactiveTools: ['bash'] }),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function sandboxJoin(workDir, relative) {
|
|
303
|
+
const base = workDir.endsWith('/') ? workDir.slice(0, -1) : workDir;
|
|
304
|
+
return `${base}/${relative}`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Self-contained (zero-`require`) hand-port of `snapshot.ts`'s
|
|
308
|
+
* `normalise`/`evaluate` — Tier A1 only. See the module-level comment above
|
|
309
|
+
* for what this deliberately does and does not cover, and
|
|
310
|
+
* `__tests__/harness.test.ts` for the behavioural-parity check against the
|
|
311
|
+
* real `snapshot.evaluate()` this port is pinned to.
|
|
312
|
+
*
|
|
313
|
+
* Contract matches the laptop's `claude-code-check.js`
|
|
314
|
+
* (`services/sync-daemon/src/harness/claudeCodeHooks.ts`) deliberately:
|
|
315
|
+
* Claude Code's PreToolUse hook stdin carries `{tool_name, tool_input,
|
|
316
|
+
* session_id}` (this reads both the snake_case and camelCase field names,
|
|
317
|
+
* same as the laptop script); exit `2` blocks the call, exit `0` allows it;
|
|
318
|
+
* any parse/read error fails CLOSED (exit `2`), the same posture
|
|
319
|
+
* `emitJsFailClosedPrelude` documents for the laptop's version.
|
|
320
|
+
*/
|
|
321
|
+
function renderSandboxGateScript(rulesFileName) {
|
|
322
|
+
return `#!/usr/bin/env node
|
|
323
|
+
'use strict';
|
|
324
|
+
/**
|
|
325
|
+
* Intutic sandbox PreToolUse gate (Tier A1 only — policy-snapshot rules).
|
|
326
|
+
* Auto-generated by @intutic/gate's intuticSandboxBootstrap(). DO NOT EDIT.
|
|
327
|
+
*
|
|
328
|
+
* Unlike the laptop's claude-code-check.js, this script has no live
|
|
329
|
+
* control-plane connection at call time (the recommended sandbox network
|
|
330
|
+
* policy is deny-all) and therefore implements NEITHER the SOP tier (A3)
|
|
331
|
+
* NOR review-hold parking NOR governance-event draining. See TD-417.
|
|
332
|
+
*/
|
|
333
|
+
const fs = require('fs');
|
|
334
|
+
const path = require('path');
|
|
335
|
+
|
|
336
|
+
const RULES_PATH = path.join(__dirname, ${JSON.stringify(rulesFileName)});
|
|
337
|
+
|
|
338
|
+
function normalise(v) {
|
|
339
|
+
const s = v === null || v === undefined ? '' : String(v);
|
|
340
|
+
return ' ' + s.replace(/\\s+/g, ' ').trim() + ' ';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function loadRules() {
|
|
344
|
+
let text;
|
|
345
|
+
try {
|
|
346
|
+
text = fs.readFileSync(RULES_PATH, 'utf-8');
|
|
347
|
+
} catch (e) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
const rules = [];
|
|
351
|
+
for (const line of text.split('\\n')) {
|
|
352
|
+
if (!line || line.charAt(0) === '#') continue;
|
|
353
|
+
const f = line.split('\\t');
|
|
354
|
+
if (f.length < 6 || !f[5]) continue;
|
|
355
|
+
try {
|
|
356
|
+
rules.push({
|
|
357
|
+
id: f[0],
|
|
358
|
+
severity: f[1],
|
|
359
|
+
subject: f[3] || 'any',
|
|
360
|
+
reason: f[4],
|
|
361
|
+
pattern: new RegExp(f[5], f[2] === 'i' ? 'i' : ''),
|
|
362
|
+
});
|
|
363
|
+
} catch (e) {
|
|
364
|
+
// Regex would not compile — dropped, not fatal. Matches snapshot.ts.
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return rules;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function evaluate(toolName, target, command, rules) {
|
|
371
|
+
const nTool = normalise(toolName);
|
|
372
|
+
const nCommand = normalise(command);
|
|
373
|
+
const nTarget = normalise(target);
|
|
374
|
+
for (const rule of rules) {
|
|
375
|
+
const subjects =
|
|
376
|
+
rule.subject === 'tool' ? [nTool] :
|
|
377
|
+
rule.subject === 'command' ? [nCommand] :
|
|
378
|
+
rule.subject === 'target' ? [nTarget] :
|
|
379
|
+
[nCommand, nTarget];
|
|
380
|
+
for (const subject of subjects) {
|
|
381
|
+
if (!rule.pattern.test(subject)) continue;
|
|
382
|
+
return { severity: rule.severity, reason: rule.reason + ' [' + rule.id + ']' };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let inputData = '';
|
|
389
|
+
process.stdin.on('data', (chunk) => { inputData += chunk; });
|
|
390
|
+
process.stdin.on('end', () => {
|
|
391
|
+
try {
|
|
392
|
+
const ctx = JSON.parse(inputData || '{}');
|
|
393
|
+
const toolName = ctx.tool_name || ctx.toolName || '';
|
|
394
|
+
const toolInput = ctx.tool_input || ctx.toolInput || {};
|
|
395
|
+
const target = toolInput.path || toolInput.file_path || toolInput.filePath ||
|
|
396
|
+
toolInput.new_path || toolInput.target || toolInput.notebook_path || '';
|
|
397
|
+
const command = String(toolInput.command || toolInput.cmd || toolInput.script || '');
|
|
398
|
+
const rules = loadRules();
|
|
399
|
+
const decision = evaluate(toolName, target, command, rules);
|
|
400
|
+
if (decision && decision.severity === 'block') {
|
|
401
|
+
console.error('[Intutic Guardrail] BLOCKED: ' + decision.reason);
|
|
402
|
+
process.exit(2);
|
|
403
|
+
}
|
|
404
|
+
if (decision && (decision.severity === 'warn' || decision.severity === 'shadow')) {
|
|
405
|
+
console.error('[Intutic Guardrail] FLAGGED (' + decision.severity + '): ' + decision.reason);
|
|
406
|
+
}
|
|
407
|
+
process.exit(0);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
// Fail CLOSED — any hook execution error blocks the tool call, the same
|
|
410
|
+
// posture emitJsFailClosedPrelude documents for the laptop script.
|
|
411
|
+
console.error('[Intutic Guardrail] Hook error (blocking for safety):', err && err.message ? err.message : err);
|
|
412
|
+
process.exit(2);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Compiles the `.claude/settings.json` content that registers the generated
|
|
419
|
+
* hook script — same matcher set and hook shape as the laptop writer's
|
|
420
|
+
* `hookEntry()` (`services/sync-daemon/src/harness/claudeCodeHooks.ts`):
|
|
421
|
+
* `Bash`, `Edit`, `Write`, `MultiEdit`, and `mcp__.*`.
|
|
422
|
+
*/
|
|
423
|
+
function renderSandboxClaudeSettings(hookScriptSandboxPath) {
|
|
424
|
+
const hookEntry = (matcher) => ({
|
|
425
|
+
matcher,
|
|
426
|
+
hooks: [
|
|
427
|
+
{
|
|
428
|
+
type: 'command',
|
|
429
|
+
command: `node ${hookScriptSandboxPath}`,
|
|
430
|
+
timeout: 10,
|
|
431
|
+
statusMessage: 'Verifying tool execution against Intutic SOP policy...',
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
const settings = {
|
|
436
|
+
permissions: { deny: [] },
|
|
437
|
+
hooks: {
|
|
438
|
+
PreToolUse: [hookEntry('Bash'), hookEntry('Edit'), hookEntry('Write'), hookEntry('MultiEdit'), hookEntry('mcp__.*')],
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
return JSON.stringify(settings, null, 2) + '\n';
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Builds `{ bootstrapHash, onBootstrap }` for
|
|
445
|
+
* `HarnessAgentSettings.sandboxConfig` — the channel TD-417 identified as
|
|
446
|
+
* existing but unused:
|
|
447
|
+
*
|
|
448
|
+
* ```ts
|
|
449
|
+
* import { HarnessAgent } from '@ai-sdk/harness/agent'
|
|
450
|
+
* import { intuticSandboxBootstrap, recommendedHarnessSettings } from '@intutic/gate/harness'
|
|
451
|
+
*
|
|
452
|
+
* const agent = new HarnessAgent({
|
|
453
|
+
* harness,
|
|
454
|
+
* sandbox,
|
|
455
|
+
* ...recommendedHarnessSettings(),
|
|
456
|
+
* sandboxConfig: intuticSandboxBootstrap({
|
|
457
|
+
* policySnapshotRules: fs.readFileSync(snapshotPath(), 'utf-8'),
|
|
458
|
+
* workspaceId: process.env.INTUTIC_WORKSPACE_ID,
|
|
459
|
+
* }),
|
|
460
|
+
* })
|
|
461
|
+
* ```
|
|
462
|
+
*
|
|
463
|
+
* Writes two files under `<workDir>/<bootstrapDir>` (default
|
|
464
|
+
* `.intutic/hooks`) — the rules text verbatim, and a self-contained hook
|
|
465
|
+
* script implementing Tier A1 against it (see
|
|
466
|
+
* {@link renderSandboxGateScript}) — plus `.claude/settings.json` at
|
|
467
|
+
* `<workDir>/.claude/settings.json` registering that script as a
|
|
468
|
+
* `PreToolUse` hook. See the module-level comment above this section for
|
|
469
|
+
* what this does and does not cover, and why.
|
|
470
|
+
*/
|
|
471
|
+
export function intuticSandboxBootstrap(opts = {}) {
|
|
472
|
+
const bootstrapDir = opts.bootstrapDir ?? '.intutic/hooks';
|
|
473
|
+
const rulesRelPath = `${bootstrapDir}/policy-snapshot.rules`;
|
|
474
|
+
const scriptRelPath = `${bootstrapDir}/claude-code-check.js`;
|
|
475
|
+
const rulesContent = opts.policySnapshotRules ?? '';
|
|
476
|
+
const scriptContent = renderSandboxGateScript('policy-snapshot.rules');
|
|
477
|
+
// Same hashing convention `snapshot.ts` already uses for the policy
|
|
478
|
+
// digest (`createHash('sha256')` from node:crypto) — not a new mechanism.
|
|
479
|
+
const bootstrapHash = createHash('sha256')
|
|
480
|
+
.update(`workspace:${opts.workspaceId ?? ''} `)
|
|
481
|
+
.update(`${rulesRelPath}\x00${rulesContent}\x00${scriptRelPath}\x00${scriptContent}`)
|
|
482
|
+
.digest('hex');
|
|
483
|
+
return {
|
|
484
|
+
bootstrapHash,
|
|
485
|
+
onBootstrap: async ({ session, workDir }) => {
|
|
486
|
+
// Absolute paths throughout, matching the laptop writer's own
|
|
487
|
+
// convention (claudeCodeHooks.ts's hookEntry() always joins an
|
|
488
|
+
// absolute workspaceRoot) - a PreToolUse hook's invocation cwd is not
|
|
489
|
+
// guaranteed to be workDir, so a relative command would be fragile.
|
|
490
|
+
const scriptAbsPath = sandboxJoin(workDir, scriptRelPath);
|
|
491
|
+
const files = [
|
|
492
|
+
{ path: sandboxJoin(workDir, rulesRelPath), content: rulesContent },
|
|
493
|
+
{ path: scriptAbsPath, content: scriptContent },
|
|
494
|
+
{ path: sandboxJoin(workDir, '.claude/settings.json'), content: renderSandboxClaudeSettings(scriptAbsPath) },
|
|
495
|
+
];
|
|
496
|
+
for (const f of files) {
|
|
497
|
+
await session.writeTextFile({ path: f.path, content: f.content });
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
// Exposed for the behavioural-parity test — not part of the public surface.
|
|
503
|
+
export const _internal = { renderSandboxGateScript, renderSandboxClaudeSettings, sandboxJoin };
|
|
504
|
+
//# sourceMappingURL=harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of `intutic_headers()` from
|
|
3
|
+
* `packages/intutic-clawde/intutic_clawde/gate/framework.py`.
|
|
4
|
+
*/
|
|
5
|
+
export interface IntuticHeadersOptions {
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
workspaceId?: string;
|
|
8
|
+
/** Attributes traces to this adapter. Defaults to `"generic"`. */
|
|
9
|
+
harness?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Default headers for a client pointed at the Intutic proxy (an OpenAI-
|
|
13
|
+
* compatible chat client, a raw `fetch` call, etc.).
|
|
14
|
+
*
|
|
15
|
+
* `x-session-id` matters: the proxy defaults an unset one to the literal
|
|
16
|
+
* `"unknown"`, which merges every run into a single dashboard session.
|
|
17
|
+
* `x-intutic-harness` attributes traces to this adapter — the proxy honours
|
|
18
|
+
* it for trace attribution (unknown headers are ignored by older proxies, so
|
|
19
|
+
* it is always safe to send).
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
*
|
|
23
|
+
* const client = new OpenAI({ baseURL: proxyUrl, defaultHeaders: intuticHeaders({ sessionId: runId }) })
|
|
24
|
+
*/
|
|
25
|
+
export declare function intuticHeaders(opts?: IntuticHeadersOptions): Record<string, string>;
|
|
26
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWvF"}
|
package/dist/headers.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of `intutic_headers()` from
|
|
3
|
+
* `packages/intutic-clawde/intutic_clawde/gate/framework.py`.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Default headers for a client pointed at the Intutic proxy (an OpenAI-
|
|
7
|
+
* compatible chat client, a raw `fetch` call, etc.).
|
|
8
|
+
*
|
|
9
|
+
* `x-session-id` matters: the proxy defaults an unset one to the literal
|
|
10
|
+
* `"unknown"`, which merges every run into a single dashboard session.
|
|
11
|
+
* `x-intutic-harness` attributes traces to this adapter — the proxy honours
|
|
12
|
+
* it for trace attribution (unknown headers are ignored by older proxies, so
|
|
13
|
+
* it is always safe to send).
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
*
|
|
17
|
+
* const client = new OpenAI({ baseURL: proxyUrl, defaultHeaders: intuticHeaders({ sessionId: runId }) })
|
|
18
|
+
*/
|
|
19
|
+
export function intuticHeaders(opts = {}) {
|
|
20
|
+
const harness = opts.harness ?? 'generic';
|
|
21
|
+
const headers = { 'x-intutic-harness': harness };
|
|
22
|
+
const sess = opts.sessionId || process.env.INTUTIC_SESSION_ID || '';
|
|
23
|
+
if (sess)
|
|
24
|
+
headers['x-session-id'] = sess;
|
|
25
|
+
const ws = opts.workspaceId || process.env.INTUTIC_WORKSPACE_ID || '';
|
|
26
|
+
if (ws)
|
|
27
|
+
headers['x-workspace-id'] = ws;
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../src/headers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,OAA8B,EAAE;IAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,SAAS,CAAA;IACzC,MAAM,OAAO,GAA2B,EAAE,mBAAmB,EAAE,OAAO,EAAE,CAAA;IAExE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAA;IACnE,IAAI,IAAI;QAAE,OAAO,CAAC,cAAc,CAAC,GAAG,IAAI,CAAA;IAExC,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,EAAE,CAAA;IACrE,IAAI,EAAE;QAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,EAAE,CAAA;IAEtC,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Container image integrity checking.
|
|
3
|
+
*
|
|
4
|
+
* Port of `packages/intutic-clawde/intutic_clawde/gate/imagecheck.py`.
|
|
5
|
+
*
|
|
6
|
+
* Given a deploy command, find the manifests it would apply, extract every
|
|
7
|
+
* container image reference, and check each against an allowlist of approved
|
|
8
|
+
* sha256 digests. Nothing else in Intutic does this — no cosign, no sigstore,
|
|
9
|
+
* no admission webhook — so this check is the argument-level enforcement
|
|
10
|
+
* point for "deploy only pinned, reviewed images".
|
|
11
|
+
*
|
|
12
|
+
* Fail-closed on anything it cannot read. That is a deliberate departure from
|
|
13
|
+
* Intutic's fail-open default (WASM rules, hook-gate, the MCP proxy all fail
|
|
14
|
+
* open). The reasoning: a gate that cannot parse what it is being asked to
|
|
15
|
+
* approve does not know what it is approving, and "I could not read it" is
|
|
16
|
+
* not evidence of safety.
|
|
17
|
+
*/
|
|
18
|
+
export declare const E_UNPINNED_LATEST = "E_UNPINNED_LATEST";
|
|
19
|
+
export declare const E_UNPINNED_TAG = "E_UNPINNED_TAG";
|
|
20
|
+
export declare const E_UNKNOWN_REGISTRY = "E_UNKNOWN_REGISTRY";
|
|
21
|
+
export declare const E_UNKNOWN_IMAGE = "E_UNKNOWN_IMAGE";
|
|
22
|
+
export declare const E_DIGEST_MISMATCH = "E_DIGEST_MISMATCH";
|
|
23
|
+
export declare const E_MANIFEST_UNPARSEABLE = "E_MANIFEST_UNPARSEABLE";
|
|
24
|
+
/** A parsed container image reference. */
|
|
25
|
+
export interface ImageRef {
|
|
26
|
+
raw: string;
|
|
27
|
+
/** registry/namespace/name, no tag, no digest */
|
|
28
|
+
repository: string;
|
|
29
|
+
tag: string | null;
|
|
30
|
+
/** `sha256:...` or null */
|
|
31
|
+
digest: string | null;
|
|
32
|
+
/** which manifest file it came from */
|
|
33
|
+
source: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function isPinned(img: ImageRef): boolean;
|
|
36
|
+
export interface ImagePolicy {
|
|
37
|
+
require_digest?: boolean;
|
|
38
|
+
registries_allowed?: string[];
|
|
39
|
+
images?: Record<string, {
|
|
40
|
+
approved_digests?: string[];
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export interface Verdict {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
code: string;
|
|
46
|
+
detail: string;
|
|
47
|
+
images: ImageRef[];
|
|
48
|
+
}
|
|
49
|
+
/** The single line an operator sees on the dashboard. */
|
|
50
|
+
export declare function verdictReason(v: Verdict): string;
|
|
51
|
+
/**
|
|
52
|
+
* Parse `registry/repo:tag@sha256:...` into its parts.
|
|
53
|
+
*
|
|
54
|
+
* Docker's own rule for splitting registry from repository: the first path
|
|
55
|
+
* component is a registry host only if it contains a dot or a colon, or is
|
|
56
|
+
* exactly `localhost`. This function does not need to make that
|
|
57
|
+
* classification itself — it only needs to find the tag/digest boundary,
|
|
58
|
+
* which is a colon (or `@`) in the LAST path component; a colon earlier
|
|
59
|
+
* (a registry port, `localhost:5000/foo`) is left alone.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseImageRef(raw: string, source?: string): ImageRef;
|
|
62
|
+
/**
|
|
63
|
+
* Extract the -f/-k targets from a kubectl/helm command.
|
|
64
|
+
*
|
|
65
|
+
* Returns `{paths, readable}`. `readable` is false when the command applies
|
|
66
|
+
* something we cannot inspect — stdin (`-f -`), a URL, or a command we could
|
|
67
|
+
* not tokenise. That drives the fail-closed path.
|
|
68
|
+
*/
|
|
69
|
+
export declare function manifestPathsFromCommand(command: string): {
|
|
70
|
+
paths: string[];
|
|
71
|
+
readable: boolean;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Catch images named directly on the command line.
|
|
75
|
+
*
|
|
76
|
+
* `kubectl set image deploy/catalogue catalogue=repo:latest` never touches a
|
|
77
|
+
* manifest, so the YAML path would miss it entirely.
|
|
78
|
+
*/
|
|
79
|
+
export declare function inlineImagesFromCommand(command: string): ImageRef[];
|
|
80
|
+
/** Evaluate parsed references against the allowlist. First failure wins. */
|
|
81
|
+
export declare function checkImages(images: readonly ImageRef[], policy: ImagePolicy): Verdict;
|
|
82
|
+
/** Full check for a deploy command. The entry point the gate calls. */
|
|
83
|
+
export declare function checkCommand(command: string, repoRoot: string, policy: ImagePolicy): Verdict;
|
|
84
|
+
/**
|
|
85
|
+
* Check a manifest at authoring time, before it is ever applied.
|
|
86
|
+
*
|
|
87
|
+
* Lets the dashboard show the bad image being *written* a full turn before it
|
|
88
|
+
* is *applied* — the write is flagged, the apply is blocked.
|
|
89
|
+
*/
|
|
90
|
+
export declare function checkWrittenManifest(path: string, content: string, policy: ImagePolicy): Verdict;
|
|
91
|
+
/** True when this call should be image-checked. */
|
|
92
|
+
export declare function isDeployCommand(toolName: string, toolInput: unknown): boolean;
|
|
93
|
+
//# sourceMappingURL=imagecheck.d.ts.map
|