@inflexa-ai/harness 0.21.1 → 0.22.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.
@@ -38,7 +38,7 @@ import type { ThreadStore } from "../memory/thread-store.js";
38
38
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
39
39
  import type { ReportVersionStore } from "../state/report-versions.js";
40
40
  import type { ReportSessionStateGateway } from "../tools/report-authoring/authoring-tools.js";
41
- import { type ResolvePageAsset } from "../tools/report-session/index.js";
41
+ import { type MakeSessionPagePublisher, type ResolvePageAsset, type ResolvePageUrl } from "../tools/report-session/index.js";
42
42
  /** Canonical agent id -- distinct from the id of the old templating agent. */
43
43
  export declare const REPORT_SESSION_AGENT_ID: "report-session";
44
44
  /**
@@ -98,6 +98,21 @@ export interface ReportSessionAgentDeps {
98
98
  * installation of the harness.
99
99
  */
100
100
  readonly resolvePageAsset?: ResolvePageAsset;
101
+ /**
102
+ * Session-page publisher factory -- the hosted view of the rendered page. The factory
103
+ * binds one analysis and the auth of the call, like `makeResolver`, thus the mint runs
104
+ * under the credential of the caller. Bound, the preview tool builds the publisher over
105
+ * the scope of the call, mints one grant after the page lands, and attaches the URL
106
+ * beside the path. Omitted, the result carries the page path alone.
107
+ */
108
+ readonly makeSessionPages?: MakeSessionPagePublisher;
109
+ /**
110
+ * Page-URL formation of the eyes -- the URL that one look opens. A composition whose
111
+ * browser cannot reach the workspace tree binds a resolver that names a served URL, and
112
+ * the args carry the auth of the tool call beside the page identity. Omitted, the eyes
113
+ * tool navigates through a `file://` URL of the page path.
114
+ */
115
+ readonly resolvePageUrl?: ResolvePageUrl;
101
116
  /** Operational logging seam; omitted falls back to no-op. */
102
117
  readonly logger?: Logger;
103
118
  }
@@ -39,7 +39,7 @@ const REPORT_SESSION_MAX_ITERATIONS = 75;
39
39
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
40
40
  export function createReportSessionAgent(deps) {
41
41
  const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, eyes, makeResolver, resolvePageAsset, logger } = deps;
42
- const { derivations, sandboxClient, runAuthorizer } = deps;
42
+ const { derivations, sandboxClient, runAuthorizer, makeSessionPages, resolvePageUrl } = deps;
43
43
  const authoring = createReportAuthoringTools(gateway);
44
44
  const tools = [
45
45
  // The read surface toward the analysis. It roams the tree read-only.
@@ -83,6 +83,7 @@ export function createReportSessionAgent(deps) {
83
83
  resolveWorkspaceRoot,
84
84
  ...(makeResolver ? { makeResolver } : {}),
85
85
  ...(resolvePageAsset ? { resolvePageAsset } : {}),
86
+ ...(makeSessionPages ? { makeSessionPages } : {}),
86
87
  ...(logger ? { logger } : {}),
87
88
  }),
88
89
  // The eyes tool. It opens the rendered page in headless Chrome.
@@ -91,6 +92,7 @@ export function createReportSessionAgent(deps) {
91
92
  resolveWorkspaceRoot,
92
93
  chrome,
93
94
  ...(eyes ? { eyes } : {}),
95
+ ...(resolvePageUrl ? { resolvePageUrl } : {}),
94
96
  ...(logger ? { logger } : {}),
95
97
  }),
96
98
  // The record tool. It gates the whole document, records one version, then prunes each
@@ -1,21 +1,29 @@
1
1
  /**
2
2
  * Content URL construction — shared between the frontend and Cortex.
3
3
  *
4
- * The `res` claim formula and URL shape are the canonical TypeScript contract
5
- * for the content-token preview flow. The storage backend mirrors the formula in Go; both
6
- * sides are locked by the shared test vector at
7
- * `src/__tests__/fixtures/preview-res.json`.
4
+ * The `res` claim formulas and URL shapes are the canonical TypeScript contract
5
+ * for the content-token flow. The storage backend mirrors each formula in Go; both
6
+ * sides are locked by the shared test vectors at
7
+ * `src/__tests__/fixtures/preview-res.json` (previews) and
8
+ * `src/__tests__/fixtures/report-session-res.json` (report-session pages), each a
9
+ * byte-identical copy of the storage backend's `kernel/contenttoken/testdata` file.
8
10
  *
9
- * End-to-end contract:
11
+ * End-to-end contract (previews):
10
12
  * token res claim : "previews/{analysisId}/{previewId}"
11
13
  * URL : {contentBaseUrl}/previews/{analysisId}/{previewId}/{previewPath}?t={token}
12
14
  * Caddy predicate : request.path.startsWith("/" + res + "/")
13
15
  *
16
+ * End-to-end contract (report-session pages):
17
+ * token res claim : "report-sessions/{analysisId}/{threadId}" (token kind "rs")
18
+ * URL : {contentBaseUrl}/report-sessions/{analysisId}/{threadId}/{pagePath}?t={token}
19
+ * Caddy predicate : request.path.startsWith("/" + res + "/")
20
+ *
14
21
  * This is URL space only. On disk a preview lives inside its analysis's workspace tree, at
15
22
  * `{resolveWorkspaceRoot(analysisId)}/previews/{previewId}/{previewPath}` (the workspace-layout
16
- * spec) — the `res` claim carries the analysis id because the URL needs an authorization
17
- * boundary, whereas the resolved root already identifies the analysis and so does not repeat it.
18
- * A host that serves previews owns the map between the two.
23
+ * spec), and a session page at `{resolveWorkspaceRoot(analysisId)}/report-sessions/{threadId}/{pagePath}`
24
+ * (the report-session-agent spec) — the `res` claim carries the analysis id because the URL needs an
25
+ * authorization boundary, whereas the resolved root already identifies the analysis and so does not
26
+ * repeat it. A host that serves either space owns the map between the two.
19
27
  *
20
28
  * The harness's contracts module owns this TS implementation so both the harness and
21
29
  * react-client consume one canonical formula.
@@ -47,3 +55,30 @@ export declare function previewResourceId(analysisId: string, previewId: string)
47
55
  * @param token - Short-lived content token minted by the storage backend (required)
48
56
  */
