crewhaus 0.1.3 → 0.1.5
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 +1 -1
- package/dist/doctor-checks.d.ts +45 -0
- package/dist/doctor-checks.js +178 -0
- package/{src/egress-matcher.ts → dist/egress-matcher.d.ts} +17 -52
- package/dist/egress-matcher.js +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2356 -0
- package/{src/justification-gate.ts → dist/justification-gate.d.ts} +21 -61
- package/dist/justification-gate.js +72 -0
- package/{src/scope-audit.ts → dist/scope-audit.d.ts} +3 -42
- package/dist/scope-audit.js +85 -0
- package/package.json +71 -68
- package/src/doctor-checks.test.ts +0 -122
- package/src/doctor-checks.ts +0 -220
- package/src/egress-matcher.test.ts +0 -273
- package/src/eval.test.ts +0 -130
- package/src/index.test.ts +0 -699
- package/src/index.ts +0 -2513
- package/src/justification-gate.test.ts +0 -292
- package/src/scope-audit.test.ts +0 -207
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ CrewHaus compiles one spec into the shape each situation calls for — a CLI age
|
|
|
11
11
|
The fastest path is the self-contained binary — one file, no Bun/Node required:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
brew
|
|
14
|
+
brew tap crewhaus/tap && brew install crewhaus # macOS / Linux (Homebrew)
|
|
15
15
|
scoop install crewhaus # Windows (Scoop; see repo for the bucket)
|
|
16
16
|
winget install CrewHaus.CLI # Windows (winget)
|
|
17
17
|
# Debian / Ubuntu (apt): signed repo at https://crewhaus.github.io/apt
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-aware credential checks for `crewhaus doctor`, factored out of the
|
|
3
|
+
* entry file `index.ts` (which runs a top-level argv switch and so cannot be
|
|
4
|
+
* imported by a test without executing the CLI). Side-effect-free and
|
|
5
|
+
* directly unit-testable, mirroring `scope-audit.ts` / `justification-gate.ts`.
|
|
6
|
+
*
|
|
7
|
+
* The old doctor checked ONLY Anthropic credentials and exited 1 when they
|
|
8
|
+
* were absent — for every spec, including openai/gemini/bedrock/local ones
|
|
9
|
+
* that never touch Anthropic. The fix: parse the cwd spec's `agent.model`
|
|
10
|
+
* with the model-router grammar and check the matching provider's env,
|
|
11
|
+
* reporting the others informationally instead of failing on them.
|
|
12
|
+
*/
|
|
13
|
+
/** Doctor-level provider id: the router's four providers plus "local"
|
|
14
|
+
* (`local/<m>@<url>` parses to openai WITH a baseUrl — credential-free). */
|
|
15
|
+
export type DoctorProviderId = "anthropic" | "openai" | "gemini" | "bedrock" | "local";
|
|
16
|
+
export type DoctorCredentialCheck = {
|
|
17
|
+
readonly label: string;
|
|
18
|
+
readonly pass: boolean;
|
|
19
|
+
/** warn+pass renders as "~" — informational, never fails doctor. */
|
|
20
|
+
readonly warn?: boolean;
|
|
21
|
+
readonly reason?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Tolerantly extract `agent.model` from a crewhaus.yaml text. Returns
|
|
25
|
+
* undefined when the spec doesn't parse or carries no agent.model (e.g.
|
|
26
|
+
* workflow shapes with per-step models) — the caller then falls back to
|
|
27
|
+
* the legacy Anthropic-first behaviour.
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractSpecModel(yamlText: string): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Map a model string to the doctor-level provider whose credentials the
|
|
32
|
+
* run will need. Returns undefined for unparseable strings.
|
|
33
|
+
*/
|
|
34
|
+
export declare function selectedProvider(model: string): DoctorProviderId | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Build the credential section of `crewhaus doctor`.
|
|
37
|
+
*
|
|
38
|
+
* With a parseable spec model: the matching provider gets a real pass/fail
|
|
39
|
+
* check (informational for bedrock/local), and any OTHER provider with env
|
|
40
|
+
* visibly set gets a "~" informational line. Without one (no spec file, or
|
|
41
|
+
* no agent.model): legacy Anthropic-first behaviour, EXCEPT that a missing
|
|
42
|
+
* Anthropic credential downgrades to informational when another provider's
|
|
43
|
+
* env is clearly set (the operator is plainly not on Anthropic).
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildCredentialChecks(specModel: string | undefined, env: NodeJS.ProcessEnv): DoctorCredentialCheck[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { resolveAuth } from "@crewhaus/adapter-anthropic";
|
|
2
|
+
import { parseModelString } from "@crewhaus/model-router";
|
|
3
|
+
import { parseSpec } from "@crewhaus/spec";
|
|
4
|
+
function isSet(env, name) {
|
|
5
|
+
const v = env[name];
|
|
6
|
+
return typeof v === "string" && v !== "";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Tolerantly extract `agent.model` from a crewhaus.yaml text. Returns
|
|
10
|
+
* undefined when the spec doesn't parse or carries no agent.model (e.g.
|
|
11
|
+
* workflow shapes with per-step models) — the caller then falls back to
|
|
12
|
+
* the legacy Anthropic-first behaviour.
|
|
13
|
+
*/
|
|
14
|
+
export function extractSpecModel(yamlText) {
|
|
15
|
+
try {
|
|
16
|
+
const spec = parseSpec(yamlText);
|
|
17
|
+
const m = spec.agent?.model;
|
|
18
|
+
return typeof m === "string" && m.length > 0 ? m : undefined;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Map a model string to the doctor-level provider whose credentials the
|
|
26
|
+
* run will need. Returns undefined for unparseable strings.
|
|
27
|
+
*/
|
|
28
|
+
export function selectedProvider(model) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = parseModelString(model);
|
|
31
|
+
if (parsed.providerId === "openai" && parsed.baseUrl !== undefined)
|
|
32
|
+
return "local";
|
|
33
|
+
return parsed.providerId;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function anthropicStatus(env) {
|
|
40
|
+
const auth = resolveAuth(env);
|
|
41
|
+
return auth.mode !== "none"
|
|
42
|
+
? { satisfied: true, detail: `${auth.mode} credentials found` }
|
|
43
|
+
: {
|
|
44
|
+
satisfied: false,
|
|
45
|
+
detail: "set ANTHROPIC_AUTH_TOKEN (Claude subscription) or ANTHROPIC_API_KEY",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function openaiStatus(env) {
|
|
49
|
+
if (isSet(env, "OPENAI_API_KEY"))
|
|
50
|
+
return { satisfied: true, detail: "OPENAI_API_KEY set" };
|
|
51
|
+
if (isSet(env, "OPENAI_BASE_URL")) {
|
|
52
|
+
return { satisfied: true, detail: "OPENAI_BASE_URL set (OpenAI-compatible endpoint)" };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
satisfied: false,
|
|
56
|
+
detail: "set OPENAI_API_KEY (or OPENAI_BASE_URL for an OpenAI-compatible endpoint)",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function geminiStatus(env) {
|
|
60
|
+
if (isSet(env, "GEMINI_API_KEY"))
|
|
61
|
+
return { satisfied: true, detail: "GEMINI_API_KEY set" };
|
|
62
|
+
if (isSet(env, "GOOGLE_API_KEY"))
|
|
63
|
+
return { satisfied: true, detail: "GOOGLE_API_KEY set" };
|
|
64
|
+
if (isSet(env, "GOOGLE_GENAI_USE_VERTEXAI") || isSet(env, "GOOGLE_CLOUD_PROJECT")) {
|
|
65
|
+
return {
|
|
66
|
+
satisfied: true,
|
|
67
|
+
detail: "Vertex AI env set (GOOGLE_GENAI_USE_VERTEXAI/GOOGLE_CLOUD_PROJECT)",
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
satisfied: false,
|
|
72
|
+
detail: "set GEMINI_API_KEY (or GOOGLE_API_KEY; Vertex users set GOOGLE_GENAI_USE_VERTEXAI + GOOGLE_CLOUD_PROJECT)",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const BEDROCK_ENV_NAMES = [
|
|
76
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
77
|
+
"AWS_ACCESS_KEY_ID",
|
|
78
|
+
"AWS_PROFILE",
|
|
79
|
+
"AWS_REGION",
|
|
80
|
+
];
|
|
81
|
+
function bedrockStatus(env) {
|
|
82
|
+
const found = BEDROCK_ENV_NAMES.filter((n) => isSet(env, n));
|
|
83
|
+
return found.length > 0
|
|
84
|
+
? { satisfied: true, detail: `${found.join(", ")} set` }
|
|
85
|
+
: {
|
|
86
|
+
satisfied: false,
|
|
87
|
+
detail: "no AWS env vars visible (AWS_BEARER_TOKEN_BEDROCK/AWS_ACCESS_KEY_ID/AWS_PROFILE/AWS_REGION) — the SDK default credential chain (IRSA, instance profile, ~/.aws) may still apply",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function statusFor(provider, env) {
|
|
91
|
+
switch (provider) {
|
|
92
|
+
case "anthropic":
|
|
93
|
+
return anthropicStatus(env);
|
|
94
|
+
case "openai":
|
|
95
|
+
return openaiStatus(env);
|
|
96
|
+
case "gemini":
|
|
97
|
+
return geminiStatus(env);
|
|
98
|
+
case "bedrock":
|
|
99
|
+
return bedrockStatus(env);
|
|
100
|
+
case "local":
|
|
101
|
+
return {
|
|
102
|
+
satisfied: true,
|
|
103
|
+
detail: "local endpoint baked into the model string — no credentials needed",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const PROVIDER_LABEL = {
|
|
108
|
+
anthropic: "Anthropic credentials",
|
|
109
|
+
openai: "OpenAI credentials",
|
|
110
|
+
gemini: "Gemini credentials",
|
|
111
|
+
bedrock: "Bedrock (AWS) credentials",
|
|
112
|
+
local: "Local endpoint",
|
|
113
|
+
};
|
|
114
|
+
/** Providers whose env check is INFORMATIONAL even when selected: the AWS
|
|
115
|
+
* SDK's default credential chain is authoritative for bedrock (env vars are
|
|
116
|
+
* only one of its sources), and local/ endpoints need no credentials. */
|
|
117
|
+
const INFORMATIONAL_WHEN_SELECTED = new Set(["bedrock", "local"]);
|
|
118
|
+
/** Non-selected providers whose env is set get an informational line so the
|
|
119
|
+
* operator sees what else is configured. */
|
|
120
|
+
const ALL_PROVIDERS = ["anthropic", "openai", "gemini", "bedrock"];
|
|
121
|
+
/**
|
|
122
|
+
* Build the credential section of `crewhaus doctor`.
|
|
123
|
+
*
|
|
124
|
+
* With a parseable spec model: the matching provider gets a real pass/fail
|
|
125
|
+
* check (informational for bedrock/local), and any OTHER provider with env
|
|
126
|
+
* visibly set gets a "~" informational line. Without one (no spec file, or
|
|
127
|
+
* no agent.model): legacy Anthropic-first behaviour, EXCEPT that a missing
|
|
128
|
+
* Anthropic credential downgrades to informational when another provider's
|
|
129
|
+
* env is clearly set (the operator is plainly not on Anthropic).
|
|
130
|
+
*/
|
|
131
|
+
export function buildCredentialChecks(specModel, env) {
|
|
132
|
+
const provider = specModel !== undefined ? selectedProvider(specModel) : undefined;
|
|
133
|
+
const checks = [];
|
|
134
|
+
if (provider !== undefined && specModel !== undefined) {
|
|
135
|
+
const status = statusFor(provider, env);
|
|
136
|
+
const informational = INFORMATIONAL_WHEN_SELECTED.has(provider);
|
|
137
|
+
checks.push({
|
|
138
|
+
label: `${PROVIDER_LABEL[provider]} (model ${specModel})`,
|
|
139
|
+
pass: informational ? true : status.satisfied,
|
|
140
|
+
...(informational && !status.satisfied ? { warn: true } : {}),
|
|
141
|
+
...(status.satisfied && !informational ? {} : { reason: status.detail }),
|
|
142
|
+
});
|
|
143
|
+
for (const other of ALL_PROVIDERS) {
|
|
144
|
+
if (other === provider)
|
|
145
|
+
continue;
|
|
146
|
+
const otherStatus = statusFor(other, env);
|
|
147
|
+
if (otherStatus.satisfied) {
|
|
148
|
+
checks.push({
|
|
149
|
+
label: `${PROVIDER_LABEL[other]} (not required for this spec)`,
|
|
150
|
+
pass: true,
|
|
151
|
+
warn: true,
|
|
152
|
+
reason: otherStatus.detail,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return checks;
|
|
157
|
+
}
|
|
158
|
+
// No spec model — legacy Anthropic-first behaviour with one softening:
|
|
159
|
+
// when Anthropic creds are absent but another provider's env is clearly
|
|
160
|
+
// set, report informationally instead of hard-failing.
|
|
161
|
+
const anthropic = anthropicStatus(env);
|
|
162
|
+
if (anthropic.satisfied) {
|
|
163
|
+
checks.push({ label: "Anthropic credentials", pass: true });
|
|
164
|
+
return checks;
|
|
165
|
+
}
|
|
166
|
+
const others = ALL_PROVIDERS.filter((p) => p !== "anthropic").filter((p) => statusFor(p, env).satisfied);
|
|
167
|
+
if (others.length > 0) {
|
|
168
|
+
checks.push({
|
|
169
|
+
label: "Anthropic credentials",
|
|
170
|
+
pass: true,
|
|
171
|
+
warn: true,
|
|
172
|
+
reason: `not set, but ${others.map((p) => PROVIDER_LABEL[p]).join(" + ")} detected — claude-* models will not run until ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY is set`,
|
|
173
|
+
});
|
|
174
|
+
return checks;
|
|
175
|
+
}
|
|
176
|
+
checks.push({ label: "Anthropic credentials", pass: false, reason: anthropic.detail });
|
|
177
|
+
return checks;
|
|
178
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { EgressMatcher } from "@crewhaus/egress-classifier";
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* FR-006 — Pillar 3 sink-side egress-matcher selection for `crewhaus run`,
|
|
5
4
|
* factored out of the entry file `index.ts` (which runs a top-level argv
|
|
@@ -28,11 +27,7 @@ import type { EgressMatcher } from "@crewhaus/egress-classifier";
|
|
|
28
27
|
* claude judge. The default path (`"substring"` / unset) returns `undefined`
|
|
29
28
|
* and never touches the semantic package, so it pulls in no embedding code.
|
|
30
29
|
*/
|
|
31
|
-
|
|
32
30
|
export type EgressMatcherChoice = "substring" | "semantic";
|
|
33
|
-
|
|
34
|
-
const VALID_EGRESS_MATCHER_CHOICES: ReadonlyArray<EgressMatcherChoice> = ["substring", "semantic"];
|
|
35
|
-
|
|
36
31
|
/**
|
|
37
32
|
* Default embedder model for the semantic matcher when neither
|
|
38
33
|
* `--egress-embedder` nor `CREWHAUS_EGRESS_EMBEDDER` is supplied. A real
|
|
@@ -40,24 +35,15 @@ const VALID_EGRESS_MATCHER_CHOICES: ReadonlyArray<EgressMatcherChoice> = ["subst
|
|
|
40
35
|
* throws and `SemanticEgressMatcher` degrades to the substring tripwire
|
|
41
36
|
* (its built-in safe fallback) rather than dropping the egress check.
|
|
42
37
|
*/
|
|
43
|
-
export const DEFAULT_EGRESS_EMBEDDER_MODEL = "openai/text-embedding-3-small";
|
|
44
|
-
|
|
38
|
+
export declare const DEFAULT_EGRESS_EMBEDDER_MODEL = "openai/text-embedding-3-small";
|
|
45
39
|
/** Thrown by `resolveEgressMatcherChoice` on an unrecognised choice. The CLI
|
|
46
40
|
* entry file catches it and routes the message through `die()`; tests assert
|
|
47
41
|
* on `.choice` / `.message` without the process exiting. */
|
|
48
|
-
export class InvalidEgressMatcherChoiceError extends Error {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`invalid --egress-matcher "${choice}" — allowed: ${VALID_EGRESS_MATCHER_CHOICES.join(", ")}`,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
42
|
+
export declare class InvalidEgressMatcherChoiceError extends Error {
|
|
43
|
+
readonly choice: string;
|
|
44
|
+
readonly name = "InvalidEgressMatcherChoiceError";
|
|
45
|
+
constructor(choice: string);
|
|
55
46
|
}
|
|
56
|
-
|
|
57
|
-
function isEgressMatcherChoice(s: string): s is EgressMatcherChoice {
|
|
58
|
-
return (VALID_EGRESS_MATCHER_CHOICES as ReadonlyArray<string>).includes(s);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
47
|
/**
|
|
62
48
|
* Pure resolution of the matcher choice from the flag value + the lowered IR
|
|
63
49
|
* selector. `flagValue` is the raw `--egress-matcher` value (or undefined when
|
|
@@ -65,28 +51,19 @@ function isEgressMatcherChoice(s: string): s is EgressMatcherChoice {
|
|
|
65
51
|
* Throws `InvalidEgressMatcherChoiceError` for any value outside the allowed
|
|
66
52
|
* set so the caller can `die()` with a friendly message.
|
|
67
53
|
*/
|
|
68
|
-
export function resolveEgressMatcherChoice(
|
|
69
|
-
flagValue: string | undefined,
|
|
70
|
-
irEgressMatcher: EgressMatcherChoice | undefined,
|
|
71
|
-
): EgressMatcherChoice {
|
|
72
|
-
const raw = flagValue ?? irEgressMatcher ?? "substring";
|
|
73
|
-
if (!isEgressMatcherChoice(raw)) throw new InvalidEgressMatcherChoiceError(raw);
|
|
74
|
-
return raw;
|
|
75
|
-
}
|
|
76
|
-
|
|
54
|
+
export declare function resolveEgressMatcherChoice(flagValue: string | undefined, irEgressMatcher: EgressMatcherChoice | undefined): EgressMatcherChoice;
|
|
77
55
|
export type CreateEgressMatcherOptions = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Embedder model string for the `"semantic"` choice — the
|
|
58
|
+
* `@crewhaus/embedder` prefix grammar (`openai/…`, `voyage/…`, `mock/…`).
|
|
59
|
+
* Resolution precedence at the call site: `--egress-embedder` flag >
|
|
60
|
+
* `CREWHAUS_EGRESS_EMBEDDER` env > `DEFAULT_EGRESS_EMBEDDER_MODEL`. Tests
|
|
61
|
+
* pass `mock/…` for a deterministic, network-free embedder.
|
|
62
|
+
*/
|
|
63
|
+
readonly embedderModel: string;
|
|
64
|
+
/** Optional cosine threshold passed through to `SemanticEgressMatcher`. */
|
|
65
|
+
readonly threshold?: number;
|
|
88
66
|
};
|
|
89
|
-
|
|
90
67
|
/**
|
|
91
68
|
* Build the `EgressMatcher` for a resolved choice. Returns `undefined` for
|
|
92
69
|
* `"substring"` so the caller omits `egressMatcher` from `runChatLoop`
|
|
@@ -97,16 +74,4 @@ export type CreateEgressMatcherOptions = {
|
|
|
97
74
|
* with the injected embedder, so the model-backed code only loads when
|
|
98
75
|
* actually selected.
|
|
99
76
|
*/
|
|
100
|
-
export
|
|
101
|
-
choice: EgressMatcherChoice,
|
|
102
|
-
opts: CreateEgressMatcherOptions,
|
|
103
|
-
): Promise<EgressMatcher | undefined> {
|
|
104
|
-
if (choice === "substring") return undefined;
|
|
105
|
-
const { createEmbedder } = await import("@crewhaus/embedder");
|
|
106
|
-
const { createSemanticEgressMatcher } = await import("@crewhaus/egress-matcher-semantic");
|
|
107
|
-
const embedder = createEmbedder({ model: opts.embedderModel });
|
|
108
|
-
return createSemanticEgressMatcher({
|
|
109
|
-
embedder,
|
|
110
|
-
...(opts.threshold !== undefined ? { threshold: opts.threshold } : {}),
|
|
111
|
-
});
|
|
112
|
-
}
|
|
77
|
+
export declare function createEgressMatcher(choice: EgressMatcherChoice, opts: CreateEgressMatcherOptions): Promise<EgressMatcher | undefined>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const VALID_EGRESS_MATCHER_CHOICES = ["substring", "semantic"];
|
|
2
|
+
/**
|
|
3
|
+
* Default embedder model for the semantic matcher when neither
|
|
4
|
+
* `--egress-embedder` nor `CREWHAUS_EGRESS_EMBEDDER` is supplied. A real
|
|
5
|
+
* provider is the production intent; if its API key is unset the embedder
|
|
6
|
+
* throws and `SemanticEgressMatcher` degrades to the substring tripwire
|
|
7
|
+
* (its built-in safe fallback) rather than dropping the egress check.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_EGRESS_EMBEDDER_MODEL = "openai/text-embedding-3-small";
|
|
10
|
+
/** Thrown by `resolveEgressMatcherChoice` on an unrecognised choice. The CLI
|
|
11
|
+
* entry file catches it and routes the message through `die()`; tests assert
|
|
12
|
+
* on `.choice` / `.message` without the process exiting. */
|
|
13
|
+
export class InvalidEgressMatcherChoiceError extends Error {
|
|
14
|
+
choice;
|
|
15
|
+
name = "InvalidEgressMatcherChoiceError";
|
|
16
|
+
constructor(choice) {
|
|
17
|
+
super(`invalid --egress-matcher "${choice}" — allowed: ${VALID_EGRESS_MATCHER_CHOICES.join(", ")}`);
|
|
18
|
+
this.choice = choice;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isEgressMatcherChoice(s) {
|
|
22
|
+
return VALID_EGRESS_MATCHER_CHOICES.includes(s);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pure resolution of the matcher choice from the flag value + the lowered IR
|
|
26
|
+
* selector. `flagValue` is the raw `--egress-matcher` value (or undefined when
|
|
27
|
+
* the flag is absent); `irEgressMatcher` is `ir.security?.egressMatcher`.
|
|
28
|
+
* Throws `InvalidEgressMatcherChoiceError` for any value outside the allowed
|
|
29
|
+
* set so the caller can `die()` with a friendly message.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveEgressMatcherChoice(flagValue, irEgressMatcher) {
|
|
32
|
+
const raw = flagValue ?? irEgressMatcher ?? "substring";
|
|
33
|
+
if (!isEgressMatcherChoice(raw))
|
|
34
|
+
throw new InvalidEgressMatcherChoiceError(raw);
|
|
35
|
+
return raw;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the `EgressMatcher` for a resolved choice. Returns `undefined` for
|
|
39
|
+
* `"substring"` so the caller omits `egressMatcher` from `runChatLoop`
|
|
40
|
+
* (runtime-core then uses the built-in `substringMatcher`, the
|
|
41
|
+
* behavior-preserving default — the default egress path pulls in no embedding
|
|
42
|
+
* dependency). For `"semantic"` it lazily imports `@crewhaus/embedder` and
|
|
43
|
+
* `@crewhaus/egress-matcher-semantic` and constructs a `SemanticEgressMatcher`
|
|
44
|
+
* with the injected embedder, so the model-backed code only loads when
|
|
45
|
+
* actually selected.
|
|
46
|
+
*/
|
|
47
|
+
export async function createEgressMatcher(choice, opts) {
|
|
48
|
+
if (choice === "substring")
|
|
49
|
+
return undefined;
|
|
50
|
+
const { createEmbedder } = await import("@crewhaus/embedder");
|
|
51
|
+
const { createSemanticEgressMatcher } = await import("@crewhaus/egress-matcher-semantic");
|
|
52
|
+
const embedder = createEmbedder({ model: opts.embedderModel });
|
|
53
|
+
return createSemanticEgressMatcher({
|
|
54
|
+
embedder,
|
|
55
|
+
...(opts.threshold !== undefined ? { threshold: opts.threshold } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED