@miadi/connector 0.1.12 → 0.1.13
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 +6 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/mission-status.d.ts +121 -15
- package/dist/mission-status.js +198 -27
- package/dist/model-preflight.d.ts +144 -0
- package/dist/model-preflight.js +323 -0
- package/dist/providers.d.ts +29 -3
- package/dist/providers.js +32 -11
- package/dist/server.d.ts +5 -1
- package/dist/server.js +2 -0
- package/dist/tools.d.ts +12 -1
- package/dist/tools.js +102 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { type JevConfig, type ProviderAvailability, type ProviderId } from "./providers.js";
|
|
2
|
+
export declare const PREFLIGHT_CONTRACT = "miadi.mission_model_preflight";
|
|
3
|
+
export declare const PREFLIGHT_VERSION = 1;
|
|
4
|
+
/** Below this, a Jev choice is not acted on (the ≥0.8 practice of jgwill/Miadi#770). */
|
|
5
|
+
export declare const JEV_CONFIDENCE_FLOOR = 0.8;
|
|
6
|
+
export declare const JEV_MODEL = "jev-latest";
|
|
7
|
+
export declare const TASK_TYPES: readonly ["file_relevance", "architecture_refactor", "implementation", "review"];
|
|
8
|
+
export type TaskType = (typeof TASK_TYPES)[number];
|
|
9
|
+
export declare const CRITICALITIES: readonly ["normal", "security_critical"];
|
|
10
|
+
export type Criticality = (typeof CRITICALITIES)[number];
|
|
11
|
+
export declare const REVIEW_TIERS: readonly ["standard", "stronger_reviewer", "human"];
|
|
12
|
+
export type ReviewTier = (typeof REVIEW_TIERS)[number];
|
|
13
|
+
export interface ModelCandidate {
|
|
14
|
+
/** provider, or provider:model when a model is known; the option Jev is asked to choose. */
|
|
15
|
+
option: string;
|
|
16
|
+
provider: ProviderId;
|
|
17
|
+
/** A concrete model id when the host shows one, else null. */
|
|
18
|
+
model_id: string | null;
|
|
19
|
+
/** configured_default: the provider's own config names it; it is not proof of what a launch will use. */
|
|
20
|
+
model_id_source: "configured_default" | "fixture" | "unknown";
|
|
21
|
+
/** What is known of the model's strength, with its source; absent on a live host, where nothing is measured yet. */
|
|
22
|
+
capability_note?: string;
|
|
23
|
+
installed: boolean;
|
|
24
|
+
version?: string;
|
|
25
|
+
/** Config-directory evidence only: never a verified sign-in. */
|
|
26
|
+
auth: "config_dir_only" | "none";
|
|
27
|
+
quota: "unknown";
|
|
28
|
+
eligible: boolean;
|
|
29
|
+
policy_reason: string;
|
|
30
|
+
}
|
|
31
|
+
export interface LaunchEvidence {
|
|
32
|
+
provider: ProviderId;
|
|
33
|
+
model?: string;
|
|
34
|
+
/** What shows it (a process line, a pane header); reported by the caller, not checked by the connector. */
|
|
35
|
+
evidence: string;
|
|
36
|
+
}
|
|
37
|
+
export type PreflightState = "recommended" | "abstained" | "provider_mismatch";
|
|
38
|
+
export type DeciderSource = "jev" | "static_fallback" | "synthetic_fixture";
|
|
39
|
+
export type RouterState = "answered" | "not_requested" | "router_unconfigured" | "router_unavailable" | "invalid_answer";
|
|
40
|
+
export interface ModelPreflight {
|
|
41
|
+
contract: typeof PREFLIGHT_CONTRACT;
|
|
42
|
+
version: typeof PREFLIGHT_VERSION;
|
|
43
|
+
state: PreflightState;
|
|
44
|
+
mission: {
|
|
45
|
+
id: string;
|
|
46
|
+
resolution: string;
|
|
47
|
+
authorizes_actions: boolean;
|
|
48
|
+
primary_issue?: string;
|
|
49
|
+
};
|
|
50
|
+
/** sha256 of the decision state (what Jev sees or would see), for audit and replay. */
|
|
51
|
+
input_hash: string;
|
|
52
|
+
task: {
|
|
53
|
+
type: TaskType;
|
|
54
|
+
criticality: Criticality;
|
|
55
|
+
};
|
|
56
|
+
approved_provider: ProviderId | null;
|
|
57
|
+
/** approved: the request was approved with this provider. requested: it is not approved (yet); the provider is only asked for. */
|
|
58
|
+
provider_basis: "approved" | "requested";
|
|
59
|
+
candidates: ModelCandidate[];
|
|
60
|
+
decider: {
|
|
61
|
+
source: DeciderSource;
|
|
62
|
+
router: RouterState;
|
|
63
|
+
model?: string;
|
|
64
|
+
calibration: "uncalibrated" | "not_applicable";
|
|
65
|
+
confidence_floor?: number;
|
|
66
|
+
derivation: string;
|
|
67
|
+
detail?: string;
|
|
68
|
+
};
|
|
69
|
+
recommendation: {
|
|
70
|
+
provider?: ProviderId;
|
|
71
|
+
model_id?: string | null;
|
|
72
|
+
confidence: number | null;
|
|
73
|
+
probabilities?: Record<string, number>;
|
|
74
|
+
review_tier: ReviewTier;
|
|
75
|
+
explanation: string;
|
|
76
|
+
abstain_reasons: string[];
|
|
77
|
+
};
|
|
78
|
+
evidence: {
|
|
79
|
+
latency_ms?: number;
|
|
80
|
+
jev_billing?: Record<string, string>;
|
|
81
|
+
model_cost: "unknown";
|
|
82
|
+
quota: "unknown";
|
|
83
|
+
};
|
|
84
|
+
launch: {
|
|
85
|
+
approved_provider: ProviderId | null;
|
|
86
|
+
actual: "unverified" | (LaunchEvidence & {
|
|
87
|
+
source: "reported";
|
|
88
|
+
});
|
|
89
|
+
mismatch: boolean | null;
|
|
90
|
+
};
|
|
91
|
+
policy: {
|
|
92
|
+
deterministic: true;
|
|
93
|
+
authorizes: false;
|
|
94
|
+
note: string;
|
|
95
|
+
};
|
|
96
|
+
activation_plan?: string[];
|
|
97
|
+
}
|
|
98
|
+
/** What a decider returns for one request: answers by question id, and what the call cost. */
|
|
99
|
+
export interface DeciderAnswer {
|
|
100
|
+
answers: Record<string, unknown>;
|
|
101
|
+
billing?: Record<string, string>;
|
|
102
|
+
latency_ms?: number;
|
|
103
|
+
}
|
|
104
|
+
export interface JevAdapter {
|
|
105
|
+
source: "jev" | "synthetic_fixture";
|
|
106
|
+
model: string;
|
|
107
|
+
decide(request: JevRequest): Promise<DeciderAnswer>;
|
|
108
|
+
}
|
|
109
|
+
export interface JevRequest {
|
|
110
|
+
state: Record<string, unknown>;
|
|
111
|
+
model: string;
|
|
112
|
+
questions: Record<string, {
|
|
113
|
+
type: "choice";
|
|
114
|
+
instructions: string;
|
|
115
|
+
criteria: Record<string, string>;
|
|
116
|
+
}>;
|
|
117
|
+
}
|
|
118
|
+
/** The model a provider's own config names, when it names one. Only that key is read. */
|
|
119
|
+
export declare function configuredModel(provider: ProviderId, home?: string): string | null;
|
|
120
|
+
export declare function candidatesFor(found: ProviderAvailability[], approved: ProviderId | null, modelOf?: (p: ProviderId) => string | null, basis?: "approved" | "requested"): ModelCandidate[];
|
|
121
|
+
/** The least review a task gets, whatever any decider says. Jev may raise it, never lower it. */
|
|
122
|
+
export declare function reviewFloor(task: TaskType, criticality: Criticality): ReviewTier;
|
|
123
|
+
export declare function decisionState(task: TaskType, criticality: Criticality, candidates: ModelCandidate[]): Record<string, unknown>;
|
|
124
|
+
export declare function hashState(state: unknown): string;
|
|
125
|
+
export declare function buildJevRequest(state: Record<string, unknown>, candidates: ModelCandidate[], model?: string): JevRequest;
|
|
126
|
+
/** A choice answer against its question: the failures, empty when it holds. */
|
|
127
|
+
export declare function checkChoice(criteria: Record<string, string>, answer: any): string[];
|
|
128
|
+
/** One call to Jev, never resent: a lost answer may already have been charged. */
|
|
129
|
+
export declare function jevAdapter(config: JevConfig, fetchImpl?: typeof fetch, timeoutMs?: number): JevAdapter;
|
|
130
|
+
export interface PreflightInput {
|
|
131
|
+
mission: ModelPreflight["mission"];
|
|
132
|
+
approved_provider: ProviderId | null;
|
|
133
|
+
provider_basis?: "approved" | "requested";
|
|
134
|
+
task: TaskType;
|
|
135
|
+
criticality: Criticality;
|
|
136
|
+
candidates: ModelCandidate[];
|
|
137
|
+
launch?: LaunchEvidence;
|
|
138
|
+
/** The decider to consult, when the caller asked for one; absent means static fallback only. */
|
|
139
|
+
adapter?: JevAdapter;
|
|
140
|
+
/** Why no adapter: not_requested or router_unconfigured. */
|
|
141
|
+
routerWhenAbsent?: "not_requested" | "router_unconfigured";
|
|
142
|
+
}
|
|
143
|
+
export declare const ACTIVATION_PLAN: string[];
|
|
144
|
+
export declare function modelPreflight(input: PreflightInput): Promise<ModelPreflight>;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mission model-choice preflight (jgwill/Miadi#913 C): a typed, versioned,
|
|
3
|
+
* read-only answer to "which provider and model fits this mission's task, and
|
|
4
|
+
* how do we know?" — never an authorization.
|
|
5
|
+
*
|
|
6
|
+
* - Policy is deterministic and decided here, not by Jev: the approved provider
|
|
7
|
+
* is kept (no switching without a person), Claude Code only when named, an
|
|
8
|
+
* uninstalled or sign-in-less provider is ineligible, and a reported launch on
|
|
9
|
+
* another provider fails closed as provider_mismatch before anything is asked.
|
|
10
|
+
* - Jev is consulted only when the caller asks (consult_jev) and the connector's
|
|
11
|
+
* environment holds a Jev key. The state sent holds the task type, the
|
|
12
|
+
* criticality and the candidates: no task text, mission id, repository name or
|
|
13
|
+
* source. Its answer is advisory: below the confidence floor, choosing an
|
|
14
|
+
* ineligible option, or failing the answer contract, the preflight abstains.
|
|
15
|
+
* - Every answer says where its choice came from: jev, static_fallback or
|
|
16
|
+
* synthetic_fixture, and Jev's confidence is marked uncalibrated: no calibration
|
|
17
|
+
* set exists yet for this question.
|
|
18
|
+
*
|
|
19
|
+
* The request and answer checks follow lane-check's decider (smcraft
|
|
20
|
+
* examples/decision-gate/lane-check/lib/decider.mjs, revision R9), reduced to
|
|
21
|
+
* the choice questions asked here.
|
|
22
|
+
*/
|
|
23
|
+
import { createHash } from "node:crypto";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { homedir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { PROVIDERS } from "./providers.js";
|
|
28
|
+
export const PREFLIGHT_CONTRACT = "miadi.mission_model_preflight";
|
|
29
|
+
export const PREFLIGHT_VERSION = 1;
|
|
30
|
+
/** Below this, a Jev choice is not acted on (the ≥0.8 practice of jgwill/Miadi#770). */
|
|
31
|
+
export const JEV_CONFIDENCE_FLOOR = 0.8;
|
|
32
|
+
export const JEV_MODEL = "jev-latest";
|
|
33
|
+
const SUM_TOLERANCE = 0.025;
|
|
34
|
+
export const TASK_TYPES = ["file_relevance", "architecture_refactor", "implementation", "review"];
|
|
35
|
+
export const CRITICALITIES = ["normal", "security_critical"];
|
|
36
|
+
export const REVIEW_TIERS = ["standard", "stronger_reviewer", "human"];
|
|
37
|
+
// ── candidates, by deterministic policy ──
|
|
38
|
+
/** The model a provider's own config names, when it names one. Only that key is read. */
|
|
39
|
+
export function configuredModel(provider, home = homedir()) {
|
|
40
|
+
try {
|
|
41
|
+
if (provider === "codex") {
|
|
42
|
+
const line = readFileSync(join(home, ".codex", "config.toml"), "utf8").split("\n").find((l) => /^\s*model\s*=/.test(l));
|
|
43
|
+
return line ? (/=\s*"([^"]+)"/.exec(line)?.[1] ?? null) : null;
|
|
44
|
+
}
|
|
45
|
+
if (provider === "claude") {
|
|
46
|
+
const model = JSON.parse(readFileSync(join(home, ".claude", "settings.json"), "utf8"))?.model;
|
|
47
|
+
return typeof model === "string" && model ? model : null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
export function candidatesFor(found, approved, modelOf = (p) => configuredModel(p), basis = "approved") {
|
|
56
|
+
return PROVIDERS.map((spec) => {
|
|
57
|
+
const a = found.find((p) => p.id === spec.id);
|
|
58
|
+
const installed = Boolean(a?.installed);
|
|
59
|
+
const auth = a?.auth_evidence === "found" ? "config_dir_only" : "none";
|
|
60
|
+
const model = installed ? modelOf(spec.id) : null;
|
|
61
|
+
let eligible = true;
|
|
62
|
+
let reason = "Installed, with sign-in evidence (config directory only; quota unknown).";
|
|
63
|
+
if (!installed)
|
|
64
|
+
[eligible, reason] = [false, "Not installed on the connector host."];
|
|
65
|
+
else if (auth === "none")
|
|
66
|
+
[eligible, reason] = [false, "No sign-in evidence on this host."];
|
|
67
|
+
else if (approved && spec.id !== approved)
|
|
68
|
+
[eligible, reason] = [false, `Not the ${basis} provider (${approved}); switching needs a person's approval.`];
|
|
69
|
+
else if (!approved && !spec.selectable_by_default)
|
|
70
|
+
[eligible, reason] = [false, `${spec.name} runs only when a request names it.`];
|
|
71
|
+
return {
|
|
72
|
+
option: model ? `${spec.id}:${model}` : spec.id,
|
|
73
|
+
provider: spec.id,
|
|
74
|
+
model_id: model,
|
|
75
|
+
model_id_source: model ? "configured_default" : "unknown",
|
|
76
|
+
installed,
|
|
77
|
+
...(a?.version ? { version: a.version } : {}),
|
|
78
|
+
auth,
|
|
79
|
+
quota: "unknown",
|
|
80
|
+
eligible,
|
|
81
|
+
policy_reason: reason,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** The least review a task gets, whatever any decider says. Jev may raise it, never lower it. */
|
|
86
|
+
export function reviewFloor(task, criticality) {
|
|
87
|
+
return criticality === "security_critical" || task === "architecture_refactor" ? "stronger_reviewer" : "standard";
|
|
88
|
+
}
|
|
89
|
+
const tierRank = (t) => REVIEW_TIERS.indexOf(t);
|
|
90
|
+
// ── the Jev request, with nothing private in it ──
|
|
91
|
+
export function decisionState(task, criticality, candidates) {
|
|
92
|
+
return {
|
|
93
|
+
task: { type: task, criticality },
|
|
94
|
+
candidates: candidates
|
|
95
|
+
.filter((c) => c.installed)
|
|
96
|
+
.map((c) => ({
|
|
97
|
+
option: c.option,
|
|
98
|
+
provider: c.provider,
|
|
99
|
+
model_id: c.model_id,
|
|
100
|
+
model_id_source: c.model_id_source,
|
|
101
|
+
...(c.capability_note ? { capability_note: c.capability_note } : {}),
|
|
102
|
+
auth: c.auth,
|
|
103
|
+
quota: c.quota,
|
|
104
|
+
eligible: c.eligible,
|
|
105
|
+
})),
|
|
106
|
+
policy: "Only an option with eligible true may be chosen. Choose abstain when no eligible option fits the task with the evidence given.",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function hashState(state) {
|
|
110
|
+
return createHash("sha256").update(JSON.stringify(state)).digest("hex");
|
|
111
|
+
}
|
|
112
|
+
export function buildJevRequest(state, candidates, model = JEV_MODEL) {
|
|
113
|
+
const fit = {};
|
|
114
|
+
for (const c of candidates.filter((x) => x.eligible)) {
|
|
115
|
+
const model = c.model_id ? ` with model ${c.model_id}${c.model_id_source === "configured_default" ? " (its configured default)" : ""}` : ", model unknown";
|
|
116
|
+
fit[c.option] = `${c.provider}${model}${c.capability_note ? ` (${c.capability_note})` : ""}; sign-in evidence is a config directory only; quota unknown.`;
|
|
117
|
+
}
|
|
118
|
+
fit.abstain = "No eligible option fits this task type and criticality on the evidence given.";
|
|
119
|
+
return {
|
|
120
|
+
state,
|
|
121
|
+
model,
|
|
122
|
+
questions: {
|
|
123
|
+
fit: {
|
|
124
|
+
type: "choice",
|
|
125
|
+
instructions: "Which option in `candidates` best fits a task of type `task.type` with criticality `task.criticality`? A file-relevance task needs high recall; an architecture or refactor task needs a strong model.",
|
|
126
|
+
criteria: fit,
|
|
127
|
+
},
|
|
128
|
+
review_tier: {
|
|
129
|
+
type: "choice",
|
|
130
|
+
instructions: "What review should the result of this task get before it is merged?",
|
|
131
|
+
criteria: {
|
|
132
|
+
standard: "One reviewer pass is enough.",
|
|
133
|
+
stronger_reviewer: "A stronger model or reviewer must check it: security-relevant files or architecture are involved.",
|
|
134
|
+
human: "A person must review it before anything is merged.",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const isUnit = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 && x <= 1;
|
|
141
|
+
/** A choice answer against its question: the failures, empty when it holds. */
|
|
142
|
+
export function checkChoice(criteria, answer) {
|
|
143
|
+
const out = [];
|
|
144
|
+
if (!answer || typeof answer !== "object")
|
|
145
|
+
return ["no answer"];
|
|
146
|
+
if (answer.type !== "choice")
|
|
147
|
+
out.push(`answered a ${answer.type}, asked a choice`);
|
|
148
|
+
if (!isUnit(answer.confidence))
|
|
149
|
+
out.push(`confidence ${answer.confidence} is not within 0 and 1`);
|
|
150
|
+
const keys = Object.keys(criteria);
|
|
151
|
+
const probs = answer.probabilities;
|
|
152
|
+
if (!probs || typeof probs !== "object")
|
|
153
|
+
return [...out, "no probabilities"];
|
|
154
|
+
const outOfRange = Object.keys(probs).filter((k) => !isUnit(probs[k]));
|
|
155
|
+
if (outOfRange.length)
|
|
156
|
+
out.push(`probabilities outside 0 and 1: ${outOfRange.join(", ")}`);
|
|
157
|
+
const missing = keys.filter((k) => !(k in probs));
|
|
158
|
+
const extra = Object.keys(probs).filter((k) => !keys.includes(k));
|
|
159
|
+
if (missing.length || extra.length)
|
|
160
|
+
out.push("probabilities do not cover exactly the declared options");
|
|
161
|
+
const sum = keys.reduce((acc, k) => acc + (isUnit(probs[k]) ? probs[k] : 0), 0);
|
|
162
|
+
if (!missing.length && Math.abs(sum - 1) > SUM_TOLERANCE)
|
|
163
|
+
out.push(`probabilities sum to ${sum.toFixed(3)}`);
|
|
164
|
+
if (!keys.includes(answer.choice))
|
|
165
|
+
out.push(`chose ${answer.choice}, not a declared option`);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
// ── the adapter to the real service ──
|
|
169
|
+
/** One call to Jev, never resent: a lost answer may already have been charged. */
|
|
170
|
+
export function jevAdapter(config, fetchImpl = fetch, timeoutMs = 60_000) {
|
|
171
|
+
return {
|
|
172
|
+
source: "jev",
|
|
173
|
+
model: JEV_MODEL,
|
|
174
|
+
async decide(request) {
|
|
175
|
+
const t0 = Date.now();
|
|
176
|
+
const res = await fetchImpl(config.url, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json", accept: "application/json" },
|
|
179
|
+
body: JSON.stringify(request),
|
|
180
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
181
|
+
});
|
|
182
|
+
const latency_ms = Date.now() - t0;
|
|
183
|
+
const billing = {};
|
|
184
|
+
res.headers.forEach((v, k) => {
|
|
185
|
+
if (k.startsWith("x-jev-"))
|
|
186
|
+
billing[k.slice(6).replace(/-/g, "_")] = v;
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok)
|
|
189
|
+
throw new Error(`Jev answered ${res.status}${billing.run_id ? ` [run ${billing.run_id}]` : ""}`);
|
|
190
|
+
const body = await res.json();
|
|
191
|
+
return { answers: body?.answers ?? {}, billing: { ...(body?.billing ?? {}), ...billing }, latency_ms };
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export const ACTIVATION_PLAN = [
|
|
196
|
+
"Put JEV_AI_API_KEY (or TYPESAFE_API_KEY) in an environment file readable only by the connector's user, never in the repository or the unit file.",
|
|
197
|
+
"Add that file to miadi-connector.service as EnvironmentFile and restart the service.",
|
|
198
|
+
"Call provider_capabilities: router.state must be reachable (GET /v1/models, which runs no inference and is not charged).",
|
|
199
|
+
"Call mission_model_preflight with consult_jev true on one mission. Each such call is charged by Jev; its billing headers come back in evidence.jev_billing.",
|
|
200
|
+
];
|
|
201
|
+
export async function modelPreflight(input) {
|
|
202
|
+
const { task, criticality, candidates, approved_provider } = input;
|
|
203
|
+
const state = decisionState(task, criticality, candidates);
|
|
204
|
+
const floor = reviewFloor(task, criticality);
|
|
205
|
+
const eligible = candidates.filter((c) => c.eligible);
|
|
206
|
+
const base = {
|
|
207
|
+
contract: PREFLIGHT_CONTRACT,
|
|
208
|
+
version: PREFLIGHT_VERSION,
|
|
209
|
+
mission: input.mission,
|
|
210
|
+
input_hash: hashState(state),
|
|
211
|
+
task: { type: task, criticality },
|
|
212
|
+
approved_provider,
|
|
213
|
+
provider_basis: input.provider_basis ?? "approved",
|
|
214
|
+
candidates,
|
|
215
|
+
evidence: { model_cost: "unknown", quota: "unknown" },
|
|
216
|
+
policy: {
|
|
217
|
+
deterministic: true,
|
|
218
|
+
authorizes: false,
|
|
219
|
+
note: "A recommendation authorizes nothing. Permissions, provider switching, deployment and spending stay with the approved request and a person's decision.",
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const launch = input.launch
|
|
223
|
+
? { approved_provider, actual: { ...input.launch, source: "reported" }, mismatch: approved_provider ? input.launch.provider !== approved_provider : null }
|
|
224
|
+
: { approved_provider, actual: "unverified", mismatch: null };
|
|
225
|
+
// Fail closed before anything is asked or spent.
|
|
226
|
+
if (launch.mismatch) {
|
|
227
|
+
return {
|
|
228
|
+
...base,
|
|
229
|
+
state: "provider_mismatch",
|
|
230
|
+
decider: { source: "static_fallback", router: "not_requested", calibration: "not_applicable", derivation: "deterministic policy: the reported launch is not the approved provider" },
|
|
231
|
+
recommendation: {
|
|
232
|
+
confidence: null,
|
|
233
|
+
review_tier: floor,
|
|
234
|
+
explanation: `${(input.provider_basis ?? "approved") === "approved" ? "Approved" : "Requested (not approved)"} ${approved_provider}, reported running on ${input.launch.provider}. Nothing proceeds on an unapproved provider; a person re-approves or the launch is corrected.`,
|
|
235
|
+
abstain_reasons: ["provider_mismatch"],
|
|
236
|
+
},
|
|
237
|
+
launch,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const fallback = (router, reasons, detail, extra = {}) => {
|
|
241
|
+
const pick = approved_provider ? eligible.find((c) => c.provider === approved_provider) : eligible[0];
|
|
242
|
+
const abstain = [...reasons, ...(pick ? [] : [approved_provider ? "approved_provider_ineligible" : "no_eligible_provider"])];
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
state: pick && !reasons.length ? "recommended" : "abstained",
|
|
246
|
+
decider: {
|
|
247
|
+
source: "static_fallback",
|
|
248
|
+
router,
|
|
249
|
+
calibration: "not_applicable",
|
|
250
|
+
derivation: approved_provider ? `deterministic policy: the ${input.provider_basis ?? "approved"} provider and its configured default model` : "deterministic policy: preference order codex, pi, copilot; claude only when named",
|
|
251
|
+
...(detail ? { detail } : {}),
|
|
252
|
+
},
|
|
253
|
+
recommendation: {
|
|
254
|
+
...(pick && !reasons.length ? { provider: pick.provider, model_id: pick.model_id } : {}),
|
|
255
|
+
confidence: null,
|
|
256
|
+
review_tier: floor,
|
|
257
|
+
explanation: pick
|
|
258
|
+
? reasons.length
|
|
259
|
+
? `The static policy would keep ${pick.option}, but the preflight abstains: ${reasons.join(", ")}.`
|
|
260
|
+
: `${pick.option} by static policy. No task-aware choice was made; its model is the provider's configured default.`
|
|
261
|
+
: `No eligible option: ${candidates.map((c) => `${c.provider} (${c.policy_reason})`).join("; ")}`,
|
|
262
|
+
abstain_reasons: pick && !reasons.length ? [] : abstain,
|
|
263
|
+
},
|
|
264
|
+
evidence: { ...base.evidence, ...extra },
|
|
265
|
+
launch,
|
|
266
|
+
...(router === "router_unconfigured" ? { activation_plan: ACTIVATION_PLAN } : {}),
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
if (!input.adapter)
|
|
270
|
+
return fallback(input.routerWhenAbsent ?? "not_requested", []);
|
|
271
|
+
if (!eligible.length)
|
|
272
|
+
return fallback("not_requested", [], "No eligible option, so Jev was not asked.");
|
|
273
|
+
const request = buildJevRequest(state, candidates, input.adapter.model);
|
|
274
|
+
let answer;
|
|
275
|
+
try {
|
|
276
|
+
answer = await input.adapter.decide(request);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return fallback("router_unavailable", ["jev_unavailable"], error instanceof Error ? error.message : String(error));
|
|
280
|
+
}
|
|
281
|
+
const fitQ = request.questions.fit.criteria;
|
|
282
|
+
const tierQ = request.questions.review_tier.criteria;
|
|
283
|
+
const fit = answer.answers.fit;
|
|
284
|
+
const tier = answer.answers.review_tier;
|
|
285
|
+
const failures = [...checkChoice(fitQ, fit).map((f) => `fit: ${f}`), ...checkChoice(tierQ, tier).map((f) => `review_tier: ${f}`)];
|
|
286
|
+
const spent = { ...(answer.latency_ms !== undefined ? { latency_ms: answer.latency_ms } : {}), ...(answer.billing && Object.keys(answer.billing).length ? { jev_billing: answer.billing } : {}) };
|
|
287
|
+
if (failures.length)
|
|
288
|
+
return fallback("invalid_answer", ["jev_invalid_answer"], failures.join("; "), spent);
|
|
289
|
+
const chosen = eligible.find((c) => c.option === fit.choice);
|
|
290
|
+
const reviewTier = tierRank(tier.choice) > tierRank(floor) ? tier.choice : floor;
|
|
291
|
+
const reasons = [];
|
|
292
|
+
if (fit.choice === "abstain")
|
|
293
|
+
reasons.push("jev_abstained");
|
|
294
|
+
else if (!chosen)
|
|
295
|
+
reasons.push("jev_choice_ineligible");
|
|
296
|
+
if (fit.confidence < JEV_CONFIDENCE_FLOOR)
|
|
297
|
+
reasons.push("jev_low_confidence");
|
|
298
|
+
const recommend = !reasons.length && chosen;
|
|
299
|
+
return {
|
|
300
|
+
...base,
|
|
301
|
+
state: recommend ? "recommended" : "abstained",
|
|
302
|
+
decider: {
|
|
303
|
+
source: input.adapter.source,
|
|
304
|
+
router: "answered",
|
|
305
|
+
model: input.adapter.model,
|
|
306
|
+
calibration: "uncalibrated",
|
|
307
|
+
confidence_floor: JEV_CONFIDENCE_FLOOR,
|
|
308
|
+
derivation: "Jev chose among the eligible options; the policy decided eligibility and the review floor, and abstains below the confidence floor",
|
|
309
|
+
},
|
|
310
|
+
recommendation: {
|
|
311
|
+
...(recommend ? { provider: chosen.provider, model_id: chosen.model_id } : {}),
|
|
312
|
+
confidence: fit.confidence,
|
|
313
|
+
probabilities: fit.probabilities,
|
|
314
|
+
review_tier: reviewTier,
|
|
315
|
+
explanation: recommend
|
|
316
|
+
? `${input.adapter.source === "jev" ? "Jev" : "The synthetic fixture"} chose ${chosen.option} at confidence ${fit.confidence} (uncalibrated); review ${reviewTier}${reviewTier !== tier.choice ? " (the policy floor)" : ""}.`
|
|
317
|
+
: `Abstained: ${reasons.join(", ")}. ${input.adapter.source === "jev" ? "Jev" : "The fixture"} answered ${fit.choice} at confidence ${fit.confidence}.`,
|
|
318
|
+
abstain_reasons: reasons,
|
|
319
|
+
},
|
|
320
|
+
evidence: { ...base.evidence, ...spent },
|
|
321
|
+
launch,
|
|
322
|
+
};
|
|
323
|
+
}
|
package/dist/providers.d.ts
CHANGED
|
@@ -42,20 +42,46 @@ export type ProviderResolution = {
|
|
|
42
42
|
* by an explicit request.
|
|
43
43
|
*/
|
|
44
44
|
export declare function resolveProvider(requested: string | undefined, found: ProviderAvailability[]): ProviderResolution;
|
|
45
|
+
/**
|
|
46
|
+
* Where Jev is reached from this host (jgwill/Miadi#913 C1), by the names
|
|
47
|
+
* smcraft's lane-check decider uses (examples/decision-gate/lane-check,
|
|
48
|
+
* jgwill/Miadi#770): JEV_AI_API_KEY at jev-ai.pro, else TYPESAFE_API_KEY at
|
|
49
|
+
* typesafe.ai, each with an optional base override; MIADI_JEV_URL is kept as a
|
|
50
|
+
* base override. Only the connector's own environment is read, never a file.
|
|
51
|
+
* The key stays in the returned object for the adapter; nothing here prints it.
|
|
52
|
+
*/
|
|
53
|
+
export interface JevConfig {
|
|
54
|
+
service: string;
|
|
55
|
+
key_name: string;
|
|
56
|
+
base: string;
|
|
57
|
+
url: string;
|
|
58
|
+
models_url: string;
|
|
59
|
+
/** Never serialised: callers pass the config to the adapter, not to an answer. */
|
|
60
|
+
readonly apiKey: string;
|
|
61
|
+
}
|
|
62
|
+
export declare const JEV_KEY_NAMES: string[];
|
|
63
|
+
export declare function jevConfigOf(env?: NodeJS.ProcessEnv): JevConfig | null;
|
|
45
64
|
export type RouterPreflight = {
|
|
46
65
|
state: "unconfigured";
|
|
47
66
|
note: string;
|
|
48
67
|
} | {
|
|
49
68
|
state: "reachable";
|
|
69
|
+
service: string;
|
|
70
|
+
key_name: string;
|
|
50
71
|
url: string;
|
|
72
|
+
models: string[];
|
|
73
|
+
note: string;
|
|
51
74
|
} | {
|
|
52
75
|
state: "router_unavailable";
|
|
76
|
+
service: string;
|
|
77
|
+
key_name: string;
|
|
53
78
|
url: string;
|
|
54
79
|
message: string;
|
|
55
80
|
};
|
|
56
81
|
/**
|
|
57
|
-
* The read-only Jev
|
|
58
|
-
*
|
|
59
|
-
*
|
|
82
|
+
* The read-only Jev preflight: is a Jev key in the connector's environment, and
|
|
83
|
+
* does Jev list models for it? GET /v1/models runs no inference and is not
|
|
84
|
+
* charged. Reachable is not a decision: a scored choice comes only from
|
|
85
|
+
* mission_model_preflight with consult_jev. No key value is read into an answer.
|
|
60
86
|
*/
|
|
61
87
|
export declare function routerPreflight(env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): Promise<RouterPreflight>;
|
package/dist/providers.js
CHANGED
|
@@ -95,26 +95,47 @@ export function resolveProvider(requested, found) {
|
|
|
95
95
|
}
|
|
96
96
|
return { state: "provider_unavailable", message: "No agent provider is installed on the connector host.", available: [] };
|
|
97
97
|
}
|
|
98
|
+
const JEV_SERVICES = [
|
|
99
|
+
{ service: "jev-ai.pro", key: "JEV_AI_API_KEY", baseEnv: "JEV_AI_BASE_URL", base: "https://jev-ai.pro/api" },
|
|
100
|
+
{ service: "typesafe.ai", key: "TYPESAFE_API_KEY", baseEnv: "TYPESAFE_BASE_URL", base: "https://api.typesafe.ai" },
|
|
101
|
+
];
|
|
102
|
+
export const JEV_KEY_NAMES = JEV_SERVICES.map((s) => s.key);
|
|
103
|
+
export function jevConfigOf(env = process.env) {
|
|
104
|
+
for (const s of JEV_SERVICES) {
|
|
105
|
+
const apiKey = env[s.key];
|
|
106
|
+
if (!apiKey)
|
|
107
|
+
continue;
|
|
108
|
+
const base = (env[s.baseEnv] || env.MIADI_JEV_URL || s.base).replace(/\/+$/, "");
|
|
109
|
+
const config = { service: s.service, key_name: s.key, base, url: `${base}/v1/systemone`, models_url: `${base}/v1/models` };
|
|
110
|
+
Object.defineProperty(config, "apiKey", { value: apiKey, enumerable: false });
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
98
115
|
/**
|
|
99
|
-
* The read-only Jev
|
|
100
|
-
*
|
|
101
|
-
*
|
|
116
|
+
* The read-only Jev preflight: is a Jev key in the connector's environment, and
|
|
117
|
+
* does Jev list models for it? GET /v1/models runs no inference and is not
|
|
118
|
+
* charged. Reachable is not a decision: a scored choice comes only from
|
|
119
|
+
* mission_model_preflight with consult_jev. No key value is read into an answer.
|
|
102
120
|
*/
|
|
103
121
|
export async function routerPreflight(env = process.env, fetchImpl = fetch) {
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
122
|
+
const jev = jevConfigOf(env);
|
|
123
|
+
if (!jev) {
|
|
106
124
|
return {
|
|
107
125
|
state: "unconfigured",
|
|
108
|
-
note:
|
|
126
|
+
note: `No ${JEV_KEY_NAMES.join(" or ")} in the connector's environment: model choice falls back to the explicit provider policy and the person's choice, never to a silent default.`,
|
|
109
127
|
};
|
|
110
128
|
}
|
|
129
|
+
const where = { service: jev.service, key_name: jev.key_name, url: jev.base };
|
|
111
130
|
try {
|
|
112
|
-
const res = await fetchImpl(
|
|
113
|
-
if (res.ok)
|
|
114
|
-
return { state: "
|
|
115
|
-
|
|
131
|
+
const res = await fetchImpl(jev.models_url, { headers: { authorization: `Bearer ${jev.apiKey}`, accept: "application/json" }, signal: AbortSignal.timeout(5000) });
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
return { state: "router_unavailable", ...where, message: `Jev answered ${res.status} to the model list; route by the explicit provider policy instead.` };
|
|
134
|
+
const body = await res.json().catch(() => ({}));
|
|
135
|
+
const models = Array.isArray(body?.models) ? body.models.map((m) => (typeof m === "string" ? m : String(m?.id ?? m?.name ?? ""))).filter(Boolean) : [];
|
|
136
|
+
return { state: "reachable", ...where, models, note: "Jev lists models for this key. That is reachability, not a decision." };
|
|
116
137
|
}
|
|
117
138
|
catch (error) {
|
|
118
|
-
return { state: "router_unavailable",
|
|
139
|
+
return { state: "router_unavailable", ...where, message: `Jev is not reachable (${error instanceof Error ? error.message : String(error)}); route by the explicit provider policy instead.` };
|
|
119
140
|
}
|
|
120
141
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { Ledger } from "./store.js";
|
|
|
4
4
|
import type { ConnectorRecord } from "./records.js";
|
|
5
5
|
import type { ConnectorConfig } from "./config.js";
|
|
6
6
|
import type { TmuxLike } from "./steer.js";
|
|
7
|
-
import type { DiscoveryProbe } from "./providers.js";
|
|
7
|
+
import type { DiscoveryProbe, ProviderId } from "./providers.js";
|
|
8
|
+
import type { JevAdapter } from "./model-preflight.js";
|
|
8
9
|
export interface ConnectorDeps {
|
|
9
10
|
identify: (token: string) => Promise<MiadiPerson | null>;
|
|
10
11
|
fetchImpl?: typeof fetch;
|
|
@@ -15,6 +16,9 @@ export interface ConnectorDeps {
|
|
|
15
16
|
tmux?: TmuxLike;
|
|
16
17
|
/** Provider discovery probes (jgwill/Miadi#893); tests inject fakes. */
|
|
17
18
|
providersProbe?: DiscoveryProbe;
|
|
19
|
+
/** A provider's configured default model and a Jev decider (jgwill/Miadi#913); tests inject fakes. */
|
|
20
|
+
modelOf?: (provider: ProviderId) => string | null;
|
|
21
|
+
jevAdapter?: JevAdapter;
|
|
18
22
|
/** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
|
|
19
23
|
steerSettleMs?: number;
|
|
20
24
|
/** Person-token attempts allowed per address in 15 minutes on each sign-in form (default 30). */
|
package/dist/server.js
CHANGED
|
@@ -108,6 +108,8 @@ export function createConnectorApp(config, deps) {
|
|
|
108
108
|
tmux: deps.tmux,
|
|
109
109
|
tmuxSessions: deps.tmuxSessions,
|
|
110
110
|
providersProbe: deps.providersProbe,
|
|
111
|
+
modelOf: deps.modelOf,
|
|
112
|
+
jevAdapter: deps.jevAdapter,
|
|
111
113
|
steerSettleMs: deps.steerSettleMs,
|
|
112
114
|
};
|
|
113
115
|
const server = new McpServer({ name: "miadi", version: VERSION }, { instructions: INSTRUCTIONS });
|
package/dist/tools.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ import type { OAuthState } from "./oauth.js";
|
|
|
7
7
|
import { type ConnectorRecord } from "./records.js";
|
|
8
8
|
import { type ApprovalDeps } from "./approve.js";
|
|
9
9
|
import { type DecisionTokens } from "./decisions.js";
|
|
10
|
-
import { type DiscoveryProbe } from "./providers.js";
|
|
10
|
+
import { type DiscoveryProbe, type ProviderId } from "./providers.js";
|
|
11
|
+
import { type JevAdapter } from "./model-preflight.js";
|
|
11
12
|
import { type TmuxLike } from "./steer.js";
|
|
12
13
|
export interface ToolContext {
|
|
13
14
|
config: ConnectorConfig;
|
|
@@ -25,9 +26,19 @@ export interface ToolContext {
|
|
|
25
26
|
tmuxSessions?: () => string[];
|
|
26
27
|
/** Provider discovery probes; tests inject fakes (jgwill/Miadi#893). */
|
|
27
28
|
providersProbe?: DiscoveryProbe;
|
|
29
|
+
/** The configured default model of a provider; tests inject fakes (jgwill/Miadi#913). */
|
|
30
|
+
modelOf?: (provider: ProviderId) => string | null;
|
|
31
|
+
/** A Jev decider; tests inject a synthetic fixture. Absent, the connector's env decides (jgwill/Miadi#913). */
|
|
32
|
+
jevAdapter?: JevAdapter;
|
|
28
33
|
/** Re-peek delay after a steer is typed (default 4000ms; tests pass 0). */
|
|
29
34
|
steerSettleMs?: number;
|
|
30
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Why GitHub did not answer a reference (jgwill/Miadi#913 B). A 404 without a
|
|
38
|
+
* token is a private repository as often as a missing issue, so nothing here is
|
|
39
|
+
* ever reported as not found.
|
|
40
|
+
*/
|
|
41
|
+
export declare function githubAccess(status: number, hasToken: boolean): string;
|
|
31
42
|
export declare function registerTools(server: McpServer, ctx: ToolContext): void;
|
|
32
43
|
/** What get_capabilities reports for each tool. */
|
|
33
44
|
export declare const TOOL_STATUS: Record<string, string>;
|