@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/approve.js
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
import express from "express";
|
|
13
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
15
16
|
import { join } from "node:path";
|
|
16
17
|
import { escapeHtml } from "@miadi/node-service-kit/format";
|
|
18
|
+
import { DEFAULT_DECISION_WAIT_MS } from "./config.js";
|
|
17
19
|
import { MiadiError } from "./miadi.js";
|
|
18
20
|
import { MiadiUnavailable } from "./people.js";
|
|
19
21
|
import { current, newRecordId } from "./records.js";
|
|
@@ -22,7 +24,45 @@ import { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
|
22
24
|
import { factoryLedgerPath, readFactoryReceipts, receiptsForRecord } from "./factory-receipt.js";
|
|
23
25
|
import { steerAttempts, steerSession, tmuxAdapter } from "./steer.js";
|
|
24
26
|
import { contentHash, versionOf } from "./revisions.js";
|
|
27
|
+
import { canReadReading } from "./reading/privacy.js";
|
|
28
|
+
import { authorizationReceipt, authorizeProvider, briefAttributes, CHOICE_TTL_MS, CODEX_POLICIES, isPersonId, needsPersonChoice, providerPolicyOf, } from "./provider-policy.js";
|
|
25
29
|
export { factoryDir, runBriefFile, readQueueState } from "./queue-state.js";
|
|
30
|
+
/**
|
|
31
|
+
* One line per step of a decision, for the journal: the request, a correlation id,
|
|
32
|
+
* the door it came through, timings, the outcome and the HTTP status the run met.
|
|
33
|
+
* Never a token, never instructions (jgwill/Miadi#924).
|
|
34
|
+
*/
|
|
35
|
+
export function logDecision(deps, event) {
|
|
36
|
+
const line = { evt: "connector.decision", at: new Date().toISOString(), ...event };
|
|
37
|
+
try {
|
|
38
|
+
(deps.log ?? ((e) => console.log(JSON.stringify(e))))(line);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* a log that fails never fails a decision */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** How long a decision answer waits for its run before saying it is recorded and still running. */
|
|
45
|
+
export function decisionWaitOf(deps) {
|
|
46
|
+
return deps.config.decisionWaitMs ?? DEFAULT_DECISION_WAIT_MS;
|
|
47
|
+
}
|
|
48
|
+
/** What `settleWithin` answers when the promise has not settled in time; the promise goes on. */
|
|
49
|
+
export const STILL_RUNNING = Symbol("still_running");
|
|
50
|
+
/** The promise's value if it settles within `ms`, else STILL_RUNNING. A rejection after the bound is caught, never unhandled. */
|
|
51
|
+
export async function settleWithin(p, ms) {
|
|
52
|
+
let timer;
|
|
53
|
+
const bound = new Promise((resolve) => {
|
|
54
|
+
timer = setTimeout(() => resolve(STILL_RUNNING), ms);
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const out = await Promise.race([p, bound]);
|
|
58
|
+
if (out === STILL_RUNNING)
|
|
59
|
+
p.catch(() => { });
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
26
66
|
/**
|
|
27
67
|
* What a person may decide on a request. Approving with changes runs it with the
|
|
28
68
|
* approver's instructions carried into what runs; a revision sends it back to
|
|
@@ -65,8 +105,10 @@ export function proposalTask(record) {
|
|
|
65
105
|
}
|
|
66
106
|
return lines.join("\n");
|
|
67
107
|
}
|
|
68
|
-
export function runBrief(record, approver, sessions, instructions) {
|
|
108
|
+
export function runBrief(record, approver, sessions, instructions, providerAttrs = {}) {
|
|
69
109
|
const body = (record.kind === "work_proposal" ? { task: proposalTask(record) } : record.body);
|
|
110
|
+
// A request recorded before providers were resolved (jgwill/Miadi#893) names only what was asked (#926 review RR7).
|
|
111
|
+
const provider = body.resolved_provider ?? body.provider;
|
|
70
112
|
const short = record.id.split(":")[1]?.slice(0, 8) ?? "run";
|
|
71
113
|
const to = body.session && sessions.includes(body.session) ? body.session : `new:${body.session ?? `connector-${yymmdd()}-${short}`}`;
|
|
72
114
|
// A new session opens where the request says, else where the factory runner's config says (defaultCwd): no host path here (#911).
|
|
@@ -78,7 +120,9 @@ export function runBrief(record, approver, sessions, instructions) {
|
|
|
78
120
|
`record="${attr(record.id)}"`,
|
|
79
121
|
`version="${versionOf(record)}"`,
|
|
80
122
|
// The provider the connector verified at request time (jgwill/Miadi#893); the intake may honour it or record why not.
|
|
81
|
-
...(
|
|
123
|
+
...(provider ? [`provider="${attr(provider)}"`] : []),
|
|
124
|
+
// What authorizes a provider that needs the requester's own choice; the runner refuses Codex without it (jgwill/Miadi#926).
|
|
125
|
+
...Object.entries(providerAttrs).map(([k, v]) => `${k}="${attr(v)}"`),
|
|
82
126
|
...(to.startsWith("new:") && cwd ? [`cwd="${attr(cwd)}"`] : []),
|
|
83
127
|
];
|
|
84
128
|
const lines = [
|
|
@@ -151,6 +195,11 @@ export async function executeApproved(record, approver, deps, instructions) {
|
|
|
151
195
|
return { status: res.status, ok: res.ok, response: parsed };
|
|
152
196
|
};
|
|
153
197
|
const queueBrief = () => {
|
|
198
|
+
// Checked again where the brief is written: an approval, and every rerun of it (jgwill/Miadi#926).
|
|
199
|
+
const authorization = record.kind === "run_request" ? authorizeProvider(record, deps.ledger.all()) : undefined;
|
|
200
|
+
if (authorization && !authorization.authorized) {
|
|
201
|
+
return { outcome: "failed", provider_authorization: authorizationReceipt(authorization), message: `No brief was written. ${authorization.explanation}` };
|
|
202
|
+
}
|
|
154
203
|
const queue = join(deps.factoryDir ?? factoryDir(), "queue");
|
|
155
204
|
mkdirSync(queue, { recursive: true });
|
|
156
205
|
// A lister that cannot answer means no session is known live: the brief opens a new one (#922 R16).
|
|
@@ -161,10 +210,16 @@ export async function executeApproved(record, approver, deps, instructions) {
|
|
|
161
210
|
catch {
|
|
162
211
|
sessions = [];
|
|
163
212
|
}
|
|
164
|
-
const { to, brief } = runBrief(record, approver, sessions, instructions);
|
|
213
|
+
const { to, brief } = runBrief(record, approver, sessions, instructions, authorization ? briefAttributes(authorization) : {});
|
|
165
214
|
const file = runBriefFile(record);
|
|
166
215
|
writeFileSync(join(queue, file), brief, { flag: "wx", mode: 0o640 });
|
|
167
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
outcome: "queued",
|
|
218
|
+
to,
|
|
219
|
+
file,
|
|
220
|
+
...(authorization ? { provider_authorization: authorizationReceipt(authorization) } : {}),
|
|
221
|
+
note: "The factory intake delivers queued briefs every 5 minutes.",
|
|
222
|
+
};
|
|
168
223
|
};
|
|
169
224
|
switch (record.kind) {
|
|
170
225
|
case "run_request":
|
|
@@ -300,9 +355,15 @@ ${versionOf(r) > 1 ? `<dt>Version</dt><dd>${versionOf(r)}, the one a decision he
|
|
|
300
355
|
* caller has already checked who the person is, their write tier, and that the
|
|
301
356
|
* request still awaits approval.
|
|
302
357
|
*/
|
|
303
|
-
/** A request is decided once: a second decision while one runs, or after it, is refused. */
|
|
358
|
+
/** A request is decided once: a second, different decision while one runs, or after it, is refused. */
|
|
304
359
|
export class AlreadyDecided extends Error {
|
|
305
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* A decision that differs from the one recorded or being recorded (another choice, or other
|
|
363
|
+
* instructions). The same decision sent again is not refused: it answers with the first one's receipt (#924).
|
|
364
|
+
*/
|
|
365
|
+
export class DecisionConflict extends AlreadyDecided {
|
|
366
|
+
}
|
|
306
367
|
/** A decision on a version the request no longer holds: it was revised after the page or card was read (#919). */
|
|
307
368
|
export class StaleVersion extends AlreadyDecided {
|
|
308
369
|
currentVersion;
|
|
@@ -313,32 +374,129 @@ export class StaleVersion extends AlreadyDecided {
|
|
|
313
374
|
this.decidedVersion = decidedVersion;
|
|
314
375
|
}
|
|
315
376
|
}
|
|
316
|
-
|
|
377
|
+
/**
|
|
378
|
+
* An approval of a run whose provider needs the requester's own choice for this mission,
|
|
379
|
+
* and has none (jgwill/Miadi#926). Declining or sending it back stays possible.
|
|
380
|
+
*/
|
|
381
|
+
export class ProviderChoiceRequired extends Error {
|
|
382
|
+
authorization;
|
|
383
|
+
constructor(authorization) {
|
|
384
|
+
super(authorization.explanation);
|
|
385
|
+
this.authorization = authorization;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/** The decisions being recorded now, by request id: one at a time per request, in this process. */
|
|
389
|
+
const deciding = new Map();
|
|
317
390
|
/** Whether a decision on this request is being recorded now, its run may still be going. */
|
|
318
391
|
export function isBeingDecided(recordId) {
|
|
319
392
|
return deciding.has(recordId);
|
|
320
393
|
}
|
|
394
|
+
const APPROVAL_OF = {
|
|
395
|
+
approve: "approved",
|
|
396
|
+
approve_with_changes: "approved",
|
|
397
|
+
request_revision: "revision_requested",
|
|
398
|
+
decline: "declined",
|
|
399
|
+
};
|
|
400
|
+
/** The approval state a choice leaves a request in. */
|
|
401
|
+
export function approvalStateOf(choice) {
|
|
402
|
+
return APPROVAL_OF[choice];
|
|
403
|
+
}
|
|
404
|
+
const sameDecision = (a, b) => a.choice === b.choice && String(a.instructions ?? "").trim() === String(b.instructions ?? "").trim();
|
|
405
|
+
/**
|
|
406
|
+
* Record a person's decision once. The page and the card (or two cards, or a host
|
|
407
|
+
* that retries a call it gave up waiting on) may send it more than once:
|
|
408
|
+
* - the same decision while the first is being recorded waits for that one and
|
|
409
|
+
* answers with its receipt, marked `replayed`; nothing runs twice;
|
|
410
|
+
* - the same decision after it was recorded answers with the recorded receipt;
|
|
411
|
+
* - a different decision is refused as a DecisionConflict (jgwill/Miadi#924).
|
|
412
|
+
*/
|
|
321
413
|
export async function decide(r, person, choice, deps, opts) {
|
|
322
414
|
// Checked again here, with no await between the check and the claim: the
|
|
323
415
|
// page and the card (or two cards) may decide the same request at once, and
|
|
324
416
|
// a revision may land between reading a version and deciding it (#919).
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
417
|
+
const asked = { choice, instructions: opts.instructions };
|
|
418
|
+
const inflight = deciding.get(r.id);
|
|
419
|
+
if (inflight) {
|
|
420
|
+
if (inflight.promise && inflight.choice !== "rerun" && sameDecision({ choice: inflight.choice, instructions: inflight.instructions }, asked)) {
|
|
421
|
+
logDecision(deps, { phase: "replay", record: r.id, correlation: inflight.correlation, via: opts.via, choice, while: "recording" });
|
|
422
|
+
return { ...(await inflight.promise), replayed: true };
|
|
423
|
+
}
|
|
424
|
+
logDecision(deps, { phase: "conflict", record: r.id, correlation: inflight.correlation, via: opts.via, choice, while: "recording" });
|
|
425
|
+
throw new DecisionConflict(inflight.choice === "rerun" ? "This request is being run again." : "A different decision on this request is being recorded.");
|
|
328
426
|
}
|
|
427
|
+
const live = current(deps.ledger.all()).find((x) => x.id === r.id);
|
|
428
|
+
if (!live)
|
|
429
|
+
throw new AlreadyDecided("This request is gone.");
|
|
329
430
|
// A decision that names no version (a page opened before versions, a surface that sends none) decides version 1:
|
|
330
431
|
// a request revised since is refused, never decided blind (#919 review F1).
|
|
331
432
|
const decidedVersion = opts.expectedVersion ?? 1;
|
|
433
|
+
if (live.approval_state !== "awaiting_approval") {
|
|
434
|
+
const prior = (live.completion_receipt ?? {});
|
|
435
|
+
const priorVersion = typeof prior.version === "number" ? prior.version : 1;
|
|
436
|
+
if (typeof prior.decision === "string" && sameDecision({ choice: prior.decision, instructions: prior.instructions }, asked) && priorVersion === decidedVersion) {
|
|
437
|
+
logDecision(deps, { phase: "replay", record: r.id, via: opts.via, choice, while: "recorded", state: live.state });
|
|
438
|
+
return { decision: live.approval_state, state: live.state, receipt: prior, replayed: true };
|
|
439
|
+
}
|
|
440
|
+
logDecision(deps, { phase: "conflict", record: r.id, via: opts.via, choice, while: "recorded", recorded: prior.decision });
|
|
441
|
+
throw new DecisionConflict(`This request is already ${live.approval_state ?? "gone"}.`);
|
|
442
|
+
}
|
|
332
443
|
if (versionOf(live) !== decidedVersion)
|
|
333
444
|
throw new StaleVersion(versionOf(live), decidedVersion);
|
|
334
|
-
|
|
445
|
+
// A generic approval never chooses Codex: the requester's own fresh choice or policy must already stand (jgwill/Miadi#926).
|
|
446
|
+
if (live.kind === "run_request" && approvalStateOf(choice) === "approved") {
|
|
447
|
+
const authorization = authorizeProvider(live, deps.ledger.all(), { requireFresh: true });
|
|
448
|
+
if (!authorization.authorized) {
|
|
449
|
+
logDecision(deps, { phase: "provider_choice_required", record: r.id, via: opts.via, choice, provider: authorization.provider });
|
|
450
|
+
throw new ProviderChoiceRequired(authorization);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const entry = { ...asked, correlation: randomUUID(), started: Date.now() };
|
|
454
|
+
deciding.set(r.id, entry);
|
|
455
|
+
entry.promise = decideOnce(live, person, choice, deps, { ...opts, correlation: entry.correlation, started: entry.started });
|
|
335
456
|
try {
|
|
336
|
-
return await
|
|
457
|
+
return await entry.promise;
|
|
337
458
|
}
|
|
338
459
|
finally {
|
|
339
460
|
deciding.delete(r.id);
|
|
340
461
|
}
|
|
341
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* The kinds whose run is done at once on this host (a brief written to the
|
|
465
|
+
* factory queue, an ending recorded): nothing outside is called, so nothing is
|
|
466
|
+
* written ahead of it. Every other approved run is recorded as running first.
|
|
467
|
+
*/
|
|
468
|
+
const RUNS_AT_ONCE = new Set(["run_request", "work_proposal", "session_completion_proposal"]);
|
|
469
|
+
/** What the receipt of a run the connector lost to a restart says (jgwill/Miadi#924). */
|
|
470
|
+
export const INTERRUPTED_MESSAGE = "The connector stopped while this ran, so whether it finished is not known. Read what it was to change before running it again. A review create is safe to run again: the review service answers with the review it already holds for that video.";
|
|
471
|
+
/**
|
|
472
|
+
* A run recorded as running whose process is gone (the connector restarted mid-run)
|
|
473
|
+
* becomes a failed run that a person may run again. Called once when the connector
|
|
474
|
+
* starts; a run this process is still recording is left alone.
|
|
475
|
+
*/
|
|
476
|
+
export function reconcileInterrupted(deps) {
|
|
477
|
+
const out = [];
|
|
478
|
+
for (const r of current(deps.ledger.all())) {
|
|
479
|
+
const receipt = (r.completion_receipt ?? {});
|
|
480
|
+
if (r.approval_state !== "approved" || r.state !== "in_progress" || receipt.outcome !== "running" || deciding.has(r.id))
|
|
481
|
+
continue;
|
|
482
|
+
deps.ledger.append({
|
|
483
|
+
id: newRecordId(r.kind),
|
|
484
|
+
kind: r.kind,
|
|
485
|
+
created_at: new Date().toISOString(),
|
|
486
|
+
actor: { person_id: "system:miadi-connector", name: "Miadi connector", client_id: "restart" },
|
|
487
|
+
refs: r.refs,
|
|
488
|
+
evidence_refs: [],
|
|
489
|
+
supersedes: r.id,
|
|
490
|
+
approval_state: "approved",
|
|
491
|
+
state: "blocked",
|
|
492
|
+
completion_receipt: { ...receipt, outcome: "failed", interrupted: true, message: INTERRUPTED_MESSAGE, interrupted_at: new Date().toISOString() },
|
|
493
|
+
body: { decision: "interrupted" },
|
|
494
|
+
});
|
|
495
|
+
logDecision(deps, { phase: "interrupted", record: r.id, correlation: receipt.correlation, started_at: receipt.started_at });
|
|
496
|
+
out.push(r.id);
|
|
497
|
+
}
|
|
498
|
+
return out;
|
|
499
|
+
}
|
|
342
500
|
/** Whether a request was approved and its run failed, so a person may run it again. */
|
|
343
501
|
export function isRerunnable(r) {
|
|
344
502
|
return !!r && r.approval_state === "approved" && r.state === "blocked";
|
|
@@ -355,7 +513,7 @@ export async function rerun(r, person, deps, opts) {
|
|
|
355
513
|
if (!isRerunnable(live) || deciding.has(r.id)) {
|
|
356
514
|
throw new AlreadyDecided(deciding.has(r.id) ? "This request is being run." : `This request is ${live?.approval_state ?? "gone"} and ${live?.state ?? "unknown"}; only an approved request whose run failed is run again.`);
|
|
357
515
|
}
|
|
358
|
-
deciding.
|
|
516
|
+
deciding.set(r.id, { choice: "rerun", correlation: randomUUID(), started: Date.now() });
|
|
359
517
|
try {
|
|
360
518
|
const prior = (live.completion_receipt ?? {});
|
|
361
519
|
const instructions = typeof prior.instructions === "string" ? prior.instructions : undefined;
|
|
@@ -403,7 +561,8 @@ export async function rerun(r, person, deps, opts) {
|
|
|
403
561
|
}
|
|
404
562
|
async function decideOnce(r, person, choice, deps, opts) {
|
|
405
563
|
const { instructions } = opts;
|
|
406
|
-
const decision = choice
|
|
564
|
+
const decision = approvalStateOf(choice);
|
|
565
|
+
logDecision(deps, { phase: "start", record: r.id, kind: r.kind, correlation: opts.correlation, via: opts.via, choice, version: versionOf(r) });
|
|
407
566
|
let receipt = {
|
|
408
567
|
decided_by: { person_id: person.id, name: person.name },
|
|
409
568
|
decided_at: new Date().toISOString(),
|
|
@@ -414,9 +573,28 @@ async function decideOnce(r, person, choice, deps, opts) {
|
|
|
414
573
|
via: opts.via,
|
|
415
574
|
...(instructions ? { instructions } : {}),
|
|
416
575
|
note: opts.note,
|
|
576
|
+
correlation: opts.correlation,
|
|
417
577
|
};
|
|
418
578
|
let state = decision === "approved" ? "completed" : decision === "revision_requested" ? "pending" : "withdrawn";
|
|
419
579
|
if (decision === "approved") {
|
|
580
|
+
if (!RUNS_AT_ONCE.has(r.kind)) {
|
|
581
|
+
// Written ahead of the run: the decision is in the ledger before anything is called, so a restart
|
|
582
|
+
// mid-run can never offer the request for a second approval, and every surface reads it as running (#924).
|
|
583
|
+
deps.ledger.append({
|
|
584
|
+
id: newRecordId(r.kind),
|
|
585
|
+
kind: r.kind,
|
|
586
|
+
created_at: new Date().toISOString(),
|
|
587
|
+
actor: { person_id: person.id, name: person.name, client_id: opts.via },
|
|
588
|
+
refs: r.refs,
|
|
589
|
+
evidence_refs: [],
|
|
590
|
+
supersedes: r.id,
|
|
591
|
+
approval_state: decision,
|
|
592
|
+
state: "in_progress",
|
|
593
|
+
completion_receipt: { ...receipt, outcome: "running", started_at: new Date(opts.started).toISOString() },
|
|
594
|
+
body: { decision: choice, ...(instructions ? { instructions } : {}) },
|
|
595
|
+
});
|
|
596
|
+
logDecision(deps, { phase: "running", record: r.id, correlation: opts.correlation, ms: Date.now() - opts.started });
|
|
597
|
+
}
|
|
420
598
|
try {
|
|
421
599
|
const outcome = await executeApproved(r, person, deps, instructions);
|
|
422
600
|
receipt = { ...receipt, ...outcome };
|
|
@@ -444,17 +622,29 @@ async function decideOnce(r, person, choice, deps, opts) {
|
|
|
444
622
|
completion_receipt: receipt,
|
|
445
623
|
body: { decision: choice, ...(instructions ? { instructions } : {}) },
|
|
446
624
|
});
|
|
625
|
+
logDecision(deps, {
|
|
626
|
+
phase: "done",
|
|
627
|
+
record: r.id,
|
|
628
|
+
correlation: opts.correlation,
|
|
629
|
+
decision,
|
|
630
|
+
state,
|
|
631
|
+
outcome: receipt.outcome,
|
|
632
|
+
...(typeof receipt.status === "number" ? { status: receipt.status } : {}),
|
|
633
|
+
ms: Date.now() - opts.started,
|
|
634
|
+
});
|
|
447
635
|
return { decision, state, receipt };
|
|
448
636
|
}
|
|
449
637
|
export function isApprovable(r) {
|
|
450
638
|
return Boolean(r && APPROVABLE.has(r.kind));
|
|
451
639
|
}
|
|
452
640
|
/** What approving a request does, in one sentence, for the card a person decides on. */
|
|
453
|
-
export function consequenceOf(r) {
|
|
641
|
+
export function consequenceOf(r, authorization) {
|
|
454
642
|
const b = r.body;
|
|
455
643
|
switch (r.kind) {
|
|
456
644
|
case "run_request":
|
|
457
|
-
return
|
|
645
|
+
return authorization && !authorization.authorized
|
|
646
|
+
? `Approving is refused until ${r.actor.name} chooses ${authorization.provider} for this mission on its approval page; approving does not choose it. Declining or sending it back stays possible.`
|
|
647
|
+
: `Approving queues a brief for the factory intake, which opens or reaches an agent session within minutes${authorization?.provider ? ` with ${authorization.provider}` : ""}.`;
|
|
458
648
|
case "work_proposal":
|
|
459
649
|
return "Approving starts the work: a brief written from the proposal and your instructions is queued for an agent session.";
|
|
460
650
|
case "review_operation_request":
|
|
@@ -506,8 +696,43 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
506
696
|
if (!writer && r.actor.person_id !== person.id) {
|
|
507
697
|
return void res.status(403).type("html").send(signInToRead(r, `Reading and deciding this request needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.`));
|
|
508
698
|
}
|
|
699
|
+
const requester = r.actor.person_id === person.id && isPersonId(person.id);
|
|
509
700
|
if (!body.decision)
|
|
510
|
-
return void res.type("html").send(approvePage(deps, r, { token: writer ? token : undefined }));
|
|
701
|
+
return void res.type("html").send(approvePage(deps, r, { token: writer ? token : undefined, chooserToken: requester ? token : undefined }));
|
|
702
|
+
if (body.decision === "choose_provider") {
|
|
703
|
+
// The requester's own, separate choice of a provider that needs one, for this version only (jgwill/Miadi#926).
|
|
704
|
+
const live = find(r.id);
|
|
705
|
+
const again = (status, error) => void res.status(status).type("html").send(approvePage(deps, live, { token: writer ? token : undefined, chooserToken: requester ? token : undefined, error }));
|
|
706
|
+
if (!requester) {
|
|
707
|
+
return again(403, isPersonId(r.actor.person_id)
|
|
708
|
+
? `Only ${r.actor.name}, who asked, can choose a provider for their own mission.`
|
|
709
|
+
: `${r.actor.name} is a service token: only a person chooses a provider, for a request of their own. Name another provider for this request.`);
|
|
710
|
+
}
|
|
711
|
+
if (live.approval_state !== "awaiting_approval" || isBeingDecided(live.id))
|
|
712
|
+
return again(409, "A provider is chosen only while the request awaits a decision.");
|
|
713
|
+
const provider = String(body.provider ?? "");
|
|
714
|
+
const named = authorizeProvider(live, deps.ledger.all(), { requireFresh: true });
|
|
715
|
+
if (!needsPersonChoice(provider) || provider !== named.provider)
|
|
716
|
+
return again(400, `This request names ${named.provider ?? "no provider"}; only that provider can be chosen here.`);
|
|
717
|
+
if (Number(body.version) !== versionOf(live))
|
|
718
|
+
return again(409, `This request is now at version ${versionOf(live)}. Read it and choose again.`);
|
|
719
|
+
if (body.confirm !== "yes")
|
|
720
|
+
return again(400, `Tick the box to choose ${provider} for this mission.`);
|
|
721
|
+
if (!named.authorized || named.state !== "person_choice") {
|
|
722
|
+
deps.ledger.append({
|
|
723
|
+
id: newRecordId("provider_choice"),
|
|
724
|
+
kind: "provider_choice",
|
|
725
|
+
created_at: new Date().toISOString(),
|
|
726
|
+
actor: { person_id: person.id, name: person.name, client_id: "approval-page" },
|
|
727
|
+
refs: [live.id],
|
|
728
|
+
evidence_refs: [],
|
|
729
|
+
claim_category: "observed",
|
|
730
|
+
body: { request_id: live.id, provider, version: versionOf(live), content_hash: contentHash(live) },
|
|
731
|
+
});
|
|
732
|
+
logDecision(deps, { phase: "provider_chosen", record: live.id, provider, version: versionOf(live) });
|
|
733
|
+
}
|
|
734
|
+
return void res.type("html").send(approvePage(deps, find(r.id), { token: writer ? token : undefined, chooserToken: token }));
|
|
735
|
+
}
|
|
511
736
|
if (!writer)
|
|
512
737
|
return void res.status(403).type("html").send(approvePage(deps, r, { error: `Deciding needs Miadi's write tier; ${person.name} has ${person.api_tier ?? "no API tier"}.` }));
|
|
513
738
|
if (body.decision === "rerun") {
|
|
@@ -523,12 +748,11 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
523
748
|
const failedAgain = ran.receipt.outcome === "failed";
|
|
524
749
|
return void res.type("html").send(page("Recorded", `<h1>${failedAgain ? "Run again, and it failed again" : "Run again"}</h1>${summary(find(r.id))}<pre>${escapeHtml(JSON.stringify(ran.receipt, null, 2))}</pre>`));
|
|
525
750
|
}
|
|
526
|
-
if (r.approval_state !== "awaiting_approval") {
|
|
527
|
-
return void res.status(409).type("html").send(approvePage(deps, r, { error: `This request is already ${r.approval_state}.` }));
|
|
528
|
-
}
|
|
529
751
|
const choice = DECISIONS.includes(String(body.decision)) ? body.decision : "decline";
|
|
530
752
|
const instructions = String(body.instructions ?? "").trim().slice(0, 20000) || undefined;
|
|
531
|
-
|
|
753
|
+
const via = String(body.via ?? "approval-page").slice(0, 60);
|
|
754
|
+
// Only a decision that would be recorded now needs its instructions; a form sent again after it was recorded is answered below (#924).
|
|
755
|
+
if (r.approval_state === "awaiting_approval" && !isBeingDecided(r.id) && (choice === "approve_with_changes" || choice === "request_revision") && !instructions) {
|
|
532
756
|
return void res
|
|
533
757
|
.status(400)
|
|
534
758
|
.type("html")
|
|
@@ -537,32 +761,47 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
537
761
|
let decided;
|
|
538
762
|
try {
|
|
539
763
|
const shown = Number(body.version);
|
|
540
|
-
|
|
764
|
+
const running = decide(r, person, choice, deps, {
|
|
541
765
|
instructions,
|
|
542
766
|
note: body.note || undefined,
|
|
543
|
-
via
|
|
767
|
+
via,
|
|
544
768
|
...(Number.isInteger(shown) && shown >= 1 ? { expectedVersion: shown } : {}),
|
|
545
769
|
});
|
|
770
|
+
// A run that takes longer than a proxy waits (the review service may take minutes) is answered as
|
|
771
|
+
// recorded and running; the run goes on, and signing in again shows how it ended (#924).
|
|
772
|
+
const settled = await settleWithin(running, decisionWaitOf(deps));
|
|
773
|
+
if (settled === STILL_RUNNING) {
|
|
774
|
+
logDecision(deps, { phase: "answered_running", record: r.id, via, choice });
|
|
775
|
+
const now = find(r.id);
|
|
776
|
+
return void res.status(202).type("html").send(page("Recorded", `<h1>${escapeHtml(projectionOf(now, deps)?.label ?? "Being recorded")}</h1>${summary(now)}${projectionBlock(projectionOf(now, deps), undefined)}${refreshForm(token)}`));
|
|
777
|
+
}
|
|
778
|
+
decided = settled;
|
|
546
779
|
}
|
|
547
780
|
catch (error) {
|
|
781
|
+
if (error instanceof ProviderChoiceRequired) {
|
|
782
|
+
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, chooserToken: requester ? token : undefined, error: error.message }));
|
|
783
|
+
}
|
|
548
784
|
if (error instanceof StaleVersion)
|
|
549
785
|
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { token, error: error.message }));
|
|
550
786
|
if (error instanceof AlreadyDecided)
|
|
551
787
|
return void res.status(409).type("html").send(approvePage(deps, find(r.id), { error: error.message }));
|
|
552
788
|
throw error;
|
|
553
789
|
}
|
|
554
|
-
const { decision, receipt } = decided;
|
|
790
|
+
const { decision, receipt, replayed } = decided;
|
|
555
791
|
const p = projectionOf(find(r.id), deps);
|
|
556
792
|
const heading = decision === "approved"
|
|
557
793
|
? p?.stage === "failed"
|
|
558
794
|
? "Approved, but running it failed"
|
|
559
|
-
: p?.
|
|
560
|
-
? "Approved,
|
|
561
|
-
:
|
|
795
|
+
: p?.stage === "recording"
|
|
796
|
+
? "Approved, running"
|
|
797
|
+
: p?.outstanding.length
|
|
798
|
+
? "Approved, changes still to do"
|
|
799
|
+
: "Approved"
|
|
562
800
|
: decision === "revision_requested"
|
|
563
801
|
? "Sent back for revision"
|
|
564
802
|
: "Declined";
|
|
565
|
-
|
|
803
|
+
const again = replayed ? "<p>This decision was already recorded. Nothing ran again; here is where it stands.</p>" : "";
|
|
804
|
+
res.type("html").send(page("Recorded", `<h1>${heading}</h1>${again}${summary(find(r.id))}${projectionBlock(p, receipt)}${p?.stage === "recording" ? refreshForm(token) : ""}`));
|
|
566
805
|
});
|
|
567
806
|
// A record's own page, for citations in ChatGPT: its author and anyone with Miadi's read tier may read it.
|
|
568
807
|
app.get("/records/:id", (req, res) => {
|
|
@@ -583,12 +822,19 @@ export function registerApprovalRoutes(app, deps) {
|
|
|
583
822
|
const r = find(String(req.params.id));
|
|
584
823
|
if (!r)
|
|
585
824
|
return void res.status(404).type("html").send(page("Not found", "<h1>No record with that id</h1>"));
|
|
825
|
+
if (!canReadReading(r, current(deps.ledger.all()), person)) {
|
|
826
|
+
return void res.status(403).type("html").send(page("Miadi record", recordForm(String(req.params.id), "This source and its reading are private to the source owner and factory operators.")));
|
|
827
|
+
}
|
|
586
828
|
if (!person.api_tier && r.actor.person_id !== person.id) {
|
|
587
829
|
return void res.status(403).type("html").send(page("Miadi record", recordForm(String(req.params.id), `Reading records needs Miadi's read tier; ${person.name} has none.`)));
|
|
588
830
|
}
|
|
589
831
|
res.type("html").send(page("Miadi record", `<h1>${escapeHtml(r.kind)}</h1>${summary(r)}`));
|
|
590
832
|
});
|
|
591
833
|
}
|
|
834
|
+
/** A button that shows where a running request stands now, signed in with the token the person just used. */
|
|
835
|
+
function refreshForm(token) {
|
|
836
|
+
return `<form method="post"><input type="hidden" name="person_token" value="${escapeHtml(token)}"><div class="row"><button class="primary" type="submit">Show where it stands now</button></div></form>`;
|
|
837
|
+
}
|
|
592
838
|
function recordForm(id, error) {
|
|
593
839
|
return `<h1>Miadi record</h1><p>${escapeHtml(id)}</p>
|
|
594
840
|
${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
@@ -639,8 +885,12 @@ function approvePage(deps, r, o = {}) {
|
|
|
639
885
|
const open = r.approval_state === "awaiting_approval";
|
|
640
886
|
// A decision being recorded offers no buttons: a second click would decide the same request again.
|
|
641
887
|
const actionable = open && !isBeingDecided(r.id);
|
|
888
|
+
// Freshness binds a decision still to come; a decided run shows the authorization it holds (#926 review R1).
|
|
889
|
+
const authorization = r.kind === "run_request" ? authorizeProvider(r, deps.ledger.all(), { requireFresh: open }) : undefined;
|
|
890
|
+
const blocked = Boolean(authorization && !authorization.authorized);
|
|
642
891
|
return page("Approve a Miadi request", `<h1>${open ? "Approve a request" : "A decided request"}</h1>
|
|
643
|
-
${actionable ? `<p>${escapeHtml(consequenceOf(r))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
|
|
892
|
+
${actionable ? `<p>${escapeHtml(consequenceOf(r, authorization))} Declining records your refusal. Either is kept in the ledger with your name.</p>` : ""}
|
|
893
|
+
${authorization ? providerSection(r, authorization, actionable ? o.chooserToken : undefined) : ""}
|
|
644
894
|
${summary(r)}
|
|
645
895
|
${projectionBlock(projectionOf(r, deps), open ? undefined : r.completion_receipt)}
|
|
646
896
|
${o.error ? `<p class="err" role="alert">${escapeHtml(o.error)}</p>` : ""}
|
|
@@ -651,19 +901,104 @@ ${actionable && o.token
|
|
|
651
901
|
<label for="i">Instructions (needed to approve with changes or to send back)</label>
|
|
652
902
|
<textarea id="i" name="instructions" rows="4"></textarea>
|
|
653
903
|
<div class="row">
|
|
654
|
-
|
|
655
|
-
|
|
904
|
+
${blocked
|
|
905
|
+
? ""
|
|
906
|
+
: `<button class="primary" type="submit" name="decision" value="approve">Approve and run</button>
|
|
907
|
+
<button type="submit" name="decision" value="approve_with_changes">Approve with my changes</button>`}
|
|
656
908
|
<button type="submit" name="decision" value="request_revision">Send back for revision</button>
|
|
657
909
|
<button type="submit" name="decision" value="decline">Decline</button>
|
|
658
910
|
</div></form>`
|
|
659
911
|
: actionable
|
|
660
912
|
? "<p>A person with Miadi's write tier decides this request.</p>"
|
|
661
|
-
:
|
|
662
|
-
?
|
|
913
|
+
: o.token && projectionOf(r, deps)?.stage === "recording"
|
|
914
|
+
? refreshForm(o.token)
|
|
915
|
+
: isRerunnable(r) && o.token
|
|
916
|
+
? `<p>It was approved, and running it failed. When what it calls has been fixed, it can be run again with the same approval and instructions; the ledger records who ran it again.</p>
|
|
663
917
|
<form method="post">
|
|
664
918
|
<input type="hidden" name="person_token" value="${escapeHtml(o.token)}">
|
|
665
919
|
<div class="row"><button class="primary" type="submit" name="decision" value="rerun">Run it again</button></div></form>`
|
|
666
|
-
|
|
920
|
+
: ""}`);
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Which provider a run launches with, who chose it, and, when the requester's own choice is
|
|
924
|
+
* missing, the separate control only the requester sees (jgwill/Miadi#926).
|
|
925
|
+
*/
|
|
926
|
+
function providerSection(r, a, chooserToken) {
|
|
927
|
+
const lines = [
|
|
928
|
+
`<dt>Provider</dt><dd>${escapeHtml(a.provider ?? "the factory runner's default")}</dd>`,
|
|
929
|
+
`<dt>Asked for</dt><dd>${escapeHtml(a.requested ?? "none named")}${a.requester.client ? ` (through ${escapeHtml(a.requester.client)})` : ""}</dd>`,
|
|
930
|
+
`<dt>Authorized by</dt><dd>${escapeHtml(a.state.replace(/_/g, " "))}</dd>`,
|
|
931
|
+
].join("");
|
|
932
|
+
const choose = a.state === "needs_person_choice" && chooserToken && a.provider
|
|
933
|
+
? `<form method="post">
|
|
934
|
+
<input type="hidden" name="person_token" value="${escapeHtml(chooserToken)}">
|
|
935
|
+
<input type="hidden" name="version" value="${versionOf(r)}">
|
|
936
|
+
<input type="hidden" name="provider" value="${escapeHtml(a.provider)}">
|
|
937
|
+
<label><input type="checkbox" name="confirm" value="yes" required> I choose ${escapeHtml(a.provider)} for this mission (version ${versionOf(r)}) myself. It does not cover any other mission.</label>
|
|
938
|
+
<div class="row"><button type="submit" name="decision" value="choose_provider">Choose ${escapeHtml(a.provider)} for this mission</button></div>
|
|
939
|
+
</form>`
|
|
940
|
+
: a.state === "needs_person_choice" && !isPersonId(r.actor.person_id)
|
|
941
|
+
? ""
|
|
942
|
+
: a.state === "needs_person_choice"
|
|
943
|
+
? `<p>Only ${escapeHtml(r.actor.name)}, who asked, can choose ${escapeHtml(a.provider ?? "")} for this mission, signed in here. Within ${CHOICE_TTL_MS / 3_600_000} hours of that choice it can be approved.</p>`
|
|
944
|
+
: "";
|
|
945
|
+
return `<section aria-label="Provider"><dl>${lines}</dl><p>${escapeHtml(a.explanation)}</p>${choose}</section>`;
|
|
946
|
+
}
|
|
947
|
+
/** The person's own provider policy page: read and change it, signed in with their person token (jgwill/Miadi#926). */
|
|
948
|
+
export function registerProviderPolicyRoutes(app, deps) {
|
|
949
|
+
const form = express.urlencoded({ extended: false, limit: "16kb" });
|
|
950
|
+
const signIn = (error) => page("Your provider policy", `<h1>Your provider policy</h1><p>Which agent providers a request made in your name may name. Sign in to read or change yours; it applies to your requests only.</p>
|
|
951
|
+
${error ? `<p class="err" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
952
|
+
<form method="post"><label for="t">Your Miadi person token</label><input id="t" name="person_token" type="password" autocomplete="off" required>
|
|
953
|
+
<div class="row"><button class="primary" type="submit">Read my policy</button></div></form>`);
|
|
954
|
+
app.get("/providers", (_req, res) => void res.type("html").send(signIn()));
|
|
955
|
+
app.post("/providers", form, async (req, res) => {
|
|
956
|
+
const body = (req.body ?? {});
|
|
957
|
+
const token = String(body.person_token ?? "").trim();
|
|
958
|
+
let person;
|
|
959
|
+
try {
|
|
960
|
+
person = await deps.identify(token);
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
if (!(error instanceof MiadiUnavailable))
|
|
964
|
+
throw error;
|
|
965
|
+
return void res.status(503).type("html").send(signIn("Miadi could not be reached. Try again in a minute."));
|
|
966
|
+
}
|
|
967
|
+
if (!person)
|
|
968
|
+
return void res.status(401).type("html").send(signIn("Miadi did not recognise that person token."));
|
|
969
|
+
if (!isPersonId(person.id))
|
|
970
|
+
return void res.status(403).type("html").send(signIn("A service token has no provider policy; a person sets their own."));
|
|
971
|
+
let saved = "";
|
|
972
|
+
if (body.codex !== undefined) {
|
|
973
|
+
if (!CODEX_POLICIES.includes(body.codex))
|
|
974
|
+
return void res.status(400).type("html").send(signIn("Unknown Codex policy."));
|
|
975
|
+
if (providerPolicyOf(deps.ledger.all(), person.id).codex !== body.codex) {
|
|
976
|
+
deps.ledger.append({
|
|
977
|
+
id: newRecordId("provider_policy"),
|
|
978
|
+
kind: "provider_policy",
|
|
979
|
+
created_at: new Date().toISOString(),
|
|
980
|
+
actor: { person_id: person.id, name: person.name, client_id: "providers-page" },
|
|
981
|
+
refs: [],
|
|
982
|
+
evidence_refs: [],
|
|
983
|
+
claim_category: "observed",
|
|
984
|
+
body: { codex: body.codex },
|
|
985
|
+
});
|
|
986
|
+
logDecision(deps, { phase: "provider_policy_set", person: person.id, codex: body.codex });
|
|
987
|
+
}
|
|
988
|
+
saved = "<p>Saved. It applies to your requests from now on, including a failed run of yours that is run again. A per-mission choice you already made still covers its mission.</p>";
|
|
989
|
+
}
|
|
990
|
+
const policy = providerPolicyOf(deps.ledger.all(), person.id);
|
|
991
|
+
const option = (value, label) => `<label><input type="radio" name="codex" value="${value}"${policy.codex === value ? " checked" : ""}> ${escapeHtml(label)}</label>`;
|
|
992
|
+
res.type("html").send(page("Your provider policy", `<h1>${escapeHtml(person.name)}'s provider policy</h1>${saved}
|
|
993
|
+
<dl><dt>Codex</dt><dd>${escapeHtml(policy.codex.replace(/_/g, " "))}${policy.source === "default" ? " (the default)" : `, set ${escapeHtml(policy.set_at ?? "")}`}</dd>
|
|
994
|
+
<dt>Other providers</dt><dd>Pi, Copilot and Claude Code run when a request names them and they are installed; Pi and Copilot may also be picked by the host's order.</dd></dl>
|
|
995
|
+
<form method="post"><input type="hidden" name="person_token" value="${escapeHtml(token)}">
|
|
996
|
+
<fieldset><legend>When a request in my name names Codex</legend>
|
|
997
|
+
${option("per_mission_choice", "Codex runs only when I choose it for that mission on its approval page (default).")}
|
|
998
|
+
${option("selectable", "Codex may be named for my requests without a per-mission choice.")}
|
|
999
|
+
</fieldset>
|
|
1000
|
+
<div class="row"><button class="primary" type="submit">Save my policy</button></div></form>`));
|
|
1001
|
+
});
|
|
667
1002
|
}
|
|
668
1003
|
/** How a request is named to a person. */
|
|
669
1004
|
export const KIND_LABELS = {
|
package/dist/config.d.ts
CHANGED
|
@@ -14,5 +14,17 @@ export interface ConnectorConfig {
|
|
|
14
14
|
inquiryRoot?: string;
|
|
15
15
|
/** owner/repo a saved report's inquiry issue is opened in (MIADI_INQUIRY_GITHUB_REPO). */
|
|
16
16
|
inquiryRepo?: string;
|
|
17
|
+
/**
|
|
18
|
+
* How long a card's decision token stays usable, in seconds (MIADI_CONNECTOR_DECISION_TOKEN_TTL).
|
|
19
|
+
* Default 7 days, held between 5 minutes and 30 days. The request itself does not expire (jgwill/Miadi#924).
|
|
20
|
+
*/
|
|
21
|
+
decisionTokenTtl?: number;
|
|
22
|
+
/**
|
|
23
|
+
* How long a decision answer waits for the run it starts, in milliseconds (MIADI_CONNECTOR_DECISION_WAIT_MS).
|
|
24
|
+
* Past it the answer says the decision is recorded and still running, and the run goes on. Default 20 s, at most 50 s.
|
|
25
|
+
*/
|
|
26
|
+
decisionWaitMs?: number;
|
|
17
27
|
}
|
|
28
|
+
export declare const DEFAULT_DECISION_TOKEN_TTL: number;
|
|
29
|
+
export declare const DEFAULT_DECISION_WAIT_MS = 20000;
|
|
18
30
|
export declare function configFromEnv(env?: NodeJS.ProcessEnv): ConnectorConfig;
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { resolveEnv } from "@miadi/inquiry-weave";
|
|
2
2
|
import { reviewToken } from "./reviews.js";
|
|
3
|
+
export const DEFAULT_DECISION_TOKEN_TTL = 7 * 24 * 3600;
|
|
4
|
+
export const DEFAULT_DECISION_WAIT_MS = 20_000;
|
|
5
|
+
/** A number from the environment held between two bounds; anything unreadable is the default. */
|
|
6
|
+
function bounded(raw, fallback, min, max) {
|
|
7
|
+
const n = Number(raw);
|
|
8
|
+
if (raw === undefined || raw.trim() === "" || !Number.isFinite(n))
|
|
9
|
+
return fallback;
|
|
10
|
+
return Math.min(max, Math.max(min, Math.round(n)));
|
|
11
|
+
}
|
|
3
12
|
export function configFromEnv(env = process.env) {
|
|
4
13
|
const port = Number(env.MIADI_CONNECTOR_PORT || 3337);
|
|
5
14
|
const shelf = resolveEnv({}, env);
|
|
@@ -14,5 +23,7 @@ export function configFromEnv(env = process.env) {
|
|
|
14
23
|
stateDir: env.MIADI_CONNECTOR_STATE_DIR || "/srv/miadi/connector",
|
|
15
24
|
inquiryRoot: shelf.inquiryRoot,
|
|
16
25
|
inquiryRepo: shelf.repo,
|
|
26
|
+
decisionTokenTtl: bounded(env.MIADI_CONNECTOR_DECISION_TOKEN_TTL, DEFAULT_DECISION_TOKEN_TTL, 300, 30 * 24 * 3600),
|
|
27
|
+
decisionWaitMs: bounded(env.MIADI_CONNECTOR_DECISION_WAIT_MS, DEFAULT_DECISION_WAIT_MS, 0, 50_000),
|
|
17
28
|
};
|
|
18
29
|
}
|