@inflexa-ai/harness 0.5.0 → 0.7.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.
@@ -52,6 +52,18 @@ export interface DataProfileDeps {
52
52
  readonly embedding: EmbeddingProvider;
53
53
  /** Absolute path to the skills tree (one subdirectory per skill). */
54
54
  readonly skillsDir: string;
55
+ /**
56
+ * Host path of the library store's `packages.txt`. Omit when the host mounts the
57
+ * store at the sandbox's own path; a host whose store is baked into the image must
58
+ * inject its extracted copy, or the profiler reads the inventory as unknown.
59
+ */
60
+ readonly packagesFile?: string;
61
+ /**
62
+ * Host path of the reference store. Omit only when none is provisioned: the
63
+ * profiler carries reference discovery, and without this it reports an installed
64
+ * store as absent.
65
+ */
66
+ readonly refStorePath?: string;
55
67
  }
56
68
  /**
57
69
  * Workflow input. JSON-serialisable — DBOS persists it as the workflow's
@@ -216,6 +216,8 @@ export async function runDataProfileBody(input, deps) {
216
216
  embedding: deps.embedding,
217
217
  model: deps.model,
218
218
  skillsDir: deps.skillsDir,
219
+ ...(deps.packagesFile ? { packagesFile: deps.packagesFile } : {}),
220
+ ...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}),
219
221
  bioKeys: deps.bioKeys,
220
222
  step: {
221
223
  sandbox,
@@ -21,6 +21,14 @@ export interface AskRequest {
21
21
  readonly title: string;
22
22
  readonly command: string;
23
23
  readonly detail?: string;
24
+ /**
25
+ * Keys the standing grant an `always` records when the class being blessed is
26
+ * broader than the displayed `command` — a tool that grants a whole family of
27
+ * operations passes the family's key here while still showing one concrete
28
+ * command. It is a generic, opaque key string, not a tool- or domain-specific
29
+ * field, and the surface never renders it. Absent, the grant keys on `command`.
30
+ */
31
+ readonly grantKey?: string;
24
32
  }
25
33
  /**
26
34
  * A user's decision on an approval request — three distinct outcomes, never a
@@ -27,11 +27,13 @@ export function createAskGateway(deps) {
27
27
  title: request.title,
28
28
  command: request.command,
29
29
  detail: request.detail ?? null,
30
+ grantKey: request.grantKey ?? null,
30
31
  createdAt: now,
31
32
  };
32
- // A standing grant for this exact command short-circuits the prompt: record
33
- // the invocation as `resolved` for the audit trail and return without pausing.
34
- if (unwrapOrThrow(await selectGrant(pool, ctx.analysisId, request.command))) {
33
+ // A standing grant for this ask's grant key — the command unless the tool
34
+ // supplied a broader key — short-circuits the prompt: record the invocation as
35
+ // `resolved` for the audit trail and return without pausing.
36
+ if (unwrapOrThrow(await selectGrant(pool, ctx.analysisId, request.grantKey ?? request.command))) {
35
37
  unwrapOrThrow(await insertGrantedAsk(pool, row, now));
36
38
  return { kind: "always" };
37
39
  }
@@ -40,6 +40,8 @@ export interface AskRow {
40
40
  readonly title: string;
41
41
  readonly command: string;
42
42
  readonly detail: string | null;
43
+ /** Grant key an `always` keys its standing grant under; `null` falls back to `command`. */
44
+ readonly grantKey: string | null;
43
45
  readonly createdAt: string;
44
46
  }
45
47
  /** Insert a fresh `pending` ask before its decision is awaited. */
@@ -50,8 +52,8 @@ export declare function insertPendingAsk(querier: Querier, row: AskRow): ResultA
50
52
  * every approval-gated action. The recorded reply is `{ kind: "always" }`.
51
53
  */
52
54
  export declare function insertGrantedAsk(querier: Querier, row: AskRow, resolvedAt: string): ResultAsync<void, DbError>;
53
- /** Does a standing grant cover `(analysisId, command)` in this analysis? */
54
- export declare function selectGrant(querier: Querier, analysisId: string, command: string): ResultAsync<boolean, DbError>;
55
+ /** Does a standing grant cover `(analysisId, grantKey)` in this analysis? */
56
+ export declare function selectGrant(querier: Querier, analysisId: string, grantKey: string): ResultAsync<boolean, DbError>;
55
57
  /** Read a row's current status + recorded reply. `null` when no such row exists. */
