@dungle-scrubs/harness-cli-normalizer 0.4.6 → 0.5.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/README.md +43 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -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 +25 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inspect.d.ts.map +1 -1
- package/dist/cli/inspect.js +63 -24
- package/dist/cli/inspect.js.map +1 -1
- 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/resume-guard.d.ts +18 -0
- package/dist/cli/resume-guard.d.ts.map +1 -0
- package/dist/cli/resume-guard.js +26 -0
- package/dist/cli/resume-guard.js.map +1 -0
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +16 -54
- package/dist/cli/run.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 +2 -0
- package/dist/interpretation/argv.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/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 +205 -85
- 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/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 +16 -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/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 +8 -7
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/args.ts +5 -0
- package/src/cli/config.ts +88 -0
- package/src/cli/help.ts +25 -7
- package/src/cli/inspect.ts +61 -20
- package/src/cli/refuse.ts +50 -0
- package/src/cli/resume-guard.ts +35 -0
- package/src/cli/run.ts +16 -71
- package/src/interpretation/argv.ts +5 -0
- package/src/interpretation/hints.ts +8 -0
- package/src/interpretation/resolve-options.ts +129 -3
- package/src/interpretation/support.ts +30 -0
- package/src/interpretation/tool-selection.ts +225 -101
- package/src/interpretation/tool-vocabulary.ts +183 -0
- package/src/interpretation/turn-options.ts +99 -2
- 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 +37 -2
- package/src/knowledge/muse.ts +18 -3
- package/src/knowledge/overrides.ts +4 -2
- package/src/knowledge/pi.ts +8 -7
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { failureFromRejected } from "../execution/failure.js";
|
|
2
|
+
import type { ArgvRefusalError, RefusalIssue } from "../interpretation/refusal.js";
|
|
3
|
+
import { writeEventNdjson } from "./render.js";
|
|
4
|
+
|
|
5
|
+
export interface Refusal {
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly issue: RefusalIssue;
|
|
8
|
+
readonly option?: ArgvRefusalError["option"];
|
|
9
|
+
readonly facet?: ArgvRefusalError["facet"];
|
|
10
|
+
readonly supported?: readonly string[];
|
|
11
|
+
readonly supportedBy?: ArgvRefusalError["supportedBy"];
|
|
12
|
+
readonly hint?: string;
|
|
13
|
+
readonly trailer?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const refusalOf = (err: ArgvRefusalError): Refusal => ({
|
|
17
|
+
message: err.message,
|
|
18
|
+
issue: err.issue,
|
|
19
|
+
option: err.option,
|
|
20
|
+
facet: err.facet,
|
|
21
|
+
supported: err.supported,
|
|
22
|
+
supportedBy: err.supportedBy,
|
|
23
|
+
hint: err.hint,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const refuse = (r: Refusal, json: boolean): void => {
|
|
27
|
+
process.stderr.write(`${r.message}\n`);
|
|
28
|
+
if (r.hint) process.stderr.write(`hint: ${r.hint}\n`);
|
|
29
|
+
if (r.supportedBy?.length) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
`supported on: ${r.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (r.supported?.length) process.stderr.write(`supported: ${r.supported.join(", ")}\n`);
|
|
35
|
+
for (const line of r.trailer ?? []) process.stderr.write(`${line}\n`);
|
|
36
|
+
if (json) {
|
|
37
|
+
const summary = failureFromRejected({
|
|
38
|
+
issue: r.issue,
|
|
39
|
+
option: r.option,
|
|
40
|
+
facet: r.facet,
|
|
41
|
+
supported: r.supported,
|
|
42
|
+
supportedBy: r.supportedBy,
|
|
43
|
+
hint: r.hint,
|
|
44
|
+
detail: r.message,
|
|
45
|
+
});
|
|
46
|
+
writeEventNdjson({ kind: "failure", ...summary });
|
|
47
|
+
writeEventNdjson({ kind: "done", exitCode: null, cause: "failed", failure: summary });
|
|
48
|
+
}
|
|
49
|
+
process.exitCode = 2;
|
|
50
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { storePath } from "../interpretation/store.js";
|
|
3
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
4
|
+
|
|
5
|
+
export interface ResumeStoreCheck {
|
|
6
|
+
/** The store path hcn expects for this cwd and id, or null when the
|
|
7
|
+
* descriptor cannot compute one. */
|
|
8
|
+
readonly path: string | null;
|
|
9
|
+
readonly exists: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Where a harness that creates sessions on an unknown id (pi, muse) would
|
|
13
|
+
* have filed this session. The cwd is resolved to its real path first: the
|
|
14
|
+
* harness slugs the directory it actually ran in, and on macOS a temp
|
|
15
|
+
* directory reached through /var is really under /private/var - slugging
|
|
16
|
+
* the unresolved path refused valid resumes. */
|
|
17
|
+
export const resumeStore = (
|
|
18
|
+
h: HarnessDescriptor,
|
|
19
|
+
opts: { readonly cwd: string; readonly home: string; readonly sessionId: string },
|
|
20
|
+
): ResumeStoreCheck => {
|
|
21
|
+
let cwd = opts.cwd;
|
|
22
|
+
try {
|
|
23
|
+
cwd = realpathSync.native(opts.cwd);
|
|
24
|
+
} catch {
|
|
25
|
+
// A cwd that does not exist yet resolves to itself; the harness will
|
|
26
|
+
// create it and slug whatever it created.
|
|
27
|
+
}
|
|
28
|
+
let path: string | null = null;
|
|
29
|
+
try {
|
|
30
|
+
path = storePath(h, { home: opts.home, cwd, sessionId: opts.sessionId });
|
|
31
|
+
} catch {
|
|
32
|
+
path = null;
|
|
33
|
+
}
|
|
34
|
+
return { path, exists: path !== null && existsSync(path) };
|
|
35
|
+
};
|
package/src/cli/run.ts
CHANGED
|
@@ -1,71 +1,17 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import type { HarnessEvent } from "../execution/events.js";
|
|
3
|
-
import { failureFromRejected } from "../execution/failure.js";
|
|
4
2
|
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
5
3
|
import { KILL_GRACE_MS, redactArgv, streamTurn } from "../execution/stream-turn.js";
|
|
6
4
|
import { buildLaunchArgv, buildResumeArgv } from "../interpretation/argv.js";
|
|
7
5
|
import { composeEscalatedPrompt } from "../interpretation/question.js";
|
|
8
|
-
import type { RefusalIssue } from "../interpretation/refusal.js";
|
|
9
6
|
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
10
7
|
import { FloorExceededError, resolveEffectiveOptions } from "../interpretation/resolve-options.js";
|
|
11
|
-
import { storePath } from "../interpretation/store.js";
|
|
12
8
|
import { recognizeNativeSpelling, supportedBy } from "../interpretation/support.js";
|
|
13
9
|
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
14
10
|
import { parseRunExtra, parseTurnOptions, resolvePromptAsync } from "./args.js";
|
|
11
|
+
import { refusalOf, refuse } from "./refuse.js";
|
|
15
12
|
import { createRenderState, renderEvent, writeEventNdjson } from "./render.js";
|
|
16
13
|
import { resolveHarness } from "./resolve-harness.js";
|
|
17
|
-
|
|
18
|
-
/** One shape for every hcn-side refusal. Stderr keeps the prose (message,
|
|
19
|
-
* hint, support lists, usage pointer); under --json the same fields also
|
|
20
|
-
* ride stdout as a `failure` event plus a `done`, so a stream reader never
|
|
21
|
-
* meets an empty stream on exit 2. */
|
|
22
|
-
interface Refusal {
|
|
23
|
-
readonly message: string;
|
|
24
|
-
readonly issue: RefusalIssue;
|
|
25
|
-
readonly option?: ArgvRefusalError["option"];
|
|
26
|
-
readonly facet?: ArgvRefusalError["facet"];
|
|
27
|
-
readonly supported?: readonly string[];
|
|
28
|
-
readonly supportedBy?: ArgvRefusalError["supportedBy"];
|
|
29
|
-
readonly hint?: string;
|
|
30
|
-
/** Plain stderr lines written after the structured ones. */
|
|
31
|
-
readonly trailer?: readonly string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const refusalOf = (err: ArgvRefusalError): Refusal => ({
|
|
35
|
-
message: err.message,
|
|
36
|
-
issue: err.issue,
|
|
37
|
-
option: err.option,
|
|
38
|
-
facet: err.facet,
|
|
39
|
-
supported: err.supported,
|
|
40
|
-
supportedBy: err.supportedBy,
|
|
41
|
-
hint: err.hint,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const refuse = (r: Refusal, json: boolean): void => {
|
|
45
|
-
process.stderr.write(`${r.message}\n`);
|
|
46
|
-
if (r.hint) process.stderr.write(`hint: ${r.hint}\n`);
|
|
47
|
-
if (r.supportedBy?.length) {
|
|
48
|
-
process.stderr.write(
|
|
49
|
-
`supported on: ${r.supportedBy.map((e) => `${e.harness} (${e.spelling})`).join(", ")}\n`,
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
if (r.supported?.length) process.stderr.write(`supported: ${r.supported.join(", ")}\n`);
|
|
53
|
-
for (const line of r.trailer ?? []) process.stderr.write(`${line}\n`);
|
|
54
|
-
if (json) {
|
|
55
|
-
const summary = failureFromRejected({
|
|
56
|
-
issue: r.issue,
|
|
57
|
-
option: r.option,
|
|
58
|
-
facet: r.facet,
|
|
59
|
-
supported: r.supported,
|
|
60
|
-
supportedBy: r.supportedBy,
|
|
61
|
-
hint: r.hint,
|
|
62
|
-
detail: r.message,
|
|
63
|
-
});
|
|
64
|
-
writeEventNdjson({ kind: "failure", ...summary });
|
|
65
|
-
writeEventNdjson({ kind: "done", exitCode: null, cause: "failed", failure: summary });
|
|
66
|
-
}
|
|
67
|
-
process.exitCode = 2;
|
|
68
|
-
};
|
|
14
|
+
import { resumeStore } from "./resume-guard.js";
|
|
69
15
|
|
|
70
16
|
export const run = async (harnessName: string, rawArgs: string[]): Promise<void> => {
|
|
71
17
|
const h = resolveHarness(harnessName);
|
|
@@ -295,6 +241,10 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
295
241
|
refuse({ message: resErr.message, issue: "invalid-tool-grant" }, wantJson);
|
|
296
242
|
return;
|
|
297
243
|
}
|
|
244
|
+
if (resErr instanceof ArgvRefusalError) {
|
|
245
|
+
refuse(refusalOf(resErr), wantJson);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
298
248
|
throw resErr;
|
|
299
249
|
}
|
|
300
250
|
const { provenance, unrenderable } = resolved;
|
|
@@ -408,12 +358,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
408
358
|
// all refuses inside buildLaunchArgv below, so a throw here is left
|
|
409
359
|
// to that path.
|
|
410
360
|
const grant = (fullOpts.resume ? turnOpts : effectiveTurnOpts).tools;
|
|
411
|
-
if (grant !== undefined && grant.length > 0
|
|
361
|
+
if (grant !== undefined && grant.length > 0) {
|
|
412
362
|
try {
|
|
413
363
|
const { renderToolSelection } = await import("../interpretation/tool-selection.js");
|
|
414
|
-
const {
|
|
415
|
-
if (
|
|
416
|
-
process.stderr.write(`provenance:
|
|
364
|
+
const { passthrough } = renderToolSelection(h, { include: [...grant] });
|
|
365
|
+
if (passthrough.length > 0) {
|
|
366
|
+
process.stderr.write(`provenance: native tools = ${JSON.stringify(passthrough)}\n`);
|
|
417
367
|
}
|
|
418
368
|
} catch {
|
|
419
369
|
// refused below with the structured message
|
|
@@ -426,17 +376,12 @@ export const run = async (harnessName: string, rawArgs: string[]): Promise<void>
|
|
|
426
376
|
// the session store path does not exist; where the path cannot be
|
|
427
377
|
// computed, the runner's pre-spawn warning is the only guard.
|
|
428
378
|
if (fullOpts.resume !== undefined && h.resume.onMissing === "create") {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
});
|
|
436
|
-
} catch {
|
|
437
|
-
path = null;
|
|
438
|
-
}
|
|
439
|
-
if (path !== null && !existsSync(path)) {
|
|
379
|
+
const { path, exists } = resumeStore(h, {
|
|
380
|
+
home: process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
381
|
+
cwd: extra.cwd ?? process.cwd(),
|
|
382
|
+
sessionId: fullOpts.resume,
|
|
383
|
+
});
|
|
384
|
+
if (path !== null && !exists) {
|
|
440
385
|
refuse(
|
|
441
386
|
{
|
|
442
387
|
message: `no ${h.name} session ${fullOpts.resume} found at ${path}`,
|
|
@@ -83,6 +83,9 @@ export interface TurnOptions {
|
|
|
83
83
|
readonly escalateQuestions?: boolean;
|
|
84
84
|
/** Internal: set by CLI when prompt came from --prompt/--prompt-file to bypass leading '-' guard */
|
|
85
85
|
readonly __explicitPrompt?: boolean;
|
|
86
|
+
/** toolMap extensible vocabulary per harness (issue toolMap) */
|
|
87
|
+
readonly toolMap?: Readonly<Record<string, Readonly<Record<string, string>>>>;
|
|
88
|
+
readonly access?: "read" | "write";
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export interface ResumeOptions extends TurnOptions {
|
|
@@ -124,9 +127,11 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
124
127
|
tail.push(h.autonomy.flag);
|
|
125
128
|
}
|
|
126
129
|
if (opts.tools !== undefined || opts.excludeTools !== undefined) {
|
|
130
|
+
const perHarnessMap = opts.toolMap?.[h.name];
|
|
127
131
|
const rendered = renderToolSelection(h, {
|
|
128
132
|
include: opts.tools,
|
|
129
133
|
exclude: opts.excludeTools,
|
|
134
|
+
toolMap: perHarnessMap,
|
|
130
135
|
});
|
|
131
136
|
tail.push(...rendered.tokens);
|
|
132
137
|
}
|
|
@@ -27,6 +27,10 @@ const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFr
|
|
|
27
27
|
"claude has no isolated instruction-file toggle; --setting-sources project isolates from user-level settings but also skips hooks, LSP and keychain reads - weigh that before using it as an approximation",
|
|
28
28
|
},
|
|
29
29
|
codex: {
|
|
30
|
+
tools:
|
|
31
|
+
"codex has no per-tool name lists; use --sandbox read-only for a read-only grant or category switches via config keys (features.shell_tool, web_search)",
|
|
32
|
+
excludeTools:
|
|
33
|
+
"codex has no per-tool name lists; use category switches via config keys (features.shell_tool) or --sandbox modes",
|
|
30
34
|
write:
|
|
31
35
|
"codex has no write toggle; use --sandbox read-only (config: sandbox_mode) so shell commands cannot write either",
|
|
32
36
|
// issue #48: the append half has no codex spelling.
|
|
@@ -58,6 +62,10 @@ const HINTS: Readonly<Record<string, Readonly<Record<string, string>>>> = deepFr
|
|
|
58
62
|
"pi has no step cap flag; bound the work in the prompt or impose a wall-clock timeout at the caller",
|
|
59
63
|
},
|
|
60
64
|
muse: {
|
|
65
|
+
tools:
|
|
66
|
+
"muse has no per-tool name lists; gate execution with --disable-write, --disable-shell, --disable-web-tools - there is no allowlist",
|
|
67
|
+
excludeTools:
|
|
68
|
+
"muse has no per-tool name lists; gate execution with --disable-write, --disable-shell, --disable-web-tools category switches",
|
|
61
69
|
sandbox:
|
|
62
70
|
"muse's sandbox is on by default and not selectable per-call; --disable-sandbox exists to turn it OFF, and exposure can be tuned with --disable-write, --disable-shell, --disable-web-tools - there is no mode selector",
|
|
63
71
|
provider:
|
|
@@ -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
|