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,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Personal model-preference storage + sync. Works on ANY tier — standard
|
|
3
|
+
* or enterprise — unlike enterprise/policy.ts's org-wide provider policy.
|
|
4
|
+
* Set locally via `crosscheck models set <provider> <model>` (pins the
|
|
5
|
+
* max/primary model) and `crosscheck models fallback <provider> <model>`
|
|
6
|
+
* (greenlights a fallback), or from the browser at crosscheckagent.com/
|
|
7
|
+
* account/models; each side best-effort pushes to the other so either one
|
|
8
|
+
* reflects the current chain.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately fails OPEN (falls back to whatever's cached locally, or no
|
|
11
|
+
* preference at all) rather than restrictive, unlike the enterprise policy
|
|
12
|
+
* cache — a preference is a self-serve convenience, not a security
|
|
13
|
+
* control, so a network hiccup here should never block a call that would
|
|
14
|
+
* otherwise work fine on the engine's own default.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Keyed by CLI-canonical provider name (see key-format.ts's
|
|
18
|
+
* CANONICAL_PROVIDERS — "grok", not "xai"). Each value is an ORDERED
|
|
19
|
+
* chain of model ids: index 0 is the pinned max/primary model, the rest
|
|
20
|
+
* are greenlit fallbacks tried in order if the primary comes back with a
|
|
21
|
+
* model-access failure (see crosscheck-mcp's core/model-fallback.ts —
|
|
22
|
+
* this is the exact list that ends up on the wire as
|
|
23
|
+
* `<PROVIDER>_MODEL` (index 0) + `<PROVIDER>_MODEL_FALLBACKS` (the rest).
|
|
24
|
+
*/
|
|
25
|
+
export type ModelPreferences = Readonly<Record<string, readonly string[]>>;
|
|
26
|
+
/**
|
|
27
|
+
* Conflicts are persisted rather than reported inline because they are
|
|
28
|
+
* detected during `serve`, which has no way to ask anything: stdout is the
|
|
29
|
+
* MCP JSON-RPC channel and there is no terminal attached. So the detection
|
|
30
|
+
* and the question happen in different places — recorded here, asked the next
|
|
31
|
+
* time the user runs an interactive command.
|
|
32
|
+
*/
|
|
33
|
+
export declare function writePendingConflicts(conflicts: readonly PrefConflict[]): void;
|
|
34
|
+
export declare function readPendingConflicts(): PrefConflict[];
|
|
35
|
+
export declare function clearPendingConflicts(): void;
|
|
36
|
+
/** confer vs confer-super. Mirrors MODEL_MODES on the server. */
|
|
37
|
+
export type ModelMode = "default" | "super";
|
|
38
|
+
/** Mode-scoped preferences, as stored locally and returned by the server's
|
|
39
|
+
* `modes` field. */
|
|
40
|
+
export type ModeScopedPreferences = Readonly<Record<ModelMode, ModelPreferences>>;
|
|
41
|
+
/**
|
|
42
|
+
* Who chose a given provider's chain.
|
|
43
|
+
*
|
|
44
|
+
* Without this the cache cannot tell "the user pinned this here" from "we
|
|
45
|
+
* cached what the server said last time", which makes a non-destructive sync
|
|
46
|
+
* impossible: every server change would look either like an update (and
|
|
47
|
+
* silently clobber a deliberate local pin) or like a conflict (and nag about
|
|
48
|
+
* values nobody chose).
|
|
49
|
+
*/
|
|
50
|
+
export type PrefOrigin = "local" | "server";
|
|
51
|
+
export type OriginMap = Readonly<Record<ModelMode, Readonly<Record<string, PrefOrigin>>>>;
|
|
52
|
+
/** A local pin that disagrees with the server. Kept — never overwritten —
|
|
53
|
+
* and surfaced for the user to resolve when they're somewhere interactive. */
|
|
54
|
+
export interface PrefConflict {
|
|
55
|
+
mode: ModelMode;
|
|
56
|
+
provider: string;
|
|
57
|
+
local: readonly string[];
|
|
58
|
+
server: readonly string[];
|
|
59
|
+
}
|
|
60
|
+
export declare function readLocalOrigins(): OriginMap;
|
|
61
|
+
/**
|
|
62
|
+
* The chains that should actually apply for a mode.
|
|
63
|
+
*
|
|
64
|
+
* Super layers OVER default per provider rather than replacing it wholesale:
|
|
65
|
+
* a user who pinned Anthropic for super and left the rest alone keeps their
|
|
66
|
+
* normal choices for the rest, instead of silently losing them.
|
|
67
|
+
*/
|
|
68
|
+
export declare function effectiveForMode(prefs: ModeScopedPreferences, mode: ModelMode): ModelPreferences;
|
|
69
|
+
export declare function readLocalModelPrefs(): ModelPreferences;
|
|
70
|
+
/** The full mode-scoped cache. Fails open to empty, like everything else in
|
|
71
|
+
* this module — a preference is a convenience, not a gate. */
|
|
72
|
+
export declare function readLocalModelModes(): ModeScopedPreferences;
|
|
73
|
+
/** Exported for the mutators below and for tests. */
|
|
74
|
+
export declare function __markOrigin(mode: ModelMode, provider: string, origin: PrefOrigin): void;
|
|
75
|
+
/** Replace one provider's chain in one mode, preserving everything else.
|
|
76
|
+
* Used by conflict resolution, which needs to write a specific mode. */
|
|
77
|
+
export declare function setChainForMode(mode: ModelMode, provider: string, chain: readonly string[], origin: PrefOrigin): void;
|
|
78
|
+
/** Pins the max/primary model (chain position 0), keeping any existing
|
|
79
|
+
* fallbacks for that provider unchanged. */
|
|
80
|
+
export declare function setLocalModelPref(provider: string, model: string): ModelPreferences;
|
|
81
|
+
/** Greenlights a fallback model — appended after the primary and any
|
|
82
|
+
* earlier fallbacks, unless it's already in the chain. Setting a
|
|
83
|
+
* fallback for a provider with no primary yet is fine; the fallback
|
|
84
|
+
* chain still works once a primary is set (or the engine default acts
|
|
85
|
+
* as the de-facto primary). */
|
|
86
|
+
export declare function addLocalModelFallback(provider: string, model: string): ModelPreferences;
|
|
87
|
+
/** Removes one model from a provider's fallback chain — the primary
|
|
88
|
+
* (position 0) can be removed this way too, promoting the next entry. */
|
|
89
|
+
export declare function removeLocalModelFallback(provider: string, model: string): ModelPreferences;
|
|
90
|
+
/** Clears a provider's whole chain (primary + fallbacks). */
|
|
91
|
+
export declare function unsetLocalModelPref(provider: string): ModelPreferences;
|
|
92
|
+
/**
|
|
93
|
+
* Pull the server-stored preference chains and merge them into the local
|
|
94
|
+
* cache — server values win for keys it returns; existing local-only
|
|
95
|
+
* entries (not yet pushed, e.g. from an offline `models set`) are
|
|
96
|
+
* preserved. Any failure just returns whatever's already cached locally.
|
|
97
|
+
*/
|
|
98
|
+
export declare function syncModelPreferencesFromServer(jwt: string): Promise<ModelPreferences>;
|
|
99
|
+
/** Best-effort push of one provider's whole chain to the server — a
|
|
100
|
+
* failure here only means the browser page won't reflect it until the
|
|
101
|
+
* next successful sync; the local change still takes effect immediately. */
|
|
102
|
+
export declare function pushModelPreferenceToServer(jwt: string, provider: string, models: readonly string[]): Promise<boolean>;
|
|
103
|
+
/** Best-effort removal of one provider's whole chain on the server. */
|
|
104
|
+
export declare function deleteModelPreferenceOnServer(jwt: string, provider: string): Promise<boolean>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "what model actually gets called for this provider, and why"
|
|
3
|
+
* resolution — used both to build the env vars `mcp.ts` hands the spawned
|
|
4
|
+
* engine, and to render `crosscheck models list`/`doctor`'s display. Pure
|
|
5
|
+
* given its inputs (env, local prefs, policy), so it's fully testable
|
|
6
|
+
* without touching the filesystem or network.
|
|
7
|
+
*/
|
|
8
|
+
import { type ModelPolicy } from "./model-policy.js";
|
|
9
|
+
/** CLI-canonical provider name -> the engine's `<PROVIDER>_MODEL` env var.
|
|
10
|
+
* Mirrors mcp.ts's KEY_BINDINGS pairing (same providers, same env-var
|
|
11
|
+
* prefixes, `_API_KEY` -> `_MODEL`) — kept here since both the env
|
|
12
|
+
* injection (mcp.ts) and the CLI/doctor display need it. The engine
|
|
13
|
+
* reads the matching `<...>_MODEL_FALLBACKS` (comma-separated) itself —
|
|
14
|
+
* see crosscheck-mcp's providers/registry.ts. */
|
|
15
|
+
export declare const MODEL_ENV_VARS: Readonly<Record<string, string>>;
|
|
16
|
+
export type ModelSource =
|
|
17
|
+
/** An explicit `<PROVIDER>_MODEL` env var in the shell — always wins. */
|
|
18
|
+
"env"
|
|
19
|
+
/** A personal preference set via `crosscheck models set` or the browser. */
|
|
20
|
+
| "preference"
|
|
21
|
+
/** The org admin's `pinned` fallback, used because the candidate (if
|
|
22
|
+
* any) wasn't allowed. */
|
|
23
|
+
| "policy-pinned"
|
|
24
|
+
/** The first policy-allowed model, used because there was no candidate
|
|
25
|
+
* and no `pinned` fallback either. */
|
|
26
|
+
| "policy-fallback"
|
|
27
|
+
/** Nothing set anywhere — the engine's own built-in default will run. */
|
|
28
|
+
| "default"
|
|
29
|
+
/** A candidate existed (env var or preference) but enterprise policy
|
|
30
|
+
* rejected it, and no pinned/fallback model could be found either. */
|
|
31
|
+
| "blocked";
|
|
32
|
+
export type ResolvedModel = {
|
|
33
|
+
provider: string;
|
|
34
|
+
/** undefined means: don't set the env var, defer to the engine's own
|
|
35
|
+
* default for this provider. */
|
|
36
|
+
model?: string;
|
|
37
|
+
source: ModelSource;
|
|
38
|
+
/** Ordered "greenlight" fallbacks for this provider — tried, in order,
|
|
39
|
+
* by the engine only when `model` comes back with a model-access
|
|
40
|
+
* failure. Layered: the user's own configured fallback chain first,
|
|
41
|
+
* then (if an enterprise model policy restricts this provider to a
|
|
42
|
+
* specific allow-list) any other allowed models not already in the
|
|
43
|
+
* list, as an automatic safety net even for a user who set no
|
|
44
|
+
* fallbacks themselves. Always excludes `model` itself. */
|
|
45
|
+
fallbackModels: readonly string[];
|
|
46
|
+
};
|
|
47
|
+
export declare function resolveModelForProvider(args: {
|
|
48
|
+
provider: string;
|
|
49
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
50
|
+
localPrefs: Readonly<Record<string, readonly string[]>>;
|
|
51
|
+
modelPolicy?: ModelPolicy;
|
|
52
|
+
}): ResolvedModel;
|
|
53
|
+
/** Resolves every canonical provider, regardless of whether it has a key
|
|
54
|
+
* configured — callers filter to "has a key" themselves (mcp.ts) or show
|
|
55
|
+
* everything (models list, so a not-yet-configured provider's policy is
|
|
56
|
+
* still visible). */
|
|
57
|
+
export declare function resolveAllModels(args: {
|
|
58
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
59
|
+
localPrefs: Readonly<Record<string, readonly string[]>>;
|
|
60
|
+
modelPolicy?: ModelPolicy;
|
|
61
|
+
}): ResolvedModel[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `crosscheck models` — the terminal half of model selection (the other
|
|
3
|
+
* half is crosscheckagent.com/account/models and, for org-wide policy,
|
|
4
|
+
* /account/enterprise). "Pin your max model, greenlight fallbacks": `set`
|
|
5
|
+
* pins the primary; `fallback` adds/removes greenlit fallbacks tried, in
|
|
6
|
+
* order, only when the primary comes back with a model-access failure
|
|
7
|
+
* (never for any other kind of error — see crosscheck-mcp's core/model-
|
|
8
|
+
* fallback.ts). See model-resolve.ts for the actual precedence — this
|
|
9
|
+
* file is purely command plumbing over it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runModelsCommand(sub: string | undefined, rest: string[]): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser wizard for 1Password setup — same trust pattern as
|
|
3
|
+
* keys-browser-flow.ts: the CLI spins up a localhost HTTP server, opens a
|
|
4
|
+
* Crosscheck (XC)-served form, and the form POSTs the collected config
|
|
5
|
+
* directly back to loopback. No one has to hand-write
|
|
6
|
+
* ~/.crosscheck/1password.json.
|
|
7
|
+
*
|
|
8
|
+
* The page collects authMode + per-provider Vault/Item/Field (combined into
|
|
9
|
+
* an op://vault/item/field reference client-side) — never a secret value,
|
|
10
|
+
* just where to find one. What gets POSTed back is re-validated in full by
|
|
11
|
+
* config.ts's writeOnePasswordConfig() exactly as if it had been hand-typed
|
|
12
|
+
* into the file — the browser is a convenience, not a trust boundary.
|
|
13
|
+
*
|
|
14
|
+
* Security properties (mirrors captureKeysViaBrowser):
|
|
15
|
+
* - State token (32 bytes urlsafe) bound at server-start.
|
|
16
|
+
* - Loopback only — binds 127.0.0.1, never 0.0.0.0.
|
|
17
|
+
* - CORS locked to the served page's origin.
|
|
18
|
+
* - Server accepts exactly one valid POST; subsequent calls 410.
|
|
19
|
+
* - 10-minute timeout; self-terminates after the value lands or expires.
|
|
20
|
+
*/
|
|
21
|
+
import type { AuthMode } from "./config.js";
|
|
22
|
+
export type CapturedOnePasswordConfig = {
|
|
23
|
+
authMode: AuthMode;
|
|
24
|
+
mappings: Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
export declare function captureOnePasswordConfigViaBrowser(args: {
|
|
27
|
+
providers: string[];
|
|
28
|
+
/** Existing mappings (op:// references, never values) to pre-fill —
|
|
29
|
+
* lets `keys setup --backend 1password` re-open the wizard to add or
|
|
30
|
+
* edit providers without losing what's already configured. */
|
|
31
|
+
existing?: Record<string, string>;
|
|
32
|
+
existingAuthMode?: AuthMode;
|
|
33
|
+
pageBaseUrl?: string;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}): Promise<CapturedOnePasswordConfig>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader for ~/.crosscheck/1password.json — the per-provider secret-reference
|
|
3
|
+
* mapping for the (opt-in, enterprise) 1Password backend.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately self-contained: this does NOT import from keychain.ts. That's
|
|
6
|
+
* not an accident of convenience — it keeps this security checkpoint's blast
|
|
7
|
+
* radius to exactly the src/onepassword/ tree instead of coupling it to the
|
|
8
|
+
* existing file/keychain backend internals (which aren't exported anyway).
|
|
9
|
+
*
|
|
10
|
+
* User-scope only, always — this never looks at a project/repo-local path.
|
|
11
|
+
* A config file living in the current working directory could let a
|
|
12
|
+
* malicious repo silently remap "anthropic" to a different, more-privileged
|
|
13
|
+
* 1Password item the developer's own session happens to have access to, and
|
|
14
|
+
* crosscheck would dutifully resolve and inject it. That's a secret-
|
|
15
|
+
* substitution vector this loader refuses to open.
|
|
16
|
+
*/
|
|
17
|
+
export type AuthMode = "desktop" | "service-account";
|
|
18
|
+
export type OnePasswordConfig = {
|
|
19
|
+
authMode: AuthMode;
|
|
20
|
+
/** provider name -> validated op:// reference */
|
|
21
|
+
mappings: Readonly<Record<string, string>>;
|
|
22
|
+
};
|
|
23
|
+
export type OnePasswordConfigError = "not-found" | "not-a-regular-file" | "insecure-permissions" | "invalid-json" | "missing-auth-mode" | "invalid-auth-mode" | "missing-mappings" | "unknown-provider" | "invalid-reference";
|
|
24
|
+
export type LoadOnePasswordConfigResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
config: OnePasswordConfig;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: OnePasswordConfigError;
|
|
30
|
+
detail: string;
|
|
31
|
+
};
|
|
32
|
+
export declare function configFilePath(): string;
|
|
33
|
+
export declare function loadOnePasswordConfig(): LoadOnePasswordConfigResult;
|
|
34
|
+
export type WriteOnePasswordConfigError = "invalid-auth-mode" | "unknown-provider" | "invalid-reference";
|
|
35
|
+
export type WriteOnePasswordConfigResult = {
|
|
36
|
+
ok: true;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: WriteOnePasswordConfigError;
|
|
40
|
+
detail: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Validate + atomically write ~/.crosscheck/1password.json. Used by the
|
|
44
|
+
* browser wizard (browser-flow.ts) so whatever a page POSTs back is held to
|
|
45
|
+
* EXACTLY the same grammar/provider checks a hand-written file would face on
|
|
46
|
+
* load — the browser is a convenience, never a trust boundary. Rejects the
|
|
47
|
+
* WHOLE write on any invalid entry rather than silently dropping bad ones,
|
|
48
|
+
* since a partially-applied config is easy to miss.
|
|
49
|
+
*/
|
|
50
|
+
export declare function writeOnePasswordConfig(input: {
|
|
51
|
+
authMode: string;
|
|
52
|
+
mappings: Record<string, string>;
|
|
53
|
+
}): WriteOnePasswordConfigResult;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `op` CLI runner — the only place this backend ever executes anything.
|
|
3
|
+
* Security checkpoint (see docs/onepassword-security.md): argv construction,
|
|
4
|
+
* environment policy, timeouts/cleanup, output caps, and the error taxonomy
|
|
5
|
+
* all live here.
|
|
6
|
+
*
|
|
7
|
+
* Hard rules:
|
|
8
|
+
* - spawn() only, never a shell. Fixed argv shape, no user-controlled flags.
|
|
9
|
+
* - The reference is re-validated here even though config.ts already
|
|
10
|
+
* validated it on load — defense in depth, not redundancy for its own
|
|
11
|
+
* sake, since this is the last checkpoint before anything reaches argv.
|
|
12
|
+
* - Resolved secret values, tokens, and full op:// references never reach
|
|
13
|
+
* a log, an error message, or telemetry. Errors carry category +
|
|
14
|
+
* remediation + provider-agnostic detail only.
|
|
15
|
+
* - Fail closed: any ambiguity (unparseable version, unmatched stderr,
|
|
16
|
+
* unexpected exit) becomes a generic "unknown" error, never a pass.
|
|
17
|
+
*/
|
|
18
|
+
import type { AuthMode } from "./config.js";
|
|
19
|
+
export declare const MIN_OP_VERSION = "2.18.0";
|
|
20
|
+
export type OpReadErrorKind = "not-installed" | "wrong-version" | "not-signed-in" | "token-missing" | "malformed-reference" | "access-denied" | "item-or-field-missing" | "empty-value" | "output-too-large" | "timeout" | "unknown";
|
|
21
|
+
export type OpReadResult = {
|
|
22
|
+
ok: true;
|
|
23
|
+
value: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
error: OpReadErrorKind;
|
|
27
|
+
detail: string;
|
|
28
|
+
};
|
|
29
|
+
export type OpVersionCheckResult = {
|
|
30
|
+
ok: true;
|
|
31
|
+
version: string;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
error: "not-installed" | "wrong-version" | "unknown";
|
|
35
|
+
detail: string;
|
|
36
|
+
};
|
|
37
|
+
export type OpRunnerOptions = {
|
|
38
|
+
authMode: AuthMode;
|
|
39
|
+
/** Required when authMode === "service-account"; ignored otherwise. */
|
|
40
|
+
serviceAccountToken?: string;
|
|
41
|
+
/** Override the `op` binary — defaults to "op" resolved via PATH. Tests
|
|
42
|
+
* point this at an absolute path to a fake binary. */
|
|
43
|
+
opBin?: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function checkOpVersion(opts?: {
|
|
46
|
+
opBin?: string;
|
|
47
|
+
}): Promise<OpVersionCheckResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Read one secret value from 1Password. Re-validates `refInput` itself
|
|
50
|
+
* (never trusts a caller's prior validation), never falls back to any other
|
|
51
|
+
* source on failure, and never lets a resolved value, token, or reference
|
|
52
|
+
* reach the returned error on the failure path.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readOpSecret(refInput: string, opts: OpRunnerOptions): Promise<OpReadResult>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict parser/validator for 1Password secret references (`op://vault/item/field`).
|
|
3
|
+
*
|
|
4
|
+
* v1 supports only the plain 3-segment form — no optional `section` segment.
|
|
5
|
+
* Keeping the grammar minimal keeps this file (and its test surface) small,
|
|
6
|
+
* which matters because it's the first of two defense-in-depth checkpoints
|
|
7
|
+
* against CWE-78: op.ts (PR 2) re-validates immediately before spawning `op`,
|
|
8
|
+
* but rejecting anything that isn't a clean `op://...` string here means a
|
|
9
|
+
* malformed/hostile config value never gets far enough to be a runner
|
|
10
|
+
* concern. Anything not starting with the literal "op://" prefix also can't
|
|
11
|
+
* be mistaken for a flag by `op`'s own argument parser, since it's always
|
|
12
|
+
* passed as a single argv element via execFile — never a shell.
|
|
13
|
+
*/
|
|
14
|
+
export type OpReference = string & {
|
|
15
|
+
readonly __brand: "OpReference";
|
|
16
|
+
};
|
|
17
|
+
export type OpReferenceError = "empty" | "too-long" | "control-characters" | "query-or-fragment" | "missing-prefix" | "wrong-segment-count" | "empty-segment" | "invalid-characters";
|
|
18
|
+
export type ParseOpReferenceResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
ref: OpReference;
|
|
21
|
+
vault: string;
|
|
22
|
+
item: string;
|
|
23
|
+
field: string;
|
|
24
|
+
} | {
|
|
25
|
+
ok: false;
|
|
26
|
+
error: OpReferenceError;
|
|
27
|
+
detail: string;
|
|
28
|
+
};
|
|
29
|
+
export declare function parseOpReference(input: string): ParseOpReferenceResult;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `crosscheck keys setup --backend 1password` — validates the config end to
|
|
3
|
+
* end BEFORE ever persisting the backend preference. Never writes anything
|
|
4
|
+
* to 1Password (the backend is read-only); a "test read" only ever proves
|
|
5
|
+
* connectivity — the value is discarded immediately, never printed, never
|
|
6
|
+
* stored.
|
|
7
|
+
*/
|
|
8
|
+
import { type AuthMode } from "./config.js";
|
|
9
|
+
export type ProviderCheckResult = {
|
|
10
|
+
provider: string;
|
|
11
|
+
ok: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
export type OnePasswordSetupResult = {
|
|
15
|
+
ok: true;
|
|
16
|
+
authMode: AuthMode;
|
|
17
|
+
providers: ProviderCheckResult[];
|
|
18
|
+
} | {
|
|
19
|
+
ok: false;
|
|
20
|
+
stage: "config" | "op-version" | "no-providers-resolved";
|
|
21
|
+
detail: string;
|
|
22
|
+
providers?: ProviderCheckResult[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Validate config → `op` version → test-read every mapped provider.
|
|
26
|
+
* Discloses provider NAMES only, never a full op:// reference — matching
|
|
27
|
+
* the same disclosure discipline as op.ts's error taxonomy. Never touches
|
|
28
|
+
* the persisted backend preference; the caller only does that after seeing
|
|
29
|
+
* `{ ok: true }`.
|
|
30
|
+
*
|
|
31
|
+
* "Success" here means at least one mapped provider resolved — not that
|
|
32
|
+
* every one did. A single bad item/field name is a per-provider config
|
|
33
|
+
* typo the user can fix later; it shouldn't block enabling the backend for
|
|
34
|
+
* every OTHER provider that already works. A systemic problem (op missing,
|
|
35
|
+
* wrong version, or every single provider failing — usually a signed-out
|
|
36
|
+
* or invalid-token case) DOES block, since persisting a backend where
|
|
37
|
+
* nothing works at all isn't useful and would just defer the failure to
|
|
38
|
+
* the next `crosscheck serve`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateOnePasswordSetup(): Promise<OnePasswordSetupResult>;
|
|
41
|
+
/**
|
|
42
|
+
* Orchestrates `crosscheck keys setup --backend 1password`: by default,
|
|
43
|
+
* opens a browser wizard to collect authMode + per-provider Vault/Item/Field
|
|
44
|
+
* (pre-filled with whatever's already configured, so re-running to add one
|
|
45
|
+
* more provider doesn't lose the rest) and writes the config — no one has
|
|
46
|
+
* to hand-edit ~/.crosscheck/1password.json. Pass `{ useFile: true }` (the
|
|
47
|
+
* CLI's `--file` flag) to skip the browser and just validate a config
|
|
48
|
+
* that's already been placed there some other way (dotfiles, CI
|
|
49
|
+
* provisioning, etc.). Either way, ends the same: validate, then (only on
|
|
50
|
+
* success) persist the preference and report what's usable.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runOnePasswordKeysSetup(opts?: {
|
|
53
|
+
useFile?: boolean;
|
|
54
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort cross-platform browser opener. Failure is silent — callers
|
|
3
|
+
* always print the URL too, so a headless box just falls back to copy-paste.
|
|
4
|
+
* Kept dependency-light (no `open` package) on purpose.
|
|
5
|
+
*/
|
|
6
|
+
export declare function openBrowser(url: string): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn on Claude Code's OpenTelemetry metrics export and point it at
|
|
3
|
+
* Crosscheck, by merge-writing ~/.claude/settings.json.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists at all: the coding agent's own token usage is the
|
|
6
|
+
* larger share of what a feature costs, and Crosscheck cannot observe it from
|
|
7
|
+
* inside its own process. The only supported way to see it is Claude Code's
|
|
8
|
+
* OTel metrics export — which needs six environment variables set.
|
|
9
|
+
*
|
|
10
|
+
* Asking a developer to export six variables guarantees the feature is never
|
|
11
|
+
* used correctly, so the CLI writes them, exactly the way it already writes
|
|
12
|
+
* MCP registration. The user runs one command and types nothing.
|
|
13
|
+
*
|
|
14
|
+
* PRIVACY: metrics only. `OTEL_LOGS_EXPORTER` is explicitly pinned to "none",
|
|
15
|
+
* because Claude Code's *log* export can carry prompt content while its
|
|
16
|
+
* metrics are numeric by construction. That single line is what keeps the
|
|
17
|
+
* "server never sees prompt content" guarantee structural rather than a
|
|
18
|
+
* promise about what our parser chooses to read.
|
|
19
|
+
*
|
|
20
|
+
* Merge-write, never clobber: settings.json belongs to the user and may hold
|
|
21
|
+
* unrelated env, hooks, and permissions.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Export interval, in ms. Claude Code's default is 60s, which means a metric
|
|
25
|
+
* point can span a minute — and any point straddling a branch switch has to be
|
|
26
|
+
* reported as ambiguous rather than assigned. Ten seconds shrinks that
|
|
27
|
+
* ambiguous band to roughly a percent of tokens. Lower is not obviously
|
|
28
|
+
* better: it multiplies request volume for precision nobody will read.
|
|
29
|
+
*/
|
|
30
|
+
export declare const EXPORT_INTERVAL_MS = 10000;
|
|
31
|
+
export declare function claudeSettingsPath(): string;
|
|
32
|
+
export type OtelEnv = Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* The env block Claude Code needs.
|
|
35
|
+
*
|
|
36
|
+
* Auth is the seat's activation JWT in a header, NOT a per-event HMAC. The
|
|
37
|
+
* exporter is Claude Code itself — it has no access to the seat signing key
|
|
38
|
+
* and cannot be made to sign individual metric points. A bearer credential the
|
|
39
|
+
* server validates is the honest mechanism available here; the ingest route
|
|
40
|
+
* treats it accordingly and never trusts anything else in the payload.
|
|
41
|
+
*/
|
|
42
|
+
export declare function otelEnvFor(serverUrl: string, seatJwt: string): OtelEnv;
|
|
43
|
+
/** Keys this module owns. Anything else in `env` is the user's and is left
|
|
44
|
+
* untouched — including on disable, which removes only these. */
|
|
45
|
+
export declare const MANAGED_KEYS: readonly ["CLAUDE_CODE_ENABLE_TELEMETRY", "OTEL_METRICS_EXPORTER", "OTEL_LOGS_EXPORTER", "OTEL_EXPORTER_OTLP_PROTOCOL", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_METRIC_EXPORT_INTERVAL"];
|
|
46
|
+
type Settings = Record<string, unknown> & {
|
|
47
|
+
env?: Record<string, string>;
|
|
48
|
+
};
|
|
49
|
+
export declare function readSettings(file?: string): Settings;
|
|
50
|
+
/**
|
|
51
|
+
* Merge the OTel env into settings.json.
|
|
52
|
+
*
|
|
53
|
+
* Returns `false` without writing when the file exists but doesn't parse:
|
|
54
|
+
* overwriting a developer's hooks and permissions to enable a cost dashboard
|
|
55
|
+
* is a spectacularly bad trade.
|
|
56
|
+
*/
|
|
57
|
+
export declare function enableOtel(serverUrl: string, seatJwt: string, file?: string): {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
path: string;
|
|
60
|
+
reason?: string;
|
|
61
|
+
};
|
|
62
|
+
/** Remove only the keys we manage, leaving the rest of `env` alone. */
|
|
63
|
+
export declare function disableOtel(file?: string): {
|
|
64
|
+
ok: boolean;
|
|
65
|
+
path: string;
|
|
66
|
+
};
|
|
67
|
+
/** Whether host-usage capture is currently wired up — used by `doctor`. */
|
|
68
|
+
export declare function otelEnabled(file?: string): boolean;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving model-preference conflicts, interactively, at a moment when
|
|
3
|
+
* there's actually a human present.
|
|
4
|
+
*
|
|
5
|
+
* The split matters: conflicts are DETECTED during `serve`, which cannot ask
|
|
6
|
+
* anything — stdout is the MCP JSON-RPC channel and there's no terminal. So
|
|
7
|
+
* serve records them and keeps the local pin, and the question is asked the
|
|
8
|
+
* next time someone runs an interactive command.
|
|
9
|
+
*
|
|
10
|
+
* The permission gate is the other half. An org's model policy can forbid the
|
|
11
|
+
* model a user pinned locally. Offering "keep yours" there would be offering
|
|
12
|
+
* something that cannot work — the engine would refuse it downstream — so a
|
|
13
|
+
* policy-blocked pin isn't presented as a choice. It's reported, and the
|
|
14
|
+
* server value is taken, because that is the only outcome available.
|
|
15
|
+
*/
|
|
16
|
+
import { type PrefConflict } from "./model-prefs.js";
|
|
17
|
+
import type { ModelPolicy } from "./model-policy.js";
|
|
18
|
+
export type ConflictOutcome = {
|
|
19
|
+
action: "none";
|
|
20
|
+
} | {
|
|
21
|
+
action: "resolved";
|
|
22
|
+
kept: number;
|
|
23
|
+
updated: number;
|
|
24
|
+
policyForced: number;
|
|
25
|
+
};
|
|
26
|
+
/** Only ask where a person can answer. A prompt written to a pipe would hang
|
|
27
|
+
* a script forever, which is a far worse failure than a stale preference. */
|
|
28
|
+
export declare function canPrompt(): boolean;
|
|
29
|
+
/** Is the locally pinned primary something org policy would refuse? If so,
|
|
30
|
+
* "keep mine" isn't a real option and shouldn't be offered as one. */
|
|
31
|
+
export declare function localPinAllowed(c: PrefConflict, policy?: ModelPolicy): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Ask about any pending conflicts. No-op when there are none, when there's no
|
|
34
|
+
* terminal, or when the user declines to engage.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolvePendingConflicts(policy?: ModelPolicy): Promise<ConflictOutcome>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-healing reconciliation, run on every `serve`.
|
|
3
|
+
*
|
|
4
|
+
* THE PRINCIPLE: anything `install` does that is idempotent and
|
|
5
|
+
* non-interactive should also happen every time the engine starts. `install`
|
|
6
|
+
* is then genuinely first-run-only — activation, key capture, MCP
|
|
7
|
+
* registration — and a seat converges on the correct configuration by simply
|
|
8
|
+
* being used, rather than by someone remembering to re-run a command.
|
|
9
|
+
*
|
|
10
|
+
* Before this existed, two things only `install` did, so a seat that merely
|
|
11
|
+
* reconnected drifted:
|
|
12
|
+
*
|
|
13
|
+
* 1. The OTel host-telemetry config. Never refreshed — and worse, it embeds
|
|
14
|
+
* the seat JWT, which expires after 7 days. Once it lapsed, host token
|
|
15
|
+
* capture 401'd silently and forever, with nothing anywhere to say why.
|
|
16
|
+
* 2. Server-side model preferences. `syncModelPreferencesFromServer` was
|
|
17
|
+
* reachable ONLY through `crosscheck models sync`, so a model chosen in
|
|
18
|
+
* the browser reached a machine only if its owner happened to run that
|
|
19
|
+
* command. The picker saved a choice that nothing collected.
|
|
20
|
+
* 3. Provider key METADATA (presence and last-4 only, never material).
|
|
21
|
+
* Without it the browser knows nothing about any machine except the one
|
|
22
|
+
* it is paired to, so a key that lapsed on another machine is invisible
|
|
23
|
+
* until a panel silently comes back short a provider.
|
|
24
|
+
*
|
|
25
|
+
* Everything here is best-effort and fails OPEN. This runs on the critical
|
|
26
|
+
* path of an MCP connection: a slow or unreachable server must cost a stale
|
|
27
|
+
* preference, never a failed startup.
|
|
28
|
+
*/
|
|
29
|
+
export interface ReconcileReport {
|
|
30
|
+
/** "written" when we (re)wrote the OTel config, "current" when it already
|
|
31
|
+
* matched, "skipped" when there was no JWT, "failed" with a reason. */
|
|
32
|
+
otel: "written" | "current" | "skipped" | "failed";
|
|
33
|
+
otelReason?: string;
|
|
34
|
+
/** "synced" when server preferences were pulled, "timeout" when the server
|
|
35
|
+
* didn't answer in time, "skipped" without a JWT, "failed" otherwise. */
|
|
36
|
+
prefs: "synced" | "timeout" | "skipped" | "failed";
|
|
37
|
+
/**
|
|
38
|
+
* "dispatched" when a provider-metadata report was started, "skipped" when
|
|
39
|
+
* reporting is off or the seat has no usable JWT.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately NOT the send's outcome. The report is fire-and-forget, so by
|
|
42
|
+
* the time it succeeds or fails this object has already been returned and
|
|
43
|
+
* read; a field claiming to hold the result would be reporting a value its
|
|
44
|
+
* reader can never have seen. Failures surface via CROSSCHECK_DEBUG.
|
|
45
|
+
*/
|
|
46
|
+
keyStatus: "dispatched" | "skipped";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Bring this machine's configuration in line with the server and with what
|
|
50
|
+
* the current release expects. Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export declare function reconcileSeat(args: {
|
|
53
|
+
jwt: string | null;
|
|
54
|
+
serverUrl: string;
|
|
55
|
+
}): Promise<ReconcileReport>;
|
|
56
|
+
/** One line to stderr when something was actually corrected. Silent on the
|
|
57
|
+
* happy path — a message every single launch trains people to ignore it.
|
|
58
|
+
* stderr, never stdout: stdout is the MCP JSON-RPC channel. */
|
|
59
|
+
export declare function reportReconcile(r: ReconcileReport): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical event signing. MUST match the server's
|
|
3
|
+
* src/lib/crosscheck/crypto.ts byte-for-byte. Any drift breaks telemetry
|
|
4
|
+
* ingest because the server re-derives the seat HMAC key and recomputes
|
|
5
|
+
* the signature with the canonical body — if the strings differ, the
|
|
6
|
+
* signature won't verify.
|
|
7
|
+
*
|
|
8
|
+
* If the server adds a field to SignableEvent, this file must change in
|
|
9
|
+
* lockstep. Tests on both sides should sign + verify the same fixture to
|
|
10
|
+
* catch divergence.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Order-stable, delimiter-safe canonical form of a usage event. Excludes
|
|
14
|
+
* the signature itself. Pipe-separated. Numbers stringified; cost is
|
|
15
|
+
* fixed at 6 decimals (toFixed(6)) to avoid floating-point drift.
|
|
16
|
+
*/
|
|
17
|
+
export type SignableEvent = {
|
|
18
|
+
eventId: string;
|
|
19
|
+
seatId: string;
|
|
20
|
+
orgId: string;
|
|
21
|
+
ts: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
model: string;
|
|
24
|
+
pattern: string;
|
|
25
|
+
promptTokens: number;
|
|
26
|
+
completionTokens: number;
|
|
27
|
+
costUsdEstimate: number;
|
|
28
|
+
latencyMs: number;
|
|
29
|
+
status: string;
|
|
30
|
+
errorClass?: string;
|
|
31
|
+
/** Groups every call emitted by one tool invocation. */
|
|
32
|
+
runId?: string;
|
|
33
|
+
/** Role of this call inside the run (debate / synth / worker / …). */
|
|
34
|
+
purpose?: string;
|
|
35
|
+
/** Opaque git-context fingerprint for feature attribution. */
|
|
36
|
+
fingerprint?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Canonical body, v2.
|
|
40
|
+
*
|
|
41
|
+
* The first 13 fields are FROZEN — seats already in the field sign exactly
|
|
42
|
+
* that string, and the server must keep verifying it. runId/purpose are
|
|
43
|
+
* appended only when at least one is present, so a legacy event yields the
|
|
44
|
+
* identical v1 string it always did.
|
|
45
|
+
*/
|
|
46
|
+
export declare function canonicalEventBody(e: SignableEvent): string;
|
|
47
|
+
/** Sign a usage event with the per-seat HMAC key obtained at activation. */
|
|
48
|
+
export declare function signEvent(seatSigningKey: string, e: SignableEvent): string;
|