humanish 0.113.0 → 0.114.0
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/CHANGELOG.md +22 -0
- package/CONTEXT.md +1 -0
- package/README.md +1 -1
- package/dist/actors/computer-use/openai-conversation.js +9 -7
- package/dist/actors/contract.d.ts +4 -2
- package/dist/analysis/admission.d.ts +37 -0
- package/dist/analysis/admission.js +60 -0
- package/dist/analysis/automatic-completion.d.ts +0 -6
- package/dist/analysis/automatic-completion.js +3 -3
- package/dist/analysis/automatic-config.d.ts +10 -2
- package/dist/analysis/automatic-config.js +29 -14
- package/dist/analysis/automatic.js +7 -0
- package/dist/analysis/execute.d.ts +19 -5
- package/dist/analysis/execute.js +58 -27
- package/dist/analysis/image-tokens.d.ts +2 -0
- package/dist/analysis/image-tokens.js +1 -1
- package/dist/analysis/job.d.ts +9 -1
- package/dist/analysis/job.js +11 -2
- package/dist/analysis/service.js +6 -3
- package/dist/cli/commands/analyze.js +7 -2
- package/dist/cli/commands/notes.d.ts +3 -0
- package/dist/cli/commands/notes.js +144 -0
- package/dist/cli/commands/observe.js +1 -1
- package/dist/cli/commands/study-run.js +2 -1
- package/dist/cli/findings.js +4 -1
- package/dist/cli/io.d.ts +1 -0
- package/dist/cli/io.js +11 -12
- package/dist/cli/program.js +2 -0
- package/dist/evidence/encoded-text.d.ts +15 -2
- package/dist/evidence/encoded-text.js +68 -4
- package/dist/evidence/secret-scrub.d.ts +1 -1
- package/dist/evidence/secret-scrub.js +23 -2
- package/dist/feedback/draft.d.ts +19 -0
- package/dist/feedback/draft.js +51 -1
- package/dist/feedback/export-bundle.js +2 -2
- package/dist/feedback/export.js +47 -3
- package/dist/feedback/feedback.js +42 -2
- package/dist/observer/artifact.d.ts +5 -0
- package/dist/observer/artifact.js +18 -2
- package/dist/observer/notes-route.d.ts +21 -0
- package/dist/observer/notes-route.js +140 -0
- package/dist/observer/pinned-files.d.ts +8 -1
- package/dist/observer/pinned-files.js +32 -3
- package/dist/observer/render.js +38 -14
- package/dist/observer/run-routes.d.ts +8 -2
- package/dist/observer/run-routes.js +47 -6
- package/dist/observer/serve.d.ts +3 -0
- package/dist/observer/serve.js +28 -3
- package/dist/observer-app.html +7 -6
- package/dist/run/contained-output.d.ts +7 -0
- package/dist/run/contained-output.js +35 -1
- package/dist/run/note-files.d.ts +73 -0
- package/dist/run/note-files.js +184 -0
- package/dist/run/notes.d.ts +37 -0
- package/dist/run/notes.js +198 -0
- package/dist/run/participant-caption.d.ts +9 -0
- package/dist/run/participant-caption.js +19 -0
- package/dist/study/doctor.js +1 -1
- package/dist/study/preflight-result.js +2 -1
- package/dist/study/summary.js +2 -2
- package/docs/contracts/errors.md +2 -2
- package/docs/contracts/run-bundle.md +57 -0
- package/docs/contracts/schemas.md +5 -2
- package/docs/contracts/study-analysis.md +26 -8
- package/docs/principles/invariants-and-defaults.md +2 -2
- package/docs/product/automatic-analysis.md +22 -16
- package/package.json +2 -2
- package/skills/humanish/SKILL.md +5 -0
package/dist/observer/render.js
CHANGED
|
@@ -12,12 +12,14 @@ import { loadAnalysis } from "../analysis/load.js";
|
|
|
12
12
|
import { physicalCwdOf, runIdOf, bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveLatestRunDirectory, validatePreparedRunArtifactPaths, } from "../run/paths.js";
|
|
13
13
|
import { writeContainedOutputFile } from "../run/contained-output.js";
|
|
14
14
|
import { loadRunBundlePrepared, readRunJsonIfExists } from "../run/locate.js";
|
|
15
|
+
import { readRunNotes } from "../run/notes.js";
|
|
15
16
|
import { RUN_STATUS_FILE } from "../run/status.js";
|
|
16
17
|
import { verifyRunPrepared } from "../verify/verify.js";
|
|
17
18
|
import { renderObserverHtml } from "./artifact.js";
|
|
18
19
|
import { buildObserverData } from "./data.js";
|
|
19
20
|
import { buildServeSecurityHeaders, hostAllowed, parsePublicOrigin } from "./http.js";
|
|
20
21
|
import { listenOnLoopback } from "./listen.js";
|
|
22
|
+
import { createNotesWriter, NOTES_PATH } from "./notes-route.js";
|
|
21
23
|
import { pinDirectChildDirectory, pinDirectory, readContainedFile, } from "./pinned-files.js";
|
|
22
24
|
import { buildHistoryIndex, matchRunRoute, serveRunPath, writeResponse, } from "./run-routes.js";
|
|
23
25
|
import { cli } from "../cli/invocation.js";
|
|
@@ -114,7 +116,7 @@ export async function renderObserver(cwdInput, runInput, options = {}) {
|
|
|
114
116
|
};
|
|
115
117
|
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "observer-data.json"), `${JSON.stringify(observerData, null, 2)}\n`, "utf8");
|
|
116
118
|
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "study-analysis.json"), `${JSON.stringify(analysis, null, 2)}\n`, "utf8");
|
|
117
|
-
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "index.html"), renderObserverHtml(observerData, { analysis }), "utf8");
|
|
119
|
+
await writeContainedOutputFile(preparedRunPaths, path.join("observer", "index.html"), renderObserverHtml(observerData, { analysis, notes: await readRunNotes(preparedRunPaths) }), "utf8");
|
|
118
120
|
await validatePreparedRunArtifactPaths(preparedRunPaths);
|
|
119
121
|
const relativeObserverPath = path.join(preparedRunPaths.relativeRunRoot, "observer", "index.html");
|
|
120
122
|
const relativeObserverDataPath = path.join(preparedRunPaths.relativeRunRoot, "observer", "observer-data.json");
|
|
@@ -180,6 +182,16 @@ async function resolveObserverRunSelection(cwd, runInput) {
|
|
|
180
182
|
return null;
|
|
181
183
|
return { runId: pointer.runId, runRoot, runsRoot };
|
|
182
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* The served run history. Exposed watch and selected-run viewers pass `onlyRun` and cannot
|
|
187
|
+
* enumerate other runs; full-library loopback viewers get the complete project index.
|
|
188
|
+
*/
|
|
189
|
+
async function writeHistory(response, proofRoot, onlyRun) {
|
|
190
|
+
const history = await buildHistoryIndex(proofRoot);
|
|
191
|
+
const runs = onlyRun === null ? history.runs : history.runs.filter((run) => run.runId === onlyRun);
|
|
192
|
+
const listed = onlyRun === null ? history : { latestRunId: runs.length > 0 ? onlyRun : null, runs };
|
|
193
|
+
writeResponse(response, 200, JSON.stringify(listed, null, 2), "application/json; charset=utf-8");
|
|
194
|
+
}
|
|
183
195
|
export async function serveObserver(result, options = {}) {
|
|
184
196
|
if (!result.ok || !result.observerPath) {
|
|
185
197
|
throw new Error("Cannot serve an observer result that did not render successfully.");
|
|
@@ -206,6 +218,14 @@ export async function serveObserver(result, options = {}) {
|
|
|
206
218
|
const runtimeStreamUrls = () => observerRuntimeStreamUrls.get(result) ?? [];
|
|
207
219
|
const exposed = options.exposed === true;
|
|
208
220
|
const scopedToRun = exposed || options.scope === "run";
|
|
221
|
+
let port = 0;
|
|
222
|
+
// Notes are taken for the selected run only, whose paths this server already holds.
|
|
223
|
+
const notes = createNotesWriter({
|
|
224
|
+
exposed,
|
|
225
|
+
port: () => port,
|
|
226
|
+
resolveRun: async (runId) => (runId === result.run ? preparedRunPaths : null),
|
|
227
|
+
refresh: (prepared) => renderObserver(cwd, result.run, { open: false, expectedRun: prepared }),
|
|
228
|
+
});
|
|
209
229
|
// Host allowlist for exposed mode (DNS-rebinding defense, identical to the run-library surface).
|
|
210
230
|
// Seeded with the loopback names after bind; addPublicOrigin extends it with the tunnel/public-url
|
|
211
231
|
// host. Never consulted in loopback (non-exposed) mode.
|
|
@@ -224,21 +244,22 @@ export async function serveObserver(result, options = {}) {
|
|
|
224
244
|
}
|
|
225
245
|
}
|
|
226
246
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
247
|
+
if (url.pathname === NOTES_PATH) {
|
|
248
|
+
await notes.handle(request, response);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Adding a note is the only write; nothing else is read with another method.
|
|
252
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
253
|
+
writeResponse(response, 405, "Method Not Allowed", "text/plain; charset=utf-8");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
227
256
|
if (url.pathname === "/") {
|
|
228
257
|
response.writeHead(302, { location: "/observer/index.html" });
|
|
229
258
|
response.end();
|
|
230
259
|
return;
|
|
231
260
|
}
|
|
232
261
|
if (url.pathname === "/_humanish/history.json") {
|
|
233
|
-
|
|
234
|
-
// Exposed watch and selected-run viewers cannot enumerate other runs. Full-library
|
|
235
|
-
// loopback viewers retain the complete project index.
|
|
236
|
-
if (scopedToRun) {
|
|
237
|
-
const attachedRuns = history.runs.filter((entry) => entry.runId === result.run);
|
|
238
|
-
writeResponse(response, 200, JSON.stringify({ latestRunId: attachedRuns.length > 0 ? result.run : null, runs: attachedRuns }, null, 2), "application/json; charset=utf-8");
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
writeResponse(response, 200, JSON.stringify(history, null, 2), "application/json; charset=utf-8");
|
|
262
|
+
await writeHistory(response, proofRoot, scopedToRun ? result.run : null);
|
|
242
263
|
return;
|
|
243
264
|
}
|
|
244
265
|
if (url.pathname.startsWith("/_humanish/runs/")) {
|
|
@@ -257,10 +278,10 @@ export async function serveObserver(result, options = {}) {
|
|
|
257
278
|
writeResponse(response, 404, "Run not found", "text/plain; charset=utf-8");
|
|
258
279
|
return;
|
|
259
280
|
}
|
|
260
|
-
await serveRunPath(targetRoot, runRoute.relativePath || "observer/index.html", response, runRoute.runId === result.run ? runtimeStreamUrls() : [], request);
|
|
281
|
+
await serveRunPath(targetRoot, runRoute.relativePath || "observer/index.html", response, runRoute.runId === result.run ? runtimeStreamUrls() : [], request, runRoute.runId === result.run ? notes.pageToken(request) : undefined);
|
|
261
282
|
return;
|
|
262
283
|
}
|
|
263
|
-
await serveRunPath(runRoot, decodeURIComponent(url.pathname.slice(1)), response, runtimeStreamUrls(), request);
|
|
284
|
+
await serveRunPath(runRoot, decodeURIComponent(url.pathname.slice(1)), response, runtimeStreamUrls(), request, notes.pageToken(request));
|
|
264
285
|
}
|
|
265
286
|
catch {
|
|
266
287
|
writeResponse(response, 500, "Observer request failed", "text/plain; charset=utf-8");
|
|
@@ -269,7 +290,7 @@ export async function serveObserver(result, options = {}) {
|
|
|
269
290
|
const server = createServer((request, response) => {
|
|
270
291
|
void handleRequest(request, response);
|
|
271
292
|
});
|
|
272
|
-
|
|
293
|
+
port = await listenOnLoopback(server, options.port ?? 0);
|
|
273
294
|
if (exposed) {
|
|
274
295
|
hostAllowlist.add(`127.0.0.1:${port}`);
|
|
275
296
|
hostAllowlist.add(`localhost:${port}`);
|
|
@@ -295,7 +316,10 @@ export async function serveObserver(result, options = {}) {
|
|
|
295
316
|
hostAllowlist.add(parsed.host);
|
|
296
317
|
}
|
|
297
318
|
},
|
|
298
|
-
close: () =>
|
|
319
|
+
close: async () => {
|
|
320
|
+
await notes.settled();
|
|
321
|
+
await closeServer(server);
|
|
322
|
+
},
|
|
299
323
|
};
|
|
300
324
|
}
|
|
301
325
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type PreparedRunArtifactPaths } from "../run/paths.js";
|
|
2
3
|
import type { RunDisplay } from "../run/display.js";
|
|
3
4
|
import { type ObserverData } from "./data.js";
|
|
4
5
|
import { type PinnedDirectory } from "./pinned-files.js";
|
|
@@ -10,8 +11,13 @@ export interface ObserverRuntimeStreamUrl {
|
|
|
10
11
|
* evidence (keyframe replay/screenshot) instead of rendering "sandbox not found". */
|
|
11
12
|
ended?: boolean;
|
|
12
13
|
}
|
|
13
|
-
/**
|
|
14
|
-
export declare function
|
|
14
|
+
/** The run's artifact paths, only when they are still the pinned directory being served. */
|
|
15
|
+
export declare function servedRunPaths(runRoot: PinnedDirectory): Promise<PreparedRunArtifactPaths>;
|
|
16
|
+
/**
|
|
17
|
+
* internal: consumed by src/observer/serve.ts. `notesToken` goes into the run's Observer page so
|
|
18
|
+
* it can add notes; a server passes it only for a run it takes notes for.
|
|
19
|
+
*/
|
|
20
|
+
export declare function serveRunPath(runRoot: PinnedDirectory, relativePath: string, response: ServerResponse, runtimeStreamUrls?: ObserverRuntimeStreamUrl[], request?: Pick<IncomingMessage, "method" | "headers">, notesToken?: string): Promise<void>;
|
|
15
21
|
/** internal: exported for the stream lifecycle tests. */
|
|
16
22
|
export declare function withRuntimeStreamUrls(data: ObserverData, runtimeStreamUrls: ObserverRuntimeStreamUrl[]): ObserverData;
|
|
17
23
|
/** internal: consumed by src/observer/serve.ts */
|
|
@@ -9,6 +9,7 @@ import { listRuns } from "../run/stored-runs.js";
|
|
|
9
9
|
import { readAnalysisAccounting } from "../run/costs.js";
|
|
10
10
|
import { bindExistingRunArtifactPaths, isPathInside, isSafeRunIdSegment, } from "../run/paths.js";
|
|
11
11
|
import { isRunOutcome } from "../run/bundle-shape.js";
|
|
12
|
+
import { decodeRunNote, listNoteEntries, MAX_NOTE_FILE_BYTES, noteListing, RUN_NOTES_DIR, runNoteFile, } from "../run/note-files.js";
|
|
12
13
|
import { analysisCostOf, runCost, runCostLabel } from "../run/run-cost.js";
|
|
13
14
|
import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord, } from "../run/status.js";
|
|
14
15
|
import { renderObserverHtml } from "./artifact.js";
|
|
@@ -16,7 +17,7 @@ import { buildObserverData, recordedStreamEmbed, withObserverEndings, } from "./
|
|
|
16
17
|
import { buildArtifactSecurityHeaders } from "./http.js";
|
|
17
18
|
import { assertPinnedDirectory, openContainedFile, pinDirectChildDirectory, readContainedFile, relativeToRoot, sha256OfOpenedFile, } from "./pinned-files.js";
|
|
18
19
|
/** The run's artifact paths, only when they are still the pinned directory being served. */
|
|
19
|
-
async function servedRunPaths(runRoot) {
|
|
20
|
+
export async function servedRunPaths(runRoot) {
|
|
20
21
|
const runId = path.basename(runRoot.physicalPath);
|
|
21
22
|
const cwd = path.dirname(path.dirname(path.dirname(runRoot.physicalPath)));
|
|
22
23
|
const prepared = await bindExistingRunArtifactPaths(cwd, runId);
|
|
@@ -57,8 +58,40 @@ async function readServedAnalysisSpend(runRoot) {
|
|
|
57
58
|
return undefined;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The run's reviewer notes as served: notes/ listed, and each note file read within
|
|
63
|
+
* MAX_NOTE_FILE_BYTES through the root's checks, so a --safe root's admission and hash checks
|
|
64
|
+
* apply to every note. A file that is not a readable note of the run is skipped and counted.
|
|
65
|
+
*/
|
|
66
|
+
async function readServedNotes(runRoot) {
|
|
67
|
+
const runId = path.basename(runRoot.physicalPath);
|
|
68
|
+
let listing;
|
|
69
|
+
try {
|
|
70
|
+
await assertPinnedDirectory(runRoot);
|
|
71
|
+
listing = await listNoteEntries(path.join(runRoot.physicalPath, RUN_NOTES_DIR));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { runId, notes: [], skipped: [`${RUN_NOTES_DIR} is not a plain folder.`] };
|
|
75
|
+
}
|
|
76
|
+
if (listing === null)
|
|
77
|
+
return { runId, notes: [], skipped: [] };
|
|
78
|
+
const { ids, skipped } = noteListing(listing);
|
|
79
|
+
const notes = [];
|
|
80
|
+
for (const id of ids) {
|
|
81
|
+
const bytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, runNoteFile(id)), { maxBytes: MAX_NOTE_FILE_BYTES });
|
|
82
|
+
const note = bytes === null ? null : decodeRunNote(bytes, runId, id);
|
|
83
|
+
if (note === null)
|
|
84
|
+
skipped.push(`${runNoteFile(id)} was skipped.`);
|
|
85
|
+
else
|
|
86
|
+
notes.push(note);
|
|
87
|
+
}
|
|
88
|
+
return { runId, notes, skipped };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* internal: consumed by src/observer/serve.ts. `notesToken` goes into the run's Observer page so
|
|
92
|
+
* it can add notes; a server passes it only for a run it takes notes for.
|
|
93
|
+
*/
|
|
94
|
+
export async function serveRunPath(runRoot, relativePath, response, runtimeStreamUrls = [], request, notesToken) {
|
|
62
95
|
const root = runRoot.physicalPath;
|
|
63
96
|
const filePath = path.resolve(root, relativePath === "" ? "observer/index.html" : relativePath);
|
|
64
97
|
if (!isPathInside(root, filePath)) {
|
|
@@ -87,7 +120,12 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
87
120
|
return;
|
|
88
121
|
}
|
|
89
122
|
const analysis = await readObserverAnalysis(runRoot);
|
|
90
|
-
|
|
123
|
+
const notes = await readServedNotes(runRoot);
|
|
124
|
+
writeResponse(response, 200, renderObserverHtml(observerData, {
|
|
125
|
+
analysis,
|
|
126
|
+
notes,
|
|
127
|
+
...(notesToken === undefined ? {} : { notesToken }),
|
|
128
|
+
}), "text/html; charset=utf-8");
|
|
91
129
|
return;
|
|
92
130
|
}
|
|
93
131
|
if (cleanedRelativePath === "observer/observer-data.json") {
|
|
@@ -103,7 +141,10 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
103
141
|
writeResponse(response, 200, JSON.stringify(await readObserverAnalysis(runRoot)), "application/json; charset=utf-8");
|
|
104
142
|
return;
|
|
105
143
|
}
|
|
106
|
-
|
|
144
|
+
// Every file under notes/ is read within the note limit, whatever its name, so media handling
|
|
145
|
+
// never streams one.
|
|
146
|
+
const notePath = derivedRoot === RUN_NOTES_DIR;
|
|
147
|
+
if (!notePath && path.extname(filePath).toLowerCase() === ".mp4") {
|
|
107
148
|
await serveContainedMedia(runRoot, filePath, response, request);
|
|
108
149
|
return;
|
|
109
150
|
}
|
|
@@ -112,7 +153,7 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
112
153
|
return;
|
|
113
154
|
}
|
|
114
155
|
try {
|
|
115
|
-
const body = await readContainedFile(runRoot, filePath);
|
|
156
|
+
const body = await readContainedFile(runRoot, filePath, notePath ? { maxBytes: MAX_NOTE_FILE_BYTES } : {});
|
|
116
157
|
if (!body) {
|
|
117
158
|
writeResponse(response, 404, "Not found", "text/plain; charset=utf-8");
|
|
118
159
|
return;
|
package/dist/observer/serve.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { type NotesWriter } from "./notes-route.js";
|
|
2
3
|
import type { PinnedDirectory } from "./pinned-files.js";
|
|
3
4
|
import { type AdmittedRun } from "./run-inventory.js";
|
|
4
5
|
import type { LibraryHistory } from "./library.js";
|
|
@@ -70,6 +71,8 @@ export declare function createShareSafetyAdmission(cwd: string, options?: {
|
|
|
70
71
|
}): ShareSafetyAdmission;
|
|
71
72
|
export interface ServeRequestHandlerOptions {
|
|
72
73
|
proofRoot: PinnedDirectory;
|
|
74
|
+
/** Takes reviewer notes on loopback; without it the library refuses them. */
|
|
75
|
+
notes?: NotesWriter;
|
|
73
76
|
safe: boolean;
|
|
74
77
|
admit: (runId: string) => Promise<AdmittedRun | null>;
|
|
75
78
|
hostAllowlist: ReadonlySet<string>;
|
package/dist/observer/serve.js
CHANGED
|
@@ -3,7 +3,9 @@ import { runNotFoundMessage } from "../run/run-not-found.js";
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pinDirectChildDirectory, pinDirectory } from "./pinned-files.js";
|
|
6
|
-
import {
|
|
6
|
+
import { createNotesWriter, NOTES_PATH } from "./notes-route.js";
|
|
7
|
+
import { renderObserver } from "./render.js";
|
|
8
|
+
import { buildHistoryIndex, matchRunRoute, servedRunPaths, serveRunPath } from "./run-routes.js";
|
|
7
9
|
import { hashRunInventory, inventoryRoot, readRunInventory, } from "./run-inventory.js";
|
|
8
10
|
import { renderLibraryHtml } from "./library.js";
|
|
9
11
|
import { buildServeSecurityHeaders, hostAllowed, parsePublicOrigin, } from "./http.js";
|
|
@@ -104,6 +106,13 @@ export function createServeRequestHandler(options) {
|
|
|
104
106
|
for (const [name, value] of Object.entries(buildServeSecurityHeaders())) {
|
|
105
107
|
response.setHeader(name, value);
|
|
106
108
|
}
|
|
109
|
+
if (new URL(request.url ?? "/", "http://127.0.0.1").pathname === NOTES_PATH) {
|
|
110
|
+
if (options.notes)
|
|
111
|
+
await options.notes.handle(request, response);
|
|
112
|
+
else
|
|
113
|
+
writeText(response, 405, "Method Not Allowed");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
107
116
|
const method = request.method ?? "GET";
|
|
108
117
|
if (method !== "GET" && method !== "HEAD") {
|
|
109
118
|
writeText(response, 405, "Method Not Allowed");
|
|
@@ -161,7 +170,7 @@ export function createServeRequestHandler(options) {
|
|
|
161
170
|
}
|
|
162
171
|
// The explicit empty runtimeStreamUrls keeps a future refactor from
|
|
163
172
|
// reintroducing auth-keyed stream injection on the serve surface.
|
|
164
|
-
await serveRunPath(targetRoot, runRoute.relativePath || "observer/index.html", response, [], request);
|
|
173
|
+
await serveRunPath(targetRoot, runRoute.relativePath || "observer/index.html", response, [], request, options.notes?.pageToken(request));
|
|
165
174
|
return;
|
|
166
175
|
}
|
|
167
176
|
writeText(response, 404, "Not found");
|
|
@@ -260,8 +269,24 @@ export async function serveObserverLibrary(cwdInput, options) {
|
|
|
260
269
|
}
|
|
261
270
|
const declaredOrigin = options.publicOrigin ? parsePublicOrigin(options.publicOrigin) : null;
|
|
262
271
|
const hostAllowlist = new Set();
|
|
272
|
+
let port = 0;
|
|
273
|
+
const notes = createNotesWriter({
|
|
274
|
+
exposed: mode !== "loopback",
|
|
275
|
+
port: () => port,
|
|
276
|
+
resolveRun: async (runId) => {
|
|
277
|
+
const pinned = await pinDirectChildDirectory(proofRoot, runId);
|
|
278
|
+
if (!pinned || (options.safe && (await admission.admit(runId)) === null))
|
|
279
|
+
return null;
|
|
280
|
+
return servedRunPaths(pinned);
|
|
281
|
+
},
|
|
282
|
+
refresh: (prepared) => renderObserver(cwd, path.basename(prepared.physicalRunRoot), {
|
|
283
|
+
open: false,
|
|
284
|
+
expectedRun: prepared,
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
263
287
|
const handler = createServeRequestHandler({
|
|
264
288
|
proofRoot,
|
|
289
|
+
notes,
|
|
265
290
|
safe: options.safe,
|
|
266
291
|
admit: (runId) => admission.admit(runId),
|
|
267
292
|
hostAllowlist,
|
|
@@ -276,7 +301,6 @@ export async function serveObserverLibrary(cwdInput, options) {
|
|
|
276
301
|
const server = createServer((request, response) => {
|
|
277
302
|
void handler(request, response);
|
|
278
303
|
});
|
|
279
|
-
let port;
|
|
280
304
|
try {
|
|
281
305
|
port = await listenOnLoopback(server, options.port);
|
|
282
306
|
}
|
|
@@ -314,6 +338,7 @@ export async function serveObserverLibrary(cwdInput, options) {
|
|
|
314
338
|
hostAllowlist.add(parsed.host);
|
|
315
339
|
},
|
|
316
340
|
close: async () => {
|
|
341
|
+
await notes.settled();
|
|
317
342
|
const closed = closeServer(server);
|
|
318
343
|
// Keep-alive sockets would otherwise keep close() pending past the point
|
|
319
344
|
// the operator believes Ctrl-C tore the server down.
|