49
57
  export declare function buildPreviewUrl(contentBaseUrl: string, analysisId: string, previewId: string, previewPath: string, token: string): string;
58
+ /**
59
+ * Canonical `res` claim formula for a report-session page. Returns the literal string
60
+ * `report-sessions/{analysisId}/{threadId}` with no leading or trailing slash.
61
+ *
62
+ * This is the URL sub-path served by the content-server (Caddy) and the token's
63
+ * authorization boundary — NOT a filesystem sub-path; see the module doc.
64
+ *
65
+ * MUST match `fmt.Sprintf("report-sessions/%s/%s", analysisID, threadID)` in
66
+ * the storage backend's Go `kernel/contenttoken` implementation. Drift is caught by the
67
+ * shared test vector at `src/__tests__/fixtures/report-session-res.json`.
68
+ */
69
+ export declare function reportSessionResourceId(analysisId: string, threadId: string): string;
70
+ /**
71
+ * Build a content-token URL for a report-session page.
72
+ *
73
+ * Produces `{contentBaseUrl}/{res}/{pagePath}?t={token}` where `res` is
74
+ * the output of `reportSessionResourceId(analysisId, threadId)`. The query parameter
75
+ * name is `t` (matching Caddy's `from_query t` directive). The token is
76
+ * URL-encoded.
77
+ *
78
+ * @param contentBaseUrl - Content server base URL (trailing slash tolerated)
79
+ * @param analysisId - The analysis ID (authorization boundary)
80
+ * @param threadId - The report thread ID (one session directory per thread)
81
+ * @param pagePath - Session-relative path (typically "index.html"). Leading slash tolerated.
82
+ * @param token - Short-lived content token minted by the storage backend (required)
83
+ */
84
+ export declare function buildReportSessionUrl(contentBaseUrl: string, analysisId: string, threadId: string, pagePath: string, token: string): string;
@@ -1,21 +1,29 @@
1
1
  /**
2
2
  * Content URL construction — shared between the frontend and Cortex.
3
3
  *
4
- * The `res` claim formula and URL shape are the canonical TypeScript contract
5
- * for the content-token preview flow. The storage backend mirrors the formula in Go; both
6
- * sides are locked by the shared test vector at
7
- * `src/__tests__/fixtures/preview-res.json`.
4
+ * The `res` claim formulas and URL shapes are the canonical TypeScript contract
5
+ * for the content-token flow. The storage backend mirrors each formula in Go; both
6
+ * sides are locked by the shared test vectors at
7
+ * `src/__tests__/fixtures/preview-res.json` (previews) and
8
+ * `src/__tests__/fixtures/report-session-res.json` (report-session pages), each a
9
+ * byte-identical copy of the storage backend's `kernel/contenttoken/testdata` file.
8
10
  *
9
- * End-to-end contract:
11
+ * End-to-end contract (previews):
10
12
  * token res claim : "previews/{analysisId}/{previewId}"
11
13
  * URL : {contentBaseUrl}/previews/{analysisId}/{previewId}/{previewPath}?t={token}
12
14
  * Caddy predicate : request.path.startsWith("/" + res + "/")
13
15
  *
16
+ * End-to-end contract (report-session pages):
17
+ * token res claim : "report-sessions/{analysisId}/{threadId}" (token kind "rs")
18
+ * URL : {contentBaseUrl}/report-sessions/{analysisId}/{threadId}/{pagePath}?t={token}
19
+ * Caddy predicate : request.path.startsWith("/" + res + "/")
20
+ *
14
21
  * This is URL space only. On disk a preview lives inside its analysis's workspace tree, at
15
22
  * `{resolveWorkspaceRoot(analysisId)}/previews/{previewId}/{previewPath}` (the workspace-layout
16
- * spec) — the `res` claim carries the analysis id because the URL needs an authorization
17
- * boundary, whereas the resolved root already identifies the analysis and so does not repeat it.
18
- * A host that serves previews owns the map between the two.
23
+ * spec), and a session page at `{resolveWorkspaceRoot(analysisId)}/report-sessions/{threadId}/{pagePath}`
24
+ * (the report-session-agent spec) — the `res` claim carries the analysis id because the URL needs an
25
+ * authorization boundary, whereas the resolved root already identifies the analysis and so does not
26
+ * repeat it. A host that serves either space owns the map between the two.
19
27
  *
20
28
  * The harness's contracts module owns this TS implementation so both the harness and
21
29
  * react-client consume one canonical formula.
@@ -54,3 +62,37 @@ export function buildPreviewUrl(contentBaseUrl, analysisId, previewId, previewPa
54
62
  const res = previewResourceId(analysisId, previewId);
55
63
  return `${base}/${res}/${path}?t=${encodeURIComponent(token)}`;
56
64
  }
65
+ /**
66
+ * Canonical `res` claim formula for a report-session page. Returns the literal string
67
+ * `report-sessions/{analysisId}/{threadId}` with no leading or trailing slash.
68
+ *
69
+ * This is the URL sub-path served by the content-server (Caddy) and the token's
70
+ * authorization boundary — NOT a filesystem sub-path; see the module doc.
71
+ *
72
+ * MUST match `fmt.Sprintf("report-sessions/%s/%s", analysisID, threadID)` in
73
+ * the storage backend's Go `kernel/contenttoken` implementation. Drift is caught by the
74
+ * shared test vector at `src/__tests__/fixtures/report-session-res.json`.
75
+ */
76
+ export function reportSessionResourceId(analysisId, threadId) {
77
+ return `report-sessions/${analysisId}/${threadId}`;
78
+ }
79
+ /**
80
+ * Build a content-token URL for a report-session page.
81
+ *
82
+ * Produces `{contentBaseUrl}/{res}/{pagePath}?t={token}` where `res` is
83
+ * the output of `reportSessionResourceId(analysisId, threadId)`. The query parameter
84
+ * name is `t` (matching Caddy's `from_query t` directive). The token is
85
+ * URL-encoded.
86
+ *
87
+ * @param contentBaseUrl - Content server base URL (trailing slash tolerated)
88
+ * @param analysisId - The analysis ID (authorization boundary)
89
+ * @param threadId - The report thread ID (one session directory per thread)
90
+ * @param pagePath - Session-relative path (typically "index.html"). Leading slash tolerated.
91
+ * @param token - Short-lived content token minted by the storage backend (required)
92
+ */
93
+ export function buildReportSessionUrl(contentBaseUrl, analysisId, threadId, pagePath, token) {
94
+ const base = contentBaseUrl.replace(/\/+$/, "");
95
+ const path = pagePath.replace(/^\/+/, "");
96
+ const res = reportSessionResourceId(analysisId, threadId);
97
+ return `${base}/${res}/${path}?t=${encodeURIComponent(token)}`;
98
+ }
@@ -15,4 +15,4 @@ export { CoverageSchema, RowCoverageSchema, EntitySchema, ClaimSupportSchema, Cl
15
15
  export type { Coverage, RowCoverage, Entity, EvidenceItem, ClaimSupport, ClaimEvidence, LiabilityBullet, LiabilitySummary, TractabilitySection, ClinicalTrialAttribution, ClinicalTrialRow, ClinicalDevelopment, FailedTrialRow, TrialOutcomeRow, OrganRiskRow, OffTargetRow, RegulatoryActionRow, SafetyFlag, SafetyProfile, OffTissueRow, QualityGateStatus, QualityGates, Derived, Dossier, DossierBody, SectionBlurbKey, TargetAssessmentPhase, TargetAssessmentProgressEvent, } from "./target-dossier.js";
16
16
  export { TargetAssessmentStatusSchema, TargetAssessmentErrorSchema, TargetAssessmentListRowSchema, TargetAssessmentRowSchema, } from "./target-assessment-row.js";
17
17
  export type { TargetAssessmentStatus, TargetAssessmentError, TargetAssessmentListRow, TargetAssessmentRow } from "./target-assessment-row.js";
18
- export { buildPreviewUrl, previewResourceId } from "./content-url.js";
18
+ export { buildPreviewUrl, buildReportSessionUrl, previewResourceId, reportSessionResourceId } from "./content-url.js";
@@ -5,4 +5,4 @@ export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./part
5
5
  export { AnalogyCoverageSchema, AnalogyReportSchema, AnalogyReportErrorSchema, AnalogicalReasonerOutputSchema } from "./analogy-report.js";
6
6
  export { CoverageSchema, RowCoverageSchema, EntitySchema, ClaimSupportSchema, ClaimEvidenceSchema, LiabilityBulletSchema, LiabilitySummarySchema, TractabilitySchema, IndicationsSchema, DrugInteractionsSchema, ClinicalTrialAttributionSchema, ClinicalDevelopmentSchema, OrganRiskRowSchema, OffTargetRowSchema, OffTargetPanelSchema, RegulatoryActionRowSchema, SafetyFlagSchema, SafetyProfileSchema, OffTissueRowSchema, OffTissueRiskSchema, ReferenceBiologyShape, EvidenceConflictsSchema, EvidenceTimelineSchema, TranslationalChainSchema, AdditionalEvidenceSchema, DiscoveryTrialsSchema, QualityGateStatusSchema, QualityGatesSchema, DerivedSchema, DossierSchema, DossierBodySchema, isDossier, dossierJsonSchema, SECTION_BLURBS, TargetAssessmentPhaseSchema, TargetAssessmentProgressEventSchema, } from "./target-dossier.js";
7
7
  export { TargetAssessmentStatusSchema, TargetAssessmentErrorSchema, TargetAssessmentListRowSchema, TargetAssessmentRowSchema, } from "./target-assessment-row.js";
8
- export { buildPreviewUrl, previewResourceId } from "./content-url.js";
8
+ export { buildPreviewUrl, buildReportSessionUrl, previewResourceId, reportSessionResourceId } from "./content-url.js";
package/dist/index.d.ts CHANGED
@@ -35,6 +35,9 @@ export type { ArtifactManifestEntry } from "./schemas/artifact-manifest.js";
35
35
  export type { ProvenanceCollector } from "./provenance/collector.js";
36
36
  export { UnavailablePreviewPublisher } from "./tools/report/preview-publisher.js";
37
37
  export type { PreviewPublisher, PreviewMintResult } from "./tools/report/preview-publisher.js";
38
+ export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
39
+ export type { MakeSessionPagePublisher, SessionPagePublisher, SessionPageMintResult } from "./tools/report-session/session-page-publisher.js";
40
+ export type { ResolvePageUrl, SessionPageAccess } from "./tools/report-session/index.js";
38
41
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
39
42
  export type { RunLauncher, LaunchOptions } from "./execution/run-launcher.js";
40
43
  export { createDbosWorkflowPurger } from "./execution/dbos-workflow-purger.js";
package/dist/index.js CHANGED
@@ -42,6 +42,13 @@ export { createNoopUsageRecorder } from "./billing/noop-usage-recorder.js";
42
42
  export { createNoopArtifactRegistry } from "./execution/noop-artifact-registry.js";
43
43
  // Seam: report preview publishing.
44
44
  export { UnavailablePreviewPublisher } from "./tools/report/preview-publisher.js";
45
+ // Seam: the hosted view of a report-session page. A managed host serves the URL space
46
+ // `report-sessions/{analysisId}/{threadId}` and mints access through its own publisher,
47
+ // thus the seam belongs on the front door. Both seams bind per call over the auth of the
48
+ // tool call, thus a host mints under the credential of the caller and holds no ambient
49
+ // state. The URL seam of the eyes rides beside the publisher factory: a host whose
50
+ // browser cannot reach the workspace tree binds a served URL for the look.
51
+ export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
45
52
  // Run launching.
46
53
  export { createDbosRunLauncher } from "./execution/dbos-run-launcher.js";
47
54
  // Seam: workflow-ledger reclamation — cancels and deletes an analysis's rows in
@@ -32,7 +32,7 @@ import { type CitationResolverConfig } from "../citations/resolve.js";
32
32
  import type { CitationResolver } from "../citations/types.js";
33
33
  import type { AuthContext } from "../auth/types.js";
34
34
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
35
- import type { ResolvePageAsset } from "../tools/report-session/index.js";
35
+ import type { MakeSessionPagePublisher, ResolvePageAsset, ResolvePageUrl } from "../tools/report-session/index.js";
36
36
  /** Registered child sandbox-step callable the parent's child dispatch closes over. */
37
37
  export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxStepResult>;
38
38
  /**
@@ -132,6 +132,22 @@ export interface CoreRuntimeDeps {
132
132
  * materializes them to disk and binds its own lookup here.
133
133
  */
134
134
  readonly resolveReportPageAsset?: ResolvePageAsset;
135
+ /**
136
+ * The hosted view of a report-session page. A managed host that serves the session URL
137
+ * space binds its publisher factory here, and the preview tool of the report agent then
138
+ * attaches the minted URL beside the page path. The factory binds one analysis and the
139
+ * auth of the tool call, like `makeReportReferenceResolver`, thus a host that mints under
140
+ * the credential of the caller binds it with no ambient state. Absent, the tool attaches
141
+ * nothing and the page path stays the whole result, which is the whole local path.
142
+ */
143
+ readonly makeSessionPagePublisher?: MakeSessionPagePublisher;
144
+ /**
145
+ * The page-URL formation of the report-session eyes. A managed host whose browser cannot
146
+ * reach the workspace tree binds a resolver that names a served URL for the look, and the
147
+ * args carry the auth of the tool call beside the page identity. Absent, the eyes tool
148
+ * navigates through a `file://` URL of the page path.
149
+ */
150
+ readonly resolveReportPageUrl?: ResolvePageUrl;
135
151
  /**
136
152
  * The eyes of a report session — where a browser comes from for one look at the rendered
137
153
  * page. A host with no standing sidecar starts a browser for one look. Thus it binds its own
@@ -164,6 +164,8 @@ export function assembleCoreRuntime(deps) {
164
164
  makeResolver: makeReportResolver,
165
165
  ...(eyes ? { eyes } : {}),
166
166
  ...(deps.resolveReportPageAsset ? { resolvePageAsset: deps.resolveReportPageAsset } : {}),
167
+ ...(deps.makeSessionPagePublisher ? { makeSessionPages: deps.makeSessionPagePublisher } : {}),
168
+ ...(deps.resolveReportPageUrl ? { resolvePageUrl: deps.resolveReportPageUrl } : {}),
167
169
  ...(conversation.logger ? { logger: conversation.logger } : {}),
168
170
  });
169
171
  // The single registration point for every typed agent. A future thread type's
@@ -5,16 +5,18 @@
5
5
  * see: the screenshot, the console errors, and the failed requests. The agent reads the picture and the
6
6
  * faults, decides, and repairs. The tool never judges, thus it never blocks the loop.
7
7
  *
8
- * The session tree has no URL space, thus the tool navigates to the page file through a `file://` URL. The
9
- * page lives at `report-sessions/{threadId}/index.html` under the workspace root, which the preview writes.
10
- * A missed page means that no preview ran, and it is a typed outcome, not a throw.
8
+ * The composition says which URL one look opens, and the URL seam is that answer. Absent the seam, the tool
9
+ * navigates to the page file through a `file://` URL. The page lives at `report-sessions/{threadId}/index.html`
10
+ * under the workspace root, which the preview writes. A missed page means that no preview ran, and it is a
11
+ * typed outcome, not a throw.
11
12
  *
12
13
  * The composition says where a browser comes from, and the eyes seam is that answer. One look acquires one
13
14
  * lease, and the tool captures against the endpoint of that lease.
14
15
  *
15
- * The `file://` URL resolves on the filesystem of the browser, because the connection is out of process.
16
- * Thus the browser of a lease must hold the workspace tree of the harness host at the same path. A browser
17
- * with no such mount reports the page as an unreachable request, and the tool gives back that fault.
16
+ * The default `file://` URL resolves on the filesystem of the browser, because the connection is out of
17
+ * process. Thus, absent the URL seam, the browser of a lease must hold the workspace tree of the harness host
18
+ * at the same path. A browser with no such mount reports the page as an unreachable request, and the tool
19
+ * gives back that fault. A bound URL seam lifts that constraint, because the browser then opens a served URL.
18
20
  *
19
21
  * The tool releases the lease after the look. The release runs on a pass and on a failed capture alike. That
20
22
  * release is hygiene, and it is not the guarantee against a leak. A process can die between the acquire and
@@ -43,6 +45,7 @@
43
45
  * and no outcome would ever arrive.
44
46
  */
45
47
  import { z } from "zod";
48
+ import type { AuthContext } from "../../auth/types.js";
46
49
  import { type ChromeConfig } from "../../lib/chrome.js";
47
50
  import { type AcquireEyes } from "../../lib/eyes.js";
48
51
  import { type Logger } from "../../lib/logger.js";
@@ -51,6 +54,18 @@ import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
51
54
  import { type Tool } from "../define-tool.js";
52
55
  import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
53
56
  export type { CapturePage, FailedRequest, PageCapture };
57
+ /**
58
+ * The URL formation of one look. It maps the page of the thread onto the URL that the browser opens.
59
+ * A composition whose browser cannot reach the workspace tree binds a resolver that names a served URL.
60
+ * The args carry the auth of the tool call beside the page identity, thus a realization mints the URL
61
+ * under the credential of the caller and holds no ambient state.
62
+ */
63
+ export type ResolvePageUrl = (args: {
64
+ pagePath: string;
65
+ analysisId: string;
66
+ threadId: string;
67
+ auth: AuthContext;
68
+ }) => Promise<string>;
54
69
  /** The empty input. The tool examines the current page of the thread, thus it needs no field. */
55
70
  declare const examinePageInput: z.ZodObject<{}, z.core.$strip>;
56
71
  export type ExaminePageInput = z.infer<typeof examinePageInput>;
@@ -90,15 +105,17 @@ export type ExaminePageResult = {
90
105
  * `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
91
106
  * serves every analysis. `eyes` gives a browser for one look, and the scope of the acquire carries that
92
107
  * analysis and its root. `chrome` carries the connection settings of the capture, and the browser that it
93
- * names must hold the workspace tree at the same path, because the tool navigates to a `file://` URL.
94
- * `capture` is optional and it replaces the whole transport, thus a test injects a seam that reads no
95
- * browser.
108
+ * names must hold the workspace tree at the same path when the tool navigates to a `file://` URL.
109
+ * `resolvePageUrl` is optional and it replaces that URL, thus a composition whose browser cannot reach the
110
+ * tree names a served URL instead. `capture` is optional and it replaces the whole transport, thus a test
111
+ * injects a seam that reads no browser.
96
112
  */
97
113
  export interface ExaminePageToolDeps {
98
114
  readonly gateway: ReportSessionStateGateway;
99
115
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
100
116
  readonly chrome: ChromeConfig;
101
117
  readonly eyes?: AcquireEyes;
118
+ readonly resolvePageUrl?: ResolvePageUrl;
102
119
  readonly capture?: CapturePage;
103
120
  readonly logger?: Logger;
104
121
  /**
@@ -5,16 +5,18 @@
5
5
  * see: the screenshot, the console errors, and the failed requests. The agent reads the picture and the
6
6
  * faults, decides, and repairs. The tool never judges, thus it never blocks the loop.
7
7
  *
8
- * The session tree has no URL space, thus the tool navigates to the page file through a `file://` URL. The
9
- * page lives at `report-sessions/{threadId}/index.html` under the workspace root, which the preview writes.
10
- * A missed page means that no preview ran, and it is a typed outcome, not a throw.
8
+ * The composition says which URL one look opens, and the URL seam is that answer. Absent the seam, the tool
9
+ * navigates to the page file through a `file://` URL. The page lives at `report-sessions/{threadId}/index.html`
10
+ * under the workspace root, which the preview writes. A missed page means that no preview ran, and it is a
11
+ * typed outcome, not a throw.
11
12
  *
12
13
  * The composition says where a browser comes from, and the eyes seam is that answer. One look acquires one
13
14
  * lease, and the tool captures against the endpoint of that lease.
14
15
  *
15
- * The `file://` URL resolves on the filesystem of the browser, because the connection is out of process.
16
- * Thus the browser of a lease must hold the workspace tree of the harness host at the same path. A browser
17
- * with no such mount reports the page as an unreachable request, and the tool gives back that fault.
16
+ * The default `file://` URL resolves on the filesystem of the browser, because the connection is out of
17
+ * process. Thus, absent the URL seam, the browser of a lease must hold the workspace tree of the harness host
18
+ * at the same path. A browser with no such mount reports the page as an unreachable request, and the tool
19
+ * gives back that fault. A bound URL seam lifts that constraint, because the browser then opens a served URL.
18
20
  *
19
21
  * The tool releases the lease after the look. The release runs on a pass and on a failed capture alike. That
20
22
  * release is hygiene, and it is not the guarantee against a leak. A process can die between the acquire and
@@ -270,13 +272,26 @@ export function createExaminePageTool(deps) {
270
272
  logger.warn("the session page could not be read", { threadId, analysisId, ...defaultErrorFields(cause) });
271
273
  return ok({ outcome: "capture-failed", detail: "the session page could not be read" });
272
274
  }
275
+ // The URL seam speaks the throw protocol like the other seams, thus a fault of the formation
276
+ // becomes a typed outcome and the look never starts.
277
+ let url;
278
+ try {
279
+ url =
280
+ deps.resolvePageUrl !== undefined
281
+ ? await deps.resolvePageUrl({ pagePath, analysisId, threadId, auth: ctx.session.auth })
282
+ : pathToFileURL(pagePath).href;
283
+ }
284
+ catch (cause) {
285
+ logger.warn("the page URL did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
286
+ return ok({ outcome: "capture-failed", detail: "the page URL did not resolve" });
287
+ }
273
288
  // The scope of the acquire carries the analysis and the root that this call resolved. Thus a
274
289
  // realization that starts a browser mounts the same tree, and it holds no second resolver.
275
290
  const looked = await runLook({
276
291
  transport,
277
292
  chrome: deps.chrome,
278
293
  scope: { analysisId, workspaceRoot: root },
279
- url: pathToFileURL(pagePath).href,
294
+ url,
280
295
  acquireMs,
281
296
  releaseMs,
282
297
  logger,
@@ -5,8 +5,9 @@
5
5
  * render seams, thus each tool binds the per-session state to the thread of the call. The barrel exports
6
6
  * the factories and their public types for the composition root.
7
7
  */
8
- export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, } from "./preview-report.js";
9
- export { createExaminePageTool, type CapturePage, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, } from "./examine-page.js";
8
+ export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, type SessionPageAccess, } from "./preview-report.js";
9
+ export { createExaminePageTool, type CapturePage, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, type ResolvePageUrl, } from "./examine-page.js";
10
+ export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type MakeSessionPagePublisher, type SessionPageMintFailure, type SessionPageMintResult, type SessionPagePublisher, } from "./session-page-publisher.js";
10
11
  export { createRecordVersionTool, type RecordVersionInput, type RecordVersionResult, type RecordVersionToolDeps } from "./record-version.js";
