crewhaus 0.1.3
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 +84 -0
- package/package.json +117 -0
- package/src/doctor-checks.test.ts +122 -0
- package/src/doctor-checks.ts +220 -0
- package/src/egress-matcher.test.ts +273 -0
- package/src/egress-matcher.ts +112 -0
- package/src/eval.test.ts +130 -0
- package/src/index.test.ts +699 -0
- package/src/index.ts +2513 -0
- package/src/justification-gate.test.ts +292 -0
- package/src/justification-gate.ts +124 -0
- package/src/scope-audit.test.ts +207 -0
- package/src/scope-audit.ts +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# crewhaus
|
|
2
|
+
|
|
3
|
+
The [factory](https://github.com/crewhaus/factory) command line: compile, run, eval, and deploy agent harness specs (`crewhaus.yaml`).
|
|
4
|
+
|
|
5
|
+
CrewHaus compiles one spec into the shape each situation calls for — a CLI agent, channel bot, RAG pipeline, multi-agent crew, eval bundle, and more. Every factory workflow starts with a `crewhaus` subcommand.
|
|
6
|
+
|
|
7
|
+
> This is the bare, unscoped `crewhaus` package. (The earlier `@crewhaus/cli` name is deprecated and points here; the supporting libraries remain scoped under `@crewhaus/*`.)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
The fastest path is the self-contained binary — one file, no Bun/Node required:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
brew install crewhaus/tap/crewhaus # macOS / Linux (Homebrew)
|
|
15
|
+
scoop install crewhaus # Windows (Scoop; see repo for the bucket)
|
|
16
|
+
winget install CrewHaus.CLI # Windows (winget)
|
|
17
|
+
# Debian / Ubuntu (apt): signed repo at https://crewhaus.github.io/apt
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install from npm — this package runs on [Bun](https://bun.sh) ≥ 1.2:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g crewhaus # global
|
|
24
|
+
bun add -d crewhaus # project-local dev dependency
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Confirm it:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
crewhaus --version
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quickstart
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Scaffold a starter spec
|
|
37
|
+
crewhaus init my-agent
|
|
38
|
+
cd my-agent
|
|
39
|
+
|
|
40
|
+
# Inspect the lowered IR — a quick sanity check of what the compiler sees
|
|
41
|
+
crewhaus compile crewhaus.yaml --emit-ir
|
|
42
|
+
|
|
43
|
+
# Set a provider credential (the starter spec uses an Anthropic model;
|
|
44
|
+
# --model accepts other providers, e.g. openai/<m>, gemini/<m>, local/<m>@<url>)
|
|
45
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
46
|
+
|
|
47
|
+
# Compile in-memory and run the agent
|
|
48
|
+
crewhaus run crewhaus.yaml
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To emit a runnable bundle on disk instead, use `crewhaus compile crewhaus.yaml -o build`.
|
|
52
|
+
|
|
53
|
+
## Subcommands
|
|
54
|
+
|
|
55
|
+
Run `crewhaus` with no arguments for the full usage text, or any subcommand with `--help` for its flags.
|
|
56
|
+
|
|
57
|
+
| Subcommand | What it does |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `compile <spec.yaml> -o <out-dir>` | Compile a spec to a runnable bundle; `--emit-ir` prints the lowered IR as JSON instead |
|
|
60
|
+
| `run <spec.yaml>` | Compile in-memory and execute the agent (`--model`, `--resume <id>` / `--continue`, `--prompt <text>`, …) |
|
|
61
|
+
| `eval <spec.yaml> --dataset <data> --graders <graders.yaml>` | Run the agent against a dataset and grade (deterministic graders + LLM-as-judge) |
|
|
62
|
+
| `eval-report diff <prev> <new>` | Compare two eval runs and emit a diff report |
|
|
63
|
+
| `optimize <spec.yaml> --dataset <data> --graders <graders.yaml>` | Active eval-driven optimization; `--write-back` rewrites the spec in place |
|
|
64
|
+
| `init [name]` | Scaffold a new `crewhaus.yaml` |
|
|
65
|
+
| `doctor` | Check environment health |
|
|
66
|
+
| `context --bundle` | Emit a single-markdown orientation manifest |
|
|
67
|
+
| `cost-summary --session <id>` | Summarize cost-accrual events for a session |
|
|
68
|
+
| `secrets doctor` / `secrets rotate <name>` | List or rotate secrets via the configured backend |
|
|
69
|
+
| `spec put\|list\|get\|pin\|alias …` | Versioned spec storage |
|
|
70
|
+
| `deploy promote\|rollback …` | Re-pin a spec for an environment |
|
|
71
|
+
| `migrate-all --from N --to N` | Batch-migrate every spec in the registry |
|
|
72
|
+
| `build-image <target> --tag <tag>` | Build the docker image for a target shape |
|
|
73
|
+
| `cloud deploy` / `cloud teardown` | Deploy or tear down a managed CrewHaus cluster |
|
|
74
|
+
| `federation discover <deployment>` | Resolve a federated peer's endpoint and cert fingerprint |
|
|
75
|
+
| `sandbox doctor` | List registered sandbox images and healthcheck status |
|
|
76
|
+
| `compliance evidence` | Collect SOC 2 / ISO 27001 / HIPAA evidence |
|
|
77
|
+
| `version` | Print the CLI version (also `--version`, `-v`) |
|
|
78
|
+
|
|
79
|
+
## Docs
|
|
80
|
+
|
|
81
|
+
- [Documentation](https://crewhaus.ai/docs) — start with the [quickstart](https://crewhaus.ai/docs/quickstart) and the [CLI target guide](https://crewhaus.ai/docs/targets/cli)
|
|
82
|
+
- [factory repository](https://github.com/crewhaus/factory) — architecture, contributing, and the full target-shape table
|
|
83
|
+
|
|
84
|
+
Apache-2.0.
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crewhaus",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CrewHaus — the meta-harness compiler for AI agents. Compile one crewhaus.yaml spec into a CLI agent, channel bot, RAG pipeline, multi-agent crew, eval harness, voice or browser agent, and more.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"crewhaus",
|
|
8
|
+
"ai",
|
|
9
|
+
"agent",
|
|
10
|
+
"agents",
|
|
11
|
+
"llm",
|
|
12
|
+
"compiler",
|
|
13
|
+
"meta-harness",
|
|
14
|
+
"cli",
|
|
15
|
+
"claude",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"rag",
|
|
18
|
+
"eval"
|
|
19
|
+
],
|
|
20
|
+
"main": "src/index.ts",
|
|
21
|
+
"bin": {
|
|
22
|
+
"crewhaus": "src/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.ts"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "bun test src"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@crewhaus/adapter-anthropic": "0.1.3",
|
|
32
|
+
"@crewhaus/agent-context-isolation": "0.1.3",
|
|
33
|
+
"@crewhaus/audit-log": "0.1.3",
|
|
34
|
+
"@crewhaus/eval-optimizer-orchestrator": "0.1.3",
|
|
35
|
+
"@crewhaus/prompt-optimizer": "0.1.3",
|
|
36
|
+
"@crewhaus/prompt-optimizer-claude": "0.1.3",
|
|
37
|
+
"@crewhaus/justification-judge-claude": "0.1.3",
|
|
38
|
+
"@crewhaus/spec-patch": "0.1.3",
|
|
39
|
+
"@crewhaus/canary-controller": "0.1.3",
|
|
40
|
+
"@crewhaus/compiler": "0.1.3",
|
|
41
|
+
"@crewhaus/computer-use-driver": "0.1.3",
|
|
42
|
+
"@crewhaus/context-bundle": "0.1.3",
|
|
43
|
+
"@crewhaus/deployment-controller": "0.1.3",
|
|
44
|
+
"@crewhaus/egress-classifier": "0.1.3",
|
|
45
|
+
"@crewhaus/egress-matcher-semantic": "0.1.3",
|
|
46
|
+
"@crewhaus/embedder": "0.1.3",
|
|
47
|
+
"@crewhaus/errors": "0.1.3",
|
|
48
|
+
"@crewhaus/eval-dataset": "0.1.3",
|
|
49
|
+
"@crewhaus/eval-grader": "0.1.3",
|
|
50
|
+
"@crewhaus/eval-report": "0.1.3",
|
|
51
|
+
"@crewhaus/eval-runner": "0.1.3",
|
|
52
|
+
"@crewhaus/hooks-engine": "0.1.3",
|
|
53
|
+
"@crewhaus/infra-utils": "0.1.3",
|
|
54
|
+
"@crewhaus/ir": "0.1.3",
|
|
55
|
+
"@crewhaus/logging": "0.1.3",
|
|
56
|
+
"@crewhaus/mcp-host": "0.1.3",
|
|
57
|
+
"@crewhaus/model-router": "0.1.3",
|
|
58
|
+
"@crewhaus/migration-engine": "0.1.3",
|
|
59
|
+
"@crewhaus/migration-runner": "0.1.3",
|
|
60
|
+
"@crewhaus/permission-engine": "0.1.3",
|
|
61
|
+
"@crewhaus/run-context": "0.1.3",
|
|
62
|
+
"@crewhaus/runtime-core": "0.1.3",
|
|
63
|
+
"@crewhaus/secrets-manager": "0.1.3",
|
|
64
|
+
"@crewhaus/session-store": "0.1.3",
|
|
65
|
+
"@crewhaus/spec-registry": "0.1.3",
|
|
66
|
+
"@crewhaus/skills-registry": "0.1.3",
|
|
67
|
+
"@crewhaus/slash-commands": "0.1.3",
|
|
68
|
+
"@crewhaus/spec": "0.1.3",
|
|
69
|
+
"@crewhaus/sub-agent-spawner": "0.1.3",
|
|
70
|
+
"@crewhaus/trace-event-bus": "0.1.3",
|
|
71
|
+
"@crewhaus/tool-bash": "0.1.3",
|
|
72
|
+
"@crewhaus/tool-builder": "0.1.3",
|
|
73
|
+
"@crewhaus/tool-catalog": "0.1.3",
|
|
74
|
+
"@crewhaus/tool-codegraph": "0.1.3",
|
|
75
|
+
"@crewhaus/tool-fetch": "0.1.3",
|
|
76
|
+
"@crewhaus/tool-fs": "0.1.3",
|
|
77
|
+
"@crewhaus/tool-image": "0.1.3",
|
|
78
|
+
"@crewhaus/tool-image-generation": "0.1.3",
|
|
79
|
+
"@crewhaus/tool-document-ingest": "0.1.3",
|
|
80
|
+
"@crewhaus/tool-mcp": "0.1.3",
|
|
81
|
+
"@crewhaus/tool-mouse-keyboard": "0.1.3",
|
|
82
|
+
"@crewhaus/tool-navigate": "0.1.3",
|
|
83
|
+
"@crewhaus/tool-screen-capture": "0.1.3",
|
|
84
|
+
"@crewhaus/tool-task": "0.1.3",
|
|
85
|
+
"@crewhaus/tool-todo": "0.1.3",
|
|
86
|
+
"@crewhaus/tool-vision-grounding": "0.1.3",
|
|
87
|
+
"@crewhaus/tool-web": "0.1.3",
|
|
88
|
+
"@crewhaus/docker-images": "0.1.3",
|
|
89
|
+
"@crewhaus/crewhaus-cloud": "0.1.3",
|
|
90
|
+
"@crewhaus/federation-discovery": "0.1.3",
|
|
91
|
+
"@crewhaus/sandbox": "0.1.3",
|
|
92
|
+
"@crewhaus/sandbox-image-registry": "0.1.3",
|
|
93
|
+
"@crewhaus/compliance-controls": "0.1.3"
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"zod": "^3.23.8"
|
|
97
|
+
},
|
|
98
|
+
"license": "Apache-2.0",
|
|
99
|
+
"author": {
|
|
100
|
+
"name": "Max Meier",
|
|
101
|
+
"email": "max@crewhaus.ai",
|
|
102
|
+
"url": "https://crewhaus.ai"
|
|
103
|
+
},
|
|
104
|
+
"repository": {
|
|
105
|
+
"type": "git",
|
|
106
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
107
|
+
"directory": "apps/cli"
|
|
108
|
+
},
|
|
109
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/apps/cli#readme",
|
|
110
|
+
"bugs": {
|
|
111
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
112
|
+
},
|
|
113
|
+
"publishConfig": {
|
|
114
|
+
"access": "public"
|
|
115
|
+
},
|
|
116
|
+
"files": ["src", "README.md", "LICENSE", "NOTICE"]
|
|
117
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildCredentialChecks, extractSpecModel, selectedProvider } from "./doctor-checks";
|
|
3
|
+
|
|
4
|
+
const yamlFor = (model: string): string =>
|
|
5
|
+
`name: t\ntarget: cli\nagent:\n model: ${model}\n instructions: hi\n`;
|
|
6
|
+
|
|
7
|
+
describe("extractSpecModel", () => {
|
|
8
|
+
test("extracts agent.model from a valid spec", () => {
|
|
9
|
+
expect(extractSpecModel(yamlFor("openai/gpt-4o-mini"))).toBe("openai/gpt-4o-mini");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("returns undefined for an unparseable spec (doctor must not crash)", () => {
|
|
13
|
+
expect(extractSpecModel("name: t\ntarget: cli\n")).toBeUndefined();
|
|
14
|
+
expect(extractSpecModel(":::not yaml at all")).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("selectedProvider", () => {
|
|
19
|
+
test("maps the full router grammar to doctor providers", () => {
|
|
20
|
+
expect(selectedProvider("claude-sonnet-4-5")).toBe("anthropic");
|
|
21
|
+
expect(selectedProvider("openai/gpt-4o-mini")).toBe("openai");
|
|
22
|
+
expect(selectedProvider("gemini/gemini-2.5-flash")).toBe("gemini");
|
|
23
|
+
expect(selectedProvider("bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0")).toBe(
|
|
24
|
+
"bedrock",
|
|
25
|
+
);
|
|
26
|
+
// local/<m>@<url> parses to openai WITH a baseUrl → credential-free.
|
|
27
|
+
expect(selectedProvider("local/llama3.2@http://localhost:11434/v1")).toBe("local");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("returns undefined for unrecognised model strings", () => {
|
|
31
|
+
expect(selectedProvider("gpt-4o-mini")).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("buildCredentialChecks — model-aware", () => {
|
|
36
|
+
test("openai spec + OPENAI_API_KEY passes; missing Anthropic creds do NOT fail", () => {
|
|
37
|
+
const checks = buildCredentialChecks("openai/gpt-4o-mini", { OPENAI_API_KEY: "sk-x" });
|
|
38
|
+
const selected = checks[0];
|
|
39
|
+
expect(selected?.label).toContain("OpenAI credentials");
|
|
40
|
+
expect(selected?.pass).toBe(true);
|
|
41
|
+
expect(checks.every((c) => c.pass)).toBe(true);
|
|
42
|
+
expect(checks.some((c) => c.label.includes("Anthropic") && !c.pass)).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("openai spec accepts OPENAI_BASE_URL as the credential surface", () => {
|
|
46
|
+
const checks = buildCredentialChecks("openai/gpt-4o-mini", {
|
|
47
|
+
OPENAI_BASE_URL: "http://vllm:8000/v1",
|
|
48
|
+
});
|
|
49
|
+
expect(checks[0]?.pass).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("openai spec without provider env FAILS the selected check", () => {
|
|
53
|
+
const checks = buildCredentialChecks("openai/gpt-4o-mini", {});
|
|
54
|
+
expect(checks[0]?.pass).toBe(false);
|
|
55
|
+
expect(checks[0]?.reason).toContain("OPENAI_API_KEY");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("gemini spec: GEMINI_API_KEY, GOOGLE_API_KEY, or Vertex env all satisfy", () => {
|
|
59
|
+
expect(buildCredentialChecks("gemini/gemini-2.5-flash", { GEMINI_API_KEY: "g" })[0]?.pass).toBe(
|
|
60
|
+
true,
|
|
61
|
+
);
|
|
62
|
+
expect(buildCredentialChecks("gemini/gemini-2.5-flash", { GOOGLE_API_KEY: "g" })[0]?.pass).toBe(
|
|
63
|
+
true,
|
|
64
|
+
);
|
|
65
|
+
expect(
|
|
66
|
+
buildCredentialChecks("gemini/gemini-2.5-flash", {
|
|
67
|
+
GOOGLE_GENAI_USE_VERTEXAI: "1",
|
|
68
|
+
GOOGLE_CLOUD_PROJECT: "p",
|
|
69
|
+
})[0]?.pass,
|
|
70
|
+
).toBe(true);
|
|
71
|
+
const missing = buildCredentialChecks("gemini/gemini-2.5-flash", {});
|
|
72
|
+
expect(missing[0]?.pass).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("bedrock spec is INFORMATIONAL — never fails even with no AWS env (SDK chain is authoritative)", () => {
|
|
76
|
+
const none = buildCredentialChecks("bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", {});
|
|
77
|
+
expect(none[0]?.pass).toBe(true);
|
|
78
|
+
expect(none[0]?.warn).toBe(true);
|
|
79
|
+
expect(none[0]?.reason).toContain("SDK default credential chain");
|
|
80
|
+
const some = buildCredentialChecks("bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0", {
|
|
81
|
+
AWS_PROFILE: "dev",
|
|
82
|
+
});
|
|
83
|
+
expect(some[0]?.pass).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("local spec needs no credentials", () => {
|
|
87
|
+
const checks = buildCredentialChecks("local/llama3.2@http://localhost:11434/v1", {});
|
|
88
|
+
expect(checks[0]?.pass).toBe(true);
|
|
89
|
+
expect(checks[0]?.label).toContain("Local endpoint");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("non-selected providers with env set surface as informational warn lines, not failures", () => {
|
|
93
|
+
const checks = buildCredentialChecks("openai/gpt-4o-mini", {
|
|
94
|
+
OPENAI_API_KEY: "sk-x",
|
|
95
|
+
ANTHROPIC_API_KEY: "sk-ant",
|
|
96
|
+
});
|
|
97
|
+
const anthropicLine = checks.find((c) => c.label.includes("Anthropic"));
|
|
98
|
+
expect(anthropicLine?.pass).toBe(true);
|
|
99
|
+
expect(anthropicLine?.warn).toBe(true);
|
|
100
|
+
expect(anthropicLine?.label).toContain("not required");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("buildCredentialChecks — no spec model (legacy fallback)", () => {
|
|
105
|
+
test("Anthropic creds present → plain pass (unchanged behaviour)", () => {
|
|
106
|
+
const checks = buildCredentialChecks(undefined, { ANTHROPIC_API_KEY: "sk-ant" });
|
|
107
|
+
expect(checks).toEqual([{ label: "Anthropic credentials", pass: true }]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("no creds at all → hard fail (unchanged behaviour)", () => {
|
|
111
|
+
const checks = buildCredentialChecks(undefined, {});
|
|
112
|
+
expect(checks[0]?.pass).toBe(false);
|
|
113
|
+
expect(checks[0]?.reason).toContain("ANTHROPIC_AUTH_TOKEN");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("missing Anthropic creds downgrade to informational when another provider env is clearly set", () => {
|
|
117
|
+
const checks = buildCredentialChecks(undefined, { OPENAI_API_KEY: "sk-x" });
|
|
118
|
+
expect(checks[0]?.pass).toBe(true);
|
|
119
|
+
expect(checks[0]?.warn).toBe(true);
|
|
120
|
+
expect(checks[0]?.reason).toContain("OpenAI");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { resolveAuth } from "@crewhaus/adapter-anthropic";
|
|
2
|
+
import { parseModelString } from "@crewhaus/model-router";
|
|
3
|
+
import { parseSpec } from "@crewhaus/spec";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Model-aware credential checks for `crewhaus doctor`, factored out of the
|
|
7
|
+
* entry file `index.ts` (which runs a top-level argv switch and so cannot be
|
|
8
|
+
* imported by a test without executing the CLI). Side-effect-free and
|
|
9
|
+
* directly unit-testable, mirroring `scope-audit.ts` / `justification-gate.ts`.
|
|
10
|
+
*
|
|
11
|
+
* The old doctor checked ONLY Anthropic credentials and exited 1 when they
|
|
12
|
+
* were absent — for every spec, including openai/gemini/bedrock/local ones
|
|
13
|
+
* that never touch Anthropic. The fix: parse the cwd spec's `agent.model`
|
|
14
|
+
* with the model-router grammar and check the matching provider's env,
|
|
15
|
+
* reporting the others informationally instead of failing on them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Doctor-level provider id: the router's four providers plus "local"
|
|
19
|
+
* (`local/<m>@<url>` parses to openai WITH a baseUrl — credential-free). */
|
|
20
|
+
export type DoctorProviderId = "anthropic" | "openai" | "gemini" | "bedrock" | "local";
|
|
21
|
+
|
|
22
|
+
export type DoctorCredentialCheck = {
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly pass: boolean;
|
|
25
|
+
/** warn+pass renders as "~" — informational, never fails doctor. */
|
|
26
|
+
readonly warn?: boolean;
|
|
27
|
+
readonly reason?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isSet(env: NodeJS.ProcessEnv, name: string): boolean {
|
|
31
|
+
const v = env[name];
|
|
32
|
+
return typeof v === "string" && v !== "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tolerantly extract `agent.model` from a crewhaus.yaml text. Returns
|
|
37
|
+
* undefined when the spec doesn't parse or carries no agent.model (e.g.
|
|
38
|
+
* workflow shapes with per-step models) — the caller then falls back to
|
|
39
|
+
* the legacy Anthropic-first behaviour.
|
|
40
|
+
*/
|
|
41
|
+
export function extractSpecModel(yamlText: string): string | undefined {
|
|
42
|
+
try {
|
|
43
|
+
const spec = parseSpec(yamlText) as unknown as { agent?: { model?: unknown } };
|
|
44
|
+
const m = spec.agent?.model;
|
|
45
|
+
return typeof m === "string" && m.length > 0 ? m : undefined;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Map a model string to the doctor-level provider whose credentials the
|
|
53
|
+
* run will need. Returns undefined for unparseable strings.
|
|
54
|
+
*/
|
|
55
|
+
export function selectedProvider(model: string): DoctorProviderId | undefined {
|
|
56
|
+
try {
|
|
57
|
+
const parsed = parseModelString(model);
|
|
58
|
+
if (parsed.providerId === "openai" && parsed.baseUrl !== undefined) return "local";
|
|
59
|
+
return parsed.providerId;
|
|
60
|
+
} catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type EnvStatus = { readonly satisfied: boolean; readonly detail: string };
|
|
66
|
+
|
|
67
|
+
function anthropicStatus(env: NodeJS.ProcessEnv): EnvStatus {
|
|
68
|
+
const auth = resolveAuth(env);
|
|
69
|
+
return auth.mode !== "none"
|
|
70
|
+
? { satisfied: true, detail: `${auth.mode} credentials found` }
|
|
71
|
+
: {
|
|
72
|
+
satisfied: false,
|
|
73
|
+
detail: "set ANTHROPIC_AUTH_TOKEN (Claude subscription) or ANTHROPIC_API_KEY",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function openaiStatus(env: NodeJS.ProcessEnv): EnvStatus {
|
|
78
|
+
if (isSet(env, "OPENAI_API_KEY")) return { satisfied: true, detail: "OPENAI_API_KEY set" };
|
|
79
|
+
if (isSet(env, "OPENAI_BASE_URL")) {
|
|
80
|
+
return { satisfied: true, detail: "OPENAI_BASE_URL set (OpenAI-compatible endpoint)" };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
satisfied: false,
|
|
84
|
+
detail: "set OPENAI_API_KEY (or OPENAI_BASE_URL for an OpenAI-compatible endpoint)",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function geminiStatus(env: NodeJS.ProcessEnv): EnvStatus {
|
|
89
|
+
if (isSet(env, "GEMINI_API_KEY")) return { satisfied: true, detail: "GEMINI_API_KEY set" };
|
|
90
|
+
if (isSet(env, "GOOGLE_API_KEY")) return { satisfied: true, detail: "GOOGLE_API_KEY set" };
|
|
91
|
+
if (isSet(env, "GOOGLE_GENAI_USE_VERTEXAI") || isSet(env, "GOOGLE_CLOUD_PROJECT")) {
|
|
92
|
+
return {
|
|
93
|
+
satisfied: true,
|
|
94
|
+
detail: "Vertex AI env set (GOOGLE_GENAI_USE_VERTEXAI/GOOGLE_CLOUD_PROJECT)",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
satisfied: false,
|
|
99
|
+
detail:
|
|
100
|
+
"set GEMINI_API_KEY (or GOOGLE_API_KEY; Vertex users set GOOGLE_GENAI_USE_VERTEXAI + GOOGLE_CLOUD_PROJECT)",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const BEDROCK_ENV_NAMES = [
|
|
105
|
+
"AWS_BEARER_TOKEN_BEDROCK",
|
|
106
|
+
"AWS_ACCESS_KEY_ID",
|
|
107
|
+
"AWS_PROFILE",
|
|
108
|
+
"AWS_REGION",
|
|
109
|
+
] as const;
|
|
110
|
+
|
|
111
|
+
function bedrockStatus(env: NodeJS.ProcessEnv): EnvStatus {
|
|
112
|
+
const found = BEDROCK_ENV_NAMES.filter((n) => isSet(env, n));
|
|
113
|
+
return found.length > 0
|
|
114
|
+
? { satisfied: true, detail: `${found.join(", ")} set` }
|
|
115
|
+
: {
|
|
116
|
+
satisfied: false,
|
|
117
|
+
detail:
|
|
118
|
+
"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",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function statusFor(provider: DoctorProviderId, env: NodeJS.ProcessEnv): EnvStatus {
|
|
123
|
+
switch (provider) {
|
|
124
|
+
case "anthropic":
|
|
125
|
+
return anthropicStatus(env);
|
|
126
|
+
case "openai":
|
|
127
|
+
return openaiStatus(env);
|
|
128
|
+
case "gemini":
|
|
129
|
+
return geminiStatus(env);
|
|
130
|
+
case "bedrock":
|
|
131
|
+
return bedrockStatus(env);
|
|
132
|
+
case "local":
|
|
133
|
+
return {
|
|
134
|
+
satisfied: true,
|
|
135
|
+
detail: "local endpoint baked into the model string — no credentials needed",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PROVIDER_LABEL: Record<DoctorProviderId, string> = {
|
|
141
|
+
anthropic: "Anthropic credentials",
|
|
142
|
+
openai: "OpenAI credentials",
|
|
143
|
+
gemini: "Gemini credentials",
|
|
144
|
+
bedrock: "Bedrock (AWS) credentials",
|
|
145
|
+
local: "Local endpoint",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Providers whose env check is INFORMATIONAL even when selected: the AWS
|
|
149
|
+
* SDK's default credential chain is authoritative for bedrock (env vars are
|
|
150
|
+
* only one of its sources), and local/ endpoints need no credentials. */
|
|
151
|
+
const INFORMATIONAL_WHEN_SELECTED: ReadonlySet<DoctorProviderId> = new Set(["bedrock", "local"]);
|
|
152
|
+
|
|
153
|
+
/** Non-selected providers whose env is set get an informational line so the
|
|
154
|
+
* operator sees what else is configured. */
|
|
155
|
+
const ALL_PROVIDERS: readonly DoctorProviderId[] = ["anthropic", "openai", "gemini", "bedrock"];
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build the credential section of `crewhaus doctor`.
|
|
159
|
+
*
|
|
160
|
+
* With a parseable spec model: the matching provider gets a real pass/fail
|
|
161
|
+
* check (informational for bedrock/local), and any OTHER provider with env
|
|
162
|
+
* visibly set gets a "~" informational line. Without one (no spec file, or
|
|
163
|
+
* no agent.model): legacy Anthropic-first behaviour, EXCEPT that a missing
|
|
164
|
+
* Anthropic credential downgrades to informational when another provider's
|
|
165
|
+
* env is clearly set (the operator is plainly not on Anthropic).
|
|
166
|
+
*/
|
|
167
|
+
export function buildCredentialChecks(
|
|
168
|
+
specModel: string | undefined,
|
|
169
|
+
env: NodeJS.ProcessEnv,
|
|
170
|
+
): DoctorCredentialCheck[] {
|
|
171
|
+
const provider = specModel !== undefined ? selectedProvider(specModel) : undefined;
|
|
172
|
+
const checks: DoctorCredentialCheck[] = [];
|
|
173
|
+
|
|
174
|
+
if (provider !== undefined && specModel !== undefined) {
|
|
175
|
+
const status = statusFor(provider, env);
|
|
176
|
+
const informational = INFORMATIONAL_WHEN_SELECTED.has(provider);
|
|
177
|
+
checks.push({
|
|
178
|
+
label: `${PROVIDER_LABEL[provider]} (model ${specModel})`,
|
|
179
|
+
pass: informational ? true : status.satisfied,
|
|
180
|
+
...(informational && !status.satisfied ? { warn: true } : {}),
|
|
181
|
+
...(status.satisfied && !informational ? {} : { reason: status.detail }),
|
|
182
|
+
});
|
|
183
|
+
for (const other of ALL_PROVIDERS) {
|
|
184
|
+
if (other === provider) continue;
|
|
185
|
+
const otherStatus = statusFor(other, env);
|
|
186
|
+
if (otherStatus.satisfied) {
|
|
187
|
+
checks.push({
|
|
188
|
+
label: `${PROVIDER_LABEL[other]} (not required for this spec)`,
|
|
189
|
+
pass: true,
|
|
190
|
+
warn: true,
|
|
191
|
+
reason: otherStatus.detail,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return checks;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// No spec model — legacy Anthropic-first behaviour with one softening:
|
|
199
|
+
// when Anthropic creds are absent but another provider's env is clearly
|
|
200
|
+
// set, report informationally instead of hard-failing.
|
|
201
|
+
const anthropic = anthropicStatus(env);
|
|
202
|
+
if (anthropic.satisfied) {
|
|
203
|
+
checks.push({ label: "Anthropic credentials", pass: true });
|
|
204
|
+
return checks;
|
|
205
|
+
}
|
|
206
|
+
const others = ALL_PROVIDERS.filter((p) => p !== "anthropic").filter(
|
|
207
|
+
(p) => statusFor(p, env).satisfied,
|
|
208
|
+
);
|
|
209
|
+
if (others.length > 0) {
|
|
210
|
+
checks.push({
|
|
211
|
+
label: "Anthropic credentials",
|
|
212
|
+
pass: true,
|
|
213
|
+
warn: true,
|
|
214
|
+
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`,
|
|
215
|
+
});
|
|
216
|
+
return checks;
|
|
217
|
+
}
|
|
218
|
+
checks.push({ label: "Anthropic credentials", pass: false, reason: anthropic.detail });
|
|
219
|
+
return checks;
|
|
220
|
+
}
|