@miadi/connector 0.1.19 → 0.1.21
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 +40 -5
- package/dist/approve.d.ts +16 -2
- package/dist/approve.js +185 -16
- package/dist/bin.js +3 -0
- package/dist/mission-status.d.ts +13 -1
- package/dist/mission-status.js +13 -5
- package/dist/model-preflight.d.ts +9 -2
- package/dist/model-preflight.js +23 -1
- package/dist/provider-policy.d.ts +94 -0
- package/dist/provider-policy.js +150 -0
- package/dist/providers.d.ts +7 -3
- package/dist/providers.js +13 -7
- package/dist/queue-state.js +12 -1
- package/dist/reading/adapters.d.ts +18 -0
- package/dist/reading/adapters.js +90 -0
- package/dist/reading/contract.d.ts +153 -0
- package/dist/reading/contract.js +79 -0
- package/dist/reading/markdown.d.ts +47 -0
- package/dist/reading/markdown.js +52 -0
- package/dist/reading/privacy.d.ts +7 -0
- package/dist/reading/privacy.js +14 -0
- package/dist/reading/workflow.d.ts +93 -0
- package/dist/reading/workflow.js +325 -0
- package/dist/records.d.ts +1 -1
- package/dist/records.js +6 -0
- package/dist/request-state.js +9 -1
- package/dist/research-digest.d.ts +30 -0
- package/dist/research-digest.js +104 -0
- package/dist/research-shelf.d.ts +173 -0
- package/dist/research-shelf.js +297 -0
- package/dist/runtime-steer.d.ts +182 -0
- package/dist/runtime-steer.js +543 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +4 -12
- package/dist/steer.d.ts +21 -7
- package/dist/steer.js +115 -10
- package/dist/store.d.ts +2 -0
- package/dist/store.js +18 -1
- package/dist/tools.d.ts +29 -1
- package/dist/tools.js +435 -50
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -1
package/dist/model-preflight.js
CHANGED
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* is kept (no switching without a person), Claude Code only when named, an
|
|
8
8
|
* uninstalled or sign-in-less provider is ineligible, and a reported launch on
|
|
9
9
|
* another provider fails closed as provider_mismatch before anything is asked.
|
|
10
|
+
* - Codex is eligible only with its requester's own choice for the mission or
|
|
11
|
+
* their own policy (provider-policy.ts, jgwill/Miadi#926); a reported Codex
|
|
12
|
+
* launch without either fails closed as provider_not_authorized.
|
|
10
13
|
* - Jev is consulted only when the caller asks (consult_jev) and the connector's
|
|
11
14
|
* environment holds a Jev key. The state sent holds the task type, the
|
|
12
15
|
* criticality and the candidates: no task text, mission id, repository name or
|
|
@@ -25,6 +28,7 @@ import { readFileSync } from "node:fs";
|
|
|
25
28
|
import { homedir } from "node:os";
|
|
26
29
|
import { join } from "node:path";
|
|
27
30
|
import { PROVIDERS } from "./providers.js";
|
|
31
|
+
import { needsPersonChoice } from "./provider-policy.js";
|
|
28
32
|
export const PREFLIGHT_CONTRACT = "miadi.mission_model_preflight";
|
|
29
33
|
export const PREFLIGHT_VERSION = 1;
|
|
30
34
|
/** Below this, a Jev choice is not acted on (the ≥0.8 practice of jgwill/Miadi#770). */
|
|
@@ -52,7 +56,7 @@ export function configuredModel(provider, home = homedir()) {
|
|
|
52
56
|
}
|
|
53
57
|
return null;
|
|
54
58
|
}
|
|
55
|
-
export function candidatesFor(found, approved, modelOf = (p) => configuredModel(p), basis = "approved") {
|
|
59
|
+
export function candidatesFor(found, approved, modelOf = (p) => configuredModel(p), basis = "approved", authorization) {
|
|
56
60
|
return PROVIDERS.map((spec) => {
|
|
57
61
|
const a = found.find((p) => p.id === spec.id);
|
|
58
62
|
const installed = Boolean(a?.installed);
|
|
@@ -68,6 +72,9 @@ export function candidatesFor(found, approved, modelOf = (p) => configuredModel(
|
|
|
68
72
|
[eligible, reason] = [false, `Not the ${basis} provider (${approved}); switching needs a person's approval.`];
|
|
69
73
|
else if (!approved && !spec.selectable_by_default)
|
|
70
74
|
[eligible, reason] = [false, `${spec.name} runs only when a request names it.`];
|
|
75
|
+
else if (needsPersonChoice(spec.id) && !(authorization?.authorized && authorization.provider === spec.id)) {
|
|
76
|
+
[eligible, reason] = [false, `${spec.name} runs only when ${authorization?.requester.name ?? "the requester"} chooses it for this mission, or their own policy allows it (jgwill/Miadi#926).`];
|
|
77
|
+
}
|
|
71
78
|
return {
|
|
72
79
|
option: model ? `${spec.id}:${model}` : spec.id,
|
|
73
80
|
provider: spec.id,
|
|
@@ -212,6 +219,7 @@ export async function modelPreflight(input) {
|
|
|
212
219
|
approved_provider,
|
|
213
220
|
provider_basis: input.provider_basis ?? "approved",
|
|
214
221
|
candidates,
|
|
222
|
+
...(input.provider_authorization ? { provider_authorization: input.provider_authorization } : {}),
|
|
215
223
|
evidence: { model_cost: "unknown", quota: "unknown" },
|
|
216
224
|
policy: {
|
|
217
225
|
deterministic: true,
|
|
@@ -223,6 +231,20 @@ export async function modelPreflight(input) {
|
|
|
223
231
|
? { approved_provider, actual: { ...input.launch, source: "reported" }, mismatch: approved_provider ? input.launch.provider !== approved_provider : null }
|
|
224
232
|
: { approved_provider, actual: "unverified", mismatch: null };
|
|
225
233
|
// Fail closed before anything is asked or spent.
|
|
234
|
+
if (input.launch && needsPersonChoice(input.launch.provider) && !(input.provider_authorization?.authorized && approved_provider === input.launch.provider)) {
|
|
235
|
+
return {
|
|
236
|
+
...base,
|
|
237
|
+
state: "provider_not_authorized",
|
|
238
|
+
decider: { source: "static_fallback", router: "not_requested", calibration: "not_applicable", derivation: "deterministic policy: Codex launched without its requester's own choice or policy (jgwill/Miadi#926)" },
|
|
239
|
+
recommendation: {
|
|
240
|
+
confidence: null,
|
|
241
|
+
review_tier: floor,
|
|
242
|
+
explanation: `Reported running on ${input.launch.provider}, which no choice of the requester's authorizes for this mission. Nothing proceeds on it; the requester chooses it on the approval page, or the mission runs on another provider.`,
|
|
243
|
+
abstain_reasons: ["provider_not_authorized"],
|
|
244
|
+
},
|
|
245
|
+
launch,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
226
248
|
if (launch.mismatch) {
|
|
227
249
|
return {
|
|
228
250
|
...base,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex runs only on its requester's own choice for that one mission (jgwill/Miadi#926).
|
|
3
|
+
*
|
|
4
|
+
* The assistant that asks (ChatGPT or any MCP client) may name provider "codex", but that
|
|
5
|
+
* name is the caller's, not the person's. The connector cannot read the chat, so it never
|
|
6
|
+
* treats the provider field as the person's choice. Codex is authorized for a run only by:
|
|
7
|
+
*
|
|
8
|
+
* - a `provider_choice` record the requester made themself on the request's approval page,
|
|
9
|
+
* a browser form signed in with their own Miadi person token, outside the MCP channel
|
|
10
|
+
* (an OAuth client never holds the person token). The choice is bound to the request id,
|
|
11
|
+
* its version and its content hash, and must be fresh (CHOICE_TTL_MS) when the request
|
|
12
|
+
* is approved. A revision makes it stale. It covers that mission only;
|
|
13
|
+
* - or the requester's own provider policy, set by them on the connector's /providers page,
|
|
14
|
+
* saying Codex may be named for their requests without a per-mission choice.
|
|
15
|
+
*
|
|
16
|
+
* A generic approval (page or card), the caller's provider field, the host's preference
|
|
17
|
+
* order, a Jev recommendation, and a choice made by anyone other than the requester
|
|
18
|
+
* authorize nothing. Other providers keep their regular policy (jgwill/Miadi#893, #913).
|
|
19
|
+
* The check runs when a request is made or revised, when it is approved, when its brief
|
|
20
|
+
* is written (approval and every rerun), and the factory runner checks the brief again
|
|
21
|
+
* before it launches anything.
|
|
22
|
+
*/
|
|
23
|
+
import type { ConnectorRecord } from "./records.js";
|
|
24
|
+
/** Providers a request may name only with the requester's own per-mission choice. */
|
|
25
|
+
export declare const CHOICE_PROVIDERS: readonly string[];
|
|
26
|
+
export declare const CODEX_POLICIES: readonly ["per_mission_choice", "selectable"];
|
|
27
|
+
export type CodexPolicy = (typeof CODEX_POLICIES)[number];
|
|
28
|
+
export declare const DEFAULT_CODEX_POLICY: CodexPolicy;
|
|
29
|
+
/** How long a per-mission choice stays good for the approval that follows it. */
|
|
30
|
+
export declare const CHOICE_TTL_MS: number;
|
|
31
|
+
export declare function needsPersonChoice(provider: string | null | undefined): boolean;
|
|
32
|
+
/** Whether a person id may make a choice or set a policy: a person, never a service token. */
|
|
33
|
+
export declare function isPersonId(personId: string | undefined): boolean;
|
|
34
|
+
export interface ProviderPolicy {
|
|
35
|
+
person_id: string;
|
|
36
|
+
codex: CodexPolicy;
|
|
37
|
+
source: "default" | "person";
|
|
38
|
+
record?: string;
|
|
39
|
+
set_at?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A person's provider policy: the latest one they set themself, else the default. */
|
|
42
|
+
export declare function providerPolicyOf(records: ConnectorRecord[], personId: string): ProviderPolicy;
|
|
43
|
+
export interface ProviderChoiceView {
|
|
44
|
+
record: string;
|
|
45
|
+
provider: string;
|
|
46
|
+
by: {
|
|
47
|
+
person_id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
50
|
+
at: string;
|
|
51
|
+
version: number;
|
|
52
|
+
content_hash: string;
|
|
53
|
+
via?: string;
|
|
54
|
+
}
|
|
55
|
+
export type AuthorizationState = "not_required" | "person_choice" | "policy_selectable" | "needs_person_choice";
|
|
56
|
+
export interface ProviderAuthorization {
|
|
57
|
+
state: AuthorizationState;
|
|
58
|
+
authorized: boolean;
|
|
59
|
+
/** The provider the run would launch with (the resolved one), or null when the runner's default applies. */
|
|
60
|
+
provider: string | null;
|
|
61
|
+
/** The provider field as the caller wrote it: evidence of what was asked, never of who chose it. */
|
|
62
|
+
requested: string | null;
|
|
63
|
+
requester: {
|
|
64
|
+
person_id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
client?: string;
|
|
67
|
+
};
|
|
68
|
+
policy: {
|
|
69
|
+
codex: CodexPolicy;
|
|
70
|
+
source: ProviderPolicy["source"];
|
|
71
|
+
record?: string;
|
|
72
|
+
set_at?: string;
|
|
73
|
+
};
|
|
74
|
+
choice?: ProviderChoiceView;
|
|
75
|
+
/** Choices recorded for this request that do not bind it (another version, another person, too old). */
|
|
76
|
+
not_binding?: Array<{
|
|
77
|
+
record: string;
|
|
78
|
+
why: string;
|
|
79
|
+
}>;
|
|
80
|
+
explanation: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether the provider a request names may launch, and why. Pure: the ledger is read,
|
|
84
|
+
* nothing is written. `requireFresh` (the approval) also refuses a choice older than
|
|
85
|
+
* CHOICE_TTL_MS; the brief written for an approved request checks the binding only.
|
|
86
|
+
*/
|
|
87
|
+
export declare function authorizeProvider(record: ConnectorRecord, records: ConnectorRecord[], opts?: {
|
|
88
|
+
now?: number;
|
|
89
|
+
requireFresh?: boolean;
|
|
90
|
+
}): ProviderAuthorization;
|
|
91
|
+
/** The authorization as the brief carries it to the factory runner: attributes it checks before launching. */
|
|
92
|
+
export declare function briefAttributes(a: ProviderAuthorization): Record<string, string>;
|
|
93
|
+
/** The authorization as a receipt keeps it: no body text, only who, what and which record. */
|
|
94
|
+
export declare function authorizationReceipt(a: ProviderAuthorization): Record<string, unknown>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { contentHash, versionOf } from "./revisions.js";
|
|
2
|
+
/** Providers a request may name only with the requester's own per-mission choice. */
|
|
3
|
+
export const CHOICE_PROVIDERS = ["codex"];
|
|
4
|
+
export const CODEX_POLICIES = ["per_mission_choice", "selectable"];
|
|
5
|
+
export const DEFAULT_CODEX_POLICY = "per_mission_choice";
|
|
6
|
+
/** How long a per-mission choice stays good for the approval that follows it. */
|
|
7
|
+
export const CHOICE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
export function needsPersonChoice(provider) {
|
|
9
|
+
return typeof provider === "string" && CHOICE_PROVIDERS.includes(provider);
|
|
10
|
+
}
|
|
11
|
+
/** Whether a person id may make a choice or set a policy: a person, never a service token. */
|
|
12
|
+
export function isPersonId(personId) {
|
|
13
|
+
return typeof personId === "string" && personId.length > 0 && !personId.startsWith("system:");
|
|
14
|
+
}
|
|
15
|
+
/** A person's provider policy: the latest one they set themself, else the default. */
|
|
16
|
+
export function providerPolicyOf(records, personId) {
|
|
17
|
+
let latest;
|
|
18
|
+
for (const r of records) {
|
|
19
|
+
if (r.kind !== "provider_policy" || r.actor.person_id !== personId || !isPersonId(r.actor.person_id))
|
|
20
|
+
continue;
|
|
21
|
+
if (!CODEX_POLICIES.includes(String(r.body.codex)))
|
|
22
|
+
continue;
|
|
23
|
+
latest = r;
|
|
24
|
+
}
|
|
25
|
+
if (!latest)
|
|
26
|
+
return { person_id: personId, codex: DEFAULT_CODEX_POLICY, source: "default" };
|
|
27
|
+
return { person_id: personId, codex: latest.body.codex, source: "person", record: latest.id, set_at: latest.created_at };
|
|
28
|
+
}
|
|
29
|
+
function str(v) {
|
|
30
|
+
return typeof v === "string" && v ? v : null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether the provider a request names may launch, and why. Pure: the ledger is read,
|
|
34
|
+
* nothing is written. `requireFresh` (the approval) also refuses a choice older than
|
|
35
|
+
* CHOICE_TTL_MS; the brief written for an approved request checks the binding only.
|
|
36
|
+
*/
|
|
37
|
+
export function authorizeProvider(record, records, opts = {}) {
|
|
38
|
+
const body = (record.body ?? {});
|
|
39
|
+
const requested = str(body.provider);
|
|
40
|
+
const provider = str(body.resolved_provider) ?? requested;
|
|
41
|
+
const requester = {
|
|
42
|
+
person_id: record.actor.person_id,
|
|
43
|
+
name: record.actor.name,
|
|
44
|
+
...(record.actor.client_name ? { client: record.actor.client_name } : {}),
|
|
45
|
+
};
|
|
46
|
+
const policy = providerPolicyOf(records, record.actor.person_id);
|
|
47
|
+
const policyView = { codex: policy.codex, source: policy.source, ...(policy.record ? { record: policy.record, set_at: policy.set_at } : {}) };
|
|
48
|
+
const base = { provider, requested, requester, policy: policyView };
|
|
49
|
+
if (!needsPersonChoice(provider)) {
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
state: "not_required",
|
|
53
|
+
authorized: true,
|
|
54
|
+
explanation: provider
|
|
55
|
+
? `${provider} needs no per-mission choice; it was ${requested ? "named in the request" : "picked by the host's preference order"} and verified installed.`
|
|
56
|
+
: "No provider is named: the factory runner's configured default launches, and the runner never starts Codex for a connector brief without the brief's own authorization.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (policy.codex === "selectable" && isPersonId(record.actor.person_id)) {
|
|
60
|
+
return {
|
|
61
|
+
...base,
|
|
62
|
+
state: "policy_selectable",
|
|
63
|
+
authorized: true,
|
|
64
|
+
explanation: `${record.actor.name}'s own provider policy (set ${policy.set_at}) lets their requests name Codex without a per-mission choice. They can change it on the connector's providers page.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const now = opts.now ?? Date.now();
|
|
68
|
+
const version = versionOf(record);
|
|
69
|
+
const hash = contentHash(record);
|
|
70
|
+
const notBinding = [];
|
|
71
|
+
let choice;
|
|
72
|
+
for (const r of records) {
|
|
73
|
+
if (r.kind !== "provider_choice")
|
|
74
|
+
continue;
|
|
75
|
+
const b = (r.body ?? {});
|
|
76
|
+
if (b.request_id !== record.id)
|
|
77
|
+
continue;
|
|
78
|
+
const why = b.provider !== provider
|
|
79
|
+
? `it chose ${String(b.provider)}, not ${provider}`
|
|
80
|
+
: r.actor.person_id !== record.actor.person_id || !isPersonId(r.actor.person_id)
|
|
81
|
+
? `${r.actor.name} is not the requester`
|
|
82
|
+
: b.version !== version || b.content_hash !== hash
|
|
83
|
+
? `it was made for version ${String(b.version)}; the request is at version ${version}`
|
|
84
|
+
: opts.requireFresh && now - Date.parse(r.created_at) > CHOICE_TTL_MS
|
|
85
|
+
? `it is older than ${CHOICE_TTL_MS / 3_600_000} hours`
|
|
86
|
+
: null;
|
|
87
|
+
if (why)
|
|
88
|
+
notBinding.push({ record: r.id, why });
|
|
89
|
+
else
|
|
90
|
+
choice = r;
|
|
91
|
+
}
|
|
92
|
+
const extra = notBinding.length ? { not_binding: notBinding } : {};
|
|
93
|
+
if (choice) {
|
|
94
|
+
const b = choice.body;
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
...extra,
|
|
98
|
+
state: "person_choice",
|
|
99
|
+
authorized: true,
|
|
100
|
+
choice: {
|
|
101
|
+
record: choice.id,
|
|
102
|
+
provider: String(b.provider),
|
|
103
|
+
by: { person_id: choice.actor.person_id, name: choice.actor.name },
|
|
104
|
+
at: choice.created_at,
|
|
105
|
+
version: Number(b.version),
|
|
106
|
+
content_hash: String(b.content_hash),
|
|
107
|
+
...(choice.actor.client_id ? { via: choice.actor.client_id } : {}),
|
|
108
|
+
},
|
|
109
|
+
explanation: `${choice.actor.name} chose Codex for this mission (version ${version}) on its approval page at ${choice.created_at}. The choice covers this mission only.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (!isPersonId(record.actor.person_id)) {
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
...extra,
|
|
116
|
+
state: "needs_person_choice",
|
|
117
|
+
authorized: false,
|
|
118
|
+
explanation: `The request names Codex and was made with a service token (${record.actor.name}). Only a person chooses Codex, for a request of their own, so this request cannot run Codex. Name another provider.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
...base,
|
|
123
|
+
...extra,
|
|
124
|
+
state: "needs_person_choice",
|
|
125
|
+
authorized: false,
|
|
126
|
+
explanation: `The request names Codex${requester.client ? `, asked through ${requester.client}` : ""}. Codex runs only when ${record.actor.name} chooses it for this mission on its approval page; approving the request, on the page or on a card, does not choose it. Name another provider to run without that step.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** The authorization as the brief carries it to the factory runner: attributes it checks before launching. */
|
|
130
|
+
export function briefAttributes(a) {
|
|
131
|
+
if (!needsPersonChoice(a.provider))
|
|
132
|
+
return {};
|
|
133
|
+
if (a.state === "person_choice" && a.choice)
|
|
134
|
+
return { provider_authorization: "person_choice", provider_choice: a.choice.record };
|
|
135
|
+
if (a.state === "policy_selectable" && a.policy.record)
|
|
136
|
+
return { provider_authorization: "policy_selectable", provider_policy: a.policy.record };
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
/** The authorization as a receipt keeps it: no body text, only who, what and which record. */
|
|
140
|
+
export function authorizationReceipt(a) {
|
|
141
|
+
return {
|
|
142
|
+
state: a.state,
|
|
143
|
+
authorized: a.authorized,
|
|
144
|
+
provider: a.provider,
|
|
145
|
+
requested: a.requested,
|
|
146
|
+
requester: a.requester,
|
|
147
|
+
policy: a.policy,
|
|
148
|
+
...(a.choice ? { choice: a.choice } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
package/dist/providers.d.ts
CHANGED
|
@@ -8,7 +8,11 @@ export interface ProviderSpec {
|
|
|
8
8
|
/** Config paths whose existence suggests a sign-in; contents are never read. */
|
|
9
9
|
auth_evidence: string[];
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Preference order when a run request names no provider (jgwill/Miadi#893). Codex is never
|
|
13
|
+
* picked by default: a request names it, and it runs only on the requester's own choice for
|
|
14
|
+
* that mission or their own policy (provider-policy.ts, jgwill/Miadi#926).
|
|
15
|
+
*/
|
|
12
16
|
export declare const PROVIDERS: ProviderSpec[];
|
|
13
17
|
export interface ProviderAvailability {
|
|
14
18
|
id: ProviderId;
|
|
@@ -38,8 +42,8 @@ export type ProviderResolution = {
|
|
|
38
42
|
/**
|
|
39
43
|
* Resolve the provider a run will ask the intake for. A requested provider must
|
|
40
44
|
* be known and installed; with no request the first installed provider in the
|
|
41
|
-
* preference order that may be selected by default wins
|
|
42
|
-
* by
|
|
45
|
+
* preference order that may be selected by default wins. Codex and Claude Code
|
|
46
|
+
* only ever by name; whether a named Codex may run is provider-policy.ts's answer.
|
|
43
47
|
*/
|
|
44
48
|
export declare function resolveProvider(requested: string | undefined, found: ProviderAvailability[]): ProviderResolution;
|
|
45
49
|
/**
|
package/dist/providers.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* The connector never assumes a provider works because a task text names one:
|
|
5
5
|
* it reports what is installed on this host, what sign-in evidence exists (the
|
|
6
6
|
* presence of the provider's own config directory — never its contents), and
|
|
7
|
-
* applies an explicit preference order. Claude Code
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* applies an explicit preference order. Codex and Claude Code are never selected
|
|
8
|
+
* silently: each requires the request to name it, and Codex also needs the
|
|
9
|
+
* requester's own choice for that mission (provider-policy.ts, jgwill/Miadi#926).
|
|
10
|
+
* When nothing safe is configured the answer is `provider_unavailable` and the
|
|
11
|
+
* mission is left unstarted.
|
|
10
12
|
*
|
|
11
13
|
* No bearer token is read, logged or returned here. Version probes run the
|
|
12
14
|
* provider's own `--version` with a short timeout.
|
|
@@ -15,9 +17,13 @@ import { execFileSync } from "node:child_process";
|
|
|
15
17
|
import { existsSync } from "node:fs";
|
|
16
18
|
import { homedir } from "node:os";
|
|
17
19
|
import { join } from "node:path";
|
|
18
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Preference order when a run request names no provider (jgwill/Miadi#893). Codex is never
|
|
22
|
+
* picked by default: a request names it, and it runs only on the requester's own choice for
|
|
23
|
+
* that mission or their own policy (provider-policy.ts, jgwill/Miadi#926).
|
|
24
|
+
*/
|
|
19
25
|
export const PROVIDERS = [
|
|
20
|
-
{ id: "codex", cli: "codex", name: "Codex CLI", selectable_by_default:
|
|
26
|
+
{ id: "codex", cli: "codex", name: "Codex CLI", selectable_by_default: false, auth_evidence: [".codex"] },
|
|
21
27
|
{ id: "pi", cli: "pi", name: "Pi coding agent", selectable_by_default: true, auth_evidence: [".pi", ".config/pi"] },
|
|
22
28
|
{ id: "copilot", cli: "copilot", name: "GitHub Copilot CLI", selectable_by_default: true, auth_evidence: [".copilot"] },
|
|
23
29
|
{ id: "claude", cli: "claude", name: "Claude Code", selectable_by_default: false, auth_evidence: [".claude"] },
|
|
@@ -59,8 +65,8 @@ export function discoverProviders(probe = {}, specs = PROVIDERS) {
|
|
|
59
65
|
/**
|
|
60
66
|
* Resolve the provider a run will ask the intake for. A requested provider must
|
|
61
67
|
* be known and installed; with no request the first installed provider in the
|
|
62
|
-
* preference order that may be selected by default wins
|
|
63
|
-
* by
|
|
68
|
+
* preference order that may be selected by default wins. Codex and Claude Code
|
|
69
|
+
* only ever by name; whether a named Codex may run is provider-policy.ts's answer.
|
|
64
70
|
*/
|
|
65
71
|
export function resolveProvider(requested, found) {
|
|
66
72
|
const installed = found.filter((p) => p.installed);
|
package/dist/queue-state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared, read-only factory queue evidence for every request-state surface. */
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
export function factoryDir(env = process.env) {
|
|
5
5
|
// The factory's data folder, beside the connector's own /srv/miadi/connector: never a home directory (jgwill/Miadi#911).
|
|
@@ -18,6 +18,17 @@ export function readQueueState(record, dir = factoryDir()) {
|
|
|
18
18
|
return { state: "delivered", file };
|
|
19
19
|
if (existsSync(join(queue, "cancelled", file)))
|
|
20
20
|
return { state: "cancelled", file };
|
|
21
|
+
if (existsSync(join(queue, "refused", file))) {
|
|
22
|
+
// The factory runner would not launch its provider (jgwill/Miadi#926): the reason it wrote beside the brief.
|
|
23
|
+
let why = "";
|
|
24
|
+
try {
|
|
25
|
+
why = readFileSync(join(queue, "refused", `${file}.why`), "utf8").trim().slice(0, 500);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* no reason written */
|
|
29
|
+
}
|
|
30
|
+
return { state: "refused", file, note: `The factory runner refused to launch it${why ? `: ${why}` : "."}` };
|
|
31
|
+
}
|
|
21
32
|
if (existsSync(join(queue, "stale", file)))
|
|
22
33
|
return { state: "stale", file, note: "Its seat did not take it in time; the intake moved it aside." };
|
|
23
34
|
return { state: record.approval_state === "approved" ? "missing" : "not_queued", file };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReadingScope } from "./contract.js";
|
|
2
|
+
import type { ReadingAdapters, ReviewBase } from "./workflow.js";
|
|
3
|
+
export declare function reviewAdapter(baseUrl: string, token: string | undefined, fetchImpl?: typeof fetch): {
|
|
4
|
+
lookup(video: string): Promise<ReviewBase | null>;
|
|
5
|
+
readExact: (id: string, version: number) => Promise<ReviewBase>;
|
|
6
|
+
atomicImport: boolean;
|
|
7
|
+
/** Future v0 contract; enable only from an authenticated live handshake, never a caller flag. */
|
|
8
|
+
discoverAtomicImport(): Promise<boolean>;
|
|
9
|
+
importExact(input: Parameters<ReadingAdapters["importExact"]>[0]): Promise<{
|
|
10
|
+
id: any;
|
|
11
|
+
version: any;
|
|
12
|
+
}>;
|
|
13
|
+
storeTranscript(id: string, text: string): Promise<any>;
|
|
14
|
+
};
|
|
15
|
+
/** Public literature GETs only: allowlisted HTTPS hosts, no credentials, redirects or private IPs. */
|
|
16
|
+
export declare function literatureVerifier(hosts: string[], fetchImpl?: typeof fetch): ReadingAdapters["verifySource"];
|
|
17
|
+
/** Human-readable factory task; its scope is data in the approved record, never shell interpolation. */
|
|
18
|
+
export declare function readingTask(scope: ReadingScope): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** The real service adapter deliberately refuses the legacy unconditional import endpoint. */
|
|
2
|
+
import { ReviewClient, reviewIdOf } from "../reviews.js";
|
|
3
|
+
import { block, canonicalVideo } from "./contract.js";
|
|
4
|
+
export function reviewAdapter(baseUrl, token, fetchImpl = fetch) {
|
|
5
|
+
const client = new ReviewClient(baseUrl, token, fetchImpl);
|
|
6
|
+
const request = async (path, init = {}) => {
|
|
7
|
+
const res = await fetchImpl(`${baseUrl}${path}`, { ...init, redirect: "error", signal: AbortSignal.timeout(30000), headers: { accept: "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}), ...init.headers } });
|
|
8
|
+
if (res.status === 401 || res.status === 403)
|
|
9
|
+
block("blocked_permission", "Refresh the review service credential in its configured environment; do not paste it into chat.");
|
|
10
|
+
if (res.status === 409 || res.status === 412)
|
|
11
|
+
block("blocked_permission", "The review changed after staging. Rebase and obtain approval for a new candidate.");
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
block("failed_retryable", `Review service answered ${res.status}; retry the same operation key after recovery.`);
|
|
14
|
+
return res.json();
|
|
15
|
+
};
|
|
16
|
+
const readExact = async (id, version) => {
|
|
17
|
+
if (reviewIdOf(id) !== id)
|
|
18
|
+
block("blocked_permission", "Review service returned an invalid review id.");
|
|
19
|
+
const body = await client.get(id, version);
|
|
20
|
+
if (!body || body.version?.version !== version)
|
|
21
|
+
block("failed_retryable", "Pinned review version is unavailable; fallback-to-latest is refused.");
|
|
22
|
+
const raw = await fetchImpl(`${baseUrl}/review/${id}/raw?v=${version}`, { redirect: "error", signal: AbortSignal.timeout(30000) });
|
|
23
|
+
if (!raw.ok)
|
|
24
|
+
block("failed_retryable", "Pinned raw Markdown could not be read.");
|
|
25
|
+
const markdown = await raw.text();
|
|
26
|
+
if (markdown !== body.version.markdown)
|
|
27
|
+
block("failed_retryable", "Raw and JSON Markdown differ; inspect the service before importing.");
|
|
28
|
+
return { id, version, markdown, video_id: canonicalVideo(body.video_id) };
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
async lookup(video) {
|
|
32
|
+
if (!token)
|
|
33
|
+
block("blocked_permission", "Configure the review search token on the host before generating a potentially duplicate V1.");
|
|
34
|
+
const response = await request(`/api/reviews/search?${new URLSearchParams({ q: canonicalVideo(video), limit: "100" })}`);
|
|
35
|
+
const rows = response.reviews ?? response.results;
|
|
36
|
+
if (!Array.isArray(rows))
|
|
37
|
+
block("failed_retryable", "Review search returned malformed data; do not infer that no review exists.");
|
|
38
|
+
const matches = rows.filter(r => r.video_id === video);
|
|
39
|
+
if (matches.length > 1)
|
|
40
|
+
block("blocked_permission", "Multiple reviews exist for this video; the author must select the base.");
|
|
41
|
+
if (!matches.length)
|
|
42
|
+
return null;
|
|
43
|
+
if (!matches[0].latest_version)
|
|
44
|
+
block("failed_retryable", "Existing review is pending generation; no duplicate V1 will be generated.");
|
|
45
|
+
return readExact(matches[0].id, matches[0].latest_version);
|
|
46
|
+
},
|
|
47
|
+
readExact,
|
|
48
|
+
atomicImport: false,
|
|
49
|
+
/** Future v0 contract; enable only from an authenticated live handshake, never a caller flag. */
|
|
50
|
+
async discoverAtomicImport() {
|
|
51
|
+
const res = await fetchImpl(`${baseUrl}/api/reviews/capabilities`, { redirect: "error", signal: AbortSignal.timeout(15000), headers: token ? { authorization: `Bearer ${token}` } : {} });
|
|
52
|
+
if (!res.ok)
|
|
53
|
+
return false;
|
|
54
|
+
const c = await res.json();
|
|
55
|
+
return c.conditional_import === "sha256-cas-v1" && c.idempotency === "durable-transactional-v1" && c.publication === "explicit";
|
|
56
|
+
},
|
|
57
|
+
async importExact(input) {
|
|
58
|
+
// Kept on the real /import path, with a negotiated extension. Never send a legacy write.
|
|
59
|
+
if (!await this.discoverAtomicImport())
|
|
60
|
+
block("blocked_permission", "Atomic import is not implemented by the review service. Apply the v0 prompt first.");
|
|
61
|
+
const result = await request("/api/reviews/import", { method: "POST", headers: { "content-type": "application/json", "idempotency-key": input.idempotency_key }, body: JSON.stringify({ url: `https://youtu.be/${input.video_id}`, markdown: input.markdown, review_id: input.review_id, expected_version: input.expected_version, expected_sha256: input.expected_sha256, sha256: input.sha256, public_release: true, contract: "sha256-cas-v1" }) });
|
|
62
|
+
return { id: result.id, version: result.version };
|
|
63
|
+
},
|
|
64
|
+
async storeTranscript(id, text) {
|
|
65
|
+
// No generation-on-empty, trimming, or overwrite. Connector source-part remains canonical.
|
|
66
|
+
if (!text.trim() || reviewIdOf(id) !== id)
|
|
67
|
+
block("blocked_transcript", "An existing review id and nonempty exact transcript are required.");
|
|
68
|
+
return request(`/api/reviews/${id}/transcript`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ transcript: text, overwrite: false }) });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Public literature GETs only: allowlisted HTTPS hosts, no credentials, redirects or private IPs. */
|
|
73
|
+
export function literatureVerifier(hosts, fetchImpl = fetch) {
|
|
74
|
+
return async (source) => {
|
|
75
|
+
const u = new URL(source.url);
|
|
76
|
+
if (u.protocol !== "https:" || u.username || u.password || u.port || !hosts.includes(u.hostname) || /^[\d.]+$/.test(u.hostname) || u.hostname.includes(":"))
|
|
77
|
+
block("blocked_citations", "Literature source is outside the host's public-source allowlist.");
|
|
78
|
+
const res = await fetchImpl(u, { redirect: "error", signal: AbortSignal.timeout(15000), headers: { accept: "text/plain, text/html, application/json" } });
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
block("blocked_citations", "Public source could not be retrieved; no citation was marked verified.");
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
if (Buffer.byteLength(text) > 2 * 1024 * 1024)
|
|
83
|
+
block("blocked_citations", "Public source exceeds the bounded verification size.");
|
|
84
|
+
return { url: source.url, text: text.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " "), fetched_at: new Date().toISOString() };
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Human-readable factory task; its scope is data in the approved record, never shell interpolation. */
|
|
88
|
+
export function readingTask(scope) {
|
|
89
|
+
return `Run the bounded transcript-first reading worker for THIS approved connector record:\nnode packages/connector/scripts/reading.mjs run <this-run-request-id>\nThe typed reading grant in this record is the only processing authority. Read packages/connector/scripts/READING.md.\nReturn stage receipts and staged V1/V2/V3 with hashes, insertion diff and provenance. Stop at editorial approval. No public import, Deep Diver, Chronicle, paid fallback or media upload beyond the grant.\nKind: ${scope.kind}; language: ${scope.language}; total provider ceiling USD ${scope.max_cost_usd}. Missing capabilities are explicit blockers, never permission to generate from memory.`;
|
|
90
|
+
}
|