11
12
  export { createDeriveTableTool, type DeriveTableInput, type DeriveTableResult, type DeriveTableToolDeps } from "./derive-table.js";
12
13
  export { createListPinnedArtifactsTool, type ListPinnedArtifactsInput, type ListPinnedArtifactsResult, type ListPinnedArtifactsToolDeps, type PinnedArtifact, type PinnedCitation, } from "./list-artifacts.js";
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export { createPreviewReportTool, } from "./preview-report.js";
9
9
  export { createExaminePageTool, } from "./examine-page.js";
10
+ export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, } from "./session-page-publisher.js";
10
11
  export { createRecordVersionTool } from "./record-version.js";
11
12
  export { createDeriveTableTool } from "./derive-table.js";
12
13
  export { createListPinnedArtifactsTool, } from "./list-artifacts.js";
@@ -16,9 +16,12 @@
16
16
  * namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
17
17
  * trees. The result carries the absolute page path, thus a local host shows the page with no seam.
18
18
  *
19
- * The result carries no access grant. `PreviewPublisher` authorizes the URL space
20
- * `previews/{analysisId}/{previewId}` (`contracts/content-url.ts`), and that space cannot name a page of
21
- * this tree. A hosted view of a session page is a later capability, with a URL space of its own.
19
+ * A hosted view rides the same result. When the composition binds the session-page factory, the tool
20
+ * builds the publisher over the scope of the call, mints one grant after the page lands, and the
21
+ * `rendered` arm carries the URL of the URL space `report-sessions/{analysisId}/{threadId}`
22
+ * (`contracts/content-url.ts`) beside the path. A refused mint rides the arm as data, and the page path
23
+ * stays good — a broken grant surface never costs the render. An unbound factory changes nothing: the
24
+ * arm carries no access field, and a local host opens the file.
22
25
  *
