@miadi/connector 0.1.18 → 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 +10 -2
- package/dist/approve.d.ts +62 -9
- package/dist/approve.js +368 -33
- package/dist/config.d.ts +12 -0
- package/dist/config.js +11 -0
- package/dist/decisions.d.ts +4 -2
- package/dist/decisions.js +54 -18
- 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.d.ts +3 -1
- package/dist/request-state.js +8 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +6 -2
- package/dist/store.d.ts +2 -0
- package/dist/store.js +18 -1
- package/dist/tools.js +204 -30
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -16,17 +16,22 @@ import { reviewIdOf, videoIdOf } from "./reviews.js";
|
|
|
16
16
|
import { SCOPE_APPROVE, SCOPE_CONTRIBUTE } from "./oauth.js";
|
|
17
17
|
import { CIRCLES, CIRCLE_IDS, CLAIM_CATEGORIES, DOMAINS, HANDOFF_STATES, current, newRecordId, } from "./records.js";
|
|
18
18
|
import { VERSION } from "./version.js";
|
|
19
|
-
import { AlreadyDecided, DECISIONS, KIND_LABELS, StaleVersion, consequenceOf, decide, isApprovable, isBeingDecided, readQueueState, requestProjection } from "./approve.js";
|
|
19
|
+
import { AlreadyDecided, DECISIONS, DecisionConflict, KIND_LABELS, ProviderChoiceRequired, STILL_RUNNING, StaleVersion, approvalStateOf, consequenceOf, decide, decisionWaitOf, isApprovable, isBeingDecided, logDecision, readQueueState, requestProjection, settleWithin, } from "./approve.js";
|
|
20
20
|
import { RevisionRefused, requestHistory, revisionRecord, supersedeRecords, versionOf } from "./revisions.js";
|
|
21
21
|
import { DECISIONS_CARD_HTML, DECISIONS_CARD_URI, DECISION_TOKENS_META } from "./decisions.js";
|
|
22
22
|
import { discoverProviders, jevConfigOf, resolveProvider, routerPreflight, PROVIDERS } from "./providers.js";
|
|
23
23
|
import { authorizesActions, missionStatus, primaryIssueOf, resolveMissionSubject } from "./mission-status.js";
|
|
24
|
+
import { authorizationReceipt, authorizeProvider, CHOICE_PROVIDERS, isPersonId, needsPersonChoice, providerPolicyOf } from "./provider-policy.js";
|
|
24
25
|
import { candidatesFor, CRITICALITIES, jevAdapter, modelPreflight, PREFLIGHT_CONTRACT, PREFLIGHT_VERSION, TASK_TYPES, } from "./model-preflight.js";
|
|
25
26
|
import { checkGrant, isBusy, steerAttempts, steeringGrantOf, steerSession } from "./steer.js";
|
|
26
27
|
import { factoryLedgerPath, readFactoryReceipts, receiptsForRecord, recordsForSession } from "./factory-receipt.js";
|
|
27
28
|
import { missionResult, redactResult } from "./mission-result.js";
|
|
28
29
|
import { factoryDir as defaultFactoryDir } from "./queue-state.js";
|
|
29
30
|
import { join } from "node:path";
|
|
31
|
+
import { readingScope, canonicalVideo, suppliedDialogue } from "./reading/contract.js";
|
|
32
|
+
import { workflowId } from "./reading/workflow.js";
|
|
33
|
+
import { readingTask } from "./reading/adapters.js";
|
|
34
|
+
import { canReadReading, readingOwner } from "./reading/privacy.js";
|
|
30
35
|
import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
|
|
31
36
|
import { ResearchUploads, UploadRefusal } from "./uploads.js";
|
|
32
37
|
import { DEFAULT_PAGE_BYTES, FOUNDATIONS_PERMISSION, FileRefusal, MAX_TEXT_BYTES, MIN_PAGE_BYTES, TEXT_TYPES, checkRelativePath, episodeNumberOf, extensionOf, fileRef, holdsFoundationRead, isFoundationDoc, pageOf, parseFileRef, } from "./episode-files.js";
|
|
@@ -179,7 +184,14 @@ export function registerTools(server, ctx) {
|
|
|
179
184
|
const redact = (r) => {
|
|
180
185
|
if (!viewer || viewer.api_tier === "write" || r.actor.person_id === viewer.id)
|
|
181
186
|
return r;
|
|
187
|
+
if (r.body.reading && r.kind === "observation") {
|
|
188
|
+
const stage = r.body.reading;
|
|
189
|
+
return { ...r, refs: [], evidence_refs: [], body: { reading: { workflow_id: stage.workflow_id, stage: stage.stage, status: stage.status, sha256: stage.sha256, visibility: "source_owner_or_factory_operator" } } };
|
|
190
|
+
}
|
|
182
191
|
const body = { ...r.body };
|
|
192
|
+
// Grants contain source rights and locators; only the owner and operators inspect them.
|
|
193
|
+
if (body.reading)
|
|
194
|
+
body.reading = { visibility: "source_owner_or_factory_operator" };
|
|
183
195
|
// A steer's binding, pane, target and artifact path name the seat as the session does (#922 R2).
|
|
184
196
|
for (const k of ["session", "cwd", "session_id", "binding", "pane"])
|
|
185
197
|
delete body[k];
|
|
@@ -222,7 +234,47 @@ export function registerTools(server, ctx) {
|
|
|
222
234
|
};
|
|
223
235
|
/** Every record, folded; for logic. What a tool returns goes through `shown`. */
|
|
224
236
|
const all = () => current(ledger.all());
|
|
225
|
-
const records = () =>
|
|
237
|
+
const records = () => {
|
|
238
|
+
const raw = all();
|
|
239
|
+
// Completion results can contain exact drafts too. Hide their parent mission as
|
|
240
|
+
// well, so missionStatus/get_run_result cannot rehydrate it from trusted records.
|
|
241
|
+
return raw.filter(r => !viewer || canReadReading(r, raw, viewer)).map(r => {
|
|
242
|
+
// The source owner can read a factory-authored result for their private mission.
|
|
243
|
+
if (readingOwner(r, raw) === viewer?.id)
|
|
244
|
+
return r;
|
|
245
|
+
return redact(r);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
/** Which provider a run may launch with and who authorized it, as every surface shows it (jgwill/Miadi#926). */
|
|
249
|
+
const providerAuthorizationOf = (r) => r && r.kind === "run_request" ? authorizeProvider(r, ledger.all(), { requireFresh: r.approval_state === "awaiting_approval" }) : undefined;
|
|
250
|
+
const authorizationOut = (a) => ({
|
|
251
|
+
...authorizationReceipt(a),
|
|
252
|
+
explanation: a.explanation,
|
|
253
|
+
...(a.not_binding ? { not_binding: a.not_binding } : {}),
|
|
254
|
+
});
|
|
255
|
+
/** A mission's status with the authorization of its provider, and a note when the provider that ran was never authorized. */
|
|
256
|
+
const withProviderAuthorization = (status) => {
|
|
257
|
+
const subject = status?.subject?.id ? all().find((r) => r.id === status.subject.id) : undefined;
|
|
258
|
+
const a = providerAuthorizationOf(subject);
|
|
259
|
+
if (!a)
|
|
260
|
+
return status;
|
|
261
|
+
// An approval records the authorization it was given; a run approved before jgwill/Miadi#926 recorded none.
|
|
262
|
+
const given = subject?.completion_receipt?.provider_authorization;
|
|
263
|
+
const actual = typeof status.provider?.actual === "string" ? status.provider.actual : null;
|
|
264
|
+
const unauthorizedLaunch = Boolean(actual && needsPersonChoice(actual) && !a.authorized && !given?.authorized);
|
|
265
|
+
return {
|
|
266
|
+
...status,
|
|
267
|
+
provider_authorization: { ...authorizationOut(a), ...(given ? { at_approval: given } : {}), ...(unauthorizedLaunch ? { launched_without_authorization: true } : {}) },
|
|
268
|
+
...(unauthorizedLaunch
|
|
269
|
+
? {
|
|
270
|
+
notes: [
|
|
271
|
+
...(Array.isArray(status.notes) ? status.notes : []),
|
|
272
|
+
`${actual} ran for this mission without ${a.requester.name}'s own choice for it on record (jgwill/Miadi#926). Nothing was stopped or re-run because of it.`,
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
: {}),
|
|
276
|
+
};
|
|
277
|
+
};
|
|
226
278
|
const uploads = new ResearchUploads(join(config.stateDir, "uploads"));
|
|
227
279
|
const touching = (ref) => records().filter((r) => r.refs.includes(ref) || r.evidence_refs.includes(ref) || r.id === ref);
|
|
228
280
|
/** The factory runner's delivery receipts, read fresh: operator data, for the write tier only. */
|
|
@@ -248,6 +300,13 @@ export function registerTools(server, ctx) {
|
|
|
248
300
|
service: "@miadi/connector",
|
|
249
301
|
version: VERSION,
|
|
250
302
|
contract: "jgwill/Miadi#815 Connector v2, jgwill/Miadi#816 Review Bridge",
|
|
303
|
+
reading_workflow: {
|
|
304
|
+
contract: "transcript-first-v1",
|
|
305
|
+
provider_runtime: process.env.MIADI_READING_ADAPTER ? "host_configured_not_verified" : "not_configured",
|
|
306
|
+
transcript_store: "private connector ledger source-part receipts before generation",
|
|
307
|
+
import: "blocked_until_authenticated_sha256_cas_v1_handshake",
|
|
308
|
+
publication: "separate_exact_candidate_approval",
|
|
309
|
+
},
|
|
251
310
|
signed_in_as: { person_id: person.id, name: person.name, role: person.role ?? null, miadi_api_tier: person.api_tier, via: auth.clientId },
|
|
252
311
|
scopes: auth.scopes,
|
|
253
312
|
can_contribute: auth.scopes.includes(SCOPE_CONTRIBUTE),
|
|
@@ -732,7 +791,7 @@ export function registerTools(server, ctx) {
|
|
|
732
791
|
}, wrap(async ({ ref }, extra) => {
|
|
733
792
|
const { person } = who(extra);
|
|
734
793
|
const writeTier = person.api_tier === "write";
|
|
735
|
-
return missionStatus(ref.trim(), records(), {
|
|
794
|
+
return withProviderAuthorization(await missionStatus(ref.trim(), records(), {
|
|
736
795
|
projectOf: (r) => requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
|
|
737
796
|
queueOf: (r) => (r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined),
|
|
738
797
|
// Without a lister the host cannot say which sessions live: unknown, never "absent" (jgwill/Miadi#909).
|
|
@@ -788,11 +847,11 @@ export function registerTools(server, ctx) {
|
|
|
788
847
|
return { state: i.state, title: i.title };
|
|
789
848
|
},
|
|
790
849
|
liveVersionOf: (pkg) => (pkg === "@miadi/connector" ? VERSION : undefined),
|
|
791
|
-
});
|
|
850
|
+
}));
|
|
792
851
|
}));
|
|
793
852
|
server.registerTool("provider_capabilities", {
|
|
794
853
|
title: "Which agent providers this host can launch",
|
|
795
|
-
description: "The provider reality of the connector host (jgwill/Miadi#893): which agent CLIs are installed, their versions, whether sign-in evidence exists, the preference order (
|
|
854
|
+
description: "The provider reality of the connector host (jgwill/Miadi#893): which agent CLIs are installed, their versions, whether sign-in evidence exists, the preference order (Pi, Copilot by default; Codex and Claude Code only when a request names them), the rule that a named Codex runs only on the requester's own choice for that mission or their own policy (jgwill/Miadi#926), your own provider policy, and the read-only Jev routing preflight. No token is read or shown. Needs Miadi's write tier: this is host truth.",
|
|
796
855
|
inputSchema: {},
|
|
797
856
|
annotations: READ,
|
|
798
857
|
}, wrap(async (_args, extra) => {
|
|
@@ -801,9 +860,12 @@ export function registerTools(server, ctx) {
|
|
|
801
860
|
return {
|
|
802
861
|
preference_order: PROVIDERS.map((p) => p.id),
|
|
803
862
|
explicit_only: PROVIDERS.filter((p) => !p.selectable_by_default).map((p) => p.id),
|
|
863
|
+
needs_person_choice: [...CHOICE_PROVIDERS],
|
|
864
|
+
your_policy: providerPolicyOf(ledger.all(), person.id),
|
|
865
|
+
policy_page: `${config.publicUrl}/providers`,
|
|
804
866
|
providers: discoverProviders(ctx.providersProbe),
|
|
805
867
|
router: await routerPreflight(process.env, ctx.approval?.fetchImpl ?? fetch),
|
|
806
|
-
note: "A provider name in a task's text selects nothing; request_authorized_run's provider field is verified against this list, and an unavailable provider leaves the mission unstarted.",
|
|
868
|
+
note: "A provider name in a task's text selects nothing; request_authorized_run's provider field is verified against this list, and an unavailable provider leaves the mission unstarted. A request may name Codex, but it runs only when its requester chooses Codex for that mission on the approval page, or their own policy on the providers page says so; a generic approval, the assistant's provider field, the host's order or Jev never choose it.",
|
|
807
869
|
};
|
|
808
870
|
}));
|
|
809
871
|
server.registerTool("mission_model_preflight", {
|
|
@@ -842,7 +904,8 @@ export function registerTools(server, ctx) {
|
|
|
842
904
|
const body = record.body;
|
|
843
905
|
const provider = (typeof body.resolved_provider === "string" ? body.resolved_provider : typeof body.provider === "string" ? body.provider : null);
|
|
844
906
|
const basis = record.approval_state === "approved" ? "approved" : "requested";
|
|
845
|
-
const
|
|
907
|
+
const authorization = providerAuthorizationOf(record);
|
|
908
|
+
const candidates = candidatesFor(discoverProviders(ctx.providersProbe), provider, ctx.modelOf, basis, authorization);
|
|
846
909
|
const jevConfig = jevConfigOf(process.env);
|
|
847
910
|
const jev = a.consult_jev ? (ctx.jevAdapter ?? (jevConfig ? jevAdapter(jevConfig, ctx.approval?.fetchImpl ?? fetch) : undefined)) : undefined;
|
|
848
911
|
const configured = Boolean(jevConfig);
|
|
@@ -859,6 +922,7 @@ export function registerTools(server, ctx) {
|
|
|
859
922
|
task: a.task_type,
|
|
860
923
|
criticality: a.criticality,
|
|
861
924
|
candidates,
|
|
925
|
+
...(authorization ? { provider_authorization: authorizationOut(authorization) } : {}),
|
|
862
926
|
...(a.launch ? { launch: a.launch } : {}),
|
|
863
927
|
...(jev ? { adapter: jev } : { routerWhenAbsent: configured ? "not_requested" : "router_unconfigured" }),
|
|
864
928
|
});
|
|
@@ -1064,6 +1128,17 @@ export function registerTools(server, ctx) {
|
|
|
1064
1128
|
const video = videoIdOf(a.video);
|
|
1065
1129
|
if (!video)
|
|
1066
1130
|
throw new ToolRefusal("invalid_ref", "Give a YouTube URL or an 11-character video id.");
|
|
1131
|
+
const prior = all().find(r => r.kind === "source_intent" && r.actor.person_id === person.id && r.idempotency_key === `${video}:${a.purpose}`);
|
|
1132
|
+
if (prior && prior.body.readiness !== a.readiness) {
|
|
1133
|
+
// Release/withdrawal is an explicit source-owner contribution, not processing authority.
|
|
1134
|
+
const transition = {
|
|
1135
|
+
id: newRecordId("source_intent"), kind: "source_intent", created_at: new Date().toISOString(), actor: actorOf(person, auth), supersedes: prior.id,
|
|
1136
|
+
refs: prior.refs, evidence_refs: [], state: a.readiness === "released_for_review" ? "ready" : a.readiness === "withheld" ? "blocked" : "pending",
|
|
1137
|
+
body: { ...prior.body, readiness: a.readiness, source_readiness_transition: true },
|
|
1138
|
+
};
|
|
1139
|
+
ledger.appendDurable(transition);
|
|
1140
|
+
return all().find(r => r.id === prior.id);
|
|
1141
|
+
}
|
|
1067
1142
|
return append("source_intent", person, auth, {
|
|
1068
1143
|
circle: "continuity",
|
|
1069
1144
|
refs: [...a.refs, `https://youtu.be/${video}`],
|
|
@@ -1281,6 +1356,55 @@ export function registerTools(server, ctx) {
|
|
|
1281
1356
|
};
|
|
1282
1357
|
}));
|
|
1283
1358
|
// ── requests: recorded, approved by a person on the approval page, then run ──
|
|
1359
|
+
server.registerTool("request_screenwalk_reading", {
|
|
1360
|
+
title: "Release a source for a bounded research reading",
|
|
1361
|
+
description: "One approved run archives the released source transcript before generating V1, an academic survey and a labelled reflective reading. It stops at the exact candidate's editorial gate. No paid fallback, public release, Deep Diver or Chronicle is included. A host runtime adapter must be configured; unavailable providers are reported as blockers.",
|
|
1362
|
+
inputSchema: { scope: readingScope, companion: suppliedDialogue.optional(), ...base },
|
|
1363
|
+
annotations: WRITE,
|
|
1364
|
+
}, wrap(async (a, extra) => {
|
|
1365
|
+
const { person, auth } = who(extra);
|
|
1366
|
+
requireContribute(auth);
|
|
1367
|
+
const scope = readingScope.parse(a.scope);
|
|
1368
|
+
const source = all().find(r => r.id === scope.source_intent);
|
|
1369
|
+
if (!source || source.kind !== "source_intent" || source.actor.person_id !== person.id || source.state !== "ready" || source.body.readiness !== "released_for_review")
|
|
1370
|
+
throw new ToolRefusal("source_not_released", "The source owner must explicitly register this source as released_for_review before requesting processing.");
|
|
1371
|
+
const video = canonicalVideo(String(source.body.video_id));
|
|
1372
|
+
const id = workflowId(person.id, video, scope.revision_of);
|
|
1373
|
+
if (scope.revision_of) {
|
|
1374
|
+
const prior = ledger.all().find(r => r.kind === "observation" && r.body.reading?.workflow_id === scope.revision_of && r.body.reading?.stage === "registration");
|
|
1375
|
+
const priorRequest = all().find(r => r.kind === "run_request" && r.body.workflow_id === scope.revision_of && r.actor.person_id === person.id && r.approval_state === "approved" && r.body.reading?.source_intent === scope.source_intent);
|
|
1376
|
+
if ((!prior && !priorRequest) || (prior && (prior.actor.person_id !== person.id || prior.body.reading?.video_id !== video)))
|
|
1377
|
+
throw new ToolRefusal("invalid_revision", "A revision must name your prior workflow for the same video.");
|
|
1378
|
+
}
|
|
1379
|
+
const existing = all().find(r => r.kind === "run_request" && r.actor.person_id === person.id && r.body.workflow_id === id);
|
|
1380
|
+
if (existing)
|
|
1381
|
+
return { ...redact(existing), workflow_id: id, approval_url: approvalUrl(existing.id), replayed: true, note: "This video's bounded request already exists; read or resume it. The new scope was not substituted." };
|
|
1382
|
+
const resolution = resolveProvider(undefined, discoverProviders(ctx.providersProbe));
|
|
1383
|
+
if (resolution.state !== "ok")
|
|
1384
|
+
return { state: resolution.state, note: "No runtime provider was verified; nothing was queued." };
|
|
1385
|
+
const r = append("run_request", person, auth, {
|
|
1386
|
+
circle: "engineering", refs: [source.id, `https://youtu.be/${video}`], intention: a.intention,
|
|
1387
|
+
idempotency_key: id, approval_state: "awaiting_approval", state: "pending",
|
|
1388
|
+
body: { task: readingTask(scope), reading: scope, ...(a.companion ? { reading_companion: a.companion } : {}), session: `reading-${id.slice(8)}`, workflow_id: id, resolved_provider: resolution.provider.id },
|
|
1389
|
+
});
|
|
1390
|
+
return { ...r, workflow_id: id, approval_url: approvalUrl(r.id), gates: ["editorial_public_import", "deep_diver", "chronicle", "cultural_reuse"] };
|
|
1391
|
+
}));
|
|
1392
|
+
server.registerTool("request_reading_import", {
|
|
1393
|
+
title: "Approve the exact staged reading for public import",
|
|
1394
|
+
description: "Request a separate editorial decision for one candidate hash and its exact base. Import is a public release; it refuses legacy unconditional writes. Does not authorize Deep Diver, Chronicle or further reuse.",
|
|
1395
|
+
inputSchema: { workflow_id: z.string().regex(/^reading:[a-f\d]{32}$/), sha256: z.string().regex(/^[a-f\d]{64}$/), expected_version: z.number().int().min(0), expected_sha256: z.string().regex(/^[a-f\d]{64}$/), public_release: z.literal(true), ...base },
|
|
1396
|
+
annotations: WRITE,
|
|
1397
|
+
}, wrap(async (a, extra) => {
|
|
1398
|
+
const { person, auth } = who(extra);
|
|
1399
|
+
requireContribute(auth);
|
|
1400
|
+
// Read from the existing ledger, without constructing a second workflow store.
|
|
1401
|
+
const candidates = ledger.all().filter(r => r.kind === "observation" && r.body.reading?.workflow_id === a.workflow_id && r.body.reading?.stage === "v3" && r.body.reading?.status === "completed");
|
|
1402
|
+
const candidate = candidates[0]?.body.reading?.artifact;
|
|
1403
|
+
if (!candidate || candidates[0].actor.person_id !== person.id || candidate.sha256 !== a.sha256 || candidate.expected_version !== a.expected_version || candidate.expected_sha256 !== a.expected_sha256)
|
|
1404
|
+
throw new ToolRefusal("candidate_mismatch", "Read this person's staged candidate and request its exact hash and base.");
|
|
1405
|
+
const r = append("run_request", person, auth, { circle: "story", refs: [a.workflow_id], intention: a.intention, idempotency_key: `${a.workflow_id}:import:${a.sha256}`, state: "pending", approval_state: "awaiting_approval", body: { task: "Run node packages/connector/scripts/reading.mjs import <this-run-request-id>. Only import the exact typed reading_import candidate after the service proves atomic CAS and durable idempotency; read it back exactly. Stop on any mismatch. No Deep Diver or Chronicle.", reading_import: { workflow_id: a.workflow_id, sha256: a.sha256, expected_version: a.expected_version, expected_sha256: a.expected_sha256, public_release: true }, reading_preview: { markdown: candidate.markdown, diffs: candidate.diffs, manifest: candidate.manifest } } });
|
|
1406
|
+
return { ...r, approval_url: approvalUrl(r.id), candidate: { sha256: candidate.sha256, bytes: candidate.bytes, diff: candidate.diffs, manifest: candidate.manifest } };
|
|
1407
|
+
}));
|
|
1284
1408
|
server.registerTool("request_authorized_run", {
|
|
1285
1409
|
title: "Request a bounded agent run",
|
|
1286
1410
|
description: "Ask for an agent session to do one bounded task. The request waits for a person with Miadi's write tier to approve it on the returned approval_url; once approved, a brief is queued for the factory intake, which opens or reaches the session within minutes. Read it back with get_run_result. This is for a new mission only: to change a request that still awaits a decision, call revise_pending_request with its id, so the person keeps one card to decide.",
|
|
@@ -1294,7 +1418,7 @@ export function registerTools(server, ctx) {
|
|
|
1294
1418
|
provider: z
|
|
1295
1419
|
.enum(["codex", "pi", "copilot", "claude"])
|
|
1296
1420
|
.optional()
|
|
1297
|
-
.describe("The provider the run should launch with, verified installed on the connector host. Claude Code runs only when named
|
|
1421
|
+
.describe("The provider the run should launch with, verified installed on the connector host. Name it only when the person asked for that provider for this mission. Claude Code runs only when named. Codex runs only when the person also chooses Codex for this mission on the approval page (or their own policy allows it): naming it here does not choose it for them (jgwill/Miadi#926). Unknown or unavailable leaves the mission unstarted, and nothing falls back to Codex."),
|
|
1298
1422
|
steering: z
|
|
1299
1423
|
.object({
|
|
1300
1424
|
max_attempts: z.number().int().min(1).max(20).describe("How many authorized follow-up steers this approval covers."),
|
|
@@ -1334,10 +1458,25 @@ export function registerTools(server, ctx) {
|
|
|
1334
1458
|
...(a.steering ? { steering: a.steering } : {}),
|
|
1335
1459
|
},
|
|
1336
1460
|
});
|
|
1461
|
+
const authorization = providerAuthorizationOf(all().find((x) => x.id === r.id) ?? r);
|
|
1337
1462
|
return {
|
|
1338
1463
|
...r,
|
|
1339
|
-
provider: {
|
|
1464
|
+
provider: {
|
|
1465
|
+
requested: a.provider ?? null,
|
|
1466
|
+
resolved: resolution.provider.id,
|
|
1467
|
+
explicit: resolution.explicit,
|
|
1468
|
+
version: resolution.provider.version,
|
|
1469
|
+
authorization: authorizationOut(authorization),
|
|
1470
|
+
},
|
|
1340
1471
|
approval_url: approvalUrl(r.id),
|
|
1472
|
+
...(authorization.authorized
|
|
1473
|
+
? {}
|
|
1474
|
+
: {
|
|
1475
|
+
// #926 review R4, RR5: a provider is revised to another one, never cleared; a service token cannot choose at all.
|
|
1476
|
+
next_action: isPersonId(person.id)
|
|
1477
|
+
? `Tell ${person.name} that a Codex run needs their own choice on the approval page (${approvalUrl(r.id)}); approving alone, on the page or a card, will not run it. Do not choose for them. If they did not ask for Codex for this mission, revise the request with another provider (pi, copilot or claude).`
|
|
1478
|
+
: `A request made with a service token cannot run Codex: only a person chooses it, for a request of their own. Revise it with another provider (pi, copilot or claude).`,
|
|
1479
|
+
}),
|
|
1341
1480
|
};
|
|
1342
1481
|
}));
|
|
1343
1482
|
// ── one mission, many versions, one decision (jgwill/Miadi#919) ────────────
|
|
@@ -1435,10 +1574,13 @@ export function registerTools(server, ctx) {
|
|
|
1435
1574
|
throw error;
|
|
1436
1575
|
}
|
|
1437
1576
|
ledger.append(record);
|
|
1577
|
+
// A revision moves the version: a provider choice made for an earlier version no longer binds (jgwill/Miadi#926).
|
|
1578
|
+
const revisedAuthorization = providerAuthorizationOf(all().find((x) => x.id === (record.supersedes ?? record.id)));
|
|
1438
1579
|
return {
|
|
1439
1580
|
...revisionOut(record.supersedes ? { ...record, id: record.supersedes } : record),
|
|
1440
1581
|
revised: record.revision,
|
|
1441
1582
|
...(provider ? { provider } : {}),
|
|
1583
|
+
...(revisedAuthorization ? { provider_authorization: authorizationOut(revisedAuthorization) } : {}),
|
|
1442
1584
|
note: `One request, one approval: its page and card now show version ${record.revision.version}. Earlier versions stay readable with get_request_history.`,
|
|
1443
1585
|
};
|
|
1444
1586
|
}));
|
|
@@ -1878,8 +2020,9 @@ export function registerTools(server, ctx) {
|
|
|
1878
2020
|
created_at: r.created_at,
|
|
1879
2021
|
circle: r.circle,
|
|
1880
2022
|
intention: r.intention,
|
|
1881
|
-
consequence: consequenceOf(r),
|
|
2023
|
+
consequence: consequenceOf(r, providerAuthorizationOf(r)),
|
|
1882
2024
|
detail: detailOf(r),
|
|
2025
|
+
...(r.kind === "run_request" ? { provider_authorization: authorizationOut(providerAuthorizationOf(r)) } : {}),
|
|
1883
2026
|
approval_url: approvalUrl(r.id),
|
|
1884
2027
|
projection: requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
|
|
1885
2028
|
}));
|
|
@@ -1932,42 +2075,71 @@ export function registerTools(server, ctx) {
|
|
|
1932
2075
|
const projectionNow = () => requestProjection(all().find((x) => x.id === r.id) ?? r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir });
|
|
1933
2076
|
const check = ctx.decisionTokens.verify(a.decision_token, r.id, person.id, auth.clientId, undefined, versionOf(r));
|
|
1934
2077
|
if (check === "stale") {
|
|
1935
|
-
throw new ToolRefusal("stale_version", `This card showed an earlier version; the request is now at version ${versionOf(r)}. Show the requests again and decide version ${versionOf(r)}.`, { projection: projectionNow() });
|
|
2078
|
+
throw new ToolRefusal("stale_version", `This card showed an earlier version; the request is now at version ${versionOf(r)}. Show the requests again and decide version ${versionOf(r)}.`, { projection: projectionNow(), approval_url: approvalUrl(r.id) });
|
|
1936
2079
|
}
|
|
1937
|
-
if (check
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2080
|
+
if (check === "expired") {
|
|
2081
|
+
// The request does not expire: the card's token did. Its approval page still decides it, and a fresh card does too (#924).
|
|
2082
|
+
throw new ToolRefusal("token_expired", "This card's decision token has expired; the request has not. Show the requests again for a fresh card, or decide on its approval page.", {
|
|
2083
|
+
projection: projectionNow(),
|
|
2084
|
+
approval_url: approvalUrl(r.id),
|
|
2085
|
+
});
|
|
1941
2086
|
}
|
|
2087
|
+
if (check !== "ok")
|
|
2088
|
+
throw new ToolRefusal("invalid_token", "This decision did not come from the person's card.");
|
|
1942
2089
|
const instructions = a.instructions?.trim() || undefined;
|
|
1943
|
-
if ((a.decision === "approve_with_changes" || a.decision === "request_revision") && !instructions) {
|
|
2090
|
+
if (r.approval_state === "awaiting_approval" && !isBeingDecided(r.id) && (a.decision === "approve_with_changes" || a.decision === "request_revision") && !instructions) {
|
|
1944
2091
|
throw new ToolRefusal("instructions_required", a.decision === "request_revision" ? "Say what to revise." : "Write the changes to apply.");
|
|
1945
2092
|
}
|
|
1946
2093
|
const client = ctx.oauthState.read().clients[auth.clientId];
|
|
2094
|
+
const via = `card:${String(client?.client_name ?? auth.clientId).slice(0, 50)}`;
|
|
1947
2095
|
let decided;
|
|
1948
2096
|
try {
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
2097
|
+
const running = decide(r, person, a.decision, ctx.approval, { instructions, via, expectedVersion: versionOf(r) });
|
|
2098
|
+
// A host gives up on a call after about a minute and may send it again; a review create can take longer.
|
|
2099
|
+
// Past the bound the answer is the decision, recorded and running, and the card follows it to its outcome (#924).
|
|
2100
|
+
const settled = await settleWithin(running, decisionWaitOf(ctx.approval));
|
|
2101
|
+
if (settled === STILL_RUNNING) {
|
|
2102
|
+
logDecision(ctx.approval, { phase: "answered_running", record: r.id, via, choice: a.decision });
|
|
2103
|
+
const decision = approvalStateOf(a.decision);
|
|
2104
|
+
const out = {
|
|
2105
|
+
record_id: r.id,
|
|
2106
|
+
version: versionOf(r),
|
|
2107
|
+
decision,
|
|
2108
|
+
state: "in_progress",
|
|
2109
|
+
recording: true,
|
|
2110
|
+
summary: `${decision === "approved" ? "Approved" : "Decided"} by ${person.name}, and still running. The card shows the outcome when it finishes; get_run_result reads it.`,
|
|
2111
|
+
projection: projectionNow(),
|
|
2112
|
+
};
|
|
2113
|
+
return { structuredContent: out, content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
2114
|
+
}
|
|
2115
|
+
decided = settled;
|
|
1954
2116
|
}
|
|
1955
2117
|
catch (error) {
|
|
2118
|
+
// No projection here: the card shows this message, and the request still awaits a decision (jgwill/Miadi#926).
|
|
2119
|
+
if (error instanceof ProviderChoiceRequired) {
|
|
2120
|
+
throw new ToolRefusal("provider_choice_required", `${error.message} Open its approval page: ${approvalUrl(r.id)}`, {
|
|
2121
|
+
approval_url: approvalUrl(r.id),
|
|
2122
|
+
provider_authorization: authorizationOut(error.authorization),
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
1956
2125
|
if (error instanceof StaleVersion)
|
|
1957
|
-
throw new ToolRefusal("stale_version", error.message, { projection: projectionNow() });
|
|
1958
|
-
// A decision recorded a moment earlier, by another client, is reported with where it stands.
|
|
2126
|
+
throw new ToolRefusal("stale_version", error.message, { projection: projectionNow(), approval_url: approvalUrl(r.id) });
|
|
2127
|
+
// A different decision recorded a moment earlier, by another click or client, is reported with where it stands.
|
|
2128
|
+
if (error instanceof DecisionConflict)
|
|
2129
|
+
throw new ToolRefusal("decision_conflict", error.message, { projection: projectionNow() });
|
|
1959
2130
|
if (error instanceof AlreadyDecided)
|
|
1960
2131
|
throw new ToolRefusal("already_decided", error.message, { projection: projectionNow() });
|
|
1961
2132
|
throw error;
|
|
1962
2133
|
}
|
|
1963
|
-
const { decision, state, receipt } = decided;
|
|
2134
|
+
const { decision, state, receipt, replayed } = decided;
|
|
1964
2135
|
const projection = projectionNow();
|
|
1965
|
-
const summary =
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
2136
|
+
const summary = (replayed ? "Already recorded; nothing ran again. " : "") +
|
|
2137
|
+
(decision === "approved"
|
|
2138
|
+
? `Approved by ${receipt.decided_by?.name ?? person.name}. ${receipt.outcome === "failed" ? "Running it failed; see the receipt." : `Outcome: ${String(receipt.outcome ?? "recorded").replace(/_/g, " ")}.`}`
|
|
2139
|
+
: decision === "revision_requested"
|
|
2140
|
+
? `Sent back for revision by ${receipt.decided_by?.name ?? person.name}.`
|
|
2141
|
+
: `Declined by ${receipt.decided_by?.name ?? person.name}.`);
|
|
2142
|
+
const out = { record_id: r.id, version: versionOf(r), decision, state, summary, receipt, projection, ...(replayed ? { replayed: true } : {}) };
|
|
1971
2143
|
return { structuredContent: out, content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
1972
2144
|
}
|
|
1973
2145
|
catch (error) {
|
|
@@ -2064,6 +2236,8 @@ export const TOOL_STATUS = {
|
|
|
2064
2236
|
supersede_pending_requests: "live, contribute (retires named earlier requests still waiting and joins them as earlier versions of the one that stands)",
|
|
2065
2237
|
request_session_steer: "live, contribute, write tier (runs inside an approved run's steering grant, else one scoped human decision; get_capabilities.steering says whether the transport works now)",
|
|
2066
2238
|
request_review_operation: "live, contribute, needs_approval",
|
|
2239
|
+
request_screenwalk_reading: "live, contribute, needs_approval (host provider adapter required; stops at editorial decision)",
|
|
2240
|
+
request_reading_import: "live, contribute, needs_approval (exact public import; atomic service contract required)",
|
|
2067
2241
|
request_deep_diver: "live, contribute, needs_approval",
|
|
2068
2242
|
link_review_to_episode: "live, contribute, needs_approval",
|
|
2069
2243
|
save_research: "live, contribute (the whole report, in parts when long; saved on the inquiry shelf with its issue, pushed; private by default)",
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.20";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.20";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/connector",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "The Miadi connector: one remote MCP service through which a person signed in with their Miadi person token, from ChatGPT or any MCP client, reads sessions, reviews, episodes and circles and records observations, inquiries, handoffs and proposals with their name on each record.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Miadi",
|