@smthrs/std 1.0.0-rc.6 → 1.0.0-rc.7
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 +36 -36
- package/dist/cjs/Bash.d.ts +10 -0
- package/dist/cjs/Bash.d.ts.map +1 -1
- package/dist/cjs/Bash.js +13 -12
- package/dist/cjs/Bash.js.map +3 -3
- package/dist/cjs/internal/Exec.d.ts +14 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +65 -27
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/cjs/internal/Text.d.ts +11 -0
- package/dist/cjs/internal/Text.d.ts.map +1 -1
- package/dist/cjs/internal/Text.js +3 -0
- package/dist/cjs/internal/Text.js.map +2 -2
- package/dist/esm/Bash.d.ts +10 -0
- package/dist/esm/Bash.d.ts.map +1 -1
- package/dist/esm/Bash.js +22 -13
- package/dist/esm/Bash.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +14 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +86 -21
- package/dist/esm/internal/Exec.js.map +1 -1
- package/dist/esm/internal/Text.d.ts +11 -0
- package/dist/esm/internal/Text.d.ts.map +1 -1
- package/dist/esm/internal/Text.js +11 -0
- package/dist/esm/internal/Text.js.map +1 -1
- package/docs/api.md +1 -1
- package/docs/concepts/limits-and-disclosure.md +8 -5
- package/docs/guides/run-a-shell-command.md +16 -6
- package/docs/reference/flows.md +2 -2
- package/docs/troubleshooting.md +3 -3
- package/package.json +6 -6
- package/src/Bash.ts +25 -13
- package/src/internal/Exec.ts +125 -43
- package/src/internal/Text.ts +15 -0
package/README.md
CHANGED
|
@@ -16,41 +16,41 @@ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/m
|
|
|
16
16
|
|
|
17
17
|
The root entry point exports these namespaces; each is also importable from `@smthrs/std/<Module>`.
|
|
18
18
|
|
|
19
|
-
| Module | Public exports
|
|
20
|
-
| -------------------- |
|
|
21
|
-
| `ApplyPatch` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`, `paths`
|
|
22
|
-
| `Bash` | `name`, `description`, `DEFAULT_TIMEOUT_MS`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `invocation`, `inTerminal`, `outsideContainer`, `sealed`, `presentation`
|
|
23
|
-
| `Checkpoints` | `baseId`, `scratchDirectory`, `configSection`, `Snapshot`, `Materialized`, `Checkpoints`, `make`, `unavailable`, `makeNoop`, `layerNoop`, `GitOptions`, `makeGit`, `layerGit`, `Relocation`, `relocate`
|
|
24
|
-
| `Container` | `Request`, `Plan`, `Container`, `make`, `unavailable`, `makeNoop`, `layerNoop`, `makeCommand`, `layerCommand`
|
|
25
|
-
| `Edit` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`
|
|
26
|
-
| `ExaWebSearch` | `layer`
|
|
27
|
-
| `Explore` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `make`, `flow`
|
|
28
|
-
| `Fetch` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`
|
|
29
|
-
| `Glob` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`
|
|
30
|
-
| `Grep` | `name`, `description`, `Input`, `ContextLine`, `Symbol`, `Match`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`
|
|
31
|
-
| `HttpPost` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`
|
|
32
|
-
| `LanguageServer` | `Position`, `LanguageServer`, `make`, `makeNoop`, `layerNoop`
|
|
33
|
-
| `Ls` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`
|
|
34
|
-
| `Lsp` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`
|
|
35
|
-
| `Manifest` | `flows`, `handlers`, `effectsFor`, `names`, `readOnly`
|
|
36
|
-
| `NativeSearch` | `MAX_CAPTURE_BYTES`, `make`, `layer`
|
|
37
|
-
| `NodeLanguageServer` | `Config`, `MAX_QUEUED_FRAMES`, `MAX_PENDING_REQUESTS`, `make`, `makeLazy`, `layer`, `layerLazy`
|
|
38
|
-
| `PortableSearch` | `make`, `layer`
|
|
39
|
-
| `Probe` | `key`, `Reason`, `InvalidProbe`, `Attribution`, `CONFIDENCE_FLOOR`, `MAX_OUTPUT_BYTES`, `posix`, `probeAttribution`, `classify`, `unjudged`
|
|
40
|
-
| `Read` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`, `MAX_READ_FILE_BYTES`, `Preconditions`, `ReadSession`, `VersionedFileSystem`
|
|
41
|
-
| `Relocate` | `Relocation`, `relocate`
|
|
42
|
-
| `Search` | `GrepInput`, `GrepLine`, `ContextLine`, `Symbol`, `GrepMatch`, `GrepOutput`, `GlobInput`, `GlobOutput`, `Search`, `make`, `makeNoop`, `layerNoop`
|
|
43
|
-
| `SearchConformance` | `GeneratedFile`, `Plan`, `Divergence`, `plan`, `materialize`, `compare`, `report`
|
|
44
|
-
| `SearchContract` | `validatePattern`, `validateGlob`, `canonicalGlob`, `matchesGlob`, `includedByGlobs`, `expression`, `unsatisfiableNotice`, `isContractRejection`
|
|
45
|
-
| `ShellCommand` | `name`, `description`, `DEFAULT_TIMEOUT_MS`, `MAX_CAPTURE_BYTES`, `DEFAULT_MAX_OUTPUT_TOKENS`, `TIMEOUT_EXIT_CODE`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`
|
|
46
|
-
| `StdError` | `Code`, `StdError`
|
|
47
|
-
| `TestRun` | `name`, `description`, `scratchDirectory`, `DEFAULT_TIMEOUT_MS`, `MAX_CAPTURE_BYTES`, `Input`, `Outcome`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`
|
|
48
|
-
| `TreeFingerprint` | `key`, `defaultPrune`, `defaultIgnoreSuffixes`, `maxPaths`, `Measurement`, `script`, `parse`, `moved`
|
|
49
|
-
| `TestRunner` | `captureBase`, `Runner`, `TestRunner`, `make`, `makeNoop`, `layer`, `layerNoop`
|
|
50
|
-
| `UpdatePlan` | `name`, `description`, `StepStatus`, `Plan`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`
|
|
51
|
-
| `WebFetch` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`
|
|
52
|
-
| `WebSearch` | `name`, `description`, `Input`, `Result`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `WebSearch`, `make`, `makeNoop`, `layerNoop`, `run`
|
|
53
|
-
| `Write` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`
|
|
19
|
+
| Module | Public exports | Description |
|
|
20
|
+
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
21
|
+
| `ApplyPatch` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`, `paths` | Declares and applies a V4A patch. |
|
|
22
|
+
| `Bash` | `name`, `description`, `DEFAULT_TIMEOUT_MS`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `invocation`, `inTerminal`, `outsideContainer`, `sealed`, `presentation`, `elideMiddle` | Declares and runs bounded shell commands, here or in a registered terminal. |
|
|
23
|
+
| `Checkpoints` | `baseId`, `scratchDirectory`, `configSection`, `Snapshot`, `Materialized`, `Checkpoints`, `make`, `unavailable`, `makeNoop`, `layerNoop`, `GitOptions`, `makeGit`, `layerGit`, `Relocation`, `relocate` | Pins a working tree as a git checkpoint and materializes it again. |
|
|
24
|
+
| `Container` | `Request`, `Plan`, `Container`, `make`, `unavailable`, `makeNoop`, `layerNoop`, `makeCommand`, `layerCommand` | Routes a command into a named container. |
|
|
25
|
+
| `Edit` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation` | Declares and applies exact text replacements. |
|
|
26
|
+
| `ExaWebSearch` | `layer` | Provides WebSearch through the Exa API and kernel HTTP client. |
|
|
27
|
+
| `Explore` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `make`, `flow` | Declares a dynamic exploration flow composed from other standard flows. |
|
|
28
|
+
| `Fetch` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run` | Declares and runs raw HTTP GET requests. |
|
|
29
|
+
| `Glob` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation` | Declares and runs filesystem glob searches. |
|
|
30
|
+
| `Grep` | `name`, `description`, `Input`, `ContextLine`, `Symbol`, `Match`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation` | Declares and runs text searches over files. |
|
|
31
|
+
| `HttpPost` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run` | Declares and runs HTTP POST requests. |
|
|
32
|
+
| `LanguageServer` | `Position`, `LanguageServer`, `make`, `makeNoop`, `layerNoop` | Defines the language-server query service. |
|
|
33
|
+
| `Ls` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation` | Declares and runs directory listings. |
|
|
34
|
+
| `Lsp` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run` | Declares and runs ten language-server queries, from hover to diagnostics. |
|
|
35
|
+
| `Manifest` | `flows`, `handlers`, `effectsFor`, `names`, `readOnly` | Exposes frozen flow, handler, and narrowing registries plus the read-only set. |
|
|
36
|
+
| `NativeSearch` | `MAX_CAPTURE_BYTES`, `make`, `layer` | Implements Search through the ripgrep binary. |
|
|
37
|
+
| `NodeLanguageServer` | `Config`, `MAX_QUEUED_FRAMES`, `MAX_PENDING_REQUESTS`, `make`, `makeLazy`, `layer`, `layerLazy` | Implements LanguageServer with Node child processes, routed by extension. |
|
|
38
|
+
| `PortableSearch` | `make`, `layer` | Implements Search by walking the kernel filesystem in process. |
|
|
39
|
+
| `Probe` | `key`, `Reason`, `InvalidProbe`, `Attribution`, `CONFIDENCE_FLOOR`, `MAX_OUTPUT_BYTES`, `posix`, `probeAttribution`, `classify`, `unjudged` | Attributes a non-zero exit to the command or to the tree, with Jev. |
|
|
40
|
+
| `Read` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation`, `MAX_READ_FILE_BYTES`, `Preconditions`, `ReadSession`, `VersionedFileSystem` | Declares and runs bounded file reads. |
|
|
41
|
+
| `Relocate` | `Relocation`, `relocate` | Rewrites a call's input so the call runs against a materialized checkpoint. |
|
|
42
|
+
| `Search` | `GrepInput`, `GrepLine`, `ContextLine`, `Symbol`, `GrepMatch`, `GrepOutput`, `GlobInput`, `GlobOutput`, `Search`, `make`, `makeNoop`, `layerNoop` | Defines the search service both peers implement. |
|
|
43
|
+
| `SearchConformance` | `GeneratedFile`, `Plan`, `Divergence`, `plan`, `materialize`, `compare`, `report` | Generates a tree and calls, then reports where two Search peers disagree. |
|
|
44
|
+
| `SearchContract` | `validatePattern`, `validateGlob`, `canonicalGlob`, `matchesGlob`, `includedByGlobs`, `expression`, `unsatisfiableNotice`, `isContractRejection` | The shared matcher an external Search peer must build on. |
|
|
45
|
+
| `ShellCommand` | `name`, `description`, `DEFAULT_TIMEOUT_MS`, `MAX_CAPTURE_BYTES`, `DEFAULT_MAX_OUTPUT_TOKENS`, `TIMEOUT_EXIT_CODE`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run` | Declares and runs a Codex-shaped shell command. |
|
|
46
|
+
| `StdError` | `Code`, `StdError` | Defines typed standard-tool failures. |
|
|
47
|
+
| `TestRun` | `name`, `description`, `scratchDirectory`, `DEFAULT_TIMEOUT_MS`, `MAX_CAPTURE_BYTES`, `Input`, `Outcome`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation` | Runs the declared test suite and reads its report. |
|
|
48
|
+
| `TreeFingerprint` | `key`, `defaultPrune`, `defaultIgnoreSuffixes`, `maxPaths`, `Measurement`, `script`, `parse`, `moved` | Measures a container's working directory so `bash` can report a write there. |
|
|
49
|
+
| `TestRunner` | `captureBase`, `Runner`, `TestRunner`, `make`, `makeNoop`, `layer`, `layerNoop` | Declares how the project under test runs its suite. |
|
|
50
|
+
| `UpdatePlan` | `name`, `description`, `StepStatus`, `Plan`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run` | Acknowledges a Codex plan update. |
|
|
51
|
+
| `WebFetch` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run` | Declares and runs normalized web-page fetches. |
|
|
52
|
+
| `WebSearch` | `name`, `description`, `Input`, `Result`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `WebSearch`, `make`, `makeNoop`, `layerNoop`, `run` | Declares web search and its injectable provider service. |
|
|
53
|
+
| `Write` | `name`, `description`, `Input`, `Output`, `effects`, `effectsFor`, `capabilities`, `flow`, `run`, `activity`, `presentation` | Declares and runs file writes. |
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
56
|
import { Read } from "@smthrs/std"
|
|
@@ -103,7 +103,7 @@ Every limit is a display budget disclosed to the caller, never a silent cut. A c
|
|
|
103
103
|
| `NodeLanguageServer.MAX_QUEUED_FRAMES` | 256 | frames buffered for one language server's stdin |
|
|
104
104
|
| `NodeLanguageServer.MAX_PENDING_REQUESTS` | 512 | concurrent in-flight JSON-RPC requests to one server |
|
|
105
105
|
|
|
106
|
-
Shell capture is bounded where it is read rather than after: a command that prints gigabytes costs the bound, not the whole of what it printed, and the `<stream>DroppedBytes` fields count what the process actually produced. Every caller-supplied command (`bash`, `test`, `shell_command`) passes a bound. The internal `git` plumbing calls behind `Checkpoints` and `TestRun`'s baseline do not, because a listing read for its content is useless with its head missing. Those fields and the `<stream>Truncated` flags beside them are a wire convention: [`@smthrs/harness`](https://harness.smithers.sh) reads them to refuse a later write of those exact bytes, so check the flag before writing captured output anywhere.
|
|
106
|
+
Shell capture is bounded where it is read rather than after: a command that prints gigabytes costs the bound, not the whole of what it printed, and the `<stream>DroppedBytes` fields count what the process actually produced. `bash` keeps the first and last 15,000 bytes of a stream and puts one line naming the omitted bytes where the middle was. Every caller-supplied command (`bash`, `test`, `shell_command`) passes a bound. The internal `git` plumbing calls behind `Checkpoints` and `TestRun`'s baseline do not, because a listing read for its content is useless with its head missing. Those fields and the `<stream>Truncated` flags beside them are a wire convention: [`@smthrs/harness`](https://harness.smithers.sh) reads them to refuse a later write of those exact bytes, so check the flag before writing captured output anywhere.
|
|
107
107
|
|
|
108
108
|
## Failures
|
|
109
109
|
|
package/dist/cjs/Bash.d.ts
CHANGED
|
@@ -133,6 +133,16 @@ export declare const Output: Schema.Struct<{
|
|
|
133
133
|
* @since 1.0.0
|
|
134
134
|
*/
|
|
135
135
|
export type Output = typeof Output.Type;
|
|
136
|
+
/**
|
|
137
|
+
* The text of a stream cut to its start and end: the start, one line naming
|
|
138
|
+
* the bytes omitted between them, and the end. A host that captures output
|
|
139
|
+
* itself, such as a terminal behind {@link inTerminal}, cuts with this so every
|
|
140
|
+
* {@link Output} reads the same.
|
|
141
|
+
*
|
|
142
|
+
* @category rendering
|
|
143
|
+
* @since 1.0.0-rc.5
|
|
144
|
+
*/
|
|
145
|
+
export declare const elideMiddle: (start: string, droppedBytes: number, end: string) => string;
|
|
136
146
|
/**
|
|
137
147
|
* Static conservative effect envelope for the bash flow.
|
|
138
148
|
*
|
package/dist/cjs/Bash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Bash.d.ts","sourceRoot":"","sources":["../../src/Bash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,KAAK,mBAAmB,MAAM,oCAAoC,CAAA;AAC9E,OAAO,KAAK,IAAI,MAAM,qBAAqB,CAAA;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAQvC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AAGzC;;;;;GAKG;AACH,eAAO,MAAM,IAAI,SAAS,CAAA;AAE1B;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,yMACgL,CAAA;AAExM;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,SAAU,CAAA;AAmCzC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;IAiChB,CAAA;AAEF;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAErC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"Bash.d.ts","sourceRoot":"","sources":["../../src/Bash.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/Bash.js
CHANGED
|
@@ -35,6 +35,7 @@ __export(Bash_exports, {
|
|
|
35
35
|
description: () => description,
|
|
36
36
|
effects: () => effects,
|
|
37
37
|
effectsFor: () => effectsFor,
|
|
38
|
+
elideMiddle: () => elideMiddle,
|
|
38
39
|
flow: () => flow,
|
|
39
40
|
inTerminal: () => inTerminal,
|
|
40
41
|
invocation: () => invocation,
|
|
@@ -129,10 +130,10 @@ const Input = Schema.Union([
|
|
|
129
130
|
const Output = Schema.Struct({
|
|
130
131
|
exitCode: Schema.Number.annotate({ description: "Command exit code, including non-zero codes" }),
|
|
131
132
|
stdout: Schema.String.annotate({
|
|
132
|
-
description: "Captured stdout;
|
|
133
|
+
description: "Captured stdout; when stdoutTruncated is true, only its start and end, with one line naming the bytes omitted between them"
|
|
133
134
|
}),
|
|
134
135
|
stderr: Schema.String.annotate({
|
|
135
|
-
description: "Captured stderr;
|
|
136
|
+
description: "Captured stderr; when stderrTruncated is true, only its start and end, with one line naming the bytes omitted between them"
|
|
136
137
|
}),
|
|
137
138
|
stdoutTruncated: Schema.Boolean.annotate({
|
|
138
139
|
description: "Whether stdout exceeded the capture limit, leaving stdout a fragment that must not be written to a file"
|
|
@@ -140,8 +141,8 @@ const Output = Schema.Struct({
|
|
|
140
141
|
stderrTruncated: Schema.Boolean.annotate({
|
|
141
142
|
description: "Whether stderr exceeded the capture limit, leaving stderr a fragment that must not be written to a file"
|
|
142
143
|
}),
|
|
143
|
-
stdoutDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the
|
|
144
|
-
stderrDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the
|
|
144
|
+
stdoutDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the middle of stdout" }),
|
|
145
|
+
stderrDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the middle of stderr" }),
|
|
145
146
|
invalidProbe: Schema.optional(
|
|
146
147
|
Probe.InvalidProbe.annotate({
|
|
147
148
|
description: "Reserved for a confirmed launch failure; an exit code alone does not establish one"
|
|
@@ -153,6 +154,7 @@ const Output = Schema.Struct({
|
|
|
153
154
|
})
|
|
154
155
|
)
|
|
155
156
|
});
|
|
157
|
+
const elideMiddle = import_Text.elideMiddle;
|
|
156
158
|
const effects = (0, import_Declaration.envelope)({
|
|
157
159
|
tier: "irreversible",
|
|
158
160
|
mode: "expected",
|
|
@@ -297,21 +299,20 @@ const run = Effect.fn("Bash.run")(function* (input) {
|
|
|
297
299
|
...spawned.env === void 0 ? {} : { env: spawned.env },
|
|
298
300
|
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
299
301
|
maxCaptureBytes: import_Text.MAX_SHELL_OUTPUT_BYTES,
|
|
302
|
+
overflow: "ends",
|
|
300
303
|
...spawned.args === void 0 ? {} : { args: spawned.args },
|
|
301
304
|
...spawned.stdin === void 0 ? {} : { stdin: spawned.stdin }
|
|
302
305
|
}).pipe(Effect.mapError((error) => Exec.toStdError(spawned.quoted, error)));
|
|
303
306
|
const mutated = TreeFingerprint.moved(before, yield* fingerprint(input, transport));
|
|
304
307
|
if (!contained) yield* Diagnostics.refresh;
|
|
305
|
-
const stdout = (0, import_Text.truncateBytes)(result.stdout, import_Text.MAX_SHELL_OUTPUT_BYTES, { keep: "tail" });
|
|
306
|
-
const stderr = (0, import_Text.truncateBytes)(result.stderr, import_Text.MAX_SHELL_OUTPUT_BYTES, { keep: "tail" });
|
|
307
308
|
return {
|
|
308
309
|
exitCode: result.exitCode,
|
|
309
|
-
stdout: stdout
|
|
310
|
-
stderr: stderr
|
|
311
|
-
stdoutTruncated: result.stdoutDroppedBytes > 0
|
|
312
|
-
stderrTruncated: result.stderrDroppedBytes > 0
|
|
313
|
-
stdoutDroppedBytes: result.stdoutDroppedBytes
|
|
314
|
-
stderrDroppedBytes: result.stderrDroppedBytes
|
|
310
|
+
stdout: result.stdout,
|
|
311
|
+
stderr: result.stderr,
|
|
312
|
+
stdoutTruncated: result.stdoutDroppedBytes > 0,
|
|
313
|
+
stderrTruncated: result.stderrDroppedBytes > 0,
|
|
314
|
+
stdoutDroppedBytes: result.stdoutDroppedBytes,
|
|
315
|
+
stderrDroppedBytes: result.stderrDroppedBytes,
|
|
315
316
|
...mutated === void 0 ? {} : { mutated }
|
|
316
317
|
};
|
|
317
318
|
});
|
package/dist/cjs/Bash.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/Bash.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Bash flow declaration and portable handler.\n *\n * Two ways in, and the difference is where the payload is parsed. `command` is\n * a line for the platform shell, exactly as before. `script` is program text\n * delivered to an interpreter on standard input, as data: nothing quotes it,\n * escapes it, or terminates it with a heredoc marker, so the class of failure\n * that produced `docker exec c bash -lc '\u2026python - <<EOF\u2026'` cannot occur. Its\n * `args` reach the script as arguments rather than as interpolated text.\n *\n * `container` names a container the host knows how to reach, and the argv is\n * built by the injected {@link Container} transport rather than typed by the\n * caller. A host that binds no transport refuses such a call by saying so; the\n * requirement is optional here precisely so a host without containers is not\n * forced to declare one.\n *\n * @since 1.0.0\n */\n\nimport type * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Path from \"@smthrs/kernel/Path\"\nimport * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as Container from \"./Container.ts\"\nimport { capability, envelope } from \"./internal/Declaration.ts\"\nimport * as Diagnostics from \"./internal/Diagnostics.ts\"\nimport { outsideEnvelope } from \"./internal/EnvelopePrecheck.ts\"\nimport * as Exec from \"./internal/Exec.ts\"\nimport { MAX_SHELL_OUTPUT_BYTES, truncateBytes } from \"./internal/Text.ts\"\nimport * as Probe from \"./Probe.ts\"\nimport * as StdError from \"./StdError.ts\"\nimport * as TreeFingerprint from \"./TreeFingerprint.ts\"\n\n/**\n * Registry name for the bash flow.\n *\n * @category identifiers\n * @since 1.0.0\n */\nexport const name = \"bash\"\n\n/**\n * Model-facing description of the bash flow.\n *\n * Capped at 200 characters and read every frame, which is why the\n * invalid-probe contract is not restated here: the cell contract teaches it\n * once, and a result that has one explains itself.\n *\n * @category descriptions\n * @since 1.0.0\n */\nexport const description =\n \"Run a shell command, or an interpreter over a script passed as data instead of quoted into a line. container routes it through the host's container transport. mode:hermetic pre-checks path tokens.\"\n\n/**\n * Default command timeout in milliseconds.\n *\n * Ten minutes lets full builds and test suites finish while ensuring a stalled\n * process eventually releases the flow.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const DEFAULT_TIMEOUT_MS = 600_000\n\nconst Command = Schema.optional(Schema.String.annotate({\n description: \"Shell command line to execute; give this or script, never both\"\n}))\nconst Script = Schema.optional(Schema.String.annotate({\n description: \"Program text delivered to the interpreter on stdin as data, so it needs no quoting or heredoc\"\n}))\nconst Interpreter = Schema.optional(Schema.String.annotate({\n description:\n \"Program that reads script on stdin; defaults to bash. python, python3, sh, zsh, node, ruby and perl are known\"\n}))\nconst Args = Schema.optional(\n Schema.Array(Schema.String).annotate({\n description: \"Arguments passed to the script as data, never interpolated into it\"\n })\n)\nconst Stdin = Schema.optional(Schema.String.annotate({\n description: \"Text written to the command's standard input; use script instead when the text is the program\"\n}))\nconst ContainerName = Schema.optional(Schema.String.annotate({\n description: \"Run inside this container through the host's container transport; requires mode:unhermetic\"\n}))\nconst Cwd = Schema.optional(Schema.String).annotate({\n description: \"Working directory for the command\",\n message: \"cwd must be a path string. For a checkpoint, pass { at: ctx.base } as the third ctx.call argument.\"\n})\nconst Env = Schema.optional(\n Schema.Record(Schema.String, Schema.String).annotate({ description: \"Environment variables for the command\" })\n)\nconst TimeoutMs = Schema.optional(Schema.Number).annotate({\n description: \"Wall-clock timeout in milliseconds; defaults to 600000 ms\",\n message: \"timeoutMs must be a number of milliseconds.\"\n})\n\n/**\n * Input schema for the bash flow.\n *\n * The mode named `hermetic` requires explicit read and write envelopes for a\n * lexical pre-check; it does not by itself confine the process. Unhermetic mode\n * deliberately omits those declarations so the distinction stays visible in\n * the input shape.\n *\n * @category schemas\n * @since 1.0.0\n */\nexport const Input = Schema.Union([\n Schema.Struct({\n mode: Schema.Literal(\"hermetic\").annotate({\n description: \"Lexically pre-check explicit path tokens against declared reads and writes; does not sandbox\"\n }),\n command: Command,\n script: Script,\n interpreter: Interpreter,\n args: Args,\n stdin: Stdin,\n container: ContainerName,\n reads: Schema.Array(Schema.NonEmptyString).annotate({ description: \"Paths or path globs the command may read\" }),\n writes: Schema.Array(Schema.NonEmptyString).annotate({ description: \"Paths or path globs the command may write\" }),\n cwd: Cwd,\n env: Env,\n timeoutMs: TimeoutMs\n }),\n Schema.Struct({\n // Omitted means unhermetic: it is the envelope `effects` already declares\n // for every call, so the default grants nothing a named mode would not.\n mode: Schema.Literal(\"unhermetic\").annotate({\n description: \"Run without a declared filesystem envelope as an irreversible effect; the default\"\n }).pipe(Schema.optionalKey, Schema.withDecodingDefaultKey(Effect.succeed(\"unhermetic\" as const))),\n command: Command,\n script: Script,\n interpreter: Interpreter,\n args: Args,\n stdin: Stdin,\n container: ContainerName,\n cwd: Cwd,\n env: Env,\n timeoutMs: TimeoutMs\n })\n])\n\n/**\n * Decoded input accepted by the bash handler.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Input = typeof Input.Type\n\n/**\n * Output schema for the bash flow.\n *\n * `invalidProbe` is present when the shell itself refused to start the\n * command, which its exit code says outright: 127 for a program it could not\n * find, 126 for one it could not execute. Only that. Reading a runner's own\n * wording is a judgment rather than a fact, and `Probe.classify` makes it with\n * Jev for the `test` flow, whose caller is asking about a suite; a shell call\n * is any command at all and stays free of the gateway. See `Probe`.\n *\n * The `<stream>Truncated` flags are a wire convention, not a display detail.\n * A truncated capture is a fragment of what the process printed, and the\n * harness reads these flags to refuse a later write of those exact bytes \u2014\n * `@smthrs/harness/TruncatedOutput` is the consuming half. Renaming a flag, or\n * dropping it when the capture is cut, disarms that guard silently.\n *\n * @category schemas\n * @since 1.0.0\n */\nexport const Output = Schema.Struct({\n exitCode: Schema.Number.annotate({ description: \"Command exit code, including non-zero codes\" }),\n stdout: Schema.String.annotate({\n description: \"Captured stdout; only the tail, and not the whole output, when stdoutTruncated is true\"\n }),\n stderr: Schema.String.annotate({\n description: \"Captured stderr; only the tail, and not the whole output, when stderrTruncated is true\"\n }),\n stdoutTruncated: Schema.Boolean.annotate({\n description:\n \"Whether stdout exceeded the capture limit, leaving stdout a fragment that must not be written to a file\"\n }),\n stderrTruncated: Schema.Boolean.annotate({\n description:\n \"Whether stderr exceeded the capture limit, leaving stderr a fragment that must not be written to a file\"\n }),\n stdoutDroppedBytes: Schema.Number.annotate({ description: \"UTF-8 bytes omitted from the start of stdout\" }),\n stderrDroppedBytes: Schema.Number.annotate({ description: \"UTF-8 bytes omitted from the start of stderr\" }),\n invalidProbe: Schema.optional(\n Probe.InvalidProbe.annotate({\n description: \"Reserved for a confirmed launch failure; an exit code alone does not establish one\"\n })\n ),\n mutated: Schema.optional(\n Schema.Boolean.annotate({\n description:\n \"Present for a containerised command whose working directory was measured before and after it: true when the tree changed, false when it held still. Absent when the tree was not measured, which is never read as unchanged.\"\n })\n )\n})\n\n/**\n * Decoded output returned by the bash handler.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Output = typeof Output.Type\n\n/**\n * Static conservative effect envelope for the bash flow.\n *\n * The declaration is the registry-time worst case because the mode is not\n * known until an invocation is decoded.\n *\n * @category effects\n * @since 1.0.0\n */\nexport const effects = envelope({\n tier: \"irreversible\",\n mode: \"expected\",\n reads: [],\n writes: []\n})\n\n/**\n * Narrows the effect envelope for a decoded invocation.\n *\n * @category effects\n * @since 1.0.0\n */\nexport const effectsFor = (input: Input) =>\n input.mode === \"hermetic\"\n ? envelope({\n tier: \"irreversible\",\n mode: \"hermetic\",\n reads: input.reads,\n writes: input.writes\n })\n : envelope({\n tier: \"irreversible\",\n mode: \"expected\",\n reads: [],\n writes: []\n })\n\n/**\n * Capabilities required by the bash flow.\n *\n * @category capabilities\n * @since 1.0.0\n */\nexport const capabilities = [capability(\"proc:spawn\", \"*\")]\n\n/**\n * Declaration-only bash flow.\n *\n * @category flows\n * @since 1.0.0\n */\nexport const flow = { name, description, input: Input, output: Output, capabilities, effects } as const\n\n/*\n * `bash` declares no activity on purpose. What a shell call does is in the\n * command, not in the declaration: the same flow runs a test suite, a type\n * check, and `cat`. A reader that recognises a direct runner says so from the\n * recorded command; everything else stays unknown, which is what an absent\n * activity means.\n */\n\n/**\n * How one recorded call to this flow reads: the verb for each settlement, the\n * input field that is its subject, and the measured output fields a one-line\n * summary may count.\n *\n * Display metadata only, governed by `@smthrs/registry`\n * `Descriptor.CallPresentation` and checked where `StandardFlows` binds it.\n *\n * @category presentation\n * @since 1.0.0-rc.0\n */\nexport const presentation = {\n verb: { pending: \"running\", success: \"ran\", failure: \"failed to run\" },\n subject: \"command\",\n result: \"command\"\n} as const\n\n/**\n * Select standard input explicitly. Shells need `--` before script arguments\n * so even an argument such as `-c` cannot replace the stdin program.\n * Other supported interpreters treat arguments after `-` as program data.\n */\nconst stdinArguments = (interpreter: string): ReadonlyArray<string> | undefined => {\n switch (interpreter) {\n case \"bash\":\n case \"zsh\":\n case \"sh\":\n case \"dash\":\n return [\"-s\", \"--\"]\n case \"python\":\n case \"python3\":\n case \"node\":\n case \"ruby\":\n case \"perl\":\n return [\"-\"]\n default:\n return undefined\n }\n}\n\n/** Whether a script is shell text the lexical pre-check can actually read. */\nconst shells = new Set([\"bash\", \"zsh\", \"sh\", \"dash\"])\n\n/** What one invocation runs, before any container transport rewrites it. */\ninterface Plan {\n readonly file: string\n readonly args: ReadonlyArray<string> | undefined\n readonly stdin: string | undefined\n readonly env: Record<string, string> | undefined\n readonly quoted: string\n}\n\nconst invalid = (message: string): StdError.StdError => new StdError.StdError({ code: \"invalid_input\", message })\n\n/** The invocation an input names, or the failure explaining why it names none. */\nconst plan = (input: Input): Plan | StdError.StdError => {\n const interpreter = input.interpreter ?? \"bash\"\n if (input.command !== undefined && input.script !== undefined) {\n return invalid(\"Give command or script, never both: a script is program text, a command is a shell line\")\n }\n if (input.command === undefined && input.script === undefined) {\n return invalid(\"Give either command (a shell line) or script (program text run by interpreter)\")\n }\n if (input.script !== undefined && input.stdin !== undefined) {\n return invalid(\"A script already arrives on standard input, so it cannot also carry stdin\")\n }\n if (input.command !== undefined && input.args !== undefined) {\n return invalid(\"args belong to a script; a command carries its own arguments in the line\")\n }\n if (input.command !== undefined && input.interpreter !== undefined) {\n return invalid(\"interpreter runs a script; a command runs in the platform shell\")\n }\n if (input.mode === \"hermetic\" && input.container !== undefined) {\n return invalid(\n \"A container has its own filesystem, so declared reads and writes cannot describe it. Use mode:unhermetic for a containerised command\"\n )\n }\n if (input.mode === \"hermetic\" && input.script !== undefined && !shells.has(interpreter)) {\n return invalid(\n `The hermetic pre-check reads shell text, and ${interpreter} is not a shell. Run this script with mode:unhermetic, or express it as shell`\n )\n }\n if (input.command !== undefined) {\n return { file: input.command, args: undefined, stdin: input.stdin, env: input.env, quoted: input.command }\n }\n const selector = stdinArguments(interpreter)\n if (selector === undefined) {\n return invalid(`Unsupported script interpreter: ${interpreter}`)\n }\n const args = [...selector, ...(input.args ?? [])]\n return {\n file: interpreter,\n args,\n stdin: input.script,\n env: input.env,\n quoted: [interpreter, ...args].join(\" \")\n }\n}\n\n/**\n * The plan, refused when its input names no one invocation or when a hermetic\n * call's paths fall outside its declared envelope. Spawned and terminal\n * execution share this one check.\n */\nconst precheck = (input: Input, path: Path.Path): Plan | StdError.StdError => {\n const intent = plan(input)\n if (intent instanceof StdError.StdError || input.mode !== \"hermetic\") return intent\n // A planned call carries exactly one of command or script.\n return outsideEnvelope(input, input.command ?? input.script!, path) ?? intent\n}\n\n/** The plan as the host will spawn it, once the container transport has had it. */\nconst routed = (\n plan: Plan,\n input: Input,\n transport: Option.Option<Container.Container>\n): Effect.Effect<Plan, StdError.StdError> => {\n if (input.container === undefined) return Effect.succeed(plan)\n if (Option.isNone(transport)) return Effect.fail(Container.unavailable(input.container))\n return Effect.map(\n transport.value.exec(request(plan, input, input.container)),\n (routing): Plan => ({\n file: routing.file,\n args: routing.args,\n stdin: plan.stdin,\n env: routing.env,\n // Diagnostics describe the logical invocation, not transport arguments.\n quoted: plan.quoted\n })\n )\n}\n\n/**\n * One containerised request, always through a login shell.\n *\n * A container's environment is what its profile makes it. The images this\n * harness runs against activate the project's interpreter \u2014 a conda env, a\n * virtualenv, a modified `PATH` \u2014 from `/etc/profile.d`, so a program spawned\n * directly by `docker exec` gets a different Python from the one that owns the\n * repository's dependencies. That is measured, not theoretical: r90 typed\n * `docker exec \u2026 bash -lc '\u2026'` by hand and got the activation for free, r91\n * routed `interpreter: \"python3\"` straight at the container and filled its\n * traces with `ModuleNotFoundError: No module named 'numpy'` and\n * `exec: \"/usr/local/bin/python\": no such file or directory`. Thirty of 45\n * instances then spent 138 cells scanning `/opt` for the real interpreter.\n *\n * So the login shell is the transport's, not the caller's. A command line is\n * one argument to `bash -lc`, as before. A program run under an interpreter\n * becomes `bash -lc 'exec \"$@\"' bash <interpreter> <args\u2026>`: the shell reads\n * the profile, then `exec` replaces it with the interpreter, so the script\n * still arrives on the inherited standard input and no argument is ever\n * re-parsed as shell text.\n */\nconst request = (plan: Plan, input: Input, container: string): Container.Request => ({\n container,\n file: \"bash\",\n args: plan.args === undefined\n ? [\"-lc\", plan.file]\n : [\"-lc\", `exec \"$@\"`, \"bash\", plan.file, ...plan.args],\n ...(input.cwd === undefined ? {} : { cwd: input.cwd }),\n ...(input.env === undefined ? {} : { env: input.env }),\n stdin: plan.stdin !== undefined\n})\n\n/** How long one fingerprint of the container's working directory may take. */\nconst FINGERPRINT_TIMEOUT_MS = 60_000\n\n/**\n * Measures the tree a containerised command is about to run in, or just ran in.\n *\n * The host's workspace walk cannot see a container's filesystem, so the\n * measurement is taken where the tree is: `TreeFingerprint.script` runs\n * through the same transport as the command, in the same working directory,\n * and answers with one checksum. Every way this can fail \u2014 no transport, a\n * container without `find` or `stat`, a timeout, a bounded listing \u2014 is read\n * as \"unmeasured\" and reported as an absent `mutated`, never as a tree that\n * held still. The time it costs is bounded by the command's own timeout so a\n * stalled container cannot spend more waiting to be measured than it may\n * spend running.\n */\nconst fingerprint = (\n input: Input,\n transport: Option.Option<Container.Container>\n): Effect.Effect<TreeFingerprint.Measurement | undefined, never, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n if (input.container === undefined || Option.isNone(transport)) return undefined\n const routing = yield* transport.value.exec({\n container: input.container,\n file: \"sh\",\n args: [\"-c\", TreeFingerprint.script()],\n ...(input.cwd === undefined ? {} : { cwd: input.cwd }),\n stdin: false\n })\n const result = yield* Exec.exec(routing.file, {\n ...(routing.env === undefined ? {} : { env: routing.env }),\n timeoutMs: Math.min(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, FINGERPRINT_TIMEOUT_MS),\n maxCaptureBytes: MAX_SHELL_OUTPUT_BYTES,\n args: routing.args\n })\n return result.exitCode === 0 ? TreeFingerprint.parse(result.stdout) : undefined\n }).pipe(Effect.orElseSucceed(() => undefined))\n\n/**\n * Executes a shell command through the permission-aware kernel service.\n *\n * Non-zero exit codes remain successful values. Only timeout, host, permission,\n * and declared-envelope pre-check failures use the typed error channel.\n *\n * @category handlers\n * @since 1.0.0\n */\nexport const run = Effect.fn(\"Bash.run\")(function*(\n input: Input\n): Effect.fn.Return<Output, StdError.StdError, ChildProcessSpawner.ChildProcessSpawner | Path.Path> {\n const intent = precheck(input, yield* Path.Path)\n if (intent instanceof StdError.StdError) return yield* Effect.fail(intent)\n // The working directory belongs to the container. The transport supplies\n // any environment overrides its host process needs to forward into it.\n const contained = input.container !== undefined\n const transport = yield* Effect.serviceOption(Container.Container)\n const spawned = yield* routed(intent, input, transport)\n\n // The tree the command runs in, before it runs. Measured in the container\n // because nothing on this host can see it; see `TreeFingerprint`.\n const before = yield* fingerprint(input, transport)\n const result = yield* Exec.exec(spawned.file, {\n ...(input.cwd === undefined || contained ? {} : { cwd: input.cwd }),\n ...(spawned.env === undefined ? {} : { env: spawned.env }),\n timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n maxCaptureBytes: MAX_SHELL_OUTPUT_BYTES,\n ...(spawned.args === undefined ? {} : { args: spawned.args }),\n ...(spawned.stdin === undefined ? {} : { stdin: spawned.stdin })\n }).pipe(Effect.mapError((error) => Exec.toStdError(spawned.quoted, error)))\n const mutated = TreeFingerprint.moved(before, yield* fingerprint(input, transport))\n // A command on this host may have changed files a language server holds\n // open; a container's files are not the ones it was sent.\n if (!contained) yield* Diagnostics.refresh\n const stdout = truncateBytes(result.stdout, MAX_SHELL_OUTPUT_BYTES, { keep: \"tail\" })\n const stderr = truncateBytes(result.stderr, MAX_SHELL_OUTPUT_BYTES, { keep: \"tail\" })\n // Exit codes alone cannot establish whether the shell refused to launch a\n // command or a launched runner chose that status after executing tests.\n return {\n exitCode: result.exitCode,\n stdout: stdout.text,\n stderr: stderr.text,\n stdoutTruncated: result.stdoutDroppedBytes > 0 || stdout.truncated,\n stderrTruncated: result.stderrDroppedBytes > 0 || stderr.truncated,\n stdoutDroppedBytes: result.stdoutDroppedBytes + stdout.droppedBytes,\n stderrDroppedBytes: result.stderrDroppedBytes + stderr.droppedBytes,\n ...(mutated === undefined ? {} : { mutated })\n }\n})\n\n/**\n * What one call runs, after planning and before any transport: the program,\n * its arguments (absent for a shell command line), standard input, the\n * caller's environment, and the logical invocation as a person reads it.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Invocation {\n readonly file: string\n readonly args: ReadonlyArray<string> | undefined\n readonly stdin: string | undefined\n readonly env: Record<string, string> | undefined\n readonly quoted: string\n}\n\n/**\n * The invocation `input` names, or the typed refusal explaining why it names\n * none. The same plan {@link run} spawns; a terminal provider runs it instead.\n *\n * @category constructors\n * @since 1.0.0\n */\nexport const invocation = (input: Input): Invocation | StdError.StdError => plan(input)\n\n/**\n * {@link run}'s contract through a registered terminal (T-TRM-05): the same\n * plan, hermetic pre-check, bounded output and diagnostics refresh, with\n * execution supplied by `execute` instead of a spawn on this host. A terminal\n * has no container transport, so a containerised call is refused before it\n * reaches `execute`.\n *\n * @category handlers\n * @since 1.0.0\n */\nexport const inTerminal = <R>(\n execute: (input: Input) => Effect.Effect<Output, StdError.StdError, R>\n) =>\n Effect.fn(\"Bash.inTerminal\")(function*(\n input: Input\n ): Effect.fn.Return<Output, StdError.StdError, R | Path.Path> {\n const intent = precheck(input, yield* Path.Path)\n if (intent instanceof StdError.StdError) return yield* Effect.fail(intent)\n if (input.container !== undefined) return yield* Effect.fail(Container.unavailable(input.container))\n const output = yield* execute(input)\n // The terminal ran on this workspace: files a language server holds may\n // have changed, exactly as after a spawn here.\n yield* Diagnostics.refresh\n return output\n })\n\n/**\n * The refusal a sealed host answers a call outside its container with.\n *\n * @category errors\n * @since 1.0.0-rc.1\n */\nexport const outsideContainer = (sealedTo: string, requested: string | undefined): StdError.StdError =>\n new StdError.StdError({\n code: \"outside_container\",\n message: requested === undefined\n ? `This host runs commands only inside container \"${sealedTo}\". Name it: { mode: \"unhermetic\", container: \"${sealedTo}\", command: \"...\" }.`\n : `This host runs commands only inside container \"${sealedTo}\", not \"${requested}\".`\n })\n\n/**\n * {@link run}, sealed to one container.\n *\n * A host that must not be reachable from the cell (a benchmark host holding\n * other tasks' tests and reference solutions, say) binds this instead of\n * {@link run}: a call naming `sealedTo` runs as usual, and every other call,\n * including one with no container that would run on the host, fails with\n * `outside_container` before anything is spawned.\n *\n * @category handlers\n * @since 1.0.0-rc.1\n */\nexport const sealed = (sealedTo: string) =>\n(\n input: Input\n): Effect.Effect<Output, StdError.StdError, ChildProcessSpawner.ChildProcessSpawner | Path.Path> =>\n input.container === sealedTo ? run(input) : Effect.fail(outsideContainer(sealedTo, input.container))\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,WAAsB;AACtB,aAAwB;AACxB,aAAwB;AACxB,aAAwB;AACxB,gBAA2B;AAC3B,yBAAqC;AACrC,kBAA6B;AAC7B,8BAAgC;AAChC,WAAsB;AACtB,
|
|
6
|
-
"names": ["plan"]
|
|
4
|
+
"sourcesContent": ["/**\n * Bash flow declaration and portable handler.\n *\n * Two ways in, and the difference is where the payload is parsed. `command` is\n * a line for the platform shell, exactly as before. `script` is program text\n * delivered to an interpreter on standard input, as data: nothing quotes it,\n * escapes it, or terminates it with a heredoc marker, so the class of failure\n * that produced `docker exec c bash -lc '\u2026python - <<EOF\u2026'` cannot occur. Its\n * `args` reach the script as arguments rather than as interpolated text.\n *\n * `container` names a container the host knows how to reach, and the argv is\n * built by the injected {@link Container} transport rather than typed by the\n * caller. A host that binds no transport refuses such a call by saying so; the\n * requirement is optional here precisely so a host without containers is not\n * forced to declare one.\n *\n * @since 1.0.0\n */\n\nimport type * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Path from \"@smthrs/kernel/Path\"\nimport * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport * as Container from \"./Container.ts\"\nimport { capability, envelope } from \"./internal/Declaration.ts\"\nimport * as Diagnostics from \"./internal/Diagnostics.ts\"\nimport { outsideEnvelope } from \"./internal/EnvelopePrecheck.ts\"\nimport * as Exec from \"./internal/Exec.ts\"\nimport { elideMiddle as elide, MAX_SHELL_OUTPUT_BYTES } from \"./internal/Text.ts\"\nimport * as Probe from \"./Probe.ts\"\nimport * as StdError from \"./StdError.ts\"\nimport * as TreeFingerprint from \"./TreeFingerprint.ts\"\n\n/**\n * Registry name for the bash flow.\n *\n * @category identifiers\n * @since 1.0.0\n */\nexport const name = \"bash\"\n\n/**\n * Model-facing description of the bash flow.\n *\n * Capped at 200 characters and read every frame, which is why the\n * invalid-probe contract is not restated here: the cell contract teaches it\n * once, and a result that has one explains itself.\n *\n * @category descriptions\n * @since 1.0.0\n */\nexport const description =\n \"Run a shell command, or an interpreter over a script passed as data instead of quoted into a line. container routes it through the host's container transport. mode:hermetic pre-checks path tokens.\"\n\n/**\n * Default command timeout in milliseconds.\n *\n * Ten minutes lets full builds and test suites finish while ensuring a stalled\n * process eventually releases the flow.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const DEFAULT_TIMEOUT_MS = 600_000\n\nconst Command = Schema.optional(Schema.String.annotate({\n description: \"Shell command line to execute; give this or script, never both\"\n}))\nconst Script = Schema.optional(Schema.String.annotate({\n description: \"Program text delivered to the interpreter on stdin as data, so it needs no quoting or heredoc\"\n}))\nconst Interpreter = Schema.optional(Schema.String.annotate({\n description:\n \"Program that reads script on stdin; defaults to bash. python, python3, sh, zsh, node, ruby and perl are known\"\n}))\nconst Args = Schema.optional(\n Schema.Array(Schema.String).annotate({\n description: \"Arguments passed to the script as data, never interpolated into it\"\n })\n)\nconst Stdin = Schema.optional(Schema.String.annotate({\n description: \"Text written to the command's standard input; use script instead when the text is the program\"\n}))\nconst ContainerName = Schema.optional(Schema.String.annotate({\n description: \"Run inside this container through the host's container transport; requires mode:unhermetic\"\n}))\nconst Cwd = Schema.optional(Schema.String).annotate({\n description: \"Working directory for the command\",\n message: \"cwd must be a path string. For a checkpoint, pass { at: ctx.base } as the third ctx.call argument.\"\n})\nconst Env = Schema.optional(\n Schema.Record(Schema.String, Schema.String).annotate({ description: \"Environment variables for the command\" })\n)\nconst TimeoutMs = Schema.optional(Schema.Number).annotate({\n description: \"Wall-clock timeout in milliseconds; defaults to 600000 ms\",\n message: \"timeoutMs must be a number of milliseconds.\"\n})\n\n/**\n * Input schema for the bash flow.\n *\n * The mode named `hermetic` requires explicit read and write envelopes for a\n * lexical pre-check; it does not by itself confine the process. Unhermetic mode\n * deliberately omits those declarations so the distinction stays visible in\n * the input shape.\n *\n * @category schemas\n * @since 1.0.0\n */\nexport const Input = Schema.Union([\n Schema.Struct({\n mode: Schema.Literal(\"hermetic\").annotate({\n description: \"Lexically pre-check explicit path tokens against declared reads and writes; does not sandbox\"\n }),\n command: Command,\n script: Script,\n interpreter: Interpreter,\n args: Args,\n stdin: Stdin,\n container: ContainerName,\n reads: Schema.Array(Schema.NonEmptyString).annotate({ description: \"Paths or path globs the command may read\" }),\n writes: Schema.Array(Schema.NonEmptyString).annotate({ description: \"Paths or path globs the command may write\" }),\n cwd: Cwd,\n env: Env,\n timeoutMs: TimeoutMs\n }),\n Schema.Struct({\n // Omitted means unhermetic: it is the envelope `effects` already declares\n // for every call, so the default grants nothing a named mode would not.\n mode: Schema.Literal(\"unhermetic\").annotate({\n description: \"Run without a declared filesystem envelope as an irreversible effect; the default\"\n }).pipe(Schema.optionalKey, Schema.withDecodingDefaultKey(Effect.succeed(\"unhermetic\" as const))),\n command: Command,\n script: Script,\n interpreter: Interpreter,\n args: Args,\n stdin: Stdin,\n container: ContainerName,\n cwd: Cwd,\n env: Env,\n timeoutMs: TimeoutMs\n })\n])\n\n/**\n * Decoded input accepted by the bash handler.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Input = typeof Input.Type\n\n/**\n * Output schema for the bash flow.\n *\n * `invalidProbe` is present when the shell itself refused to start the\n * command, which its exit code says outright: 127 for a program it could not\n * find, 126 for one it could not execute. Only that. Reading a runner's own\n * wording is a judgment rather than a fact, and `Probe.classify` makes it with\n * Jev for the `test` flow, whose caller is asking about a suite; a shell call\n * is any command at all and stays free of the gateway. See `Probe`.\n *\n * The `<stream>Truncated` flags are a wire convention, not a display detail.\n * A truncated capture is a fragment of what the process printed, and the\n * harness reads these flags to refuse a later write of those exact bytes \u2014\n * `@smthrs/harness/TruncatedOutput` is the consuming half. Renaming a flag, or\n * dropping it when the capture is cut, disarms that guard silently.\n *\n * @category schemas\n * @since 1.0.0\n */\nexport const Output = Schema.Struct({\n exitCode: Schema.Number.annotate({ description: \"Command exit code, including non-zero codes\" }),\n stdout: Schema.String.annotate({\n description:\n \"Captured stdout; when stdoutTruncated is true, only its start and end, with one line naming the bytes omitted between them\"\n }),\n stderr: Schema.String.annotate({\n description:\n \"Captured stderr; when stderrTruncated is true, only its start and end, with one line naming the bytes omitted between them\"\n }),\n stdoutTruncated: Schema.Boolean.annotate({\n description:\n \"Whether stdout exceeded the capture limit, leaving stdout a fragment that must not be written to a file\"\n }),\n stderrTruncated: Schema.Boolean.annotate({\n description:\n \"Whether stderr exceeded the capture limit, leaving stderr a fragment that must not be written to a file\"\n }),\n stdoutDroppedBytes: Schema.Number.annotate({ description: \"UTF-8 bytes omitted from the middle of stdout\" }),\n stderrDroppedBytes: Schema.Number.annotate({ description: \"UTF-8 bytes omitted from the middle of stderr\" }),\n invalidProbe: Schema.optional(\n Probe.InvalidProbe.annotate({\n description: \"Reserved for a confirmed launch failure; an exit code alone does not establish one\"\n })\n ),\n mutated: Schema.optional(\n Schema.Boolean.annotate({\n description:\n \"Present for a containerised command whose working directory was measured before and after it: true when the tree changed, false when it held still. Absent when the tree was not measured, which is never read as unchanged.\"\n })\n )\n})\n\n/**\n * Decoded output returned by the bash handler.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Output = typeof Output.Type\n\n/**\n * The text of a stream cut to its start and end: the start, one line naming\n * the bytes omitted between them, and the end. A host that captures output\n * itself, such as a terminal behind {@link inTerminal}, cuts with this so every\n * {@link Output} reads the same.\n *\n * @category rendering\n * @since 1.0.0-rc.5\n */\nexport const elideMiddle: (start: string, droppedBytes: number, end: string) => string = elide\n\n/**\n * Static conservative effect envelope for the bash flow.\n *\n * The declaration is the registry-time worst case because the mode is not\n * known until an invocation is decoded.\n *\n * @category effects\n * @since 1.0.0\n */\nexport const effects = envelope({\n tier: \"irreversible\",\n mode: \"expected\",\n reads: [],\n writes: []\n})\n\n/**\n * Narrows the effect envelope for a decoded invocation.\n *\n * @category effects\n * @since 1.0.0\n */\nexport const effectsFor = (input: Input) =>\n input.mode === \"hermetic\"\n ? envelope({\n tier: \"irreversible\",\n mode: \"hermetic\",\n reads: input.reads,\n writes: input.writes\n })\n : envelope({\n tier: \"irreversible\",\n mode: \"expected\",\n reads: [],\n writes: []\n })\n\n/**\n * Capabilities required by the bash flow.\n *\n * @category capabilities\n * @since 1.0.0\n */\nexport const capabilities = [capability(\"proc:spawn\", \"*\")]\n\n/**\n * Declaration-only bash flow.\n *\n * @category flows\n * @since 1.0.0\n */\nexport const flow = { name, description, input: Input, output: Output, capabilities, effects } as const\n\n/*\n * `bash` declares no activity on purpose. What a shell call does is in the\n * command, not in the declaration: the same flow runs a test suite, a type\n * check, and `cat`. A reader that recognises a direct runner says so from the\n * recorded command; everything else stays unknown, which is what an absent\n * activity means.\n */\n\n/**\n * How one recorded call to this flow reads: the verb for each settlement, the\n * input field that is its subject, and the measured output fields a one-line\n * summary may count.\n *\n * Display metadata only, governed by `@smthrs/registry`\n * `Descriptor.CallPresentation` and checked where `StandardFlows` binds it.\n *\n * @category presentation\n * @since 1.0.0-rc.0\n */\nexport const presentation = {\n verb: { pending: \"running\", success: \"ran\", failure: \"failed to run\" },\n subject: \"command\",\n result: \"command\"\n} as const\n\n/**\n * Select standard input explicitly. Shells need `--` before script arguments\n * so even an argument such as `-c` cannot replace the stdin program.\n * Other supported interpreters treat arguments after `-` as program data.\n */\nconst stdinArguments = (interpreter: string): ReadonlyArray<string> | undefined => {\n switch (interpreter) {\n case \"bash\":\n case \"zsh\":\n case \"sh\":\n case \"dash\":\n return [\"-s\", \"--\"]\n case \"python\":\n case \"python3\":\n case \"node\":\n case \"ruby\":\n case \"perl\":\n return [\"-\"]\n default:\n return undefined\n }\n}\n\n/** Whether a script is shell text the lexical pre-check can actually read. */\nconst shells = new Set([\"bash\", \"zsh\", \"sh\", \"dash\"])\n\n/** What one invocation runs, before any container transport rewrites it. */\ninterface Plan {\n readonly file: string\n readonly args: ReadonlyArray<string> | undefined\n readonly stdin: string | undefined\n readonly env: Record<string, string> | undefined\n readonly quoted: string\n}\n\nconst invalid = (message: string): StdError.StdError => new StdError.StdError({ code: \"invalid_input\", message })\n\n/** The invocation an input names, or the failure explaining why it names none. */\nconst plan = (input: Input): Plan | StdError.StdError => {\n const interpreter = input.interpreter ?? \"bash\"\n if (input.command !== undefined && input.script !== undefined) {\n return invalid(\"Give command or script, never both: a script is program text, a command is a shell line\")\n }\n if (input.command === undefined && input.script === undefined) {\n return invalid(\"Give either command (a shell line) or script (program text run by interpreter)\")\n }\n if (input.script !== undefined && input.stdin !== undefined) {\n return invalid(\"A script already arrives on standard input, so it cannot also carry stdin\")\n }\n if (input.command !== undefined && input.args !== undefined) {\n return invalid(\"args belong to a script; a command carries its own arguments in the line\")\n }\n if (input.command !== undefined && input.interpreter !== undefined) {\n return invalid(\"interpreter runs a script; a command runs in the platform shell\")\n }\n if (input.mode === \"hermetic\" && input.container !== undefined) {\n return invalid(\n \"A container has its own filesystem, so declared reads and writes cannot describe it. Use mode:unhermetic for a containerised command\"\n )\n }\n if (input.mode === \"hermetic\" && input.script !== undefined && !shells.has(interpreter)) {\n return invalid(\n `The hermetic pre-check reads shell text, and ${interpreter} is not a shell. Run this script with mode:unhermetic, or express it as shell`\n )\n }\n if (input.command !== undefined) {\n return { file: input.command, args: undefined, stdin: input.stdin, env: input.env, quoted: input.command }\n }\n const selector = stdinArguments(interpreter)\n if (selector === undefined) {\n return invalid(`Unsupported script interpreter: ${interpreter}`)\n }\n const args = [...selector, ...(input.args ?? [])]\n return {\n file: interpreter,\n args,\n stdin: input.script,\n env: input.env,\n quoted: [interpreter, ...args].join(\" \")\n }\n}\n\n/**\n * The plan, refused when its input names no one invocation or when a hermetic\n * call's paths fall outside its declared envelope. Spawned and terminal\n * execution share this one check.\n */\nconst precheck = (input: Input, path: Path.Path): Plan | StdError.StdError => {\n const intent = plan(input)\n if (intent instanceof StdError.StdError || input.mode !== \"hermetic\") return intent\n // A planned call carries exactly one of command or script.\n return outsideEnvelope(input, input.command ?? input.script!, path) ?? intent\n}\n\n/** The plan as the host will spawn it, once the container transport has had it. */\nconst routed = (\n plan: Plan,\n input: Input,\n transport: Option.Option<Container.Container>\n): Effect.Effect<Plan, StdError.StdError> => {\n if (input.container === undefined) return Effect.succeed(plan)\n if (Option.isNone(transport)) return Effect.fail(Container.unavailable(input.container))\n return Effect.map(\n transport.value.exec(request(plan, input, input.container)),\n (routing): Plan => ({\n file: routing.file,\n args: routing.args,\n stdin: plan.stdin,\n env: routing.env,\n // Diagnostics describe the logical invocation, not transport arguments.\n quoted: plan.quoted\n })\n )\n}\n\n/**\n * One containerised request, always through a login shell.\n *\n * A container's environment is what its profile makes it. The images this\n * harness runs against activate the project's interpreter \u2014 a conda env, a\n * virtualenv, a modified `PATH` \u2014 from `/etc/profile.d`, so a program spawned\n * directly by `docker exec` gets a different Python from the one that owns the\n * repository's dependencies. That is measured, not theoretical: r90 typed\n * `docker exec \u2026 bash -lc '\u2026'` by hand and got the activation for free, r91\n * routed `interpreter: \"python3\"` straight at the container and filled its\n * traces with `ModuleNotFoundError: No module named 'numpy'` and\n * `exec: \"/usr/local/bin/python\": no such file or directory`. Thirty of 45\n * instances then spent 138 cells scanning `/opt` for the real interpreter.\n *\n * So the login shell is the transport's, not the caller's. A command line is\n * one argument to `bash -lc`, as before. A program run under an interpreter\n * becomes `bash -lc 'exec \"$@\"' bash <interpreter> <args\u2026>`: the shell reads\n * the profile, then `exec` replaces it with the interpreter, so the script\n * still arrives on the inherited standard input and no argument is ever\n * re-parsed as shell text.\n */\nconst request = (plan: Plan, input: Input, container: string): Container.Request => ({\n container,\n file: \"bash\",\n args: plan.args === undefined\n ? [\"-lc\", plan.file]\n : [\"-lc\", `exec \"$@\"`, \"bash\", plan.file, ...plan.args],\n ...(input.cwd === undefined ? {} : { cwd: input.cwd }),\n ...(input.env === undefined ? {} : { env: input.env }),\n stdin: plan.stdin !== undefined\n})\n\n/** How long one fingerprint of the container's working directory may take. */\nconst FINGERPRINT_TIMEOUT_MS = 60_000\n\n/**\n * Measures the tree a containerised command is about to run in, or just ran in.\n *\n * The host's workspace walk cannot see a container's filesystem, so the\n * measurement is taken where the tree is: `TreeFingerprint.script` runs\n * through the same transport as the command, in the same working directory,\n * and answers with one checksum. Every way this can fail \u2014 no transport, a\n * container without `find` or `stat`, a timeout, a bounded listing \u2014 is read\n * as \"unmeasured\" and reported as an absent `mutated`, never as a tree that\n * held still. The time it costs is bounded by the command's own timeout so a\n * stalled container cannot spend more waiting to be measured than it may\n * spend running.\n */\nconst fingerprint = (\n input: Input,\n transport: Option.Option<Container.Container>\n): Effect.Effect<TreeFingerprint.Measurement | undefined, never, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n if (input.container === undefined || Option.isNone(transport)) return undefined\n const routing = yield* transport.value.exec({\n container: input.container,\n file: \"sh\",\n args: [\"-c\", TreeFingerprint.script()],\n ...(input.cwd === undefined ? {} : { cwd: input.cwd }),\n stdin: false\n })\n const result = yield* Exec.exec(routing.file, {\n ...(routing.env === undefined ? {} : { env: routing.env }),\n timeoutMs: Math.min(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, FINGERPRINT_TIMEOUT_MS),\n maxCaptureBytes: MAX_SHELL_OUTPUT_BYTES,\n args: routing.args\n })\n return result.exitCode === 0 ? TreeFingerprint.parse(result.stdout) : undefined\n }).pipe(Effect.orElseSucceed(() => undefined))\n\n/**\n * Executes a shell command through the permission-aware kernel service.\n *\n * Non-zero exit codes remain successful values. Only timeout, host, permission,\n * and declared-envelope pre-check failures use the typed error channel.\n *\n * @category handlers\n * @since 1.0.0\n */\nexport const run = Effect.fn(\"Bash.run\")(function*(\n input: Input\n): Effect.fn.Return<Output, StdError.StdError, ChildProcessSpawner.ChildProcessSpawner | Path.Path> {\n const intent = precheck(input, yield* Path.Path)\n if (intent instanceof StdError.StdError) return yield* Effect.fail(intent)\n // The working directory belongs to the container. The transport supplies\n // any environment overrides its host process needs to forward into it.\n const contained = input.container !== undefined\n const transport = yield* Effect.serviceOption(Container.Container)\n const spawned = yield* routed(intent, input, transport)\n\n // The tree the command runs in, before it runs. Measured in the container\n // because nothing on this host can see it; see `TreeFingerprint`.\n const before = yield* fingerprint(input, transport)\n const result = yield* Exec.exec(spawned.file, {\n ...(input.cwd === undefined || contained ? {} : { cwd: input.cwd }),\n ...(spawned.env === undefined ? {} : { env: spawned.env }),\n timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n maxCaptureBytes: MAX_SHELL_OUTPUT_BYTES,\n overflow: \"ends\",\n ...(spawned.args === undefined ? {} : { args: spawned.args }),\n ...(spawned.stdin === undefined ? {} : { stdin: spawned.stdin })\n }).pipe(Effect.mapError((error) => Exec.toStdError(spawned.quoted, error)))\n const mutated = TreeFingerprint.moved(before, yield* fingerprint(input, transport))\n // A command on this host may have changed files a language server holds\n // open; a container's files are not the ones it was sent.\n if (!contained) yield* Diagnostics.refresh\n // Exit codes alone cannot establish whether the shell refused to launch a\n // command or a launched runner chose that status after executing tests.\n return {\n exitCode: result.exitCode,\n stdout: result.stdout,\n stderr: result.stderr,\n stdoutTruncated: result.stdoutDroppedBytes > 0,\n stderrTruncated: result.stderrDroppedBytes > 0,\n stdoutDroppedBytes: result.stdoutDroppedBytes,\n stderrDroppedBytes: result.stderrDroppedBytes,\n ...(mutated === undefined ? {} : { mutated })\n }\n})\n\n/**\n * What one call runs, after planning and before any transport: the program,\n * its arguments (absent for a shell command line), standard input, the\n * caller's environment, and the logical invocation as a person reads it.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Invocation {\n readonly file: string\n readonly args: ReadonlyArray<string> | undefined\n readonly stdin: string | undefined\n readonly env: Record<string, string> | undefined\n readonly quoted: string\n}\n\n/**\n * The invocation `input` names, or the typed refusal explaining why it names\n * none. The same plan {@link run} spawns; a terminal provider runs it instead.\n *\n * @category constructors\n * @since 1.0.0\n */\nexport const invocation = (input: Input): Invocation | StdError.StdError => plan(input)\n\n/**\n * {@link run}'s contract through a registered terminal (T-TRM-05): the same\n * plan, hermetic pre-check, bounded output and diagnostics refresh, with\n * execution supplied by `execute` instead of a spawn on this host. A terminal\n * has no container transport, so a containerised call is refused before it\n * reaches `execute`.\n *\n * @category handlers\n * @since 1.0.0\n */\nexport const inTerminal = <R>(\n execute: (input: Input) => Effect.Effect<Output, StdError.StdError, R>\n) =>\n Effect.fn(\"Bash.inTerminal\")(function*(\n input: Input\n ): Effect.fn.Return<Output, StdError.StdError, R | Path.Path> {\n const intent = precheck(input, yield* Path.Path)\n if (intent instanceof StdError.StdError) return yield* Effect.fail(intent)\n if (input.container !== undefined) return yield* Effect.fail(Container.unavailable(input.container))\n const output = yield* execute(input)\n // The terminal ran on this workspace: files a language server holds may\n // have changed, exactly as after a spawn here.\n yield* Diagnostics.refresh\n return output\n })\n\n/**\n * The refusal a sealed host answers a call outside its container with.\n *\n * @category errors\n * @since 1.0.0-rc.1\n */\nexport const outsideContainer = (sealedTo: string, requested: string | undefined): StdError.StdError =>\n new StdError.StdError({\n code: \"outside_container\",\n message: requested === undefined\n ? `This host runs commands only inside container \"${sealedTo}\". Name it: { mode: \"unhermetic\", container: \"${sealedTo}\", command: \"...\" }.`\n : `This host runs commands only inside container \"${sealedTo}\", not \"${requested}\".`\n })\n\n/**\n * {@link run}, sealed to one container.\n *\n * A host that must not be reachable from the cell (a benchmark host holding\n * other tasks' tests and reference solutions, say) binds this instead of\n * {@link run}: a call naming `sealedTo` runs as usual, and every other call,\n * including one with no container that would run on the host, fails with\n * `outside_container` before anything is spawned.\n *\n * @category handlers\n * @since 1.0.0-rc.1\n */\nexport const sealed = (sealedTo: string) =>\n(\n input: Input\n): Effect.Effect<Output, StdError.StdError, ChildProcessSpawner.ChildProcessSpawner | Path.Path> =>\n input.container === sealedTo ? run(input) : Effect.fail(outsideContainer(sealedTo, input.container))\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["elide", "plan"]
|
|
7
7
|
}
|
|
@@ -60,9 +60,9 @@ export interface ExecResult {
|
|
|
60
60
|
readonly stdout: string;
|
|
61
61
|
readonly stderr: string;
|
|
62
62
|
readonly exitCode: number;
|
|
63
|
-
/** UTF-8 bytes
|
|
63
|
+
/** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */
|
|
64
64
|
readonly stdoutDroppedBytes: number;
|
|
65
|
-
/** UTF-8 bytes
|
|
65
|
+
/** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */
|
|
66
66
|
readonly stderrDroppedBytes: number;
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
@@ -90,17 +90,24 @@ export interface ExecOptions {
|
|
|
90
90
|
*/
|
|
91
91
|
readonly stdin?: string | undefined;
|
|
92
92
|
/**
|
|
93
|
-
* Bytes retained per captured stream. A stream that prints more keeps
|
|
94
|
-
*
|
|
95
|
-
* bounded amount of memory rather than the whole of what it
|
|
93
|
+
* Bytes retained per captured stream. A stream that prints more keeps what
|
|
94
|
+
* `overflow` names and reports what it dropped, so a command printing
|
|
95
|
+
* gigabytes costs a bounded amount of memory rather than the whole of what it
|
|
96
|
+
* printed.
|
|
96
97
|
*
|
|
97
98
|
* Absent means unbounded, which is what a caller wants when it needs every
|
|
98
99
|
* byte a tool produced (`rg --files`, `git worktree list`).
|
|
99
100
|
*/
|
|
100
101
|
readonly maxCaptureBytes?: number | undefined;
|
|
101
|
-
/**
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* What a stream past the bound keeps. `tail`, the default, keeps its last
|
|
104
|
+
* bytes. `ends` keeps its first and last halves and puts one line naming the
|
|
105
|
+
* dropped bytes where the middle was. `refuse` fails if either stream exceeds
|
|
106
|
+
* the bound.
|
|
107
|
+
*/
|
|
108
|
+
readonly overflow?: Overflow | undefined;
|
|
103
109
|
}
|
|
110
|
+
type Overflow = "tail" | "ends" | "refuse";
|
|
104
111
|
/**
|
|
105
112
|
* Runs a command line through the platform shell and buffers its output.
|
|
106
113
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Exec.d.ts","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,mBAAmB,MAAM,oCAAoC,CAAA;AACzE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"Exec.d.ts","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,mBAAmB,MAAM,oCAAoC,CAAA;AACzE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAG1C;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,0EAAkE,CAAA;AAE5F;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;;;;IAenD,uDAAuD;;;AAbzD;;;;;;;;;GASG;AACH,qBAAa,SAAU,SAAQ,cAK7B;CAAG;AAEL;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,sFAAsF;IACtF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;IACnC,sFAAsF;IACtF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,SAAS,CAAA;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC;;;;;;;;OAQG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7C;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;CACzC;AAED,KAAK,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AA0N1C;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI,GACf,SAAS,MAAM,EACf,UAAS,WAAgB,KACxB,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,SAAS,EAAE,mBAAmB,CAAC,mBAAmB,CAazE,CAAA;AAEN;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,SAAS,KAAG,QAAQ,CAAC,IACF,CAAA;AAEzD;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,OAAO,SAAS,KAAG,QAAQ,CAAC,QAOpE,CAAA"}
|
|
@@ -42,6 +42,7 @@ var ChildProcess = __toESM(require("effect/process/ChildProcess"), 1);
|
|
|
42
42
|
var Schema = __toESM(require("effect/Schema"), 1);
|
|
43
43
|
var Stream = __toESM(require("effect/Stream"), 1);
|
|
44
44
|
var StdError = __toESM(require("../StdError.js"), 1);
|
|
45
|
+
var import_Text = require("./Text.js");
|
|
45
46
|
const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "capture_overflow"]);
|
|
46
47
|
class ExecError extends Schema.TaggedError()("@smthrs/std/ExecError", {
|
|
47
48
|
code: ExecErrorCode,
|
|
@@ -51,44 +52,81 @@ class ExecError extends Schema.TaggedError()("@smthrs/std/ExecError", {
|
|
|
51
52
|
}) {
|
|
52
53
|
}
|
|
53
54
|
const decoder = new TextDecoder("utf-8");
|
|
54
|
-
const
|
|
55
|
-
|
|
55
|
+
const emptyTail = () => ({ chunks: [], bytes: 0, dropped: 0 });
|
|
56
|
+
const isContinuation = (byte) => (byte & 192) === 128;
|
|
57
|
+
const join = (chunks, size) => {
|
|
58
|
+
const bytes = new Uint8Array(size);
|
|
56
59
|
let offset = 0;
|
|
57
|
-
for (const chunk of
|
|
60
|
+
for (const chunk of chunks) {
|
|
58
61
|
bytes.set(chunk, offset);
|
|
59
62
|
offset += chunk.byteLength;
|
|
60
63
|
}
|
|
64
|
+
return bytes;
|
|
65
|
+
};
|
|
66
|
+
const keepTail = (state, chunk, maxBytes) => {
|
|
67
|
+
if (chunk.byteLength === 0) return;
|
|
68
|
+
state.chunks.push(chunk);
|
|
69
|
+
state.bytes += chunk.byteLength;
|
|
70
|
+
while (state.bytes > maxBytes && state.chunks.length > 0) {
|
|
71
|
+
const excess = state.bytes - maxBytes;
|
|
72
|
+
const first = state.chunks[0];
|
|
73
|
+
if (first.byteLength <= excess) {
|
|
74
|
+
state.chunks.shift();
|
|
75
|
+
state.bytes -= first.byteLength;
|
|
76
|
+
state.dropped += first.byteLength;
|
|
77
|
+
} else {
|
|
78
|
+
state.chunks[0] = first.subarray(excess);
|
|
79
|
+
state.bytes -= excess;
|
|
80
|
+
state.dropped += excess;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const decodeTail = (state) => {
|
|
85
|
+
const bytes = join(state.chunks, state.bytes);
|
|
61
86
|
if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 };
|
|
62
87
|
let start = 0;
|
|
63
|
-
while (start < bytes.byteLength && start < 3 && (bytes[start]
|
|
88
|
+
while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start])) start++;
|
|
64
89
|
return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start };
|
|
65
90
|
};
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
state.dropped += first.byteLength;
|
|
80
|
-
} else {
|
|
81
|
-
state.chunks[0] = first.subarray(excess);
|
|
82
|
-
state.bytes -= excess;
|
|
83
|
-
state.dropped += excess;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return state;
|
|
91
|
+
const wholeLength = (bytes) => {
|
|
92
|
+
for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {
|
|
93
|
+
const byte = bytes[bytes.byteLength - back];
|
|
94
|
+
if (isContinuation(byte)) continue;
|
|
95
|
+
const width = byte >= 240 ? 4 : byte >= 224 ? 3 : byte >= 192 ? 2 : 1;
|
|
96
|
+
return width > back ? bytes.byteLength - back : bytes.byteLength;
|
|
97
|
+
}
|
|
98
|
+
return bytes.byteLength;
|
|
99
|
+
};
|
|
100
|
+
const decodeEnds = (state) => {
|
|
101
|
+
if (state.tail.dropped === 0) {
|
|
102
|
+
const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes);
|
|
103
|
+
return { text: decoder.decode(whole), droppedBytes: 0 };
|
|
87
104
|
}
|
|
88
|
-
|
|
105
|
+
const head = join(state.head, state.headBytes);
|
|
106
|
+
const kept = wholeLength(head);
|
|
107
|
+
const tail = decodeTail(state.tail);
|
|
108
|
+
const droppedBytes = head.byteLength - kept + tail.droppedBytes;
|
|
109
|
+
return { text: (0, import_Text.elideMiddle)(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes };
|
|
110
|
+
};
|
|
111
|
+
const boundedTail = (stream, maxBytes, overflow) => Stream.runFold(stream, emptyTail, (state, chunk) => {
|
|
112
|
+
keepTail(state, chunk, maxBytes);
|
|
113
|
+
return state;
|
|
114
|
+
}).pipe(Effect.map(
|
|
89
115
|
(state) => overflow === "refuse" && state.dropped > 0 ? { text: "", droppedBytes: state.dropped } : decodeTail(state)
|
|
90
116
|
));
|
|
91
|
-
const
|
|
117
|
+
const boundedEnds = (stream, maxBytes) => {
|
|
118
|
+
const headBytes = Math.floor(maxBytes / 2);
|
|
119
|
+
return Stream.runFold(stream, () => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {
|
|
120
|
+
const taken = Math.min(headBytes - state.headBytes, chunk.byteLength);
|
|
121
|
+
if (taken > 0) {
|
|
122
|
+
state.head.push(chunk.subarray(0, taken));
|
|
123
|
+
state.headBytes += taken;
|
|
124
|
+
}
|
|
125
|
+
keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes);
|
|
126
|
+
return state;
|
|
127
|
+
}).pipe(Effect.map(decodeEnds));
|
|
128
|
+
};
|
|
129
|
+
const capture = (stream, maxBytes, overflow) => maxBytes === void 0 ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 })) : overflow === "ends" ? boundedEnds(stream, maxBytes) : boundedTail(stream, maxBytes, overflow);
|
|
92
130
|
const unbounded = (command, options) => Effect.gen(function* () {
|
|
93
131
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
94
132
|
const settings = {
|