crosscheck-cli 0.0.63 → 0.0.65
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/dist/activate.d.ts +55 -0
- package/dist/activate.js +8 -2
- package/dist/activate.js.map +1 -1
- package/dist/cli-enterprise.d.ts +11 -0
- package/dist/cli.d.ts +8 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.js +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/engine-version.d.ts +27 -0
- package/dist/enterprise/compliance.d.ts +20 -0
- package/dist/enterprise/doctor.d.ts +17 -0
- package/dist/enterprise/install.d.ts +13 -0
- package/dist/enterprise/keys-setup.d.ts +8 -0
- package/dist/enterprise/login.d.ts +8 -0
- package/dist/enterprise/policy.d.ts +26 -0
- package/dist/explain.d.ts +38 -0
- package/dist/fingerprint.d.ts +31 -0
- package/dist/heartbeat.d.ts +32 -0
- package/dist/http.d.ts +33 -0
- package/dist/install.d.ts +19 -0
- package/dist/jwt.d.ts +35 -0
- package/dist/key-format.d.ts +40 -0
- package/dist/key-status-signing.d.ts +25 -0
- package/dist/key-status.d.ts +61 -0
- package/dist/key-verify.d.ts +31 -0
- package/dist/keychain.d.ts +120 -0
- package/dist/keys-browser-flow.d.ts +68 -0
- package/dist/keys-manage-cli.d.ts +11 -0
- package/dist/keys-session.d.ts +89 -0
- package/dist/keys-setup.d.ts +38 -0
- package/dist/lib.d.ts +45 -0
- package/dist/lib.js +57 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-register.d.ts +49 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/model-policy.d.ts +44 -0
- package/dist/model-prefs.d.ts +104 -0
- package/dist/model-resolve.d.ts +61 -0
- package/dist/models-cli.d.ts +11 -0
- package/dist/onepassword/browser-flow.d.ts +35 -0
- package/dist/onepassword/config.d.ts +53 -0
- package/dist/onepassword/op.d.ts +54 -0
- package/dist/onepassword/reference.d.ts +29 -0
- package/dist/onepassword/setup.d.ts +54 -0
- package/dist/open-browser.d.ts +6 -0
- package/dist/otel-setup.d.ts +69 -0
- package/dist/pref-conflicts.d.ts +36 -0
- package/dist/reconcile.d.ts +59 -0
- package/dist/signing.d.ts +48 -0
- package/dist/single-instance.d.ts +97 -0
- package/dist/telemetry.d.ts +25 -0
- package/dist/update-check.d.ts +17 -0
- package/package.json +14 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report which providers this machine has keys for — and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS: keys are per-machine and never leave the machine, which is
|
|
5
|
+
* the right security posture but leaves the browser blind. Open /account on
|
|
6
|
+
* your laptop and it cannot tell you that your desktop lost its OpenAI key
|
|
7
|
+
* two days ago, because it has no idea the desktop ever had one. Every
|
|
8
|
+
* machine but the one you are sitting at is a black hole. This closes that
|
|
9
|
+
* with metadata, without weakening the posture at all.
|
|
10
|
+
*
|
|
11
|
+
* WHAT LEAVES THIS MACHINE, exhaustively:
|
|
12
|
+
* provider name · a key is present yes/no · the last 4 characters ·
|
|
13
|
+
* how it last behaved · when that was
|
|
14
|
+
*
|
|
15
|
+
* The last-4 is the only part derived from the key itself, and it exists for
|
|
16
|
+
* one reason: so a human can tell whether the key on the dashboard is the one
|
|
17
|
+
* they think it is before replacing it. `suffix4()` is the ONLY place in this
|
|
18
|
+
* file that touches key material, it is four characters wide by construction,
|
|
19
|
+
* and the server's schema independently rejects anything longer. Two
|
|
20
|
+
* independent bounds, because a single one is a single edit away from gone.
|
|
21
|
+
*
|
|
22
|
+
* Reporting is opt-OUT via CROSSCHECK_NO_KEY_STATUS=1. An org that considers
|
|
23
|
+
* provider coverage sensitive can turn it off and lose only the dashboard.
|
|
24
|
+
*/
|
|
25
|
+
/** How a key last behaved. Mirrors cb_key_result on the server. */
|
|
26
|
+
export type KeyResult = "ok" | "invalid_key" | "quota_billing" | "network" | "provider_down" | "unknown";
|
|
27
|
+
export type KeyStatusItem = {
|
|
28
|
+
provider: string;
|
|
29
|
+
present: boolean;
|
|
30
|
+
suffix?: string;
|
|
31
|
+
lastResult?: KeyResult;
|
|
32
|
+
lastCheckedAt?: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The last four characters, or nothing.
|
|
36
|
+
*
|
|
37
|
+
* Short keys return undefined rather than a padded or partial value: a key
|
|
38
|
+
* under 8 characters is not a real key, and reporting a suffix of a 5-char
|
|
39
|
+
* string would expose most of it. The threshold is deliberately well above 4.
|
|
40
|
+
*/
|
|
41
|
+
export declare function suffix4(key: string | null | undefined): string | undefined;
|
|
42
|
+
export declare function keyStatusDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Read the local store and describe it. Absent providers are reported
|
|
45
|
+
* explicitly as `present: false` rather than omitted, because the server
|
|
46
|
+
* treats a report as a full replacement of the seat's provider set and
|
|
47
|
+
* "missing from the list" has to mean "no longer configured".
|
|
48
|
+
*/
|
|
49
|
+
export declare function collectKeyStatus(): Promise<KeyStatusItem[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Send the report. Best-effort and silent: this runs on the MCP startup path,
|
|
52
|
+
* where a network problem must cost a stale dashboard row and never a failed
|
|
53
|
+
* connection.
|
|
54
|
+
*/
|
|
55
|
+
export declare function reportKeyStatus(args: {
|
|
56
|
+
seatId: string;
|
|
57
|
+
orgId: string;
|
|
58
|
+
jwt: string;
|
|
59
|
+
serverUrl?: string;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
}): Promise<"sent" | "skipped" | "failed">;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Does this key actually work?" — answered by asking the provider, cheaply.
|
|
3
|
+
*
|
|
4
|
+
* Every probe here is a model-list GET: no completion, no tokens, no cost.
|
|
5
|
+
* That matters, because a verify button that quietly bills you is a verify
|
|
6
|
+
* button people learn not to press.
|
|
7
|
+
*
|
|
8
|
+
* THE DISTINCTION THIS FILE EXISTS FOR: "we could not reach the provider" and
|
|
9
|
+
* "the provider rejected your key" are completely different problems with
|
|
10
|
+
* completely different fixes, and they arrive looking similar. Collapsing
|
|
11
|
+
* them sends someone to regenerate a key because their wifi dropped. So the
|
|
12
|
+
* default for anything ambiguous is `network` or `unknown` — never
|
|
13
|
+
* `invalid_key`. We accuse a key only when a provider actually refused it.
|
|
14
|
+
*/
|
|
15
|
+
/** Mirrors cb_key_result on the server and KeyResult in key-status.ts. */
|
|
16
|
+
export type VerifyResult = "ok" | "invalid_key" | "quota_billing" | "network" | "provider_down" | "unknown";
|
|
17
|
+
export type VerifyOutcome = {
|
|
18
|
+
result: VerifyResult;
|
|
19
|
+
/** One line, safe to show a user. NEVER contains the key. */
|
|
20
|
+
detail: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function canVerify(provider: string): boolean;
|
|
23
|
+
export declare function classifyStatus(status: number, body: string): VerifyOutcome;
|
|
24
|
+
/**
|
|
25
|
+
* Probe one provider. `fetchImpl` is injectable so the classification can be
|
|
26
|
+
* tested without touching the network.
|
|
27
|
+
*/
|
|
28
|
+
export declare function verifyKey(provider: string, key: string, opts?: {
|
|
29
|
+
fetchImpl?: typeof fetch;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}): Promise<VerifyOutcome>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret store — NO native dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the former `@napi-rs/keyring` backend, whose per-platform native
|
|
5
|
+
* binary kept corrupting on `npx` installs (truncated `.node` → dlopen
|
|
6
|
+
* failure; npm/cli#4828). The public API is unchanged so call sites don't move.
|
|
7
|
+
*
|
|
8
|
+
* Three backends:
|
|
9
|
+
* - "file" (DEFAULT, recommended): an encrypted file at <dir>/secrets.enc
|
|
10
|
+
* (AES-256-GCM) keyed by a random 32-byte key at <dir>/secret.key
|
|
11
|
+
* (mode 0400); data file 0600, dir 0700. Pure Node `crypto`, ZERO password
|
|
12
|
+
* prompts. <dir> defaults to ~/.crosscheck — in your home directory, never
|
|
13
|
+
* inside the repo you're working in, never committed to git. Threat model:
|
|
14
|
+
* protects against casual file copying / sync, not a process already
|
|
15
|
+
* running as you (same as gh / aws / .env).
|
|
16
|
+
* - "keychain" (opt-in, macOS only): shells out to `/usr/bin/security`. More
|
|
17
|
+
* OS-integrated, but macOS prompts for your LOGIN PASSWORD on every write —
|
|
18
|
+
* so a fresh setup asks several times. Not the default for that reason.
|
|
19
|
+
* Every spawn is hard-timeout-bounded with captured stdio, so a Keychain
|
|
20
|
+
* GUI prompt can never block a non-interactive `serve` (a timeout is
|
|
21
|
+
* treated as "not found" — we degrade, never hang).
|
|
22
|
+
* - "1password" (opt-in, enterprise): routes ONLY "key:<provider>" lookups
|
|
23
|
+
* that have an entry in ~/.crosscheck/1password.json to the 1Password
|
|
24
|
+
* backend (see src/onepassword/); everything else — the activation JWT,
|
|
25
|
+
* the HMAC signing key, installUuid, and any provider NOT listed in that
|
|
26
|
+
* config — still uses the file store, always. 1Password can't hold a
|
|
27
|
+
* machine-local secret and was never asked to; this is explicit routing
|
|
28
|
+
* by secret class, not a fallback. Read-only: writes to a 1Password-
|
|
29
|
+
* mapped provider throw ReadOnlyBackendError. A configured-but-failing
|
|
30
|
+
* 1Password read throws OnePasswordReadError rather than silently
|
|
31
|
+
* reading a stale value from the local file store — see
|
|
32
|
+
* docs/onepassword-security.md for the full design.
|
|
33
|
+
*
|
|
34
|
+
* Selection precedence:
|
|
35
|
+
* 1. CROSSCHECK_SECRET_STORE = file | keychain | 1password (env override)
|
|
36
|
+
* 2. saved preference in <dir>/store.json (set by `keys store` / `keys setup --backend 1password`)
|
|
37
|
+
* 3. "file" (default)
|
|
38
|
+
*
|
|
39
|
+
* Migration: when the file backend is active and no file store exists yet, we
|
|
40
|
+
* transparently import any existing macOS-keychain items once (those reads are
|
|
41
|
+
* prompt-free), so upgrading from a keychain build never strands a user.
|
|
42
|
+
*
|
|
43
|
+
* Keys never leave the machine, except when the 1password backend is active
|
|
44
|
+
* and a provider is explicitly mapped to it — that key never touches
|
|
45
|
+
* crosscheck's own storage at all, encrypted or not.
|
|
46
|
+
*/
|
|
47
|
+
import { type OpReadErrorKind } from "./onepassword/op.js";
|
|
48
|
+
/**
|
|
49
|
+
* "1password-only" is crosscheck-enterprise's mode: unlike "1password"
|
|
50
|
+
* (opt-in per provider, unmapped providers silently fall back to the file
|
|
51
|
+
* store), it closes that fallback specifically for provider keys — an
|
|
52
|
+
* unmapped provider throws instead of reading/writing the file store.
|
|
53
|
+
* Machine-local secrets (jwt, hmacSigningKey, etc.) are unaffected either
|
|
54
|
+
* way; see onePasswordProviderFor's null-for-non-"key:"-names behavior.
|
|
55
|
+
*/
|
|
56
|
+
export type Backend = "keychain" | "file" | "1password" | "1password-only";
|
|
57
|
+
export declare function dataDir(): string;
|
|
58
|
+
/** Backend label for `crosscheck doctor`. */
|
|
59
|
+
export declare function secretBackend(): Backend;
|
|
60
|
+
/** Where the encrypted file store lives (shown by `doctor`). */
|
|
61
|
+
export declare function fileStorePath(): string;
|
|
62
|
+
/** Thrown by setSecret/deleteSecret for a "key:<provider>" that's mapped to
|
|
63
|
+
* the 1Password backend — it's read-only in this version. */
|
|
64
|
+
export declare class ReadOnlyBackendError extends Error {
|
|
65
|
+
constructor(name: string);
|
|
66
|
+
}
|
|
67
|
+
/** Thrown by getSecret when a provider IS mapped to 1Password but the read
|
|
68
|
+
* failed — never silently falls back to the local file store instead,
|
|
69
|
+
* which would defeat the point of an enterprise-mandated backend and could
|
|
70
|
+
* resurrect a stale or already-rotated key. */
|
|
71
|
+
export declare class OnePasswordReadError extends Error {
|
|
72
|
+
readonly provider: string;
|
|
73
|
+
readonly code: OpReadErrorKind;
|
|
74
|
+
constructor(provider: string, code: OpReadErrorKind, detail: string);
|
|
75
|
+
}
|
|
76
|
+
/** Thrown by getSecret/setSecret/deleteSecret in "1password-only" mode for a
|
|
77
|
+
* "key:<provider>" that has no 1Password mapping — the enterprise-mode
|
|
78
|
+
* equivalent of ReadOnlyBackendError, but for the CLOSED fallback rather
|
|
79
|
+
* than a read-only mapped entry. */
|
|
80
|
+
export declare class NotMappedInEnterpriseModeError extends Error {
|
|
81
|
+
constructor(provider: string);
|
|
82
|
+
}
|
|
83
|
+
export declare function setSecret(name: string, value: string): Promise<void>;
|
|
84
|
+
export declare function getSecret(name: string): Promise<string | null>;
|
|
85
|
+
export declare function deleteSecret(name: string): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* List every Crosscheck-owned credential. Used by `crosscheck keys list` and
|
|
88
|
+
* by deactivation to wipe everything in one pass.
|
|
89
|
+
*
|
|
90
|
+
* For a 1Password-mapped provider this NEVER performs an actual 1Password
|
|
91
|
+
* read: doing so here would mean every `listSecrets()` call (doctor, `keys
|
|
92
|
+
* list`, deactivation) silently fans out into one `op` invocation per
|
|
93
|
+
* mapped provider — in desktop mode, that's N sequential GUI unlock
|
|
94
|
+
* prompts for an operation the user didn't ask to unlock anything for. The
|
|
95
|
+
* placeholder marker reports "configured," never the resolved value.
|
|
96
|
+
*/
|
|
97
|
+
export declare const ONEPASSWORD_LIST_PLACEHOLDER = "(configured via 1Password \u2014 value not read)";
|
|
98
|
+
export declare function listSecrets(): Promise<Array<{
|
|
99
|
+
account: string;
|
|
100
|
+
password: string;
|
|
101
|
+
}>>;
|
|
102
|
+
/**
|
|
103
|
+
* Switch the secret store backend and migrate existing secrets into it.
|
|
104
|
+
* Persists the choice so all future invocations use it. Returns how many
|
|
105
|
+
* credentials were migrated. NOTE: migrating *to* keychain triggers a macOS
|
|
106
|
+
* login-password prompt per credential — callers should warn first.
|
|
107
|
+
*/
|
|
108
|
+
/**
|
|
109
|
+
* Persist "1password" as the active backend WITHOUT setSecretBackend()'s
|
|
110
|
+
* copy-existing-secrets-in behavior, which is meaningless for a read-only,
|
|
111
|
+
* config-driven backend. Callers (`keys setup --backend 1password`) must
|
|
112
|
+
* validate config + connectivity themselves FIRST — this only flips the
|
|
113
|
+
* switch once that validation already succeeded.
|
|
114
|
+
*/
|
|
115
|
+
export declare function enableOnePasswordBackend(): void;
|
|
116
|
+
/** crosscheck-enterprise only — see the Backend type's "1password-only" doc. */
|
|
117
|
+
export declare function enableOnePasswordOnlyBackend(): void;
|
|
118
|
+
export declare function setSecretBackend(target: Backend): Promise<{
|
|
119
|
+
migrated: number;
|
|
120
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-paste flow for upstream LLM keys. Same trust pattern as
|
|
3
|
+
* `gh auth login`: the CLI spins up a localhost HTTP server, opens a
|
|
4
|
+
* Crosscheck (XC)-served form in the user's browser, and the form POSTs the
|
|
5
|
+
* pasted key directly back to localhost. Crosscheck's servers never
|
|
6
|
+
* see the value.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: pasting a 200-character API key into a terminal is
|
|
9
|
+
* (a) easy to screw up (newlines, shell history), (b) reveals the value
|
|
10
|
+
* on-screen, and (c) culturally foreign to non-CLI-native users. The
|
|
11
|
+
* browser form is masked-input, has Cmd-V "Paste from Clipboard"
|
|
12
|
+
* affordances built in, and is what every developer is used to from
|
|
13
|
+
* console.anthropic.com etc.
|
|
14
|
+
*
|
|
15
|
+
* Security properties:
|
|
16
|
+
* - State token (32 bytes urlsafe) bound at server-start. Mismatched
|
|
17
|
+
* state → CLI rejects the POST.
|
|
18
|
+
* - Loopback only — server binds to 127.0.0.1 (NOT 0.0.0.0). Other
|
|
19
|
+
* machines on the LAN can't hit it.
|
|
20
|
+
* - Browser → CLI path is HTTP, but it's loopback, so it never crosses
|
|
21
|
+
* the network. Modern browsers special-case localhost CORS.
|
|
22
|
+
* - 5-minute timeout. Server self-terminates after the value lands or
|
|
23
|
+
* the deadline elapses.
|
|
24
|
+
* - Server accepts exactly one valid POST. Subsequent calls 410.
|
|
25
|
+
* - Provider list pinned at compile time on both sides (server + CLI)
|
|
26
|
+
* so an attacker-controlled query string can't store a key under a
|
|
27
|
+
* made-up provider name.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SUPPORTED_PROVIDERS: Set<string>;
|
|
30
|
+
export type CapturedKey = {
|
|
31
|
+
provider: string;
|
|
32
|
+
value: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Run the browser flow. Returns the captured key on success; throws on
|
|
36
|
+
* timeout, invalid state, or server error.
|
|
37
|
+
*/
|
|
38
|
+
export declare function captureKeyViaBrowser(args: {
|
|
39
|
+
provider: string;
|
|
40
|
+
/**
|
|
41
|
+
* Optional override for the page URL — useful for local dev / preview
|
|
42
|
+
* deployments. Defaults to ${SERVER_URL}/cli/keys (crosscheckagent.com).
|
|
43
|
+
*/
|
|
44
|
+
pageBaseUrl?: string;
|
|
45
|
+
}): Promise<CapturedKey>;
|
|
46
|
+
/**
|
|
47
|
+
* Batch variant: collect keys for MULTIPLE providers in ONE browser page.
|
|
48
|
+
* The CLI opens /cli/keys?providers=a,b,c&state&callback; the page POSTs a
|
|
49
|
+
* single { state, keys: { provider: value } } to the loopback /keys endpoint.
|
|
50
|
+
* Returns the captured (non-empty) keys map. Used by `keys setup` / `install`
|
|
51
|
+
* so there is zero interactive terminal input. Keys never transit our backend.
|
|
52
|
+
*/
|
|
53
|
+
export declare function captureKeysViaBrowser(args: {
|
|
54
|
+
providers: string[];
|
|
55
|
+
pageBaseUrl?: string;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
/** When false, the CLI does NOT open /cli/keys itself — the keys form is
|
|
58
|
+
* hosted elsewhere (e.g. /cli/start) and posts to this same loopback. */
|
|
59
|
+
openPage?: boolean;
|
|
60
|
+
/** Invoked once the loopback is listening, with its callback URL + state, so
|
|
61
|
+
* the caller can route the user to a page that posts keys back here (e.g.
|
|
62
|
+
* run device-auth opening /cli/start with these params). The keys timeout
|
|
63
|
+
* starts after this resolves. */
|
|
64
|
+
onReady?: (info: {
|
|
65
|
+
callback: string;
|
|
66
|
+
state: string;
|
|
67
|
+
}) => Promise<void> | void;
|
|
68
|
+
}): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `crosscheck keys manage` — open a live key-management session in the browser.
|
|
3
|
+
*
|
|
4
|
+
* Runs in the foreground on purpose. The session is a capability over your
|
|
5
|
+
* local key store, and a capability should be visible while it exists: the
|
|
6
|
+
* terminal shows what's happening, and closing it closes the session. A
|
|
7
|
+
* background daemon quietly holding this open would be the wrong shape.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runKeysManage(opts?: {
|
|
10
|
+
open?: boolean;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A live, authenticated key-management session on this machine.
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS CHANGES. Key management used to be one-shot: `crosscheck keys`
|
|
5
|
+
* opened a page, the page POSTed one batch, the CLI applied it and exited.
|
|
6
|
+
* You could not sit in your account page and manage keys, and you could not
|
|
7
|
+
* see what a change did. This turns that into a session the browser can drive
|
|
8
|
+
* for as long as it's open — which is what makes /account a place you return
|
|
9
|
+
* to rather than a form you submit.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IT DOES NOT CHANGE: keys still never reach our servers. The browser
|
|
12
|
+
* talks straight to 127.0.0.1. Everything below exists to make that channel
|
|
13
|
+
* safe to leave open for ten minutes instead of ten seconds.
|
|
14
|
+
*
|
|
15
|
+
* THE TOKEN NEVER TOUCHES OUR SERVER. It is delivered to the page in the URL
|
|
16
|
+
* *fragment* (`#xc=port.token`), and fragments are not sent in HTTP requests.
|
|
17
|
+
* So crosscheckagent.com — and its logs, and its proxies, and anyone who ever
|
|
18
|
+
* breaches it — cannot obtain the capability to manage keys on your machine.
|
|
19
|
+
* This is deliberately stricter than the design called for: registering the
|
|
20
|
+
* token server-side would have enabled a nicer "click a button in the browser
|
|
21
|
+
* first" flow, at the cost of making our server able to reach into your key
|
|
22
|
+
* store. That trade wasn't worth making, so the flow starts in the terminal.
|
|
23
|
+
*
|
|
24
|
+
* Defence in depth, each layer assuming the others failed:
|
|
25
|
+
* - binds 127.0.0.1 only, never 0.0.0.0 — nothing on the LAN can see it
|
|
26
|
+
* - every request needs the bearer token, compared in constant time
|
|
27
|
+
* - CORS locked to our origin, so a hostile page can't read responses
|
|
28
|
+
* - Private Network Access preflight answered explicitly (see below)
|
|
29
|
+
* - idle TTL of 10 minutes, hard cap of 60, so a forgotten tab isn't
|
|
30
|
+
* an indefinitely open door
|
|
31
|
+
* - responses carry the last 4 characters of a key and never more, so a
|
|
32
|
+
* bug in this file cannot turn into a key disclosure
|
|
33
|
+
*/
|
|
34
|
+
import { type VerifyResult } from "./key-verify.js";
|
|
35
|
+
/** Idle timeout. Every authenticated request pushes it out. */
|
|
36
|
+
export declare const IDLE_TTL_MS: number;
|
|
37
|
+
/** Absolute ceiling, regardless of activity. A session left open all day is
|
|
38
|
+
* not a session, it's a hole. */
|
|
39
|
+
export declare const MAX_SESSION_MS: number;
|
|
40
|
+
export type ProviderState = {
|
|
41
|
+
provider: string;
|
|
42
|
+
label: string;
|
|
43
|
+
present: boolean;
|
|
44
|
+
suffix?: string;
|
|
45
|
+
lastResult?: VerifyResult;
|
|
46
|
+
lastCheckedAt?: string;
|
|
47
|
+
canVerify: boolean;
|
|
48
|
+
};
|
|
49
|
+
/** In-memory record of verifies done this session, so the page can show the
|
|
50
|
+
* result of the button the user just pressed. Not persisted: a verify is a
|
|
51
|
+
* point-in-time fact and pretending otherwise is how "healthy" badges lie. */
|
|
52
|
+
type VerifyMemo = Map<string, {
|
|
53
|
+
result: VerifyResult;
|
|
54
|
+
at: string;
|
|
55
|
+
}>;
|
|
56
|
+
export declare function isManageableProvider(name: string): boolean;
|
|
57
|
+
export declare function readProviderStates(memo: VerifyMemo): Promise<ProviderState[]>;
|
|
58
|
+
/**
|
|
59
|
+
* CORS, plus the header that stops Chromium blocking this outright.
|
|
60
|
+
*
|
|
61
|
+
* Chromium's Private Network Access treats a request from a public HTTPS page
|
|
62
|
+
* to 127.0.0.1 as a privilege escalation and sends a preflight carrying
|
|
63
|
+
* `Access-Control-Request-Private-Network: true`. If the listener doesn't
|
|
64
|
+
* answer `Access-Control-Allow-Private-Network: true`, the request is blocked
|
|
65
|
+
* with no visible error — the fetch just fails, and the page has no way to
|
|
66
|
+
* tell that apart from "the CLI isn't running". Cheap to send, and it is the
|
|
67
|
+
* difference between this feature working and mysteriously not.
|
|
68
|
+
*/
|
|
69
|
+
export declare function sessionCorsHeaders(origin?: string): Record<string, string>;
|
|
70
|
+
export declare function tokensMatch(expected: string, provided: string | undefined): boolean;
|
|
71
|
+
export declare function bearerFrom(header: string | undefined): string | undefined;
|
|
72
|
+
export type SessionHandle = {
|
|
73
|
+
port: number;
|
|
74
|
+
token: string;
|
|
75
|
+
url: string;
|
|
76
|
+
/** Resolves when the session ends, for any reason. */
|
|
77
|
+
done: Promise<"ended" | "idle" | "expired">;
|
|
78
|
+
close: (reason?: "ended" | "idle" | "expired") => void;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Start the session server. Resolves once it's listening; the caller decides
|
|
82
|
+
* whether to open a browser and how to report progress.
|
|
83
|
+
*/
|
|
84
|
+
export declare function startKeysSession(opts?: {
|
|
85
|
+
onEvent?: (msg: string) => void;
|
|
86
|
+
}): Promise<SessionHandle>;
|
|
87
|
+
/** Providers currently holding a key — used for the terminal summary. */
|
|
88
|
+
export declare function configuredProviders(): Promise<string[]>;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-key setup. Non-interactive by design — it never reads stdin (so it
|
|
3
|
+
* can't hang in a non-TTY / agent context):
|
|
4
|
+
*
|
|
5
|
+
* 1. Silently import any keys already present in the environment
|
|
6
|
+
* (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY,
|
|
7
|
+
* KIMI_API_KEY).
|
|
8
|
+
* 2. For any provider still missing a key, open ONE browser page that
|
|
9
|
+
* collects them all at once and posts them straight to a localhost
|
|
10
|
+
* loopback server → the local secret store. Zero terminal typing.
|
|
11
|
+
*
|
|
12
|
+
* Keys are stored only in the local secret store (see keychain.ts: OS keychain
|
|
13
|
+
* on macOS, an encrypted file elsewhere) and never transit crosscheckagent.com.
|
|
14
|
+
* Power users who want to type a single key can still use
|
|
15
|
+
* `crosscheck keys add <provider> --paste`.
|
|
16
|
+
*/
|
|
17
|
+
export type PanelProvider = {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
env: string[];
|
|
21
|
+
};
|
|
22
|
+
/** The default panel. `grok` maps to XAI_API_KEY inside the engine. `env` lists
|
|
23
|
+
* the environment variables we'll auto-import from, in priority order — the
|
|
24
|
+
* aliases catch people who set GROK_API_KEY / GOOGLE_API_KEY / etc. */
|
|
25
|
+
export declare const KEY_PANEL: PanelProvider[];
|
|
26
|
+
/**
|
|
27
|
+
* Use what's already available: count existing keychain entries, import any
|
|
28
|
+
* env-present keys (writing them to the keychain), and return the providers
|
|
29
|
+
* that still need a key. No prompts, no network.
|
|
30
|
+
*/
|
|
31
|
+
export declare function importEnvKeys(): Promise<{
|
|
32
|
+
configured: number;
|
|
33
|
+
need: PanelProvider[];
|
|
34
|
+
}>;
|
|
35
|
+
/** Standalone `crosscheck keys setup`: env-import, then one browser page for the rest. */
|
|
36
|
+
export declare function runKeysSetup(): Promise<{
|
|
37
|
+
configured: number;
|
|
38
|
+
}>;
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library surface — the API Crosscheck Desktop consumes.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
* --------------------
|
|
6
|
+
* The desktop app needs the same vault, the same activation, the same model
|
|
7
|
+
* resolution and the same key verification the CLI has. There are three ways
|
|
8
|
+
* to get that and only one of them is safe:
|
|
9
|
+
*
|
|
10
|
+
* 1. Copy the code into the desktop repo. REJECTED. The secret store and
|
|
11
|
+
* the activation signing are exactly the code that must never diverge
|
|
12
|
+
* between two clients writing the same `~/.crosscheck/` directory. Two
|
|
13
|
+
* copies is two encryption implementations and eventually two file
|
|
14
|
+
* formats.
|
|
15
|
+
* 2. Extract into a third npm package. REJECTED for now. It buys nothing
|
|
16
|
+
* over this file and adds a package to version, publish and keep in
|
|
17
|
+
* lockstep — the same npx-cache trap that already forces a CLI bump on
|
|
18
|
+
* every engine release (see config.ts, ENGINE_FALLBACK_VERSION).
|
|
19
|
+
* 3. Export from the CLI itself. THIS. The desktop app takes a single
|
|
20
|
+
* dependency on `crosscheck-cli`, which already pins `crosscheck-mcp`
|
|
21
|
+
* exactly — so one dependency delivers the vault AND a version-locked
|
|
22
|
+
* engine, and the CLI and the desktop app are the same code by
|
|
23
|
+
* construction rather than by discipline.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IS DELIBERATELY NOT HERE
|
|
26
|
+
* -----------------------------
|
|
27
|
+
* Nothing that prints to the console or reads stdin. Every export below is a
|
|
28
|
+
* pure function or an async call that returns data. The CLI's interactive
|
|
29
|
+
* wizards (`keys-setup`, `keys-manage-cli`, `doctor`) stay out: the desktop
|
|
30
|
+
* app renders its own UI over the same primitives, and a library that can
|
|
31
|
+
* `process.exit()` is a library that can take an Electron main process down
|
|
32
|
+
* with it.
|
|
33
|
+
*/
|
|
34
|
+
export { dataDir, fileStorePath, secretBackend, setSecretBackend, getSecret, setSecret, deleteSecret, listSecrets, ONEPASSWORD_LIST_PLACEHOLDER, ReadOnlyBackendError, OnePasswordReadError, NotMappedInEnterpriseModeError, type Backend, } from "./keychain.js";
|
|
35
|
+
export { CANONICAL_PROVIDERS, KEY_FORMATS, resolveProvider, looksLikeApiKey, validateKey, formatWarning, } from "./key-format.js";
|
|
36
|
+
export { canVerify, classifyStatus, verifyKey, type VerifyResult, type VerifyOutcome, } from "./key-verify.js";
|
|
37
|
+
export { suffix4, keyStatusDisabled, collectKeyStatus, reportKeyStatus, type KeyResult, type KeyStatusItem, } from "./key-status.js";
|
|
38
|
+
export { activate, activateDeviceFlow } from "./activate.js";
|
|
39
|
+
export { currentState, isAllowedToRun, runHeartbeat, startScheduler, type HeartbeatState, } from "./heartbeat.js";
|
|
40
|
+
export { readLocalModelPrefs, readLocalModelModes, readLocalOrigins, effectiveForMode, setChainForMode, setLocalModelPref, addLocalModelFallback, removeLocalModelFallback, unsetLocalModelPref, syncModelPreferencesFromServer, pushModelPreferenceToServer, deleteModelPreferenceOnServer, readPendingConflicts, writePendingConflicts, clearPendingConflicts, type ModelPreferences, type ModeScopedPreferences, type ModelMode, type PrefOrigin, type OriginMap, type PrefConflict, } from "./model-prefs.js";
|
|
41
|
+
export { MODEL_ENV_VARS, resolveModelForProvider, resolveAllModels, type ModelSource, type ResolvedModel, } from "./model-resolve.js";
|
|
42
|
+
export { resolveEffectiveModel, isBlockedByPolicy, type ModelPolicy, type ModelPolicyEntry, } from "./model-policy.js";
|
|
43
|
+
export { SERVER_URL, CLI_VERSION, ENGINE_PACKAGE, CLI_PACKAGE } from "./config.js";
|
|
44
|
+
export { resolveEngineVersion } from "./engine-version.js";
|
|
45
|
+
export { registerMcpServer, SERVER_NAME as MCP_SERVER_NAME, SERVER_COMMAND as MCP_SERVER_COMMAND, SERVER_ARGS as MCP_SERVER_ARGS, type RegisterResult, type ServerRegistration, } from "./mcp-register.js";
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library surface — the API Crosscheck Desktop consumes.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
* --------------------
|
|
6
|
+
* The desktop app needs the same vault, the same activation, the same model
|
|
7
|
+
* resolution and the same key verification the CLI has. There are three ways
|
|
8
|
+
* to get that and only one of them is safe:
|
|
9
|
+
*
|
|
10
|
+
* 1. Copy the code into the desktop repo. REJECTED. The secret store and
|
|
11
|
+
* the activation signing are exactly the code that must never diverge
|
|
12
|
+
* between two clients writing the same `~/.crosscheck/` directory. Two
|
|
13
|
+
* copies is two encryption implementations and eventually two file
|
|
14
|
+
* formats.
|
|
15
|
+
* 2. Extract into a third npm package. REJECTED for now. It buys nothing
|
|
16
|
+
* over this file and adds a package to version, publish and keep in
|
|
17
|
+
* lockstep — the same npx-cache trap that already forces a CLI bump on
|
|
18
|
+
* every engine release (see config.ts, ENGINE_FALLBACK_VERSION).
|
|
19
|
+
* 3. Export from the CLI itself. THIS. The desktop app takes a single
|
|
20
|
+
* dependency on `crosscheck-cli`, which already pins `crosscheck-mcp`
|
|
21
|
+
* exactly — so one dependency delivers the vault AND a version-locked
|
|
22
|
+
* engine, and the CLI and the desktop app are the same code by
|
|
23
|
+
* construction rather than by discipline.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IS DELIBERATELY NOT HERE
|
|
26
|
+
* -----------------------------
|
|
27
|
+
* Nothing that prints to the console or reads stdin. Every export below is a
|
|
28
|
+
* pure function or an async call that returns data. The CLI's interactive
|
|
29
|
+
* wizards (`keys-setup`, `keys-manage-cli`, `doctor`) stay out: the desktop
|
|
30
|
+
* app renders its own UI over the same primitives, and a library that can
|
|
31
|
+
* `process.exit()` is a library that can take an Electron main process down
|
|
32
|
+
* with it.
|
|
33
|
+
*/
|
|
34
|
+
// --- The vault. Same file, same lock, same AES-256-GCM. ---------------------
|
|
35
|
+
export { dataDir, fileStorePath, secretBackend, setSecretBackend, getSecret, setSecret, deleteSecret, listSecrets, ONEPASSWORD_LIST_PLACEHOLDER, ReadOnlyBackendError, OnePasswordReadError, NotMappedInEnterpriseModeError, } from "./keychain.js";
|
|
36
|
+
// --- Which providers exist, and does this string look like one's key? -------
|
|
37
|
+
export { CANONICAL_PROVIDERS, KEY_FORMATS, resolveProvider, looksLikeApiKey, validateKey, formatWarning, } from "./key-format.js";
|
|
38
|
+
// --- Does the key actually work? Asks the provider, never our server. -------
|
|
39
|
+
export { canVerify, classifyStatus, verifyKey, } from "./key-verify.js";
|
|
40
|
+
// --- Metadata-only reporting. Presence, last 4, last outcome. ---------------
|
|
41
|
+
export { suffix4, keyStatusDisabled, collectKeyStatus, reportKeyStatus, } from "./key-status.js";
|
|
42
|
+
// --- Seat activation and the signed heartbeat. ------------------------------
|
|
43
|
+
export { activate, activateDeviceFlow } from "./activate.js";
|
|
44
|
+
export { currentState, isAllowedToRun, runHeartbeat, startScheduler, } from "./heartbeat.js";
|
|
45
|
+
// --- Model pinning: local preferences, org policy, effective resolution. ----
|
|
46
|
+
export { readLocalModelPrefs, readLocalModelModes, readLocalOrigins, effectiveForMode, setChainForMode, setLocalModelPref, addLocalModelFallback, removeLocalModelFallback, unsetLocalModelPref, syncModelPreferencesFromServer, pushModelPreferenceToServer, deleteModelPreferenceOnServer, readPendingConflicts, writePendingConflicts, clearPendingConflicts, } from "./model-prefs.js";
|
|
47
|
+
export { MODEL_ENV_VARS, resolveModelForProvider, resolveAllModels, } from "./model-resolve.js";
|
|
48
|
+
export { resolveEffectiveModel, isBlockedByPolicy, } from "./model-policy.js";
|
|
49
|
+
// --- Identity and versions, so the app can report what it is running. -------
|
|
50
|
+
export { SERVER_URL, CLI_VERSION, ENGINE_PACKAGE, CLI_PACKAGE } from "./config.js";
|
|
51
|
+
export { resolveEngineVersion } from "./engine-version.js";
|
|
52
|
+
// --- Registering the MCP server with Claude Code. -------------------------
|
|
53
|
+
// The wizard's last step. Shared so the app and `crosscheck install` write
|
|
54
|
+
// byte-identical entries to ~/.claude.json — two spellings of the same server
|
|
55
|
+
// is how a user ends up with a duplicate that fails opaquely.
|
|
56
|
+
export { registerMcpServer, SERVER_NAME as MCP_SERVER_NAME, SERVER_COMMAND as MCP_SERVER_COMMAND, SERVER_ARGS as MCP_SERVER_ARGS, } from "./mcp-register.js";
|
|
57
|
+
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,+EAA+E;AAC/E,OAAO,EACL,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,4BAA4B,EAC5B,oBAAoB,EACpB,oBAAoB,EACpB,8BAA8B,GAE/B,MAAM,eAAe,CAAC;AAEvB,+EAA+E;AAC/E,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,eAAe,EACf,eAAe,EACf,WAAW,EACX,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,+EAA+E;AAC/E,OAAO,EACL,SAAS,EACT,cAAc,EACd,SAAS,GAGV,MAAM,iBAAiB,CAAC;AAEzB,+EAA+E;AAC/E,OAAO,EACL,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,GAGhB,MAAM,iBAAiB,CAAC;AAEzB,+EAA+E;AAC/E,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EACL,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,cAAc,GAEf,MAAM,gBAAgB,CAAC;AAExB,+EAA+E;AAC/E,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,qBAAqB,EACrB,wBAAwB,EACxB,mBAAmB,EACnB,8BAA8B,EAC9B,2BAA2B,EAC3B,6BAA6B,EAC7B,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,GAOtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,cAAc,EACd,uBAAuB,EACvB,gBAAgB,GAGjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAGlB,MAAM,mBAAmB,CAAC;AAE3B,+EAA+E;AAC/E,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE3D,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,8DAA8D;AAC9D,OAAO,EACL,iBAAiB,EACjB,WAAW,IAAI,eAAe,EAC9B,cAAc,IAAI,kBAAkB,EACpC,WAAW,IAAI,eAAe,GAG/B,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-register the crosscheck MCP server with Claude Code so the user never
|
|
3
|
+
* hand-edits JSON.
|
|
4
|
+
*
|
|
5
|
+
* Primary path: the `claude` CLI — `claude mcp add --scope user crosscheck --
|
|
6
|
+
* npx -y -p crosscheck-cli@latest crosscheck serve`. User scope means it's
|
|
7
|
+
* available in every workspace, which matches a per-seat license.
|
|
8
|
+
*
|
|
9
|
+
* Fallback: if the `claude` CLI isn't on PATH, merge-write the user's
|
|
10
|
+
* ~/.claude.json `mcpServers` map in place — never clobbering existing
|
|
11
|
+
* servers or other settings.
|
|
12
|
+
*
|
|
13
|
+
* The bin name is pinned explicitly via `-p <pkg> <bin>` rather than the
|
|
14
|
+
* shorter `npx -y crosscheck-cli@latest serve` — bare `npx <pkg>` only
|
|
15
|
+
* auto-selects a bin when the package ships exactly one, or one matching
|
|
16
|
+
* the package name. crosscheck-cli briefly shipped a second bin
|
|
17
|
+
* (crosscheck-enterprise) in 0.0.50, which made every existing
|
|
18
|
+
* registration written with the short form fail with "could not
|
|
19
|
+
* determine executable to run" (silently, since this string gets written
|
|
20
|
+
* to ~/.claude.json and only fails later as an opaque MCP connection
|
|
21
|
+
* error). Keep this form even now that crosscheck-cli is back to a single
|
|
22
|
+
* bin — it's what makes a future second bin safe to add again.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SERVER_NAME = "crosscheck";
|
|
25
|
+
export declare const SERVER_COMMAND = "npx";
|
|
26
|
+
export declare const SERVER_ARGS: string[];
|
|
27
|
+
export type RegisterResult = {
|
|
28
|
+
method: "cli";
|
|
29
|
+
} | {
|
|
30
|
+
method: "file";
|
|
31
|
+
configPath: string;
|
|
32
|
+
} | {
|
|
33
|
+
method: "manual";
|
|
34
|
+
snippet: string;
|
|
35
|
+
reason: string;
|
|
36
|
+
};
|
|
37
|
+
export type ServerRegistration = {
|
|
38
|
+
name: string;
|
|
39
|
+
command: string;
|
|
40
|
+
args: string[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* `reg` defaults to the standard crosscheck-cli registration. crosscheck-
|
|
44
|
+
* enterprise passes its own {name: "crosscheck-enterprise", command: "npx",
|
|
45
|
+
* args: ["-y", "crosscheck-enterprise@latest", "serve"]} so Claude Code
|
|
46
|
+
* spawns the right binary — both can coexist in the same ~/.claude.json
|
|
47
|
+
* under different server names.
|
|
48
|
+
*/
|
|
49
|
+
export declare function registerMcpServer(reg?: ServerRegistration): RegisterResult;
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server façade. Spawns the TypeScript crosscheck-mcp engine as a
|
|
3
|
+
* subprocess and lets the IDE's MCP stdio JSON-RPC traffic flow through it.
|
|
4
|
+
*
|
|
5
|
+
* (Previously this bundled and spawned a Python server. crosscheck-mcp is now
|
|
6
|
+
* pure TypeScript and ships as a normal npm dependency — no Python required.)
|
|
7
|
+
*
|
|
8
|
+
* Pre-flight (before spawning the child):
|
|
9
|
+
* 1. Verify the activation JWT from the OS keychain — refuse to serve if
|
|
10
|
+
* missing, expired, or revoked.
|
|
11
|
+
* 2. Resolve the crosscheck-mcp engine entry from node_modules (falling back
|
|
12
|
+
* to `npx -y crosscheck-mcp` if it isn't installed alongside).
|
|
13
|
+
* 3. Pull every upstream LLM key from the keychain into the child's env
|
|
14
|
+
* (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, XAI_API_KEY,
|
|
15
|
+
* GROQ_API_KEY, DEEPSEEK_API_KEY, MISTRAL_API_KEY). The customer never
|
|
16
|
+
* hand-manages env vars.
|
|
17
|
+
*
|
|
18
|
+
* The child inherits stdio so the IDE's MCP client talks to it directly; we
|
|
19
|
+
* don't proxy or interpret the JSON-RPC traffic. The CLI is the
|
|
20
|
+
* license/onboarding wrapper; crosscheck-mcp is the engine.
|
|
21
|
+
*/
|
|
22
|
+
export declare function startMcpServer(opts?: {
|
|
23
|
+
enforceProviderPolicy?: boolean;
|
|
24
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resolution logic for "which model actually gets called for this
|
|
3
|
+
* provider" — no I/O. Two independent things feed it:
|
|
4
|
+
* - a candidate model (explicit env var override > a saved personal
|
|
5
|
+
* preference > nothing), resolved by the caller before this runs
|
|
6
|
+
* - an optional enterprise model policy entry for that provider
|
|
7
|
+
* (crosscheck-enterprise only — a standard install never has one)
|
|
8
|
+
*
|
|
9
|
+
* A model policy is a hard clamp, not a suggestion: if the candidate isn't
|
|
10
|
+
* allowed, this never returns it. Deliberately does NOT know crosscheck-mcp's
|
|
11
|
+
* own DEFAULT_MODELS (duplicating that map here would just reintroduce the
|
|
12
|
+
* exact staleness bug detectStalePins was built to catch) — when nothing
|
|
13
|
+
* safe can be resolved, it returns undefined and the caller leaves the
|
|
14
|
+
* env var unset, deferring to the engine's own built-in default. An admin
|
|
15
|
+
* who wants a *specific* fallback should set `pinned`, or list it first
|
|
16
|
+
* in `allow`.
|
|
17
|
+
*/
|
|
18
|
+
export type ModelPolicyEntry = {
|
|
19
|
+
/** "*" = no model-level restriction for this provider (only the existing
|
|
20
|
+
* provider-level allowlist applies). Otherwise the exact list of model
|
|
21
|
+
* ids this org permits for this provider. */
|
|
22
|
+
allow: "*" | readonly string[];
|
|
23
|
+
/** Always wins over `allow`, including over "*". */
|
|
24
|
+
deny: readonly string[];
|
|
25
|
+
/** Admin-chosen fallback when neither the caller's candidate nor a
|
|
26
|
+
* user-set preference is usable. Optional. */
|
|
27
|
+
pinned?: string;
|
|
28
|
+
};
|
|
29
|
+
export type ModelPolicy = Readonly<Record<string, ModelPolicyEntry>>;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the effective model for one provider. `policy` is this
|
|
32
|
+
* provider's entry only (or undefined — most providers on a standard
|
|
33
|
+
* install have none). Returns undefined when nothing safe to inject was
|
|
34
|
+
* found, meaning: don't set the env var, let the engine use its own
|
|
35
|
+
* default.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveEffectiveModel(args: {
|
|
38
|
+
candidate?: string;
|
|
39
|
+
policy?: ModelPolicyEntry;
|
|
40
|
+
}): string | undefined;
|
|
41
|
+
/** True when `model` would be rejected by `policy` — used to tell the user
|
|
42
|
+
* WHY their chosen preference didn't take effect, distinct from "no
|
|
43
|
+
* preference was set at all." */
|
|
44
|
+
export declare function isBlockedByPolicy(model: string, policy: ModelPolicyEntry): boolean;
|