@dungle-scrubs/harness-cli-normalizer 0.4.5 → 0.5.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/README.md +53 -7
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +13 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +65 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/help.d.ts +2 -2
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +28 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +81 -25
- package/dist/cli/inspect.js.map +1 -1
- package/dist/cli/provenance.d.ts +3 -0
- package/dist/cli/provenance.d.ts.map +1 -0
- package/dist/cli/provenance.js +11 -0
- package/dist/cli/provenance.js.map +1 -0
- package/dist/cli/refuse.d.ts +14 -0
- package/dist/cli/refuse.d.ts.map +1 -0
- package/dist/cli/refuse.js +38 -0
- package/dist/cli/refuse.js.map +1 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +27 -68
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/session.d.ts.map +1 -1
- package/dist/cli/session.js +3 -3
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/skills-root.d.ts +2 -1
- package/dist/cli/skills-root.d.ts.map +1 -1
- package/dist/cli/skills-root.js +6 -4
- package/dist/cli/skills-root.js.map +1 -1
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +2 -1
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +1 -1
- package/dist/execution/deps.js +1 -1
- package/dist/execution/failure.d.ts +5 -1
- package/dist/execution/failure.d.ts.map +1 -1
- package/dist/execution/failure.js +12 -0
- package/dist/execution/failure.js.map +1 -1
- package/dist/execution/index.d.ts +2 -2
- package/dist/execution/index.js +2 -2
- package/dist/execution/open-session.d.ts.map +1 -1
- package/dist/execution/open-session.js +23 -8
- package/dist/execution/open-session.js.map +1 -1
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +18 -10
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +3 -0
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +6 -1
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/content.d.ts.map +1 -1
- package/dist/interpretation/content.js +4 -1
- package/dist/interpretation/content.js.map +1 -1
- package/dist/interpretation/hints.d.ts.map +1 -1
- package/dist/interpretation/hints.js +4 -0
- package/dist/interpretation/hints.js.map +1 -1
- package/dist/interpretation/limits.d.ts +1 -0
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +22 -0
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/resolve-options.d.ts.map +1 -1
- package/dist/interpretation/resolve-options.js +124 -3
- package/dist/interpretation/resolve-options.js.map +1 -1
- package/dist/interpretation/support.d.ts +1 -0
- package/dist/interpretation/support.d.ts.map +1 -1
- package/dist/interpretation/support.js +29 -0
- package/dist/interpretation/support.js.map +1 -1
- package/dist/interpretation/tool-selection.d.ts +6 -16
- package/dist/interpretation/tool-selection.d.ts.map +1 -1
- package/dist/interpretation/tool-selection.js +216 -80
- package/dist/interpretation/tool-selection.js.map +1 -1
- package/dist/interpretation/tool-vocabulary.d.ts +38 -0
- package/dist/interpretation/tool-vocabulary.d.ts.map +1 -0
- package/dist/interpretation/tool-vocabulary.js +142 -0
- package/dist/interpretation/tool-vocabulary.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -1
- package/dist/interpretation/turn-options.js +107 -2
- package/dist/interpretation/turn-options.js.map +1 -1
- package/dist/interpretation/versions.d.ts +1 -1
- package/dist/interpretation/versions.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.d.ts +1 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +5 -0
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +14 -13
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +10 -4
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +20 -1
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +18 -1
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/index.d.ts +1 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +1 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +2 -1
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +15 -0
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +18 -3
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +3 -2
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +12 -8
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +13 -0
- package/src/cli/config.ts +88 -0
- package/src/cli/help.ts +28 -7
- package/src/cli/inspect.ts +78 -21
- package/src/cli/provenance.ts +19 -0
- package/src/cli/refuse.ts +50 -0
- package/src/cli/run.ts +27 -94
- package/src/cli/session.ts +4 -3
- package/src/cli/skills-root.ts +11 -4
- package/src/execution/decode.ts +2 -1
- package/src/execution/deps.ts +1 -1
- package/src/execution/failure.ts +17 -1
- package/src/execution/index.ts +2 -2
- package/src/execution/open-session.ts +33 -7
- package/src/execution/stream-turn.ts +21 -9
- package/src/interpretation/argv.ts +9 -1
- package/src/interpretation/content.ts +5 -1
- package/src/interpretation/hints.ts +8 -0
- package/src/interpretation/limits.ts +29 -0
- package/src/interpretation/resolve-options.ts +129 -3
- package/src/interpretation/support.ts +30 -0
- package/src/interpretation/tool-selection.ts +236 -96
- package/src/interpretation/tool-vocabulary.ts +183 -0
- package/src/interpretation/turn-options.ts +99 -2
- package/src/interpretation/versions.ts +1 -1
- package/src/interpretation/vocabulary.ts +5 -0
- package/src/knowledge/claude-code.ts +14 -13
- package/src/knowledge/codex.ts +10 -4
- package/src/knowledge/descriptor.ts +42 -2
- package/src/knowledge/index.ts +5 -1
- package/src/knowledge/matchers.ts +17 -1
- package/src/knowledge/muse.ts +18 -3
- package/src/knowledge/overrides.ts +4 -2
- package/src/knowledge/pi.ts +12 -8
|
@@ -23,7 +23,9 @@ import type {
|
|
|
23
23
|
HarnessDescriptor,
|
|
24
24
|
LimitCode,
|
|
25
25
|
LimitMatcher,
|
|
26
|
+
TransportMatcher,
|
|
26
27
|
} from "../knowledge/descriptor.js";
|
|
28
|
+
import { SHARED_TRANSPORT_MATCHERS } from "../knowledge/matchers.js";
|
|
27
29
|
|
|
28
30
|
/** Bottom-up batch scans stop after this many non-empty lines: the wall is
|
|
29
31
|
* virtually always the last thing a dying turn printed, and an unbounded
|
|
@@ -152,3 +154,30 @@ export const detectAuthFailureInLine = (
|
|
|
152
154
|
|
|
153
155
|
export const detectAuthFailure = (h: HarnessDescriptor, output: string): AuthFailureKind | null =>
|
|
154
156
|
scanTail(output, compileAuthMatchers(h.authMatchers));
|
|
157
|
+
|
|
158
|
+
const transportCache = new WeakMap<
|
|
159
|
+
ReadonlyArray<TransportMatcher>,
|
|
160
|
+
ReadonlyArray<readonly [RegExp, boolean]>
|
|
161
|
+
>();
|
|
162
|
+
|
|
163
|
+
const compileTransportMatchers = (
|
|
164
|
+
matchers: ReadonlyArray<TransportMatcher>,
|
|
165
|
+
): ReadonlyArray<readonly [RegExp, boolean]> => {
|
|
166
|
+
const cached = transportCache.get(matchers);
|
|
167
|
+
if (cached !== undefined) return cached;
|
|
168
|
+
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
169
|
+
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
170
|
+
}
|
|
171
|
+
const compiled = matchers.map((m) => [validateAndCompile(m.pattern, m.flags), true] as const);
|
|
172
|
+
transportCache.set(matchers, compiled);
|
|
173
|
+
return compiled;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export const detectTransportInLine = (line: string): boolean => {
|
|
177
|
+
const compiled = compileTransportMatchers(SHARED_TRANSPORT_MATCHERS);
|
|
178
|
+
const windowed = line.slice(0, WINDOW).trim();
|
|
179
|
+
for (const [re] of compiled) {
|
|
180
|
+
if (re.test(windowed)) return true;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
};
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
* sandbox default already follows).
|
|
9
9
|
*/
|
|
10
10
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
11
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
11
12
|
import { DEFAULT_TURN_PROFILE, type ProfileKey } from "../knowledge/profile.js";
|
|
12
13
|
import type { TurnOptions } from "./argv.js";
|
|
14
|
+
import { ArgvRefusalError } from "./refusal.js";
|
|
15
|
+
import type { ToolMap } from "./tool-vocabulary.js";
|
|
16
|
+
import { allCanonicalNames, mergeToolMaps, validateCanonicalList } from "./tool-vocabulary.js";
|
|
17
|
+
import { validateAccess } from "./vocabulary.js";
|
|
13
18
|
|
|
14
19
|
export type ProvenanceTier = "arg" | "project-config" | "user-config" | "profile" | "harness";
|
|
15
20
|
|
|
@@ -126,8 +131,100 @@ export const resolveEffectiveOptions = (
|
|
|
126
131
|
}
|
|
127
132
|
const resolved: Record<string, unknown> = { ...effectiveArgs };
|
|
128
133
|
|
|
129
|
-
//
|
|
130
|
-
|
|
134
|
+
// Validate access value before exclusivity so invalid reports invalid-option-value, not mutual exclusion.
|
|
135
|
+
if (resolved.access !== undefined) {
|
|
136
|
+
const v = validateAccess(String(resolved.access));
|
|
137
|
+
if (!v.ok) {
|
|
138
|
+
throw new ArgvRefusalError({
|
|
139
|
+
issue: "invalid-option-value",
|
|
140
|
+
harness: h.name,
|
|
141
|
+
option: "access",
|
|
142
|
+
supported: ["read", "write"],
|
|
143
|
+
detail: String(resolved.access),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Access exclusivity on codex: explicit --sandbox together with --access refuses.
|
|
148
|
+
// Profile sandbox yields to access - only explicit sandbox counts.
|
|
149
|
+
if (h.name === "codex" && resolved.access !== undefined) {
|
|
150
|
+
const hasExplicitSandbox =
|
|
151
|
+
effectiveArgs.sandbox !== undefined || sourceTier("sandbox") !== undefined;
|
|
152
|
+
if (hasExplicitSandbox) {
|
|
153
|
+
throw new ArgvRefusalError({
|
|
154
|
+
issue: "mutually-exclusive-options",
|
|
155
|
+
harness: h.name,
|
|
156
|
+
option: "access",
|
|
157
|
+
supported: ["--access or --sandbox, not both on codex"],
|
|
158
|
+
detail: "mutual exclusion",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Access vs tools exclusivity: only explicit tools/excludeTools from args or config, never profile-derived.
|
|
163
|
+
if (resolved.access !== undefined) {
|
|
164
|
+
const explicitTools = effectiveArgs.tools !== undefined || sourceTier("tools") !== undefined;
|
|
165
|
+
const explicitExclude =
|
|
166
|
+
(effectiveArgs as unknown as Record<string, unknown>).excludeTools !== undefined ||
|
|
167
|
+
sourceTier("excludeTools") !== undefined;
|
|
168
|
+
if (explicitTools || explicitExclude) {
|
|
169
|
+
throw new ArgvRefusalError({
|
|
170
|
+
issue: "mutually-exclusive-options",
|
|
171
|
+
harness: h.name,
|
|
172
|
+
option: "access",
|
|
173
|
+
supported: ["--access is a preset allowlist, not a filter over --tools/--exclude-tools"],
|
|
174
|
+
detail: "mutual exclusion",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// toolMap merge per harness per canonical (project > user)
|
|
180
|
+
const rawToolMapUser = (tiers.user as { toolMap?: ToolMap } | undefined)?.toolMap;
|
|
181
|
+
const rawToolMapProject = (tiers.project as { toolMap?: ToolMap } | undefined)?.toolMap;
|
|
182
|
+
const mergedToolMap = mergeToolMaps({ user: rawToolMapUser, project: rawToolMapProject });
|
|
183
|
+
if (Object.keys(mergedToolMap).length > 0) {
|
|
184
|
+
// Convert mergedToolMap to legacy shape for resolved.toolMap consumers
|
|
185
|
+
const legacy: Record<string, Record<string, string>> = {};
|
|
186
|
+
for (const [harness, per] of Object.entries(mergedToolMap)) {
|
|
187
|
+
legacy[harness] = {};
|
|
188
|
+
for (const [canon, entry] of Object.entries(per as Record<string, { native: string }>)) {
|
|
189
|
+
legacy[harness]![canon] = entry.native;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
resolved.toolMap = legacy as unknown as typeof resolved.toolMap;
|
|
193
|
+
const harnessMap = mergedToolMap[h.name];
|
|
194
|
+
if (harnessMap) {
|
|
195
|
+
for (const [canonical, entry] of Object.entries(harnessMap)) {
|
|
196
|
+
provenance.push({
|
|
197
|
+
key: `tools.${canonical}`,
|
|
198
|
+
value: entry.native,
|
|
199
|
+
tier: entry.tier as ProvenanceTier,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Lazy allCanonical build only when tools context present
|
|
206
|
+
const needsCanonical =
|
|
207
|
+
tiers.project?.tools !== undefined ||
|
|
208
|
+
tiers.user?.tools !== undefined ||
|
|
209
|
+
effectiveArgs.tools !== undefined ||
|
|
210
|
+
(effectiveArgs as unknown as Record<string, unknown>).excludeTools !== undefined ||
|
|
211
|
+
Object.keys(toolsets).length > 0 ||
|
|
212
|
+
tiers.project?.toolMap !== undefined ||
|
|
213
|
+
tiers.user?.toolMap !== undefined;
|
|
214
|
+
let allCanonical: readonly string[] | undefined;
|
|
215
|
+
const getAllCanonical = (): readonly string[] => {
|
|
216
|
+
if (allCanonical) return allCanonical;
|
|
217
|
+
allCanonical = allCanonicalNames(defaultDescriptors(), mergedToolMap as unknown as ToolMap);
|
|
218
|
+
return allCanonical;
|
|
219
|
+
};
|
|
220
|
+
if (needsCanonical) {
|
|
221
|
+
const ac = getAllCanonical();
|
|
222
|
+
validateCanonicalList(tiers.project?.tools as readonly string[] | undefined, ac, h.name);
|
|
223
|
+
validateCanonicalList(tiers.user?.tools as readonly string[] | undefined, ac, h.name);
|
|
224
|
+
for (const set of Object.values(toolsets)) {
|
|
225
|
+
validateCanonicalList(set as readonly string[], ac, h.name);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
131
228
|
const floor = tiers.project?.tools;
|
|
132
229
|
if (floor !== undefined && effectiveArgs.tools !== undefined) {
|
|
133
230
|
const floorSet = new Set(floor);
|
|
@@ -158,6 +255,19 @@ export const resolveEffectiveOptions = (
|
|
|
158
255
|
provenance.push({ key, value, tier: "harness" });
|
|
159
256
|
continue;
|
|
160
257
|
}
|
|
258
|
+
// When access is set, skip the all-known expansion (access is a preset allowlist, not a filter).
|
|
259
|
+
// Same shape as --no-tools skip; provenance owned by tier that set access.
|
|
260
|
+
if (key === "tools" && value === "all-known" && resolved.access !== undefined) {
|
|
261
|
+
const accessTier: ProvenanceTier =
|
|
262
|
+
effectiveArgs.access !== undefined ? "arg" : (sourceTier("access") ?? "user-config");
|
|
263
|
+
provenance.push({ key, value: "none (access preset)", tier: accessTier });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// Profile sandbox yields to access - when access is set, drop profile sandbox.
|
|
267
|
+
if (key === "sandbox" && resolved.access !== undefined) {
|
|
268
|
+
provenance.push({ key, value: `${String(value)} (access)`, tier: "harness" });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
161
271
|
// D13: the tools marker expands per descriptor. On a harness whose
|
|
162
272
|
// default is already everything (claude), expansion emits nothing -
|
|
163
273
|
// the emit-nothing rule, recorded in provenance. On a harness with
|
|
@@ -186,7 +296,9 @@ export const resolveEffectiveOptions = (
|
|
|
186
296
|
provenance.push({ key, value: "all known (already default)", tier: "profile" });
|
|
187
297
|
continue;
|
|
188
298
|
}
|
|
189
|
-
const expanded = h.tools.builtins
|
|
299
|
+
const expanded = h.tools.builtins
|
|
300
|
+
.filter((t) => t.canonical !== null)
|
|
301
|
+
.map((t) => t.canonical as string);
|
|
190
302
|
resolved[key] = expanded;
|
|
191
303
|
provenance.push({ key, value: expanded, tier: "profile" });
|
|
192
304
|
continue;
|
|
@@ -213,6 +325,7 @@ export const resolveEffectiveOptions = (
|
|
|
213
325
|
// (validated later by the same renderers as args).
|
|
214
326
|
for (const [key, value] of Object.entries(config)) {
|
|
215
327
|
if (key === "toolsets") continue; // expanded into args above, never a turn option
|
|
328
|
+
if (key === "toolMap") continue; // per-canonical provenance already emitted
|
|
216
329
|
if (key in DEFAULT_TURN_PROFILE) continue;
|
|
217
330
|
if (effectiveArgs[key as keyof TurnOptions] !== undefined) {
|
|
218
331
|
provenance.push({ key, value: effectiveArgs[key as keyof TurnOptions], tier: "arg" });
|
|
@@ -223,6 +336,19 @@ export const resolveEffectiveOptions = (
|
|
|
223
336
|
provenance.push({ key, value, tier });
|
|
224
337
|
}
|
|
225
338
|
|
|
339
|
+
// Access divergence / fixup
|
|
340
|
+
if (resolved.access !== undefined && h.turnOptions.access === undefined) {
|
|
341
|
+
unrenderable.push("access");
|
|
342
|
+
for (let i = provenance.length - 1; i >= 0; i--)
|
|
343
|
+
if (provenance[i]?.key === "access") provenance.splice(i, 1);
|
|
344
|
+
provenance.push({ key: "access", value: resolved.access as string, tier: "harness" });
|
|
345
|
+
delete (resolved as Record<string, unknown>).access;
|
|
346
|
+
} else if (resolved.access !== undefined && !provenance.some((p) => p.key === "access")) {
|
|
347
|
+
const tier: ProvenanceTier =
|
|
348
|
+
effectiveArgs.access !== undefined ? "arg" : (sourceTier("access") ?? "user-config");
|
|
349
|
+
provenance.push({ key: "access", value: resolved.access as string, tier });
|
|
350
|
+
}
|
|
351
|
+
|
|
226
352
|
return {
|
|
227
353
|
options: resolved as unknown as TurnOptions,
|
|
228
354
|
provenance,
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
15
15
|
import type { DescriptorSet } from "../knowledge/overrides.js";
|
|
16
16
|
import type { RefusalOption } from "./refusal.js";
|
|
17
|
+
import type { VocabularyEntry } from "./tool-vocabulary.js";
|
|
18
|
+
import { canonicalTable } from "./tool-vocabulary.js";
|
|
17
19
|
|
|
18
20
|
export interface SupportEntry {
|
|
19
21
|
readonly harness: string;
|
|
@@ -23,6 +25,15 @@ export interface SupportEntry {
|
|
|
23
25
|
|
|
24
26
|
const spellingOf = (h: HarnessDescriptor, option: RefusalOption): string | null => {
|
|
25
27
|
switch (option) {
|
|
28
|
+
case "access": {
|
|
29
|
+
const spec = h.turnOptions.access;
|
|
30
|
+
if (spec === undefined) return null;
|
|
31
|
+
if (spec.kind === "flag-value") return spec.flag;
|
|
32
|
+
if (spec.kind === "flag-list-by-value") return Object.values(spec.flags)[0]?.[0] ?? null;
|
|
33
|
+
if (spec.kind === "tool-preset")
|
|
34
|
+
return h.tools.includeFlag ?? h.tools.excludeFlag ?? "--sandbox";
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
26
37
|
case "tools":
|
|
27
38
|
return h.tools.includeFlag;
|
|
28
39
|
case "excludeTools":
|
|
@@ -69,6 +80,25 @@ export const supportedBy = (set: DescriptorSet, option: RefusalOption): readonly
|
|
|
69
80
|
return out;
|
|
70
81
|
};
|
|
71
82
|
|
|
83
|
+
export const supportedByCanonical = (
|
|
84
|
+
set: DescriptorSet,
|
|
85
|
+
canonical: string,
|
|
86
|
+
): readonly SupportEntry[] => {
|
|
87
|
+
const table = canonicalTable(set);
|
|
88
|
+
const entry = table[canonical];
|
|
89
|
+
if (!entry) return [];
|
|
90
|
+
const out: SupportEntry[] = [];
|
|
91
|
+
for (const [har, val] of Object.entries(entry)) {
|
|
92
|
+
if (val === undefined) continue;
|
|
93
|
+
const spelling =
|
|
94
|
+
(val as VocabularyEntry).kind === "builtin"
|
|
95
|
+
? (val as { kind: "builtin"; native: string }).native
|
|
96
|
+
: (val as { kind: "category"; key: string }).key;
|
|
97
|
+
out.push({ harness: har, spelling });
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
|
|
72
102
|
/** Reverse lookup for native-spelling recognition (D7 part B): given a raw
|
|
73
103
|
* flag token a caller typed, find the option it belongs to and which
|
|
74
104
|
* harnesses spell it that way. Returns null for tokens no descriptor
|
|
@@ -1,73 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tool-selection rendering
|
|
3
|
-
* per-harness argv tokens, per decisions D1-D3 and the Phase 0 evidence.
|
|
4
|
-
*
|
|
5
|
-
* Semantics:
|
|
6
|
-
* - include: exact allowlist over the descriptor's curated names. pi's flag
|
|
7
|
-
* is strict over built-ins, rendered directly. claude's include flag only
|
|
8
|
-
* pre-approves without restricting the visible set, so an exact allowlist
|
|
9
|
-
* on claude renders as a deny-complement (all known minus included).
|
|
10
|
-
* - exclude: the complement of the named tools over all descriptor-known
|
|
11
|
-
* names (D2). pi's native exclude subtracts from its default set (4
|
|
12
|
-
* tools), so a D2 exclude renders as a computed include list there.
|
|
13
|
-
* - mutual exclusion (D1): both flags in one call refuse.
|
|
14
|
-
* - extensible rule (D3): curated names validate and map; unknown
|
|
15
|
-
* clean-selector names pass through and are reported as unmapped so
|
|
16
|
-
* provenance can surface them.
|
|
2
|
+
* Tool-selection rendering with canonical vocabulary.
|
|
17
3
|
*/
|
|
18
4
|
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
19
5
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
6
|
+
import { hintFor } from "./hints.js";
|
|
20
7
|
import { ArgvRefusalError } from "./refusal.js";
|
|
21
|
-
import { supportedBy } from "./support.js";
|
|
8
|
+
import { supportedBy, supportedByCanonical } from "./support.js";
|
|
9
|
+
import {
|
|
10
|
+
allCanonicalNames,
|
|
11
|
+
canonicalTable,
|
|
12
|
+
hasCounterpart,
|
|
13
|
+
NATIVE_PREFIX,
|
|
14
|
+
nativeFor,
|
|
15
|
+
parseToolSelector,
|
|
16
|
+
READ_PRESET,
|
|
17
|
+
validateCanonicalList,
|
|
18
|
+
} from "./tool-vocabulary.js";
|
|
22
19
|
|
|
23
20
|
export const TOOL_SELECTOR = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/;
|
|
24
21
|
|
|
25
22
|
export interface ToolSelection {
|
|
26
23
|
readonly include?: readonly string[];
|
|
27
24
|
readonly exclude?: readonly string[];
|
|
25
|
+
readonly toolMap?: Readonly<Record<string, string>>;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
export interface RenderedToolSelection {
|
|
31
29
|
readonly tokens: readonly string[];
|
|
32
|
-
readonly
|
|
30
|
+
readonly passthrough: readonly string[];
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
33
|
+
const validateInnerName = (h: HarnessDescriptor, name: string, raw: readonly string[]): void => {
|
|
34
|
+
if (name.trim() === "" || name.includes(",")) {
|
|
35
|
+
throw new ArgvRefusalError({
|
|
36
|
+
issue: "invalid-tool-grant",
|
|
37
|
+
harness: h.name,
|
|
38
|
+
supported: ["non-empty, comma-free tool names"],
|
|
39
|
+
detail: `tools=${JSON.stringify(raw)}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!TOOL_SELECTOR.test(name)) {
|
|
43
|
+
throw new ArgvRefusalError({
|
|
44
|
+
issue: "invalid-tool-grant",
|
|
45
|
+
harness: h.name,
|
|
46
|
+
supported: [`must match ${TOOL_SELECTOR.source}`],
|
|
47
|
+
detail: name,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const categoriesFor = (
|
|
53
|
+
h: HarnessDescriptor,
|
|
54
|
+
canonicals: readonly string[],
|
|
55
|
+
table: ReturnType<typeof canonicalTable>,
|
|
56
|
+
): Set<string> => {
|
|
57
|
+
const cats = new Set<string>();
|
|
58
|
+
for (const c of canonicals) {
|
|
59
|
+
const val = table[c]?.[h.name];
|
|
60
|
+
if (val?.kind === "category") cats.add(val.key);
|
|
53
61
|
}
|
|
54
|
-
return
|
|
62
|
+
return cats;
|
|
55
63
|
};
|
|
56
64
|
|
|
57
|
-
|
|
58
|
-
* names and which of them the descriptor could not vouch for. */
|
|
59
|
-
const resolveNames = (
|
|
65
|
+
export const renderAccessPreset = (
|
|
60
66
|
h: HarnessDescriptor,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
67
|
+
access: "read" | "write",
|
|
68
|
+
opts: { toolMap?: Readonly<Record<string, string>> } = {},
|
|
69
|
+
): string[] => {
|
|
70
|
+
if (access === "write") return [];
|
|
71
|
+
// read preset
|
|
72
|
+
const filtered = (READ_PRESET as readonly string[]).filter((c) => {
|
|
73
|
+
if (opts.toolMap?.[c] !== undefined) return true;
|
|
74
|
+
const table = canonicalTable(defaultDescriptors());
|
|
75
|
+
return table[c]?.[h.name] !== undefined;
|
|
76
|
+
});
|
|
77
|
+
if (filtered.length === 0) return [];
|
|
78
|
+
// For tool-preset harnesses, render via tool-selection include
|
|
79
|
+
const spec = h.turnOptions.access;
|
|
80
|
+
if (spec && spec.kind === "tool-preset") {
|
|
81
|
+
const rendered = renderToolSelection(h, { include: filtered, toolMap: opts.toolMap });
|
|
82
|
+
return [...rendered.tokens];
|
|
69
83
|
}
|
|
70
|
-
|
|
84
|
+
// For flag-value / flag-list-by-value harnesses, caller should handle via spec mapping;
|
|
85
|
+
// we return empty here so turn-options can map via spec values.
|
|
86
|
+
return [];
|
|
71
87
|
};
|
|
72
88
|
|
|
73
89
|
export const renderToolSelection = (
|
|
@@ -77,7 +93,6 @@ export const renderToolSelection = (
|
|
|
77
93
|
const hasInclude = selection.include !== undefined;
|
|
78
94
|
const hasExclude = selection.exclude !== undefined;
|
|
79
95
|
|
|
80
|
-
// D1: mutual exclusivity, structured refusal.
|
|
81
96
|
if (hasInclude && hasExclude) {
|
|
82
97
|
throw new ArgvRefusalError({
|
|
83
98
|
issue: "mutually-exclusive-options",
|
|
@@ -87,84 +102,209 @@ export const renderToolSelection = (
|
|
|
87
102
|
detail: "mutual exclusion",
|
|
88
103
|
});
|
|
89
104
|
}
|
|
90
|
-
if (!hasInclude && !hasExclude) return { tokens: [],
|
|
105
|
+
if (!hasInclude && !hasExclude) return { tokens: [], passthrough: [] };
|
|
106
|
+
|
|
107
|
+
const set = defaultDescriptors();
|
|
108
|
+
const table = canonicalTable(set);
|
|
109
|
+
const toolMap = selection.toolMap ?? {};
|
|
110
|
+
const allCanonical = allCanonicalNames(set, {
|
|
111
|
+
[h.name]: toolMap as unknown as Record<string, string>,
|
|
112
|
+
} as unknown as import("./tool-vocabulary.js").ToolMap);
|
|
113
|
+
|
|
114
|
+
const splitSelection = (
|
|
115
|
+
names: readonly string[],
|
|
116
|
+
): { canonical: string[]; passthrough: string[] } => {
|
|
117
|
+
const canonical: string[] = [];
|
|
118
|
+
const passthrough: string[] = [];
|
|
119
|
+
const setForValidate = new Set(allCanonical);
|
|
120
|
+
for (const raw of names) {
|
|
121
|
+
const parsed = parseToolSelector(raw);
|
|
122
|
+
if (parsed.kind === "native") {
|
|
123
|
+
validateInnerName(h, parsed.name, names);
|
|
124
|
+
passthrough.push(parsed.name);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
validateInnerName(h, parsed.name, names);
|
|
128
|
+
if (!setForValidate.has(parsed.name)) {
|
|
129
|
+
throw new ArgvRefusalError({
|
|
130
|
+
issue: "unknown-tool-name",
|
|
131
|
+
harness: h.name,
|
|
132
|
+
option: "tools",
|
|
133
|
+
supported: [...allCanonical],
|
|
134
|
+
hint: "use native:<name> for an extension or MCP tool",
|
|
135
|
+
detail: `unknown tool name(s) ${parsed.name}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
canonical.push(parsed.name);
|
|
139
|
+
}
|
|
140
|
+
return { canonical, passthrough };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const isCodexShape = h.tools.includeFlag === null && h.tools.categories.length === 0;
|
|
144
|
+
const isCategoryShape = h.tools.includeFlag === null && h.tools.categories.length > 0;
|
|
91
145
|
|
|
92
|
-
if (
|
|
93
|
-
const option = hasInclude ? "tools" : "excludeTools";
|
|
94
|
-
const by = supportedBy(defaultDescriptors(), option);
|
|
146
|
+
if (isCodexShape) {
|
|
95
147
|
throw new ArgvRefusalError({
|
|
96
148
|
issue: "unsupported-option",
|
|
97
149
|
harness: h.name,
|
|
98
|
-
option,
|
|
150
|
+
option: hasInclude ? "tools" : "excludeTools",
|
|
99
151
|
supported: ["per-tool name lists"],
|
|
100
|
-
supportedBy:
|
|
152
|
+
supportedBy: supportedBy(set, hasInclude ? "tools" : "excludeTools"),
|
|
101
153
|
hint:
|
|
102
|
-
h.name
|
|
103
|
-
|
|
104
|
-
: "nearest control on muse: category switches (--disable-write, --disable-shell, --disable-web-tools) gate tool execution per session",
|
|
154
|
+
hintFor(h.name, hasInclude ? "tools" : "excludeTools") ??
|
|
155
|
+
"nearest control on codex: category switches via config keys (features.shell_tool, web_search) or sandbox modes - see `hcn inspect codex`",
|
|
105
156
|
});
|
|
106
157
|
}
|
|
158
|
+
if (isCategoryShape) {
|
|
159
|
+
const names = hasInclude ? selection.include! : selection.exclude!;
|
|
160
|
+
const { canonical, passthrough } = splitSelection(names);
|
|
107
161
|
|
|
162
|
+
if (hasExclude) {
|
|
163
|
+
// one helper replaces the three no-counterpart loops
|
|
164
|
+
for (const c of canonical) {
|
|
165
|
+
const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
|
|
166
|
+
if (!has) {
|
|
167
|
+
throw new ArgvRefusalError({
|
|
168
|
+
issue: "unsupported-option",
|
|
169
|
+
harness: h.name,
|
|
170
|
+
option: "excludeTools",
|
|
171
|
+
supported: ["per-tool name lists"],
|
|
172
|
+
supportedBy: supportedByCanonical(set, c),
|
|
173
|
+
hint: hintFor(h.name, "excludeTools"),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const cats = categoriesFor(h, canonical, table);
|
|
178
|
+
const tokens: string[] = [];
|
|
179
|
+
for (const cat of h.tools.categories) {
|
|
180
|
+
if (cats.has(cat.key) && cat.disableFlag) tokens.push(cat.disableFlag);
|
|
181
|
+
}
|
|
182
|
+
return { tokens, passthrough };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const c of canonical) {
|
|
186
|
+
const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
|
|
187
|
+
if (!has) {
|
|
188
|
+
throw new ArgvRefusalError({
|
|
189
|
+
issue: "unsupported-option",
|
|
190
|
+
harness: h.name,
|
|
191
|
+
option: "tools",
|
|
192
|
+
supported: ["per-tool name lists"],
|
|
193
|
+
supportedBy: supportedByCanonical(set, c),
|
|
194
|
+
hint: hintFor(h.name, "tools"),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (canonical.length === 0) {
|
|
199
|
+
const tokens = h.tools.categories.map((c) => c.disableFlag).filter((f): f is string => !!f);
|
|
200
|
+
return { tokens, passthrough };
|
|
201
|
+
}
|
|
202
|
+
const grantedCats = categoriesFor(h, canonical, table);
|
|
203
|
+
const tokens: string[] = [];
|
|
204
|
+
for (const cat of h.tools.categories) {
|
|
205
|
+
if (!grantedCats.has(cat.key) && cat.disableFlag) tokens.push(cat.disableFlag);
|
|
206
|
+
}
|
|
207
|
+
return { tokens, passthrough };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// From here: h has list flags (claude, pi)
|
|
108
211
|
if (hasInclude) {
|
|
109
|
-
const names =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (mapped.length === 0 && unmapped.length > 0) {
|
|
212
|
+
const names = selection.include!;
|
|
213
|
+
if (names.length === 0) {
|
|
214
|
+
if (h.tools.includeIsStrictAllowlist) {
|
|
113
215
|
throw new ArgvRefusalError({
|
|
114
|
-
issue: "
|
|
216
|
+
issue: "invalid-tool-grant",
|
|
115
217
|
harness: h.name,
|
|
116
218
|
option: "tools",
|
|
117
|
-
supported: [
|
|
118
|
-
detail:
|
|
219
|
+
supported: ["non-empty tool list (empty include would deny everything)"],
|
|
220
|
+
detail: "empty include list",
|
|
119
221
|
});
|
|
120
222
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
223
|
+
const known = h.tools.builtins.map((t) => t.name);
|
|
224
|
+
return { tokens: [h.tools.excludeFlag!, known.join(",")], passthrough: [] };
|
|
225
|
+
}
|
|
226
|
+
const { canonical, passthrough } = splitSelection(names);
|
|
227
|
+
for (const c of canonical) {
|
|
228
|
+
const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
|
|
229
|
+
if (!has) {
|
|
230
|
+
throw new ArgvRefusalError({
|
|
231
|
+
issue: "unsupported-option",
|
|
232
|
+
harness: h.name,
|
|
233
|
+
option: "tools",
|
|
234
|
+
supported: ["per-tool name lists"],
|
|
235
|
+
supportedBy: supportedByCanonical(set, c),
|
|
236
|
+
hint: `add toolMap.${h.name}.${c} to ~/.config/hcn/config.json or pass native:${c}`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const mapped: string[] = [];
|
|
241
|
+
for (const c of canonical) {
|
|
242
|
+
let n: string | null = toolMap[c] ?? null;
|
|
243
|
+
if (n === null) {
|
|
244
|
+
const entry = table[c]?.[h.name];
|
|
245
|
+
if (entry?.kind === "builtin") n = entry.native;
|
|
246
|
+
}
|
|
247
|
+
if (n) mapped.push(n);
|
|
248
|
+
}
|
|
249
|
+
if (!h.tools.includeIsStrictAllowlist) {
|
|
250
|
+
// claude's include flag pre-approves without restricting, so an exact allowlist renders as the deny complement
|
|
127
251
|
const known = h.tools.builtins.map((t) => t.name);
|
|
128
252
|
const excluded = known.filter((n) => !mapped.includes(n));
|
|
129
|
-
const tokens: string[] = [h.tools.includeFlag!, [...mapped, ...
|
|
130
|
-
|
|
131
|
-
|
|
253
|
+
const tokens: string[] = [h.tools.includeFlag!, [...mapped, ...passthrough].join(",")];
|
|
254
|
+
// only add exclude if needed? original pushed exclude even when? Keep original behavior: push excludeFlag excluded
|
|
255
|
+
if (excluded.length > 0 || passthrough.length > 0) {
|
|
256
|
+
tokens.push(h.tools.excludeFlag!, excluded.join(","));
|
|
257
|
+
} else {
|
|
258
|
+
// when all tools included, exclude nothing? but keep token? original pushed exclude always
|
|
259
|
+
tokens.push(h.tools.excludeFlag!, excluded.join(","));
|
|
260
|
+
}
|
|
261
|
+
return { tokens, passthrough };
|
|
132
262
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
263
|
+
return {
|
|
264
|
+
tokens: [h.tools.includeFlag!, [...mapped, ...passthrough].join(",")],
|
|
265
|
+
passthrough,
|
|
266
|
+
};
|
|
136
267
|
}
|
|
137
268
|
|
|
138
|
-
// exclude
|
|
139
|
-
const names =
|
|
140
|
-
const {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
269
|
+
// exclude
|
|
270
|
+
const names = selection.exclude!;
|
|
271
|
+
const { canonical, passthrough: excludedPassthrough } = splitSelection(names);
|
|
272
|
+
for (const c of canonical) {
|
|
273
|
+
const has = toolMap[c] !== undefined || table[c]?.[h.name] !== undefined;
|
|
274
|
+
if (!has) {
|
|
275
|
+
throw new ArgvRefusalError({
|
|
276
|
+
issue: "unsupported-option",
|
|
277
|
+
harness: h.name,
|
|
278
|
+
option: "excludeTools",
|
|
279
|
+
supported: ["per-tool name lists"],
|
|
280
|
+
supportedBy: supportedByCanonical(set, c),
|
|
281
|
+
hint: `add toolMap.${h.name}.${c} to ~/.config/hcn/config.json or pass native:${c}`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (excludedPassthrough.length > 0) {
|
|
146
286
|
if (!h.tools.includeIsStrictAllowlist) {
|
|
147
|
-
const
|
|
148
|
-
|
|
287
|
+
const mapped = canonical.map(
|
|
288
|
+
(c) => toolMap[c] ?? (table[c]?.[h.name] as { native: string } | undefined)?.native ?? c,
|
|
289
|
+
);
|
|
290
|
+
const tokens = [h.tools.excludeFlag!, [...mapped, ...excludedPassthrough].join(",")];
|
|
291
|
+
return { tokens, passthrough: excludedPassthrough };
|
|
149
292
|
}
|
|
150
|
-
// pi: a D2 exclude renders as a computed include (its native exclude
|
|
151
|
-
// subtracts from the default set, which cannot express "all minus X"
|
|
152
|
-
// when off-by-default tools are involved). An unknown excluded name
|
|
153
|
-
// cannot be complemented - refuse rather than silently grant it.
|
|
154
293
|
throw new ArgvRefusalError({
|
|
155
294
|
issue: "unknown-tool-name",
|
|
156
295
|
harness: h.name,
|
|
157
296
|
option: "excludeTools",
|
|
158
297
|
supported: [`known tool names: ${h.tools.builtins.map((t) => t.name).join(", ")}`],
|
|
159
|
-
detail: `cannot exclude unknown name(s) ${
|
|
298
|
+
detail: `cannot exclude unknown name(s) ${excludedPassthrough.join(", ")}: the complement cannot be computed`,
|
|
160
299
|
});
|
|
161
300
|
}
|
|
301
|
+
const mapped = canonical
|
|
302
|
+
.map((c) => toolMap[c] ?? (table[c]?.[h.name] as { native: string } | undefined)?.native ?? "")
|
|
303
|
+
.filter(Boolean);
|
|
162
304
|
const known = h.tools.builtins.filter((t) => !mapped.includes(t.name));
|
|
163
305
|
const kept = known.map((t) => t.name);
|
|
164
306
|
if (!h.tools.includeIsStrictAllowlist) {
|
|
165
|
-
|
|
166
|
-
return { tokens: [h.tools.excludeFlag!, mapped.join(",")], unmapped: [] };
|
|
307
|
+
return { tokens: [h.tools.excludeFlag!, mapped.join(",")], passthrough: [] };
|
|
167
308
|
}
|
|
168
|
-
|
|
169
|
-
return { tokens: [h.tools.includeFlag!, kept.join(",")], unmapped: [] };
|
|
309
|
+
return { tokens: [h.tools.includeFlag!, kept.join(",")], passthrough: [] };
|
|
170
310
|
};
|