23
26
  * Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
24
27
  * absence, an unresolvable root at resolver construction, an unresolved reference, a bridge mismatch, a
@@ -41,12 +44,27 @@ import { type BridgeMismatch } from "../../report-render/value-bridge.js";
41
44
  import type { RenderProblem } from "../../report-render/types.js";
42
45
  import { type Tool } from "../define-tool.js";
43
46
  import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
47
+ import { type MakeSessionPagePublisher } from "./session-page-publisher.js";
44
48
  /** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
45
49
  declare const previewReportInput: z.ZodObject<{}, z.core.$strip>;
46
50
  export type PreviewReportInput = z.infer<typeof previewReportInput>;
51
+ /**
52
+ * The access grant of the hosted view, as data on the `rendered` arm. A granted mint carries the URL of
53
+ * the served page and its expiry. A refused mint carries the described refusal, and the page path stays
54
+ * good.
55
+ */
56
+ export type SessionPageAccess = {
57
+ granted: true;
58
+ url: string;
59
+ expiresAt: string;
60
+ } | {
61
+ granted: false;
62
+ detail: string;
63
+ };
47
64
  /**
48
65
  * The typed outcome of the preview tool. Each arm is ok-channel data, thus the tool never throws for a
49
- * degraded condition. `rendered` carries the absolute page path.
66
+ * degraded condition. `rendered` carries the absolute page path, and the access grant of the hosted view
67
+ * when the composition binds the publisher.
50
68
  */
