@inflexa-ai/harness 0.46.2 → 0.48.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/README.md +2 -2
- package/dist/agents/conversation-agent.d.ts +7 -6
- package/dist/agents/conversation-agent.js +26 -4
- package/dist/agents/report-session-agent.d.ts +14 -14
- package/dist/agents/report-session-agent.js +40 -26
- package/dist/agents/sandbox/shared.d.ts +18 -13
- package/dist/agents/sandbox/shared.js +42 -27
- package/dist/agents/sandbox/types.d.ts +1 -1
- package/dist/agents/sandbox-catalog.d.ts +2 -0
- package/dist/agents/sandbox-catalog.js +30 -1
- package/dist/app/synthesize-run.d.ts +10 -6
- package/dist/app/synthesize-run.js +4 -2
- package/dist/contracts/analogy-report.d.ts +46 -2
- package/dist/contracts/analogy-report.js +31 -25
- package/dist/contracts/chat-parts.d.ts +3 -14
- package/dist/contracts/report-reference.d.ts +2 -1
- package/dist/contracts/report-reference.js +2 -1
- package/dist/contracts/schemas/chat-parts.d.ts +0 -27
- package/dist/contracts/schemas/chat-parts.js +1 -3
- package/dist/execution/run-canceler.d.ts +2 -0
- package/dist/execution/run-canceler.js +9 -0
- package/dist/execution/run-synthesis.d.ts +36 -8
- package/dist/execution/run-synthesis.js +114 -32
- package/dist/index.d.ts +5 -6
- package/dist/index.js +5 -7
- package/dist/input-scan/enrich.d.ts +5 -10
- package/dist/input-scan/enrich.js +12 -8
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/lib/markdown-pdf.d.ts +27 -0
- package/dist/lib/markdown-pdf.js +236 -0
- package/dist/lib/metrics.d.ts +31 -0
- package/dist/lib/metrics.js +64 -0
- package/dist/lib/pdf-text.d.ts +23 -0
- package/dist/lib/pdf-text.js +34 -0
- package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
- package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
- package/dist/loop/chat-stream.js +2 -1
- package/dist/loop/run-loop.d.ts +5 -0
- package/dist/loop/run-loop.js +30 -2
- package/dist/loop/tool-name-repair.d.ts +11 -0
- package/dist/loop/tool-name-repair.js +23 -0
- package/dist/loop/types.d.ts +19 -0
- package/dist/memory/card-builders.d.ts +1 -1
- package/dist/prompts/SOUL.d.ts +6 -6
- package/dist/prompts/SOUL.js +22 -121
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +35 -101
- package/dist/prompts/briefing.d.ts +20 -11
- package/dist/prompts/briefing.js +36 -19
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +103 -383
- package/dist/prompts/planner.d.ts +1 -5
- package/dist/prompts/planner.js +45 -128
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +29 -58
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +4 -5
- package/dist/prompts/sandbox/data-profiler.js +11 -34
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -1
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +3 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +3 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -1
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +0 -12
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +3 -1
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
- package/dist/prompts/sandbox-standards.d.ts +15 -18
- package/dist/prompts/sandbox-standards.js +96 -305
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +24 -90
- package/dist/provenance/collector.d.ts +2 -5
- package/dist/provenance/collector.js +2 -5
- package/dist/provenance/seam.d.ts +36 -1
- package/dist/provenance/workspace-reads.d.ts +36 -0
- package/dist/provenance/workspace-reads.js +33 -0
- package/dist/report-model/production-resolver.d.ts +22 -52
- package/dist/report-model/production-resolver.js +43 -112
- package/dist/report-render/chart-export.d.ts +9 -0
- package/dist/report-render/chart-export.js +1 -1
- package/dist/report-render/views/chart-view.d.ts +1 -0
- package/dist/report-render/views/chart-view.js +1 -0
- package/dist/report-render/views/lineage.d.ts +1 -0
- package/dist/report-render/views/page-view.d.ts +1 -0
- package/dist/report-render/views/page-view.js +1 -0
- package/dist/report-render/views/prose.d.ts +1 -0
- package/dist/report-render/views/prose.js +1 -0
- package/dist/report-render/views/references-view.d.ts +1 -0
- package/dist/report-render/views/values.d.ts +1 -0
- package/dist/report-render/views/values.js +1 -0
- package/dist/runtime/assemble.d.ts +32 -12
- package/dist/runtime/assemble.js +49 -38
- package/dist/sandbox/client.d.ts +5 -3
- package/dist/sandbox/create-sandbox.d.ts +7 -12
- package/dist/sandbox/create-sandbox.js +15 -37
- package/dist/sandbox/docker-client.d.ts +1 -3
- package/dist/sandbox/docker-client.js +3 -5
- package/dist/sandbox/exec-outcome.d.ts +11 -11
- package/dist/sandbox/exec-outcome.js +22 -22
- package/dist/sandbox/exec.d.ts +5 -0
- package/dist/sandbox/exec.js +9 -1
- package/dist/sandbox/identity.d.ts +4 -16
- package/dist/sandbox/identity.js +5 -21
- package/dist/sandbox/k8s-client.d.ts +0 -1
- package/dist/sandbox/k8s-client.js +8 -13
- package/dist/sandbox/lease-names.d.ts +6 -0
- package/dist/sandbox/lease-names.js +12 -0
- package/dist/sandbox/leases.d.ts +105 -0
- package/dist/sandbox/leases.js +519 -0
- package/dist/sandbox/liveness.d.ts +5 -2
- package/dist/sandbox/mount-plan.d.ts +10 -1
- package/dist/sandbox/mount-plan.js +7 -1
- package/dist/sandbox/reconciler.d.ts +58 -0
- package/dist/sandbox/reconciler.js +153 -0
- package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
- package/dist/sandbox/types.d.ts +25 -18
- package/dist/sandbox/types.js +10 -4
- package/dist/schemas/plan-schemas.d.ts +0 -21
- package/dist/schemas/plan-schemas.js +2 -3
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/schemas/run-synthesis.js +3 -3
- package/dist/schemas/validate-plan.d.ts +1 -14
- package/dist/schemas/validate-plan.js +5 -24
- package/dist/schemas/workflow-state.d.ts +3 -32
- package/dist/schemas/workflow-state.js +4 -13
- package/dist/state/index.d.ts +2 -3
- package/dist/state/index.js +1 -2
- package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
- package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
- package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
- package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
- package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
- package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
- package/dist/state/migrations/index.js +6 -0
- package/dist/state/purge-analysis.d.ts +4 -2
- package/dist/state/purge-analysis.js +22 -11
- package/dist/state/sandbox-leases.d.ts +81 -0
- package/dist/state/sandbox-leases.js +247 -0
- package/dist/state/schema.d.ts +1 -13
- package/dist/state/schema.js +1 -6
- package/dist/state/step-executions.d.ts +13 -2
- package/dist/state/step-executions.js +23 -4
- package/dist/tasks/data-profile.d.ts +40 -2
- package/dist/tasks/data-profile.js +89 -87
- package/dist/tools/ad-hoc-router.d.ts +1 -6
- package/dist/tools/ad-hoc-router.js +5 -48
- package/dist/tools/bio/comptox.js +1 -1
- package/dist/tools/bio/drug-gene-interactions.js +1 -1
- package/dist/tools/bio/gene-disease-evidence.js +1 -1
- package/dist/tools/bio/keys.d.ts +1 -1
- package/dist/tools/bio/pubmed.js +31 -0
- package/dist/tools/bio/search-gene.js +17 -0
- package/dist/tools/bio/search-geo-datasets.js +13 -0
- package/dist/tools/bio/search-regulatory-approvals.js +1 -1
- package/dist/tools/define-tool.d.ts +25 -2
- package/dist/tools/define-tool.js +11 -1
- package/dist/tools/display/show-user.js +1 -1
- package/dist/tools/execute-analysis.d.ts +1 -3
- package/dist/tools/execute-analysis.js +5 -33
- package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
- package/dist/tools/report-authoring/authoring-tools.js +31 -6
- package/dist/tools/report-session/index.d.ts +0 -1
- package/dist/tools/report-session/index.js +0 -1
- package/dist/tools/report-session/script-tools.d.ts +33 -0
- package/dist/tools/report-session/script-tools.js +186 -0
- package/dist/tools/research/generate-analogy-report.d.ts +16 -9
- package/dist/tools/research/generate-analogy-report.js +111 -49
- package/dist/tools/research/generate-plan.d.ts +102 -7
- package/dist/tools/research/generate-plan.js +74 -43
- package/dist/tools/research/index.d.ts +4 -3
- package/dist/tools/research/index.js +4 -3
- package/dist/tools/research/inspect-run.d.ts +0 -1
- package/dist/tools/research/inspect-run.js +5 -10
- package/dist/tools/research/search-arxiv.js +15 -0
- package/dist/tools/research/search-github-repos.js +13 -0
- package/dist/tools/research/search-semantic-scholar.js +16 -0
- package/dist/tools/research/synthesis-lookups.d.ts +21 -0
- package/dist/tools/research/synthesis-lookups.js +29 -0
- package/dist/tools/research/web-fetch.d.ts +1 -1
- package/dist/tools/research/web-fetch.js +14 -12
- package/dist/tools/sandbox/lease-tools.d.ts +32 -0
- package/dist/tools/sandbox/lease-tools.js +60 -0
- package/dist/tools/script/archive.d.ts +34 -0
- package/dist/tools/script/archive.js +182 -0
- package/dist/tools/script/codemode-worker.d.ts +5 -0
- package/dist/tools/script/codemode-worker.js +5 -0
- package/dist/tools/script/file-tools.d.ts +106 -0
- package/dist/tools/script/file-tools.js +491 -0
- package/dist/tools/script/metrics.d.ts +30 -0
- package/dist/tools/script/metrics.js +53 -0
- package/dist/tools/script/quickjs.wasm +0 -0
- package/dist/tools/script/run-script.d.ts +76 -0
- package/dist/tools/script/run-script.js +413 -0
- package/dist/tools/script/scratch.d.ts +84 -0
- package/dist/tools/script/scratch.js +145 -0
- package/dist/tools/start-report-session.js +1 -1
- package/dist/tools/workspace/edit-file.js +2 -1
- package/dist/tools/workspace/execute-command.d.ts +9 -13
- package/dist/tools/workspace/execute-command.js +23 -19
- package/dist/tools/workspace/file-stat.js +1 -1
- package/dist/tools/workspace/grep.d.ts +5 -2
- package/dist/tools/workspace/grep.js +19 -10
- package/dist/tools/workspace/mutator.d.ts +51 -4
- package/dist/tools/workspace/mutator.js +54 -13
- package/dist/tools/workspace/read-file.d.ts +5 -2
- package/dist/tools/workspace/read-file.js +15 -11
- package/dist/tools/workspace/read-surface.d.ts +17 -0
- package/dist/tools/workspace/read-surface.js +17 -0
- package/dist/tools/workspace/result-bounds.d.ts +1 -0
- package/dist/tools/workspace/run-exec.d.ts +12 -13
- package/dist/tools/workspace/run-exec.js +13 -13
- package/dist/tools/workspace/scan-inputs.d.ts +2 -5
- package/dist/tools/workspace/scan-inputs.js +2 -3
- package/dist/tools/workspace/show-file.js +1 -1
- package/dist/tools/workspace/show-plan.js +4 -3
- package/dist/tools/workspace/write-file.js +2 -1
- package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
- package/dist/workflows/execute-analysis-scheduler.js +12 -32
- package/dist/workflows/execute-analysis.d.ts +17 -35
- package/dist/workflows/execute-analysis.js +13 -107
- package/dist/workflows/pause.js +29 -22
- package/dist/workflows/sandbox-step.d.ts +16 -31
- package/dist/workflows/sandbox-step.js +36 -71
- package/dist/workflows/test/sql-writes.d.ts +0 -1
- package/dist/workflows/test/sql-writes.js +0 -2
- package/dist/workspace/filesystem.d.ts +2 -2
- package/dist/workspace/filesystem.js +10 -5
- package/dist/workspace/paths.d.ts +19 -1
- package/dist/workspace/paths.js +22 -1
- package/dist/workspace/session-files.d.ts +35 -0
- package/dist/workspace/session-files.js +16 -0
- package/package.json +7 -3
- package/dist/prompts/literature-reviewer.d.ts +0 -1
- package/dist/prompts/literature-reviewer.js +0 -114
- package/dist/sandbox/reaper.d.ts +0 -81
- package/dist/sandbox/reaper.js +0 -161
- package/dist/sandbox/step-sandbox.d.ts +0 -47
- package/dist/sandbox/step-sandbox.js +0 -53
- package/dist/state/active-sandboxes.d.ts +0 -33
- package/dist/state/active-sandboxes.js +0 -65
- package/dist/tasks/extract-values-script.d.ts +0 -64
- package/dist/tasks/extract-values-script.js +0 -180
- package/dist/tasks/extract-values.d.ts +0 -77
- package/dist/tasks/extract-values.js +0 -146
- package/dist/tools/report-session/derive-table.d.ts +0 -164
- package/dist/tools/report-session/derive-table.js +0 -419
- package/dist/tools/research/literature-reviewer.d.ts +0 -42
- package/dist/tools/research/literature-reviewer.js +0 -107
- package/dist/workflows/metrics.d.ts +0 -23
- package/dist/workflows/metrics.js +0 -38
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* `execute_command` — the single chokepoint through which sandbox commands
|
|
3
3
|
* run.
|
|
4
4
|
*
|
|
5
|
-
* Dependency-bearing factory (see the harness-durable-runtime spec): captures
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* across all sandbox command execution.
|
|
5
|
+
* Dependency-bearing factory (see the harness-durable-runtime spec): captures the
|
|
6
|
+
* lease of the step. The sandbox module gives the machine of the lease, or a new
|
|
7
|
+
* one when the machine went away, thus the durability, idempotency, and liveness
|
|
8
|
+
* story of `harness-sandbox-exec` holds for each command. A command with no lease
|
|
9
|
+
* gives an error result that names `acquire_sandbox`.
|
|
11
10
|
*
|
|
12
11
|
* The loop runs each call as one durable step, and the exec runs inside it,
|
|
13
12
|
* thus the exec id is the id of that step and a replay returns the cached
|
|
@@ -27,8 +26,8 @@
|
|
|
27
26
|
*/
|
|
28
27
|
import { z } from "zod";
|
|
29
28
|
import { type Tool, type WithToolCallRecord } from "../define-tool.js";
|
|
30
|
-
import type {
|
|
31
|
-
import type { ProvenanceFrame
|
|
29
|
+
import type { BoundLease } from "../../sandbox/leases.js";
|
|
30
|
+
import type { ProvenanceFrame } from "../../sandbox/types.js";
|
|
32
31
|
import type { ProvenanceCollector } from "../../provenance/collector.js";
|
|
33
32
|
import type { WorkspaceMutator, WriteRecord } from "./mutator.js";
|
|
34
33
|
import { type BoundedExecResult } from "./result-bounds.js";
|
|
@@ -55,11 +54,8 @@ type ExecuteCommandResult = {
|
|
|
55
54
|
export type ExecuteCommandDeps = {
|
|
56
55
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
57
56
|
readonly logger?: Logger;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
readonly sandbox: SandboxRef;
|
|
61
|
-
/** Absolute unix-ms deadline of the exec. Typically derived from `step.timeout`. */
|
|
62
|
-
readonly deadlineMs: () => number;
|
|
57
|
+
/** The lease of the step: `acquire_sandbox` takes it, and each command runs on its machine. */
|
|
58
|
+
readonly lease: BoundLease;
|
|
63
59
|
/**
|
|
64
60
|
* In-sandbox absolute path of the agent's working directory (e.g.
|
|
65
61
|
* `/{resourceId}/runs/{runId}/{stepId}`). Used as the default `cwd` so a
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
* `execute_command` — the single chokepoint through which sandbox commands
|
|
3
3
|
* run.
|
|
4
4
|
*
|
|
5
|
-
* Dependency-bearing factory (see the harness-durable-runtime spec): captures
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
* across all sandbox command execution.
|
|
5
|
+
* Dependency-bearing factory (see the harness-durable-runtime spec): captures the
|
|
6
|
+
* lease of the step. The sandbox module gives the machine of the lease, or a new
|
|
7
|
+
* one when the machine went away, thus the durability, idempotency, and liveness
|
|
8
|
+
* story of `harness-sandbox-exec` holds for each command. A command with no lease
|
|
9
|
+
* gives an error result that names `acquire_sandbox`.
|
|
11
10
|
*
|
|
12
11
|
* The loop runs each call as one durable step, and the exec runs inside it,
|
|
13
12
|
* thus the exec id is the id of that step and a replay returns the cached
|
|
@@ -26,7 +25,7 @@
|
|
|
26
25
|
* code that ran.
|
|
27
26
|
*/
|
|
28
27
|
import { posix as posixPath } from "node:path";
|
|
29
|
-
import { ok } from "neverthrow";
|
|
28
|
+
import { err, ok } from "neverthrow";
|
|
30
29
|
import { z } from "zod";
|
|
31
30
|
import { defineTool, withToolCallRecord } from "../define-tool.js";
|
|
32
31
|
import { feedExecFrame } from "../../provenance/exec-frame.js";
|
|
@@ -39,16 +38,17 @@ const ExecuteCommandInputSchema = z.object({
|
|
|
39
38
|
.array(z.string().min(1))
|
|
40
39
|
.min(1)
|
|
41
40
|
.describe("argv array — e.g. ['python', 'scripts/run.py'] or ['head', '-n', '50', 'data/inputs/x.csv']. " +
|
|
42
|
-
"A one-element array runs through `sh -c`, so pipes, `&&`, quoting, and globbing work
|
|
43
|
-
"an array of two or more elements runs directly, with no shell, and nothing is expanded."),
|
|
41
|
+
"A one-element array runs through `sh -c`, so pipes, `&&`, quoting, and globbing work, and a path that holds a space " +
|
|
42
|
+
"needs double quotes there; an array of two or more elements runs directly, with no shell, and nothing is expanded."),
|
|
44
43
|
cwd: z
|
|
45
44
|
.string()
|
|
46
45
|
.optional()
|
|
47
46
|
.describe("Working directory inside the sandbox. Defaults to your working " +
|
|
48
47
|
"directory; a relative value is resolved against it, an absolute " +
|
|
49
|
-
"'/<analysisId>/...' value is used as-is."
|
|
48
|
+
"'/<analysisId>/...' value is used as-is. Each call starts here: a `cd` in an earlier call " +
|
|
49
|
+
"does not carry over, thus pass `cwd` or chain the commands with `&&` in one call."),
|
|
50
50
|
env: z.record(z.string(), z.string()).optional().describe("Extra environment variables for this command."),
|
|
51
|
-
timeoutSeconds: z.number().int().positive().optional().describe("Per-command timeout in seconds
|
|
51
|
+
timeoutSeconds: z.number().int().positive().optional().describe("Per-command timeout in seconds. Default 3600."),
|
|
52
52
|
});
|
|
53
53
|
/** Extensions that mark an argv token as the thing being run, not a flag or a data path. */
|
|
54
54
|
const SCRIPT_EXTENSIONS = [".py", ".R", ".r", ".sh", ".js", ".ts", ".ipynb"];
|
|
@@ -57,18 +57,21 @@ function scriptToken(command) {
|
|
|
57
57
|
return command.find((arg) => SCRIPT_EXTENSIONS.some((extension) => arg.endsWith(extension)));
|
|
58
58
|
}
|
|
59
59
|
export function createExecuteCommandTool(deps) {
|
|
60
|
-
const {
|
|
60
|
+
const { lease, defaultCwd, lineageCollector, mountRoot } = deps;
|
|
61
61
|
return defineTool({
|
|
62
62
|
id: "execute_command",
|
|
63
|
+
ownStep: true,
|
|
63
64
|
description: "Run a command in the sandbox and return its stdout/stderr/exit code. " +
|
|
64
65
|
"Use for scripts, CLI tools, shell pipes, and anything the workspace " +
|
|
65
66
|
`tools don't express. stdout and stderr are each capped at ${EXEC_STREAM_BYTE_CAP / (1024 * 1024)} MiB; the end ` +
|
|
66
67
|
"of a longer stream is dropped, and the result carries its truncation flag. A long result comes back as an " +
|
|
67
68
|
"excerpt of its start and its end, with a reference to read the rest when the harness keeps it. " +
|
|
68
69
|
"Failures are returned as data, not thrown: `status` is `ok` whenever the call completed, so read " +
|
|
69
|
-
"`exitCode` for the outcome of the command. `timedOut: true` means the timeout stopped the command
|
|
70
|
-
"`syntheticFailure` with `exitCode: null` means the sandbox
|
|
71
|
-
"
|
|
70
|
+
"`exitCode` for the outcome of the command. `timedOut: true` means the timeout stopped the command. A " +
|
|
71
|
+
"`syntheticFailure` with `exitCode: null` means the sandbox ended under the command: `sandbox-oom-killed` " +
|
|
72
|
+
"(the command passed `memoryLimitGb`), `sandbox-dead`, or `sandbox-replaced`. The command may have left " +
|
|
73
|
+
"partial files, and nothing runs it again. The next command runs on a new sandbox of the same size; call " +
|
|
74
|
+
"`acquire_sandbox` with a larger size for more memory. stdout and stderr are not a deliverable: they are ephemeral, " +
|
|
72
75
|
"so use them to confirm that a command ran. A result that you compute with 'python -c' or an inline " +
|
|
73
76
|
"one-liner and read from stdout is lost. Write a script and persist its outputs to output/.",
|
|
74
77
|
inputSchema: ExecuteCommandInputSchema,
|
|
@@ -110,16 +113,17 @@ export function createExecuteCommandTool(deps) {
|
|
|
110
113
|
recordWrites.push(noted.write);
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
sandbox,
|
|
116
|
+
const ran = await runSandboxExec({
|
|
117
|
+
lease,
|
|
116
118
|
command,
|
|
117
119
|
cwd: effectiveCwd,
|
|
118
120
|
env,
|
|
119
121
|
timeoutSeconds,
|
|
120
|
-
deadlineMs: deadlineMs(),
|
|
121
122
|
emit: ctx.emit,
|
|
122
123
|
});
|
|
124
|
+
if (ran.isErr())
|
|
125
|
+
return err(ran.error);
|
|
126
|
+
const result = ran.value;
|
|
123
127
|
const bounded = { status: "ok", ...boundExecResult(result) };
|
|
124
128
|
if ((!lineageCollector || !mountRoot) && recordWrites.length === 0)
|
|
125
129
|
return ok(bounded);
|
|
@@ -19,7 +19,7 @@ export function createFileStatTool(fs, workingDir) {
|
|
|
19
19
|
return defineTool({
|
|
20
20
|
id: "file_stat",
|
|
21
21
|
description: "Stat a workspace path: whether it is a file or directory and its size " +
|
|
22
|
-
"in bytes. Use before reading to size a file. Missing and out-of-scope " +
|
|
22
|
+
"in bytes. It reads the workspace directly and runs no sandbox command. Use before reading to size a file. Missing and out-of-scope " +
|
|
23
23
|
"paths return a data variant, not an error.",
|
|
24
24
|
inputSchema: FileStatInputSchema,
|
|
25
25
|
describeCall: "none",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* `out_of_scope`. Only unexpected I/O failures throw.
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { type Tool } from "../define-tool.js";
|
|
11
|
+
import { type Tool, type WithToolCallRecord } from "../define-tool.js";
|
|
12
|
+
import type { WorkspaceReadLineage, WorkspaceReadRecord } from "../../provenance/workspace-reads.js";
|
|
12
13
|
import type { ListEntry, WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
13
14
|
declare const GrepInputSchema: z.ZodObject<{
|
|
14
15
|
pattern: z.ZodString;
|
|
@@ -49,7 +50,9 @@ type GrepOutput = {
|
|
|
49
50
|
pattern: string;
|
|
50
51
|
matches: Match[];
|
|
51
52
|
};
|
|
52
|
-
|
|
53
|
+
type GrepToolOutput = GrepOutput | WithToolCallRecord<GrepOutput, WorkspaceReadRecord>;
|
|
54
|
+
/** `lineage` records each file whose lines the result returns in the lineage of a step. */
|
|
55
|
+
export declare function createGrepTool(fs: WorkspaceFilesystem, workingDir?: string, lineage?: WorkspaceReadLineage): Tool<z.infer<typeof GrepInputSchema>, GrepToolOutput>;
|
|
53
56
|
/** Exposed for tests — the caps `grep` enforces. */
|
|
54
57
|
export declare const GREP_LIMITS: {
|
|
55
58
|
readonly DEFAULT_MAX_MATCHES: 50;
|
|
@@ -14,7 +14,7 @@ import { ok } from "neverthrow";
|
|
|
14
14
|
import { presentAt } from "../../lib/present.js";
|
|
15
15
|
import { splitLines } from "../../lib/text.js";
|
|
16
16
|
import { z } from "zod";
|
|
17
|
-
import { defineTool } from "../define-tool.js";
|
|
17
|
+
import { defineTool, withToolCallRecord } from "../define-tool.js";
|
|
18
18
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
19
19
|
const DEFAULT_MAX_MATCHES = 50;
|
|
20
20
|
const DEFAULT_MAX_MATCH_BYTES = 512;
|
|
@@ -33,11 +33,12 @@ const GrepInputSchema = z.object({
|
|
|
33
33
|
.describe("Workspace path to a file or directory. Directories are searched " + "recursively up to a bounded depth."),
|
|
34
34
|
ignoreCase: z.boolean().optional().describe("Case-insensitive match. Defaults to false."),
|
|
35
35
|
});
|
|
36
|
-
|
|
36
|
+
/** `lineage` records each file whose lines the result returns in the lineage of a step. */
|
|
37
|
+
export function createGrepTool(fs, workingDir, lineage) {
|
|
37
38
|
return defineTool({
|
|
38
39
|
id: "grep",
|
|
39
40
|
description: "Regex search over the text files of the workspace (input data, prior runs, step " +
|
|
40
|
-
"outputs, summaries, syntheses). Pass a file or a " +
|
|
41
|
+
"outputs, summaries, syntheses). It reads the workspace directly and runs no sandbox command. Pass a file or a " +
|
|
41
42
|
`directory path; directories are walked recursively, to a depth of ${MAX_DIR_DEPTH}. Results are ` +
|
|
42
43
|
`capped at ${DEFAULT_MAX_MATCHES} matches, and each preview at ${DEFAULT_MAX_MATCH_BYTES} characters of its line. ` +
|
|
43
44
|
`Only the first ${MAX_GREP_FILE_BYTES / (1024 * 1024)} MiB of each file is searched, and a file with a NUL byte ` +
|
|
@@ -47,6 +48,7 @@ export function createGrepTool(fs, workingDir) {
|
|
|
47
48
|
// Both fields, because either alone loses the call: the same pattern over
|
|
48
49
|
// two trees and two patterns over one tree are both ordinary sequences.
|
|
49
50
|
describeCall: ({ pattern, path }) => `${pattern} in ${path}`,
|
|
51
|
+
foldCallRecord: lineage === undefined ? undefined : (record) => lineage.track(record.reads),
|
|
50
52
|
execute: async ({ pattern, path, ignoreCase }, ctx) => {
|
|
51
53
|
let regex;
|
|
52
54
|
try {
|
|
@@ -68,6 +70,7 @@ export function createGrepTool(fs, workingDir) {
|
|
|
68
70
|
return ok({ status: "not_found", path });
|
|
69
71
|
}
|
|
70
72
|
const matches = [];
|
|
73
|
+
const reads = [];
|
|
71
74
|
let truncated = false;
|
|
72
75
|
const session = ctx.session;
|
|
73
76
|
async function searchFile(filePath) {
|
|
@@ -90,10 +93,11 @@ export function createGrepTool(fs, workingDir) {
|
|
|
90
93
|
if (read.kind === "truncated")
|
|
91
94
|
truncated = true;
|
|
92
95
|
const lines = splitLines(text);
|
|
96
|
+
const before = matches.length;
|
|
93
97
|
for (let i = 0; i < lines.length; i++) {
|
|
94
98
|
if (matches.length >= DEFAULT_MAX_MATCHES) {
|
|
95
99
|
truncated = true;
|
|
96
|
-
|
|
100
|
+
break;
|
|
97
101
|
}
|
|
98
102
|
const line = presentAt(lines, i, "the scanned line");
|
|
99
103
|
if (regex.test(line)) {
|
|
@@ -104,6 +108,11 @@ export function createGrepTool(fs, workingDir) {
|
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
110
|
}
|
|
111
|
+
if (matches.length === before)
|
|
112
|
+
return;
|
|
113
|
+
const noted = lineage?.readOf(filePath, read.kind === "ok" ? buf : null);
|
|
114
|
+
if (noted !== undefined)
|
|
115
|
+
reads.push(noted);
|
|
107
116
|
}
|
|
108
117
|
async function walk(dirPath, depth) {
|
|
109
118
|
if (depth > MAX_DIR_DEPTH)
|
|
@@ -138,17 +147,17 @@ export function createGrepTool(fs, workingDir) {
|
|
|
138
147
|
if (matches.length === 0) {
|
|
139
148
|
return ok({ status: "no_matches", path, pattern });
|
|
140
149
|
}
|
|
141
|
-
// oxlint-disable-next-line no-unnecessary-condition -- tsgolint cannot see the walk/searchFile closures that set truncated; the check is live (it
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
// oxlint-disable-next-line no-unnecessary-condition -- tsgolint cannot see the walk/searchFile closures that set truncated; the check is live (it selects the truncated result).
|
|
151
|
+
const output = truncated
|
|
152
|
+
? {
|
|
144
153
|
status: "truncated",
|
|
145
154
|
path,
|
|
146
155
|
pattern,
|
|
147
156
|
matches,
|
|
148
157
|
cap: { maxMatches: DEFAULT_MAX_MATCHES, maxBytesPerLine: DEFAULT_MAX_MATCH_BYTES },
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return ok(
|
|
158
|
+
}
|
|
159
|
+
: { status: "ok", path, pattern, matches };
|
|
160
|
+
return ok(reads.length === 0 ? output : withToolCallRecord(output, { reads }));
|
|
152
161
|
},
|
|
153
162
|
});
|
|
154
163
|
}
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* closes over fixed per-step coordinates and records into the step-scoped
|
|
20
20
|
* lineage collector (the sandbox agents). `createSessionWorkspaceMutator`
|
|
21
21
|
* resolves its coordinates per call from the session's analysis scope — the
|
|
22
|
-
* write prefix is the analysis root — and emits a
|
|
23
|
-
* through the provenance seam (the conversation
|
|
24
|
-
* run and no step).
|
|
22
|
+
* write prefix is the analysis root, less the staged inputs — and emits a
|
|
23
|
+
* `write-file` session event through the provenance seam (the conversation
|
|
24
|
+
* agent, which runs under no run and no step).
|
|
25
25
|
*
|
|
26
26
|
* The landing is hardened beyond the lexical resolver: missing parent
|
|
27
27
|
* directories are created only inside the write prefix, the deepest existing
|
|
@@ -38,6 +38,7 @@ import type { AgentSession } from "../../auth/types.js";
|
|
|
38
38
|
import type { Logger } from "../../lib/logger.js";
|
|
39
39
|
import type { ProvenanceCollector } from "../../provenance/collector.js";
|
|
40
40
|
import { type ProvenanceSeam } from "../../provenance/seam.js";
|
|
41
|
+
import { type SyncSessionFile } from "../../workspace/session-files.js";
|
|
41
42
|
import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
|
|
42
43
|
/** Outcome of a confined write. Expected outcomes are data variants — never throws on them. */
|
|
43
44
|
export type WriteFileResult = {
|
|
@@ -53,6 +54,10 @@ export type WriteFileResult = {
|
|
|
53
54
|
} | {
|
|
54
55
|
readonly status: "symlink_denied";
|
|
55
56
|
readonly path: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly status: "read_only";
|
|
59
|
+
readonly path: string;
|
|
60
|
+
readonly reason: string;
|
|
56
61
|
};
|
|
57
62
|
/**
|
|
58
63
|
* What one landed write leaves for provenance: the hash and the size of the
|
|
@@ -136,6 +141,44 @@ export type WorkspaceMutator = {
|
|
|
136
141
|
*/
|
|
137
142
|
recordWrite(record: WriteRecord): void;
|
|
138
143
|
};
|
|
144
|
+
/** Outcome of the hardened landing — the disk-touching half of a confined write. */
|
|
145
|
+
type LandingStatus = "ok" | "out_of_prefix" | "symlink_denied" | "read_only";
|
|
146
|
+
/** One landed confined write: the paths in both frames, and the exact bytes on disk. */
|
|
147
|
+
export type ConfinedWriteOk = {
|
|
148
|
+
readonly status: "ok";
|
|
149
|
+
/** Analysis-root-relative, forward-slashed — the provenance-facing form. */
|
|
150
|
+
readonly relative: string;
|
|
151
|
+
/** The `/{analysisId}/…` frame-independent form — the agent-facing result path. */
|
|
152
|
+
readonly agentPath: string;
|
|
153
|
+
readonly bytes: Buffer;
|
|
154
|
+
};
|
|
155
|
+
/** A refused confined write: the status, and the reason when the status alone does not give it. */
|
|
156
|
+
type ConfinedWriteRefusal = {
|
|
157
|
+
readonly status: Exclude<LandingStatus, "ok" | "read_only"> | "out_of_scope";
|
|
158
|
+
} | {
|
|
159
|
+
readonly status: "read_only";
|
|
160
|
+
readonly reason: string;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* The shared write gauntlet both realizations run: resolve + confine
|
|
164
|
+
* (`resolveForWrite`), then the hardened landing. A refusal comes back as data;
|
|
165
|
+
* an ok carries what the caller's provenance record needs — the
|
|
166
|
+
* analysis-relative path and the exact bytes that landed. `beforeLand` gets
|
|
167
|
+
* the same ok while the bytes wait in the temporary file. With
|
|
168
|
+
* `refuseStagedInputs`, a write under the staged inputs is refused before any
|
|
169
|
+
* I/O, and the landing refuses a symlink that aims into them.
|
|
170
|
+
*/
|
|
171
|
+
export declare function confinedWrite(args: {
|
|
172
|
+
readonly workspaceRoot: string;
|
|
173
|
+
readonly analysisId: string;
|
|
174
|
+
readonly workingDir: string;
|
|
175
|
+
readonly path: string;
|
|
176
|
+
/** Text lands as UTF-8. */
|
|
177
|
+
readonly content: string | Uint8Array;
|
|
178
|
+
readonly invocationId: string;
|
|
179
|
+
readonly beforeLand: ((staged: ConfinedWriteOk) => Promise<void>) | undefined;
|
|
180
|
+
readonly refuseStagedInputs: boolean;
|
|
181
|
+
}): Promise<ConfinedWriteRefusal | ConfinedWriteOk>;
|
|
139
182
|
export declare function createWorkspaceMutator(deps: WorkspaceMutatorDeps): WorkspaceMutator;
|
|
140
183
|
/** Construction deps of the session-scoped realization — see {@link createSessionWorkspaceMutator}. */
|
|
141
184
|
export type SessionWorkspaceMutatorDeps = {
|
|
@@ -149,6 +192,8 @@ export type SessionWorkspaceMutatorDeps = {
|
|
|
149
192
|
* proceeds unchanged.
|
|
150
193
|
*/
|
|
151
194
|
readonly provenance?: ProvenanceSeam;
|
|
195
|
+
/** The host copy of each file that lands, owned by the analysis: the path is anywhere in its tree. */
|
|
196
|
+
readonly syncSessionFile?: SyncSessionFile;
|
|
152
197
|
readonly logger?: Logger;
|
|
153
198
|
};
|
|
154
199
|
/**
|
|
@@ -158,10 +203,12 @@ export type SessionWorkspaceMutatorDeps = {
|
|
|
158
203
|
* resolves the workspace root from the session's analysis scope, and the write
|
|
159
204
|
* prefix IS that root — the agent modifies its own analysis tree freely, while
|
|
160
205
|
* `..` traversal, a foreign analysis, and the symlink escapes stay refused by
|
|
161
|
-
* the same gauntlet.
|
|
206
|
+
* the same gauntlet. The staged inputs are the one part of the tree that it
|
|
207
|
+
* cannot write: a write under them comes back `read_only`, with the reason.
|
|
162
208
|
*
|
|
163
209
|
* A write here runs under no run and no step, so its provenance is a session
|
|
164
210
|
* observation, not a collector record: each successful write emits one
|
|
165
211
|
* `write-file` session event (see `provenance/seam.ts`).
|
|
166
212
|
*/
|
|
167
213
|
export declare function createSessionWorkspaceMutator(deps: SessionWorkspaceMutatorDeps): WorkspaceMutator;
|
|
214
|
+
export {};
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* closes over fixed per-step coordinates and records into the step-scoped
|
|
20
20
|
* lineage collector (the sandbox agents). `createSessionWorkspaceMutator`
|
|
21
21
|
* resolves its coordinates per call from the session's analysis scope — the
|
|
22
|
-
* write prefix is the analysis root — and emits a
|
|
23
|
-
* through the provenance seam (the conversation
|
|
24
|
-
* run and no step).
|
|
22
|
+
* write prefix is the analysis root, less the staged inputs — and emits a
|
|
23
|
+
* `write-file` session event through the provenance seam (the conversation
|
|
24
|
+
* agent, which runs under no run and no step).
|
|
25
25
|
*
|
|
26
26
|
* The landing is hardened beyond the lexical resolver: missing parent
|
|
27
27
|
* directories are created only inside the write prefix, the deepest existing
|
|
@@ -43,7 +43,8 @@ import { computeSha256 } from "../../lib/fs-helpers.js";
|
|
|
43
43
|
import { tryFs, tryFsWrite } from "../../lib/fs-result.js";
|
|
44
44
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
45
45
|
import { bindSessionEmit } from "../../provenance/seam.js";
|
|
46
|
-
import {
|
|
46
|
+
import { syncWrittenFile } from "../../workspace/session-files.js";
|
|
47
|
+
import { resolveForWrite, STAGED_INPUT_ROOT } from "../../workspace/paths.js";
|
|
47
48
|
/**
|
|
48
49
|
* Agent-visible name of the tool driving a confined write. Rides the write args
|
|
49
50
|
* so a successful write is attributed to the invoking tool in provenance
|
|
@@ -51,6 +52,15 @@ import { resolveForWrite } from "../../workspace/paths.js";
|
|
|
51
52
|
* inspecting its caller.
|
|
52
53
|
*/
|
|
53
54
|
export const MUTATE_TOOL_NAMES = ["write_file", "edit_file", "knowledge_template", "execute_command"];
|
|
55
|
+
/** The refusal of a write that lands under the staged inputs. */
|
|
56
|
+
const STAGED_INPUT_REFUSAL = {
|
|
57
|
+
status: "read_only",
|
|
58
|
+
reason: `${STAGED_INPUT_ROOT}/ holds the staged inputs of the analysis, and they are immutable. Write the file to a different directory.`,
|
|
59
|
+
};
|
|
60
|
+
/** True when `path` is `dir` or a path under it. */
|
|
61
|
+
function isWithin(dir, path) {
|
|
62
|
+
return path === dir || path.startsWith(dir + sep);
|
|
63
|
+
}
|
|
54
64
|
/**
|
|
55
65
|
* Land `bytes` at `absolute`, hardened against a symlink re-aiming the write.
|
|
56
66
|
* `absolute` is already lexically confined under `prefix` by the resolver;
|
|
@@ -70,12 +80,20 @@ export const MUTATE_TOOL_NAMES = ["write_file", "edit_file", "knowledge_template
|
|
|
70
80
|
* temporary file beside it and rename that file over it. The rename
|
|
71
81
|
* replaces a symlink raced in after the check, and never follows it.
|
|
72
82
|
*
|
|
83
|
+
* `readOnlyDir` is a directory under `prefix` that takes no write. The landing
|
|
84
|
+
* refuses a canonical target under it as `read_only`, before the mkdir of
|
|
85
|
+
* step 3 and again before step 4. Thus a symlinked ancestor cannot aim a write
|
|
86
|
+
* into it.
|
|
87
|
+
*
|
|
73
88
|
* Refusals are data; a genuine I/O failure throws (the loop's dispatch catch
|
|
74
89
|
* maps it to an error tool result). A throw removes the temporary file.
|
|
75
90
|
*/
|
|
76
|
-
async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename) {
|
|
91
|
+
async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename, readOnlyDir) {
|
|
77
92
|
unwrapOrThrow(await tryFsWrite("mutator.mkdirPrefix", () => mkdir(prefix, { recursive: true }), { path: prefix }));
|
|
78
93
|
const realPrefix = unwrapOrThrow(await tryFs("mutator.realpathPrefix", () => realpath(prefix), { path: prefix }));
|
|
94
|
+
const realReadOnly = readOnlyDir === undefined
|
|
95
|
+
? undefined
|
|
96
|
+
: (unwrapOrThrow(await tryFs("mutator.realpathReadOnly", () => realpath(readOnlyDir), { path: readOnlyDir, onAbsent: () => null })) ?? joinPath(realPrefix, relativePath(prefix, readOnlyDir)));
|
|
79
97
|
// Deepest existing ancestor of the target. The walk stops at the prefix,
|
|
80
98
|
// which exists — the resolver already confined the lexical path under it.
|
|
81
99
|
let deepest = dirname(absolute);
|
|
@@ -91,6 +109,8 @@ async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename) {
|
|
|
91
109
|
if (realDeepest !== realPrefix && !realDeepest.startsWith(realPrefix + sep))
|
|
92
110
|
return "out_of_prefix";
|
|
93
111
|
const targetDir = joinPath(realDeepest, relativePath(deepest, dirname(absolute)));
|
|
112
|
+
if (realReadOnly !== undefined && isWithin(realReadOnly, joinPath(targetDir, basename(absolute))))
|
|
113
|
+
return "read_only";
|
|
94
114
|
let landingDir = realDeepest;
|
|
95
115
|
if (targetDir !== realDeepest) {
|
|
96
116
|
unwrapOrThrow(await tryFsWrite("mutator.mkdirParents", () => mkdir(targetDir, { recursive: true }), { path: targetDir }));
|
|
@@ -108,6 +128,8 @@ async function landBytes(prefix, absolute, bytes, stagingKey, beforeRename) {
|
|
|
108
128
|
landingDir = realTarget;
|
|
109
129
|
}
|
|
110
130
|
const finalPath = joinPath(landingDir, basename(absolute));
|
|
131
|
+
if (realReadOnly !== undefined && isWithin(realReadOnly, finalPath))
|
|
132
|
+
return "read_only";
|
|
111
133
|
const finalStat = unwrapOrThrow(await tryFs("mutator.lstatFinal", () => lstat(finalPath), { path: finalPath, onAbsent: () => null }));
|
|
112
134
|
if (finalStat !== null && finalStat.isSymbolicLink())
|
|
113
135
|
return "symlink_denied";
|
|
@@ -158,9 +180,11 @@ async function stageBytes(stagingPath, bytes, replacedMode) {
|
|
|
158
180
|
* (`resolveForWrite`), then the hardened landing. A refusal comes back as data;
|
|
159
181
|
* an ok carries what the caller's provenance record needs — the
|
|
160
182
|
* analysis-relative path and the exact bytes that landed. `beforeLand` gets
|
|
161
|
-
* the same ok while the bytes wait in the temporary file.
|
|
183
|
+
* the same ok while the bytes wait in the temporary file. With
|
|
184
|
+
* `refuseStagedInputs`, a write under the staged inputs is refused before any
|
|
185
|
+
* I/O, and the landing refuses a symlink that aims into them.
|
|
162
186
|
*/
|
|
163
|
-
async function confinedWrite(args) {
|
|
187
|
+
export async function confinedWrite(args) {
|
|
164
188
|
const scoped = resolveForWrite({
|
|
165
189
|
workspaceRoot: args.workspaceRoot,
|
|
166
190
|
analysisId: args.analysisId,
|
|
@@ -175,12 +199,17 @@ async function confinedWrite(args) {
|
|
|
175
199
|
// path prepends the `/{analysisId}` frame; a provenance record uses
|
|
176
200
|
// the bare tail (the collector normalizes it step-relative itself).
|
|
177
201
|
const relative = scoped.relative.split("\\").join("/");
|
|
202
|
+
if (args.refuseStagedInputs && (relative === STAGED_INPUT_ROOT || relative.startsWith(`${STAGED_INPUT_ROOT}/`)))
|
|
203
|
+
return STAGED_INPUT_REFUSAL;
|
|
178
204
|
const agentPath = `/${args.analysisId}/${relative}`;
|
|
179
|
-
const bytes = Buffer.from(args.content, "utf8");
|
|
205
|
+
const bytes = typeof args.content === "string" ? Buffer.from(args.content, "utf8") : Buffer.from(args.content);
|
|
180
206
|
const prefix = resolvePath(args.workingDir);
|
|
181
207
|
const confined = { status: "ok", relative, agentPath, bytes };
|
|
182
208
|
const { beforeLand } = args;
|
|
183
|
-
const
|
|
209
|
+
const readOnlyDir = args.refuseStagedInputs ? resolvePath(args.workspaceRoot, STAGED_INPUT_ROOT) : undefined;
|
|
210
|
+
const landed = await landBytes(prefix, scoped.absolute, bytes, args.invocationId, beforeLand === undefined ? undefined : () => beforeLand(confined), readOnlyDir);
|
|
211
|
+
if (landed === "read_only")
|
|
212
|
+
return STAGED_INPUT_REFUSAL;
|
|
184
213
|
if (landed !== "ok")
|
|
185
214
|
return { status: landed };
|
|
186
215
|
return confined;
|
|
@@ -212,9 +241,10 @@ export function createWorkspaceMutator(deps) {
|
|
|
212
241
|
content,
|
|
213
242
|
invocationId,
|
|
214
243
|
beforeLand: beforeLand === undefined ? undefined : (staged) => beforeLand(landedWrite(staged)),
|
|
244
|
+
refuseStagedInputs: false,
|
|
215
245
|
});
|
|
216
246
|
if (landed.status !== "ok")
|
|
217
|
-
return {
|
|
247
|
+
return { ...landed, path };
|
|
218
248
|
return landedWrite(landed);
|
|
219
249
|
},
|
|
220
250
|
recordWrite(record) {
|
|
@@ -235,7 +265,8 @@ export function createWorkspaceMutator(deps) {
|
|
|
235
265
|
* resolves the workspace root from the session's analysis scope, and the write
|
|
236
266
|
* prefix IS that root — the agent modifies its own analysis tree freely, while
|
|
237
267
|
* `..` traversal, a foreign analysis, and the symlink escapes stay refused by
|
|
238
|
-
* the same gauntlet.
|
|
268
|
+
* the same gauntlet. The staged inputs are the one part of the tree that it
|
|
269
|
+
* cannot write: a write under them comes back `read_only`, with the reason.
|
|
239
270
|
*
|
|
240
271
|
* A write here runs under no run and no step, so its provenance is a session
|
|
241
272
|
* observation, not a collector record: each successful write emits one
|
|
@@ -271,10 +302,20 @@ export function createSessionWorkspaceMutator(deps) {
|
|
|
271
302
|
content,
|
|
272
303
|
invocationId,
|
|
273
304
|
beforeLand: beforeLand === undefined ? undefined : (staged) => beforeLand(landedWrite(staged)),
|
|
305
|
+
refuseStagedInputs: true,
|
|
274
306
|
});
|
|
275
307
|
if (landed.status !== "ok")
|
|
276
|
-
return {
|
|
277
|
-
|
|
308
|
+
return { ...landed, path };
|
|
309
|
+
const write = landedWrite(landed);
|
|
310
|
+
await syncWrittenFile(deps.syncSessionFile, logger, {
|
|
311
|
+
session,
|
|
312
|
+
owner: { kind: "analysis" },
|
|
313
|
+
path: write.record.path,
|
|
314
|
+
hash: write.record.hash,
|
|
315
|
+
size: write.record.size,
|
|
316
|
+
content: landed.bytes,
|
|
317
|
+
});
|
|
318
|
+
return write;
|
|
278
319
|
},
|
|
279
320
|
recordWrite(record) {
|
|
280
321
|
const event = {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* wraps them as `is_error`.
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { type Tool } from "../define-tool.js";
|
|
11
|
+
import { type Tool, type WithToolCallRecord } from "../define-tool.js";
|
|
12
|
+
import type { WorkspaceReadLineage, WorkspaceReadRecord } from "../../provenance/workspace-reads.js";
|
|
12
13
|
import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
13
14
|
type ReadFileOutput = {
|
|
14
15
|
status: "invalid_input";
|
|
@@ -43,5 +44,7 @@ declare const ReadFileInputSchema: z.ZodObject<{
|
|
|
43
44
|
headLines: z.ZodOptional<z.ZodNumber>;
|
|
44
45
|
tailLines: z.ZodOptional<z.ZodNumber>;
|
|
45
46
|
}, z.core.$strip>;
|
|
46
|
-
|
|
47
|
+
type ReadFileToolOutput = ReadFileOutput | WithToolCallRecord<ReadFileOutput, WorkspaceReadRecord>;
|
|
48
|
+
/** `lineage` records each read that gave text to the agent in the lineage of a step. */
|
|
49
|
+
export declare function createReadFileTool(fs: WorkspaceFilesystem, workingDir?: string, lineage?: WorkspaceReadLineage): Tool<z.infer<typeof ReadFileInputSchema>, ReadFileToolOutput>;
|
|
47
50
|
export {};
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { ok } from "neverthrow";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
-
import { defineTool } from "../define-tool.js";
|
|
12
|
+
import { defineTool, withToolCallRecord } from "../define-tool.js";
|
|
13
13
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
14
14
|
/** Default cap — large enough for typical CSV/MD/JSON, small enough to keep tool results out of context-blowing territory. */
|
|
15
15
|
const DEFAULT_MAX_BYTES = 256 * 1024;
|
|
@@ -49,11 +49,17 @@ const ReadFileInputSchema = z.object({
|
|
|
49
49
|
.optional()
|
|
50
50
|
.describe("Read only the last N complete lines (tail). Use for logs and any " + "file where the tail is what matters. Mutually exclusive with headLines."),
|
|
51
51
|
});
|
|
52
|
-
|
|
52
|
+
/** `lineage` records each read that gave text to the agent in the lineage of a step. */
|
|
53
|
+
export function createReadFileTool(fs, workingDir, lineage) {
|
|
54
|
+
/** The output, with the read of `path` as its call record when a lineage records it. */
|
|
55
|
+
function withRead(output, path, wholeFile) {
|
|
56
|
+
const read = lineage?.readOf(path, wholeFile);
|
|
57
|
+
return read === undefined ? output : withToolCallRecord(output, { reads: [read] });
|
|
58
|
+
}
|
|
53
59
|
return defineTool({
|
|
54
60
|
id: "read_file",
|
|
55
61
|
description: "Read a workspace file: input data, prior-run outputs, current-run " +
|
|
56
|
-
"outputs, step summaries, or run syntheses. Returns the file content " +
|
|
62
|
+
"outputs, step summaries, or run syntheses. It reads the workspace directly and runs no sandbox command. Returns the file content " +
|
|
57
63
|
"as UTF-8 text. Output is capped at " +
|
|
58
64
|
`${DEFAULT_MAX_BYTES} bytes; pass headLines or tailLines to read a ` +
|
|
59
65
|
"specific window of a large file (typical for bio CSV/TSV/log files). " +
|
|
@@ -70,6 +76,7 @@ export function createReadFileTool(fs, workingDir) {
|
|
|
70
76
|
return `${path} (last ${tailLines} lines)`;
|
|
71
77
|
return path;
|
|
72
78
|
},
|
|
79
|
+
foldCallRecord: lineage === undefined ? undefined : (record) => lineage.track(record.reads),
|
|
73
80
|
execute: async ({ path, headLines, tailLines }, ctx) => {
|
|
74
81
|
if (headLines !== undefined && tailLines !== undefined) {
|
|
75
82
|
return ok({
|
|
@@ -91,23 +98,20 @@ export function createReadFileTool(fs, workingDir) {
|
|
|
91
98
|
case "ok":
|
|
92
99
|
if (looksBinary(result.content))
|
|
93
100
|
return ok({ status: "binary", path, mode });
|
|
94
|
-
return ok({
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
mode,
|
|
98
|
-
content: result.content.toString("utf8"),
|
|
99
|
-
});
|
|
101
|
+
return ok(withRead({ status: "ok", path, mode, content: result.content.toString("utf8") }, path,
|
|
102
|
+
// A line window is a part of the file, even when the window holds each line.
|
|
103
|
+
mode === "full" ? result.content : null));
|
|
100
104
|
case "truncated":
|
|
101
105
|
if (looksBinary(result.content))
|
|
102
106
|
return ok({ status: "binary", path, mode, totalSize: result.totalSize });
|
|
103
|
-
return ok({
|
|
107
|
+
return ok(withRead({
|
|
104
108
|
status: "truncated",
|
|
105
109
|
path,
|
|
106
110
|
mode,
|
|
107
111
|
content: result.content.toString("utf8"),
|
|
108
112
|
totalSize: result.totalSize,
|
|
109
113
|
returnedBytes: result.content.length,
|
|
110
|
-
});
|
|
114
|
+
}, path, null));
|
|
111
115
|
case "not_found":
|
|
112
116
|
return ok({ status: "not_found", path });
|
|
113
117
|
case "out_of_scope":
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-only surface of the workspace for an agent that plans or interprets the work of a run: the file reads,
|
|
3
|
+
* the semantic search, and the data profile. Each tool reads the tree of the analysis of its session, and no tool
|
|
4
|
+
* writes.
|
|
5
|
+
*/
|
|
6
|
+
import type { Querier } from "../../state/db.js";
|
|
7
|
+
import type { EmbeddingProvider } from "../../providers/types.js";
|
|
8
|
+
import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
9
|
+
import type { Tool } from "../define-tool.js";
|
|
10
|
+
import type { WorkspaceSearchQuerier } from "./workspace-search.js";
|
|
11
|
+
export type ReadSurfaceDeps = {
|
|
12
|
+
readonly workspaceFs: WorkspaceFilesystem;
|
|
13
|
+
readonly pool: Querier & WorkspaceSearchQuerier;
|
|
14
|
+
/** Absent: the agent gets no `workspace_search`. */
|
|
15
|
+
readonly embedding?: EmbeddingProvider;
|
|
16
|
+
};
|
|
17
|
+
export declare function createReadSurfaceTools(deps: ReadSurfaceDeps): Tool[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The read-only surface of the workspace for an agent that plans or interprets the work of a run: the file reads,
|
|
3
|
+
* the semantic search, and the data profile. Each tool reads the tree of the analysis of its session, and no tool
|
|
4
|
+
* writes.
|
|
5
|
+
*/
|
|
6
|
+
import { createInspectDataProfileTool } from "../research/inspect-data-profile.js";
|
|
7
|
+
import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "./index.js";
|
|
8
|
+
export function createReadSurfaceTools(deps) {
|
|
9
|
+
return [
|
|
10
|
+
createReadFileTool(deps.workspaceFs),
|
|
11
|
+
createListFilesTool(deps.workspaceFs),
|
|
12
|
+
createFileStatTool(deps.workspaceFs),
|
|
13
|
+
createGrepTool(deps.workspaceFs),
|
|
14
|
+
...(deps.embedding ? [createWorkspaceSearchTool(deps.pool, deps.embedding)] : []),
|
|
15
|
+
createInspectDataProfileTool(deps.pool),
|
|
16
|
+
];
|
|
17
|
+
}
|