@miadi/connector 0.1.19 → 0.1.20
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 +3 -1
- package/dist/approve.d.ts +13 -2
- package/dist/approve.js +176 -10
- package/dist/mission-status.js +3 -1
- 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 +4 -0
- package/dist/request-state.js +3 -0
- package/dist/server.js +2 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.js +18 -1
- package/dist/tools.js +162 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,9 @@ A mission is found by the issue it implements (jgwill/Miadi#913). `request_autho
|
|
|
62
62
|
|
|
63
63
|
`request_session_steer` sends one authorized follow-up to one existing live agent, within the steering grant an approved run carries (`steering: { max_attempts, expires_at, pane }` on `request_authorized_run`). It peeks at the bound pane — the grant's pane or the session's first pane, never whichever pane is active — refuses a busy agent or a pane that is not an agent, types the exact instruction literally, and re-peeks to verify the agent accepted it (`accepted`) or only holds it at the prompt (`submitted`). Every attempt leaves a durable artifact with the instruction's sha256, its grant, its target and both captures. When the factory recorded the mission's pane, the steer goes to that pane id and nowhere else: another session is `stale_binding`, another pane `wrong_pane`, a replaced pane `stale_binding`, and text already at the prompt `composing` (nothing typed, Enter not pressed). A pane whose prompt box cannot be read (any TUI but Claude Code's today) is `blocked`. Every tmux address is exact (`=name:w.p`), because tmux otherwise matches a session by the start of its name. Only a steer that typed its instruction uses one of the grant's attempts (`typed`, and `attempts` in the answer). The connector's service steers through the tmux server of the user it runs as; `get_capabilities.steering` says whether that transport answers now, and while it does not, `request_session_steer` records nothing and uses no attempt. A steer the grant does not cover waits for one explicit scoped human decision on its `approval_url`, which runs exactly that instruction once.
|
|
64
64
|
|
|
65
|
-
`request_authorized_run` may name a `provider` (`codex`, `pi`, `copilot`, `claude`). The connector verifies it is installed on the host before recording anything; an unknown or unavailable provider answers `provider_unavailable` and the mission stays unstarted. With no provider named, the first installed provider in the preference order is resolved and named in the brief; Claude Code
|
|
65
|
+
`request_authorized_run` may name a `provider` (`codex`, `pi`, `copilot`, `claude`). The connector verifies it is installed on the host before recording anything; an unknown or unavailable provider answers `provider_unavailable` and the mission stays unstarted. With no provider named, the first installed provider in the preference order that may be picked by default (Pi, then Copilot) is resolved and named in the brief; Codex and Claude Code are never chosen silently, each runs only when named. `provider_capabilities` (write tier) reports what the host has installed, the sign-in evidence, the preference order, and the read-only Jev check: `unconfigured` without `JEV_AI_API_KEY` or `TYPESAFE_API_KEY` in the connector's environment, else the models Jev lists for the key (no charge). A provider name inside a task's text selects nothing.
|
|
66
|
+
|
|
67
|
+
**Codex runs only on its requester's own choice (jgwill/Miadi#926).** The provider field is the caller's: the connector cannot read the chat, so `provider: "codex"` chooses nothing for the person. A run that names Codex is recorded, and its answer says it `needs_person_choice`. The approval page then shows no Approve button. Only the requester, signed in there with their own Miadi person token, can tick and send "Choose codex for this mission". That `provider_choice` record is bound to the request id, its version and its content hash, must be under 24 hours old when the request is approved, and covers that one mission. A revision makes it stale. A generic Approve, on the page or on a card, is refused as `provider_choice_required`, and so is a choice by any other person or by a service token. The requester can instead set their own policy at `/providers` so their requests may name Codex without a per-mission choice; that policy is theirs only and revisable. The check runs at request, revision, approval, and where the brief is written (approval and every rerun). The brief carries `provider_authorization` and `provider_choice` (or `provider_policy`), and the factory runner refuses to launch Codex for a connector brief without them. `request_authorized_run`, `revise_pending_request`, `mission_status`, `show_decisions` and `mission_model_preflight` show `provider_authorization`; after approval it is also in the request's `completion_receipt`, which `get_run_result` returns. A Codex launch without it is named on the mission and fails the preflight closed as `provider_not_authorized`. A brief the runner refuses goes to `queue/refused/` with its reason, and the request shows it. Other providers keep their regular policy, and nothing falls back to Codex. What this does not cover: anyone who can write `/srv/miadi/factory/queue` or the connector's state directory can forge a brief or a record (the folder's permissions are the boundary); a configured launcher that runs Codex behind a wrapper whose name does not say codex is trusted as configured; a brief to an existing session reaches only an idle Claude Code agent today, so it is not checked; and an agent that starts `codex` itself from inside its session is outside the connector's reach.
|
|
66
68
|
|
|
67
69
|
`mission_model_preflight` (write tier) answers, for one mission, which provider and model fit its task type and criticality, and how that is known: contract `miadi.mission_model_preflight` v1, candidates with their configured default model, sign-in evidence (config directory only), quota `unknown`, policy eligibility, a review floor, and a recommendation or an abstention with reasons. Its `decider.source` is `static_fallback`, `jev` or `synthetic_fixture`. Jev is asked only with `consult_jev: true` and a key on the host; it is sent task type, criticality and candidates, never task text, mission id or repository, and its answer is advisory: uncalibrated, below 0.8 or outside the contract, the preflight abstains. A reported `launch` on another provider than the approved one answers `provider_mismatch` before anything is asked. It authorizes nothing. `bench/model-preflight-bench.mjs` compares the 0.1.12 baseline, the static preflight and Jev on synthetic scenarios; `bench/results/` keeps the runs.
|
|
68
70
|
|
package/dist/approve.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { type MiadiPerson } from "./people.js";
|
|
|
17
17
|
import { type ConnectorRecord } from "./records.js";
|
|
18
18
|
import { type RequestProjection } from "./request-state.js";
|
|
19
19
|
import { type TmuxLike } from "./steer.js";
|
|
20
|
+
import { type ProviderAuthorization } from "./provider-policy.js";
|
|
20
21
|
export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
21
22
|
export interface ApprovalDeps {
|
|
22
23
|
config: ConnectorConfig;
|
|
@@ -55,7 +56,7 @@ export declare const DECISIONS: readonly ["approve", "approve_with_changes", "re
|
|
|
55
56
|
export type Decision = (typeof DECISIONS)[number];
|
|
56
57
|
/** The task a work proposal asks for, written from the proposal's own fields. */
|
|
57
58
|
export declare function proposalTask(record: ConnectorRecord): string;
|
|
58
|
-
export declare function runBrief(record: ConnectorRecord, approver: MiadiPerson, sessions: string[], instructions?: string): {
|
|
59
|
+
export declare function runBrief(record: ConnectorRecord, approver: MiadiPerson, sessions: string[], instructions?: string, providerAttrs?: Record<string, string>): {
|
|
59
60
|
to: string;
|
|
60
61
|
brief: string;
|
|
61
62
|
};
|
|
@@ -83,6 +84,14 @@ export declare class StaleVersion extends AlreadyDecided {
|
|
|
83
84
|
readonly decidedVersion: number;
|
|
84
85
|
constructor(currentVersion: number, decidedVersion: number);
|
|
85
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* An approval of a run whose provider needs the requester's own choice for this mission,
|
|
89
|
+
* and has none (jgwill/Miadi#926). Declining or sending it back stays possible.
|
|
90
|
+
*/
|
|
91
|
+
export declare class ProviderChoiceRequired extends Error {
|
|
92
|
+
readonly authorization: ProviderAuthorization;
|
|
93
|
+
constructor(authorization: ProviderAuthorization);
|
|
94
|
+
}
|
|
86
95
|
/** What a decision answers: the state it left, its receipt, and `replayed` when it repeats a decision already taken. */
|
|
87
96
|
export interface Decided {
|
|
88
97
|
decision: NonNullable<ConnectorRecord["approval_state"]>;
|
|
@@ -135,7 +144,7 @@ export declare function rerun(r: ConnectorRecord, person: MiadiPerson, deps: App
|
|
|
135
144
|
}>;
|
|
136
145
|
export declare function isApprovable(r: ConnectorRecord | undefined): r is ConnectorRecord;
|
|
137
146
|
/** What approving a request does, in one sentence, for the card a person decides on. */
|
|
138
|
-
export declare function consequenceOf(r: ConnectorRecord): string;
|
|
147
|
+
export declare function consequenceOf(r: ConnectorRecord, authorization?: ProviderAuthorization): string;
|
|
139
148
|
export declare function registerApprovalRoutes(app: Express, deps: ApprovalDeps): void;
|
|
140
149
|
/** The request, and the decision form for a write-tier person (their token carried in the form they just signed into). */
|
|
141
150
|
/**
|
|
@@ -146,5 +155,7 @@ export declare function requestProjection(record: ConnectorRecord, opts?: {
|
|
|
146
155
|
reviewBase?: string;
|
|
147
156
|
factoryDir?: string;
|
|
148
157
|
}): RequestProjection | null;
|
|
158
|
+
/** The person's own provider policy page: read and change it, signed in with their person token (jgwill/Miadi#926). */
|
|
159
|
+
export declare function registerProviderPolicyRoutes(app: Express, deps: Pick<ApprovalDeps, "ledger" | "identify" | "log">): void;
|
|
149
160
|
/** How a request is named to a person. */
|
|
150
161
|
export declare const KIND_LABELS: Record<string, string>;
|
package/dist/approve.js
CHANGED
|
@@ -24,6 +24,8 @@ import { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
|
24
24
|
import { factoryLedgerPath, readFactoryReceipts, receiptsForRecord } from "./factory-receipt.js";
|
|
25
25
|
import { steerAttempts, steerSession, tmuxAdapter } from "./steer.js";
|
|
26
26
|
import { contentHash, versionOf } from "./revisions.js";
|
|
27
|
+
import { canReadReading } from "./reading/privacy.js";
|
|
28
|
+
import { authorizationReceipt, authorizeProvider, briefAttributes, CHOICE_TTL_MS, CODEX_POLICIES, isPersonId, needsPersonChoice, providerPolicyOf, } from "./provider-policy.js";
|
|
27
29
|
export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
28
30
|
/**
|
|
29
31
|
* One line per step of a decision, for the journal: the request, a correlation id,
|
|
@@ -103,8 +105,10 @@ export function proposalTask(record) {
|
|
|
103
105
|
}
|
|
104
106
|
return lines.join("\n");
|
|
105
107
|
}
|
|
106
|
-
export function runBrief(record, approver, sessions, instructions) {
|
|
108
|
+
export function runBrief(record, approver, sessions, instructions, providerAttrs = {}) {
|
|
107
109
|
const body = (record.kind === "work_proposal" ? { task: proposalTask(record) } : record.body);
|
|
110
|
+
// A request recorded before providers were resolved (jgwill/Miadi#893) names only what was asked (#926 review RR7).
|
|
111
|
+
const provider = body.resolved_provider ?? body.provider;
|
|
108
112
|
const short = record.id.split(":")[1]?.slice(0, 8) ?? "run";
|
|
109
113
|
const to = body.session && sessions.includes(body.session) ? body.session : `new:${body.session ?? `connector-${yymmdd()}-${short}`}`;
|
|
110
114
|
// A new session opens where the request says, else where the factory runner's config says (defaultCwd): no host path here (#911).
|
|
@@ -116,7 +120,9 @@ export function runBrief(record, approver, sessions, instructions) {
|
|
|
116
120
|
`record="${attr(record.id)}"`,
|
|
117
121
|
`version="${versionOf(record)}"`,
|
|
118
122
|
// The provider the connector verified at request time (jgwill/Miadi#893); the intake may honour it or record why not.
|
|
119
|
-
...(
|
|
123
|
+
...(provider ? [`provider="${attr(provider)}"`] : []),
|
|
124
|
+
// What authorizes a provider that needs the requester's own choice; the runner refuses Codex without it (jgwill/Miadi#926).
|
|
125
|
+
...Object.entries(providerAttrs).map(([k, v]) => `${k}="${attr(v)}"`),
|
|
120
126
|
...(to.startsWith("new:") && cwd ? [`cwd="${attr(cwd)}"`] : []),
|
|
121
127
|
];
|
|
122
128
|
const lines = [
|
|
@@ -189,6 +195,11 @@ export async function executeApproved(record, approver, deps, instructions) {
|
|
|
189
195
|
return { status: res.status, ok: res.ok, response: parsed };
|
|
190
196
|
};
|
|
191
197
|
const queueBrief = () => {
|
|
198
|
+
// Checked again where the brief is written: an approval, and every rerun of it (jgwill/Miadi#926).
|
|
199
|
+
const authorization = record.kind === "run_request" ? authorizeProvider(record, deps.ledger.all()) : undefined;
|
|
200
|
+
if (authorization && !authorization.authorized) {
|
|
201
|
+
return { outcome: "failed", provider_authorization: authorizationReceipt(authorization), message: `No brief was written. ${authorization.explanation}` };
|
|
202
|
+
}
|
|
192
203
|
const queue = join(deps.factoryDir ?? factoryDir(), "queue");
|
|
193
204
|
mkdirSync(queue, { recursive: true });
|
|
194
205
|
// A lister that cannot answer means no session is known live: the brief opens a new one (#922 R16).
|
|
@@ -199,10 +210,16 @@ export async function executeApproved(record, approver, deps, instructions) {
|
|
|
199
210
|
catch {
|
|
200
211
|
sessions = [];
|
|
201
212
|
}
|
|
202
|
-
const { to, brief } = runBrief(record, approver, sessions, instructions);
|
|
213
|
+
const { to, brief } = runBrief(record, approver, sessions, instructions, authorization ? briefAttributes(authorization) : {});
|
|
203
214
|
const file = runBriefFile(record);
|
|
204
215
|
writeFileSync(join(queue, file), brief, { flag: "wx", mode: 0o640 });
|
|
205
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
outcome: "queued",
|
|
218
|
+
to,
|
|
219
|
+
file,
|
|
220
|
+
...(authorization ? { provider_authorization: authorizationReceipt(authorization) } : {}),
|
|
221
|
+
note: "The factory intake delivers queued briefs every 5 minutes.",
|
|
222
|
+
};
|
|
206
223
|
};
|
|
207
224
|
switch (record.kind) {
|
|
208
225
|
case "run_request":
|
|
@@ -357,6 +374,17 @@ export class StaleVersion extends AlreadyDecided {
|
|
|
357
374
|
this.decidedVersion = decidedVersion;
|
|
358
375
|
}
|
|
359
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* An approval of a run whose provider needs the requester's own choice for this mission,
|
|
379
|
+
* and has none (jgwill/Miadi#926). Declining or sending it back stays possible.
|
|
380
|
+
*/
|
|
381
|
+
export class ProviderChoiceRequired extends Error {
|
|
382
|
+
authorization;
|
|
383
|
+
constructor(authorization) {
|
|
384
|
+
super(authorization.explanation);
|
|
385
|
+
this.authorization = authorization;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
360
388
|
/** The decisions being recorded now, by request id: one at a time per request, in this process. */
|
|
361
389
|
const deciding = new Map();
|
|
362
390
|
/** Whether a decision on this request is being recorded now, its run may still be going. */
|
|
@@ -414,6 +442,14 @@ export async function decide(r, person, choice, deps, opts) {
|
|
|
414
442
|
}
|
|
415
443
|
if (versionOf(live) !== decidedVersion)
|
|
416
444
|
throw new StaleVersion(versionOf(live), decidedVersion);
|
|
445
|
+
// A generic approval never chooses Codex: the requester's own fresh choice or policy must already stand (jgwill/Miadi#926).
|
|
446
|
+
if (live.kind === "run_request" && approvalStateOf(choice) === "approved") {
|
|
447
|
+
const authorization = authorizeProvider(live, deps.ledger.all(), { requireFresh: true });
|
|
448
|
+
if (!authorization.authorized) {
|
|
449
|
+
logDecision(deps, { phase: "provider_choice_required", record: r.id, via: opts.via, choice, provider: authorization.provider });
|
|
450
|
+
throw new ProviderChoiceRequired(authorization);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
417
453
|
const entry = { ...asked, correlation: randomUUID(), started: Date.now() };
|
|
418
454
|
deciding.set(r.id, entry);
|
|
419
455
|
entry.promise = decideOnce(live, person, choice, deps, { ...opts, correlation: entry.correlation, started: entry.started });
|
|
@@ -602,11 +638,13 @@ export function isApprovable(r) {
|
|
|
602
638
|
return Boolean(r && APPROVABLE.has(r.kind));
|
|
603
639
|
}
|
|
604
640
|
/** What approving a request does, in one sentence, for the card a person decides on. */
|
|
605
|
-
export function consequenceOf(r) {
|
|
641
|
+
export function consequenceOf(r, authorization) {
|
|
606
642
|
const b = r.body;
|
|
607
643
|
switch (r.kind) {
|
|
608
644
|
case "run_request":
|
|
609
|
-
return
|
|
645
|
+
return authorization && !authorization.authorized
|
|
646
|
+
? `Approving is refused until ${r.actor.name} chooses ${authorization.provider} for this mission on its approval page; approving does not choose it. Declining or sending it back stays possible.`
|
|
647
|
+
: `Approving queues a brief for the factory intake, which opens or reaches an agent session within minutes${authorization?.provider ? ` with ${authorization.provider}` : ""}.`;
|
|
610
648
|
case "work_proposal":
|
|
611
649
|
return "Approving starts the work: a brief written from the proposal and your instructions is queued for an agent session.";
|
|
612
650
|
case "review_operation_request":
|
|
@@ -658,8 +696,43 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
658
696
|
if (!writer && r.actor.person_id !== person.id) {
|
|
659
697
|
return void res.status(403).type("html").send(signInToRead(r, `Reading and deciding this request needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.`));
|
|
660
698
|
}
|
|
699
|
+
const requester = r.actor.person_id === person.id && isPersonId(person.id);
|
|
661
700
|
if (!body.decision)
|
|
662
|
-
return void res.type("html").send(approvePage(deps, r, { token: writer ? token : undefined }));
|
|
701
|
+
return void res.type("html").send(approvePage(deps, r, { token: writer ? token : undefined, chooserToken: requester ? token : undefined }));
|
|
702
|
+
if (body.decision === "choose_provider") {
|
|
703
|
+
// The requester's own, separate choice of a provider that needs one, for this version only (jgwill/Miadi#926).
|
|
704
|
+
const live = find(r.id);
|
|
705
|
+
const again = (status, error) => void res.status(status).type("html").send(approvePage(deps, live, { token: writer ? token : undefined, chooserToken: requester ? token : undefined, error }));
|
|
706
|
+
if (!requester) {
|
|
707
|
+
return again(403, isPersonId(r.actor.person_id)
|
|
708
|
+
? `Only ${r.actor.name}, who asked, can choose a provider for their own mission.`
|
|
709
|
+
: `${r.actor.name} is a service token: only a person chooses a provider, for a request of their own. Name another provider for this request.`);
|
|
710
|
+
}
|
|
711
|
+
if (live.approval_state !== "awaiting_approval" || isBeingDecided(live.id))
|
|
712
|
+
return again(409, "A provider is chosen only while the request awaits a decision.");
|
|
713
|
+
const provider = String(body.provider ?? "");
|
|
714
|
+
const named = authorizeProvider(live, deps.ledger.all(), { requireFresh: true });
|
|
715
|
+
if (!needsPersonChoice(provider) || provider !== named.provider)
|
|
716
|
+
return again(400, `This request names ${named.provider ?? "no provider"}; only that provider can be chosen here.`);
|
|
717
|
+
if (Number(body.version) !== versionOf(live))
|
|
718
|
+
return again(409, `This request is now at version ${versionOf(live)}. Read it and choose again.`);
|
|
719
|
+
if (body.confirm !== "yes")
|
|
720
|
+
return again(400, `Tick the box to choose ${provider} for this mission.`);
|
|
721
|
+
if (!named.authorized || named.state !== "person_choice") {
|
|
722
|
+
deps.ledger.append({
|
|
723
|
+
id: newRecordId("provider_choice"),
|
|
724
|
+
kind: "provider_choice",
|
|
725
|
+
created_at: new Date().toISOString(),
|
|
726
|
+
actor: { person_id: person.id, name: person.name, client_id: "approval-page" },
|
|
727
|
+
refs: [live.id],
|
|
728
|
+
evidence_refs: [],
|
|
729
|
+
claim_category: "observed",
|
|
730
|
+
body: { request_id: live.id, provider, version: versionOf(live), content_hash: contentHash(live) },
|
|
731
|
+
});
|
|
732
|
+
logDecision(deps, { phase: "provider_chosen", record: live.id, provider, version: versionOf(live) });
|
|
733
|
+
}
|
|
734
|
+
return void res.type("html").send(approvePage(deps, find(r.id), { token: writer ? token : undefined, chooserToken: token }));
|
|
735
|
+
}
|
|
663
736
|
if (!writer)
|
|
664
737
|
return void res.status(403).type("html").send(approvePage(deps, r, { error: `Deciding needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.` }));
|
|
665
738
|
if (body.decision === "rerun") {
|
|
@@ -705,6 +778,9 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
705
778
|
decided = settled;
|
|
706
779
|
}
|
|
707
780
|
catch (error) {
|
|
781
|
+
if (error instanceof ProviderChoiceRequired) {
|
|
782
|
+
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, chooserToken: requester ? token : undefined, error: error.message }));
|
|
783
|
+
}
|
|
708
784
|
if (error instanceof StaleVersion)
|
|
709
785
|
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, error: error.message }));
|
|
710
786
|
if (error instanceof AlreadyDecided)
|
|
@@ -746,6 +822,9 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
746
822
|
const r = find(String(req.params.id));
|
|
747
823
|
if (!r)
|
|
748
824
|
return void res.status(404).type("html").send(page("Not found", "<h1>No record with that id</h1>"));
|
|
825
|
+
if (!canReadReading(r, current(deps.ledger.all()), person)) {
|
|
826
|
+
return void res.status(403).type("html").send(page("Miadi record", recordForm(String(req.params.id), "This source and its reading are private to the source owner and factory operators.")));
|
|
827
|
+
}
|
|
749
828
|
if (!person.api_tier && r.actor.person_id !== person.id) {
|
|
750
829
|
return void res.status(403).type("html").send(page("Miadi record", recordForm(String(req.params.id), `Reading records needs Miadi's read tier; ${person.name} has none.`)));
|
|
751
830
|
}
|
|
@@ -806,8 +885,12 @@ function approvePage(deps, r, o = {}) {
|
|
|
806
885
|
const open = r.approval_state === "awaiting_approval";
|
|
807
886
|
// A decision being recorded offers no buttons: a second click would decide the same request again.
|
|
808
887
|
const actionable = open && !isBeingDecided(r.id);
|
|
888
|
+
// Freshness binds a decision still to come; a decided run shows the authorization it holds (#926 review R1).
|
|
889
|
+
const authorization = r.kind === "run_request" ? authorizeProvider(r, deps.ledger.all(), { requireFresh: open }) : undefined;
|
|
890
|
+
const blocked = Boolean(authorization && !authorization.authorized);
|
|
809
891
|
return page("Approve a Miadi request", `<h1>${open ? "Approve a request" : "A decided request"}</h1>
|
|
810
|
-
${actionable ? `<p>${escapeHtml(consequenceOf(r))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
|
|
892
|
+
${actionable ? `<p>${escapeHtml(consequenceOf(r, authorization))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
|
|
893
|
+
${authorization ? providerSection(r, authorization, actionable ? o.chooserToken : undefined) : ""}
|
|
811
894
|
${summary(r)}
|
|
812
895
|
${projectionBlock(projectionOf(r, deps), open ? undefined : r.completion_receipt)}
|
|
813
896
|
${o.error ? `<p class="err" role="alert">${escapeHtml(o.error)}</p>` : ""}
|
|
@@ -818,8 +901,10 @@ ${actionable && o.token
|
|
|
818
901
|
<label for="i">Instructions (needed to approve with changes or to send back)</label>
|
|
819
902
|
<textarea id="i" name="instructions" rows="4"></textarea>
|
|
820
903
|
<div class="row">
|
|
821
|
-
|
|
822
|
-
|
|
904
|
+
${blocked
|
|
905
|
+
? ""
|
|
906
|
+
: `<button class="primary" type="submit" name="decision" value="approve">Approve and run</button>
|
|
907
|
+
<button type="submit" name="decision" value="approve_with_changes">Approve with my changes</button>`}
|
|
823
908
|
<button type="submit" name="decision" value="request_revision">Send back for revision</button>
|
|
824
909
|
<button type="submit" name="decision" value="decline">Decline</button>
|
|
825
910
|
</div></form>`
|
|
@@ -834,6 +919,87 @@ ${actionable && o.token
|
|
|
834
919
|
<div class="row"><button class="primary" type="submit" name="decision" value="rerun">Run it again</button></div></form>`
|
|
835
920
|
: ""}`);
|
|
836
921
|
}
|
|
922
|
+
/**
|
|
923
|
+
* Which provider a run launches with, who chose it, and, when the requester's own choice is
|
|
924
|
+
* missing, the separate control only the requester sees (jgwill/Miadi#926).
|
|
925
|
+
*/
|
|
926
|
+
function providerSection(r, a, chooserToken) {
|
|
927
|
+
const lines = [
|
|
928
|
+
`<dt>Provider</dt><dd>${escapeHtml(a.provider ?? "the factory runner's default")}</dd>`,
|
|
929
|
+
`<dt>Asked for</dt><dd>${escapeHtml(a.requested ?? "none named")}${a.requester.client ? ` (through ${escapeHtml(a.requester.client)})` : ""}</dd>`,
|
|
930
|
+
`<dt>Authorized by</dt><dd>${escapeHtml(a.state.replace(/_/g, " "))}</dd>`,
|
|
931
|
+
].join("");
|
|
932
|
+
const choose = a.state === "needs_person_choice" && chooserToken && a.provider
|
|
933
|
+
? `<form method="post">
|
|
934
|
+
<input type="hidden" name="person_token" value="${escapeHtml(chooserToken)}">
|
|
935
|
+
<input type="hidden" name="version" value="${versionOf(r)}">
|
|
936
|
+
<input type="hidden" name="provider" value="${escapeHtml(a.provider)}">
|
|
937
|
+
<label><input type="checkbox" name="confirm" value="yes" required> I choose ${escapeHtml(a.provider)} for this mission (version ${versionOf(r)}) myself. It does not cover any other mission.</label>
|
|
938
|
+
<div class="row"><button type="submit" name="decision" value="choose_provider">Choose ${escapeHtml(a.provider)} for this mission</button></div>
|
|
939
|
+
</form>`
|
|
940
|
+
: a.state === "needs_person_choice" && !isPersonId(r.actor.person_id)
|
|
941
|
+
? ""
|
|
942
|
+
: a.state === "needs_person_choice"
|
|
943
|
+
? `<p>Only ${escapeHtml(r.actor.name)}, who asked, can choose ${escapeHtml(a.provider ?? "")} for this mission, signed in here. Within ${CHOICE_TTL_MS / 3_600_000} hours of that choice it can be approved.</p>`
|
|
944
|
+
: "";
|
|
945
|
+
return `<section aria-label="Provider"><dl>${lines}</dl><p>${escapeHtml(a.explanation)}</p>${choose}</section>`;
|
|
946
|
+
}
|
|
947
|
+
/** The person's own provider policy page: read and change it, signed in with their person token (jgwill/Miadi#926). */
|
|
948
|
+
export function registerProviderPolicyRoutes(app, deps) {
|
|
949
|
+
const form = express.urlencoded({ extended: false, limit: "16kb" });
|
|
950
|
+
const signIn = (error) => page("Your provider policy", `<h1>Your provider policy</h1><p>Which agent providers a request made in your name may name. Sign in to read or change yours; it applies to your requests only.</p>
|
|
951
|
+
${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
952
|
+
<form method="post"><label for="t">Your Miadi person token</label><input id="t" name="person_token" type="password" autocomplete="off" required>
|
|
953
|
+
<div class="row"><button class="primary" type="submit">Read my policy</button></div></form>`);
|
|
954
|
+
app.get("/providers", (_req, res) => void res.type("html").send(signIn()));
|
|
955
|
+
app.post("/providers", form, async (req, res) => {
|
|
956
|
+
const body = (req.body ?? {});
|
|
957
|
+
const token = String(body.person_token ?? "").trim();
|
|
958
|
+
let person;
|
|
959
|
+
try {
|
|
960
|
+
person = await deps.identify(token);
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
if (!(error instanceof MiadiUnavailable))
|
|
964
|
+
throw error;
|
|
965
|
+
return void res.status(503).type("html").send(signIn("Miadi could not be reached. Try again in a minute."));
|
|
966
|
+
}
|
|
967
|
+
if (!person)
|
|
968
|
+
return void res.status(401).type("html").send(signIn("Miadi did not recognise that person token."));
|
|
969
|
+
if (!isPersonId(person.id))
|
|
970
|
+
return void res.status(403).type("html").send(signIn("A service token has no provider policy; a person sets their own."));
|
|
971
|
+
let saved = "";
|
|
972
|
+
if (body.codex !== undefined) {
|
|
973
|
+
if (!CODEX_POLICIES.includes(body.codex))
|
|
974
|
+
return void res.status(400).type("html").send(signIn("Unknown Codex policy."));
|
|
975
|
+
if (providerPolicyOf(deps.ledger.all(), person.id).codex !== body.codex) {
|
|
976
|
+
deps.ledger.append({
|
|
977
|
+
id: newRecordId("provider_policy"),
|
|
978
|
+
kind: "provider_policy",
|
|
979
|
+
created_at: new Date().toISOString(),
|
|
980
|
+
actor: { person_id: person.id, name: person.name, client_id: "providers-page" },
|
|
981
|
+
refs: [],
|
|
982
|
+
evidence_refs: [],
|
|
983
|
+
claim_category: "observed",
|
|
984
|
+
body: { codex: body.codex },
|
|
985
|
+
});
|
|
986
|
+
logDecision(deps, { phase: "provider_policy_set", person: person.id, codex: body.codex });
|
|
987
|
+
}
|
|
988
|
+
saved = "<p>Saved. It applies to your requests from now on, including a failed run of yours that is run again. A per-mission choice you already made still covers its mission.</p>";
|
|
989
|
+
}
|
|
990
|
+
const policy = providerPolicyOf(deps.ledger.all(), person.id);
|
|
991
|
+
const option = (value, label) => `<label><input type="radio" name="codex" value="${value}"${policy.codex === value ? " checked" : ""}> ${escapeHtml(label)}</label>`;
|
|
992
|
+
res.type("html").send(page("Your provider policy", `<h1>${escapeHtml(person.name)}'s provider policy</h1>${saved}
|
|
993
|
+
<dl><dt>Codex</dt><dd>${escapeHtml(policy.codex.replace(/_/g, " "))}${policy.source === "default" ? " (the default)" : `, set ${escapeHtml(policy.set_at ?? "")}`}</dd>
|
|
994
|
+
<dt>Other providers</dt><dd>Pi, Copilot and Claude Code run when a request names them and they are installed; Pi and Copilot may also be picked by the host's order.</dd></dl>
|
|
995
|
+
<form method="post"><input type="hidden" name="person_token" value="${escapeHtml(token)}">
|
|
996
|
+
<fieldset><legend>When a request in my name names Codex</legend>
|
|
997
|
+
${option("per_mission_choice", "Codex runs only when I choose it for that mission on its approval page (default).")}
|
|
998
|
+
${option("selectable", "Codex may be named for my requests without a per-mission choice.")}
|
|
999
|
+
</fieldset>
|
|
1000
|
+
<div class="row"><button class="primary" type="submit">Save my policy</button></div></form>`));
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
837
1003
|
/** How a request is named to a person. */
|
|
838
1004
|
export const KIND_LABELS = {
|
|
839
1005
|
run_request: "Agent run",
|
package/dist/mission-status.js
CHANGED
|
@@ -108,7 +108,9 @@ function nextAction(record, standing, queue, result) {
|
|
|
108
108
|
case "dispatching":
|
|
109
109
|
return queue?.state === "delivered"
|
|
110
110
|
? "The brief is with its session. Follow it, or steer it with request_session_steer inside the approved grant."
|
|
111
|
-
:
|
|
111
|
+
: queue?.state === "refused"
|
|
112
|
+
? "The factory runner refused to launch its provider; nothing runs. The requester decides on a new request with a provider they chose."
|
|
113
|
+
: "Wait for the factory intake to deliver the brief.";
|
|
112
114
|
case "failed":
|
|
113
115
|
return standing.rerun.available ? "A write-tier person may run it again with the same approval." : "Read the failure and decide.";
|
|
114
116
|
case "completed":
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type JevConfig, type ProviderAvailability, type ProviderId } from "./providers.js";
|
|
2
|
+
import { type ProviderAuthorization } from "./provider-policy.js";
|
|
2
3
|
export declare const PREFLIGHT_CONTRACT = "miadi.mission_model_preflight";
|
|
3
4
|
export declare const PREFLIGHT_VERSION = 1;
|
|
4
5
|
/** Below this, a Jev choice is not acted on (the ≥0.8 practice of jgwill/Miadi#770). */
|
|
@@ -34,7 +35,7 @@ export interface LaunchEvidence {
|
|
|
34
35
|
/** What shows it (a process line, a pane header); reported by the caller, not checked by the connector. */
|
|
35
36
|
evidence: string;
|
|
36
37
|
}
|
|
37
|
-
export type PreflightState = "recommended" | "abstained" | "provider_mismatch";
|
|
38
|
+
export type PreflightState = "recommended" | "abstained" | "provider_mismatch" | "provider_not_authorized";
|
|
38
39
|
export type DeciderSource = "jev" | "static_fallback" | "synthetic_fixture";
|
|
39
40
|
export type RouterState = "answered" | "not_requested" | "router_unconfigured" | "router_unavailable" | "invalid_answer";
|
|
40
41
|
export interface ModelPreflight {
|
|
@@ -57,6 +58,8 @@ export interface ModelPreflight {
|
|
|
57
58
|
/** approved: the request was approved with this provider. requested: it is not approved (yet); the provider is only asked for. */
|
|
58
59
|
provider_basis: "approved" | "requested";
|
|
59
60
|
candidates: ModelCandidate[];
|
|
61
|
+
/** Whether the mission's provider is authorized and by whom (jgwill/Miadi#926). */
|
|
62
|
+
provider_authorization?: Record<string, unknown>;
|
|
60
63
|
decider: {
|
|
61
64
|
source: DeciderSource;
|
|
62
65
|
router: RouterState;
|
|
@@ -117,7 +120,7 @@ export interface JevRequest {
|
|
|
117
120
|
}
|
|
118
121
|
/** The model a provider's own config names, when it names one. Only that key is read. */
|
|
119
122
|
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[];
|
|
123
|
+
export declare function candidatesFor(found: ProviderAvailability[], approved: ProviderId | null, modelOf?: (p: ProviderId) => string | null, basis?: "approved" | "requested", authorization?: Pick<ProviderAuthorization, "authorized" | "provider" | "requester">): ModelCandidate[];
|
|
121
124
|
/** The least review a task gets, whatever any decider says. Jev may raise it, never lower it. */
|
|
122
125
|
export declare function reviewFloor(task: TaskType, criticality: Criticality): ReviewTier;
|
|
123
126
|
export declare function decisionState(task: TaskType, criticality: Criticality, candidates: ModelCandidate[]): Record<string, unknown>;
|
|
@@ -134,6 +137,10 @@ export interface PreflightInput {
|
|
|
134
137
|
task: TaskType;
|
|
135
138
|
criticality: Criticality;
|
|
136
139
|
candidates: ModelCandidate[];
|
|
140
|
+
/** Whether the mission's provider is authorized, and by whom (jgwill/Miadi#926); shown as given. */
|
|
141
|
+
provider_authorization?: Record<string, unknown> & {
|
|
142
|
+
authorized?: boolean;
|
|
143
|
+
};
|
|
137
144
|
launch?: LaunchEvidence;
|
|
138
145
|
/** The decider to consult, when the caller asked for one; absent means static fallback only. */
|
|
139
146
|
adapter?: JevAdapter;
|
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>;
|