51
69
  export type PreviewReportResult = {
52
70
  outcome: "refused";
@@ -82,6 +100,7 @@ export type PreviewReportResult = {
82
100
  } | {
83
101
  outcome: "rendered";
84
102
  pagePath: string;
103
+ access?: SessionPageAccess;
85
104
  };
86
105
  /**
87
106
  * The lookup of the source file of one staged asset. It maps the module specifier of a manifest entry
@@ -99,6 +118,12 @@ export type ResolvePageAsset = (specifier: string) => string;
99
118
  * `resolvePageAsset` is optional, and absent it resolves each specifier against the installation of the
100
119
  * harness. An embedder that ships the asset bytes packed, for example a compiled single-file binary with no
101
120
  * `node_modules` tree, materializes them to disk and binds its own lookup here.
121
+ *
122
+ * `makeSessionPages` is optional, because a local host opens the page path itself. It binds one analysis
123
+ * and the auth of the call, like `makeResolver`, thus a realization mints under the credential of the
124
+ * caller. Bound, the tool builds the publisher over the scope of the call, mints one grant after the page
125
+ * lands, and attaches the URL beside the path. A refused mint rides the `rendered` arm as data, and it
126
+ * never fails the render.
102
127
  */
103
128
  export interface PreviewReportToolDeps {
104
129
  readonly gateway: ReportSessionStateGateway;
@@ -108,6 +133,7 @@ export interface PreviewReportToolDeps {
108
133
  }) => ReferenceResolver;
109
134
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
110
135
  readonly resolvePageAsset?: ResolvePageAsset;
136
+ readonly makeSessionPages?: MakeSessionPagePublisher;
111
137
  readonly logger?: Logger;
112
138
  }
