@miadi/connector 0.1.12 → 0.1.14
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 +15 -3
- package/dist/approve.d.ts +8 -1
- package/dist/approve.js +39 -7
- package/dist/decisions.d.ts +4 -2
- package/dist/decisions.js +18 -9
- 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 +199 -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/queue-state.js +2 -2
- package/dist/records.d.ts +13 -4
- package/dist/records.js +11 -3
- package/dist/request-state.d.ts +9 -1
- package/dist/request-state.js +37 -1
- package/dist/revisions.d.ts +110 -0
- package/dist/revisions.js +245 -0
- package/dist/server.d.ts +7 -3
- package/dist/server.js +4 -1
- package/dist/tools.d.ts +13 -2
- package/dist/tools.js +299 -22
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Call `get_capabilities` first. It names the signed-in person, their Miadi API ti
|
|
|
20
20
|
|
|
21
21
|
| kind | tools |
|
|
22
22
|
|---|---|
|
|
23
|
-
| reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `read_episode_file` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `mission_status` `provider_capabilities` `search` `fetch` |
|
|
23
|
+
| reads | `get_capabilities` `get_context` `list_circles` `get_circle` `list_sessions` `get_session` `get_evidence` `read_episode_file` `get_evaluations` `get_narrative_beats` `review_lookup` `review_versions` `handoff_status` `list_records` `get_run_result` `mission_status` `provider_capabilities` `mission_model_preflight` `search` `fetch` |
|
|
24
24
|
| contributions | `append_observation` `open_inquiry` `link_records` `propose_narrative_beat` `record_evaluation` `register_source_intent` `record_handoff` `update_handoff` `prepare_calendar_event` `save_research` |
|
|
25
25
|
| requests (approved by a person) | `propose_work` `propose_session_completion` `request_authorized_run` `request_session_steer` `request_review_operation` `request_deep_diver` `link_review_to_episode` `request_inquiry_episode` |
|
|
26
26
|
| decisions in the conversation | `show_decisions` (cards), `decide_request` (called by the card only) |
|
|
@@ -42,13 +42,25 @@ The model that asked cannot approve its own request.
|
|
|
42
42
|
|
|
43
43
|
`get_run_result` returns one `projection` of the decision, dispatch/completion state, receipt link and outstanding instructions. The in-chat card, approval page and Miadi's research page use that same read model. Asking `show_decisions` for an already-decided `record_id` shows its receipt without issuing another decision token. An in-flight card can refresh status without repeating the operation. A completed basic review with unapplied approval instructions remains open as **changes outstanding**, not falsely finished.
|
|
44
44
|
|
|
45
|
+
### Revising a mission before it is decided
|
|
46
|
+
|
|
47
|
+
A mission is revised, never re-requested (jgwill/Miadi#919). `revise_pending_request` changes an agent run or work proposal that still awaits a decision, or that its approver sent back, and gives it its next version. Pass the version you read as `expected_version` and only the fields that change. The request keeps its id, its approval page and its one card. A change of `provider` is verified on the host again. Each revision is a record in the ledger carrying the whole new content, its reason, author, changed fields and `sha256` content hash, so every earlier version stays readable verbatim with `get_request_history`. A request already approved, queued or declined is refused: a change after a decision is a new request or a steer.
|
|
48
|
+
|
|
49
|
+
A decision names the version it decides. The approval page carries the version it shows, and each card's decision token is bound to its version. A page or card that showed an earlier version is refused as `stale_version`, so no brief is queued for a version the person did not read. The receipt and the brief (`version="N"`, and the hash) name the version approved.
|
|
50
|
+
|
|
51
|
+
Requests made before this, one new request per revision, are joined with `supersede_pending_requests`: name the request that stands and the exact ids of the earlier ones. Each earlier request must still await a decision and be asked by the same person. It is retired (`Superseded by a later version`: it can no longer be approved, and its page says which request to decide) and becomes an earlier version of the standing request. All are checked before anything is written, and a request already approved or queued is refused, never cancelled.
|
|
52
|
+
|
|
45
53
|
### Following and steering a mission
|
|
46
54
|
|
|
47
55
|
`mission_status` joins one mission read-only: its source request and decision, the sessions bound to it (each with how it is bound — a signed `session:` ref, the delivered brief's target, or the requested name), its issues and PRs, its steering grant, and where it stands stage by stage — approved, queued, delivered, running, merged, released, closed — each by its evidence, so an idle pane or a merge is never read as completion. Live panes and Miadi's session view are joined only for the write tier.
|
|
48
56
|
|
|
57
|
+
A mission is found by the issue it implements (jgwill/Miadi#913). `request_authorized_run` and `propose_work` take `primary_issue`; older requests are read by their first issue ref, as an inference. An issue held as primary by several requests answers `ambiguous_mission` with the candidates, an issue only cited answers `related_only`, and only a record id or a declared primary that no older request holds by inference (`contested`) authorizes a steer, release or closure (`identity.authorizes_actions`). A release receipt names artifact, target, production check and check time; record it as `completion_receipt.release` on a handoff that cites the mission. `released` is `verified` only when the connector confirms it itself (the target is `miadi-connector` and the running version is the one released); any other complete receipt within seven days is `receipt_recorded`, with who recorded it and whether that was the approver. Old checks are `stale`; a version string, a future date or an incomplete receipt is `claimed_unverified`. `closed` needs an approved `propose_session_completion` ending (`exit` or `complete_then_exit`) that names the mission in its `mission` field and ends one of the mission's sessions; a completed request is `request_completed`. A GitHub reference the connector cannot read is `unverified`, with the reason (`no_github_token`, `not_visible_to_token`…), never `not_found`.
|
|
58
|
+
|
|
49
59
|
`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. A steer the grant does not cover waits for one explicit scoped human decision on its `approval_url`, which runs exactly that instruction once.
|
|
50
60
|
|
|
51
|
-
`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 is never chosen silently — it 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
|
|
61
|
+
`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 is never chosen silently — it 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.
|
|
62
|
+
|
|
63
|
+
`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.
|
|
52
64
|
|
|
53
65
|
### Deciding inside the conversation
|
|
54
66
|
|
|
@@ -56,7 +68,7 @@ In a client that renders MCP Apps (ChatGPT among them), `show_decisions` draws o
|
|
|
56
68
|
|
|
57
69
|
The model that asked cannot decide for the person:
|
|
58
70
|
- `decide_request` is declared for the card only (`_meta.ui.visibility: ["app"]`), so the host does not offer it to the model.
|
|
59
|
-
- It requires a decision token that travels only in the `_meta` of the `show_decisions` result. Hosts deliver that metadata to the card and keep it from the model. The token is bound to one request, one person and one OAuth client, and expires after 30 minutes. A decided request no longer awaits a decision, so a token works once.
|
|
71
|
+
- It requires a decision token that travels only in the `_meta` of the `show_decisions` result. Hosts deliver that metadata to the card and keep it from the model. The token is bound to one request at the version its card shows, one person and one OAuth client, and expires after 30 minutes. A decided request no longer awaits a decision, so a token works once.
|
|
60
72
|
- The sign-in must carry `miadi:approve`. A person grants it by ticking "Let me approve requests on cards inside this client" on the sign-in page. Only a person with Miadi's write tier receives it, only for a client that sends people back to chatgpt.com, and it ends when Miadi no longer gives them the write tier. A person token used directly as Bearer never carries it.
|
|
61
73
|
|
|
62
74
|
The approval page itself (`/approve/<record>`) names only the kind of request until someone signs in. A write-tier person then reads and decides it, and whoever asked can read it.
|
package/dist/approve.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface ApprovalDeps {
|
|
|
24
24
|
miadi: MiadiClient;
|
|
25
25
|
identify: (token: string) => Promise<MiadiPerson | null>;
|
|
26
26
|
fetchImpl?: typeof fetch;
|
|
27
|
-
/** The factory directory holding queue/ (default
|
|
27
|
+
/** The factory directory holding queue/ (default /srv/miadi/factory). */
|
|
28
28
|
factoryDir?: string;
|
|
29
29
|
tmuxSessions?: () => string[];
|
|
30
30
|
/** The steering surface (jgwill/Miadi#893); tests inject a fake, production uses tmux. */
|
|
@@ -57,12 +57,19 @@ export declare function executeApproved(record: ConnectorRecord, approver: Miadi
|
|
|
57
57
|
/** A request is decided once: a second decision while one runs, or after it, is refused. */
|
|
58
58
|
export declare class AlreadyDecided extends Error {
|
|
59
59
|
}
|
|
60
|
+
/** A decision on a version the request no longer holds: it was revised after the page or card was read (#919). */
|
|
61
|
+
export declare class StaleVersion extends AlreadyDecided {
|
|
62
|
+
readonly currentVersion: number;
|
|
63
|
+
readonly decidedVersion: number;
|
|
64
|
+
constructor(currentVersion: number, decidedVersion: number);
|
|
65
|
+
}
|
|
60
66
|
/** Whether a decision on this request is being recorded now, its run may still be going. */
|
|
61
67
|
export declare function isBeingDecided(recordId: string): boolean;
|
|
62
68
|
export declare function decide(r: ConnectorRecord, person: MiadiPerson, choice: Decision, deps: ApprovalDeps, opts: {
|
|
63
69
|
instructions?: string;
|
|
64
70
|
note?: string;
|
|
65
71
|
via: string;
|
|
72
|
+
expectedVersion?: number;
|
|
66
73
|
}): Promise<{
|
|
67
74
|
decision: NonNullable<ConnectorRecord["approval_state"]>;
|
|
68
75
|
state: ConnectorRecord["state"];
|
package/dist/approve.js
CHANGED
|
@@ -20,6 +20,7 @@ import { current, newRecordId } from "./records.js";
|
|
|
20
20
|
import { APPROVABLE, projectRequest } from "./request-state.js";
|
|
21
21
|
import { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
22
22
|
import { steerAttempts, steerSession, tmuxAdapter } from "./steer.js";
|
|
23
|
+
import { contentHash, versionOf } from "./revisions.js";
|
|
23
24
|
export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
24
25
|
/**
|
|
25
26
|
* What a person may decide on a request. Approving with changes runs it with the
|
|
@@ -67,21 +68,23 @@ export function runBrief(record, approver, sessions, instructions) {
|
|
|
67
68
|
const body = (record.kind === "work_proposal" ? { task: proposalTask(record) } : record.body);
|
|
68
69
|
const short = record.id.split(":")[1]?.slice(0, 8) ?? "run";
|
|
69
70
|
const to = body.session && sessions.includes(body.session) ? body.session : `new:${body.session ?? `connector-${yymmdd()}-${short}`}`;
|
|
70
|
-
|
|
71
|
+
// A new session opens where the request says, else where the factory runner's config says (defaultCwd): no host path here (#911).
|
|
72
|
+
const cwd = body.cwd;
|
|
71
73
|
const open = [
|
|
72
74
|
`to="${attr(to)}"`,
|
|
73
75
|
'status="send"',
|
|
74
76
|
'type="connector-run"',
|
|
75
77
|
`record="${attr(record.id)}"`,
|
|
78
|
+
`version="${versionOf(record)}"`,
|
|
76
79
|
// The provider the connector verified at request time (jgwill/Miadi#893); the intake may honour it or record why not.
|
|
77
80
|
...(body.resolved_provider ? [`provider="${attr(body.resolved_provider)}"`] : []),
|
|
78
|
-
...(to.startsWith("new:") ? [`cwd="${attr(cwd)}"`] : []),
|
|
81
|
+
...(to.startsWith("new:") && cwd ? [`cwd="${attr(cwd)}"`] : []),
|
|
79
82
|
];
|
|
80
83
|
const lines = [
|
|
81
84
|
`<input ${open.join(" ")}>`,
|
|
82
85
|
`A bounded task requested through the Miadi connector (jgwill/Miadi#815, #816).`,
|
|
83
86
|
"",
|
|
84
|
-
`- Record: ${text(record.id)}`,
|
|
87
|
+
`- Record: ${text(record.id)}, version ${versionOf(record)} (${contentHash(record)})`,
|
|
85
88
|
`- Requested by: ${text(record.actor.name)}${record.actor.client_name ? ` through ${text(record.actor.client_name)}` : ""}, ${text(record.created_at)}`,
|
|
86
89
|
`- Approved by: ${text(approver.name)}, ${new Date().toISOString()}`,
|
|
87
90
|
...(record.circle ? [`- Accountable circle: ${text(record.circle)}`] : []),
|
|
@@ -106,7 +109,7 @@ export function runBrief(record, approver, sessions, instructions) {
|
|
|
106
109
|
"",
|
|
107
110
|
"When it is done, reply with what was achieved, the evidence (commits, paths, URLs), what is still open, and whether this session should end. Then record the receipt through the connector's update_handoff if a handoff names this record, and end the session per its lifecycle.",
|
|
108
111
|
"",
|
|
109
|
-
"Queued by the Miadi connector's approval door, delivered by the factory
|
|
112
|
+
"Queued by the Miadi connector's approval door, delivered by the factory runner (@miadi/factory-runner).",
|
|
110
113
|
"</input>",
|
|
111
114
|
];
|
|
112
115
|
return { to, brief: `${lines.join("\n")}\n` };
|
|
@@ -267,6 +270,7 @@ ${r.circle ? `<dt>Circle</dt><dd>${escapeHtml(r.circle)}</dd>` : ""}
|
|
|
267
270
|
${r.intention ? `<dt>Intention</dt><dd>${escapeHtml(r.intention)}</dd>` : ""}
|
|
268
271
|
${r.refs.length ? `<dt>References</dt><dd>${r.refs.map(escapeHtml).join("<br>")}</dd>` : ""}
|
|
269
272
|
<dt>State</dt><dd>${escapeHtml(r.approval_state ?? "—")}</dd>
|
|
273
|
+
${versionOf(r) > 1 ? `<dt>Version</dt><dd>${versionOf(r)}, the one a decision here decides. Earlier versions stay in the ledger.</dd>` : ""}
|
|
270
274
|
</dl>
|
|
271
275
|
<pre>${escapeHtml(JSON.stringify(shown, null, 2))}</pre>`;
|
|
272
276
|
}
|
|
@@ -279,6 +283,16 @@ ${r.refs.length ? `<dt>References</dt><dd>${r.refs.map(escapeHtml).join("<br>")}
|
|
|
279
283
|
/** A request is decided once: a second decision while one runs, or after it, is refused. */
|
|
280
284
|
export class AlreadyDecided extends Error {
|
|
281
285
|
}
|
|
286
|
+
/** A decision on a version the request no longer holds: it was revised after the page or card was read (#919). */
|
|
287
|
+
export class StaleVersion extends AlreadyDecided {
|
|
288
|
+
currentVersion;
|
|
289
|
+
decidedVersion;
|
|
290
|
+
constructor(currentVersion, decidedVersion) {
|
|
291
|
+
super(`This request was revised to version ${currentVersion} after version ${decidedVersion} was shown. Read version ${currentVersion} and decide again.`);
|
|
292
|
+
this.currentVersion = currentVersion;
|
|
293
|
+
this.decidedVersion = decidedVersion;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
282
296
|
const deciding = new Set();
|
|
283
297
|
/** Whether a decision on this request is being recorded now, its run may still be going. */
|
|
284
298
|
export function isBeingDecided(recordId) {
|
|
@@ -286,11 +300,17 @@ export function isBeingDecided(recordId) {
|
|
|
286
300
|
}
|
|
287
301
|
export async function decide(r, person, choice, deps, opts) {
|
|
288
302
|
// Checked again here, with no await between the check and the claim: the
|
|
289
|
-
// page and the card (or two cards) may decide the same request at once
|
|
303
|
+
// page and the card (or two cards) may decide the same request at once, and
|
|
304
|
+
// a revision may land between reading a version and deciding it (#919).
|
|
290
305
|
const live = current(deps.ledger.all()).find((x) => x.id === r.id);
|
|
291
306
|
if (!live || live.approval_state !== "awaiting_approval" || deciding.has(r.id)) {
|
|
292
307
|
throw new AlreadyDecided(deciding.has(r.id) ? "A decision on this request is being recorded." : `This request is already ${live?.approval_state ?? "gone"}.`);
|
|
293
308
|
}
|
|
309
|
+
// A decision that names no version (a page opened before versions, a surface that sends none) decides version 1:
|
|
310
|
+
// a request revised since is refused, never decided blind (#919 review F1).
|
|
311
|
+
const decidedVersion = opts.expectedVersion ?? 1;
|
|
312
|
+
if (versionOf(live) !== decidedVersion)
|
|
313
|
+
throw new StaleVersion(versionOf(live), decidedVersion);
|
|
294
314
|
deciding.add(r.id);
|
|
295
315
|
try {
|
|
296
316
|
return await decideOnce(live, person, choice, deps, opts);
|
|
@@ -368,6 +388,9 @@ async function decideOnce(r, person, choice, deps, opts) {
|
|
|
368
388
|
decided_by: { person_id: person.id, name: person.name },
|
|
369
389
|
decided_at: new Date().toISOString(),
|
|
370
390
|
decision: choice,
|
|
391
|
+
// The exact version decided, so the brief, the receipt and the history name the same content (#919).
|
|
392
|
+
version: versionOf(r),
|
|
393
|
+
content_hash: contentHash(r),
|
|
371
394
|
via: opts.via,
|
|
372
395
|
...(instructions ? { instructions } : {}),
|
|
373
396
|
note: opts.note,
|
|
@@ -397,7 +420,7 @@ async function decideOnce(r, person, choice, deps, opts) {
|
|
|
397
420
|
supersedes: r.id,
|
|
398
421
|
approval_state: decision,
|
|
399
422
|
state,
|
|
400
|
-
...(decision === "revision_requested" ? { next_action: `Revise
|
|
423
|
+
...(decision === "revision_requested" ? { next_action: `Revise it in place with revise_pending_request (request_id ${r.id}, expected_version ${versionOf(r)}).` } : {}),
|
|
401
424
|
completion_receipt: receipt,
|
|
402
425
|
body: { decision: choice, ...(instructions ? { instructions } : {}) },
|
|
403
426
|
});
|
|
@@ -493,9 +516,17 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
493
516
|
}
|
|
494
517
|
let decided;
|
|
495
518
|
try {
|
|
496
|
-
|
|
519
|
+
const shown = Number(body.version);
|
|
520
|
+
decided = await decide(r, person, choice, deps, {
|
|
521
|
+
instructions,
|
|
522
|
+
note: body.note || undefined,
|
|
523
|
+
via: String(body.via ?? "approval-page").slice(0, 60),
|
|
524
|
+
...(Number.isInteger(shown) && shown >= 1 ? { expectedVersion: shown } : {}),
|
|
525
|
+
});
|
|
497
526
|
}
|
|
498
527
|
catch (error) {
|
|
528
|
+
if (error instanceof StaleVersion)
|
|
529
|
+
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, error: error.message }));
|
|
499
530
|
if (error instanceof AlreadyDecided)
|
|
500
531
|
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { error: error.message }));
|
|
501
532
|
throw error;
|
|
@@ -596,6 +627,7 @@ ${o.error ? `<p class="err" role="alert">${escapeHtml(o.error)}</p>` : ""}
|
|
|
596
627
|
${actionable && o.token
|
|
597
628
|
? `<form method="post">
|
|
598
629
|
<input type="hidden" name="person_token" value="${escapeHtml(o.token)}">
|
|
630
|
+
<input type="hidden" name="version" value="${versionOf(r)}">
|
|
599
631
|
<label for="i">Instructions (needed to approve with changes or to send back)</label>
|
|
600
632
|
<textarea id="i" name="instructions" rows="4"></textarea>
|
|
601
633
|
<div class="row">
|
package/dist/decisions.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export declare class DecisionTokens {
|
|
|
6
6
|
private readonly secret;
|
|
7
7
|
constructor(stateDir: string);
|
|
8
8
|
private sign;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/** A token for one card: one request, at the version the card shows, for one person in one client. */
|
|
10
|
+
issue(recordId: string, personId: string, clientId: string, now?: number, version?: number): string;
|
|
11
|
+
/** `stale`: the token is genuine but was minted for a version the request no longer holds. */
|
|
12
|
+
verify(token: string, recordId: string, personId: string, clientId: string, now?: number, currentVersion?: number): "ok" | "expired" | "invalid" | "stale";
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* The card. Self-contained: no network, no external script. It reads the
|
package/dist/decisions.js
CHANGED
|
@@ -30,23 +30,32 @@ export class DecisionTokens {
|
|
|
30
30
|
writeFileSync(file, newSecret(32), { mode: 0o600, flag: "wx" });
|
|
31
31
|
this.secret = readFileSync(file, "utf8").trim();
|
|
32
32
|
}
|
|
33
|
-
sign(recordId, personId, clientId, exp) {
|
|
34
|
-
|
|
33
|
+
sign(recordId, personId, clientId, exp, version) {
|
|
34
|
+
// A token is bound to the version the card showed (jgwill/Miadi#919); one minted before versions has none.
|
|
35
|
+
const v = version === undefined ? "" : `\nv${version}`;
|
|
36
|
+
return createHmac("sha256", this.secret).update(`${recordId}\n${personId}\n${clientId}\n${exp}${v}`).digest("base64url");
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
/** A token for one card: one request, at the version the card shows, for one person in one client. */
|
|
39
|
+
issue(recordId, personId, clientId, now = Math.floor(Date.now() / 1000), version = 1) {
|
|
37
40
|
const exp = now + DECISION_TOKEN_TTL;
|
|
38
|
-
return `${exp}.${this.sign(recordId, personId, clientId, exp)}`;
|
|
41
|
+
return `${exp}.v${version}.${this.sign(recordId, personId, clientId, exp, version)}`;
|
|
39
42
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
+
/** `stale`: the token is genuine but was minted for a version the request no longer holds. */
|
|
44
|
+
verify(token, recordId, personId, clientId, now = Math.floor(Date.now() / 1000), currentVersion = 1) {
|
|
45
|
+
const parts = String(token ?? "").split(".");
|
|
46
|
+
const exp = Number(parts[0]);
|
|
47
|
+
const versioned = parts.length === 3 && /^v\d+$/.test(parts[1]);
|
|
48
|
+
const version = versioned ? Number(parts[1].slice(1)) : undefined;
|
|
49
|
+
const mac = versioned ? parts[2] : parts.length === 2 ? parts[1] : "";
|
|
43
50
|
if (!Number.isInteger(exp) || !mac)
|
|
44
51
|
return "invalid";
|
|
45
|
-
const expected = Buffer.from(this.sign(recordId, personId, clientId, exp));
|
|
52
|
+
const expected = Buffer.from(this.sign(recordId, personId, clientId, exp, version));
|
|
46
53
|
const given = Buffer.from(mac);
|
|
47
54
|
if (expected.length !== given.length || !timingSafeEqual(expected, given))
|
|
48
55
|
return "invalid";
|
|
49
|
-
|
|
56
|
+
if (exp < now)
|
|
57
|
+
return "expired";
|
|
58
|
+
return (version ?? 1) === currentVersion ? "ok" : "stale";
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export { MiadiOAuthProvider, SCOPES, SCOPE_READ, SCOPE_CONTRIBUTE, type Person }
|
|
|
5
5
|
export { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, RECORD_KINDS, current, type ConnectorRecord, type CircleId, type ClaimCategory, type RecordKind, } from "./records.js";
|
|
6
6
|
export { TOOL_STATUS } from "./tools.js";
|
|
7
7
|
export { runBrief, readQueueState } from "./approve.js";
|
|
8
|
-
export { PROVIDERS, discoverProviders, resolveProvider, routerPreflight, type ProviderAvailability, type ProviderId, type ProviderResolution, type RouterPreflight, } from "./providers.js";
|
|
9
|
-
export { missionStatus, resolveMissionSubject, type MissionStatus, type MissionStatusDeps } from "./mission-status.js";
|
|
8
|
+
export { PROVIDERS, discoverProviders, jevConfigOf, resolveProvider, routerPreflight, type JevConfig, type ProviderAvailability, type ProviderId, type ProviderResolution, type RouterPreflight, } from "./providers.js";
|
|
9
|
+
export { missionStatus, primaryIssueOf, resolveMissionSubject, type MissionStatus, type MissionStatusDeps, type MissionIdentity } from "./mission-status.js";
|
|
10
|
+
export { modelPreflight, candidatesFor, buildJevRequest, decisionState, jevAdapter, reviewFloor, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, JEV_CONFIDENCE_FLOOR, type ModelPreflight, type ModelCandidate, type JevAdapter, type JevRequest, } from "./model-preflight.js";
|
|
10
11
|
export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, type SteerDeps, type SteerResult, type SteerStatus, type SteeringGrant, type TmuxLike, } from "./steer.js";
|
|
11
12
|
export { VERSION } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ export { MiadiOAuthProvider, SCOPES, SCOPE_READ, SCOPE_CONTRIBUTE } from "./oaut
|
|
|
5
5
|
export { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, RECORD_KINDS, current, } from "./records.js";
|
|
6
6
|
export { TOOL_STATUS } from "./tools.js";
|
|
7
7
|
export { runBrief, readQueueState } from "./approve.js";
|
|
8
|
-
export { PROVIDERS, discoverProviders, resolveProvider, routerPreflight, } from "./providers.js";
|
|
9
|
-
export { missionStatus, resolveMissionSubject } from "./mission-status.js";
|
|
8
|
+
export { PROVIDERS, discoverProviders, jevConfigOf, resolveProvider, routerPreflight, } from "./providers.js";
|
|
9
|
+
export { missionStatus, primaryIssueOf, resolveMissionSubject } from "./mission-status.js";
|
|
10
|
+
export { modelPreflight, candidatesFor, buildJevRequest, decisionState, jevAdapter, reviewFloor, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, JEV_CONFIDENCE_FLOOR, } from "./model-preflight.js";
|
|
10
11
|
export { AGENT_COMMANDS, BUSY_PATTERNS, STEER_STATUSES, checkGrant, isBusy, readSteerArtifact, steerAttempts, steerSession, steeringGrantOf, tmuxAdapter, } from "./steer.js";
|
|
11
12
|
export { VERSION } from "./version.js";
|
package/dist/mission-status.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read-only mission status (jgwill/Miadi#893, Phase 1A): where
|
|
3
|
-
* stands, joined from the connector ledger, the factory queue, tmux,
|
|
4
|
-
* session view and GitHub — with every stage named by its evidence, so an
|
|
5
|
-
* pane or a
|
|
2
|
+
* Read-only mission status (jgwill/Miadi#893, Phase 1A; corrected by #913): where
|
|
3
|
+
* one mission stands, joined from the connector ledger, the factory queue, tmux,
|
|
4
|
+
* Miadi's session view and GitHub — with every stage named by its evidence, so an
|
|
5
|
+
* idle pane, a merge, a version string or a completed request is never read as
|
|
6
|
+
* release or closure.
|
|
7
|
+
*
|
|
8
|
+
* Mission identity (#913 A). A record's *primary* issue is the one it implements:
|
|
9
|
+
* `body.primary_issue` when the requester declared it (provenance `declared`),
|
|
10
|
+
* else, for records made before that field, the first owner/repo#n in its refs
|
|
11
|
+
* (provenance `first_issue_ref`, an inference). Every other issue it cites is
|
|
12
|
+
* *related*. An issue or session reference resolves to one mission only when one
|
|
13
|
+
* candidate holds it as primary; several are `ambiguous_mission`, and a reference
|
|
14
|
+
* cited only as related is `related_only`. Newest-first selection is gone.
|
|
15
|
+
* Only `exact` (a record id) and `declared_primary` authorize a steer, release or
|
|
16
|
+
* closure against the answer; an inference names its candidate and authorizes nothing.
|
|
6
17
|
*
|
|
7
18
|
* The caller (tools.ts) decides the viewer's tier first: without the write
|
|
8
19
|
* tier there are no operator details (live panes, session internals); the
|
|
@@ -19,20 +30,96 @@ export interface MissionSession {
|
|
|
19
30
|
/** Present when Miadi's session view was joined (write tier). */
|
|
20
31
|
miadi?: unknown;
|
|
21
32
|
}
|
|
33
|
+
/** What the connector could establish about a GitHub reference. `unverified` is never `not_found`: without access the connector cannot tell. */
|
|
34
|
+
export interface IssueLookup {
|
|
35
|
+
state: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
merged?: boolean;
|
|
38
|
+
/** Why the state is `unverified`: no_github_token, not_visible_to_token, token_rejected, forbidden_or_rate_limited, github_<status>, unreachable. */
|
|
39
|
+
access?: string;
|
|
40
|
+
}
|
|
41
|
+
/** A release receipt: what was released, where, and how production was checked, when. */
|
|
42
|
+
export interface ReleaseEvidence {
|
|
43
|
+
artifact: string;
|
|
44
|
+
target: string;
|
|
45
|
+
verified_at: string;
|
|
46
|
+
verification: string;
|
|
47
|
+
/** The record that carries it and who made it. */
|
|
48
|
+
record: string;
|
|
49
|
+
by: string;
|
|
50
|
+
/** The receipt was recorded by the person who approved the mission. */
|
|
51
|
+
by_approver: boolean;
|
|
52
|
+
/** The version the connector itself is running, when the receipt's target is the connector. */
|
|
53
|
+
live_version?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ClosureEvidence {
|
|
56
|
+
record: string;
|
|
57
|
+
ending: string;
|
|
58
|
+
session_id: string;
|
|
59
|
+
/** The ending names one of the mission's own sessions. False when the mission names none to compare. */
|
|
60
|
+
session_bound: boolean;
|
|
61
|
+
decided_by?: string;
|
|
62
|
+
decided_at?: string;
|
|
63
|
+
}
|
|
22
64
|
export interface MissionStageTruth {
|
|
23
65
|
approved: boolean;
|
|
66
|
+
/** The decision as taken: approve or approve_with_changes, the approver's instructions and what is still outstanding. */
|
|
67
|
+
approval: {
|
|
68
|
+
state: string;
|
|
69
|
+
choice?: string;
|
|
70
|
+
instructions?: string;
|
|
71
|
+
outstanding: string[];
|
|
72
|
+
};
|
|
24
73
|
queued: boolean;
|
|
25
74
|
delivered: boolean;
|
|
26
75
|
/** "evidence" only while a bound session is live; an idle pane proves nothing. */
|
|
27
76
|
running: "evidence" | "no_evidence";
|
|
28
|
-
|
|
29
|
-
|
|
77
|
+
/** The run_request or work_proposal finished. This is the request, not the mission. */
|
|
78
|
+
request_completed: boolean;
|
|
79
|
+
/** "unverified": GitHub could not be read, so no merge state is claimed either way. */
|
|
80
|
+
pr_merged: "merged" | "open_or_absent" | "no_pr" | "unverified";
|
|
81
|
+
/**
|
|
82
|
+
* verified: the connector itself confirmed it: the receipt targets miadi-connector and the running version
|
|
83
|
+
* is the one released. receipt_recorded: a complete, fresh receipt (artifact, target, production check, time)
|
|
84
|
+
* recorded by the named person; the connector cannot check it. stale: the check is older than the freshness
|
|
85
|
+
* window. live_mismatch: the connector runs another version than its receipt names. claimed_unverified: a
|
|
86
|
+
* version, a published flag, an incomplete or future-dated release object. no_evidence: nothing.
|
|
87
|
+
*/
|
|
88
|
+
released: "verified" | "receipt_recorded" | "stale" | "live_mismatch" | "claimed_unverified" | "no_evidence";
|
|
89
|
+
release_evidence?: ReleaseEvidence;
|
|
90
|
+
/** True only with an approved ending (exit or complete_then_exit) naming this mission in its `mission` field, for one of its sessions. A completed request is not a closure. */
|
|
30
91
|
closed: boolean;
|
|
92
|
+
closure_evidence?: ClosureEvidence;
|
|
93
|
+
}
|
|
94
|
+
export type MissionResolution = "exact" | "declared_primary" | "inferred_primary" | "ambiguous_mission" | "related_only";
|
|
95
|
+
export interface MissionCandidate {
|
|
96
|
+
id: string;
|
|
97
|
+
kind: string;
|
|
98
|
+
created_at: string;
|
|
99
|
+
/** primary: the reference is this record's primary issue. related: an issue it cites and does not implement. cited: a session it cites. */
|
|
100
|
+
relation: "primary" | "related" | "cited";
|
|
101
|
+
/** declared: body.primary_issue. first_issue_ref: inferred from the first issue in refs. cited_ref: the reference is in its refs. */
|
|
102
|
+
provenance: "declared" | "first_issue_ref" | "cited_ref";
|
|
103
|
+
primary_issue?: string;
|
|
104
|
+
}
|
|
105
|
+
export interface MissionIdentity {
|
|
106
|
+
resolution: MissionResolution;
|
|
107
|
+
/** A declared primary that another request also holds by inference: reported, not authorizing. */
|
|
108
|
+
contested?: boolean;
|
|
109
|
+
/** True only for exact and an uncontested declared_primary: a steer, release or closure may name this mission. */
|
|
110
|
+
authorizes_actions: boolean;
|
|
111
|
+
primary_issue?: {
|
|
112
|
+
ref: string;
|
|
113
|
+
provenance: "declared" | "first_issue_ref";
|
|
114
|
+
};
|
|
115
|
+
/** Every request citing the reference, newest first, with its relation. Empty for an exact id. */
|
|
116
|
+
candidates: MissionCandidate[];
|
|
31
117
|
}
|
|
32
118
|
export interface MissionStatus {
|
|
33
119
|
ref: string;
|
|
34
120
|
found: boolean;
|
|
35
121
|
via?: string;
|
|
122
|
+
identity?: MissionIdentity;
|
|
36
123
|
subject?: {
|
|
37
124
|
id: string;
|
|
38
125
|
kind: string;
|
|
@@ -48,9 +135,11 @@ export interface MissionStatus {
|
|
|
48
135
|
sessions?: MissionSession[];
|
|
49
136
|
issues?: {
|
|
50
137
|
ref: string;
|
|
138
|
+
relation: "primary" | "related";
|
|
51
139
|
state: string;
|
|
52
140
|
title?: string;
|
|
53
141
|
merged?: boolean;
|
|
142
|
+
access?: string;
|
|
54
143
|
}[];
|
|
55
144
|
steering?: {
|
|
56
145
|
grant: "none" | "active" | "expired_grant" | "exhausted";
|
|
@@ -72,16 +161,33 @@ export interface MissionStatusDeps {
|
|
|
72
161
|
paneBusy?: (session: string) => "busy" | "idle" | "unknown";
|
|
73
162
|
/** Miadi's session view by session:<uuid> (write tier only). */
|
|
74
163
|
sessionOf?: (id: string) => Promise<unknown>;
|
|
75
|
-
issueOf?: (owner: string, repo: string, n: string) => Promise<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
164
|
+
issueOf?: (owner: string, repo: string, n: string) => Promise<IssueLookup>;
|
|
165
|
+
/** The version a package runs at on this host, when the connector can know it (its own VERSION). */
|
|
166
|
+
liveVersionOf?: (pkg: string) => string | undefined;
|
|
167
|
+
/** How old a production check may be and still count as verified. Default seven days. */
|
|
168
|
+
releaseFreshnessMs?: number;
|
|
80
169
|
now?: () => Date;
|
|
81
170
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
171
|
+
export declare const RELEASE_FRESHNESS_MS: number;
|
|
172
|
+
/** The issue a mission implements, and how that is known. */
|
|
173
|
+
export declare function primaryIssueOf(record: ConnectorRecord): {
|
|
174
|
+
ref: string;
|
|
175
|
+
provenance: "declared" | "first_issue_ref";
|
|
176
|
+
} | undefined;
|
|
177
|
+
export interface ResolvedSubject {
|
|
178
|
+
record?: ConnectorRecord;
|
|
85
179
|
via: string;
|
|
86
|
-
|
|
180
|
+
resolution: MissionResolution;
|
|
181
|
+
contested?: boolean;
|
|
182
|
+
candidates: MissionCandidate[];
|
|
183
|
+
}
|
|
184
|
+
/** Whether a resolved reference may be acted on: an exact id, or a declared primary nobody else holds. */
|
|
185
|
+
export declare function authorizesActions(found: Pick<ResolvedSubject, "resolution" | "contested">): boolean;
|
|
186
|
+
/**
|
|
187
|
+
* The record a mission reference resolves to, how it was found, and every other
|
|
188
|
+
* candidate. `record` is absent when the reference is ambiguous or only related.
|
|
189
|
+
* Expects the folded ledger (`current()`), so a superseding record never stands
|
|
190
|
+
* as a mission of its own.
|
|
191
|
+
*/
|
|
192
|
+
export declare function resolveMissionSubject(ref: string, records: ConnectorRecord[]): ResolvedSubject | null;
|
|
87
193
|
export declare function missionStatus(ref: string, records: ConnectorRecord[], deps: MissionStatusDeps): Promise<MissionStatus>;
|