@inflexa-ai/harness 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.d.ts +12 -11
- package/dist/agents/conversation-agent.js +30 -33
- package/dist/agents/report-builder.js +2 -1
- package/dist/agents/sandbox/bulk-transcriptomics-agent.js +1 -1
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -19
- package/dist/agents/sandbox/chromatin-agent.js +1 -10
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -10
- package/dist/agents/sandbox/drug-repurposing-agent.js +4 -14
- package/dist/agents/sandbox/enrichment-agent.js +1 -10
- package/dist/agents/sandbox/ephemeral-executor.js +1 -10
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -3
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -13
- package/dist/agents/sandbox/metabolomics-agent.js +1 -1
- package/dist/agents/sandbox/microbiome-agent.js +1 -1
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -12
- package/dist/agents/sandbox/multimodal-sc-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -10
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +2 -8
- package/dist/agents/sandbox/proteomics-agent.js +1 -1
- package/dist/agents/sandbox/scientific-executor.js +1 -10
- package/dist/agents/sandbox/shared.d.ts +17 -6
- package/dist/agents/sandbox/shared.js +49 -38
- package/dist/agents/sandbox/single-cell-agent.js +1 -1
- package/dist/agents/sandbox/spatial-omics-agent.js +1 -1
- package/dist/agents/sandbox/statistical-modeling-agent.js +1 -1
- package/dist/agents/sandbox/translational-safety-agent.js +3 -9
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/system-prompt.d.ts +37 -11
- package/dist/agents/system-prompt.js +35 -12
- package/dist/app/data-profile-orientation.d.ts +34 -0
- package/dist/app/data-profile-orientation.js +117 -0
- package/dist/contracts/schemas/chat-parts.d.ts +9 -9
- package/dist/execution/artifact-metadata.js +2 -1
- package/dist/execution/ephemeral-runner.d.ts +13 -0
- package/dist/execution/ephemeral-runner.js +23 -1
- package/dist/execution/run-synthesis.js +20 -19
- package/dist/execution/step-summary.js +2 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +19 -5
- package/dist/loop/__fixtures__/scripted-provider.d.ts +2 -2
- package/dist/loop/__fixtures__/scripted-provider.js +2 -1
- package/dist/loop/metrics.d.ts +33 -1
- package/dist/loop/metrics.js +56 -2
- package/dist/loop/run-agent.d.ts +13 -1
- package/dist/loop/run-agent.js +19 -4
- package/dist/memory/card-builders.d.ts +8 -1
- package/dist/memory/card-builders.js +20 -3
- package/dist/memory/reconstruct-cards.js +4 -3
- package/dist/memory/thread-history.d.ts +27 -2
- package/dist/memory/thread-history.js +45 -5
- package/dist/memory/working-memory.d.ts +93 -10
- package/dist/memory/working-memory.js +220 -84
- package/dist/prompts/SOUL.d.ts +22 -9
- package/dist/prompts/SOUL.js +56 -37
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +2 -2
- package/dist/prompts/briefing.d.ts +100 -0
- package/dist/prompts/briefing.js +166 -0
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +94 -440
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +10 -10
- package/dist/prompts/planner.js +28 -59
- package/dist/prompts/report-builder.d.ts +1 -1
- package/dist/prompts/report-builder.js +71 -24
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +2 -7
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +11 -15
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +3 -7
- package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
- package/dist/prompts/sandbox/data-profiler.js +13 -8
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +15 -16
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +2 -9
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +2 -6
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +3 -8
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +1 -7
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +2 -7
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +2 -8
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +2 -7
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +2 -7
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +2 -9
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +2 -9
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -6
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +2 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +13 -14
- package/dist/prompts/sandbox-standards.d.ts +22 -14
- package/dist/prompts/sandbox-standards.js +91 -71
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +33 -131
- package/dist/provenance/collector.js +1 -1
- package/dist/providers/ai-sdk.js +31 -4
- package/dist/providers/prompt-cache.d.ts +74 -0
- package/dist/providers/prompt-cache.js +77 -0
- package/dist/providers/types.d.ts +33 -0
- package/dist/reference-data/catalog.d.ts +131 -0
- package/dist/reference-data/catalog.js +348 -0
- package/dist/reference-data/receipt.d.ts +41 -0
- package/dist/reference-data/receipt.js +40 -0
- package/dist/schemas/data-profile-schemas.js +5 -3
- package/dist/schemas/plan-schemas.d.ts +21 -21
- package/dist/schemas/plan-schemas.js +9 -3
- package/dist/schemas/run-synthesis.d.ts +8 -4
- package/dist/schemas/run-synthesis.js +92 -56
- package/dist/schemas/validate-plan.js +1 -1
- package/dist/schemas/workflow-state.d.ts +7 -1
- package/dist/schemas/workflow-state.js +33 -19
- package/dist/state/artifacts.d.ts +13 -0
- package/dist/state/artifacts.js +27 -0
- package/dist/state/data-profile.d.ts +64 -5
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +1 -1
- package/dist/tasks/data-profile.d.ts +20 -2
- package/dist/tasks/data-profile.js +71 -22
- package/dist/tools/bio/chembl.d.ts +38 -0
- package/dist/tools/bio/chembl.js +138 -0
- package/dist/tools/bio/comptox.d.ts +50 -0
- package/dist/tools/bio/comptox.js +149 -0
- package/dist/tools/bio/get-impc-ko-profile.js +9 -6
- package/dist/tools/bio/index.d.ts +5 -17
- package/dist/tools/bio/index.js +5 -17
- package/dist/tools/bio/keys.d.ts +14 -9
- package/dist/tools/bio/keys.js +16 -16
- package/dist/tools/bio/opentargets.d.ts +38 -0
- package/dist/tools/bio/opentargets.js +89 -0
- package/dist/tools/bio/pubchem.d.ts +29 -0
- package/dist/tools/bio/pubchem.js +92 -0
- package/dist/tools/bio/pubmed.d.ts +51 -0
- package/dist/tools/bio/pubmed.js +100 -0
- package/dist/tools/bio/search-bgee-expression.js +12 -6
- package/dist/tools/bio/search-clinical-trials.js +16 -5
- package/dist/tools/bio/search-faers.js +8 -4
- package/dist/tools/bio/search-geo-datasets.js +20 -6
- package/dist/tools/bio/search-gwas-catalog.d.ts +1 -1
- package/dist/tools/bio/search-pharmgkb.js +10 -4
- package/dist/tools/display/index.d.ts +1 -0
- package/dist/tools/display/index.js +1 -0
- package/dist/tools/display/normalize-echart-spec.d.ts +61 -0
- package/dist/tools/display/normalize-echart-spec.js +211 -0
- package/dist/tools/display/show-user.d.ts +6 -1
- package/dist/tools/display/show-user.js +25 -12
- package/dist/tools/execute-plan.js +5 -2
- package/dist/tools/iterate-report.d.ts +168 -118
- package/dist/tools/iterate-report.js +334 -96
- package/dist/tools/lib/chembl-client.d.ts +12 -0
- package/dist/tools/lib/chembl-client.js +127 -0
- package/dist/tools/lib/ctx-ops.d.ts +224 -0
- package/dist/tools/lib/ctx-ops.js +450 -0
- package/dist/tools/lib/iuphar-client.js +10 -1
- package/dist/tools/lib/openfda-client.js +11 -3
- package/dist/tools/lib/pathway-client.js +10 -4
- package/dist/tools/lib/pubchem-ops.d.ts +74 -0
- package/dist/tools/lib/pubchem-ops.js +224 -0
- package/dist/tools/memory/update-working-memory.d.ts +45 -7
- package/dist/tools/memory/update-working-memory.js +111 -41
- package/dist/tools/report/index.d.ts +3 -2
- package/dist/tools/report/index.js +3 -2
- package/dist/tools/research/generate-analogy-report.js +25 -12
- package/dist/tools/research/generate-plan.d.ts +12 -5
- package/dist/tools/research/generate-plan.js +168 -37
- package/dist/tools/research/index.d.ts +1 -0
- package/dist/tools/research/index.js +1 -0
- package/dist/tools/research/inspect-data-profile.d.ts +85 -0
- package/dist/tools/research/inspect-data-profile.js +161 -0
- package/dist/tools/research/literature-reviewer.js +3 -4
- package/dist/tools/sandbox/list-available-packages.d.ts +79 -5
- package/dist/tools/sandbox/list-available-packages.js +152 -10
- package/dist/tools/sandbox/list-available-refs.d.ts +141 -5
- package/dist/tools/sandbox/list-available-refs.js +647 -134
- package/dist/tools/sandbox/report-blocker.d.ts +39 -9
- package/dist/tools/sandbox/report-blocker.js +43 -20
- package/dist/tools/workspace/read-file.js +4 -5
- package/dist/tools/workspace/run-exec.d.ts +4 -4
- package/dist/tools/workspace/run-exec.js +4 -4
- package/dist/tools/workspace/show-file.js +10 -6
- package/dist/tools/workspace/show-plan.js +12 -6
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/tools/workspace/workspace-search.js +11 -5
- package/dist/workflows/execute-analysis.d.ts +43 -4
- package/dist/workflows/execute-analysis.js +107 -6
- package/dist/workflows/target-assessment/lib/citation-validator.js +2 -2
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +2 -1
- package/dist/workspace/paths.d.ts +2 -2
- package/dist/workspace/paths.js +2 -2
- package/package.json +33 -33
- package/dist/execution/filesystem-artifact-registry.d.ts +0 -57
- package/dist/execution/filesystem-artifact-registry.js +0 -92
- package/dist/lib/identifiers.d.ts +0 -8
- package/dist/lib/identifiers.js +0 -10
- package/dist/lib/mime-detect.d.ts +0 -17
- package/dist/lib/mime-detect.js +0 -29
- package/dist/lib/streamed-download.d.ts +0 -28
- package/dist/lib/streamed-download.js +0 -111
- package/dist/lib/trace-context.d.ts +0 -11
- package/dist/lib/trace-context.js +0 -49
- package/dist/providers/llm-capabilities.d.ts +0 -33
- package/dist/providers/llm-capabilities.js +0 -58
- package/dist/schemas/render-step-prompt.d.ts +0 -32
- package/dist/schemas/render-step-prompt.js +0 -76
- package/dist/tools/bio/find-approval-precedent.d.ts +0 -32
- package/dist/tools/bio/find-approval-precedent.js +0 -72
- package/dist/tools/bio/get-article-details.d.ts +0 -11
- package/dist/tools/bio/get-article-details.js +0 -20
- package/dist/tools/bio/get-article-full-text.d.ts +0 -19
- package/dist/tools/bio/get-article-full-text.js +0 -32
- package/dist/tools/bio/get-bioactivity.d.ts +0 -11
- package/dist/tools/bio/get-bioactivity.js +0 -23
- package/dist/tools/bio/get-drug-info.d.ts +0 -9
- package/dist/tools/bio/get-drug-info.js +0 -22
- package/dist/tools/bio/get-mechanism.d.ts +0 -8
- package/dist/tools/bio/get-mechanism.js +0 -18
- package/dist/tools/bio/get-pubchem-assays.d.ts +0 -22
- package/dist/tools/bio/get-pubchem-assays.js +0 -107
- package/dist/tools/bio/get-pubchem-cross-refs.d.ts +0 -15
- package/dist/tools/bio/get-pubchem-cross-refs.js +0 -68
- package/dist/tools/bio/get-target-safety.d.ts +0 -18
- package/dist/tools/bio/get-target-safety.js +0 -26
- package/dist/tools/bio/search-compounds.d.ts +0 -22
- package/dist/tools/bio/search-compounds.js +0 -160
- package/dist/tools/bio/search-ctx-chemical.d.ts +0 -50
- package/dist/tools/bio/search-ctx-chemical.js +0 -130
- package/dist/tools/bio/search-ctx-exposure.d.ts +0 -55
- package/dist/tools/bio/search-ctx-exposure.js +0 -176
- package/dist/tools/bio/search-ctx-hazard.d.ts +0 -111
- package/dist/tools/bio/search-ctx-hazard.js +0 -178
- package/dist/tools/bio/search-opentargets.d.ts +0 -11
- package/dist/tools/bio/search-opentargets.js +0 -25
- package/dist/tools/bio/search-pubchem-compound.d.ts +0 -26
- package/dist/tools/bio/search-pubchem-compound.js +0 -110
- package/dist/tools/bio/search-pubmed.d.ts +0 -17
- package/dist/tools/bio/search-pubmed.js +0 -31
- package/dist/tools/bio/search-targets.d.ts +0 -9
- package/dist/tools/bio/search-targets.js +0 -20
- package/dist/tools/bio/search-toxcast.d.ts +0 -36
- package/dist/tools/bio/search-toxcast.js +0 -141
- package/dist/workflows/register-workflows.d.ts +0 -51
- package/dist/workflows/register-workflows.js +0 -48
- package/dist/workflows/resume-execute-analysis.d.ts +0 -70
- package/dist/workflows/resume-execute-analysis.js +0 -80
- package/dist/workflows/target-assessment/correction-loop.d.ts +0 -45
- package/dist/workflows/target-assessment/correction-loop.js +0 -62
- package/dist/workflows/target-assessment/steps/phase0-resolve.d.ts +0 -71
- package/dist/workflows/target-assessment/steps/phase0-resolve.js +0 -21
- package/dist/workflows/target-assessment/steps/phase1-aggregate.d.ts +0 -1018
- package/dist/workflows/target-assessment/steps/phase1-aggregate.js +0 -25
- package/dist/workspace/mount-strategy.d.ts +0 -50
- package/dist/workspace/mount-strategy.js +0 -67
- package/dist/workspace/provenance-collector.d.ts +0 -34
- package/dist/workspace/provenance-collector.js +0 -16
- package/dist/workspace/workspace-paths.d.ts +0 -26
- package/dist/workspace/workspace-paths.js +0 -31
|
@@ -1,147 +1,660 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Reference-store discovery — two coordinated surfaces over the read-only
|
|
3
|
+
* store at /mnt/refs:
|
|
4
|
+
*
|
|
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`.
|
|
3
14
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { basename, join } from "node:path";
|
|
15
|
+
import { basename, join, posix as posixPath } from "node:path";
|
|
6
16
|
import { ok } from "neverthrow";
|
|
7
17
|
import { z } from "zod";
|
|
18
|
+
import { REFERENCE_DATA_CATALOG } from "../../reference-data/catalog.js";
|
|
19
|
+
import { parseReferenceInstallReceipt } from "../../reference-data/receipt.js";
|
|
8
20
|
import { defineTool } from "../define-tool.js";
|
|
21
|
+
import { runSandboxExec } from "../workspace/run-exec.js";
|
|
9
22
|
const REFS_ROOT = "/mnt/refs";
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
23
|
+
const MAX_ENTRIES = 200;
|
|
24
|
+
const MAX_SCANNED_ENTRIES = 2_000;
|
|
25
|
+
const MAX_PATH_BYTES = 40_000;
|
|
26
|
+
const MAX_ENVELOPE_BYTES = 64_000;
|
|
27
|
+
const MAX_INPUT_PATH_BYTES = 4_096;
|
|
28
|
+
/** Default number of file entries a registry listing (`queryRefs`) returns. */
|
|
29
|
+
const DEFAULT_LIMIT = 40;
|
|
30
|
+
const SCAN_SCRIPT = String.raw `
|
|
31
|
+
import json, os, sys
|
|
32
|
+
|
|
33
|
+
ROOT = sys.argv[1]
|
|
34
|
+
RELATIVE = sys.argv[2]
|
|
35
|
+
TARGET = ROOT if RELATIVE == "." else os.path.join(ROOT, *RELATIVE.split("/"))
|
|
36
|
+
MAX_ENTRIES = int(sys.argv[3])
|
|
37
|
+
MAX_SCANNED = int(sys.argv[4])
|
|
38
|
+
MAX_PATH_BYTES = int(sys.argv[5])
|
|
39
|
+
MAX_ENVELOPE_BYTES = int(sys.argv[6])
|
|
40
|
+
|
|
41
|
+
def emit(value):
|
|
42
|
+
print(json.dumps(value, separators=(",", ":")))
|
|
43
|
+
|
|
44
|
+
def capped_entries(path, limit):
|
|
45
|
+
"""Inspect and sort at most limit names; probe one extra only to mark truncation."""
|
|
46
|
+
values = []
|
|
47
|
+
visited = 0
|
|
48
|
+
cut = False
|
|
49
|
+
try:
|
|
50
|
+
with os.scandir(path) as iterator:
|
|
51
|
+
for entry in iterator:
|
|
52
|
+
if visited >= limit:
|
|
53
|
+
cut = True
|
|
54
|
+
break
|
|
55
|
+
visited += 1
|
|
56
|
+
try:
|
|
57
|
+
if entry.is_symlink():
|
|
58
|
+
kind, size = "symlink", 0
|
|
59
|
+
elif entry.is_file(follow_symlinks=False):
|
|
60
|
+
kind, size = "file", entry.stat(follow_symlinks=False).st_size
|
|
61
|
+
elif entry.is_dir(follow_symlinks=False):
|
|
62
|
+
kind, size = "directory", 0
|
|
63
|
+
else:
|
|
64
|
+
kind, size = "other", 0
|
|
65
|
+
values.append((entry.name, kind, size))
|
|
66
|
+
except OSError:
|
|
67
|
+
continue
|
|
68
|
+
except OSError:
|
|
69
|
+
return [], False
|
|
70
|
+
values.sort(key=lambda value: value[0])
|
|
71
|
+
return values, cut
|
|
72
|
+
|
|
73
|
+
if not os.path.isdir(ROOT) or os.path.islink(ROOT):
|
|
74
|
+
emit({"state": "unavailable", "entries": [], "scannedEntries": 0, "truncated": False})
|
|
75
|
+
raise SystemExit(0)
|
|
76
|
+
|
|
77
|
+
relative = RELATIVE
|
|
78
|
+
cursor = ROOT
|
|
79
|
+
if relative != ".":
|
|
80
|
+
for part in relative.split(os.sep):
|
|
81
|
+
cursor = os.path.join(cursor, part)
|
|
82
|
+
if os.path.islink(cursor):
|
|
83
|
+
emit({"state": "symlink", "entries": [], "scannedEntries": 0, "truncated": False})
|
|
84
|
+
raise SystemExit(0)
|
|
85
|
+
|
|
86
|
+
if not os.path.exists(TARGET):
|
|
87
|
+
emit({"state": "not_found", "entries": [], "scannedEntries": 0, "truncated": False})
|
|
88
|
+
raise SystemExit(0)
|
|
89
|
+
if not os.path.isdir(TARGET):
|
|
90
|
+
emit({"state": "not_a_directory", "entries": [], "scannedEntries": 0, "truncated": False})
|
|
91
|
+
raise SystemExit(0)
|
|
92
|
+
|
|
93
|
+
entries = []
|
|
94
|
+
scanned = 0
|
|
95
|
+
path_bytes = 0
|
|
96
|
+
truncated = False
|
|
97
|
+
|
|
98
|
+
children, children_cut = capped_entries(TARGET, min(MAX_ENTRIES + 2, MAX_SCANNED))
|
|
99
|
+
truncated = children_cut
|
|
100
|
+
|
|
101
|
+
for name, kind, size in children:
|
|
102
|
+
path = os.path.join(TARGET, name)
|
|
103
|
+
rel = os.path.relpath(path, ROOT).replace(os.sep, "/")
|
|
104
|
+
if rel == ".inflexa" or rel.startswith(".inflexa/") or rel == "registry.json":
|
|
105
|
+
continue
|
|
106
|
+
scanned += 1
|
|
107
|
+
rendered = "/mnt/refs/" + rel
|
|
108
|
+
item = None
|
|
109
|
+
|
|
110
|
+
if kind == "symlink":
|
|
111
|
+
item = {"path": rendered, "kind": "symlink", "bytes": 0}
|
|
112
|
+
elif kind == "file":
|
|
113
|
+
item = {"path": rendered, "kind": "file", "bytes": size}
|
|
114
|
+
elif kind == "directory":
|
|
115
|
+
total_bytes = 0
|
|
116
|
+
file_count = 0
|
|
117
|
+
file_types = set()
|
|
118
|
+
directory_truncated = False
|
|
119
|
+
stack = [path]
|
|
120
|
+
while stack and scanned < MAX_SCANNED:
|
|
121
|
+
current = stack.pop()
|
|
122
|
+
descendants, descendants_cut = capped_entries(current, MAX_SCANNED - scanned)
|
|
123
|
+
directory_truncated = directory_truncated or descendants_cut
|
|
124
|
+
child_directories = []
|
|
125
|
+
for child_name, child_kind, child_size in descendants:
|
|
126
|
+
scanned += 1
|
|
127
|
+
if child_kind == "directory":
|
|
128
|
+
child_directories.append(os.path.join(current, child_name))
|
|
129
|
+
elif child_kind == "file":
|
|
130
|
+
file_count += 1
|
|
131
|
+
total_bytes += child_size
|
|
132
|
+
suffix = os.path.splitext(child_name)[1].lower()
|
|
133
|
+
if suffix:
|
|
134
|
+
file_types.add(suffix)
|
|
135
|
+
if scanned >= MAX_SCANNED:
|
|
136
|
+
directory_truncated = True
|
|
137
|
+
break
|
|
138
|
+
stack.extend(reversed(child_directories))
|
|
139
|
+
item = {
|
|
140
|
+
"path": rendered,
|
|
141
|
+
"kind": "directory",
|
|
142
|
+
"bytes": total_bytes,
|
|
143
|
+
"fileCount": file_count,
|
|
144
|
+
"fileTypes": sorted(file_types)[:10],
|
|
145
|
+
"truncated": directory_truncated,
|
|
146
|
+
}
|
|
147
|
+
truncated = truncated or directory_truncated
|
|
148
|
+
|
|
149
|
+
if item is not None:
|
|
150
|
+
if len(entries) < MAX_ENTRIES and path_bytes + len(rendered.encode()) <= MAX_PATH_BYTES:
|
|
151
|
+
entries.append(item)
|
|
152
|
+
path_bytes += len(rendered.encode())
|
|
153
|
+
else:
|
|
154
|
+
truncated = True
|
|
155
|
+
if scanned >= MAX_SCANNED:
|
|
156
|
+
truncated = True
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
receipts = []
|
|
160
|
+
receipt_bytes = 0
|
|
161
|
+
metadata_dir = os.path.join(ROOT, ".inflexa")
|
|
162
|
+
receipts_dir = os.path.join(metadata_dir, "receipts")
|
|
163
|
+
try:
|
|
164
|
+
if os.path.islink(metadata_dir) or os.path.islink(receipts_dir):
|
|
165
|
+
raise ValueError("symlinked metadata directory")
|
|
166
|
+
receipt_entries, _ = capped_entries(receipts_dir, 100)
|
|
167
|
+
for name, kind, _ in receipt_entries:
|
|
168
|
+
path = os.path.join(receipts_dir, name)
|
|
169
|
+
if kind != "file":
|
|
170
|
+
continue
|
|
171
|
+
# Per-file, so one unreadable or malformed receipt costs only its own labels.
|
|
172
|
+
# A shared try would abandon the loop and silently drop every receipt sorting
|
|
173
|
+
# after the bad one, quietly stripping metadata from datasets that are fine.
|
|
174
|
+
try:
|
|
175
|
+
with open(path, "rb") as handle:
|
|
176
|
+
raw = handle.read(16385)
|
|
177
|
+
if len(raw) > 16384 or receipt_bytes + len(raw) > 32768:
|
|
178
|
+
continue
|
|
179
|
+
parsed = json.loads(raw)
|
|
180
|
+
except Exception:
|
|
181
|
+
continue
|
|
182
|
+
receipts.append(parsed)
|
|
183
|
+
receipt_bytes += len(raw)
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
legacy_entries = []
|
|
188
|
+
try:
|
|
189
|
+
registry_path = os.path.join(ROOT, "registry.json")
|
|
190
|
+
if os.path.islink(registry_path):
|
|
191
|
+
raise ValueError("symlinked registry")
|
|
192
|
+
with open(registry_path, "rb") as handle:
|
|
193
|
+
registry = json.loads(handle.read(131073))
|
|
194
|
+
if isinstance(registry, dict):
|
|
195
|
+
by_category = registry.get("files", {}).get("by_category", {})
|
|
196
|
+
if isinstance(by_category, dict):
|
|
197
|
+
for category in sorted(by_category):
|
|
198
|
+
values = by_category.get(category)
|
|
199
|
+
if not isinstance(values, list):
|
|
200
|
+
continue
|
|
201
|
+
for item in values:
|
|
202
|
+
if len(legacy_entries) >= 200:
|
|
203
|
+
break
|
|
204
|
+
if isinstance(item, dict) and isinstance(item.get("local_path"), str):
|
|
205
|
+
legacy_entries.append({
|
|
206
|
+
"local_path": item["local_path"][:1000],
|
|
207
|
+
"category": str(category)[:500],
|
|
208
|
+
"dataset": str(item.get("dataset"))[:500] if item.get("dataset") is not None else None,
|
|
209
|
+
"subtype": str(item.get("subtype"))[:500] if item.get("subtype") is not None else None,
|
|
210
|
+
"organism": str(item.get("organism"))[:500] if item.get("organism") is not None else None,
|
|
211
|
+
"rows": item.get("rows"),
|
|
212
|
+
"endpoint": item.get("endpoint"),
|
|
213
|
+
})
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
payload = {
|
|
218
|
+
"state": "empty" if len(entries) == 0 else "populated",
|
|
219
|
+
"entries": entries,
|
|
220
|
+
"scannedEntries": scanned,
|
|
221
|
+
"truncated": truncated,
|
|
222
|
+
"receipts": receipts,
|
|
223
|
+
"legacyEntries": legacy_entries,
|
|
224
|
+
}
|
|
225
|
+
while len(json.dumps(payload, separators=(",", ":")).encode()) > MAX_ENVELOPE_BYTES:
|
|
226
|
+
payload["truncated"] = True
|
|
227
|
+
if payload["legacyEntries"]:
|
|
228
|
+
payload["legacyEntries"].pop()
|
|
229
|
+
elif payload["receipts"]:
|
|
230
|
+
payload["receipts"].pop()
|
|
231
|
+
elif payload["entries"]:
|
|
232
|
+
payload["entries"].pop()
|
|
233
|
+
else:
|
|
234
|
+
break
|
|
235
|
+
emit(payload)
|
|
236
|
+
`;
|
|
237
|
+
const ListAvailableRefsInputSchema = z.object({
|
|
238
|
+
path: z
|
|
239
|
+
.string()
|
|
240
|
+
.max(MAX_INPUT_PATH_BYTES)
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("Directory beneath /mnt/refs to inspect — a returned absolute path or a store-relative path. Use it to drill into a subtree when a listing is truncated."),
|
|
243
|
+
category: z
|
|
244
|
+
.string()
|
|
245
|
+
.max(MAX_INPUT_PATH_BYTES)
|
|
246
|
+
.optional()
|
|
247
|
+
.describe('Shorthand for a top-level group to inspect (e.g. "atlas_singlecell", "msigdb"); the groups present come back in `categories`. Ignored when `path` is given.'),
|
|
248
|
+
query: z
|
|
249
|
+
.string()
|
|
250
|
+
.optional()
|
|
251
|
+
.describe("Case-insensitive substring filter over each entry's path and its dataset, title, category, organism, and subtype labels."),
|
|
252
|
+
limit: z
|
|
253
|
+
.number()
|
|
254
|
+
.int()
|
|
255
|
+
.min(1)
|
|
256
|
+
.max(MAX_ENTRIES)
|
|
257
|
+
.optional()
|
|
258
|
+
.describe(`Max entries to return (default ${MAX_ENTRIES}); the response carries \`returned\`, \`total\`, and \`hasMore\` so truncation is never silent.`),
|
|
259
|
+
});
|
|
260
|
+
const RawScanSchema = z.object({
|
|
261
|
+
state: z.enum(["unavailable", "empty", "populated", "not_found", "not_a_directory", "symlink"]),
|
|
262
|
+
entries: z.array(z.discriminatedUnion("kind", [
|
|
263
|
+
z.object({ path: z.string().startsWith(`${REFS_ROOT}/`), kind: z.literal("file"), bytes: z.number().nonnegative() }),
|
|
264
|
+
z.object({ path: z.string().startsWith(`${REFS_ROOT}/`), kind: z.literal("symlink"), bytes: z.literal(0) }),
|
|
265
|
+
z.object({
|
|
266
|
+
path: z.string().startsWith(`${REFS_ROOT}/`),
|
|
267
|
+
kind: z.literal("directory"),
|
|
268
|
+
bytes: z.number().nonnegative(),
|
|
269
|
+
fileCount: z.number().int().nonnegative(),
|
|
270
|
+
fileTypes: z.array(z.string()),
|
|
271
|
+
truncated: z.boolean(),
|
|
272
|
+
}),
|
|
273
|
+
])),
|
|
274
|
+
scannedEntries: z.number().int().nonnegative(),
|
|
275
|
+
truncated: z.boolean(),
|
|
276
|
+
receipts: z.array(z.unknown()).optional(),
|
|
277
|
+
legacyEntries: z.array(z.unknown()).optional(),
|
|
278
|
+
});
|
|
279
|
+
function resolveRequestedPath(input) {
|
|
280
|
+
if (input === undefined || input === "" || input === REFS_ROOT)
|
|
281
|
+
return { ok: true, path: REFS_ROOT };
|
|
282
|
+
if (Buffer.byteLength(input, "utf8") > MAX_INPUT_PATH_BYTES) {
|
|
283
|
+
return { ok: false, reason: `Path exceeds the ${MAX_INPUT_PATH_BYTES}-byte POSIX path limit.` };
|
|
284
|
+
}
|
|
285
|
+
if (input.includes("\0") || input.includes("\\"))
|
|
286
|
+
return { ok: false, reason: "Path must use safe POSIX segments beneath /mnt/refs." };
|
|
287
|
+
const relative = input.startsWith(`${REFS_ROOT}/`) ? input.slice(REFS_ROOT.length + 1) : input.startsWith("/") ? undefined : input;
|
|
288
|
+
if (relative === undefined || relative.length === 0)
|
|
289
|
+
return { ok: false, reason: "Path is outside /mnt/refs." };
|
|
290
|
+
const segments = relative.split("/");
|
|
291
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
292
|
+
return { ok: false, reason: "Path traversal and empty path segments are not allowed." };
|
|
73
293
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
294
|
+
if (segments[0] === ".inflexa" || relative === "registry.json")
|
|
295
|
+
return { ok: false, reason: "Installer metadata is not reference data." };
|
|
296
|
+
const normalized = posixPath.join(REFS_ROOT, relative);
|
|
297
|
+
if (!normalized.startsWith(`${REFS_ROOT}/`))
|
|
298
|
+
return { ok: false, reason: "Path is outside /mnt/refs." };
|
|
299
|
+
return { ok: true, path: normalized };
|
|
300
|
+
}
|
|
301
|
+
function stringValue(value) {
|
|
302
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
303
|
+
}
|
|
304
|
+
function numberValue(value) {
|
|
305
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
306
|
+
}
|
|
307
|
+
function enrichEntries(entries, raw) {
|
|
308
|
+
const metadata = new Map();
|
|
309
|
+
for (const value of raw.receipts ?? []) {
|
|
310
|
+
const receipt = parseReferenceInstallReceipt(value);
|
|
311
|
+
if (!receipt)
|
|
312
|
+
continue;
|
|
313
|
+
const dataset = REFERENCE_DATA_CATALOG.datasets.find((candidate) => candidate.id === receipt.datasetId && candidate.version === receipt.datasetVersion);
|
|
314
|
+
for (const artifact of receipt.artifacts) {
|
|
315
|
+
metadata.set(`${REFS_ROOT}/managed/${receipt.datasetId}/${receipt.datasetVersion}/${artifact.path}`, {
|
|
316
|
+
datasetId: receipt.datasetId,
|
|
317
|
+
version: receipt.datasetVersion,
|
|
318
|
+
...(dataset
|
|
319
|
+
? {
|
|
320
|
+
title: dataset.title,
|
|
321
|
+
description: dataset.description,
|
|
322
|
+
sourceUrl: dataset.sourceUrl,
|
|
323
|
+
license: dataset.license.url ?? dataset.license.identifier,
|
|
324
|
+
}
|
|
325
|
+
: {}),
|
|
326
|
+
});
|
|
82
327
|
}
|
|
83
328
|
}
|
|
329
|
+
for (const value of raw.legacyEntries ?? []) {
|
|
330
|
+
if (!value || typeof value !== "object")
|
|
331
|
+
continue;
|
|
332
|
+
const item = value;
|
|
333
|
+
const localPath = stringValue(item.local_path);
|
|
334
|
+
if (!localPath)
|
|
335
|
+
continue;
|
|
336
|
+
const resolved = resolveRequestedPath(localPath);
|
|
337
|
+
if (!resolved.ok)
|
|
338
|
+
continue;
|
|
339
|
+
const legacyMetadata = {
|
|
340
|
+
...(stringValue(item.dataset) ? { datasetId: stringValue(item.dataset) } : {}),
|
|
341
|
+
...(stringValue(item.category) ? { category: stringValue(item.category) } : {}),
|
|
342
|
+
...(stringValue(item.subtype) ? { subtype: stringValue(item.subtype) } : {}),
|
|
343
|
+
...(stringValue(item.organism) ? { organism: stringValue(item.organism) } : {}),
|
|
344
|
+
...(numberValue(item.rows) !== undefined ? { rows: numberValue(item.rows) } : {}),
|
|
345
|
+
...(stringValue(item.endpoint) ? { endpoint: stringValue(item.endpoint) } : {}),
|
|
346
|
+
};
|
|
347
|
+
metadata.set(resolved.path, { ...legacyMetadata, ...metadata.get(resolved.path) });
|
|
348
|
+
}
|
|
349
|
+
return entries.map((entry) => {
|
|
350
|
+
const extra = metadata.get(entry.path);
|
|
351
|
+
return extra ? { ...entry, metadata: extra } : entry;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/** Case-insensitive substring match over an inventory entry's path and joined metadata labels. */
|
|
355
|
+
function entryMatchesQuery(entry, needle) {
|
|
356
|
+
const fields = [entry.path, entry.metadata?.datasetId, entry.metadata?.title, entry.metadata?.category, entry.metadata?.subtype, entry.metadata?.organism];
|
|
357
|
+
return fields.some((field) => typeof field === "string" && field.toLowerCase().includes(needle));
|
|
358
|
+
}
|
|
359
|
+
/** The drill-in index: directory groups present in the scan, plus any category labels joined from metadata. */
|
|
360
|
+
function collectCategories(entries) {
|
|
361
|
+
const names = new Set();
|
|
362
|
+
for (const entry of entries) {
|
|
363
|
+
if (entry.kind === "directory")
|
|
364
|
+
names.add(posixPath.basename(entry.path));
|
|
365
|
+
if (entry.metadata?.category)
|
|
366
|
+
names.add(entry.metadata.category);
|
|
367
|
+
}
|
|
368
|
+
return [...names].sort();
|
|
369
|
+
}
|
|
370
|
+
function renderContent(path, scan, entries) {
|
|
371
|
+
if (scan.state === "unavailable")
|
|
372
|
+
return "Reference store is not mounted at /mnt/refs. Reference data cannot be downloaded from sandbox workloads; provision it through the host setup flow.";
|
|
373
|
+
if (scan.state === "not_found")
|
|
374
|
+
return `Reference path does not exist: ${path}`;
|
|
375
|
+
if (scan.state === "not_a_directory")
|
|
376
|
+
return `Reference path is not a directory: ${path}`;
|
|
377
|
+
if (scan.state === "symlink")
|
|
378
|
+
return `Reference path crosses a symlink and was not scanned: ${path}`;
|
|
379
|
+
if (scan.state === "empty")
|
|
380
|
+
return `Reference store path is mounted but contains no reference files: ${path}`;
|
|
381
|
+
const lines = [`Reference files visible beneath ${path}:`];
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
const labels = entry.metadata
|
|
384
|
+
? [entry.metadata.title ?? entry.metadata.datasetId, entry.metadata.version, entry.metadata.category, entry.metadata.organism]
|
|
385
|
+
.filter(Boolean)
|
|
386
|
+
.join(", ")
|
|
387
|
+
: "";
|
|
388
|
+
const details = entry.kind === "directory"
|
|
389
|
+
? `directory, ${entry.fileCount} files, ${entry.bytes} bytes${entry.fileTypes.length > 0 ? `, types: ${entry.fileTypes.join(", ")}` : ""}${entry.truncated ? ", summary truncated" : ""}`
|
|
390
|
+
: entry.kind === "file"
|
|
391
|
+
? `file, ${entry.bytes} bytes`
|
|
392
|
+
: "symlink (not followed)";
|
|
393
|
+
lines.push(`- ${entry.path} (${details}${labels ? `; ${labels}` : ""})`);
|
|
394
|
+
}
|
|
395
|
+
if (scan.truncated)
|
|
396
|
+
lines.push(`Inventory truncated after scanning ${scan.scannedEntries} entries. Call list_available_refs with a narrower path to drill down.`);
|
|
84
397
|
return lines.join("\n");
|
|
85
398
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
content: "Reference store not available. The reference store may not be mounted at /mnt/refs. " +
|
|
141
|
-
"Gene mappings, UniProt, OmniPath, Reactome, PROGENy, CollecTRI, DoRothEA, " +
|
|
142
|
-
"WikiPathways, MSigDB, LINCS L1000, HPA, and the curated off-target safety panel " +
|
|
143
|
-
"cannot be loaded from pre-staged files.",
|
|
399
|
+
/** Create replay-safe reference discovery bound to the active sandbox. */
|
|
400
|
+
export function createListAvailableRefsTool(deps) {
|
|
401
|
+
return defineTool({
|
|
402
|
+
id: "list_available_refs",
|
|
403
|
+
executionMode: "workflow",
|
|
404
|
+
description: "Inspect the reference files actually mounted read-only at /mnt/refs — the ONLY reference data that exists here; nothing can be downloaded at runtime. " +
|
|
405
|
+
"Returns managed and user-added files even when no manifest names them; valid receipts and legacy registry data only add labels (dataset, category, organism, subtype). Symlinks are never followed. " +
|
|
406
|
+
"Results are ALWAYS bounded — prefer a targeted query over a full dump: " +
|
|
407
|
+
"`path` inspects one directory beneath /mnt/refs (a returned path, or store-relative) — drill in with it when a listing is truncated; " +
|
|
408
|
+
'`category` is shorthand for a top-level group (e.g. "atlas_singlecell", "msigdb"), and the groups present come back in `categories`; ' +
|
|
409
|
+
"`query` is a case-insensitive substring filter over each entry's path and its dataset/title/category/organism/subtype labels; " +
|
|
410
|
+
"`limit` caps the entries returned. The response carries `returned`, `total`, and `hasMore`, so truncation is never silent. " +
|
|
411
|
+
"Provision additional files through the host and re-run in a sandbox with that store mounted.",
|
|
412
|
+
inputSchema: ListAvailableRefsInputSchema,
|
|
413
|
+
execute: async ({ path, category, query, limit }, ctx) => {
|
|
414
|
+
// `category` is shorthand for a top-level path; an explicit `path` wins.
|
|
415
|
+
const requestedInput = path ?? category;
|
|
416
|
+
const requested = resolveRequestedPath(requestedInput);
|
|
417
|
+
if (!requested.ok) {
|
|
418
|
+
const response = {
|
|
419
|
+
available: false,
|
|
420
|
+
state: "out_of_scope",
|
|
421
|
+
path: requestedInput !== undefined && Buffer.byteLength(requestedInput, "utf8") <= MAX_INPUT_PATH_BYTES ? requestedInput : REFS_ROOT,
|
|
422
|
+
entries: [],
|
|
423
|
+
scannedEntries: 0,
|
|
424
|
+
returned: 0,
|
|
425
|
+
total: 0,
|
|
426
|
+
truncated: false,
|
|
427
|
+
hasMore: false,
|
|
428
|
+
categories: [],
|
|
429
|
+
content: requested.reason,
|
|
430
|
+
};
|
|
431
|
+
return ok(response);
|
|
432
|
+
}
|
|
433
|
+
const execId = `${deps.workflowId}:${deps.stepId}:${deps.nextFunctionId()}`;
|
|
434
|
+
const result = await runSandboxExec({
|
|
435
|
+
sandboxClient: deps.sandboxClient,
|
|
436
|
+
sandbox: deps.sandbox,
|
|
437
|
+
execId,
|
|
438
|
+
command: [
|
|
439
|
+
"python3",
|
|
440
|
+
"-c",
|
|
441
|
+
SCAN_SCRIPT,
|
|
442
|
+
deps.scanRoot ?? REFS_ROOT,
|
|
443
|
+
posixPath.relative(REFS_ROOT, requested.path) || ".",
|
|
444
|
+
String(MAX_ENTRIES),
|
|
445
|
+
String(MAX_SCANNED_ENTRIES),
|
|
446
|
+
String(MAX_PATH_BYTES),
|
|
447
|
+
String(MAX_ENVELOPE_BYTES),
|
|
448
|
+
],
|
|
449
|
+
timeoutSeconds: 20,
|
|
450
|
+
deadlineMs: deps.deadlineMs(),
|
|
451
|
+
emit: ctx.emit,
|
|
452
|
+
...(deps.markExecActive ? { markExecActive: deps.markExecActive } : {}),
|
|
144
453
|
});
|
|
454
|
+
if (result.exitCode !== 0)
|
|
455
|
+
throw new Error(`Reference inventory command failed (${result.exitCode}): ${result.stderr}`);
|
|
456
|
+
if (Buffer.byteLength(result.stdout, "utf8") > MAX_ENVELOPE_BYTES + 1) {
|
|
457
|
+
throw new Error("Reference inventory response exceeded its output bound");
|
|
458
|
+
}
|
|
459
|
+
const raw = RawScanSchema.parse(JSON.parse(result.stdout));
|
|
460
|
+
const enriched = enrichEntries(raw.entries, raw);
|
|
461
|
+
const categories = collectCategories(enriched);
|
|
462
|
+
const needle = query?.trim().toLowerCase();
|
|
463
|
+
const matched = needle ? enriched.filter((entry) => entryMatchesQuery(entry, needle)) : enriched;
|
|
464
|
+
const total = matched.length;
|
|
465
|
+
const cap = limit ?? MAX_ENTRIES;
|
|
466
|
+
let boundedEntries = matched.length > cap ? matched.slice(0, cap) : matched;
|
|
467
|
+
const build = (list, contentPath) => {
|
|
468
|
+
const truncated = raw.truncated || list.length < total;
|
|
469
|
+
return {
|
|
470
|
+
available: raw.state !== "unavailable",
|
|
471
|
+
state: raw.state,
|
|
472
|
+
path: contentPath,
|
|
473
|
+
entries: list,
|
|
474
|
+
scannedEntries: raw.scannedEntries,
|
|
475
|
+
returned: list.length,
|
|
476
|
+
total,
|
|
477
|
+
truncated,
|
|
478
|
+
hasMore: truncated,
|
|
479
|
+
categories,
|
|
480
|
+
content: renderContent(contentPath, { ...raw, truncated }, list),
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
let response = build(boundedEntries, requested.path);
|
|
484
|
+
while (Buffer.byteLength(JSON.stringify(response), "utf8") > MAX_ENVELOPE_BYTES && boundedEntries.length > 0) {
|
|
485
|
+
boundedEntries = boundedEntries.slice(0, -1);
|
|
486
|
+
response = build(boundedEntries, requested.path);
|
|
487
|
+
}
|
|
488
|
+
if (Buffer.byteLength(JSON.stringify(response), "utf8") > MAX_ENVELOPE_BYTES) {
|
|
489
|
+
response = {
|
|
490
|
+
...build([], REFS_ROOT),
|
|
491
|
+
content: "Reference inventory output exceeded its bound. Call list_available_refs with a shorter, narrower path.",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return ok(response);
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const CATEGORY_LABELS = {
|
|
499
|
+
atlas_singlecell: "Single-cell reference atlases for label transfer (Pan-Cancer T cell — " +
|
|
500
|
+
"Zheng 2021, Pan-Cancer Myeloid — Cheng 2021, Tumor Immune Cell Atlas — " +
|
|
501
|
+
"Nieto 2021, Tabula Sapiens v2). Available as .h5ad (Python: anndata, " +
|
|
502
|
+
"scanpy, scvi-tools, symphonypy) and/or .rds (R: Seurat, ProjecTILs). " +
|
|
503
|
+
"Tabula Sapiens ships full + immune_subset variants. " +
|
|
504
|
+
"Use these for label transfer BEFORE falling back to marker genes.",
|
|
505
|
+
atlas_azimuth: "Azimuth references (Seurat-native): PBMC v1.0.0 and Tonsil v1.0.0. " +
|
|
506
|
+
"Each entry is a (ref.Rds, idx.annoy) pair stored under " +
|
|
507
|
+
"/mnt/refs/atlas_azimuth/{pbmc,tonsil}/. Load with " +
|
|
508
|
+
"Azimuth::LoadReference(path) where path is the directory containing " +
|
|
509
|
+
"ref.Rds + idx.annoy (e.g. '/mnt/refs/atlas_azimuth/pbmc'), then " +
|
|
510
|
+
"Azimuth::RunAzimuth(query, reference = path).",
|
|
511
|
+
atlas_projectils: "ProjecTILs reference atlases (Seurat .rds): human + mouse, CD8 + CD4. " +
|
|
512
|
+
"Use ProjecTILs::ProjecTILs.classifier() to assign every CD8 to one of " +
|
|
513
|
+
"the canonical TCF7+/GZMK+/TEX/TPEX/MAIT substates without re-deriving " +
|
|
514
|
+
"the classifier from scratch.",
|
|
515
|
+
celltypist_models: "Pre-staged CellTypist .pkl classifier models (Immune_All_Low backed by " +
|
|
516
|
+
"the Cross-Tissue Immune Cell Atlas; Immune_All_High; Pan_Fetal_Human; " +
|
|
517
|
+
"COVID19_Immune_Landscape). CELLTYPIST_FOLDER env var is preset, so " +
|
|
518
|
+
"celltypist.annotate_cells(model='Immune_All_Low') resolves with no " +
|
|
519
|
+
"network call.",
|
|
520
|
+
marker_panels: "Marker gene panels: PanglaoDB (community, parquet long-format), " +
|
|
521
|
+
"CellMarker 2.0 (tumor-context, parquet long-format), and the Cortex " +
|
|
522
|
+
"hand-curated panel for Stage 1 substates (TCF7+ memory CD8, exhausted " +
|
|
523
|
+
"CD8, effector-memory CD8, proliferating T, cDC1/cDC2/LAMP3+ DC, SPP1+ " +
|
|
524
|
+
"TAM, C1Q+ TAM, Tregs) — JSON with full provenance.",
|
|
525
|
+
gene_signatures: "Curated gene signatures from key tumor-immune papers: Sade-Feldman " +
|
|
526
|
+
"2018 exhausted CD8 (Table S2), Jerby-Arnon 2018 malignant resistance " +
|
|
527
|
+
"program (Table S1, Stage 3 classifier substrate), Tirosh 2016 AXL/MITF " +
|
|
528
|
+
"programs, Puram 2017 p-EMT (Table S5, HNSCC), Reactome cGAS-STING " +
|
|
529
|
+
"pathway (Stage 2.3 type-I IFN). JSON with PMID/DOI per entry.",
|
|
530
|
+
normal_reference: "Normal-tissue references for safety firing checks: GTEx v8 per-tissue " +
|
|
531
|
+
"median TPM (parquet) + sample attributes (parquet). Use to quantify " +
|
|
532
|
+
"off-target activation of any nominated classifier across healthy tissues.",
|
|
533
|
+
gene_mappings: "Gene ID Conversion Tables: Entrez/Ensembl/RefSeq/UniProt to symbol (NCBI); " +
|
|
534
|
+
"orthologs (Ensembl Compara) — file: orthologs_{tax_id}.parquet, columns: " +
|
|
535
|
+
"tax_id, ensembl_gene_id, entrez_id, symbol, relationship, other_tax_id, " +
|
|
536
|
+
"other_ensembl_gene_id, other_entrez_id, other_symbol; symmetric — to find " +
|
|
537
|
+
"human orthologs of Macaca fascicularis genes, open orthologs_9541.parquet " +
|
|
538
|
+
"and filter other_tax_id == '9606'; cross-division (e.g. yeast↔mouse) " +
|
|
539
|
+
"requires pivoting through human: yeast→human via orthologs_4896.parquet, " +
|
|
540
|
+
"then human→mouse via orthologs_9606.parquet",
|
|
541
|
+
omnipath: "OmniPath (interactions, regulons, annotations)",
|
|
542
|
+
reactome: "Reactome Pathways",
|
|
543
|
+
progeny: "PROGENy Pathway Weights (decoupler: source/target/weight/padj)",
|
|
544
|
+
collectri: "CollecTRI TF-Target Regulons (decoupler: source/target/mor)",
|
|
545
|
+
dorothea: "DoRothEA TF Regulons (decoupler: source/target/weight/confidence)",
|
|
546
|
+
lincs: "LINCS L1000 Drug Perturbation Consensus Signatures (up/down gene sets)",
|
|
547
|
+
hpa: "Human Protein Atlas (tissue RNA expression, target safety/secretome, full 107-column atlas)",
|
|
548
|
+
wikipathways: "WikiPathways Gene Sets",
|
|
549
|
+
msigdb: "MSigDB Gene Set Collections",
|
|
550
|
+
safety_targets: "Curated Off-Target Safety Panel (CSV + JSON: chembl_id, gene_symbol, uniprot, organ_system, severity, clinical_consequence)",
|
|
551
|
+
};
|
|
552
|
+
const CATEGORY_ORDER = [
|
|
553
|
+
"atlas_singlecell",
|
|
554
|
+
"atlas_azimuth",
|
|
555
|
+
"atlas_projectils",
|
|
556
|
+
"celltypist_models",
|
|
557
|
+
"marker_panels",
|
|
558
|
+
"gene_signatures",
|
|
559
|
+
"normal_reference",
|
|
560
|
+
"safety_targets",
|
|
561
|
+
"progeny",
|
|
562
|
+
"collectri",
|
|
563
|
+
"dorothea",
|
|
564
|
+
"omnipath",
|
|
565
|
+
"lincs",
|
|
566
|
+
"msigdb",
|
|
567
|
+
"wikipathways",
|
|
568
|
+
"reactome",
|
|
569
|
+
"hpa",
|
|
570
|
+
"gene_mappings",
|
|
571
|
+
];
|
|
572
|
+
/** The registry's categories in canonical order, unknown ones appended. */
|
|
573
|
+
function orderedCategories(registry) {
|
|
574
|
+
const byCategory = registry.files.by_category;
|
|
575
|
+
const known = new Set(CATEGORY_ORDER);
|
|
576
|
+
return [...CATEGORY_ORDER.filter((c) => byCategory[c]), ...Object.keys(byCategory).filter((c) => !known.has(c))];
|
|
577
|
+
}
|
|
578
|
+
/** Case-insensitive substring match over the descriptive fields the registry carries. */
|
|
579
|
+
function matchesQuery(category, entry, needle) {
|
|
580
|
+
const haystack = [category, entry.local_path, entry.dataset, entry.organism, entry.subtype, entry.tax_id].filter((v) => typeof v === "string" && v.length > 0);
|
|
581
|
+
return haystack.some((field) => field.toLowerCase().includes(needle));
|
|
582
|
+
}
|
|
583
|
+
/** One file line: `name: /mnt/refs/<local_path> (rows, subtype, dataset)`. */
|
|
584
|
+
function renderEntry(entry) {
|
|
585
|
+
const path = join(REFS_ROOT, entry.local_path);
|
|
586
|
+
const name = basename(entry.local_path);
|
|
587
|
+
const info = [entry.rows != null ? `${entry.rows.toLocaleString()} rows` : null, entry.subtype, entry.dataset].filter(Boolean).join(", ");
|
|
588
|
+
return ` ${name}: ${path}${info ? ` (${info})` : ""}`;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Answer a refs query against a parsed registry. Pure — the tool's `execute` is
|
|
592
|
+
* only the file read plus this call.
|
|
593
|
+
*/
|
|
594
|
+
export function queryRefs(registry, { category, query, limit }) {
|
|
595
|
+
const categories = orderedCategories(registry);
|
|
596
|
+
// An unknown category is a caller mistake, not a failure — name the valid set
|
|
597
|
+
// rather than returning an empty listing with no explanation.
|
|
598
|
+
if (category && !categories.includes(category)) {
|
|
599
|
+
return {
|
|
600
|
+
available: true,
|
|
601
|
+
total: 0,
|
|
602
|
+
returned: 0,
|
|
603
|
+
hasMore: false,
|
|
604
|
+
categories,
|
|
605
|
+
content: `Unknown category "${category}". Valid categories: ${categories.join(", ")}.`,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const needle = query?.trim().toLowerCase();
|
|
609
|
+
const selected = category ? [category] : categories;
|
|
610
|
+
// Filter first so `total` is the honest count of matching files, then bound.
|
|
611
|
+
const matched = selected.map((c) => {
|
|
612
|
+
const entries = registry.files.by_category[c] ?? [];
|
|
613
|
+
return { category: c, entries: needle ? entries.filter((e) => matchesQuery(c, e, needle)) : entries };
|
|
614
|
+
});
|
|
615
|
+
const total = matched.reduce((n, m) => n + m.entries.length, 0);
|
|
616
|
+
if (total === 0) {
|
|
617
|
+
const scope = [category ? `category: ${category}` : null, query ? `query: "${query}"` : null].filter(Boolean).join(", ");
|
|
618
|
+
return {
|
|
619
|
+
available: true,
|
|
620
|
+
total: 0,
|
|
621
|
+
returned: 0,
|
|
622
|
+
hasMore: false,
|
|
623
|
+
categories,
|
|
624
|
+
content: `No reference files match this filter${scope ? ` (${scope})` : ""}. Nothing can be downloaded at runtime.`,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const cap = limit ?? DEFAULT_LIMIT;
|
|
628
|
+
const lines = [];
|
|
629
|
+
let returned = 0;
|
|
630
|
+
for (const { category: c, entries } of matched) {
|
|
631
|
+
if (entries.length === 0)
|
|
632
|
+
continue;
|
|
633
|
+
lines.push(`\n## ${CATEGORY_LABELS[c] ?? c}`);
|
|
634
|
+
const room = cap - returned;
|
|
635
|
+
if (room <= 0) {
|
|
636
|
+
lines.push(` ${entries.length} file(s) — not shown (limit reached). Call with category: "${c}" to list them.`);
|
|
637
|
+
continue;
|
|
145
638
|
}
|
|
146
|
-
|
|
147
|
-
|
|
639
|
+
const shown = entries.slice(0, room);
|
|
640
|
+
returned += shown.length;
|
|
641
|
+
for (const entry of shown)
|
|
642
|
+
lines.push(renderEntry(entry));
|
|
643
|
+
const hidden = entries.length - shown.length;
|
|
644
|
+
if (hidden > 0)
|
|
645
|
+
lines.push(` … and ${hidden} more in this category — call with category: "${c}" (and a higher \`limit\`) to list them.`);
|
|
646
|
+
}
|
|
647
|
+
const header = [
|
|
648
|
+
`Reference Store (build ${registry.build_id}, ${registry.generated_at})`,
|
|
649
|
+
`Mount: ${REFS_ROOT}`,
|
|
650
|
+
`Matching files: ${total} (showing ${returned})`,
|
|
651
|
+
].join("\n");
|
|
652
|
+
return {
|
|
653
|
+
available: true,
|
|
654
|
+
total,
|
|
655
|
+
returned,
|
|
656
|
+
hasMore: returned < total,
|
|
657
|
+
categories,
|
|
658
|
+
content: header + "\n" + lines.join("\n"),
|
|
659
|
+
};
|
|
660
|
+
}
|