113
139
  /**
@@ -16,9 +16,12 @@
16
16
  * namespace belongs to this path alone, thus the tool never writes under the old `previews/` or `reports/`
17
17
  * trees. The result carries the absolute page path, thus a local host shows the page with no seam.
18
18
  *
19
- * The result carries no access grant. `PreviewPublisher` authorizes the URL space
20
- * `previews/{analysisId}/{previewId}` (`contracts/content-url.ts`), and that space cannot name a page of
21
- * this tree. A hosted view of a session page is a later capability, with a URL space of its own.
19
+ * A hosted view rides the same result. When the composition binds the session-page factory, the tool
20
+ * builds the publisher over the scope of the call, mints one grant after the page lands, and the
21
+ * `rendered` arm carries the URL of the URL space `report-sessions/{analysisId}/{threadId}`
22
+ * (`contracts/content-url.ts`) beside the path. A refused mint rides the arm as data, and the page path
23
+ * stays good — a broken grant surface never costs the render. An unbound factory changes nothing: the
24
+ * arm carries no access field, and a local host opens the file.
22
25
  *
23
26
  * Each degraded condition is a typed outcome in the ok channel: a session refusal, a gap list, a resolver
24
27
  * absence, an unresolvable root at resolver construction, an unresolved reference, a bridge mismatch, a
@@ -36,6 +39,7 @@ import { extname, join } from "node:path";
36
39
  import { relative as relativePosix } from "node:path/posix";
37
40
  import { z } from "zod";
38
41
  import { reportSessionDir, resolveWorkspacePath } from "../../workspace/paths.js";
42
+ import { buildReportSessionUrl } from "../../contracts/content-url.js";
39
43
  import { createNoopLogger } from "../../lib/console-logger.js";
40
44
  import { describeFsError, tryFsWrite } from "../../lib/fs-result.js";
41
45
  import { defaultErrorFields } from "../../lib/logger.js";
@@ -49,6 +53,7 @@ import { ASSETS_DIR, DEPS_DIR, derivationScriptName, PAGE_ASSETS, stagedSource,
49
53
  import { renderReportPage } from "../../report-render/render.js";
50
54
  import { defineTool } from "../define-tool.js";
51
55
  import { openReportThread } from "../report-authoring/authoring-tools.js";
56
+ import { describeSessionPageMintFailure } from "./session-page-publisher.js";
52
57
  /** The empty input. The tool renders the current draft of the thread, thus it needs no field. */
