@inflexa-ai/harness 0.31.1 → 0.33.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.
Files changed (96) hide show
  1. package/dist/agents/conversation-agent.d.ts +15 -0
  2. package/dist/agents/conversation-agent.js +16 -6
  3. package/dist/agents/report-session-agent.d.ts +9 -0
  4. package/dist/agents/report-session-agent.js +10 -2
  5. package/dist/agents/sandbox/shared.d.ts +13 -1
  6. package/dist/agents/sandbox/shared.js +22 -5
  7. package/dist/app/chat-turn.d.ts +11 -0
  8. package/dist/app/chat-turn.js +17 -1
  9. package/dist/app/spawn-report-session.d.ts +12 -0
  10. package/dist/app/spawn-report-session.js +21 -1
  11. package/dist/config/environment-stores.d.ts +53 -5
  12. package/dist/contracts/report-blocks.d.ts +8 -8
  13. package/dist/contracts/schemas/chat-parts.d.ts +6 -6
  14. package/dist/index.d.ts +8 -1
  15. package/dist/index.js +11 -0
  16. package/dist/prompts/briefing.d.ts +1 -1
  17. package/dist/prompts/briefing.js +4 -0
  18. package/dist/prompts/planner.js +13 -0
  19. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  20. package/dist/prompts/sandbox/multi-omics-integration-agent.js +5 -5
  21. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  22. package/dist/prompts/sandbox/network-agent.js +4 -3
  23. package/dist/prompts/sandbox-standards.d.ts +20 -0
  24. package/dist/prompts/sandbox-standards.js +65 -0
  25. package/dist/provenance/seam.d.ts +203 -0
  26. package/dist/provenance/seam.js +71 -0
  27. package/dist/reference-data/catalog.js +11 -9
  28. package/dist/report-model/authoring-grammar.d.ts +2 -2
  29. package/dist/report-render/assets.d.ts +10 -0
  30. package/dist/report-render/assets.js +14 -0
  31. package/dist/report-render/design.js +256 -0
  32. package/dist/report-render/page.d.ts +75 -0
  33. package/dist/report-render/page.js +793 -1
  34. package/dist/report-render/provenance-data.d.ts +42 -0
  35. package/dist/report-render/provenance-data.js +62 -0
  36. package/dist/report-render/render.d.ts +20 -1
  37. package/dist/report-render/render.js +23 -10
  38. package/dist/report-render/types.d.ts +3 -0
  39. package/dist/report-render/views/chart-view.d.ts +5 -1
  40. package/dist/report-render/views/chart-view.js +6 -2
  41. package/dist/report-render/views/lineage.d.ts +93 -0
  42. package/dist/report-render/views/lineage.js +86 -0
  43. package/dist/report-render/views/page-view.d.ts +22 -1
  44. package/dist/report-render/views/page-view.js +12 -3
  45. package/dist/report-render/views/prose.d.ts +7 -1
  46. package/dist/report-render/views/prose.js +10 -6
  47. package/dist/report-render/views/references-view.d.ts +17 -1
  48. package/dist/report-render/views/references-view.js +12 -4
  49. package/dist/report-render/views/values.d.ts +17 -4
  50. package/dist/report-render/views/values.js +21 -8
  51. package/dist/runtime/assemble.d.ts +22 -0
  52. package/dist/runtime/assemble.js +4 -0
  53. package/dist/sandbox/create-sandbox.d.ts +20 -4
  54. package/dist/sandbox/create-sandbox.js +4 -0
  55. package/dist/sandbox/docker-client.d.ts +16 -6
  56. package/dist/sandbox/docker-client.js +47 -31
  57. package/dist/sandbox/farm.d.ts +95 -0
  58. package/dist/sandbox/farm.js +127 -0
  59. package/dist/sandbox/k8s-client.d.ts +24 -10
  60. package/dist/sandbox/k8s-client.js +85 -122
  61. package/dist/sandbox/mount-plan.d.ts +23 -1
  62. package/dist/sandbox/mount-plan.js +44 -7
  63. package/dist/sandbox/sandbox-error.d.ts +12 -0
  64. package/dist/sandbox/sandbox-error.js +2 -0
  65. package/dist/sandbox/types.d.ts +118 -0
  66. package/dist/schemas/artifact-manifest.d.ts +4 -4
  67. package/dist/schemas/plan-schemas.d.ts +3 -0
  68. package/dist/schemas/plan-schemas.js +1 -1
  69. package/dist/schemas/validate-plan.js +20 -1
  70. package/dist/schemas/workflow-state.d.ts +4 -0
  71. package/dist/schemas/workflow-state.js +10 -0
  72. package/dist/tasks/data-profile.d.ts +15 -0
  73. package/dist/tasks/data-profile.js +36 -26
  74. package/dist/tools/display/show-user.d.ts +1 -1
  75. package/dist/tools/execute-analysis.d.ts +18 -0
  76. package/dist/tools/execute-analysis.js +87 -0
  77. package/dist/tools/report-authoring/authoring-tools.d.ts +16 -1
  78. package/dist/tools/report-authoring/authoring-tools.js +58 -16
  79. package/dist/tools/report-session/derive-table.d.ts +3 -0
  80. package/dist/tools/report-session/derive-table.js +15 -0
  81. package/dist/tools/report-session/preview-report.d.ts +13 -4
  82. package/dist/tools/report-session/preview-report.js +23 -6
  83. package/dist/tools/report-session/record-version.d.ts +3 -0
  84. package/dist/tools/report-session/record-version.js +7 -3
  85. package/dist/tools/research/generate-plan.js +12 -2
  86. package/dist/tools/sandbox/index.d.ts +1 -0
  87. package/dist/tools/sandbox/index.js +1 -0
  88. package/dist/tools/sandbox/link-packages.d.ts +27 -0
  89. package/dist/tools/sandbox/link-packages.js +52 -0
  90. package/dist/tools/sandbox/list-available-packages.d.ts +50 -30
  91. package/dist/tools/sandbox/list-available-packages.js +131 -40
  92. package/dist/tools/start-report-session.d.ts +7 -0
  93. package/dist/tools/start-report-session.js +1 -0
  94. package/dist/workflows/execute-analysis.d.ts +15 -69
  95. package/dist/workflows/execute-analysis.js +5 -4
  96. package/package.json +2 -1
