@aloud/runner 0.2.5 → 0.3.1

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.
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Getting a credential onto this machine without anybody carrying one.
3
+ *
4
+ * The client asks the server to open an approval, prints a URL and a short code, and waits. A person
5
+ * opens that URL in a browser they are already signed in to, types the code this printed, and
6
+ * approves. The next poll returns the credential and writes it to disk.
7
+ *
8
+ * The reason this exists rather than a prompt: a prompt needs a terminal, and the thing driving
9
+ * setup is increasingly an agent that does not have one. Waiting on a click needs nothing but time,
10
+ * so the same command works for a person and for an agent, and neither of them ever sees a token.
11
+ */
12
+ export interface ApprovalStart {
13
+ id: string;
14
+ userCode: string;
15
+ deviceCode: string;
16
+ approveUrl: string;
17
+ expiresAt: string;
18
+ pollMs: number;
19
+ }
20
+
21
+ export type ApprovalOutcome =
22
+ | { status: "approved"; secret: string; kind: "runner" | "mcp" }
23
+ | { status: "denied" }
24
+ | { status: "expired" };
25
+
26
+ export class ApprovalFailed extends Error {}
27
+
28
+ export async function startApproval(input: {
29
+ server: string;
30
+ kind: "runner" | "mcp";
31
+ name?: string | null;
32
+ fetchImpl?: typeof fetch;
33
+ }): Promise<ApprovalStart> {
34
+ const response = await (input.fetchImpl ?? fetch)(new URL("api/approvals", input.server + "/"), {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json", accept: "application/json" },
37
+ redirect: "error",
38
+ body: JSON.stringify({ kind: input.kind, name: input.name ?? null }),
39
+ });
40
+ if (!response.ok) {
41
+ throw new ApprovalFailed(`${input.server} would not start an approval (${response.status}).`);
42
+ }
43
+ return (await response.json()) as ApprovalStart;
44
+ }
45
+
46
+ /**
47
+ * Polls until somebody answers, or the request expires.
48
+ *
49
+ * `204` means keep waiting, matching the run loop's claim endpoint. The interval comes from the
50
+ * server rather than being chosen here, because every poll is an invocation on the other end and
51
+ * the server is the side that knows what it can afford.
52
+ */
53
+ export async function waitForApproval(
54
+ start: ApprovalStart,
55
+ deps: {
56
+ server: string;
57
+ fetchImpl?: typeof fetch;
58
+ sleep?: (ms: number) => Promise<void>;
59
+ now?: () => number;
60
+ onWaiting?: (secondsElapsed: number) => void;
61
+ signal?: AbortSignal;
62
+ },
63
+ ): Promise<ApprovalOutcome> {
64
+ const fetchImpl = deps.fetchImpl ?? fetch;
65
+ const sleep = deps.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
66
+ const now = deps.now ?? (() => Date.now());
67
+
68
+ const startedAt = now();
69
+ const deadline = Date.parse(start.expiresAt);
70
+ let announced = 0;
71
+
72
+ for (;;) {
73
+ if (deps.signal?.aborted) throw new ApprovalFailed("Stopped waiting.");
74
+ if (now() >= deadline) return { status: "expired" };
75
+
76
+ const response = await fetchImpl(new URL("api/approvals/collect", deps.server + "/"), {
77
+ method: "POST",
78
+ headers: { authorization: `Bearer ${start.deviceCode}`, accept: "application/json" },
79
+ redirect: "error",
80
+ }).catch(() => null);
81
+
82
+ // A dropped poll is not an answer. Keep waiting rather than failing someone's setup because a
83
+ // wifi card slept: the deadline is what ends this, not one bad request.
84
+ if (response && response.status !== 204) {
85
+ if (!response.ok) {
86
+ throw new ApprovalFailed(`${deps.server} refused the approval check (${response.status}).`);
87
+ }
88
+ const body = (await response.json()) as ApprovalOutcome;
89
+ if (body.status !== "expired" || now() >= deadline) return body;
90
+ }
91
+
92
+ const elapsed = Math.round((now() - startedAt) / 1000);
93
+ if (elapsed - announced >= 15) {
94
+ announced = elapsed;
95
+ deps.onWaiting?.(elapsed);
96
+ }
97
+ await sleep(start.pollMs);
98
+ }
99
+ }
@@ -76,6 +76,7 @@ export interface ModelProxyRequest {
76
76
  system: string | null;
77
77
  prompt: string;
78
78
  responseShape: string;
79
+ responseSchema: Record<string, unknown>;
79
80
  maxOutputTokens: number;
80
81
  temperature: number;
81
82
  images: unknown[];
@@ -155,6 +155,7 @@ export async function executeLease(deps: ExecuteDeps): Promise<ExecuteResult> {
155
155
  run: { ...deps.run, snapshot },
156
156
  snapshot,
157
157
  productId: deps.productId,
158
+ syntheticEmailTemplate: deps.local.syntheticEmailTemplate,
158
159
  },
159
160
  {
160
161
  gateway,
@@ -253,6 +254,9 @@ export async function executeLease(deps: ExecuteDeps): Promise<ExecuteResult> {
253
254
  if (outcome && !leaseLost) {
254
255
  // Same order as the server's own persistence: findings before the report, because the report
255
256
  // holds ordered identifiers that have to resolve to something.
257
+ // Sessions are checkpointed before synthesis, then posted again here because integrity runs
258
+ // during synthesis and enriches them with the evidence exclusions the replay must disclose.
259
+ await postPart(deps.client, deps.lease.id, "sessions", outcome.sessions);
256
260
  await postPart(deps.client, deps.lease.id, "judgments", outcome.judgments);
257
261
  await postPart(deps.client, deps.lease.id, "issues", outcome.issues);
258
262
  await postPart(deps.client, deps.lease.id, "findings", outcome.findings);
package/src/version.ts CHANGED
@@ -10,7 +10,62 @@
10
10
  * package.json beside it to read, and importing one into the source trips the composite build's
11
11
  * rootDir. `version.test.ts` asserts this matches, so the drift this invites cannot survive CI.
12
12
  */
13
- export const RUNNER_VERSION = "0.2.5";
13
+ export const RUNNER_VERSION = "0.3.1";
14
14
 
15
15
  /** The header the server reads it from. */
16
16
  export const RUNNER_VERSION_HEADER = "x-aloud-runner-version";
17
+
18
+ export interface RunnerVersionPolicy {
19
+ /** The oldest release that may claim new work. */
20
+ minimum: string;
21
+ /** The exact official release a start should move to when it can. */
22
+ recommended: string;
23
+ }
24
+
25
+ /** Strict because a server response eventually becomes part of an npm package specifier. */
26
+ function versionParts(value: string): [number, number, number] | null {
27
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
28
+ if (!match) return null;
29
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3])] as [number, number, number];
30
+ return parts.every(Number.isSafeInteger) ? parts : null;
31
+ }
32
+
33
+ /** Numeric comparison, so 0.10.0 is newer than 0.9.9 rather than lexicographically smaller. */
34
+ export function compareRunnerVersions(left: string, right: string): number | null {
35
+ const a = versionParts(left);
36
+ const b = versionParts(right);
37
+ if (!a || !b) return null;
38
+ for (let index = 0; index < a.length; index += 1) {
39
+ if (a[index]! > b[index]!) return 1;
40
+ if (a[index]! < b[index]!) return -1;
41
+ }
42
+ return 0;
43
+ }
44
+
45
+ /** Parses only a coherent policy. A bad control-plane response never becomes an install command. */
46
+ export function runnerVersionPolicyFrom(value: unknown): RunnerVersionPolicy | null {
47
+ if (!value || typeof value !== "object") return null;
48
+ const policy = value as { minimum?: unknown; recommended?: unknown };
49
+ if (typeof policy.minimum !== "string" || typeof policy.recommended !== "string") return null;
50
+ const order = compareRunnerVersions(policy.recommended, policy.minimum);
51
+ if (order === null || order < 0) return null;
52
+ return { minimum: policy.minimum, recommended: policy.recommended };
53
+ }
54
+
55
+ export interface RunnerUpdate {
56
+ target: string;
57
+ /** Required means the installed release cannot claim work if updating fails. */
58
+ required: boolean;
59
+ }
60
+
61
+ /** What this installed runner should do with one valid server policy. Never chooses a downgrade. */
62
+ export function runnerUpdateFor(
63
+ policy: RunnerVersionPolicy,
64
+ current: string = RUNNER_VERSION,
65
+ ): RunnerUpdate | null {
66
+ const recommendedOrder = compareRunnerVersions(policy.recommended, current);
67
+ const minimumOrder = compareRunnerVersions(policy.minimum, current);
68
+ if (recommendedOrder === null || minimumOrder === null) return null;
69
+ if (recommendedOrder <= 0) return null;
70
+ return { target: policy.recommended, required: minimumOrder > 0 };
71
+ }