@miadi/connector 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -5
- package/dist/approve.d.ts +16 -2
- package/dist/approve.js +185 -16
- package/dist/bin.js +3 -0
- package/dist/mission-status.d.ts +13 -1
- package/dist/mission-status.js +13 -5
- package/dist/model-preflight.d.ts +9 -2
- package/dist/model-preflight.js +23 -1
- package/dist/provider-policy.d.ts +94 -0
- package/dist/provider-policy.js +150 -0
- package/dist/providers.d.ts +7 -3
- package/dist/providers.js +13 -7
- package/dist/queue-state.js +12 -1
- package/dist/reading/adapters.d.ts +18 -0
- package/dist/reading/adapters.js +90 -0
- package/dist/reading/contract.d.ts +153 -0
- package/dist/reading/contract.js +79 -0
- package/dist/reading/markdown.d.ts +47 -0
- package/dist/reading/markdown.js +52 -0
- package/dist/reading/privacy.d.ts +7 -0
- package/dist/reading/privacy.js +14 -0
- package/dist/reading/workflow.d.ts +93 -0
- package/dist/reading/workflow.js +325 -0
- package/dist/records.d.ts +1 -1
- package/dist/records.js +6 -0
- package/dist/request-state.js +9 -1
- package/dist/research-digest.d.ts +30 -0
- package/dist/research-digest.js +104 -0
- package/dist/research-shelf.d.ts +173 -0
- package/dist/research-shelf.js +297 -0
- package/dist/runtime-steer.d.ts +182 -0
- package/dist/runtime-steer.js +543 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +4 -12
- package/dist/steer.d.ts +21 -7
- package/dist/steer.js +115 -10
- package/dist/store.d.ts +2 -0
- package/dist/store.js +18 -1
- package/dist/tools.d.ts +29 -1
- package/dist/tools.js +435 -50
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -1
package/dist/tools.js
CHANGED
|
@@ -16,17 +16,24 @@ 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, DecisionConflict, KIND_LABELS, STILL_RUNNING, StaleVersion, approvalStateOf, consequenceOf, decide, decisionWaitOf, isApprovable, isBeingDecided, logDecision, readQueueState, requestProjection, settleWithin, } 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
|
-
import {
|
|
26
|
+
import { ShelfRefusal, listShelfArtefacts, readShelfArtefact } from "./research-shelf.js";
|
|
27
|
+
import { CLAUDE_ADAPTER, DELIVERY_STATES, readSteerDelivery, revokeInInbox, settlementOf } from "./runtime-steer.js";
|
|
28
|
+
import { checkGrant, isBusy, steerAttempts, steeringGrantOf, steeringRevoked, steerSession } from "./steer.js";
|
|
26
29
|
import { factoryLedgerPath, readFactoryReceipts, receiptsForRecord, recordsForSession } from "./factory-receipt.js";
|
|
27
30
|
import { missionResult, redactResult } from "./mission-result.js";
|
|
28
31
|
import { factoryDir as defaultFactoryDir } from "./queue-state.js";
|
|
29
32
|
import { join } from "node:path";
|
|
33
|
+
import { readingScope, canonicalVideo, suppliedDialogue } from "./reading/contract.js";
|
|
34
|
+
import { workflowId } from "./reading/workflow.js";
|
|
35
|
+
import { readingTask } from "./reading/adapters.js";
|
|
36
|
+
import { canReadReading, readingOwner } from "./reading/privacy.js";
|
|
30
37
|
import { artefactVisibility, readArtefactWeave, saveResearchArtefact } from "@miadi/inquiry-weave";
|
|
31
38
|
import { ResearchUploads, UploadRefusal } from "./uploads.js";
|
|
32
39
|
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";
|
|
@@ -145,6 +152,27 @@ export function githubGetter(fetchImpl = fetch, token = process.env.GITHUB_TOKEN
|
|
|
145
152
|
};
|
|
146
153
|
}
|
|
147
154
|
/** Whether the steering transport works now: registered is not operational (jgwill/Miadi#909). */
|
|
155
|
+
/**
|
|
156
|
+
* The provider interface as a client reads it (jgwill/Miadi#930). A correction goes to the session's
|
|
157
|
+
* own runtime, and busy is normal. Only the adapter named here exists. No other provider is assumed
|
|
158
|
+
* to behave like Claude Code.
|
|
159
|
+
*/
|
|
160
|
+
/** A steer being sent now, by its record: a retry arriving during it waits for this answer and sends nothing (jgwill/Miadi#930). */
|
|
161
|
+
const steersInFlight = new Map();
|
|
162
|
+
export const STEERING_CONTRACT = {
|
|
163
|
+
busy_is_normal: true,
|
|
164
|
+
connector: "Admits the steer, resolves mission → session → runtime, hands it to that session's inbox and reports what the runtime accepted. It does not watch tools or wait for idle.",
|
|
165
|
+
runtime: "The agent's own lifecycle presents it to its model at the next supported continuation in the same turn.",
|
|
166
|
+
delivery_states: DELIVERY_STATES,
|
|
167
|
+
provider_adapters: {
|
|
168
|
+
claude: { route: "claude_hook_inbox", continuation: "PostToolUse, PostToolUseFailure, then Stop if no tool follows", needs: `${CLAUDE_ADAPTER}, loaded when the session started` },
|
|
169
|
+
codex: { route: "unsupported" },
|
|
170
|
+
copilot: { route: "unsupported" },
|
|
171
|
+
pi: { route: "unsupported" },
|
|
172
|
+
gemini: { route: "unsupported" },
|
|
173
|
+
},
|
|
174
|
+
idle_seat: "An idle seat has no continuation coming, so its steer is typed into its bound pane once the prompt reads empty.",
|
|
175
|
+
};
|
|
148
176
|
export function steeringTransport(tmux) {
|
|
149
177
|
if (!tmux)
|
|
150
178
|
return { tool: "registered", transport: "unavailable", reason: "The connector host was started without a steering surface." };
|
|
@@ -179,7 +207,14 @@ export function registerTools(server, ctx) {
|
|
|
179
207
|
const redact = (r) => {
|
|
180
208
|
if (!viewer || viewer.api_tier === "write" || r.actor.person_id === viewer.id)
|
|
181
209
|
return r;
|
|
210
|
+
if (r.body.reading && r.kind === "observation") {
|
|
211
|
+
const stage = r.body.reading;
|
|
212
|
+
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" } } };
|
|
213
|
+
}
|
|
182
214
|
const body = { ...r.body };
|
|
215
|
+
// Grants contain source rights and locators; only the owner and operators inspect them.
|
|
216
|
+
if (body.reading)
|
|
217
|
+
body.reading = { visibility: "source_owner_or_factory_operator" };
|
|
183
218
|
// A steer's binding, pane, target and artifact path name the seat as the session does (#922 R2).
|
|
184
219
|
for (const k of ["session", "cwd", "session_id", "binding", "pane"])
|
|
185
220
|
delete body[k];
|
|
@@ -220,9 +255,81 @@ export function registerTools(server, ctx) {
|
|
|
220
255
|
...fields,
|
|
221
256
|
});
|
|
222
257
|
};
|
|
258
|
+
/**
|
|
259
|
+
* A forwarded steer's record moves when the runtime's evidence moves: done once the model has it,
|
|
260
|
+
* stopped once it never will. The ledger then agrees with its delivery on every surface that reads it
|
|
261
|
+
* (jgwill/Miadi#930). This only reads the inbox and the transcript, and appends when something changed.
|
|
262
|
+
*/
|
|
263
|
+
const settleSteers = () => {
|
|
264
|
+
if (!ctx.steeringRuntime)
|
|
265
|
+
return;
|
|
266
|
+
for (const r of current(ledger.all())) {
|
|
267
|
+
if (r.kind !== "session_steer_request")
|
|
268
|
+
continue;
|
|
269
|
+
const settled = settlementOf(ctx.steeringRuntime, r);
|
|
270
|
+
if (!settled)
|
|
271
|
+
continue;
|
|
272
|
+
ledger.append({
|
|
273
|
+
id: newRecordId(r.kind),
|
|
274
|
+
kind: r.kind,
|
|
275
|
+
created_at: new Date().toISOString(),
|
|
276
|
+
actor: r.actor,
|
|
277
|
+
refs: r.refs,
|
|
278
|
+
evidence_refs: r.evidence_refs,
|
|
279
|
+
supersedes: r.id,
|
|
280
|
+
...(r.approval_state ? { approval_state: r.approval_state } : {}),
|
|
281
|
+
state: settled.state,
|
|
282
|
+
completion_receipt: { ...r.completion_receipt, outcome: settled.outcome, delivery_state: settled.outcome, settled_at: new Date().toISOString(), ...(settled.at ? { delivered_at: settled.at } : {}), message: settled.message },
|
|
283
|
+
body: r.body,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
};
|
|
223
287
|
/** Every record, folded; for logic. What a tool returns goes through `shown`. */
|
|
224
|
-
const all = () =>
|
|
225
|
-
|
|
288
|
+
const all = () => {
|
|
289
|
+
settleSteers();
|
|
290
|
+
return current(ledger.all());
|
|
291
|
+
};
|
|
292
|
+
const records = () => {
|
|
293
|
+
const raw = all();
|
|
294
|
+
// Completion results can contain exact drafts too. Hide their parent mission as
|
|
295
|
+
// well, so missionStatus/get_run_result cannot rehydrate it from trusted records.
|
|
296
|
+
return raw.filter(r => !viewer || canReadReading(r, raw, viewer)).map(r => {
|
|
297
|
+
// The source owner can read a factory-authored result for their private mission.
|
|
298
|
+
if (readingOwner(r, raw) === viewer?.id)
|
|
299
|
+
return r;
|
|
300
|
+
return redact(r);
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
/** Which provider a run may launch with and who authorized it, as every surface shows it (jgwill/Miadi#926). */
|
|
304
|
+
const providerAuthorizationOf = (r) => r && r.kind === "run_request" ? authorizeProvider(r, ledger.all(), { requireFresh: r.approval_state === "awaiting_approval" }) : undefined;
|
|
305
|
+
const authorizationOut = (a) => ({
|
|
306
|
+
...authorizationReceipt(a),
|
|
307
|
+
explanation: a.explanation,
|
|
308
|
+
...(a.not_binding ? { not_binding: a.not_binding } : {}),
|
|
309
|
+
});
|
|
310
|
+
/** A mission's status with the authorization of its provider, and a note when the provider that ran was never authorized. */
|
|
311
|
+
const withProviderAuthorization = (status) => {
|
|
312
|
+
const subject = status?.subject?.id ? all().find((r) => r.id === status.subject.id) : undefined;
|
|
313
|
+
const a = providerAuthorizationOf(subject);
|
|
314
|
+
if (!a)
|
|
315
|
+
return status;
|
|
316
|
+
// An approval records the authorization it was given; a run approved before jgwill/Miadi#926 recorded none.
|
|
317
|
+
const given = subject?.completion_receipt?.provider_authorization;
|
|
318
|
+
const actual = typeof status.provider?.actual === "string" ? status.provider.actual : null;
|
|
319
|
+
const unauthorizedLaunch = Boolean(actual && needsPersonChoice(actual) && !a.authorized && !given?.authorized);
|
|
320
|
+
return {
|
|
321
|
+
...status,
|
|
322
|
+
provider_authorization: { ...authorizationOut(a), ...(given ? { at_approval: given } : {}), ...(unauthorizedLaunch ? { launched_without_authorization: true } : {}) },
|
|
323
|
+
...(unauthorizedLaunch
|
|
324
|
+
? {
|
|
325
|
+
notes: [
|
|
326
|
+
...(Array.isArray(status.notes) ? status.notes : []),
|
|
327
|
+
`${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.`,
|
|
328
|
+
],
|
|
329
|
+
}
|
|
330
|
+
: {}),
|
|
331
|
+
};
|
|
332
|
+
};
|
|
226
333
|
const uploads = new ResearchUploads(join(config.stateDir, "uploads"));
|
|
227
334
|
const touching = (ref) => records().filter((r) => r.refs.includes(ref) || r.evidence_refs.includes(ref) || r.id === ref);
|
|
228
335
|
/** The factory runner's delivery receipts, read fresh: operator data, for the write tier only. */
|
|
@@ -248,6 +355,13 @@ export function registerTools(server, ctx) {
|
|
|
248
355
|
service: "@miadi/connector",
|
|
249
356
|
version: VERSION,
|
|
250
357
|
contract: "jgwill/Miadi#815 Connector v2, jgwill/Miadi#816 Review Bridge",
|
|
358
|
+
reading_workflow: {
|
|
359
|
+
contract: "transcript-first-v1",
|
|
360
|
+
provider_runtime: process.env.MIADI_READING_ADAPTER ? "host_configured_not_verified" : "not_configured",
|
|
361
|
+
transcript_store: "private connector ledger source-part receipts before generation",
|
|
362
|
+
import: "blocked_until_authenticated_sha256_cas_v1_handshake",
|
|
363
|
+
publication: "separate_exact_candidate_approval",
|
|
364
|
+
},
|
|
251
365
|
signed_in_as: { person_id: person.id, name: person.name, role: person.role ?? null, miadi_api_tier: person.api_tier, via: auth.clientId },
|
|
252
366
|
scopes: auth.scopes,
|
|
253
367
|
can_contribute: auth.scopes.includes(SCOPE_CONTRIBUTE),
|
|
@@ -258,7 +372,7 @@ export function registerTools(server, ctx) {
|
|
|
258
372
|
},
|
|
259
373
|
tools: TOOL_STATUS,
|
|
260
374
|
// Registered is not operational: whether a steer can reach a pane on this host now (jgwill/Miadi#909).
|
|
261
|
-
steering: steeringTransport(ctx.tmux),
|
|
375
|
+
steering: { ...steeringTransport(ctx.tmux), runtime_route: ctx.steeringRuntime ? "configured" : "not_configured", contract: STEERING_CONTRACT },
|
|
262
376
|
foundation_read: {
|
|
263
377
|
permission: FOUNDATIONS_PERMISSION,
|
|
264
378
|
held: foundations,
|
|
@@ -710,6 +824,8 @@ export function registerTools(server, ctx) {
|
|
|
710
824
|
...(versionOf(r) > 1 ? { history: "Earlier versions: get_request_history." } : {}),
|
|
711
825
|
approval_url: r.approval_state === "awaiting_approval" ? approvalUrl(r.id) : undefined,
|
|
712
826
|
queue: r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined,
|
|
827
|
+
// Where a steer stands, joined to its id: operator detail, as mission_status has it (jgwill/Miadi#930).
|
|
828
|
+
...(r.kind === "session_steer_request" && ctx.steeringRuntime && person.api_tier === "write" ? { delivery: readSteerDelivery(ctx.steeringRuntime, all().find((x) => x.id === r.id) ?? r) ?? undefined } : {}),
|
|
713
829
|
// Where the request stands, in the words every surface uses (jgwill/Miadi#889).
|
|
714
830
|
projection: requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
|
|
715
831
|
...(r.approval_state === "revision_requested"
|
|
@@ -732,9 +848,17 @@ export function registerTools(server, ctx) {
|
|
|
732
848
|
}, wrap(async ({ ref }, extra) => {
|
|
733
849
|
const { person } = who(extra);
|
|
734
850
|
const writeTier = person.api_tier === "write";
|
|
735
|
-
return missionStatus(ref.trim(), records(), {
|
|
851
|
+
return withProviderAuthorization(await missionStatus(ref.trim(), records(), {
|
|
736
852
|
projectOf: (r) => requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
|
|
737
853
|
queueOf: (r) => (r.kind === "run_request" || r.kind === "work_proposal" ? readQueueState(r, ctx.factoryDir) : undefined),
|
|
854
|
+
...(writeTier && ctx.steeringRuntime
|
|
855
|
+
? {
|
|
856
|
+
steerDeliveryOf: (r) => {
|
|
857
|
+
const stored = all().find((x) => x.id === r.id) ?? r;
|
|
858
|
+
return readSteerDelivery(ctx.steeringRuntime, stored);
|
|
859
|
+
},
|
|
860
|
+
}
|
|
861
|
+
: {}),
|
|
738
862
|
// Without a lister the host cannot say which sessions live: unknown, never "absent" (jgwill/Miadi#909).
|
|
739
863
|
tmuxSessions: writeTier ? (ctx.tmuxSessions ?? (ctx.tmux?.listSessions ? () => ctx.tmux.listSessions() : undefined)) : undefined,
|
|
740
864
|
paneBusy: writeTier && ctx.tmux
|
|
@@ -788,11 +912,11 @@ export function registerTools(server, ctx) {
|
|
|
788
912
|
return { state: i.state, title: i.title };
|
|
789
913
|
},
|
|
790
914
|
liveVersionOf: (pkg) => (pkg === "@miadi/connector" ? VERSION : undefined),
|
|
791
|
-
});
|
|
915
|
+
}));
|
|
792
916
|
}));
|
|
793
917
|
server.registerTool("provider_capabilities", {
|
|
794
918
|
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 (
|
|
919
|
+
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
920
|
inputSchema: {},
|
|
797
921
|
annotations: READ,
|
|
798
922
|
}, wrap(async (_args, extra) => {
|
|
@@ -801,9 +925,12 @@ export function registerTools(server, ctx) {
|
|
|
801
925
|
return {
|
|
802
926
|
preference_order: PROVIDERS.map((p) => p.id),
|
|
803
927
|
explicit_only: PROVIDERS.filter((p) => !p.selectable_by_default).map((p) => p.id),
|
|
928
|
+
needs_person_choice: [...CHOICE_PROVIDERS],
|
|
929
|
+
your_policy: providerPolicyOf(ledger.all(), person.id),
|
|
930
|
+
policy_page: `${config.publicUrl}/providers`,
|
|
804
931
|
providers: discoverProviders(ctx.providersProbe),
|
|
805
932
|
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.",
|
|
933
|
+
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
934
|
};
|
|
808
935
|
}));
|
|
809
936
|
server.registerTool("mission_model_preflight", {
|
|
@@ -842,7 +969,8 @@ export function registerTools(server, ctx) {
|
|
|
842
969
|
const body = record.body;
|
|
843
970
|
const provider = (typeof body.resolved_provider === "string" ? body.resolved_provider : typeof body.provider === "string" ? body.provider : null);
|
|
844
971
|
const basis = record.approval_state === "approved" ? "approved" : "requested";
|
|
845
|
-
const
|
|
972
|
+
const authorization = providerAuthorizationOf(record);
|
|
973
|
+
const candidates = candidatesFor(discoverProviders(ctx.providersProbe), provider, ctx.modelOf, basis, authorization);
|
|
846
974
|
const jevConfig = jevConfigOf(process.env);
|
|
847
975
|
const jev = a.consult_jev ? (ctx.jevAdapter ?? (jevConfig ? jevAdapter(jevConfig, ctx.approval?.fetchImpl ?? fetch) : undefined)) : undefined;
|
|
848
976
|
const configured = Boolean(jevConfig);
|
|
@@ -859,6 +987,7 @@ export function registerTools(server, ctx) {
|
|
|
859
987
|
task: a.task_type,
|
|
860
988
|
criticality: a.criticality,
|
|
861
989
|
candidates,
|
|
990
|
+
...(authorization ? { provider_authorization: authorizationOut(authorization) } : {}),
|
|
862
991
|
...(a.launch ? { launch: a.launch } : {}),
|
|
863
992
|
...(jev ? { adapter: jev } : { routerWhenAbsent: configured ? "not_requested" : "router_unconfigured" }),
|
|
864
993
|
});
|
|
@@ -1064,6 +1193,17 @@ export function registerTools(server, ctx) {
|
|
|
1064
1193
|
const video = videoIdOf(a.video);
|
|
1065
1194
|
if (!video)
|
|
1066
1195
|
throw new ToolRefusal("invalid_ref", "Give a YouTube URL or an 11-character video id.");
|
|
1196
|
+
const prior = all().find(r => r.kind === "source_intent" && r.actor.person_id === person.id && r.idempotency_key === `${video}:${a.purpose}`);
|
|
1197
|
+
if (prior && prior.body.readiness !== a.readiness) {
|
|
1198
|
+
// Release/withdrawal is an explicit source-owner contribution, not processing authority.
|
|
1199
|
+
const transition = {
|
|
1200
|
+
id: newRecordId("source_intent"), kind: "source_intent", created_at: new Date().toISOString(), actor: actorOf(person, auth), supersedes: prior.id,
|
|
1201
|
+
refs: prior.refs, evidence_refs: [], state: a.readiness === "released_for_review" ? "ready" : a.readiness === "withheld" ? "blocked" : "pending",
|
|
1202
|
+
body: { ...prior.body, readiness: a.readiness, source_readiness_transition: true },
|
|
1203
|
+
};
|
|
1204
|
+
ledger.appendDurable(transition);
|
|
1205
|
+
return all().find(r => r.id === prior.id);
|
|
1206
|
+
}
|
|
1067
1207
|
return append("source_intent", person, auth, {
|
|
1068
1208
|
circle: "continuity",
|
|
1069
1209
|
refs: [...a.refs, `https://youtu.be/${video}`],
|
|
@@ -1281,6 +1421,55 @@ export function registerTools(server, ctx) {
|
|
|
1281
1421
|
};
|
|
1282
1422
|
}));
|
|
1283
1423
|
// ── requests: recorded, approved by a person on the approval page, then run ──
|
|
1424
|
+
server.registerTool("request_screenwalk_reading", {
|
|
1425
|
+
title: "Release a source for a bounded research reading",
|
|
1426
|
+
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.",
|
|
1427
|
+
inputSchema: { scope: readingScope, companion: suppliedDialogue.optional(), ...base },
|
|
1428
|
+
annotations: WRITE,
|
|
1429
|
+
}, wrap(async (a, extra) => {
|
|
1430
|
+
const { person, auth } = who(extra);
|
|
1431
|
+
requireContribute(auth);
|
|
1432
|
+
const scope = readingScope.parse(a.scope);
|
|
1433
|
+
const source = all().find(r => r.id === scope.source_intent);
|
|
1434
|
+
if (!source || source.kind !== "source_intent" || source.actor.person_id !== person.id || source.state !== "ready" || source.body.readiness !== "released_for_review")
|
|
1435
|
+
throw new ToolRefusal("source_not_released", "The source owner must explicitly register this source as released_for_review before requesting processing.");
|
|
1436
|
+
const video = canonicalVideo(String(source.body.video_id));
|
|
1437
|
+
const id = workflowId(person.id, video, scope.revision_of);
|
|
1438
|
+
if (scope.revision_of) {
|
|
1439
|
+
const prior = ledger.all().find(r => r.kind === "observation" && r.body.reading?.workflow_id === scope.revision_of && r.body.reading?.stage === "registration");
|
|
1440
|
+
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);
|
|
1441
|
+
if ((!prior && !priorRequest) || (prior && (prior.actor.person_id !== person.id || prior.body.reading?.video_id !== video)))
|
|
1442
|
+
throw new ToolRefusal("invalid_revision", "A revision must name your prior workflow for the same video.");
|
|
1443
|
+
}
|
|
1444
|
+
const existing = all().find(r => r.kind === "run_request" && r.actor.person_id === person.id && r.body.workflow_id === id);
|
|
1445
|
+
if (existing)
|
|
1446
|
+
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." };
|
|
1447
|
+
const resolution = resolveProvider(undefined, discoverProviders(ctx.providersProbe));
|
|
1448
|
+
if (resolution.state !== "ok")
|
|
1449
|
+
return { state: resolution.state, note: "No runtime provider was verified; nothing was queued." };
|
|
1450
|
+
const r = append("run_request", person, auth, {
|
|
1451
|
+
circle: "engineering", refs: [source.id, `https://youtu.be/${video}`], intention: a.intention,
|
|
1452
|
+
idempotency_key: id, approval_state: "awaiting_approval", state: "pending",
|
|
1453
|
+
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 },
|
|
1454
|
+
});
|
|
1455
|
+
return { ...r, workflow_id: id, approval_url: approvalUrl(r.id), gates: ["editorial_public_import", "deep_diver", "chronicle", "cultural_reuse"] };
|
|
1456
|
+
}));
|
|
1457
|
+
server.registerTool("request_reading_import", {
|
|
1458
|
+
title: "Approve the exact staged reading for public import",
|
|
1459
|
+
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.",
|
|
1460
|
+
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 },
|
|
1461
|
+
annotations: WRITE,
|
|
1462
|
+
}, wrap(async (a, extra) => {
|
|
1463
|
+
const { person, auth } = who(extra);
|
|
1464
|
+
requireContribute(auth);
|
|
1465
|
+
// Read from the existing ledger, without constructing a second workflow store.
|
|
1466
|
+
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");
|
|
1467
|
+
const candidate = candidates[0]?.body.reading?.artifact;
|
|
1468
|
+
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)
|
|
1469
|
+
throw new ToolRefusal("candidate_mismatch", "Read this person's staged candidate and request its exact hash and base.");
|
|
1470
|
+
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 } } });
|
|
1471
|
+
return { ...r, approval_url: approvalUrl(r.id), candidate: { sha256: candidate.sha256, bytes: candidate.bytes, diff: candidate.diffs, manifest: candidate.manifest } };
|
|
1472
|
+
}));
|
|
1284
1473
|
server.registerTool("request_authorized_run", {
|
|
1285
1474
|
title: "Request a bounded agent run",
|
|
1286
1475
|
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 +1483,7 @@ export function registerTools(server, ctx) {
|
|
|
1294
1483
|
provider: z
|
|
1295
1484
|
.enum(["codex", "pi", "copilot", "claude"])
|
|
1296
1485
|
.optional()
|
|
1297
|
-
.describe("The provider the run should launch with, verified installed on the connector host. Claude Code runs only when named
|
|
1486
|
+
.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
1487
|
steering: z
|
|
1299
1488
|
.object({
|
|
1300
1489
|
max_attempts: z.number().int().min(1).max(20).describe("How many authorized follow-up steers this approval covers."),
|
|
@@ -1334,10 +1523,25 @@ export function registerTools(server, ctx) {
|
|
|
1334
1523
|
...(a.steering ? { steering: a.steering } : {}),
|
|
1335
1524
|
},
|
|
1336
1525
|
});
|
|
1526
|
+
const authorization = providerAuthorizationOf(all().find((x) => x.id === r.id) ?? r);
|
|
1337
1527
|
return {
|
|
1338
1528
|
...r,
|
|
1339
|
-
provider: {
|
|
1529
|
+
provider: {
|
|
1530
|
+
requested: a.provider ?? null,
|
|
1531
|
+
resolved: resolution.provider.id,
|
|
1532
|
+
explicit: resolution.explicit,
|
|
1533
|
+
version: resolution.provider.version,
|
|
1534
|
+
authorization: authorizationOut(authorization),
|
|
1535
|
+
},
|
|
1340
1536
|
approval_url: approvalUrl(r.id),
|
|
1537
|
+
...(authorization.authorized
|
|
1538
|
+
? {}
|
|
1539
|
+
: {
|
|
1540
|
+
// #926 review R4, RR5: a provider is revised to another one, never cleared; a service token cannot choose at all.
|
|
1541
|
+
next_action: isPersonId(person.id)
|
|
1542
|
+
? `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).`
|
|
1543
|
+
: `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).`,
|
|
1544
|
+
}),
|
|
1341
1545
|
};
|
|
1342
1546
|
}));
|
|
1343
1547
|
// ── one mission, many versions, one decision (jgwill/Miadi#919) ────────────
|
|
@@ -1435,10 +1639,13 @@ export function registerTools(server, ctx) {
|
|
|
1435
1639
|
throw error;
|
|
1436
1640
|
}
|
|
1437
1641
|
ledger.append(record);
|
|
1642
|
+
// A revision moves the version: a provider choice made for an earlier version no longer binds (jgwill/Miadi#926).
|
|
1643
|
+
const revisedAuthorization = providerAuthorizationOf(all().find((x) => x.id === (record.supersedes ?? record.id)));
|
|
1438
1644
|
return {
|
|
1439
1645
|
...revisionOut(record.supersedes ? { ...record, id: record.supersedes } : record),
|
|
1440
1646
|
revised: record.revision,
|
|
1441
1647
|
...(provider ? { provider } : {}),
|
|
1648
|
+
...(revisedAuthorization ? { provider_authorization: authorizationOut(revisedAuthorization) } : {}),
|
|
1442
1649
|
note: `One request, one approval: its page and card now show version ${record.revision.version}. Earlier versions stay readable with get_request_history.`,
|
|
1443
1650
|
};
|
|
1444
1651
|
}));
|
|
@@ -1514,10 +1721,11 @@ export function registerTools(server, ctx) {
|
|
|
1514
1721
|
}));
|
|
1515
1722
|
server.registerTool("request_session_steer", {
|
|
1516
1723
|
title: "Send one authorized follow-up to a running agent",
|
|
1517
|
-
description: "
|
|
1724
|
+
description: "Send one correction to a running agent within an approved run's steering grant (jgwill/Miadi#893, #930). Busy is normal: send it while the agent works, once, and do not watch its tools, wait for idle or retry. The connector hands it to the bound session's own inbox, and the agent's runtime presents it to its model at its next continuation in the same turn. The answer's delivery says which state it reached, each with its source and time: forwarded_to_runtime and queued_at_runtime do not show the model read it; delivered_to_agent_context, acknowledged and behavior_adjusted are read from the session's transcript. A provider with no adapter, or a session started before the adapter, answers unsupported with the reason and uses no attempt. An idle seat has no continuation coming, so its steer is typed into the mission's bound pane, never whichever pane is active. When the run's grant does not cover this, the steer waits for one explicit scoped human decision on the returned approval_url. Read it later with get_run_result on the steer's id or with mission_status.",
|
|
1518
1725
|
inputSchema: {
|
|
1519
1726
|
run_request: z.string().min(1).describe("The approved run_request or work_proposal whose steering grant covers this steer."),
|
|
1520
|
-
instruction: z.string().min(1).max(4000).describe("The exact instruction
|
|
1727
|
+
instruction: z.string().min(1).max(4000).describe("The exact instruction for the agent. It is material within the approved mission's scope, not new authority."),
|
|
1728
|
+
expect_next_action: z.string().min(1).max(200).optional().describe("Words to look for in the agent's next tool inputs. When they appear after delivery, the state becomes behavior_adjusted; without this, whether the next actions follow the correction is a person's judgment."),
|
|
1521
1729
|
session: z.string().regex(/^[A-Za-z0-9._-]{3,80}$/).optional().describe("The session to steer; must be the one the grant binds."),
|
|
1522
1730
|
pane: z.string().regex(/^\d+\.\d+$/).optional().describe("The pane (window.pane); default the grant's pane, else the session's first pane. The active pane is never used."),
|
|
1523
1731
|
refs,
|
|
@@ -1541,15 +1749,36 @@ export function registerTools(server, ctx) {
|
|
|
1541
1749
|
}
|
|
1542
1750
|
const grant = steeringGrantOf(grantRecord);
|
|
1543
1751
|
const priorAttempts = steerAttempts(all(), grantRecord.id);
|
|
1544
|
-
const
|
|
1752
|
+
const deliveryOf = (rec) => (ctx.steeringRuntime ? readSteerDelivery(ctx.steeringRuntime, rec) ?? undefined : undefined);
|
|
1753
|
+
const wasSent = (rec) => {
|
|
1754
|
+
const receipt = rec.completion_receipt;
|
|
1755
|
+
return receipt?.typed === true || receipt?.forwarded === true;
|
|
1756
|
+
};
|
|
1757
|
+
const attemptsNow = (counted) => ({ used: steerAttempts(all(), grantRecord.id), ...(grant?.max_attempts !== undefined ? { max: grant.max_attempts } : {}), counted });
|
|
1758
|
+
// The same key after a client's retry or a dropped connection gets the first answer, whatever the grant says by now.
|
|
1759
|
+
if (a.idempotency_key) {
|
|
1760
|
+
const sameKey = all().find((x) => x.kind === "session_steer_request" && x.idempotency_key === a.idempotency_key && x.actor.person_id === person.id && x.refs.includes(grantRecord.id));
|
|
1761
|
+
if (sameKey && wasSent(sameKey))
|
|
1762
|
+
return { ...sameKey, delivery: deliveryOf(sameKey), attempts: attemptsNow(false) };
|
|
1763
|
+
if (sameKey && steersInFlight.has(sameKey.id)) {
|
|
1764
|
+
await steersInFlight.get(sameKey.id);
|
|
1765
|
+
const settled = all().find((x) => x.id === sameKey.id) ?? sameKey;
|
|
1766
|
+
return { ...settled, delivery: deliveryOf(settled), attempts: attemptsNow(false) };
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const revoked = steeringRevoked(all(), grantRecord.id);
|
|
1770
|
+
const covered = checkGrant(grant, priorAttempts, new Date(), revoked) === "ok";
|
|
1545
1771
|
// The pane the factory delivered this mission to: the steer goes there and nowhere else (jgwill/Miadi#909).
|
|
1546
1772
|
const bound = receiptsForRecord(receipts(), grantRecord.id)[0];
|
|
1547
|
-
const binding = bound
|
|
1773
|
+
const binding = bound
|
|
1774
|
+
? { session: bound.tmux_session, ...(bound.pane_id ? { pane_id: bound.pane_id } : {}), ...(bound.session_id ? { session_id: bound.session_id } : {}), ...(bound.provider ? { provider: bound.provider } : {}) }
|
|
1775
|
+
: undefined;
|
|
1548
1776
|
const body = {
|
|
1549
1777
|
instruction: a.instruction,
|
|
1550
1778
|
session: a.session ?? bound?.tmux_session ?? grantRecord.body.session,
|
|
1551
1779
|
pane: a.pane ?? grant?.pane,
|
|
1552
|
-
...(
|
|
1780
|
+
...(a.expect_next_action ? { expect_next_action: a.expect_next_action } : {}),
|
|
1781
|
+
...(binding ? { binding: { ...binding, by: "factory_receipt", delivered_at: bound.at } } : {}),
|
|
1553
1782
|
};
|
|
1554
1783
|
const fields = {
|
|
1555
1784
|
circle: grantRecord.circle,
|
|
@@ -1567,42 +1796,113 @@ export function registerTools(server, ctx) {
|
|
|
1567
1796
|
...r,
|
|
1568
1797
|
approval_url: approvalUrl(r.id),
|
|
1569
1798
|
state: "awaiting_approval",
|
|
1570
|
-
reason: !grant ? "no_grant" : checkGrant(grant, priorAttempts),
|
|
1799
|
+
reason: !grant ? "no_grant" : checkGrant(grant, priorAttempts, new Date(), revoked),
|
|
1571
1800
|
message: "The approved run's steering grant does not cover this steer; a person's decision is requested, once, for exactly this instruction.",
|
|
1572
1801
|
};
|
|
1573
1802
|
}
|
|
1574
1803
|
const r = append("session_steer_request", person, auth, { ...fields, approval_state: "not_required" });
|
|
1575
|
-
const
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1804
|
+
const sameWords = all().find((x) => x.kind === "session_steer_request" && x.id !== r.id && x.refs.includes(grantRecord.id) && x.body.instruction === a.instruction && wasSent(x));
|
|
1805
|
+
let finishSend = () => { };
|
|
1806
|
+
steersInFlight.set(r.id, new Promise((resolve) => (finishSend = resolve)));
|
|
1807
|
+
try {
|
|
1808
|
+
const result = await steerSession({ tmux: ctx.tmux, stateDir: config.stateDir, ...(ctx.steeringRuntime ? { runtime: ctx.steeringRuntime } : {}), ...(ctx.steerSettleMs !== undefined ? { settleMs: ctx.steerSettleMs } : {}) }, { record: r, grantRecord, priorAttempts, ...(binding ? { binding } : {}), ...(sameWords ? { sameInstructionSent: sameWords.id } : {}) });
|
|
1809
|
+
// A typed or forwarded steer is never "failed": failed is re-runnable, and that would send it twice (#922 R8).
|
|
1810
|
+
const delivered = result.typed || result.forwarded === true;
|
|
1811
|
+
ledger.append({
|
|
1812
|
+
id: newRecordId(r.kind),
|
|
1813
|
+
kind: r.kind,
|
|
1814
|
+
created_at: new Date().toISOString(),
|
|
1815
|
+
actor: r.actor,
|
|
1816
|
+
refs: r.refs,
|
|
1817
|
+
evidence_refs: [],
|
|
1818
|
+
supersedes: r.id,
|
|
1819
|
+
approval_state: "not_required",
|
|
1820
|
+
// Forwarded is under way: the runtime has not presented it yet, and delivery reads where it stands.
|
|
1821
|
+
state: result.forwarded ? "in_progress" : delivered ? "completed" : "blocked",
|
|
1822
|
+
completion_receipt: {
|
|
1823
|
+
decision: "covered_by_grant",
|
|
1824
|
+
decided_by: { person_id: person.id, name: person.name },
|
|
1825
|
+
decided_at: new Date().toISOString(),
|
|
1826
|
+
outcome: delivered ? result.status : "failed",
|
|
1827
|
+
steer_status: result.status,
|
|
1828
|
+
typed: result.typed,
|
|
1829
|
+
...(result.forwarded ? { forwarded: true } : {}),
|
|
1830
|
+
...(result.route ? { route: result.route } : {}),
|
|
1831
|
+
...(result.transcript_offset !== undefined ? { transcript_offset: result.transcript_offset } : {}),
|
|
1832
|
+
message: result.message,
|
|
1833
|
+
...(result.target ? { target: result.target } : {}),
|
|
1834
|
+
...(result.artifact ? { artifact: result.artifact } : {}),
|
|
1835
|
+
},
|
|
1836
|
+
body: r.body,
|
|
1837
|
+
});
|
|
1838
|
+
const stored = all().find((x) => x.id === r.id) ?? r;
|
|
1839
|
+
return {
|
|
1840
|
+
...(records().find((x) => x.id === r.id) ?? r),
|
|
1841
|
+
delivery: deliveryOf(stored),
|
|
1842
|
+
attempts: attemptsNow(delivered),
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
finally {
|
|
1846
|
+
finishSend();
|
|
1847
|
+
steersInFlight.delete(r.id);
|
|
1848
|
+
}
|
|
1849
|
+
}));
|
|
1850
|
+
server.registerTool("revoke_session_steering", {
|
|
1851
|
+
title: "Stop a run's steering, or one steer still waiting",
|
|
1852
|
+
description: "A person stops a correction before its agent reads it (jgwill/Miadi#930). Name the run to stop all further steers under its grant and everything still waiting in its session's inbox, or add steer to stop only that one. The agent's runtime checks again when it would consume it. The answer says exactly what was revoked and what the agent already consumed: a correction already read cannot be taken back.",
|
|
1853
|
+
inputSchema: {
|
|
1854
|
+
run_request: z.string().min(1).describe("The run_request or work_proposal the steering belongs to."),
|
|
1855
|
+
steer: z.string().min(1).optional().describe("One session_steer_request to stop; without it, the run's whole steering grant is revoked."),
|
|
1856
|
+
reason: z.string().min(1).max(1000),
|
|
1857
|
+
refs,
|
|
1858
|
+
...base,
|
|
1859
|
+
},
|
|
1860
|
+
annotations: WRITE,
|
|
1861
|
+
}, wrap(async (a, extra) => {
|
|
1862
|
+
const { person, auth } = who(extra);
|
|
1863
|
+
requireContribute(auth);
|
|
1864
|
+
requireTier(person, "write");
|
|
1865
|
+
const grantRecord = all().find((r) => r.id === a.run_request);
|
|
1866
|
+
if (!grantRecord || (grantRecord.kind !== "run_request" && grantRecord.kind !== "work_proposal")) {
|
|
1867
|
+
throw new ToolRefusal("invalid_ref", "Give the id of the run_request or work_proposal whose steering this stops.");
|
|
1868
|
+
}
|
|
1869
|
+
if (a.steer) {
|
|
1870
|
+
const steer = all().find((r) => r.id === a.steer);
|
|
1871
|
+
if (!steer || steer.kind !== "session_steer_request" || !steer.refs.includes(grantRecord.id))
|
|
1872
|
+
throw new ToolRefusal("invalid_ref", `${a.steer} is not a steer of ${grantRecord.id}.`);
|
|
1873
|
+
}
|
|
1874
|
+
const r = append("steering_revocation", person, auth, {
|
|
1875
|
+
circle: grantRecord.circle,
|
|
1876
|
+
refs: [grantRecord.id, ...(a.steer ? [a.steer] : []), ...a.refs],
|
|
1877
|
+
intention: a.intention,
|
|
1878
|
+
role: a.role,
|
|
1879
|
+
idempotency_key: a.idempotency_key,
|
|
1880
|
+
body: { reason: a.reason, ...(a.steer ? { steer: a.steer } : {}) },
|
|
1600
1881
|
});
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1882
|
+
// A steer still waiting for a person's decision is closed too, so approving it later cannot run it.
|
|
1883
|
+
if (a.steer) {
|
|
1884
|
+
const waiting = all().find((x) => x.id === a.steer);
|
|
1885
|
+
if (waiting?.approval_state === "awaiting_approval") {
|
|
1886
|
+
ledger.append({
|
|
1887
|
+
id: newRecordId(waiting.kind),
|
|
1888
|
+
kind: waiting.kind,
|
|
1889
|
+
created_at: new Date().toISOString(),
|
|
1890
|
+
actor: waiting.actor,
|
|
1891
|
+
refs: waiting.refs,
|
|
1892
|
+
evidence_refs: [],
|
|
1893
|
+
supersedes: waiting.id,
|
|
1894
|
+
approval_state: "declined",
|
|
1895
|
+
state: "withdrawn",
|
|
1896
|
+
completion_receipt: { decision: "revoked", decided_by: { person_id: person.id, name: person.name }, decided_at: new Date().toISOString(), outcome: "revoked", steer_status: "blocked", typed: false, message: a.reason },
|
|
1897
|
+
body: waiting.body,
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
const sessions = [...new Set(receiptsForRecord(receipts(), grantRecord.id).map((x) => x.session_id).filter((x) => Boolean(x)))];
|
|
1902
|
+
const inboxes = ctx.steeringRuntime
|
|
1903
|
+
? sessions.map((session_id) => ({ session_id, ...revokeInInbox(ctx.steeringRuntime, session_id, { grant: grantRecord.id, ...(a.steer ? { steer: a.steer } : {}) }, a.reason) }))
|
|
1904
|
+
: [];
|
|
1905
|
+
return { ...r, inboxes, note: a.steer ? "This steer was stopped where it had not been consumed." : "No further steer runs under this grant without a person's decision on that exact steer." };
|
|
1606
1906
|
}));
|
|
1607
1907
|
server.registerTool("request_review_operation", {
|
|
1608
1908
|
title: "Request a review operation",
|
|
@@ -1775,6 +2075,78 @@ export function registerTools(server, ctx) {
|
|
|
1775
2075
|
research_url: `${config.miadiPublicUrl}/research`,
|
|
1776
2076
|
};
|
|
1777
2077
|
}));
|
|
2078
|
+
server.registerTool("list_research_artifacts", {
|
|
2079
|
+
title: "List what is on the inquiry shelf",
|
|
2080
|
+
description: "Reports and folders on Miadi's private inquiry shelf, as you may read them: each one's title, opening, words, sections, sources, files, date, owner, visibility, issue, episodes and, for a saved report, its sha256. All of it is computed from the files, never summarised. scope mine lists what you own; scope all, for Miadi's write tier, lists every folder. Search with query, narrow with kind, order with sort, and page with offset and next_offset. Read one with read_research_artifact. Read only: nothing is changed.",
|
|
2081
|
+
inputSchema: {
|
|
2082
|
+
scope: z.enum(["mine", "all"]).default("mine"),
|
|
2083
|
+
query: z.string().max(200).optional().describe("Words to find in a title, an opening, a heading or a folder's name."),
|
|
2084
|
+
kind: z.enum(["report", "folder", "notes"]).optional().describe("report: saved through a door. folder: holds a document. notes: agent instructions or unreadable files only."),
|
|
2085
|
+
sort: z.enum(["newest", "oldest", "longest", "title"]).default("newest"),
|
|
2086
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
2087
|
+
offset: z.number().int().min(0).default(0).describe("0, or next_offset from the previous page."),
|
|
2088
|
+
},
|
|
2089
|
+
annotations: READ,
|
|
2090
|
+
}, wrap(async (a, extra) => {
|
|
2091
|
+
const { person } = who(extra);
|
|
2092
|
+
if (!config.inquiryRoot)
|
|
2093
|
+
throw new ToolRefusal("unavailable", "This connector host has no inquiry shelf configured.");
|
|
2094
|
+
try {
|
|
2095
|
+
const { scope, query, kind, sort, limit, offset } = a;
|
|
2096
|
+
return await listShelfArtefacts(config.inquiryRoot, person, {
|
|
2097
|
+
...(scope ? { scope } : {}),
|
|
2098
|
+
...(query ? { query } : {}),
|
|
2099
|
+
...(kind ? { kind } : {}),
|
|
2100
|
+
...(sort ? { sort } : {}),
|
|
2101
|
+
...(limit !== undefined ? { limit } : {}),
|
|
2102
|
+
...(offset !== undefined ? { offset } : {}),
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
catch (error) {
|
|
2106
|
+
if (error instanceof ShelfRefusal)
|
|
2107
|
+
throw new ToolRefusal(error.code, error.message);
|
|
2108
|
+
throw error;
|
|
2109
|
+
}
|
|
2110
|
+
}));
|
|
2111
|
+
server.registerTool("read_research_artifact", {
|
|
2112
|
+
title: "Read a document on the inquiry shelf",
|
|
2113
|
+
description: "Read back what is on Miadi's private inquiry shelf, as you: a folder's owner, visibility, its issue, its episodes, the commit that holds it, whether anything on disk differs from that commit, and every file with its bytes and sha256. With the folder's report, or the file you name, you also get its text, in pages when long: pass page.next back; joined, the pages hash to sha256. integrity says whether a saved report's bytes are still the bytes saved. Only the folder's owner or a person with Miadi's write tier can read it. Text files only; hidden files and links are never served. Read only: nothing is changed.",
|
|
2114
|
+
inputSchema: {
|
|
2115
|
+
artefact: z.string().min(1).max(300).describe("The shelf folder's name, inquiry-artefact:<name> or the research_artifact record id save_research returned."),
|
|
2116
|
+
file: z.string().min(1).max(500).optional().describe("A path in the folder, as files lists it. Default: its saved report, if it has one."),
|
|
2117
|
+
offset: z.number().int().min(0).optional().describe("0, or page.next.offset from the previous page."),
|
|
2118
|
+
revision: z.string().regex(/^[0-9a-f]{64}$/).optional().describe("page.next.revision from the previous page: refuses with changed if the file moved since."),
|
|
2119
|
+
max_bytes: z.number().int().min(MIN_PAGE_BYTES).max(MAX_TEXT_BYTES).optional().describe(`Page size in bytes (default ${DEFAULT_PAGE_BYTES}).`),
|
|
2120
|
+
},
|
|
2121
|
+
annotations: READ,
|
|
2122
|
+
}, wrap(async (a, extra) => {
|
|
2123
|
+
const { person } = who(extra);
|
|
2124
|
+
if (!config.inquiryRoot)
|
|
2125
|
+
throw new ToolRefusal("unavailable", "This connector host has no inquiry shelf configured.");
|
|
2126
|
+
const ref = a.artefact.trim();
|
|
2127
|
+
let name = ref.replace(/^inquiry-artefact:/, "");
|
|
2128
|
+
if (ref.startsWith("research_artifact:")) {
|
|
2129
|
+
const held = all().find((r) => r.id === ref && r.kind === "research_artifact");
|
|
2130
|
+
const artefact = held?.body?.artefact;
|
|
2131
|
+
if (typeof artefact !== "string")
|
|
2132
|
+
throw new ToolRefusal("not_found", `No research_artifact ${ref}.`);
|
|
2133
|
+
name = artefact;
|
|
2134
|
+
}
|
|
2135
|
+
try {
|
|
2136
|
+
const { file, offset, revision, max_bytes: maxBytes } = a;
|
|
2137
|
+
return await readShelfArtefact(config.inquiryRoot, name, person, {
|
|
2138
|
+
...(file !== undefined ? { file } : {}),
|
|
2139
|
+
...(offset !== undefined ? { offset } : {}),
|
|
2140
|
+
...(revision !== undefined ? { revision } : {}),
|
|
2141
|
+
...(maxBytes !== undefined ? { maxBytes } : {}),
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
catch (error) {
|
|
2145
|
+
if (error instanceof ShelfRefusal)
|
|
2146
|
+
throw new ToolRefusal(error.code, error.message);
|
|
2147
|
+
throw error;
|
|
2148
|
+
}
|
|
2149
|
+
}));
|
|
1778
2150
|
server.registerTool("request_inquiry_episode", {
|
|
1779
2151
|
title: "Request that an inquiry be woven into an episode, or start one",
|
|
1780
2152
|
description: "Ask that a folder on the inquiry shelf (from save_research) be woven into a Chronicle episode, or that a new episode be started from it. Weaving copies the folder into the episode, so the approval page shows its visibility. Waits for a person's approval; on approval it runs once through Miadi's episode doors.",
|
|
@@ -1878,8 +2250,9 @@ export function registerTools(server, ctx) {
|
|
|
1878
2250
|
created_at: r.created_at,
|
|
1879
2251
|
circle: r.circle,
|
|
1880
2252
|
intention: r.intention,
|
|
1881
|
-
consequence: consequenceOf(r),
|
|
2253
|
+
consequence: consequenceOf(r, providerAuthorizationOf(r)),
|
|
1882
2254
|
detail: detailOf(r),
|
|
2255
|
+
...(r.kind === "run_request" ? { provider_authorization: authorizationOut(providerAuthorizationOf(r)) } : {}),
|
|
1883
2256
|
approval_url: approvalUrl(r.id),
|
|
1884
2257
|
projection: requestProjection(r, { reviewBase: ctx.approval?.config.reviewUrl, factoryDir: ctx.factoryDir }),
|
|
1885
2258
|
}));
|
|
@@ -1972,6 +2345,13 @@ export function registerTools(server, ctx) {
|
|
|
1972
2345
|
decided = settled;
|
|
1973
2346
|
}
|
|
1974
2347
|
catch (error) {
|
|
2348
|
+
// No projection here: the card shows this message, and the request still awaits a decision (jgwill/Miadi#926).
|
|
2349
|
+
if (error instanceof ProviderChoiceRequired) {
|
|
2350
|
+
throw new ToolRefusal("provider_choice_required", `${error.message} Open its approval page: ${approvalUrl(r.id)}`, {
|
|
2351
|
+
approval_url: approvalUrl(r.id),
|
|
2352
|
+
provider_authorization: authorizationOut(error.authorization),
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
1975
2355
|
if (error instanceof StaleVersion)
|
|
1976
2356
|
throw new ToolRefusal("stale_version", error.message, { projection: projectionNow(), approval_url: approvalUrl(r.id) });
|
|
1977
2357
|
// A different decision recorded a moment earlier, by another click or client, is reported with where it stands.
|
|
@@ -2084,11 +2464,16 @@ export const TOOL_STATUS = {
|
|
|
2084
2464
|
revise_pending_request: "live, contribute (a waiting or sent-back run or proposal gets its next version; same id, same card)",
|
|
2085
2465
|
get_request_history: "live (every version of a request, oldest first; whoever asked, or the write tier)",
|
|
2086
2466
|
supersede_pending_requests: "live, contribute (retires named earlier requests still waiting and joins them as earlier versions of the one that stands)",
|
|
2087
|
-
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)",
|
|
2467
|
+
request_session_steer: "live, contribute, write tier (one correction to a working agent, forwarded to its runtime; runs inside an approved run's steering grant, else one scoped human decision; get_capabilities.steering says whether the transport works now and which providers have an adapter)",
|
|
2468
|
+
revoke_session_steering: "live, contribute, write tier (stops a run's steering, or one steer not yet consumed)",
|
|
2088
2469
|
request_review_operation: "live, contribute, needs_approval",
|
|
2470
|
+
request_screenwalk_reading: "live, contribute, needs_approval (host provider adapter required; stops at editorial decision)",
|
|
2471
|
+
request_reading_import: "live, contribute, needs_approval (exact public import; atomic service contract required)",
|
|
2089
2472
|
request_deep_diver: "live, contribute, needs_approval",
|
|
2090
2473
|
link_review_to_episode: "live, contribute, needs_approval",
|
|
2091
2474
|
save_research: "live, contribute (the whole report, in parts when long; saved on the inquiry shelf with its issue, pushed; private by default)",
|
|
2475
|
+
list_research_artifacts: "live (the shelf as you may read it: yours, or every folder for the write tier; searched, sorted, paged)",
|
|
2476
|
+
read_research_artifact: "live (a shelf folder's manifest and its text files, paged, with sha256 and the commit that holds them; its owner or Miadi's write tier)",
|
|
2092
2477
|
request_inquiry_episode: "live, contribute, needs_approval (weaves a shelf folder into an episode or starts one)",
|
|
2093
2478
|
show_decisions: "live (cards in the conversation; deciding on them needs the write tier and the miadi:approve sign-in)",
|
|
2094
2479
|
decide_request: "card only: the person's click on a decisions card; not offered to the assistant",
|