56
58
  export declare function selectAskResolution(querier: Querier, id: string): ResultAsync<AskResolution | null, DbError>;
57
59
  /**
@@ -15,9 +15,9 @@ export function insertPendingAsk(querier, row) {
15
15
  return tryMutation("asks.insertPending", async () => {
16
16
  await querier.query({
17
17
  text: `INSERT INTO cortex_asks
18
- (id, analysis_id, thread_id, title, command, detail, status, created_at)
19
- VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7)`,
20
- values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, row.createdAt],
18
+ (id, analysis_id, thread_id, title, command, detail, grant_key, status, created_at)
19
+ VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending', $8)`,
20
+ values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, row.grantKey ?? row.command, row.createdAt],
21
21
  });
22
22
  });
23
23
  }
@@ -30,18 +30,29 @@ export function insertGrantedAsk(querier, row, resolvedAt) {
30
30
  return tryMutation("asks.insertGranted", async () => {
31
31
  await querier.query({
32
32
  text: `INSERT INTO cortex_asks
33
- (id, analysis_id, thread_id, title, command, detail, status, reply, created_at, resolved_at)
34
- VALUES ($1, $2, $3, $4, $5, $6, 'resolved', $7::jsonb, $8, $9)`,
35
- values: [row.id, row.analysisId, row.threadId, row.title, row.command, row.detail, JSON.stringify({ kind: "always" }), row.createdAt, resolvedAt],
33
+ (id, analysis_id, thread_id, title, command, detail, grant_key, status, reply, created_at, resolved_at)
34
+ VALUES ($1, $2, $3, $4, $5, $6, $7, 'resolved', $8::jsonb, $9, $10)`,
35
+ values: [
36
+ row.id,
37
+ row.analysisId,
38
+ row.threadId,
39
+ row.title,
40
+ row.command,
41
+ row.detail,
42
+ row.grantKey ?? row.command,
43
+ JSON.stringify({ kind: "always" }),
44
+ row.createdAt,
45
+ resolvedAt,
46
+ ],
36
47
  });
37
48
  });
38
49
  }
39
- /** Does a standing grant cover `(analysisId, command)` in this analysis? */
40
- export function selectGrant(querier, analysisId, command) {
50
+ /** Does a standing grant cover `(analysisId, grantKey)` in this analysis? */
51
+ export function selectGrant(querier, analysisId, grantKey) {
41
52
  return tryQuery("asks.selectGrant", async () => {
42
53
  const result = await querier.query({
43
- text: `SELECT 1 FROM cortex_ask_grants WHERE analysis_id = $1 AND command = $2`,
44
- values: [analysisId, command],
54
+ text: `SELECT 1 FROM cortex_ask_grants WHERE analysis_id = $1 AND grant_key = $2`,
55
+ values: [analysisId, grantKey],
45
56
  });
46
57
  return (result.rowCount ?? 0) > 0;
47
58
  });
@@ -114,19 +125,23 @@ export function answerAsk(pool, id, reply, now) {
114
125
  return withTransaction(pool, "asks.answer", (client) => tryMutation("asks.answer:update", () => client.query({
115
126
  text: `UPDATE cortex_asks SET status = $1, reply = $2::jsonb, resolved_at = $3
116
127
  WHERE id = $4 AND status = 'pending'
117
- RETURNING analysis_id, command`,
128
+ RETURNING analysis_id, COALESCE(grant_key, command) AS grant_key`,
118
129
  values: [status, JSON.stringify(reply), now, id],
119
130
  })).andThen((updated) => {
120
131
  if ((updated.rowCount ?? 0) === 0)
121
132
  return discriminateMiss(client, id);
122
133
  if (reply.kind !== "always")
123
134
  return okAsync("applied");
135
+ // The cast's non-null `grant_key` is sound without any boot-order
136
+ // caveat: every insert writes grant_key (command when the tool supplied
137
+ // none), and the COALESCE above covers any pending row that predates
138
+ // the column — `command` is NOT NULL, so the key can never be null.
124
139
  const granted = updated.rows[0];
125
140
  return tryMutation("asks.answer:grant", async () => {
126
141
  await client.query({
127
- text: `INSERT INTO cortex_ask_grants (analysis_id, command, created_at)
128
- VALUES ($1, $2, $3) ON CONFLICT (analysis_id, command) DO NOTHING`,
129
- values: [granted.analysis_id, granted.command, now],
142
+ text: `INSERT INTO cortex_ask_grants (analysis_id, grant_key, created_at)
143
+ VALUES ($1, $2, $3) ON CONFLICT (analysis_id, grant_key) DO NOTHING`,
144
+ values: [granted.analysis_id, granted.grant_key, now],
130
145
  });
131
146
  }).map(() => "applied");
132
147
  }));
@@ -41,6 +41,13 @@ export interface GeneratePlanDeps {
41
41
  * default guidance and validation skips the ceiling check.
42
42
  */
43
43
  readonly resourcePolicy?: ResourcePolicy;
44
+ /**
45
+ * Host path of the reference store, giving the planner reference discovery.
46
+ * Absent, the tool reports the store as unavailable and the planner keeps
47
+ * planning as though nothing is guaranteed present — which is what the prompt
48
+ * already tells it to do.
49
+ */
50
+ readonly refStorePath?: string;
44
51
  }
45
52
  /** Build the `generate_plan` tool bound to its provider and pool. */
46
53
  export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;
@@ -32,6 +32,7 @@ import { runToTerminal } from "../../loop/run-to-terminal.js";
32
32
  import { passthroughStep } from "../../loop/run-step.js";
33
33
  import { forSubAgent, scopeResource } from "../../auth/types.js";
34
34
  import { defineTool } from "../define-tool.js";
35
+ import { createListAvailableRefsTool } from "../sandbox/list-available-refs.js";
35
36
  import { createReportBlockerToolFor } from "../sandbox/report-blocker.js";
36
37
  import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../../app/data-profile-orientation.js";
37
38
  import { isDataProfileStale } from "../../app/data-profile-policy.js";
@@ -249,7 +250,7 @@ function fullyValidate(candidate, resourcePolicy) {
249
250
  * shared `holder` so the outer `execute` reads the terminal outcome after the
250
251
  * loop finishes.
251
252
  */
252
- function buildInnerTools(holder, persistCtx, pool, resourcePolicy) {
253
+ function buildInnerTools(holder, persistCtx, pool, resourcePolicy, refStorePath) {
253
254
  const validatePlanTool = defineTool({
254
255
  id: "validate_plan",
255
256
  description: "Dry-run a candidate plan and get back everything that is wrong with it. " +
@@ -353,8 +354,12 @@ function buildInnerTools(holder, persistCtx, pool, resourcePolicy) {
353
354
  "incompatible with every available agent, etc.) — not for a plan you " +
354
355
  "could fix and submit.",
355
356
  });
357
+ // Reference discovery is non-terminal and read-only: it lets the planner check what
358
+ // reference data this environment actually holds before committing a step to needing
359
+ // it. It reads the store host-side, so no sandbox exists or is needed at plan time.
360
+ const listAvailableRefs = createListAvailableRefsTool(refStorePath === undefined ? {} : { refStorePath });
356
361
  const terminal = [submitPlanTool, requestClarificationTool, reportBlockerTool];
357
- return { all: [validatePlanTool, ...terminal], terminal };
362
+ return { all: [validatePlanTool, listAvailableRefs, ...terminal], terminal };
358
363
  }
359
364
  /**
360
365
  * Translate the captured `PlannerOutcome` (plus any loop error) into the
@@ -486,7 +491,7 @@ export function createGeneratePlanTool(deps) {
486
491
  analysisId,
487
492
  parentPlanId: input.parentPlanId ?? null,
488
493
  };
489
- const innerTools = buildInnerTools(holder, persistCtx, deps.pool, deps.resourcePolicy);
494
+ const innerTools = buildInnerTools(holder, persistCtx, deps.pool, deps.resourcePolicy, deps.refStorePath);
490
495
  const planner = {
491
496
  id: PLANNER_AGENT_ID,
492
497
  systemPrompt: composeSystemPrompt(plannerInstructions(deps.resourcePolicy)),
@@ -2,8 +2,10 @@
2
2
  * listAvailablePackages — query the R/Python/CLI/Node packages available in the
3
3
  * sandbox.
4
4
  *
5
- * The data source is `/mnt/libs/current/packages.txt`, assembled by the
6
- * library-store build (see the lib-store-build spec). Its shape is fixed by the
5
+ * The data source is the store's `packages.txt`, assembled by the library-store
6
+ * build (see the lib-store-build spec) and read from wherever the HOST can see
7
+ * it — the container path when the host mounts the same store, an injected path
8
+ * when it does not. Its shape is fixed by the
7
9
  * producers (`scripts/lib-store-common.sh`, `images/sandbox-python/inflexa-libs-refresh`):
8
10
  * two `#` advisory lines, then one `## <Section>` heading per language track
9
11
  * followed by that track's packages as a single comma-separated line.
@@ -73,7 +75,18 @@ export interface PackagesQuery {
73
75
  * is only the file read plus this call.
74
76
  */
75
77
  export declare function queryPackages(sections: readonly Section[], { names, query, language, limit }: PackagesQuery): PackagesResult;
76
- export declare const listAvailablePackagesTool: import("../define-tool.js").Tool<{
78
+ export interface ListAvailablePackagesDeps {
79
+ /**
80
+ * Path to the store's `packages.txt` AS THE HOST SEES IT. Defaults to the
81
+ * container path, which is correct only when the host mounts the same store.
82
+ * A host that does not — one whose store is baked into the sandbox image and
83
+ * never bind-mounted — must inject the path to its own extracted copy, or the
84
+ * read fails and every caller is told the inventory is unknown.
85
+ */
86
+ readonly packagesFile?: string;
87
+ }
88
+ /** Create the package inventory over a host-readable `packages.txt`. */
89
+ export declare function createListAvailablePackagesTool(deps?: ListAvailablePackagesDeps): import("../define-tool.js").Tool<{
77
90
  names?: string[] | undefined;
78
91
  query?: string | undefined;
79
92
  language?: "r" | "python" | "cli" | "node" | undefined;
@@ -2,8 +2,10 @@
2
2
  * listAvailablePackages — query the R/Python/CLI/Node packages available in the
3
3
  * sandbox.
4
4
  *
5
- * The data source is `/mnt/libs/current/packages.txt`, assembled by the
6
- * library-store build (see the lib-store-build spec). Its shape is fixed by the
5
+ * The data source is the store's `packages.txt`, assembled by the library-store
6
+ * build (see the lib-store-build spec) and read from wherever the HOST can see
7
+ * it — the container path when the host mounts the same store, an injected path
8
+ * when it does not. Its shape is fixed by the
7
9
  * producers (`scripts/lib-store-common.sh`, `images/sandbox-python/inflexa-libs-refresh`):
8
10
  * two `#` advisory lines, then one `## <Section>` heading per language track
9
11
  * followed by that track's packages as a single comma-separated line.
@@ -24,10 +26,32 @@ import { readFile } from "node:fs/promises";
24
26
  import { ok } from "neverthrow";
25
27
  import { z } from "zod";
26
28
  import { defineTool } from "../define-tool.js";
27
- const PACKAGES_FILE = "/mnt/libs/current/packages.txt";
28
- /** Default number of packages a filtered/unfiltered listing returns. */
29
- const DEFAULT_LIMIT = 100;
30
- const UNAVAILABLE_NOTE = "Package list not available. The library store may not be mounted. Assume standard bioinformatics packages (numpy, pandas, scanpy, DESeq2, etc.) are available, but do not attempt to install anything.";
29
+ /**
30
+ * Where the list lives when the host mounts the library store at the same path
31
+ * the sandbox does. Hosts that do not — the store is baked into the image and
32
+ * never bind-mounted — inject their own path instead.
33
+ */
34
+ const DEFAULT_PACKAGES_FILE = "/mnt/libs/current/packages.txt";
35
+ /**
36
+ * Default cap on a listing — high enough that the real store is never truncated.
37
+ * The shipped catalog is ~270 packages (~3 KB, well under a thousand tokens), so
38
+ * a low default bought nothing and cost correctness: a partial listing reads as a
39
+ * complete one, and an agent concludes a package is absent when it was merely not
40
+ * rendered. The cap survives only as a backstop for a downstream `FROM` image
41
+ * that adds a pathological number of packages.
42
+ */
43
+ const DEFAULT_LIMIT = 2_000;
44
+ /**
45
+ * Naming packages here would be worse than saying nothing: this state means the
46
+ * list could not be read, so any roll-call is a guess the agent has no way to
47
+ * check, and an agent told to "assume numpy is available" will import it and
48
+ * fail at runtime instead of probing first.
49
+ */
50
+ const UNAVAILABLE_NOTE = "Package list not available — the library store's inventory could not be read, so what is installed is UNKNOWN. " +
51
+ "Do not assume any package is present, and do not infer one from the analysis you were asked to run. " +
52
+ "Probe each package you intend to use before relying on it (`python3 -c 'import <pkg>'`, " +
53
+ 'R `requireNamespace("<pkg>", quietly = TRUE)`) and degrade gracefully when it is absent. ' +
54
+ "Nothing can be installed at runtime.";
31
55
  /** The `language` filter values, mapped onto the concrete section headings. */
32
56
  const LANGUAGE_MATCHERS = {
33
57
  // The R triple: `R (CRAN)`, `R (Bioconductor)`, `R (GitHub)`.
@@ -135,35 +159,45 @@ export function queryPackages(sections, { names, query, language, limit }) {
135
159
  const { content, returned } = renderListing(filtered, limit ?? DEFAULT_LIMIT);
136
160
  return { available: true, total, returned, hasMore: returned < total, content };
137
161
  }
138
- export const listAvailablePackagesTool = defineTool({
139
- id: "list_available_packages",
140
- description: "Query the R, Python, CLI, and Node packages installed in the sandbox. No packages can be installed at runtime — only what this tool reports is importable. " +
141
- "Results are ALWAYS bounded; prefer a targeted query over a full listing. " +
142
- '`names`: check specific packages (e.g. ["Seurat", "scanpy"]) — returns present/absent plus the language track for each, case-insensitively; this is the cheapest call and the right one for \'is X available?\'. ' +
143
- "`query`: case-insensitive substring filter over package names. " +
144
- "`language`: restrict to one track (r | python | cli | node). " +
145
- "`limit`: max packages listed (default 100); the response always carries the true `total` and a `hasMore` flag, so truncation is never silent. " +
146
- 'The package list carries NO version numbers — this tool cannot report a package\'s version. Check a version at runtime instead (e.g. `python -c "import scanpy; print(scanpy.__version__)"`).',
147
- inputSchema: z.object({
148
- names: z
149
- .array(z.string())
150
- .max(100)
151
- .optional()
152
- .describe("Check these exact package names for presence (case-insensitive). Returns one entry per name: present/absent + the language track it lives in."),
153
- query: z.string().optional().describe("Case-insensitive substring filter over package names."),
154
- language: z.enum(["r", "python", "cli", "node"]).optional().describe("Restrict results to one language track."),
155
- limit: z.number().int().min(1).max(2000).optional().describe(`Max packages to list (default ${DEFAULT_LIMIT}). Ignored when \`names\` is given.`),
156
- }),
157
- execute: async (input) => {
158
- // A missing library-store mount is an expected environment state — model
159
- // it as an `available: false` data variant with a helpful fallback note.
160
- let raw;
161
- try {
162
- raw = await readFile(PACKAGES_FILE, "utf-8");
163
- }
164
- catch {
165
- return ok({ available: false, content: UNAVAILABLE_NOTE });
166
- }
167
- return ok(queryPackages(parsePackagesFile(raw), input));
168
- },
169
- });
162
+ /** Create the package inventory over a host-readable `packages.txt`. */
163
+ export function createListAvailablePackagesTool(deps = {}) {
164
+ const packagesFile = deps.packagesFile ?? DEFAULT_PACKAGES_FILE;
165
+ return defineTool({
166
+ id: "list_available_packages",
167
+ description: "Query the R, Python, CLI, and Node packages installed in the sandbox. No packages can be installed at runtime — only what this tool reports is importable. " +
168
+ "A full listing is small (a few hundred packages) and is returned whole by default, so a listing you get back is the complete set unless `hasMore` says otherwise. " +
169
+ '`names`: check specific packages (e.g. ["Seurat", "scanpy"]) — returns present/absent plus the language track for each, case-insensitively; this is the cheapest call and the right one for \'is X available?\'. ' +
170
+ "`query`: case-insensitive substring filter over package names. " +
171
+ "`language`: restrict to one track (r | python | cli | node). " +
172
+ "`limit`: cap the packages listed; the response always carries the true `total` and a `hasMore` flag, so truncation is never silent. " +
173
+ 'The package list carries NO version numbers — this tool cannot report a package\'s version. Check a version at runtime instead (e.g. `python -c "import scanpy; print(scanpy.__version__)"`).',
174
+ inputSchema: z.object({
175
+ names: z
176
+ .array(z.string())
177
+ .max(100)
178
+ .optional()
179
+ .describe("Check these exact package names for presence (case-insensitive). Returns one entry per name: present/absent + the language track it lives in."),
180
+ query: z.string().optional().describe("Case-insensitive substring filter over package names."),
181
+ language: z.enum(["r", "python", "cli", "node"]).optional().describe("Restrict results to one language track."),
182
+ limit: z
183
+ .number()
184
+ .int()
185
+ .min(1)
186
+ .max(DEFAULT_LIMIT)
187
+ .optional()
188
+ .describe(`Cap the packages listed. Omit to get the whole set — the store fits well within the ${DEFAULT_LIMIT} ceiling. Ignored when \`names\` is given.`),
189
+ }),
190
+ execute: async (input) => {
191
+ // An unreadable inventory is an expected environment state — model it as an
192
+ // `available: false` data variant telling the caller the set is UNKNOWN.
193
+ let raw;
194
+ try {
195
+ raw = await readFile(packagesFile, "utf-8");
196
+ }
197
+ catch {
198
+ return ok({ available: false, content: UNAVAILABLE_NOTE });
199
+ }
200
+ return ok(queryPackages(parsePackagesFile(raw), input));
201
+ },
202
+ });
203
+ }
@@ -1,29 +1,32 @@
1
1
  /**
2
- * Reference-store discovery — two coordinated surfaces over the read-only
3
- * store at /mnt/refs:
2
+ * Reference-store discovery: `createListAvailableRefsTool`, a bounded, no-follow
3
+ * inventory of what is ACTUALLY provisioned in the reference store (managed
4
+ * installs and user-added files alike). Narrow with `path`/`category`, filter
5
+ * with `query`, cap with `limit`; every response carries `returned`/`total`/
6
+ * `hasMore` and the `categories` present, so a listing is never an unbounded
7
+ * dump. Symlinks are never followed; nothing can be downloaded at runtime.
4
8
  *
5
- * - `createListAvailableRefsTool` — the wired sandbox tool. A bounded, no-follow
6
- * inventory of what is ACTUALLY mounted (managed installs, user-added files),
7
- * scanned live inside the sandbox. Narrow with `path`/`category`, filter with
8
- * `query`, cap with `limit`; every response carries `returned`/`total`/
9
- * `hasMore` and the `categories` present, so a listing is never an unbounded
10
- * dump. Symlinks are never followed; nothing can be downloaded at runtime.
11
- * - `queryRefs` — a pure query over a parsed `/mnt/refs/registry.json` manifest
12
- * (`files.by_category`), filtering on fields the registry carries: `local_path`,
13
- * `category`, `subtype`, `organism`, `tax_id`, `dataset`, `rows`.
9
+ * The store is read from the host filesystem at the embedder-supplied
10
+ * `refStorePath` and reported at `/mnt/refs`, the container path sandboxes mount
11
+ * it at — read where it lives, render where the caller will open it. Reading it
12
+ * host-side is what lets a planner ask what reference data exists before any
13
+ * sandbox is created; a step agent asking mid-run sees the same store, because
14
+ * the mount and this path are the same bytes.
15
+ *
16
+ * The scan reports paths; meaning is joined on afterwards from the install
17
+ * receipt and the catalog it names (organism, format, and each file's internal
18
+ * shape). Callers search by what the data IS, so the store's directory layout
19
+ * stays an installer detail rather than an interface anything encodes. Metadata
20
+ * only ever ENRICHES an entry — a file with no receipt and no catalog match is
21
+ * still returned, because a user-provided store is as valid as a managed one.
14
22
  */
15
- import type { SandboxClient } from "../../sandbox/client.js";
16
- import type { SandboxRef } from "../../sandbox/types.js";
17
23
  export interface ListAvailableRefsDeps {
18
- readonly sandboxClient: SandboxClient;
19
- readonly sandbox: SandboxRef;
20
- readonly workflowId: string;
21
- readonly stepId: string;
22
- readonly nextFunctionId: () => string;
23
- readonly deadlineMs: () => number;
24
- readonly markExecActive?: (execId: string) => Promise<void>;
25
- /** Filesystem root used by the scanner; production callers omit this. */
26
- readonly scanRoot?: string;
24
+ /**
25
+ * Host path of the reference store — the same bytes sandboxes see at
26
+ * `/mnt/refs`. Omitted (or absent on disk) means no store is provisioned,
27
+ * which is a normal state reported as `unavailable`, not an error.
28
+ */
29
+ readonly refStorePath?: string;
27
30
  }
28
31
  export interface ReferenceInventoryMetadata {
29
32
  readonly datasetId?: string;
@@ -35,6 +38,10 @@ export interface ReferenceInventoryMetadata {
35
38
  readonly category?: string;
36
39
  readonly subtype?: string;
37
40
  readonly organism?: string;
41
+ /** Logical content format, independent of compression — which reader to reach for. */
42
+ readonly format?: string;
43
+ /** The file's internal shape (key columns, identifier space), so a caller can use it without opening it first. */
44
+ readonly contents?: string;
38
45
  readonly rows?: number;
39
46
  readonly endpoint?: string;
40
47
  }
@@ -42,7 +49,7 @@ interface ReferenceInventoryEntryBase {
42
49
  readonly path: string;
43
50
  readonly metadata?: ReferenceInventoryMetadata;
44
51
  }
45
- /** A bounded, no-follow observation from the live reference mount. */
52
+ /** A bounded, no-follow observation of the reference store. */
46
53
  export type ReferenceInventoryEntry = (ReferenceInventoryEntryBase & {
47
54
  readonly kind: "file";
48
55
  readonly bytes: number;
@@ -82,62 +89,11 @@ interface RawScan {
82
89
  readonly receipts?: unknown[];
83
90
  readonly legacyEntries?: unknown[];
84
91
  }
85
- /** Create replay-safe reference discovery bound to the active sandbox. */
92
+ /** Create reference discovery over the host-visible reference store. */
86
93
  export declare function createListAvailableRefsTool(deps: ListAvailableRefsDeps): import("../define-tool.js").Tool<{
87
94
  path?: string | undefined;
88
95
  category?: string | undefined;
89
96
  query?: string | undefined;
90
97
  limit?: number | undefined;
91
98
  }, ReferenceInventoryResult>;
92
- export interface RegistryEntry {
93
- local_path: string;
94
- sha256: string;
95
- bytes: number;
96
- rows: number | null;
97
- category: string | null;
98
- subtype: string | null;
99
- organism: string | null;
100
- tax_id: string | null;
101
- dataset: string | null;
102
- endpoint: string | null;
103
- }
104
- export interface Registry {
105
- registry_version: string;
106
- build_id: string;
107
- generated_at: string;
108
- files: {
109
- by_category: Record<string, RegistryEntry[]>;
110
- };
111
- summary: {
112
- total_output_files: number;
113
- categories: string[];
114
- };
115
- }
116
- /**
117
- * Either the store is unmounted, or a bounded listing: `categories` always names
118
- * the full valid set (the drill-in index), and `total`/`hasMore` make truncation
119
- * explicit.
120
- */
121
- export type RefsResult = {
122
- readonly available: false;
123
- readonly content: string;
124
- } | {
125
- readonly available: true;
126
- readonly total: number;
127
- readonly returned: number;
128
- readonly hasMore: boolean;
129
- readonly categories: readonly string[];
130
- readonly content: string;
131
- };
132
- /** The tool's input, as parsed from `inputSchema`. */
133
- export interface RefsQuery {
134
- readonly category?: string;
135
- readonly query?: string;
136
- readonly limit?: number;
137
- }
138
- /**
139
- * Answer a refs query against a parsed registry. Pure — the tool's `execute` is
140
- * only the file read plus this call.
141
- */
142
- export declare function queryRefs(registry: Registry, { category, query, limit }: RefsQuery): RefsResult;
143
99
  export {};