53
58
  const previewReportInput = z.object({});
54
59
  /**
@@ -335,6 +340,38 @@ async function sweepAssets(assetsDir, staged, logger) {
335
340
  }
336
341
  }
337
342
  }
343
+ /**
344
+ * Mint the hosted view of the page that landed.
345
+ *
346
+ * An absent factory gives no grant, and the arm carries no access field — the page path stays the whole
347
+ * local contract. The tool builds the publisher over the scope of the call, thus the mint runs under the
348
+ * auth of the caller. A refusal, a thrown construction, and a thrown realization each become the
349
+ * not-granted arm, thus a broken grant surface never costs the render. The URL spells through
350
+ * `buildReportSessionUrl`, thus the formula lives in the contract and the seam gives the content-server
351
+ * base alone.
352
+ */
353
+ async function mintAccess(makeSessionPages, analysisId, threadId, auth, logger) {
354
+ if (makeSessionPages === undefined) {
355
+ return undefined;
356
+ }
357
+ let minted;
358
+ try {
359
+ minted = await makeSessionPages({ analysisId, auth }).mintSessionPageAccess(threadId);
360
+ }
361
+ catch (cause) {
362
+ logger.warn("the session-page mint threw", { threadId, analysisId, ...defaultErrorFields(cause) });
363
+ return { granted: false, detail: "session-page-access mint failed" };
364
+ }
365
+ if (!minted.ok) {
366
+ logger.warn("the session-page mint refused", { threadId, analysisId, detail: describeSessionPageMintFailure(minted) });
367
+ return { granted: false, detail: describeSessionPageMintFailure(minted) };
368
+ }
369
+ return {
370
+ granted: true,
371
+ url: buildReportSessionUrl(minted.data.baseUrl, analysisId, threadId, "index.html", minted.data.token),
372
+ expiresAt: minted.data.expiresAt,
373
+ };
374
+ }
338
375
  /**
339
376
  * Make the render-and-preview tool over the session-state gateway and the render seams.
340
377
  *
@@ -438,7 +475,8 @@ export function createPreviewReportTool(deps) {
438
475
  logger.warn("the rendered hash did not stamp", { threadId, analysisId, detail });
439
476
  return ok({ outcome: "stamp-failed", pagePath: written.value, detail });
440
477
  }
441
- return ok({ outcome: "rendered", pagePath: written.value });
478
+ const access = await mintAccess(deps.makeSessionPages, analysisId, threadId, ctx.session.auth, logger);
479
+ return ok({ outcome: "rendered", pagePath: written.value, ...(access !== undefined ? { access } : {}) });
442
480
  },
443
481
  });
444
482
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * SessionPagePublisher — the seam between the report-session path and the hosted view of its page.
3
+ *
4
+ * The preview tool writes the page under the session directory and gives back the path. A local host
5
+ * opens the file, thus it needs no seam. A managed host serves the page over HTTP, and the one thing
6
+ * that reaches outside the process is the mint of a content-token grant for the URL space
7
+ * `report-sessions/{analysisId}/{threadId}` (`contracts/content-url.ts`). That mint hides behind this
8
+ * one method.
9
+ *
10
+ * The managed realization owns the grant mint and its client. A mint runs under the credential of the
11
+ * tool call, thus the composition binds a factory and the preview tool builds the publisher over the
12
+ * scope of the call — exactly as it builds the reference resolver. A boot-time singleton could not
13
+ * carry that credential, and a host that bans ambient state could not bind one at all. The local
14
+ * default (`UnavailableSessionPagePublisher`) returns not-ok, thus the preview tool attaches no URL
15
+ * and the render still lands — the page path stays the whole local contract.
16
+ */
17
+ import type { AuthContext } from "../../auth/types.js";
18
+ export type SessionPageMintResult = {
19
+ ok: true;
20
+ data: {
21
+ baseUrl: string;
22
+ token: string;
23
+ expiresAt: string;
24
+ };
25
+ } | {
26
+ ok: false;
27
+ status?: number;
28
+ error: {
29
+ message?: string;
30
+ };
31
+ };
32
+ /** The not-ok arm of the seam's result — what a failed mint is allowed to carry. */
33
+ export type SessionPageMintFailure = Extract<SessionPageMintResult, {
34
+ ok: false;
35
+ }>;
36
+ /**
37
+ * Renders a failed mint as the line an agent reads back from the preview tool.
38
+ *
39
+ * The failure shape is sparse by design: a realization with no HTTP transport behind it supplies
40
+ * neither a status nor, necessarily, a message. Naming a field the seam left unset would put the
41
+ * literal `status=undefined` in front of the model, thus the line carries only what actually arrived.
42
+ * It lives beside the type because its whole job is to honour that type's optionality.
43
+ */
44
+ export declare function describeSessionPageMintFailure(failure: SessionPageMintFailure): string;
45
+ export interface SessionPagePublisher {
46
+ /**
47
+ * Mint the access grant of one session page. The publisher binds the analysis, thus the mint takes
48
+ * the thread id alone. `baseUrl` is the base URL of the content server, with no res path — the
49
+ * caller spells the whole URL through `buildReportSessionUrl`, thus the formula lives in the
50
+ * contract and never in a realization.
51
+ */
52
+ mintSessionPageAccess(threadId: string): Promise<SessionPageMintResult>;
53
+ }
54
+ /**
55
+ * The per-call construction of the publisher. The factory binds one analysis and the auth of the tool
56
+ * call, thus a realization mints under the credential of the caller and holds no ambient state.
57
+ */
58
+ export type MakeSessionPagePublisher = (scope: {
59
+ analysisId: string;
60
+ auth: AuthContext;
61
+ }) => SessionPagePublisher;
62
+ /**
63
+ * Local default — no hosted view of a session page. Returns not-ok, thus the preview tool carries the
64
+ * refusal as data and the page path stays the whole result.
65
+ */
66
+ export declare class UnavailableSessionPagePublisher implements SessionPagePublisher {
67
+ mintSessionPageAccess(_threadId: string): Promise<SessionPageMintResult>;
68
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SessionPagePublisher — the seam between the report-session path and the hosted view of its page.
3
+ *
4
+ * The preview tool writes the page under the session directory and gives back the path. A local host
5
+ * opens the file, thus it needs no seam. A managed host serves the page over HTTP, and the one thing
6
+ * that reaches outside the process is the mint of a content-token grant for the URL space
7
+ * `report-sessions/{analysisId}/{threadId}` (`contracts/content-url.ts`). That mint hides behind this
8
+ * one method.
9
+ *
10
+ * The managed realization owns the grant mint and its client. A mint runs under the credential of the
11
+ * tool call, thus the composition binds a factory and the preview tool builds the publisher over the
12
+ * scope of the call — exactly as it builds the reference resolver. A boot-time singleton could not
13
+ * carry that credential, and a host that bans ambient state could not bind one at all. The local
14
+ * default (`UnavailableSessionPagePublisher`) returns not-ok, thus the preview tool attaches no URL
15
+ * and the render still lands — the page path stays the whole local contract.
16
+ */
17
+ /**
18
+ * Renders a failed mint as the line an agent reads back from the preview tool.
19
+ *
20
+ * The failure shape is sparse by design: a realization with no HTTP transport behind it supplies
21
+ * neither a status nor, necessarily, a message. Naming a field the seam left unset would put the
22
+ * literal `status=undefined` in front of the model, thus the line carries only what actually arrived.
23
+ * It lives beside the type because its whole job is to honour that type's optionality.
24
+ */
25
+ export function describeSessionPageMintFailure(failure) {
26
+ const detail = [];
27
+ if (failure.status !== undefined)
28
+ detail.push(`status=${failure.status}`);
29
+ const message = failure.error.message?.trim();
30
+ if (message)
31
+ detail.push(message);
32
+ return detail.length > 0 ? `session-page-access mint failed: ${detail.join(" ")}` : "session-page-access mint failed";
33
+ }
34
+ /**
35
+ * Local default — no hosted view of a session page. Returns not-ok, thus the preview tool carries the
36
+ * refusal as data and the page path stays the whole result.
37
+ */
38
+ export class UnavailableSessionPagePublisher {
39
+ async mintSessionPageAccess(_threadId) {
40
+ return {
41
+ ok: false,
42
+ error: { message: "the hosted view of a session page is unavailable in this environment" },
43
+ };
44
+ }
45
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.21.1",
4
- "description": "Host-agnostic bioinformatics agent harness \u2014 hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
3
+ "version": "0.22.1",
4
+ "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",