@@ -16,10 +16,26 @@ import { type DerivationChains, type ReferenceLedger } from "../references.js";
16
16
  *
17
17
  * A bracket reads as a reference inside prose, and it stays a span beside the words. One notation serves
18
18
  * every kind, thus one list answers every marker.
19
+ *
20
+ * Every grounded kind renders its marker here. Thus the lineage control has one emission point, and a
21
+ * marker that names no place carries no control. The page shows the control only where a stamp gives the
22
+ * script a key to walk.
19
23
  */
20
- export declare function Marker({ n }: {
24
+ export declare function Marker({ n, lineage }: {
21
25
  n: number;
26
+ lineage?: number;
22
27
  }): import("hono/jsx/jsx-dev-runtime").JSX.Element;
28
+ /**
29
+ * The count of hash characters that a chain line shows.
30
+ *
31
+ * A content hash is 64 hex characters, and a line that carries three whole ones reads as noise. The head
32
+ * identifies the bytes, and it matches the count that a staged file name carries. Thus a reader compares the
33
+ * two by sight.
34
+ *
35
+ * The appendix line and the hop of the lineage popover both cut a hash to this count. Thus one hash reads
36
+ * the same in the two places.
37
+ */
38
+ export declare const CHAIN_HASH_CHARS = 12;
23
39
  /**
24
40
  * The one appendix list: one entry for each distinct reference of the page, in the order of the ladder.
25
41
  *
@@ -1,14 +1,19 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
2
2
  import { citationRecordOf } from "../../report-model/reference-resolver.js";
3
3
  import { citationKeyOf } from "../references.js";
4
+ import { LineageControl } from "./lineage.js";
4
5
  /**
5
6
  * One evidence marker as a bracket number that links to its appendix entry.
6
7
  *
7
8
  * A bracket reads as a reference inside prose, and it stays a span beside the words. One notation serves
8
9
  * every kind, thus one list answers every marker.
10
+ *
11
+ * Every grounded kind renders its marker here. Thus the lineage control has one emission point, and a
12
+ * marker that names no place carries no control. The page shows the control only where a stamp gives the
13
+ * script a key to walk.
9
14
  */
10
- export function Marker({ n }) {
11
- return (_jsx("span", { class: "report-marker", children: _jsx("a", { href: `#ref-${n}`, children: `[${n}]` }) }));
15
+ export function Marker({ n, lineage }) {
16
+ return (_jsxs("span", { class: "report-marker", children: [_jsx("a", { href: `#ref-${n}`, children: `[${n}]` }), lineage !== undefined ? _jsx(LineageControl, { place: lineage }) : null] }));
12
17
  }
13
18
  /** The kind label of one reference entry. */
14
19
  function RefKind({ children }) {
@@ -32,8 +37,11 @@ function HashCode({ children }) {
32
37
  * A content hash is 64 hex characters, and a line that carries three whole ones reads as noise. The head
33
38
  * identifies the bytes, and it matches the count that a staged file name carries. Thus a reader compares the
34
39
  * two by sight.
40
+ *
41
+ * The appendix line and the hop of the lineage popover both cut a hash to this count. Thus one hash reads
42
+ * the same in the two places.
35
43
  */
36
- const CHAIN_HASH_CHARS = 12;
44
+ export const CHAIN_HASH_CHARS = 12;
37
45
  /** The head of one content hash: the hex after the algorithm name, cut to the shown length. */
38
46
  function hashHead(hash) {
39
47
  return hash.slice(hash.lastIndexOf(":") + 1).slice(0, CHAIN_HASH_CHARS);
@@ -26,6 +26,7 @@
26
26
  import type { CitationBlock, FigureBlock, MetricBlock, TableBlock } from "../../contracts/report-blocks.js";
27
27
  import { type ArtifactTableReference } from "../../contracts/report-reference.js";
28
28
  import type { CitationRecord } from "../../report-model/reference-resolver.js";
29
+ import { type ViewOptions } from "./lineage.js";
29
30
  import { type ReferenceLedger } from "../references.js";
30
31
  import type { ColumnDisplay } from "../table-data.js";
31
32
  import type { RenderValue } from "../types.js";
@@ -76,8 +77,11 @@ type FigureValue = Extract<RenderValue, {
76
77
  * The value binding joins the reference ladder, thus the label line carries the marker and the appendix
77
78
  * names the cell. The marker sits on the label and never on the value, because the value line is the one
78
79
  * figure that a reader takes from the card.
80
+ *
81
+ * `view.lineage` states that the page carries a provenance document. The card then stamps its keys, and the
82
+ * marker carries the control that opens the chain.
79
83
  */
80
- export declare function renderMetric(block: MetricBlock, ledger: ReferenceLedger, value: ScalarValue): string;
84
+ export declare function renderMetric(block: MetricBlock, ledger: ReferenceLedger, value: ScalarValue, view?: ViewOptions): string;
81
85
  /**
82
86
  * Wrap a run of stat cards in one responsive grid. The caller renders each card first, and it passes the
83
87
  * markup as one already-escaped string, thus `raw()` inserts it byte for byte. The grid carries its own
@@ -141,8 +145,11 @@ export declare function tableStatusText(shown: number, total: number): string;
141
145
  * The whole-table binding joins the reference ladder, thus the title line carries the marker and the
142
146
  * appendix names the artifact. Every evidentiary block ledgers this way, and a card with no title still
143
147
  * shows its marker on the same line.
148
+ *
149
+ * `view.lineage` states that the page carries a provenance document. The card then stamps its keys, and the
150
+ * marker carries the control that opens the chain.
144
151
  */
145
- export declare function renderTable(block: TableBlock, ledger: ReferenceLedger, rowCount: number, total?: number): string;
152
+ export declare function renderTable(block: TableBlock, ledger: ReferenceLedger, rowCount: number, total?: number, view?: ViewOptions): string;
146
153
  /**
147
154
  * Render a figure block as a corner-accent card. The source rides the `src` attribute, and the caption
148
155
  * renders below the image. The caption also fills the `alt` attribute, thus the escape keeps a hostile
@@ -151,8 +158,11 @@ export declare function renderTable(block: TableBlock, ledger: ReferenceLedger,
151
158
  * The whole-file binding joins the reference ladder, thus the caption line carries the marker and the
152
159
  * appendix names the image. A figure with no caption still shows its marker on that line. The marker stays
153
160
  * out of the `alt` text, because a screen reader reads the alt as the picture and not as a footnote.
161
+ *
162
+ * `view.lineage` states that the page carries a provenance document. The card then stamps its keys, and the
163
+ * marker carries the control that opens the chain.
154
164
  */
155
- export declare function renderFigure(block: FigureBlock, ledger: ReferenceLedger, value: FigureValue): string;
165
+ export declare function renderFigure(block: FigureBlock, ledger: ReferenceLedger, value: FigureValue, view?: ViewOptions): string;
156
166
  /**
157
167
  * Render a citation block as a bibliography card. The binding joins the one reference ladder like a claim
158
168
  * binding, thus one shared source keeps one bracket number. The card shows the marker, the short citation
@@ -161,6 +171,9 @@ export declare function renderFigure(block: FigureBlock, ledger: ReferenceLedger
161
171
  * The short citation of a `pmid:` record carries the PubMed link of its identifier. A key of another
162
172
  * identifier space shows the citation as text. A key with no pinned record shows the key and the note
163
173
  * alone, because absence is a normal condition and no text stands in for the paper.
174
+ *
175
+ * `view.lineage` states that the page carries a provenance document. The card then stamps the identity of
176
+ * the external record in place of a pin, because a paper is no artifact and no data chain reaches it.
164
177
  */
165
- export declare function renderCitation(block: CitationBlock, ledger: ReferenceLedger, record?: CitationRecord): string;
178
+ export declare function renderCitation(block: CitationBlock, ledger: ReferenceLedger, record?: CitationRecord, view?: ViewOptions): string;
166
179
  export {};
@@ -27,6 +27,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-
27
27
  import { raw } from "hono/html";
28
28
  import { declaredForColumn } from "../../contracts/report-reference.js";
29
29
  import { stagedSource, tableSidecarName } from "../assets.js";
30
+ import { DEFAULT_VIEW_OPTIONS, lineagePlace, lineageStamp } from "./lineage.js";
30
31
  import { Marker } from "./references-view.js";
31
32
  import { formatNumberCell, formatTableCell, holdsANumber, selectColumnKind, selectNumberKind, smallestPositiveValue } from "../number-format.js";
32
33
  import { citationKeyOf } from "../references.js";
@@ -67,11 +68,14 @@ const DOWNLOAD_LABEL = "Download";
67
68
  * The value binding joins the reference ladder, thus the label line carries the marker and the appendix
68
69
  * names the cell. The marker sits on the label and never on the value, because the value line is the one
69
70
  * figure that a reader takes from the card.
71
+ *
72
+ * `view.lineage` states that the page carries a provenance document. The card then stamps its keys, and the
73
+ * marker carries the control that opens the chain.
70
74
  */
71
- export function renderMetric(block, ledger, value) {
75
+ export function renderMetric(block, ledger, value, view = DEFAULT_VIEW_OPTIONS) {
72
76
  const n = ledger.mark(block.value);
73
77
  const shown = formatNumberCell(value.value, selectNumberKind(block.label, value.value));
74
- return String(_jsxs("div", { class: "stat-card corner-accents", children: [_jsx("div", { class: "stat-card-value", title: shown.full, children: shown.text }), _jsxs("div", { class: "stat-card-label", children: [block.label, _jsx(Marker, { n: n })] })] }));
78
+ return String(_jsxs("div", { class: "stat-card corner-accents", ...lineageStamp(view.lineage, block.id, [block.value]), children: [_jsx("div", { class: "stat-card-value", title: shown.full, children: shown.text }), _jsxs("div", { class: "stat-card-label", children: [block.label, _jsx(Marker, { n: n, lineage: lineagePlace(view.lineage, block.value) })] })] }));
75
79
  }
76
80
  /**
77
81
  * Wrap a run of stat cards in one responsive grid. The caller renders each card first, and it passes the
@@ -199,13 +203,16 @@ export function tableStatusText(shown, total) {
199
203
  * The whole-table binding joins the reference ladder, thus the title line carries the marker and the
200
204
  * appendix names the artifact. Every evidentiary block ledgers this way, and a card with no title still
201
205
  * shows its marker on the same line.
206
+ *
207
+ * `view.lineage` states that the page carries a provenance document. The card then stamps its keys, and the
208
+ * marker carries the control that opens the chain.
202
209
  */
203
- export function renderTable(block, ledger, rowCount, total) {
210
+ export function renderTable(block, ledger, rowCount, total, view = DEFAULT_VIEW_OPTIONS) {
204
211
  const binding = block.binding;
205
212
  const n = ledger.mark(binding);
206
213
  const download = tableSidecarName(binding.hash, binding.path);
207
214
  const bound = boundText(binding);
208
- return String(_jsxs("div", { class: "report-table", children: [_jsxs("div", { class: "report-table-title", children: [block.title, _jsx(Marker, { n: n })] }), _jsxs("div", { class: "corner-accents", children: [_jsx("div", { class: "report-grid", [GRID_MOUNT_ATTRIBUTE]: block.id }), _jsxs("div", { class: "report-table-footer", children: [_jsxs("div", { class: "report-table-footer-row", children: [_jsxs("span", { class: "report-table-status", children: [_jsx("span", { class: GRID_COUNT_CLASS, children: tableStatusText(rowCount, total ?? rowCount) }), bound !== undefined ? _jsx("span", { class: "report-table-bound", children: bound }) : null] }), _jsx("a", { class: "report-table-download", href: stagedSource(download), download: download, children: downloadLabel(binding.path) })] }), _jsx("div", { class: GRID_NOTE_CLASS })] })] }), block.caption !== undefined ? _jsx("p", { class: "report-caption", children: block.caption }) : null] }));
215
+ return String(_jsxs("div", { class: "report-table", ...lineageStamp(view.lineage, block.id, [binding]), children: [_jsxs("div", { class: "report-table-title", children: [block.title, _jsx(Marker, { n: n, lineage: lineagePlace(view.lineage, binding) })] }), _jsxs("div", { class: "corner-accents", children: [_jsx("div", { class: "report-grid", [GRID_MOUNT_ATTRIBUTE]: block.id }), _jsxs("div", { class: "report-table-footer", children: [_jsxs("div", { class: "report-table-footer-row", children: [_jsxs("span", { class: "report-table-status", children: [_jsx("span", { class: GRID_COUNT_CLASS, children: tableStatusText(rowCount, total ?? rowCount) }), bound !== undefined ? _jsx("span", { class: "report-table-bound", children: bound }) : null] }), _jsx("a", { class: "report-table-download", href: stagedSource(download), download: download, children: downloadLabel(binding.path) })] }), _jsx("div", { class: GRID_NOTE_CLASS })] })] }), block.caption !== undefined ? _jsx("p", { class: "report-caption", children: block.caption }) : null] }));
209
216
  }
210
217
  /**
211
218
  * Render a figure block as a corner-accent card. The source rides the `src` attribute, and the caption
@@ -215,11 +222,14 @@ export function renderTable(block, ledger, rowCount, total) {
215
222
  * The whole-file binding joins the reference ladder, thus the caption line carries the marker and the
216
223
  * appendix names the image. A figure with no caption still shows its marker on that line. The marker stays
217
224
  * out of the `alt` text, because a screen reader reads the alt as the picture and not as a footnote.
225
+ *
226
+ * `view.lineage` states that the page carries a provenance document. The card then stamps its keys, and the
227
+ * marker carries the control that opens the chain.
218
228
  */
219
- export function renderFigure(block, ledger, value) {
229
+ export function renderFigure(block, ledger, value, view = DEFAULT_VIEW_OPTIONS) {
220
230
  const n = ledger.mark(block.binding);
221
231
  const caption = block.caption;
222
- return String(_jsxs("figure", { class: "report-figure corner-accents", children: [_jsx("img", { src: value.src, alt: caption !== undefined ? caption : "", class: "report-figure-image" }), _jsxs("figcaption", { class: "report-caption", children: [caption, _jsx(Marker, { n: n })] })] }));
232
+ return String(_jsxs("figure", { class: "report-figure corner-accents", ...lineageStamp(view.lineage, block.id, [block.binding]), children: [_jsx("img", { src: value.src, alt: caption !== undefined ? caption : "", class: "report-figure-image" }), _jsxs("figcaption", { class: "report-caption", children: [caption, _jsx(Marker, { n: n, lineage: lineagePlace(view.lineage, block.binding) })] })] }));
223
233
  }
224
234
  /**
225
235
  * The PubMed address of one identifier. The address is deterministic, and an anchor loads nothing, thus a
@@ -234,10 +244,13 @@ const PUBMED_BASE = "https://pubmed.ncbi.nlm.nih.gov/";
234
244
  * The short citation of a `pmid:` record carries the PubMed link of its identifier. A key of another
235
245
  * identifier space shows the citation as text. A key with no pinned record shows the key and the note
236
246
  * alone, because absence is a normal condition and no text stands in for the paper.
247
+ *
248
+ * `view.lineage` states that the page carries a provenance document. The card then stamps the identity of
249
+ * the external record in place of a pin, because a paper is no artifact and no data chain reaches it.
237
250
  */
238
- export function renderCitation(block, ledger, record) {
251
+ export function renderCitation(block, ledger, record, view = DEFAULT_VIEW_OPTIONS) {
239
252
  const n = ledger.mark(block.binding);
240
253
  const binding = block.binding;
241
254
  const key = citationKeyOf(binding);
242
- return String(_jsxs("div", { class: "report-citation corner-accents", children: [_jsx(Marker, { n: n }), record !== undefined ? (_jsxs(_Fragment, { children: [" ", binding.idKind === "pmid" ? (_jsx("a", { class: "report-citation-source", href: `${PUBMED_BASE}${encodeURIComponent(binding.id)}/`, children: record.citation })) : (_jsx("span", { class: "report-citation-source", children: record.citation }))] })) : null, block.note !== undefined ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { class: "report-citation-note", children: block.note })] })) : null, " ", _jsx("span", { class: "report-citation-key", children: key })] }));
255
+ return String(_jsxs("div", { class: "report-citation corner-accents", ...lineageStamp(view.lineage, block.id, [binding]), children: [_jsx(Marker, { n: n, lineage: lineagePlace(view.lineage, binding) }), record !== undefined ? (_jsxs(_Fragment, { children: [" ", binding.idKind === "pmid" ? (_jsx("a", { class: "report-citation-source", href: `${PUBMED_BASE}${encodeURIComponent(binding.id)}/`, children: record.citation })) : (_jsx("span", { class: "report-citation-source", children: record.citation }))] })) : null, block.note !== undefined ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { class: "report-citation-note", children: block.note })] })) : null, " ", _jsx("span", { class: "report-citation-key", children: key })] }));
243
256
  }
@@ -34,6 +34,7 @@ import { type CitationResolverConfig } from "../citations/resolve.js";
34
34
  import type { CitationResolver } from "../citations/types.js";
35
35
  import type { AuthContext } from "../auth/types.js";
36
36
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
37
+ import type { ProvenanceSeam } from "../provenance/seam.js";
37
38
  import type { MakeSessionPagePublisher, ResolvePageAsset, ResolvePageUrl } from "../tools/report-session/index.js";
38
39
  /** Registered child sandbox-step callable the parent's child dispatch closes over. */
39
40
  export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxStepResult>;
@@ -160,6 +161,27 @@ export interface CoreRuntimeDeps {
160
161
  * the eyes tool of a report session then reports that condition.
161
162
  */
162
163
  readonly eyes?: AcquireEyes;
164
+ /**
165
+ * The provenance seam. Each act of a session — the creation of the session, the block operations, the
166
+ * title, the derivation, the preview, and the record — rides the session emit as one typed event, and
167
+ * the embedder lands the events in the record of the analysis that it owns. The emit is
168
+ * fire-and-forget: it gives no result, a throw of the realization reaches the log alone, and an unbound
169
+ * member emits nothing. Thus a composition that binds none behaves the same as one that never had the
170
+ * seam.
171
+ *
172
+ * The document read gives the signed document of the analysis back, as opaque text. The preview of a
173
+ * report session carries a frozen copy into the page, and the page then shows the chain of a grounded
174
+ * block with no host and no network. The harness parses nothing. Absence is a normal result: an unbound
175
+ * member, and a bound member that holds no document for the analysis, both give a page with no
176
+ * provenance and no other change.
177
+ *
178
+ * Each member is optional alone, thus a composition binds the members that it records.
179
+ *
180
+ * The assembly reaches two consumers with the seam: the report agent, and the conversation agent, whose
181
+ * start tool passes it to the spawn. The creation of a conversation thread rides `prepareChatTurn`,
182
+ * which an embedder calls on its own. Thus that composition binds the seam there as well.
183
+ */
184
+ readonly provenance?: ProvenanceSeam;
163
185
  }
164
186
  /**
165
187
  * The refusal a thread type carries no agent yet. `report` is a valid
@@ -124,6 +124,9 @@ export function assembleCoreRuntime(deps) {
124
124
  usageRecorder,
125
125
  citationResolver,
126
126
  ...(eyes ? { eyes } : {}),
127
+ // The start tool passes it to the spawn, which emits the creation of a
128
+ // report session. The same seam reaches the report agent below.
129
+ ...(deps.provenance ? { provenance: deps.provenance } : {}),
127
130
  });
128
131
  // The report agent is a singleton over the conversation deps, the same way the
129
132
  // session runtime is. Its preview tool writes the page into the analysis tree
@@ -173,6 +176,7 @@ export function assembleCoreRuntime(deps) {
173
176
  ...(deps.resolveReportPageAsset ? { resolvePageAsset: deps.resolveReportPageAsset } : {}),
174
177
  ...(deps.makeSessionPagePublisher ? { makeSessionPages: deps.makeSessionPagePublisher } : {}),
175
178
  ...(deps.resolveReportPageUrl ? { resolvePageUrl: deps.resolveReportPageUrl } : {}),
179
+ ...(deps.provenance ? { provenance: deps.provenance } : {}),
176
180
  ...(conversation.logger ? { logger: conversation.logger } : {}),
177
181
  });
178
182
  // The single registration point for every typed agent. A future thread type's
@@ -20,7 +20,7 @@ import { clearSandboxRef } from "../state/index.js";
20
20
  import { type AwaitExecOptions } from "./await-exec.js";
21
21
  import type { SandboxClient } from "./client.js";
22
22
  import { type SubmitExecDeps } from "./submit-exec.js";
23
- import { type CreateSandboxMeta, type SandboxTransport } from "./types.js";
23
+ import { type CreateSandboxMeta, type FarmSource, type SandboxTransport, type ToolchainSource } from "./types.js";
24
24
  /**
25
25
  * Narrow config slice the sandbox factory reads off `Env` — the backend
26
26
  * default and the K8s namespace fallback. Composition roots map their
@@ -65,6 +65,21 @@ export interface CreateSandboxClientConfig {
65
65
  resolveWorkspaceRoot: ResolveWorkspaceRoot;
66
66
  /** Docker: host dir bind-mounted read-only at `/mnt/libs`. */
67
67
  libStorePath?: string;
68
+ /**
69
+ * Where the farm of an analysis comes from. Required of every embedder —
70
+ * the harness never invents a farm location, and it reads no store-root
71
+ * `current` pointer. On Docker, `FarmLocation.farmPath` is a host
72
+ * directory. On K8s, it is a `subPath` into `libStorePvc`. Consulted only
73
+ * when a store is configured.
74
+ */
75
+ farmSource: FarmSource;
76
+ /**
77
+ * The declared toolchain owner. `"image"` keys the resolver env and the
78
+ * orient-core prompt text onto the image-owned toolchain. Absent means
79
+ * `"store"`: the legacy environment, byte-identical to the one before
80
+ * this field existed.
81
+ */
82
+ toolchainSource?: ToolchainSource;
68
83
  /** Docker: host dir bind-mounted read-only at `/mnt/refs`. */
69
84
  refStorePath?: string;
70
85
  /** Docker: force the container platform (e.g. `linux/amd64`) so the sandbox matches the mounted lib store's arch. */
@@ -109,9 +124,10 @@ export interface CreateSandboxClientConfig {
109
124
  /** Override the backend selection — defaults to `env.SANDBOX_BACKEND`. */
110
125
  backend?: "docker" | "k8s";
111
126
  /**
112
- * Optional logger forwarded to the Docker backend so a lib-store degradation
113
- * (a configured store whose `current` vanished/went incomplete mid-session) is
114
- * observable rather than a silent mount drop. No-op when unset.
127
+ * Optional logger forwarded to the Docker backend so a store degradation
128
+ * (a resolved farm whose `inflexa.lock` vanished or went invalid
129
+ * mid-session) is observable rather than a silent mount drop. No-op when
130
+ * unset.
115
131
  */
116
132
  logger?: Logger;
117
133
  /** Dependency seams (fetch, durable step/sleep, clock, recv, warn sink) forwarded to submit/await. */
@@ -86,6 +86,8 @@ export function createSandboxClient(config) {
86
86
  sessionPvcRoot: config.sessionPvcRoot,
87
87
  resolveWorkspaceRoot: config.resolveWorkspaceRoot,
88
88
  libStorePvc: config.libStorePvc,
89
+ farmSource: config.farmSource,
90
+ toolchainSource: config.toolchainSource,
89
91
  refStorePvc: config.refStorePvc,
90
92
  nodeSelector: config.nodeSelector,
91
93
  tolerations: config.tolerations,
@@ -98,6 +100,8 @@ export function createSandboxClient(config) {
98
100
  transport,
99
101
  resolveWorkspaceRoot: config.resolveWorkspaceRoot,
100
102
  libStorePath: config.libStorePath,
103
+ farmSource: config.farmSource,
104
+ toolchainSource: config.toolchainSource,
101
105
  refStorePath: config.refStorePath,
102
106
  platform: config.platform,
103
107
  engineSocketPath: config.engineSocketPath,
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Thin wrapper around `dockerode`. Storage is wired via `HostConfig.Binds`:
5
5
  * a flat read-only mount of the analysis tree at `/{resourceId}`, a nested
6
- * read-write mount of the step's artifact dir, and the lib/ref stores at
7
- * `/mnt/libs` / `/mnt/refs` when their host paths are configured. Container
8
- * paths and lib-store env come from the shared mount plan (`mount-plan.ts`).
6
+ * read-write mount of the step's artifact dir, the package store at
7
+ * `/mnt/libs` with the resolved farm nested inside it, and the ref store at
8
+ * `/mnt/refs` when its host path is configured. Container paths and
9
+ * lib-store env come from the shared mount plan (`mount-plan.ts`).
9
10
  *
10
11
  * ## The cpu files
11
12
  *
@@ -42,7 +43,7 @@ import { ResultAsync } from "neverthrow";
42
43
  import type { Logger } from "../lib/logger.js";
43
44
  import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
44
45
  import { type SandboxError } from "./sandbox-error.js";
45
- import type { CreateSandboxMeta, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxTransport } from "./types.js";
46
+ import type { CreateSandboxMeta, FarmSource, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxTransport, ToolchainSource } from "./types.js";
46
47
  export interface DockerClientConfig {
47
48
  image: string;
48
49
  /** Cortex base URL injected into the sandbox env in callback mode so callbacks land here. Unused in poll mode. */
@@ -53,6 +54,15 @@ export interface DockerClientConfig {
53
54
  resolveWorkspaceRoot: ResolveWorkspaceRoot;
54
55
  /** Host lib store; bind-mounted read-only at `/mnt/libs` when set. */
55
56
  libStorePath?: string;
57
+ /**
58
+ * Where the farm of an analysis comes from. Required — the harness never
59
+ * invents a farm location. `FarmLocation.farmPath` is a host directory
60
+ * here, bind-mounted read-only at the farm container path. Consulted only
61
+ * when `libStorePath` is set.
62
+ */
63
+ farmSource: FarmSource;
64
+ /** The declared toolchain owner. Absent means `"store"`, the legacy environment. */
65
+ toolchainSource?: ToolchainSource;
56
66
  /**
57
67
  * Host ref store; bind-mounted read-only at `/mnt/refs`. Embedders pass the
58
68
  * configured store location unconditionally — existence is (re-)checked at each
@@ -85,8 +95,8 @@ export interface DockerClientConfig {
85
95
  */
86
96
  readHostCpuinfo?: () => Promise<string | undefined>;
87
97
  /**
88
- * Optional logger so a lib-store degradation (a configured store whose `current`
89
- * vanished or went incomplete by sandbox-create time) is observable instead of a
98
+ * Optional logger so a store degradation (a resolved farm whose `inflexa.lock`
99
+ * is absent or invalid by sandbox-create time) is observable instead of a
90
100
  * silent libs-mount drop. Matches the `reaper`/`watchdog` logger seam.
91
101
  */
92
102
  logger?: Logger;
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Thin wrapper around `dockerode`. Storage is wired via `HostConfig.Binds`:
5
5
  * a flat read-only mount of the analysis tree at `/{resourceId}`, a nested
6
- * read-write mount of the step's artifact dir, and the lib/ref stores at
7
- * `/mnt/libs` / `/mnt/refs` when their host paths are configured. Container
8
- * paths and lib-store env come from the shared mount plan (`mount-plan.ts`).
6
+ * read-write mount of the step's artifact dir, the package store at
7
+ * `/mnt/libs` with the resolved farm nested inside it, and the ref store at
8
+ * `/mnt/refs` when its host path is configured. Container paths and
9
+ * lib-store env come from the shared mount plan (`mount-plan.ts`).
9
10
  *
10
11
  * ## The cpu files
11
12
  *
@@ -37,7 +38,7 @@
37
38
  * Either way the workload ends as uid 1000 with `no-new-privileges` and no
38
39
  * effective capabilities.
39
40
  */
40
- import { existsSync, lstatSync, statSync } from "node:fs";
41
+ import { lstatSync } from "node:fs";
41
42
  import { mkdir, rm, writeFile } from "node:fs/promises";
42
43
  import { dirname, join } from "node:path";
43
44
  import Docker from "dockerode";
@@ -45,6 +46,7 @@ import { ResultAsync, err, ok } from "neverthrow";
45
46
  import { createNoopLogger } from "../lib/console-logger.js";
46
47
  import { tailWritePrefix } from "../workspace/paths.js";
47
48
  import { trySandbox } from "./sandbox-error.js";
49
+ import { readFarmLock, resolveFarmSource } from "./farm.js";
48
50
  import { buildMountPlan, sandboxWriteTail } from "./mount-plan.js";
49
51
  import { CPU_ONLINE_PATH, CPUINFO_PATH, cpuFiles, readHostCpuinfoOnce } from "./cpu-files.js";
50
52
  import { threadLimitEnv } from "./thread-env.js";
@@ -74,26 +76,6 @@ const RUN_ID_LABEL = "cortex/run-id";
74
76
  const STEP_ID_LABEL = "cortex/step-id";
75
77
  /** The directory next to the workspace root that holds the cpu files of each sandbox. */
76
78
  const CPU_FILES_DIR = ".cpu";
77
- /**
78
- * Whether the lib store's `current` resolves to a COMPLETE, usable version, not merely a
79
- * present symlink. By sandbox-create time `current` may be gone (concurrent prune/`rm`), a
80
- * dangling symlink (its target pruned), or an incomplete tree. Binding a missing source
81
- * makes Docker auto-create a root-owned dir (bricking a later store refresh); binding a broken
82
- * one mounts broken content. Require a resolved directory carrying both completeness
83
- * markers `activate` writes before it flips the pointer.
84
- */
85
- function libStoreUsable(libStorePath) {
86
- const current = join(libStorePath, "current");
87
- try {
88
- // statSync FOLLOWS the symlink, so a dangling `current` throws here and is rejected.
89
- if (!statSync(current).isDirectory())
90
- return false;
91
- }
92
- catch {
93
- return false;
94
- }
95
- return existsSync(join(current, "packages.txt")) && existsSync(join(current, "meta.json"));
96
- }
97
79
  /**
98
80
  * Whether `refStorePath` is, right now, a real directory fit to be a bind authority.
99
81
  * Deliberately shallow — it checks existence-as-a-directory only, never the store's
@@ -253,13 +235,41 @@ export function createDockerSandboxOps(config) {
253
235
  createSandbox(meta, identity) {
254
236
  return new ResultAsync((async () => {
255
237
  const { sandboxId, callbackSecret } = identity;
256
- // Re-check AT sandbox-creation time: `config.libStorePath` was fixed at CLI
257
- // boot and the store may have gone away since (see libStoreUsable). Not
258
- // usable → skip the mount (sandbox degrades to available:false) and log it,
259
- // since an otherwise-silent libs-mount drop is invisible to operators.
260
- const libsMounted = !!config.libStorePath && libStoreUsable(config.libStorePath);
261
- if (config.libStorePath && !libsMounted) {
262
- logger.warn("lib store configured but `current` is missing or incomplete at sandbox creation — mounting no library store (sandbox degrades to available:false)", { libStorePath: config.libStorePath, sandboxId });
238
+ // Farm resolution comes before any container work: a resolver
239
+ // refusal and a resolver throw refuse the whole call, and no
240
+ // container is made. The reason of the embedder rides in the error.
241
+ let farm;
242
+ if (config.libStorePath) {
243
+ const resolved = await resolveFarmSource(config.farmSource, meta.analysisId, "docker.createSandbox");
244
+ if (resolved.isErr())
245
+ return err(resolved.error);
246
+ farm = resolved.value;
247
+ }
248
+ // The `inflexa.lock` gate runs at each create: the store may have
249
+ // gone away, or the farm may be half-written. A gate failure
250
+ // degrades — both store mounts drop, the container is still made,
251
+ // and the sandbox reports the store as unavailable. The warning is
252
+ // logged because an otherwise-silent mount drop is invisible to
253
+ // operators.
254
+ let libsMounted = false;
255
+ if (config.libStorePath && farm) {
256
+ const lock = readFarmLock(farm.farmPath);
257
+ libsMounted = lock.isOk();
258
+ if (lock.isErr()) {
259
+ logger.warn("the farm of the analysis has no usable inflexa.lock at sandbox creation — mounting no package store (sandbox degrades to available:false)", { libStorePath: config.libStorePath, farmPath: farm.farmPath, lockError: lock.error.type, sandboxId });
260
+ }
261
+ }
262
+ // The cache path is the one farm value with no gate of its
263
+ // own: the lock gate proves `farmPath`, and this bind string
264
+ // would carry a malformed cache path verbatim. An unusable
265
+ // value degrades to no cache mount — the rule of a missing
266
+ // one — because the entrypoint fallback covers the caches.
267
+ const cacheBindable = farm?.cachePath !== undefined && farm.cachePath.startsWith("/") && !farm.cachePath.includes(":");
268
+ if (farm?.cachePath !== undefined && !cacheBindable) {
269
+ logger.warn("the farm cache path cannot ride a docker bind — mounting no cache", {
270
+ cachePath: farm.cachePath,
271
+ sandboxId,
272
+ });
263
273
  }
264
274
  // Re-checked here too, so a ref store installed after boot is mounted into
265
275
  // the next sandbox. Unlike libs, a missing ref store is NOT warned: a missing
@@ -271,6 +281,8 @@ export function createDockerSandboxOps(config) {
271
281
  const plan = buildMountPlan(meta, {
272
282
  libs: libsMounted,
273
283
  refs: refsMounted,
284
+ toolchainSource: config.toolchainSource,
285
+ cache: cacheBindable,
274
286
  });
275
287
  const hostTreePath = config.resolveWorkspaceRoot(meta.analysisId);
276
288
  // `tailWritePrefix` (not a raw `join`) so the RW bind source runs through
@@ -290,12 +302,16 @@ export function createDockerSandboxOps(config) {
290
302
  const cpuBinds = await writeCpuFiles(cpuDir, cpuFiles(threads, cpuinfo), logger, sandboxId);
291
303
  if (cpuBinds.length > 0)
292
304
  cpuDirs.set(sandboxId, cpuDir);
305
+ // The farm bind comes after the store bind, thus the nesting is
306
+ // stable; the cache bind comes after the two store binds.
293
307
  const binds = [
294
308
  `${hostTreePath}:${plan.readonlyTreePath}:ro`,
295
309
  ...(write && plan.writableStepPath
296
310
  ? [`${tailWritePrefix({ workspaceRoot: hostTreePath, tail: write.tail })}:${plan.writableStepPath}:rw`]
297
311
  : []),
298
312
  ...(libsMounted && config.libStorePath ? [`${config.libStorePath}:${plan.libsPath}:ro`] : []),
313
+ ...(libsMounted && farm && plan.farmPath ? [`${farm.farmPath}:${plan.farmPath}:ro`] : []),
314
+ ...(libsMounted && cacheBindable && farm?.cachePath && plan.cachePath ? [`${farm.cachePath}:${plan.cachePath}:rw`] : []),
299
315
  ...(refsMounted && config.refStorePath ? [`${config.refStorePath}:${plan.refsPath}:ro`] : []),
300
316
  ...cpuBinds,
301
317
  ];
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Farm-source resolution and the `inflexa.lock` farm contract.
3
+ *
4
+ * A farm carries exactly one metadata file, `inflexa.lock`, beside its link
5
+ * trees and its caches (the lib-store spec). The mount gate and the package
6
+ * inventory read this one file — `packages.txt`, `meta.json`, and
7
+ * `lock.json` are not part of the farm contract.
8
+ *
9
+ * The resolution and the gate refuse differently, on purpose:
10
+ * - A resolver refusal (`unavailable`, or a throw) is a `farm_unavailable`
11
+ * `SandboxError` — the call refuses and no container is made.
12
+ * - A lock-gate failure degrades — the backend drops both store mounts,
13
+ * logs a warning, and still makes the container.
14
+ */
15
+ import { type Result } from "neverthrow";
16
+ import { z } from "zod";
17
+ import type { SandboxError } from "./sandbox-error.js";
18
+ import type { FarmLocation, FarmSource } from "./types.js";
19
+ /** The one metadata file of a farm. */
20
+ export declare const FARM_LOCK_FILE = "inflexa.lock";
21
+ /** One linked distribution. `requested` obeys the PEP 376 meaning: true for a direct ask, false for a transitive dependency. */
22
+ export declare const FarmLockPackageSchema: z.ZodObject<{
23
+ name: z.ZodString;
24
+ version: z.ZodString;
25
+ track: z.ZodString;
26
+ store_dir: z.ZodString;
27
+ hash: z.ZodString;
28
+ requested: z.ZodBoolean;
29
+ }, z.core.$loose>;
30
+ /**
31
+ * `inflexa.lock` at schema version 1. Additive fields pass through — a
32
+ * breaking change to the shape must move the schema number instead.
33
+ */
34
+ export declare const FarmLockSchema: z.ZodObject<{
35
+ schema: z.ZodLiteral<1>;
36
+ arch: z.ZodEnum<{
37
+ amd64: "amd64";
38
+ arm64: "arm64";
39
+ }>;
40
+ packages: z.ZodArray<z.ZodObject<{
41
+ name: z.ZodString;
42
+ version: z.ZodString;
43
+ track: z.ZodString;
44
+ store_dir: z.ZodString;
45
+ hash: z.ZodString;
46
+ requested: z.ZodBoolean;
47
+ }, z.core.$loose>>;
48
+ languages: z.ZodObject<{
49
+ python: z.ZodOptional<z.ZodObject<{
50
+ version: z.ZodString;
51
+ index: z.ZodString;
52
+ }, z.core.$loose>>;
53
+ r: z.ZodOptional<z.ZodObject<{
54
+ version: z.ZodString;
55
+ bioc_releases: z.ZodArray<z.ZodString>;
56
+ pak_lock: z.ZodOptional<z.ZodUnknown>;
57
+ }, z.core.$loose>>;
58
+ }, z.core.$loose>;
59
+ warm: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
60
+ script_sha256: z.ZodString;
61
+ cache_entries: z.ZodArray<z.ZodString>;
62
+ }, z.core.$loose>>>;
63
+ merge_conflicts: z.ZodOptional<z.ZodArray<z.ZodObject<{
64
+ entry: z.ZodString;
65
+ action: z.ZodEnum<{
66
+ skipped: "skipped";
67
+ "kept-first": "kept-first";
68
+ }>;
69
+ }, z.core.$loose>>>;
70
+ }, z.core.$loose>;
71
+ export type FarmLock = z.infer<typeof FarmLockSchema>;
72
+ export type FarmLockError = {
73
+ readonly type: "lock_unreadable";
74
+ readonly lockPath: string;
75
+ readonly cause: unknown;
76
+ } | {
77
+ readonly type: "lock_invalid";
78
+ readonly lockPath: string;
79
+ readonly cause: unknown;
80
+ };
81
+ /**
82
+ * Read and validate the `inflexa.lock` of a farm directory. The mount gate
83
+ * accepts a farm only when this returns ok — a farm whose lock is absent,
84
+ * unparseable, or at an unknown schema version is not usable.
85
+ */
86
+ export declare function readFarmLock(farmPath: string): Result<FarmLock, FarmLockError>;
87
+ /** Read and validate one `inflexa.lock` by its file path. The package inventory reads through this too. */
88
+ export declare function readFarmLockFile(lockPath: string): Result<FarmLock, FarmLockError>;
89
+ /**
90
+ * Resolve the farm source of one analysis. Runs before any container work.
91
+ * `fixed` answers at once. `per-analysis` calls the resolver of the
92
+ * embedder — an `unavailable` result and a throw both refuse the call with
93
+ * `farm_unavailable`, and the reason of the embedder rides in the error.
94
+ */
95
+ export declare function resolveFarmSource(source: FarmSource, analysisId: string, op: string): Promise<Result<FarmLocation, SandboxError>>;