@dungle-scrubs/harness-cli-normalizer 0.1.3 → 0.3.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 +144 -7
- package/dist/cli/args.d.ts +74 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +339 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +110 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +11 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help.d.ts +7 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +110 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +170 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/inspect.d.ts +3 -0
- package/dist/cli/inspect.d.ts.map +1 -0
- package/dist/cli/inspect.js +170 -0
- package/dist/cli/inspect.js.map +1 -0
- package/dist/cli/ls.d.ts +2 -0
- package/dist/cli/ls.d.ts.map +1 -0
- package/dist/cli/ls.js +10 -0
- package/dist/cli/ls.js.map +1 -0
- package/dist/cli/render.d.ts +8 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +55 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-harness.d.ts +4 -0
- package/dist/cli/resolve-harness.d.ts.map +1 -0
- package/dist/cli/resolve-harness.js +31 -0
- package/dist/cli/resolve-harness.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +280 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/session.d.ts +2 -0
- package/dist/cli/session.d.ts.map +1 -0
- package/dist/cli/session.js +128 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +34 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +32 -0
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +7 -1
- package/dist/execution/deps.d.ts.map +1 -1
- package/dist/execution/events.d.ts +6 -2
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +45 -0
- package/dist/execution/failure.d.ts.map +1 -0
- package/dist/execution/failure.js +121 -0
- package/dist/execution/failure.js.map +1 -0
- package/dist/execution/node-deps.d.ts.map +1 -1
- package/dist/execution/node-deps.js +19 -0
- package/dist/execution/node-deps.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +2 -0
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +181 -16
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +16 -4
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +68 -19
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/dimensions.d.ts +0 -2
- package/dist/interpretation/dimensions.d.ts.map +1 -1
- package/dist/interpretation/dimensions.js +0 -2
- package/dist/interpretation/dimensions.js.map +1 -1
- package/dist/interpretation/limits.d.ts +10 -1
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +84 -5
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +31 -0
- package/dist/interpretation/refusal.d.ts.map +1 -0
- package/dist/interpretation/refusal.js +87 -0
- package/dist/interpretation/refusal.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts +18 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -0
- package/dist/interpretation/turn-options.js +417 -0
- package/dist/interpretation/turn-options.js.map +1 -0
- package/dist/interpretation/vocabulary.d.ts +5 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +1 -1
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +13 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +35 -13
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +25 -14
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +118 -14
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +58 -0
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +3 -3
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +19 -6
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts +2 -2
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +22 -6
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts +7 -0
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +76 -0
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +25 -3
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +5 -2
- package/src/cli/args.ts +381 -0
- package/src/cli/check.ts +136 -0
- package/src/cli/exit-codes.ts +13 -0
- package/src/cli/help.ts +114 -0
- package/src/cli/index.ts +177 -0
- package/src/cli/inspect.ts +174 -0
- package/src/cli/ls.ts +14 -0
- package/src/cli/render.ts +62 -0
- package/src/cli/resolve-harness.ts +33 -0
- package/src/cli/run.ts +280 -0
- package/src/cli/session.ts +137 -0
- package/src/cli/version.ts +32 -0
- package/src/execution/decode.ts +32 -0
- package/src/execution/deps.ts +7 -1
- package/src/execution/events.ts +9 -2
- package/src/execution/failure.ts +159 -0
- package/src/execution/node-deps.ts +17 -0
- package/src/execution/stream-turn.ts +189 -17
- package/src/interpretation/argv.ts +85 -27
- package/src/interpretation/dimensions.ts +0 -6
- package/src/interpretation/limits.ts +97 -6
- package/src/interpretation/refusal.ts +116 -0
- package/src/interpretation/turn-options.ts +424 -0
- package/src/interpretation/vocabulary.ts +1 -1
- package/src/knowledge/claude-code.ts +35 -13
- package/src/knowledge/codex.ts +25 -14
- package/src/knowledge/descriptor.ts +138 -12
- package/src/knowledge/matchers.ts +22 -9
- package/src/knowledge/muse.ts +22 -6
- package/src/knowledge/overrides.ts +97 -0
- package/src/knowledge/pi.ts +25 -3
|
@@ -5,37 +5,62 @@
|
|
|
5
5
|
* no caller re-derives them.
|
|
6
6
|
*/
|
|
7
7
|
import type { HarnessDescriptor, StreamingGranularity } from "../knowledge/descriptor.js";
|
|
8
|
+
import { ArgvRefusalError } from "./refusal.js";
|
|
8
9
|
import { assertUsableSessionId } from "./session-id.js";
|
|
10
|
+
import { renderTurnOptions } from "./turn-options.js";
|
|
9
11
|
import { validateModel } from "./vocabulary.js";
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
constructor(
|
|
14
|
-
readonly issue: string,
|
|
15
|
-
message: string,
|
|
16
|
-
) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.name = "ArgvRefusalError";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
13
|
+
export type { RefusalIssue } from "./refusal.js";
|
|
14
|
+
export { ArgvRefusalError, buildRefusalMessage, REFUSAL_ISSUES } from "./refusal.js";
|
|
21
15
|
|
|
22
16
|
/** One guard for every builder that places a positional prompt. Selector
|
|
23
17
|
* hygiene (session ids) lives in session-id.ts; model selectors go through
|
|
24
18
|
* validateModel - both refuse, never sanitize. */
|
|
25
19
|
const assertCleanPrompt = (h: HarnessDescriptor, prompt: string): void => {
|
|
26
20
|
if (prompt.startsWith("-")) {
|
|
27
|
-
throw new ArgvRefusalError(
|
|
28
|
-
"prompt-flag-injection",
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
throw new ArgvRefusalError({
|
|
22
|
+
issue: "prompt-flag-injection",
|
|
23
|
+
harness: h.name,
|
|
24
|
+
supported: ["prompt must not start with '-'"],
|
|
25
|
+
detail: `it would be parsed as a flag by ${h.bin}`,
|
|
26
|
+
});
|
|
31
27
|
}
|
|
32
28
|
};
|
|
33
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Variant that allows a leading '-' when the caller explicitly opted in via
|
|
32
|
+
* --prompt / --prompt-file. The positional guard still applies for implicit
|
|
33
|
+
* positional prompts, but an explicit opt-in bypasses it so `hcn run --prompt "-bad"`
|
|
34
|
+
* succeeds while `hcn run "-bad"` refuses. The caller must set
|
|
35
|
+
* `__explicitPrompt: true` on the options object when the prompt came from an
|
|
36
|
+
* explicit flag.
|
|
37
|
+
*/
|
|
38
|
+
const assertCleanPromptMaybe = (h: HarnessDescriptor, prompt: string, explicit?: boolean): void => {
|
|
39
|
+
if (explicit) return;
|
|
40
|
+
assertCleanPrompt(h, prompt);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface DiscoveryOptions {
|
|
44
|
+
readonly tools?: boolean;
|
|
45
|
+
readonly instructionFiles?: boolean;
|
|
46
|
+
readonly extensions?: boolean;
|
|
47
|
+
readonly skills?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
export interface TurnOptions {
|
|
35
51
|
readonly prompt: string;
|
|
36
52
|
readonly tools?: readonly string[];
|
|
37
53
|
readonly model?: string;
|
|
38
54
|
readonly autonomy?: boolean;
|
|
55
|
+
readonly effort?: string;
|
|
56
|
+
readonly sandbox?: string;
|
|
57
|
+
readonly provider?: string;
|
|
58
|
+
readonly discovery?: DiscoveryOptions;
|
|
59
|
+
readonly write?: boolean;
|
|
60
|
+
readonly shell?: boolean;
|
|
61
|
+
readonly maxSteps?: number;
|
|
62
|
+
/** Internal: set by CLI when prompt came from --prompt/--prompt-file to bypass leading '-' guard */
|
|
63
|
+
readonly __explicitPrompt?: boolean;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
export interface ResumeOptions extends TurnOptions {
|
|
@@ -48,25 +73,47 @@ export type LaunchOptions = TurnOptions;
|
|
|
48
73
|
* validated selections, with the variadic tools flag LAST and fed exactly
|
|
49
74
|
* one joined token so nothing after it can be swallowed as a tool name. */
|
|
50
75
|
const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
51
|
-
|
|
76
|
+
assertCleanPromptMaybe(h, opts.prompt, opts.__explicitPrompt);
|
|
52
77
|
const tail = [opts.prompt, ...h.launch.streamFlags];
|
|
53
78
|
if (opts.model !== undefined) {
|
|
54
79
|
const validated = validateModel(h, opts.model);
|
|
55
|
-
if (!validated.ok)
|
|
80
|
+
if (!validated.ok) {
|
|
81
|
+
const supported = [...h.vocabulary.models, ...Object.keys(h.vocabulary.aliases)];
|
|
82
|
+
throw new ArgvRefusalError({
|
|
83
|
+
issue: "unknown-model",
|
|
84
|
+
harness: h.name,
|
|
85
|
+
supported: supported.length ? supported : ["no curated models"],
|
|
86
|
+
detail: opts.model,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
56
89
|
tail.push(h.vocabulary.modelFlag, validated.id);
|
|
57
90
|
}
|
|
58
91
|
if (opts.autonomy === true) {
|
|
59
92
|
if (h.autonomy === null) {
|
|
60
|
-
throw new ArgvRefusalError(
|
|
93
|
+
throw new ArgvRefusalError({
|
|
94
|
+
issue: "no-autonomy-mode",
|
|
95
|
+
harness: h.name,
|
|
96
|
+
supported: ["claude --dangerously-skip-permissions", "codex --yolo", "muse --yolo"],
|
|
97
|
+
});
|
|
61
98
|
}
|
|
62
99
|
tail.push(h.autonomy.flag);
|
|
63
100
|
}
|
|
64
|
-
if (opts.tools !== undefined
|
|
101
|
+
if (opts.tools !== undefined) {
|
|
102
|
+
if (h.launch.toolsFlag === null) {
|
|
103
|
+
throw new ArgvRefusalError({
|
|
104
|
+
issue: "unsupported-option",
|
|
105
|
+
harness: h.name,
|
|
106
|
+
supported: ["--allowedTools is claude-only"],
|
|
107
|
+
detail: "tools",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
65
110
|
if (opts.tools.length === 0 || opts.tools.some((t) => t.trim() === "" || t.includes(","))) {
|
|
66
|
-
throw new ArgvRefusalError(
|
|
67
|
-
"invalid-tool-grant",
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
throw new ArgvRefusalError({
|
|
112
|
+
issue: "invalid-tool-grant",
|
|
113
|
+
harness: h.name,
|
|
114
|
+
supported: ["non-empty, comma-free tool names"],
|
|
115
|
+
detail: `tools=${JSON.stringify(opts.tools)}`,
|
|
116
|
+
});
|
|
70
117
|
}
|
|
71
118
|
tail.push(h.launch.toolsFlag, opts.tools.join(","));
|
|
72
119
|
}
|
|
@@ -76,6 +123,7 @@ const turnTail = (h: HarnessDescriptor, opts: TurnOptions): string[] => {
|
|
|
76
123
|
export const buildLaunchArgv = (h: HarnessDescriptor, opts: LaunchOptions): string[] => [
|
|
77
124
|
h.bin,
|
|
78
125
|
...h.launch.baseFlags,
|
|
126
|
+
...renderTurnOptions(h, opts, "launch"),
|
|
79
127
|
...turnTail(h, opts),
|
|
80
128
|
];
|
|
81
129
|
|
|
@@ -92,6 +140,7 @@ export const buildResumeArgv = (h: HarnessDescriptor, opts: ResumeOptions): stri
|
|
|
92
140
|
h.resume.flag,
|
|
93
141
|
opts.sessionId,
|
|
94
142
|
...h.resume.extraFlags,
|
|
143
|
+
...renderTurnOptions(h, opts, "resume"),
|
|
95
144
|
...turnTail(h, opts),
|
|
96
145
|
];
|
|
97
146
|
};
|
|
@@ -103,10 +152,11 @@ export interface SessionOptions {
|
|
|
103
152
|
|
|
104
153
|
export const buildSessionArgv = (h: HarnessDescriptor, opts: SessionOptions): string[] => {
|
|
105
154
|
if (!h.sessionMode) {
|
|
106
|
-
throw new ArgvRefusalError(
|
|
107
|
-
"no-session-mode",
|
|
108
|
-
|
|
109
|
-
|
|
155
|
+
throw new ArgvRefusalError({
|
|
156
|
+
issue: "no-session-mode",
|
|
157
|
+
harness: h.name,
|
|
158
|
+
supported: ["session is claude-only"],
|
|
159
|
+
});
|
|
110
160
|
}
|
|
111
161
|
assertUsableSessionId(opts.sessionId);
|
|
112
162
|
const argv = [
|
|
@@ -118,7 +168,15 @@ export const buildSessionArgv = (h: HarnessDescriptor, opts: SessionOptions): st
|
|
|
118
168
|
];
|
|
119
169
|
if (opts.model !== undefined) {
|
|
120
170
|
const validated = validateModel(h, opts.model);
|
|
121
|
-
if (!validated.ok)
|
|
171
|
+
if (!validated.ok) {
|
|
172
|
+
const supported = [...h.vocabulary.models, ...Object.keys(h.vocabulary.aliases)];
|
|
173
|
+
throw new ArgvRefusalError({
|
|
174
|
+
issue: "unknown-model",
|
|
175
|
+
harness: h.name,
|
|
176
|
+
supported: supported.length ? supported : ["no curated models"],
|
|
177
|
+
detail: opts.model,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
122
180
|
argv.push(h.vocabulary.modelFlag, validated.id);
|
|
123
181
|
}
|
|
124
182
|
return argv;
|
|
@@ -8,13 +8,7 @@ import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
|
8
8
|
|
|
9
9
|
export const stdinPolicyOf = (h: HarnessDescriptor): "inherit" | "close-required" => h.stdin;
|
|
10
10
|
|
|
11
|
-
export const providerFlagOf = (h: HarnessDescriptor): string | null =>
|
|
12
|
-
h.provider === null ? null : h.provider.flag;
|
|
13
|
-
|
|
14
11
|
export const toolsFlagOf = (h: HarnessDescriptor): string | null => h.launch.toolsFlag;
|
|
15
12
|
|
|
16
|
-
export const discoveryDisableFlagsOf = (h: HarnessDescriptor): readonly string[] =>
|
|
17
|
-
h.discoveryDisableFlags;
|
|
18
|
-
|
|
19
13
|
export const autonomyFlagOf = (h: HarnessDescriptor): string | null =>
|
|
20
14
|
h.autonomy === null ? null : h.autonomy.flag;
|
|
@@ -9,21 +9,112 @@
|
|
|
9
9
|
* Feed these wall-eligible output only - stderr and the non-JSON tail of a
|
|
10
10
|
* dying turn - never assistant message content, where the model merely
|
|
11
11
|
* TALKING about limits would match.
|
|
12
|
+
*
|
|
13
|
+
* Matchers are serializable objects {pattern, flags, code/kind}; compilation
|
|
14
|
+
* to RegExp happens here with bounded inputs (pattern length, count, flags)
|
|
15
|
+
* and a WeakMap cache. The input window (first 4096 chars of a line), not
|
|
16
|
+
* pattern analysis, is the backtracking bound - a malicious pattern could
|
|
17
|
+
* otherwise catastrophically backtrack on a long line.
|
|
12
18
|
*/
|
|
13
|
-
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
AuthFailureKind,
|
|
22
|
+
AuthMatcher,
|
|
23
|
+
HarnessDescriptor,
|
|
24
|
+
LimitCode,
|
|
25
|
+
LimitMatcher,
|
|
26
|
+
} from "../knowledge/descriptor.js";
|
|
14
27
|
|
|
15
28
|
/** Bottom-up batch scans stop after this many non-empty lines: the wall is
|
|
16
29
|
* virtually always the last thing a dying turn printed, and an unbounded
|
|
17
30
|
* scan over an accumulating session buffer is O(turns x output). */
|
|
18
31
|
const BATCH_SCAN_MAX_LINES = 200;
|
|
19
32
|
|
|
33
|
+
/** Max pattern length and max matchers per harness per kind - load-bearing
|
|
34
|
+
* bounds that prevent a crafted override file from DoS'ing the matcher
|
|
35
|
+
* compiler or the scanner. */
|
|
36
|
+
const MAX_PATTERN_LENGTH = 200;
|
|
37
|
+
const MAX_MATCHERS_PER_KIND = 64;
|
|
38
|
+
const WINDOW = 4096;
|
|
39
|
+
|
|
40
|
+
// WeakMap cache: same matcher array instance reuses identical RegExp objects
|
|
41
|
+
const limitCache = new WeakMap<
|
|
42
|
+
ReadonlyArray<LimitMatcher>,
|
|
43
|
+
ReadonlyArray<readonly [RegExp, LimitCode]>
|
|
44
|
+
>();
|
|
45
|
+
const authCache = new WeakMap<
|
|
46
|
+
ReadonlyArray<AuthMatcher>,
|
|
47
|
+
ReadonlyArray<readonly [RegExp, AuthFailureKind]>
|
|
48
|
+
>();
|
|
49
|
+
|
|
50
|
+
const validateAndCompile = (pattern: string, flags: string | undefined): RegExp => {
|
|
51
|
+
const f = flags ?? "i";
|
|
52
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
53
|
+
throw new Error(`pattern over ${MAX_PATTERN_LENGTH} characters`);
|
|
54
|
+
}
|
|
55
|
+
if (f.includes("g") || f.includes("y")) {
|
|
56
|
+
throw new Error(`flags must not contain g or y (got ${JSON.stringify(f)})`);
|
|
57
|
+
}
|
|
58
|
+
for (const ch of f) {
|
|
59
|
+
if (!"imsu".includes(ch)) throw new Error(`flag ${JSON.stringify(ch)} outside imsu`);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return new RegExp(pattern, f);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw new Error(`uncompilable pattern ${JSON.stringify(pattern)}: ${(e as Error).message}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const compileLimitMatchers = (
|
|
69
|
+
matchers: ReadonlyArray<LimitMatcher>,
|
|
70
|
+
): ReadonlyArray<readonly [RegExp, LimitCode]> => {
|
|
71
|
+
const cached = limitCache.get(matchers);
|
|
72
|
+
if (cached !== undefined) return cached;
|
|
73
|
+
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
74
|
+
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
75
|
+
}
|
|
76
|
+
const compiled = matchers.map((m) => [validateAndCompile(m.pattern, m.flags), m.code] as const);
|
|
77
|
+
limitCache.set(matchers, compiled);
|
|
78
|
+
return compiled;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const compileAuthMatchers = (
|
|
82
|
+
matchers: ReadonlyArray<AuthMatcher>,
|
|
83
|
+
): ReadonlyArray<readonly [RegExp, AuthFailureKind]> => {
|
|
84
|
+
const cached = authCache.get(matchers);
|
|
85
|
+
if (cached !== undefined) return cached;
|
|
86
|
+
if (matchers.length > MAX_MATCHERS_PER_KIND) {
|
|
87
|
+
throw new Error(`more than ${MAX_MATCHERS_PER_KIND} matchers per harness per kind`);
|
|
88
|
+
}
|
|
89
|
+
const compiled = matchers.map((m) => [validateAndCompile(m.pattern, m.flags), m.kind] as const);
|
|
90
|
+
authCache.set(matchers, compiled);
|
|
91
|
+
return compiled;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Generic alias for tests that call compileMatchers directly
|
|
95
|
+
export const compileMatchers = <T extends LimitMatcher | AuthMatcher>(
|
|
96
|
+
matchers: ReadonlyArray<T>,
|
|
97
|
+
): ReadonlyArray<readonly [RegExp, unknown]> => {
|
|
98
|
+
// Dispatch based on first element's shape - limit has code, auth has kind
|
|
99
|
+
if (matchers.length === 0) return [];
|
|
100
|
+
const first = matchers[0] as unknown as Record<string, unknown>;
|
|
101
|
+
if ("code" in first)
|
|
102
|
+
return compileLimitMatchers(
|
|
103
|
+
matchers as unknown as ReadonlyArray<LimitMatcher>,
|
|
104
|
+
) as unknown as ReadonlyArray<readonly [RegExp, unknown]>;
|
|
105
|
+
return compileAuthMatchers(
|
|
106
|
+
matchers as unknown as ReadonlyArray<AuthMatcher>,
|
|
107
|
+
) as unknown as ReadonlyArray<readonly [RegExp, unknown]>;
|
|
108
|
+
};
|
|
109
|
+
|
|
20
110
|
const scanLine = <Code>(
|
|
21
111
|
line: string,
|
|
22
112
|
matchers: ReadonlyArray<readonly [RegExp, Code]>,
|
|
23
113
|
): Code | null => {
|
|
114
|
+
const windowed = line.slice(0, WINDOW);
|
|
24
115
|
for (let i = 0; i < matchers.length; i++) {
|
|
25
116
|
const matcher = matchers[i];
|
|
26
|
-
if (matcher?.[0].test(
|
|
117
|
+
if (matcher?.[0].test(windowed)) return matcher[1];
|
|
27
118
|
}
|
|
28
119
|
return null;
|
|
29
120
|
};
|
|
@@ -48,16 +139,16 @@ const scanTail = <Code>(
|
|
|
48
139
|
|
|
49
140
|
/** Per-line entry point for streaming readers: O(1) per line, no rescans. */
|
|
50
141
|
export const detectLimitInLine = (h: HarnessDescriptor, line: string): LimitCode | null =>
|
|
51
|
-
scanLine(line.trim(), h.limitMatchers);
|
|
142
|
+
scanLine(line.trim(), compileLimitMatchers(h.limitMatchers));
|
|
52
143
|
|
|
53
144
|
/** Batch convenience over a turn's tail, bounded and bottom-up. */
|
|
54
145
|
export const detectLimit = (h: HarnessDescriptor, output: string): LimitCode | null =>
|
|
55
|
-
scanTail(output, h.limitMatchers);
|
|
146
|
+
scanTail(output, compileLimitMatchers(h.limitMatchers));
|
|
56
147
|
|
|
57
148
|
export const detectAuthFailureInLine = (
|
|
58
149
|
h: HarnessDescriptor,
|
|
59
150
|
line: string,
|
|
60
|
-
): AuthFailureKind | null => scanLine(line.trim(), h.authMatchers);
|
|
151
|
+
): AuthFailureKind | null => scanLine(line.trim(), compileAuthMatchers(h.authMatchers));
|
|
61
152
|
|
|
62
153
|
export const detectAuthFailure = (h: HarnessDescriptor, output: string): AuthFailureKind | null =>
|
|
63
|
-
scanTail(output, h.authMatchers);
|
|
154
|
+
scanTail(output, compileAuthMatchers(h.authMatchers));
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured argv refusals: closed issue vocabulary and the typed error
|
|
3
|
+
* that carries harness, option, facet, and supported alternatives. The
|
|
4
|
+
* helper builds the message from those fields so the two cannot drift - a
|
|
5
|
+
* consumer branching on `issue` never has to parse English.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DiscoveryFacet, HarnessName, TurnOptionKey } from "../knowledge/descriptor.js";
|
|
9
|
+
import { deepFreeze } from "../knowledge/descriptor.js";
|
|
10
|
+
|
|
11
|
+
export const REFUSAL_ISSUES = deepFreeze([
|
|
12
|
+
"unsupported-option",
|
|
13
|
+
"unsupported-option-facet",
|
|
14
|
+
"unsupported-on-resume",
|
|
15
|
+
"invalid-option-value",
|
|
16
|
+
"unknown-effort",
|
|
17
|
+
"unknown-model",
|
|
18
|
+
"invalid-env",
|
|
19
|
+
"invalid-tool-grant",
|
|
20
|
+
"prompt-flag-injection",
|
|
21
|
+
"no-autonomy-mode",
|
|
22
|
+
"no-session-mode",
|
|
23
|
+
] as const);
|
|
24
|
+
export type RefusalIssue = (typeof REFUSAL_ISSUES)[number];
|
|
25
|
+
|
|
26
|
+
/** One helper builds the message from the structured fields so message and
|
|
27
|
+
* fields cannot drift. Every message names an alternative, not only a
|
|
28
|
+
* negation, so an agent can pivot without reading the descriptor. */
|
|
29
|
+
export const buildRefusalMessage = (
|
|
30
|
+
issue: RefusalIssue,
|
|
31
|
+
harness: HarnessName,
|
|
32
|
+
option?: TurnOptionKey,
|
|
33
|
+
facet?: DiscoveryFacet,
|
|
34
|
+
supported: readonly string[] = [],
|
|
35
|
+
detail?: string,
|
|
36
|
+
): string => {
|
|
37
|
+
const supportedStr =
|
|
38
|
+
supported.length > 0 ? `supported: ${supported.join(", ")}` : "supported: (none)";
|
|
39
|
+
const facetSuffix = facet ? ` facet ${JSON.stringify(facet)}` : "";
|
|
40
|
+
const optionPart = option
|
|
41
|
+
? ` option ${JSON.stringify(option)}${facetSuffix}`
|
|
42
|
+
: facet
|
|
43
|
+
? ` facet ${JSON.stringify(facet)}`
|
|
44
|
+
: "";
|
|
45
|
+
const detailSuffix = detail ? ` (${detail})` : "";
|
|
46
|
+
switch (issue) {
|
|
47
|
+
case "unsupported-option":
|
|
48
|
+
return `${harness} cannot express${optionPart}${detailSuffix}; ${supportedStr} - drop the option or route this work to a harness that supports it`;
|
|
49
|
+
case "unsupported-option-facet":
|
|
50
|
+
return `${harness} cannot express discovery${facetSuffix}${detailSuffix}; ${supportedStr} - drop the facet or route this work to a harness that supports it`;
|
|
51
|
+
case "unsupported-on-resume": {
|
|
52
|
+
const name = option
|
|
53
|
+
? JSON.stringify(option) + (facet ? `:${facet}` : "")
|
|
54
|
+
: facet
|
|
55
|
+
? JSON.stringify(facet)
|
|
56
|
+
: "option";
|
|
57
|
+
return `${name} cannot be expressed on resume for ${harness}${detailSuffix}; ${supportedStr} - re-launch instead of resuming or drop the option`;
|
|
58
|
+
}
|
|
59
|
+
case "invalid-option-value":
|
|
60
|
+
return `invalid value for${optionPart} on ${harness}${detailSuffix}; ${supportedStr} - use a supported value instead`;
|
|
61
|
+
case "unknown-effort":
|
|
62
|
+
return `unknown effort for ${harness}${detail ? ` ${JSON.stringify(detail)}` : ""}; ${supportedStr} - use one of the ladder values`;
|
|
63
|
+
case "unknown-model":
|
|
64
|
+
return `unknown ${harness} model${detail ? ` ${JSON.stringify(detail)}` : ""}; ${supportedStr} - use one of the supported models`;
|
|
65
|
+
case "invalid-env":
|
|
66
|
+
return `invalid env key or value for ${harness}${detailSuffix}; ${supportedStr} - keys must match ^[A-Za-z_][A-Za-z0-9_]*$ and contain no NUL`;
|
|
67
|
+
case "invalid-tool-grant":
|
|
68
|
+
return `tool grant for ${harness} contains an empty entry or a comma; a blank tool flag value grants nothing detectable, and a comma inside one name silently splits the grant; ${supportedStr} - provide comma-free, non-empty tool names as separate entries`;
|
|
69
|
+
case "prompt-flag-injection":
|
|
70
|
+
return `positional prompt may not start with '-'; it would be parsed as a flag by ${harness}${detailSuffix}; ${supportedStr} - remove leading '-' or prefix with a space`;
|
|
71
|
+
case "no-autonomy-mode":
|
|
72
|
+
return `${harness} has no unattended-run flag; ${supportedStr} - drop autonomy or route to a supporting harness (claude --dangerously-skip-permissions, codex/muse --yolo)`;
|
|
73
|
+
case "no-session-mode":
|
|
74
|
+
return `${harness} declares no persistent headless session mode; ${supportedStr} - use streamTurn instead of openSession`;
|
|
75
|
+
default: {
|
|
76
|
+
const exhaustive: never = issue;
|
|
77
|
+
return `${exhaustive as string} for ${harness}${optionPart}${detailSuffix}; ${supportedStr}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Raised when launch options would corrupt or subvert the spawned argv. */
|
|
83
|
+
export class ArgvRefusalError extends Error {
|
|
84
|
+
readonly issue: RefusalIssue;
|
|
85
|
+
readonly harness: HarnessName;
|
|
86
|
+
readonly option?: TurnOptionKey;
|
|
87
|
+
readonly facet?: DiscoveryFacet;
|
|
88
|
+
readonly supported: readonly string[];
|
|
89
|
+
constructor(args: {
|
|
90
|
+
readonly issue: RefusalIssue;
|
|
91
|
+
readonly harness: HarnessName;
|
|
92
|
+
readonly option?: TurnOptionKey;
|
|
93
|
+
readonly facet?: DiscoveryFacet;
|
|
94
|
+
readonly supported?: readonly string[];
|
|
95
|
+
readonly detail?: string;
|
|
96
|
+
readonly message?: string;
|
|
97
|
+
}) {
|
|
98
|
+
const message =
|
|
99
|
+
args.message ??
|
|
100
|
+
buildRefusalMessage(
|
|
101
|
+
args.issue,
|
|
102
|
+
args.harness,
|
|
103
|
+
args.option,
|
|
104
|
+
args.facet,
|
|
105
|
+
args.supported ?? [],
|
|
106
|
+
args.detail,
|
|
107
|
+
);
|
|
108
|
+
super(message);
|
|
109
|
+
this.name = "ArgvRefusalError";
|
|
110
|
+
this.issue = args.issue;
|
|
111
|
+
this.harness = args.harness;
|
|
112
|
+
this.option = args.option;
|
|
113
|
+
this.facet = args.facet;
|
|
114
|
+
this.supported = args.supported ?? [];
|
|
115
|
+
}
|
|
116
|
+
}
|