@hydraharness/harness-shell 0.1.1-rc.5
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/LICENSE +21 -0
- package/README.md +51 -0
- package/lib/index.js +96 -0
- package/lib/invariant.js +17 -0
- package/lib/types/index.d.ts +78 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/render.d.ts +39 -0
- package/lib/types/types.d.ts +175 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @hydraharness/harness-shell
|
|
2
|
+
|
|
3
|
+
The **`ShellExecutor`** (`ctx.shell`) defines WHAT a bash backend does — run foreground commands and start background processes — without saying HOW. Job ids, ownership, collection, cancellation, and notices belong to the generic `ctx.jobs` runtime.
|
|
4
|
+
|
|
5
|
+
This package owns the Service Definition role of the bash capability, split so each role can evolve (and be swapped) independently:
|
|
6
|
+
|
|
7
|
+
| Package | Role |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `@hydraharness/harness-shell` (this) | Service Definition: abstract service + vocabulary types |
|
|
10
|
+
| `@hydraharness/harness-bash-local` | Service Provider: local subprocesses |
|
|
11
|
+
| `@hydraharness/harness-bash-sandbox` | Service Provider: `@hydraharness/harness-bash-local`'s mechanics with every spawn confined via [`ctx.sandbox`](../../sandbox/sandbox/), denials reported as result facts |
|
|
12
|
+
| `@hydraharness/harness-tool-bash` | the model-facing tool schemas over `ctx.shell` |
|
|
13
|
+
|
|
14
|
+
The split is a standard capability seam ([capability-seams Agent Note](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): `@hydraharness/harness-bash-sandbox` is a sandboxing executor behind the same Service Definition — the Consumer detects its `sandboxMode` capability and adds escalation fields without importing the provider — and a containerized or remote executor slots in the same way.
|
|
15
|
+
|
|
16
|
+
## Service API (`ctx.shell`)
|
|
17
|
+
|
|
18
|
+
| Member | Semantics |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `run(spec)` | Foreground execution. Resolves when the command finishes. **Rejects only for infrastructure failures** (unusable workdir, missing shell, pre-aborted signal); nonzero exits, timeout kills, and abort kills resolve with a descriptive `ShellRunResult`. |
|
|
21
|
+
| `start(spec)` | Background execution. Returns a task-free `ShellProcess` handle immediately; **no timeout applies**. The caller may adapt it into `ctx.jobs`. |
|
|
22
|
+
| `sandboxMode` | The capability fact for the tool layer: the default mode a SANDBOXING executor confines under (`undefined` in the base class — "this executor does not sandbox"). `@hydraharness/harness-tool-bash` reads it at registration to advertise the escalation fields only when the composition honors them. |
|
|
23
|
+
| `ShellProcess.readOutput()` | **Incremental** output read — consecutive reads never re-deliver. Reads that lost data to buffer bounds flag `lossy` and point at full-stream spill files. |
|
|
24
|
+
| `ShellProcess.kill()` | Kill the process group. Returns `false` when it already finished. |
|
|
25
|
+
|
|
26
|
+
Implementations subclass `ShellExecutor` and implement the abstract methods. Disposal must kill every running process and await its exit.
|
|
27
|
+
|
|
28
|
+
`SHELL_SETTINGS_NAMESPACE` (`bash`) is exported here rather than by a provider because it names the capability, not an implementation. A host composes exactly one provider of `ctx.shell` — the win32 layer swaps the POSIX rows for the pwsh ones, and mounting both fails loud on a duplicate service registration — so every provider can register this one namespace with its own schema and composition entry without two of them ever colliding, and a `settings.yaml` carried between platforms keeps resolving on both.
|
|
29
|
+
|
|
30
|
+
## Vocabulary
|
|
31
|
+
|
|
32
|
+
`ShellExecRequest` (command, workdir?, timeoutMs?, stdoutMaxBytes?, signal?, stdin?, env?, hydraEnv?, sandboxPolicy?) resolves to `ShellExecSpec` (command, workdir, timeoutMs, stdoutMaxBytes, signal?, stdin?, env?, hydraEnv?, sandboxPolicy) before execution. `stdoutMaxBytes` is a trusted foreground-run capture budget for consumers that must parse complete bounded stdout; the model-facing bash tool does not expose it. `sandboxPolicy` is optional on the request and required-but-nullable on the resolved spec: it carries the complete per-call mode and workspace root. The sandbox tool path resolves it from the calling session through `ctx.sandboxPolicy`; a direct sandbox-executor caller falls back to deployment policy, while a non-sandboxing executor carries the field and confines nothing.
|
|
33
|
+
|
|
34
|
+
The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, the `effectiveSandboxMode(events)` fold, and the `setSandboxMode(session, mode)` write path) is NOT here — it is policy state shared by every enforcing family, owned by [`@hydraharness/harness-sandbox-policy`](../../sandbox/sandbox-policy/). `run()` returns `ShellRunResult`; `start()` returns `ShellProcess`, whose incremental read and kill methods are adapted by `@hydraharness/harness-tool-bash` into a generic task registration. A sandboxing executor stamps `ShellSandboxInfo` on foreground results and settled process handles. See `src/types.ts` and [subsystems/shell.md](../../../docs/subsystems/shell.md).
|
|
35
|
+
|
|
36
|
+
`stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `hydraEnv` is a separate trusted overlay restricted by type to managed keys; the exported `HYDRA_ENV_PREFIX` is the single source for that namespace, its `HydraEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.shellEnv`. Implementations remove inherited managed keys, then merge `hydraEnv` after ordinary `env`, so an omitted current fact cannot fall back to stale ambient state and an `env` entry cannot displace a managed value. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
|
|
37
|
+
|
|
38
|
+
The exported `parseExitStatus` (with `ParsedExitStatus`) is the shared rendering contract half of the shell tools: the inverse of the `[exit code: N]` / `[killed by signal: X]` markers `@hydraharness/harness-tool-bash`'s `renderResult` and `@hydraharness/harness-tool-pwsh`'s `renderPwshResult` append. Both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill; it lives with the Service Definition so the two tools never drift on the marker contract.
|
|
39
|
+
|
|
40
|
+
## Model Experience
|
|
41
|
+
|
|
42
|
+
Indirectly, through `@hydraharness/harness-tool-bash`, which turns executor output and sandbox facts into guidance and retained tool-result tokens.
|
|
43
|
+
|
|
44
|
+
#### KV Cache effect
|
|
45
|
+
|
|
46
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
47
|
+
|
|
48
|
+
## Known Limitations and Deferred Work
|
|
49
|
+
|
|
50
|
+
- **No interactive-input vocabulary** — `stdin` is written once at spawn and closed; the seam has no channel to feed a running task and no PTY session concept.
|
|
51
|
+
- **Foreground timeouts are always executor-owned** — a caller-owned-deadline mode on the seam is explicitly deferred by [the tool-call timeout-policy Agent Note](../../../.agents/notes/implemented/architecture/2026-07-07-tool-call-timeout-policy.md).
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Service } from "@hydraharness/cordis";
|
|
2
|
+
import { settingsNamespace } from "@hydraharness/harness-settings";
|
|
3
|
+
import { HYDRA_ENV_PREFIX } from "@hydraharness/harness-subprocess";
|
|
4
|
+
//#region lib/types/render.js
|
|
5
|
+
/**
|
|
6
|
+
* Shared rendering helpers for the shell tools (`@hydraharness/harness-tool-bash`,
|
|
7
|
+
* `@hydraharness/harness-tool-pwsh`): the exit-status marker contract the tools' renderers
|
|
8
|
+
* emit and the presentation layer parses back.
|
|
9
|
+
* @module @hydraharness/harness-shell/render
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Split a rendered shell-tool result string into its output body and the
|
|
13
|
+
* structured exit status — the inverse of the `[exit code: N]` /
|
|
14
|
+
* `[killed by signal: X]` markers the shell tools' renderers append. A killed
|
|
15
|
+
* marker yields `signal`; otherwise a non-zero marker yields `exitCode`;
|
|
16
|
+
* absent both means a clean exit 0.
|
|
17
|
+
*
|
|
18
|
+
* The consumed marker is removed from `body` because a terminal presentation
|
|
19
|
+
* shows the exit status as its own pill: leaving the marker in the output
|
|
20
|
+
* would render the exit twice. Other markers (timeout, sandbox denial) carry
|
|
21
|
+
* facts no pill shows, so they stay in the body.
|
|
22
|
+
*
|
|
23
|
+
* Replay only retains the rendered content text, not the original
|
|
24
|
+
* `ShellRunResult`, so terminal presentation must recover the exit pill here.
|
|
25
|
+
* Requiring a leading newline and the end of the string keeps ordinary output
|
|
26
|
+
* that merely ends with marker-like text from matching unless the final line
|
|
27
|
+
* is indistinguishable from a real marker.
|
|
28
|
+
* @param text - rendered model-facing shell-tool result.
|
|
29
|
+
* @returns the marker-free body plus the recovered terminal exit code or signal.
|
|
30
|
+
*/
|
|
31
|
+
function parseExitStatus(text) {
|
|
32
|
+
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text);
|
|
33
|
+
if (signal?.[1] !== void 0) return {
|
|
34
|
+
body: text.slice(0, signal.index),
|
|
35
|
+
signal: signal[1]
|
|
36
|
+
};
|
|
37
|
+
const exit = /\n\[exit code: (\d+)\]$/.exec(text);
|
|
38
|
+
if (exit?.[1] !== void 0) return {
|
|
39
|
+
body: text.slice(0, exit.index),
|
|
40
|
+
exitCode: Number(exit[1])
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
body: text,
|
|
44
|
+
exitCode: 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region lib/types/index.js
|
|
49
|
+
/**
|
|
50
|
+
* Service Definition for the `ctx.shell` capability seam, covering foreground commands and background process
|
|
51
|
+
* handles. Job ids, ownership, polling, and notices belong to
|
|
52
|
+
* `@hydraharness/harness-jobs`, keeping executors independent of sessions.
|
|
53
|
+
* @module @hydraharness/harness-shell
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Settings namespace of this capability, owned here rather than by either
|
|
57
|
+
* executor family because it names the capability, not an implementation: a
|
|
58
|
+
* host composes exactly one provider of `ctx.shell` (the win32 layer swaps the
|
|
59
|
+
* POSIX rows for the pwsh ones, and mounting both fails loud on a duplicate
|
|
60
|
+
* service registration), so the providers share one namespace without ever
|
|
61
|
+
* registering it twice, and a settings document carried between platforms
|
|
62
|
+
* keeps resolving on both.
|
|
63
|
+
*/
|
|
64
|
+
const SHELL_SETTINGS_NAMESPACE = settingsNamespace("shell");
|
|
65
|
+
/**
|
|
66
|
+
* Abstract bash execution service. Subclass, implement the abstract methods,
|
|
67
|
+
* and load the subclass as a plugin — it registers as `ctx.shell` (one
|
|
68
|
+
* implementation per context; loading a second throws, which is cordis'
|
|
69
|
+
* standard duplicate-service behavior).
|
|
70
|
+
*
|
|
71
|
+
* Implementations must honor these semantics:
|
|
72
|
+
* - {@link run} rejects only for infrastructure failures. Nonzero exits,
|
|
73
|
+
* timeout kills, and abort kills resolve with a {@link ShellRunResult}.
|
|
74
|
+
* - {@link start} returns immediately; no timeout applies to background
|
|
75
|
+
* processes. `done` settles at process close and never rejects; spawn
|
|
76
|
+
* failures settle as `killed` with the error on stderr.
|
|
77
|
+
* - {@link ShellProcess.readOutput} is incremental: consecutive reads never
|
|
78
|
+
* repeat output. Lossy reads report truncation and available spill files.
|
|
79
|
+
* - A still-running background process is stopped and awaited when its
|
|
80
|
+
* owning composition tears down. With the subprocess seam that
|
|
81
|
+
* boundary is `ctx.subprocess` disposal, so a background process survives
|
|
82
|
+
* an executor-only reload.
|
|
83
|
+
*/
|
|
84
|
+
var ShellExecutor = class extends Service {
|
|
85
|
+
constructor(ctx) {
|
|
86
|
+
super(ctx, "shell");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The sandbox mode this executor applies by default, or `undefined` when it
|
|
90
|
+
* does not sandbox commands.
|
|
91
|
+
* @returns the configured default sandbox mode, when supported.
|
|
92
|
+
*/
|
|
93
|
+
get sandboxMode() {}
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
export { HYDRA_ENV_PREFIX, SHELL_SETTINGS_NAMESPACE, ShellExecutor, ShellExecutor as default, parseExitStatus };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned invariant companion for the bash seam. @module @hydraharness/harness-shell/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@hydraharness/harness-shell";
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
const name = "shell-invariant";
|
|
6
|
+
/** Service required before the companion can reserve package ownership. */
|
|
7
|
+
const inject = ["invariants"];
|
|
8
|
+
/** No runtime invariant: this stateless Service Definition owns request/result types, while executors and policy own observations. */
|
|
9
|
+
const install = () => {};
|
|
10
|
+
/**
|
|
11
|
+
* Register the bash invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
16
|
+
//#endregion
|
|
17
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the `ctx.shell` capability seam, covering foreground commands and background process
|
|
3
|
+
* handles. Job ids, ownership, polling, and notices belong to
|
|
4
|
+
* `@hydraharness/harness-jobs`, keeping executors independent of sessions.
|
|
5
|
+
* @module @hydraharness/harness-shell
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Service } from '@hydraharness/cordis';
|
|
8
|
+
import type { SandboxMode } from '@hydraharness/harness-sandbox';
|
|
9
|
+
import type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellRunResult } from './types.ts';
|
|
10
|
+
/**
|
|
11
|
+
* Settings namespace of this capability, owned here rather than by either
|
|
12
|
+
* executor family because it names the capability, not an implementation: a
|
|
13
|
+
* host composes exactly one provider of `ctx.shell` (the win32 layer swaps the
|
|
14
|
+
* POSIX rows for the pwsh ones, and mounting both fails loud on a duplicate
|
|
15
|
+
* service registration), so the providers share one namespace without ever
|
|
16
|
+
* registering it twice, and a settings document carried between platforms
|
|
17
|
+
* keeps resolving on both.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SHELL_SETTINGS_NAMESPACE: import("@hydraharness/harness-settings").SettingsNamespace;
|
|
20
|
+
export { HYDRA_ENV_PREFIX } from './types.ts';
|
|
21
|
+
export type { ShellExecRequest, ShellExecSpec, ShellProcess, ShellProcessRead, ShellProcessStatus, ShellRunResult, ShellSandboxInfo, CollectedOutput, HydraEnvironment, HydraEnvironmentKey, } from './types.ts';
|
|
22
|
+
export { parseExitStatus } from './render.ts';
|
|
23
|
+
export type { ParsedExitStatus } from './render.ts';
|
|
24
|
+
declare module '@hydraharness/cordis' {
|
|
25
|
+
interface Context {
|
|
26
|
+
shell: ShellExecutor;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Abstract bash execution service. Subclass, implement the abstract methods,
|
|
31
|
+
* and load the subclass as a plugin — it registers as `ctx.shell` (one
|
|
32
|
+
* implementation per context; loading a second throws, which is cordis'
|
|
33
|
+
* standard duplicate-service behavior).
|
|
34
|
+
*
|
|
35
|
+
* Implementations must honor these semantics:
|
|
36
|
+
* - {@link run} rejects only for infrastructure failures. Nonzero exits,
|
|
37
|
+
* timeout kills, and abort kills resolve with a {@link ShellRunResult}.
|
|
38
|
+
* - {@link start} returns immediately; no timeout applies to background
|
|
39
|
+
* processes. `done` settles at process close and never rejects; spawn
|
|
40
|
+
* failures settle as `killed` with the error on stderr.
|
|
41
|
+
* - {@link ShellProcess.readOutput} is incremental: consecutive reads never
|
|
42
|
+
* repeat output. Lossy reads report truncation and available spill files.
|
|
43
|
+
* - A still-running background process is stopped and awaited when its
|
|
44
|
+
* owning composition tears down. With the subprocess seam that
|
|
45
|
+
* boundary is `ctx.subprocess` disposal, so a background process survives
|
|
46
|
+
* an executor-only reload.
|
|
47
|
+
*/
|
|
48
|
+
export declare abstract class ShellExecutor extends Service {
|
|
49
|
+
constructor(ctx: Context);
|
|
50
|
+
/**
|
|
51
|
+
* The sandbox mode this executor applies by default, or `undefined` when it
|
|
52
|
+
* does not sandbox commands.
|
|
53
|
+
* @returns the configured default sandbox mode, when supported.
|
|
54
|
+
*/
|
|
55
|
+
get sandboxMode(): SandboxMode | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Apply implementation-owned defaults and caps to a request before execution.
|
|
58
|
+
* @param request - the caller's request; omitted fields get this
|
|
59
|
+
* implementation's defaults, capped fields are clamped.
|
|
60
|
+
* @returns the fully-specified spec to hand to {@link run}/{@link start}.
|
|
61
|
+
*/
|
|
62
|
+
abstract resolve(request: ShellExecRequest): ShellExecSpec;
|
|
63
|
+
/**
|
|
64
|
+
* Run a command in the foreground; resolves when it finishes.
|
|
65
|
+
* @param spec - a resolved spec from {@link resolve}, never a raw request.
|
|
66
|
+
* @returns the outcome; nonzero exits, timeout kills, and abort kills
|
|
67
|
+
* resolve with a descriptive result rather than reject.
|
|
68
|
+
*/
|
|
69
|
+
abstract run(spec: ShellExecSpec): Promise<ShellRunResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Start a background process and return its handle immediately.
|
|
72
|
+
* @param spec - a resolved spec from {@link resolve}, never a raw request.
|
|
73
|
+
* @returns the live process handle (reads, kill, quiescence promise).
|
|
74
|
+
*/
|
|
75
|
+
abstract start(spec: ShellExecSpec): ShellProcess;
|
|
76
|
+
}
|
|
77
|
+
export default ShellExecutor;
|
|
78
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned invariant companion for the bash seam. @module @hydraharness/harness-shell/invariant */
|
|
2
|
+
import type { Context } from '@hydraharness/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "shell-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the bash invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared rendering helpers for the shell tools (`@hydraharness/harness-tool-bash`,
|
|
3
|
+
* `@hydraharness/harness-tool-pwsh`): the exit-status marker contract the tools' renderers
|
|
4
|
+
* emit and the presentation layer parses back.
|
|
5
|
+
* @module @hydraharness/harness-shell/render
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The exit status recovered from a rendered result, with the output body that
|
|
9
|
+
* status was split off from.
|
|
10
|
+
*/
|
|
11
|
+
export type ParsedExitStatus = {
|
|
12
|
+
body: string;
|
|
13
|
+
} & ({
|
|
14
|
+
exitCode: number;
|
|
15
|
+
} | {
|
|
16
|
+
signal: string;
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Split a rendered shell-tool result string into its output body and the
|
|
20
|
+
* structured exit status — the inverse of the `[exit code: N]` /
|
|
21
|
+
* `[killed by signal: X]` markers the shell tools' renderers append. A killed
|
|
22
|
+
* marker yields `signal`; otherwise a non-zero marker yields `exitCode`;
|
|
23
|
+
* absent both means a clean exit 0.
|
|
24
|
+
*
|
|
25
|
+
* The consumed marker is removed from `body` because a terminal presentation
|
|
26
|
+
* shows the exit status as its own pill: leaving the marker in the output
|
|
27
|
+
* would render the exit twice. Other markers (timeout, sandbox denial) carry
|
|
28
|
+
* facts no pill shows, so they stay in the body.
|
|
29
|
+
*
|
|
30
|
+
* Replay only retains the rendered content text, not the original
|
|
31
|
+
* `ShellRunResult`, so terminal presentation must recover the exit pill here.
|
|
32
|
+
* Requiring a leading newline and the end of the string keeps ordinary output
|
|
33
|
+
* that merely ends with marker-like text from matching unless the final line
|
|
34
|
+
* is indistinguishable from a real marker.
|
|
35
|
+
* @param text - rendered model-facing shell-tool result.
|
|
36
|
+
* @returns the marker-free body plus the recovered terminal exit code or signal.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseExitStatus(text: string): ParsedExitStatus;
|
|
39
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution types for the bash executor seam. Background job semantics belong
|
|
3
|
+
* to `@hydraharness/harness-jobs`; this seam exposes only process handles. The
|
|
4
|
+
* managed-environment and captured-output vocabulary is owned by the
|
|
5
|
+
* subprocess seam and re-exported here so bash consumers keep one import
|
|
6
|
+
* root.
|
|
7
|
+
* @module hydra-shell/types
|
|
8
|
+
*/
|
|
9
|
+
import type { SandboxEnforcement, SandboxExecutionPolicy, SandboxMode } from '@hydraharness/harness-sandbox';
|
|
10
|
+
import type { CollectedOutput, HydraEnvironment } from '@hydraharness/harness-subprocess';
|
|
11
|
+
export { HYDRA_ENV_PREFIX } from '@hydraharness/harness-subprocess';
|
|
12
|
+
export type { CollectedOutput, HydraEnvironment, HydraEnvironmentKey } from '@hydraharness/harness-subprocess';
|
|
13
|
+
/**
|
|
14
|
+
* Sandbox facts for one run, present iff a sandboxing executor handled it.
|
|
15
|
+
* Facts are reported independently of process exit status so callers can
|
|
16
|
+
* distinguish command failures from policy denials and runner failures.
|
|
17
|
+
*/
|
|
18
|
+
export interface ShellSandboxInfo {
|
|
19
|
+
/** The mode the command actually ran under. */
|
|
20
|
+
mode: SandboxMode;
|
|
21
|
+
/** Whether the sandbox denied a file operation. */
|
|
22
|
+
denied: boolean;
|
|
23
|
+
/** How completely the selected runner enforced the requested mode. */
|
|
24
|
+
enforcement?: SandboxEnforcement;
|
|
25
|
+
/** Whether the sandbox runner failed before the command could run. */
|
|
26
|
+
runnerFailed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A caller's execution REQUEST: `workdir` and `timeoutMs` are optional and
|
|
30
|
+
* filled by {@link ShellExecutor.resolve} from the implementation's config.
|
|
31
|
+
* This is the model-/plugin-facing shape; pass it to `resolve()` to obtain a
|
|
32
|
+
* fully-resolved {@link ShellExecSpec}.
|
|
33
|
+
*/
|
|
34
|
+
export interface ShellExecRequest {
|
|
35
|
+
command: string;
|
|
36
|
+
/** Working directory override (default: implementation-configured). */
|
|
37
|
+
workdir?: string | undefined;
|
|
38
|
+
/** Timeout override in milliseconds (implementations cap it). */
|
|
39
|
+
timeoutMs?: number | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Foreground stdout capture budget in bytes. Absent uses the executor's
|
|
42
|
+
* default output cap. Trusted in-process consumers use this when they must
|
|
43
|
+
* parse complete stdout up to their own bounded limit; the model-facing bash
|
|
44
|
+
* tool does not expose it as a parameter.
|
|
45
|
+
*/
|
|
46
|
+
stdoutMaxBytes?: number | undefined;
|
|
47
|
+
/** Abort signal — implementations kill the command when it fires. */
|
|
48
|
+
signal?: AbortSignal | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Bytes to write to the command's stdin, then close it. Absent leaves stdin
|
|
51
|
+
* closed/empty (the default for model-driven tool calls). Set by in-process
|
|
52
|
+
* plugins (e.g. the hooks bridges, which write a hook command's JSON payload
|
|
53
|
+
* to its stdin); the model-facing bash tool does not expose it as a parameter
|
|
54
|
+
* (a model that needs stdin uses shell syntax like a heredoc or a pipe).
|
|
55
|
+
*/
|
|
56
|
+
stdin?: string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Ordinary environment entries for the command, merged after the credential
|
|
59
|
+
* scrub. Managed facts belong in {@link hydraEnv}, which merges after this
|
|
60
|
+
* map, so an entry here can never displace one. Set by in-process plugins
|
|
61
|
+
* (the hooks bridges set `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, …); the
|
|
62
|
+
* model-facing bash tool does not expose it as a parameter.
|
|
63
|
+
*/
|
|
64
|
+
env?: Record<string, string> | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Harness-owned `HYDRA_*` variables for this execution (typed to managed
|
|
67
|
+
* keys). Executors discard ambient `HYDRA_*` entries before merging this
|
|
68
|
+
* snapshot last, so an unavailable current fact cannot inherit a stale
|
|
69
|
+
* value from the harness process and a caller {@link env} entry cannot
|
|
70
|
+
* displace a managed one.
|
|
71
|
+
*/
|
|
72
|
+
hydraEnv?: HydraEnvironment | undefined;
|
|
73
|
+
/** Fully resolved per-call sandbox policy; sandboxing executors default it. */
|
|
74
|
+
sandboxPolicy?: SandboxExecutionPolicy | undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A resolved execution spec. {@link ShellExecutor.resolve} fills and caps the
|
|
78
|
+
* required fields; {@link ShellExecutor.start} ignores `timeoutMs` because
|
|
79
|
+
* background processes have no executor timeout.
|
|
80
|
+
*/
|
|
81
|
+
export interface ShellExecSpec {
|
|
82
|
+
command: string;
|
|
83
|
+
workdir: string;
|
|
84
|
+
timeoutMs: number;
|
|
85
|
+
/**
|
|
86
|
+
* Resolved foreground stdout capture budget in bytes. `run()` uses it for
|
|
87
|
+
* stdout; background jobs and stderr keep the executor's own output cap.
|
|
88
|
+
*/
|
|
89
|
+
stdoutMaxBytes: number;
|
|
90
|
+
/** Abort signal — implementations kill the command when it fires. */
|
|
91
|
+
signal?: AbortSignal | undefined;
|
|
92
|
+
/** Bytes to write to stdin before closing it; absent means no stdin. */
|
|
93
|
+
stdin?: string | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Ordinary environment entries carried through from
|
|
96
|
+
* {@link ShellExecRequest.env}; {@link hydraEnv} still merges after them.
|
|
97
|
+
* OPTIONAL on the spec for the same reason as `stdin`: absent means no
|
|
98
|
+
* ordinary extra environment.
|
|
99
|
+
*/
|
|
100
|
+
env?: Record<string, string> | undefined;
|
|
101
|
+
/** Managed `HYDRA_*` snapshot (typed to managed keys); merges after {@link env}. */
|
|
102
|
+
hydraEnv?: HydraEnvironment | undefined;
|
|
103
|
+
/** Resolved sandbox policy; ignored by executors that do not confine. */
|
|
104
|
+
sandboxPolicy: SandboxExecutionPolicy | undefined;
|
|
105
|
+
}
|
|
106
|
+
/** The outcome of one completed (or killed) foreground run. */
|
|
107
|
+
export interface ShellRunResult {
|
|
108
|
+
/** Exit code; null when the process died from a signal. */
|
|
109
|
+
exitCode: number | null;
|
|
110
|
+
/** Terminating signal (e.g. 'SIGTERM'); null on normal exit. */
|
|
111
|
+
signal: NodeJS.Signals | null;
|
|
112
|
+
/**
|
|
113
|
+
* True when the executor's own timeout was the FIRST cause to cut the command
|
|
114
|
+
* short. Mutually exclusive with {@link aborted}: one fused deadline drives
|
|
115
|
+
* both the timeout and the caller's cancellation, so a timeout and an abort
|
|
116
|
+
* racing before process close report the single first-abort cause, not both
|
|
117
|
+
* (see the [timeout-library Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
|
|
118
|
+
*/
|
|
119
|
+
timedOut: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* True when the caller's `AbortSignal` was the FIRST cause to kill the command
|
|
122
|
+
* (and it was not the executor's own timeout). Mutually exclusive with
|
|
123
|
+
* {@link timedOut} — see there for the first-cause classification.
|
|
124
|
+
*/
|
|
125
|
+
aborted: boolean;
|
|
126
|
+
/** The effective timeout applied to this run (after defaulting/capping). */
|
|
127
|
+
timeoutMs: number;
|
|
128
|
+
stdout: CollectedOutput;
|
|
129
|
+
stderr: CollectedOutput;
|
|
130
|
+
/** Sandbox execution facts, absent for an unsandboxed executor. */
|
|
131
|
+
sandbox?: ShellSandboxInfo;
|
|
132
|
+
}
|
|
133
|
+
/** Lifecycle of a background process. */
|
|
134
|
+
export type ShellProcessStatus = 'running' | 'completed' | 'killed';
|
|
135
|
+
/** One incremental {@link ShellProcess.readOutput} read. */
|
|
136
|
+
export interface ShellProcessRead {
|
|
137
|
+
/** Output produced since the previous read (stderr in a marked section). */
|
|
138
|
+
delta: string;
|
|
139
|
+
/** True when truncation dropped unread bytes the delta cannot include. */
|
|
140
|
+
lossy: boolean;
|
|
141
|
+
/** Full stdout spill file, when stdout truncation occurred and a safe path is available. */
|
|
142
|
+
stdoutSpillPath?: string;
|
|
143
|
+
/** Full stderr spill file, when stderr truncation occurred and a safe path is available. */
|
|
144
|
+
stderrSpillPath?: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A background process handle returned by {@link ShellExecutor.start}. It is the
|
|
148
|
+
* only access path; buffered output remains readable after exit. Composition
|
|
149
|
+
* teardown (the subprocess service's disposal) kills running processes and
|
|
150
|
+
* awaits {@link done}; an executor-only reload leaves them running.
|
|
151
|
+
*/
|
|
152
|
+
export interface ShellProcess {
|
|
153
|
+
/** Process lifecycle state (settled exactly once). */
|
|
154
|
+
status: ShellProcessStatus;
|
|
155
|
+
/** Exit code once finished (null = killed by signal / still running). */
|
|
156
|
+
exitCode: number | null;
|
|
157
|
+
/** Terminating signal name, when signal-killed. */
|
|
158
|
+
signal: NodeJS.Signals | null;
|
|
159
|
+
/** Resolves when the underlying process closes (never rejects — a spawn failure settles as `killed` with the error on stderr). */
|
|
160
|
+
readonly done: Promise<void>;
|
|
161
|
+
/** Sandbox facts, stamped once a confined process settles. */
|
|
162
|
+
sandbox?: ShellSandboxInfo;
|
|
163
|
+
/**
|
|
164
|
+
* Read output produced since the previous read (consuming — consecutive
|
|
165
|
+
* reads never re-deliver). Reads that lost data flag `lossy` and point at
|
|
166
|
+
* full-stream spill files when available.
|
|
167
|
+
*/
|
|
168
|
+
readOutput(): ShellProcessRead;
|
|
169
|
+
/**
|
|
170
|
+
* Kill the process group. Returns false when it had already finished
|
|
171
|
+
* (no-op); idempotent.
|
|
172
|
+
*/
|
|
173
|
+
kill(): boolean;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hydraharness/harness-shell",
|
|
3
|
+
"description": "Abstract bash executor seam (ctx.shell) for the Hydra harness",
|
|
4
|
+
"version": "0.1.1-rc.5",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
|
|
11
|
+
"directory": "packages/shell/shell"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.5",
|
|
36
|
+
"@hydraharness/harness-sandbox": "^0.1.1-rc.5",
|
|
37
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
38
|
+
"@hydraharness/harness-subprocess": "^0.1.1-rc.5",
|
|
39
|
+
"@hydraharness/harness-settings": "^0.1.1-rc.5"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@hydraharness/harness-subprocess": "^0.1.1-rc.5",
|
|
43
|
+
"@hydraharness/harness-invariants": "^0.1.1-rc.5",
|
|
44
|
+
"@hydraharness/cordis": "^4.0.2",
|
|
45
|
+
"@hydraharness/harness-sandbox": "^0.1.1-rc.5",
|
|
46
|
+
"@hydraharness/harness-settings": "^0.1.1-rc.5"
|
|
47
|
+
}
|
|
48
|
+
}
|