hermes-action-bridge 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +111 -14
- package/dist/adapters/hermes-cli.d.ts +14 -0
- package/dist/adapters/hermes-cli.js +80 -19
- package/dist/adapters/hermes-cli.js.map +1 -1
- package/dist/approvals.d.ts +35 -0
- package/dist/approvals.js +96 -0
- package/dist/approvals.js.map +1 -0
- package/dist/audit-log.d.ts +21 -0
- package/dist/audit-log.js +31 -0
- package/dist/audit-log.js.map +1 -0
- package/dist/capabilities.d.ts +29 -0
- package/dist/capabilities.js +24 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli.js +205 -18
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +8 -1
- package/dist/doctor.js +76 -13
- package/dist/doctor.js.map +1 -1
- package/dist/execution-gate.d.ts +5 -0
- package/dist/execution-gate.js +20 -0
- package/dist/execution-gate.js.map +1 -0
- package/dist/http-server.d.ts +18 -0
- package/dist/http-server.js +310 -0
- package/dist/http-server.js.map +1 -0
- package/dist/install/launcher.d.ts +3 -0
- package/dist/install/launcher.js +7 -0
- package/dist/install/launcher.js.map +1 -0
- package/dist/install/mcp-service.d.ts +44 -0
- package/dist/install/mcp-service.js +172 -0
- package/dist/install/mcp-service.js.map +1 -0
- package/dist/install/templates.js +38 -32
- package/dist/install/templates.js.map +1 -1
- package/dist/install/types.d.ts +1 -0
- package/dist/jobs.d.ts +51 -0
- package/dist/jobs.js +244 -0
- package/dist/jobs.js.map +1 -0
- package/dist/mcp-server.d.ts +32 -0
- package/dist/mcp-server.js +382 -25
- package/dist/mcp-server.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/validation.d.ts +1 -0
- package/dist/validation.js +8 -0
- package/dist/validation.js.map +1 -0
- package/docs/ARCHITECTURE.md +67 -7
- package/docs/FUNCTIONAL-TESTS.md +27 -12
- package/examples/claude-code/SKILL.md +35 -29
- package/examples/codex/SKILL.md +35 -29
- package/package.json +5 -3
- package/plugins/hermes-action/.codex-plugin/plugin.json +30 -0
- package/plugins/hermes-action/.mcp.json +8 -0
- package/plugins/hermes-action/assets/app-icon.png +0 -0
- package/plugins/hermes-action/skills/hermes-action-bridge/SKILL.md +61 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,41 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.5.0] - 2026-07-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Complete Codex and Claude Code installation: `install all` now installs the
|
|
14
|
+
managed skills and registers a verified global MCP server through each
|
|
15
|
+
agent's native CLI using the absolute bridge launcher.
|
|
16
|
+
- `doctor` now detects missing/conflicting MCP registrations and performs a
|
|
17
|
+
token-free MCP initialize/list-tools handshake.
|
|
18
|
+
- Configuration-backed `hermes_capabilities`, concurrency-limited
|
|
19
|
+
asynchronous jobs (`submit`, `status`, `result`, `cancel`), and one-shot
|
|
20
|
+
two-phase actions (`prepare`, `approve`, `reject`).
|
|
21
|
+
- Privacy-preserving owner-only JSONL audit metadata for job and approval
|
|
22
|
+
transitions.
|
|
23
|
+
- Opt-in Streamable HTTP transport with loopback defaults and an authenticated,
|
|
24
|
+
Tailscale-only direct-listen mode.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Native skills now prefer discoverable `hermes_*` MCP tools and clearly
|
|
29
|
+
separate Hermes-owned integrations from ordinary local coding work.
|
|
30
|
+
- The Codex plugin manifest now uses the supported `mcpServers` shape, ships in
|
|
31
|
+
the npm tarball, and shares the package version.
|
|
32
|
+
- Refreshed patched `fast-uri` and `postcss` releases in the lockfile and
|
|
33
|
+
documented why the remaining transitive Hono advisory is not reachable in
|
|
34
|
+
the bridge HTTP architecture.
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
- Added a "Trusted presets (direct execution)" README section: how to define a `require_approval_for: []`
|
|
39
|
+
preset in user config to run `execute` actions without the approval gate, plus the safety trade-offs
|
|
40
|
+
(keep it in user scope, never allowlist `hermes-action run`).
|
|
41
|
+
|
|
7
42
|
## [0.4.0] - 2026-07-03
|
|
8
43
|
|
|
9
44
|
Deterministic security model, per-call MCP overrides, and installer hardening.
|
package/README.md
CHANGED
|
@@ -30,7 +30,9 @@ If an agent needs to do something outside its local coding session — research,
|
|
|
30
30
|
- Automatic large-context delivery: an oversized prompt is handed to Hermes through a secure temp file instead of overflowing the command line.
|
|
31
31
|
- Per-run timeouts (configurable, with per-mode defaults) that always reap the Hermes child process.
|
|
32
32
|
- Dry-run mode for debugging the exact Hermes command, prompt, and computed prompt size.
|
|
33
|
-
-
|
|
33
|
+
- Complete Codex and Claude Code installer: managed skill plus verified global MCP registration.
|
|
34
|
+
- MCP capability discovery, cancellable asynchronous jobs, and one-shot two-phase approvals with a privacy-preserving local audit log.
|
|
35
|
+
- Local stdio transport plus an opt-in, authenticated Streamable HTTP transport for Tailscale-restricted hosts.
|
|
34
36
|
- No project-specific assumptions. All behavior is configured through YAML and CLI flags.
|
|
35
37
|
|
|
36
38
|
## Requirements
|
|
@@ -52,13 +54,20 @@ Install from npm:
|
|
|
52
54
|
npm install -g hermes-action-bridge
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
Install the skill and MCP registration for Codex and Claude Code, then verify
|
|
58
|
+
the complete setup:
|
|
56
59
|
|
|
57
60
|
```bash
|
|
58
61
|
hermes-action --version
|
|
62
|
+
hermes-action install all
|
|
59
63
|
hermes-action doctor
|
|
60
64
|
```
|
|
61
65
|
|
|
66
|
+
Global installation is all-or-nothing for the requested agents. If Codex or
|
|
67
|
+
Claude Code is unavailable, conflicting, or cannot verify its user-scoped
|
|
68
|
+
stdio MCP entry, the command exits non-zero without installing the requested
|
|
69
|
+
skills. Use `--project` for an explicit skill-only setup.
|
|
70
|
+
|
|
62
71
|
<details>
|
|
63
72
|
<summary>From source (for development)</summary>
|
|
64
73
|
|
|
@@ -103,10 +112,12 @@ hermes-action run --dry-run --json "Summarize this project."
|
|
|
103
112
|
|
|
104
113
|
## Execution modes
|
|
105
114
|
|
|
106
|
-
- `plan`: Hermes
|
|
115
|
+
- `plan`: Hermes is instructed to return a plan only. Treat it as open-world because configured tools remain available.
|
|
107
116
|
- `draft`: Hermes produces an artifact only. No external side effects.
|
|
108
117
|
- `execute`: Hermes may execute allowed actions, while still following Hermes' own safety rules.
|
|
109
|
-
- `request-approval`: Hermes prepares
|
|
118
|
+
- `request-approval`: Hermes prepares an action without executing it. MCP
|
|
119
|
+
callers should use `hermes_prepare` and then call `hermes_approve` only after
|
|
120
|
+
explicit human approval.
|
|
110
121
|
|
|
111
122
|
The guard is deterministic and language-agnostic: in `execute` mode the bridge switches to `request-approval` by default — regardless of the prompt's wording or language — unless the preset is explicitly trusted (empty `require_approval_for`) or `--yolo` is set. It does not try to infer risk from keywords. See [Security model](#security-model) for why the real barrier is your coding agent's own approval prompt.
|
|
112
123
|
|
|
@@ -132,6 +143,30 @@ YOLO only bypasses the bridge policy. It does not remove Hermes Agent's own safe
|
|
|
132
143
|
|
|
133
144
|
Use it only when the caller and environment are trusted.
|
|
134
145
|
|
|
146
|
+
## Trusted presets (direct execution)
|
|
147
|
+
|
|
148
|
+
By default every `execute` is downgraded to `request-approval` — the right default when an agent might act unattended. When *you* are the one asking for an action and want it to run without that extra gate, define a **trusted preset**: one whose `require_approval_for` is an empty list. Put it in your **user** config so it is yours alone and available in every project:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
# ~/.config/hermes-action/config.yaml (user scope, merged before any project config)
|
|
152
|
+
presets:
|
|
153
|
+
act:
|
|
154
|
+
description: Direct execution of an action I explicitly asked for (trusted).
|
|
155
|
+
require_approval_for: []
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Run actions through it — `execute` stays `execute`, with no downgrade and no `--yolo` needed:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
hermes-action run --preset act --mode execute "post the release note to #general"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Keep it safe:**
|
|
165
|
+
|
|
166
|
+
- A trusted preset relaxes only the bridge's own gate. The real barrier stays your coding agent's approval prompt, so **do not allowlist `hermes-action run` (`execute`/`yolo`)** (see [Security model](#security-model)).
|
|
167
|
+
- Keep it in your **user** config; it never ships in the public package, and the distributed `SKILL.md` stays cautious for everyone else.
|
|
168
|
+
- Reserve trusted presets for actions a human explicitly asks for. Leave the conservative default for anything an agent could trigger on its own.
|
|
169
|
+
|
|
135
170
|
## Configuration
|
|
136
171
|
|
|
137
172
|
`hermes-action` loads config in this order:
|
|
@@ -222,6 +257,7 @@ hermes-action run [options] "request"
|
|
|
222
257
|
hermes-action presets [--json]
|
|
223
258
|
hermes-action status [--json]
|
|
224
259
|
hermes-action mcp
|
|
260
|
+
hermes-action serve [--listen 127.0.0.1] [--port 8765]
|
|
225
261
|
hermes-action install <claude-code|codex|all|mcp> [options]
|
|
226
262
|
hermes-action uninstall <claude-code|codex|all|mcp> [options]
|
|
227
263
|
hermes-action doctor [--json] [--probe]
|
|
@@ -246,17 +282,24 @@ Common `run` options:
|
|
|
246
282
|
|
|
247
283
|
## Native agent skills
|
|
248
284
|
|
|
249
|
-
Instead of pasting instructions
|
|
285
|
+
Instead of pasting instructions and MCP snippets by hand, run the complete
|
|
286
|
+
installer. It installs the open-standard skill and registers the local stdio
|
|
287
|
+
MCP server through each agent's own CLI:
|
|
250
288
|
|
|
251
289
|
```bash
|
|
252
|
-
hermes-action
|
|
253
|
-
hermes-action
|
|
290
|
+
hermes-action install all # skills + global MCP for both agents
|
|
291
|
+
hermes-action doctor # verifies skills, registrations, and MCP handshake
|
|
254
292
|
hermes-action install claude-code --print # preview, write nothing
|
|
255
293
|
hermes-action install codex --project-hint # also add a marker block to AGENTS.md
|
|
256
294
|
hermes-action uninstall all
|
|
257
295
|
```
|
|
258
296
|
|
|
259
|
-
Install behavior is safe by default: it
|
|
297
|
+
Install behavior is safe by default: it uses a stable absolute bridge path,
|
|
298
|
+
does not depend on an interactive `PATH`, never modifies `CLAUDE.md` /
|
|
299
|
+
`AGENTS.md` unless you pass `--project-hint`, and refuses to overwrite either a
|
|
300
|
+
foreign skill or a customized MCP entry. Re-running it is idempotent. Use
|
|
301
|
+
`--project` for a project-local skill only and `--dry-run` to preview
|
|
302
|
+
operations.
|
|
260
303
|
|
|
261
304
|
The generated skill is shown in [`examples/claude-code/SKILL.md`](examples/claude-code/SKILL.md). Project-hint usage is documented in [`examples/claude-code/CLAUDE.md`](examples/claude-code/CLAUDE.md) and [`examples/codex/AGENTS.md`](examples/codex/AGENTS.md).
|
|
262
305
|
|
|
@@ -271,17 +314,20 @@ codex plugin add hermes-action@hermes-action-bridge
|
|
|
271
314
|
|
|
272
315
|
This registers the `hermes-action` MCP server and the Hermes delegation skill in Codex. Hermes Agent must be installed locally — the plugin runs `npx -y hermes-action-bridge mcp`.
|
|
273
316
|
|
|
274
|
-
> **Faster MCP startup:** `npx` re-resolves the package on every server start
|
|
317
|
+
> **Faster MCP startup:** `npx` re-resolves the package on every server start.
|
|
318
|
+
> The native installer registers the absolute global launcher instead.
|
|
275
319
|
|
|
276
320
|
## MCP configuration
|
|
277
321
|
|
|
278
|
-
|
|
322
|
+
Register the global MCP server for Codex and Claude Code without reinstalling
|
|
323
|
+
their skills:
|
|
279
324
|
|
|
280
325
|
```bash
|
|
281
326
|
hermes-action install mcp
|
|
282
327
|
```
|
|
283
328
|
|
|
284
|
-
For
|
|
329
|
+
For other JSON MCP clients, or a Claude Code project-scoped configuration,
|
|
330
|
+
write or merge `.mcp.json` directly while preserving other servers:
|
|
285
331
|
|
|
286
332
|
```bash
|
|
287
333
|
hermes-action install mcp --write
|
|
@@ -325,10 +371,54 @@ Exposed tools:
|
|
|
325
371
|
|
|
326
372
|
- `hermes_run`: delegate a request to Hermes. Optional overrides: `mode`, `preset`, `contextFiles`, `yolo`, `dryRun`, and — to trade cost for speed on simple tasks — `model`, `provider`, `maxTurns`, `timeoutSeconds`.
|
|
327
373
|
- `hermes_plan`: shortcut for `hermes_run` with `mode=plan`.
|
|
374
|
+
- `hermes_capabilities`: report configured presets, skills, toolsets, runtime, and policy without inventing live integrations.
|
|
328
375
|
- `hermes_presets`: list configured presets.
|
|
329
376
|
- `hermes_status`: check the configured Hermes runtime command.
|
|
377
|
+
- `hermes_submit`, `hermes_job_status`, `hermes_result`, `hermes_cancel`:
|
|
378
|
+
manage bounded, process-local asynchronous jobs.
|
|
379
|
+
- `hermes_prepare`, `hermes_approve`, `hermes_reject`: create and consume a
|
|
380
|
+
short-lived one-shot approval before execution.
|
|
381
|
+
|
|
382
|
+
The MCP surface delegates to Hermes instead of mirroring every Hermes tool.
|
|
383
|
+
Jobs and approvals are intentionally process-local: restarting the MCP server
|
|
384
|
+
clears them. Audit events are written as owner-only JSONL metadata under
|
|
385
|
+
`$XDG_STATE_HOME/hermes-action/audit.jsonl` (or
|
|
386
|
+
`~/.local/state/hermes-action/audit.jsonl`) without prompt, context, token, or
|
|
387
|
+
full result content.
|
|
388
|
+
|
|
389
|
+
## Remote MCP over Tailscale
|
|
390
|
+
|
|
391
|
+
The HTTP transport is opt-in and loopback-only by default:
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
hermes-action serve --listen 127.0.0.1 --port 8765
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Prefer placing Tailscale Serve in front of that loopback endpoint. Forwarded
|
|
398
|
+
`*.ts.net` requests still require a bearer token, so start the loopback server
|
|
399
|
+
with `--token-env`:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
export HERMES_ACTION_HTTP_TOKEN="<at-least-32-random-bytes>"
|
|
403
|
+
hermes-action serve \
|
|
404
|
+
--listen 127.0.0.1 \
|
|
405
|
+
--port 8765 \
|
|
406
|
+
--token-env HERMES_ACTION_HTTP_TOKEN
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
For direct tailnet listening, the bridge accepts only an explicit Tailscale
|
|
410
|
+
IPv4 address and requires the same bearer-token setup:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
hermes-action serve \
|
|
414
|
+
--listen 100.64.0.10 \
|
|
415
|
+
--port 8765 \
|
|
416
|
+
--allow-tailnet \
|
|
417
|
+
--token-env HERMES_ACTION_HTTP_TOKEN
|
|
418
|
+
```
|
|
330
419
|
|
|
331
|
-
The
|
|
420
|
+
The token is never accepted as a CLI value or stored in bridge configuration.
|
|
421
|
+
Public/wildcard and non-Tailscale addresses are refused.
|
|
332
422
|
|
|
333
423
|
## Development
|
|
334
424
|
|
|
@@ -351,11 +441,18 @@ Functional tests use a fake Hermes binary to verify command construction, prompt
|
|
|
351
441
|
|
|
352
442
|
## Security model
|
|
353
443
|
|
|
354
|
-
|
|
444
|
+
Direct CLI and `hermes_run` usage assumes a **supervised host agent**: Claude
|
|
445
|
+
Code / Codex ask you to approve the command or tool call. For MCP side effects,
|
|
446
|
+
prefer the bridge's additional, testable two-phase flow:
|
|
447
|
+
`hermes_prepare` returns a local no-tool summary and opaque approval ID, and
|
|
448
|
+
`hermes_approve` consumes the unchanged request exactly once after the human
|
|
449
|
+
approves it.
|
|
355
450
|
|
|
356
451
|
The bridge's own guard is a secondary net: `execute` is downgraded to `request-approval` by default unless a preset is trusted (empty `require_approval_for`) or `--yolo` is set. The bridge never injects `--yolo` or a bypass flag on its own.
|
|
357
452
|
|
|
358
|
-
- **Do not allowlist / auto-approve `hermes-action run
|
|
453
|
+
- **Do not allowlist / auto-approve `hermes-action run`, `hermes_run`, or
|
|
454
|
+
`hermes_approve`**. A command-pattern allowlist cannot distinguish a safe
|
|
455
|
+
plan from an execution with side effects.
|
|
359
456
|
- Do not put secrets in `.hermes-action.yaml`. Keep provider credentials in Hermes Agent, your OS keychain, or the platform's secure store.
|
|
360
457
|
- Treat `--yolo` as a trusted-local escape hatch, not a default.
|
|
361
458
|
|
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
import type { AdapterResult, BridgeConfig, EffectiveRun } from "../types.js";
|
|
2
2
|
export declare function buildHermesCliArgs(run: EffectiveRun, prompt?: string, extraToolsets?: string[]): string[];
|
|
3
|
+
/** A started Hermes process. `cancel` is idempotent and asks the child to stop gracefully before forcing it. */
|
|
4
|
+
export interface HermesCliExecution {
|
|
5
|
+
result: Promise<AdapterResult>;
|
|
6
|
+
cancel: () => boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface HermesCliStartOptions {
|
|
9
|
+
maxOutputBytes?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Start a Hermes CLI invocation without awaiting it. Consumers that need asynchronous work can retain
|
|
13
|
+
* the cancellation handle; `runHermesCli` below remains the backward-compatible awaitable wrapper.
|
|
14
|
+
*/
|
|
15
|
+
export declare function startHermesCli(config: BridgeConfig, run: EffectiveRun, dryRun: boolean, options?: HermesCliStartOptions): HermesCliExecution;
|
|
16
|
+
/** Backward-compatible synchronous facade for CLI callers. */
|
|
3
17
|
export declare function runHermesCli(config: BridgeConfig, run: EffectiveRun, dryRun: boolean): Promise<AdapterResult>;
|
|
@@ -38,7 +38,15 @@ function filePointerPrompt(path) {
|
|
|
38
38
|
"Never reveal secrets.",
|
|
39
39
|
].join("\n");
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Start a Hermes CLI invocation without awaiting it. Consumers that need asynchronous work can retain
|
|
43
|
+
* the cancellation handle; `runHermesCli` below remains the backward-compatible awaitable wrapper.
|
|
44
|
+
*/
|
|
45
|
+
export function startHermesCli(config, run, dryRun, options = {}) {
|
|
46
|
+
const maxOutputBytes = options.maxOutputBytes;
|
|
47
|
+
if (maxOutputBytes !== undefined && (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0)) {
|
|
48
|
+
throw new Error("maxOutputBytes must be a positive safe integer");
|
|
49
|
+
}
|
|
42
50
|
const prompt = buildHermesPrompt(run);
|
|
43
51
|
const promptBytes = Buffer.byteLength(prompt, "utf8");
|
|
44
52
|
const isLarge = promptBytes > largePromptThresholdBytes;
|
|
@@ -46,7 +54,20 @@ export async function runHermesCli(config, run, dryRun) {
|
|
|
46
54
|
const args = isLarge ? buildHermesCliArgs(run, filePointerPrompt("<temp-file>"), ["file"]) : buildHermesCliArgs(run, prompt);
|
|
47
55
|
const command = [config.runtime.command, ...args];
|
|
48
56
|
const stdout = JSON.stringify({ command, prompt, promptBytes, promptChars: prompt.length, delivery: isLarge ? "temp-file" : "argv" }, null, 2);
|
|
49
|
-
|
|
57
|
+
const captured = captureChunk(stdout, 0, maxOutputBytes);
|
|
58
|
+
return {
|
|
59
|
+
result: Promise.resolve({
|
|
60
|
+
ok: true,
|
|
61
|
+
exitCode: 0,
|
|
62
|
+
stdout: captured.value,
|
|
63
|
+
stderr: "",
|
|
64
|
+
command,
|
|
65
|
+
prompt,
|
|
66
|
+
dryRun: true,
|
|
67
|
+
outputTruncated: captured.truncated,
|
|
68
|
+
}),
|
|
69
|
+
cancel: () => false,
|
|
70
|
+
};
|
|
50
71
|
}
|
|
51
72
|
// Large envelopes can't ride argv (E2BIG); write them to a secure temp file the child reads itself.
|
|
52
73
|
// tmpDir is set only after a successful write so a failed write cleans up and never leaks an empty dir.
|
|
@@ -69,22 +90,27 @@ export async function runHermesCli(config, run, dryRun) {
|
|
|
69
90
|
}
|
|
70
91
|
})();
|
|
71
92
|
const command = [config.runtime.command, ...args];
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
let cancelRequested = false;
|
|
94
|
+
let settled = false;
|
|
95
|
+
let killTimer;
|
|
96
|
+
let child;
|
|
97
|
+
const result = new Promise((resolve) => {
|
|
98
|
+
const spawned = spawn(config.runtime.command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
99
|
+
child = spawned;
|
|
74
100
|
let stdout = "";
|
|
75
101
|
let stderr = "";
|
|
102
|
+
let capturedBytes = 0;
|
|
103
|
+
let outputTruncated = false;
|
|
76
104
|
let timedOut = false;
|
|
77
|
-
let killTimer;
|
|
78
105
|
const timeoutMs = Math.min(run.timeoutSeconds * 1000, maxTimeoutMs);
|
|
79
106
|
const timer = setTimeout(() => {
|
|
80
107
|
timedOut = true;
|
|
81
|
-
child
|
|
82
|
-
killTimer = setTimeout(() => child
|
|
108
|
+
child?.kill("SIGTERM");
|
|
109
|
+
killTimer = setTimeout(() => child?.kill("SIGKILL"), killGraceMs);
|
|
83
110
|
}, timeoutMs);
|
|
84
111
|
// 'error' and 'close' can both fire (e.g. spawn ENOENT); guard so cleanup and resolve run exactly once.
|
|
85
112
|
// The temp file is removed here on every settle path. Only an uncatchable SIGKILL of the bridge itself
|
|
86
113
|
// would leave it behind; mkdtemp + 0600 + the OS temp reaper bound that residual risk.
|
|
87
|
-
let settled = false;
|
|
88
114
|
const settle = (result) => {
|
|
89
115
|
if (settled)
|
|
90
116
|
return;
|
|
@@ -102,26 +128,61 @@ export async function runHermesCli(config, run, dryRun) {
|
|
|
102
128
|
}
|
|
103
129
|
resolve(result);
|
|
104
130
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
spawned.stdout.setEncoding("utf8");
|
|
132
|
+
spawned.stderr.setEncoding("utf8");
|
|
133
|
+
spawned.stdout.on("data", (chunk) => {
|
|
134
|
+
const captured = captureChunk(chunk, capturedBytes, maxOutputBytes);
|
|
135
|
+
stdout += captured.value;
|
|
136
|
+
capturedBytes += captured.bytes;
|
|
137
|
+
outputTruncated ||= captured.truncated;
|
|
109
138
|
});
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
spawned.stderr.on("data", (chunk) => {
|
|
140
|
+
const captured = captureChunk(chunk, capturedBytes, maxOutputBytes);
|
|
141
|
+
stderr += captured.value;
|
|
142
|
+
capturedBytes += captured.bytes;
|
|
143
|
+
outputTruncated ||= captured.truncated;
|
|
112
144
|
});
|
|
113
|
-
|
|
114
|
-
settle({ ok: false, exitCode: 127, stdout, stderr: `${stderr}${error.message}`, command, prompt, dryRun: false });
|
|
145
|
+
spawned.on("error", (error) => {
|
|
146
|
+
settle({ ok: false, exitCode: 127, stdout, stderr: `${stderr}${error.message}`, command, prompt, dryRun: false, outputTruncated });
|
|
115
147
|
});
|
|
116
|
-
|
|
148
|
+
spawned.on("close", (code) => {
|
|
117
149
|
if (timedOut) {
|
|
118
150
|
const message = `Hermes timed out after ${run.timeoutSeconds}s and was terminated.`;
|
|
119
|
-
settle({ ok: false, exitCode: code ?? 124, stdout, stderr: stderr ? `${stderr}\n${message}` : message, command, prompt, dryRun: false, timedOut: true });
|
|
151
|
+
settle({ ok: false, exitCode: code ?? 124, stdout, stderr: stderr ? `${stderr}\n${message}` : message, command, prompt, dryRun: false, outputTruncated, timedOut: true });
|
|
120
152
|
return;
|
|
121
153
|
}
|
|
122
154
|
const exitCode = code ?? 1;
|
|
123
|
-
settle({ ok: exitCode === 0, exitCode, stdout, stderr, command, prompt, dryRun: false });
|
|
155
|
+
settle({ ok: exitCode === 0, exitCode, stdout, stderr, command, prompt, dryRun: false, outputTruncated });
|
|
124
156
|
});
|
|
125
157
|
});
|
|
158
|
+
const cancel = () => {
|
|
159
|
+
if (settled || cancelRequested || !child)
|
|
160
|
+
return false;
|
|
161
|
+
if (!child.kill("SIGTERM"))
|
|
162
|
+
return false;
|
|
163
|
+
cancelRequested = true;
|
|
164
|
+
killTimer = setTimeout(() => child?.kill("SIGKILL"), killGraceMs);
|
|
165
|
+
return true;
|
|
166
|
+
};
|
|
167
|
+
return { result, cancel };
|
|
168
|
+
}
|
|
169
|
+
/** Backward-compatible synchronous facade for CLI callers. */
|
|
170
|
+
export async function runHermesCli(config, run, dryRun) {
|
|
171
|
+
return startHermesCli(config, run, dryRun).result;
|
|
172
|
+
}
|
|
173
|
+
function captureChunk(chunk, capturedBytes, maxOutputBytes) {
|
|
174
|
+
if (maxOutputBytes === undefined)
|
|
175
|
+
return { value: chunk, bytes: Buffer.byteLength(chunk, "utf8"), truncated: false };
|
|
176
|
+
const remaining = Math.max(0, maxOutputBytes - capturedBytes);
|
|
177
|
+
if (remaining === 0)
|
|
178
|
+
return { value: "", bytes: 0, truncated: chunk.length > 0 };
|
|
179
|
+
const buffer = Buffer.from(chunk, "utf8");
|
|
180
|
+
if (buffer.length <= remaining)
|
|
181
|
+
return { value: chunk, bytes: buffer.length, truncated: false };
|
|
182
|
+
let end = remaining;
|
|
183
|
+
while (end > 0 && (buffer[end] ?? 0) >= 0x80 && (buffer[end] ?? 0) < 0xc0)
|
|
184
|
+
end -= 1;
|
|
185
|
+
const value = buffer.subarray(0, end).toString("utf8");
|
|
186
|
+
return { value, bytes: Buffer.byteLength(value, "utf8"), truncated: true };
|
|
126
187
|
}
|
|
127
188
|
//# sourceMappingURL=hermes-cli.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hermes-cli.js","sourceRoot":"","sources":["../../src/adapters/hermes-cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEjD,+EAA+E;AAC/E,MAAM,WAAW,GAAG,IAAI,CAAC;AACzB,0IAA0I;AAC1I,MAAM,YAAY,GAAG,aAAa,CAAC;AACnC;;;;GAIG;AACH,MAAM,yBAAyB,GAAG,OAAO,CAAC;AAE1C,MAAM,UAAU,kBAAkB,CAChC,GAAiB,EACjB,SAAiB,iBAAiB,CAAC,GAAG,CAAC,EACvC,gBAA0B,EAAE;IAE5B,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzF,IAAI,GAAG,CAAC,QAAQ;QAAE,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;IACxD,IAAI,GAAG,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IAC/C,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACjF,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAC1E,IAAI,QAAQ,CAAC,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACjE,IAAI,GAAG,CAAC,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACxB,OAAO,IAAI,CAAC;AACd,CAAC;
|
|
1
|
+
{"version":3,"file":"hermes-cli.js","sourceRoot":"","sources":["../../src/adapters/hermes-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EffectiveRun } from "./types.js";
|
|
2
|
+
export type ApprovalStatus = "awaiting_approval" | "approved" | "rejected" | "expired";
|
|
3
|
+
export interface ApprovalRequest {
|
|
4
|
+
id: string;
|
|
5
|
+
status: ApprovalStatus;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
expiresAt: string;
|
|
8
|
+
requestedMode: EffectiveRun["requestedMode"];
|
|
9
|
+
effectiveMode: EffectiveRun["mode"];
|
|
10
|
+
presetName: string;
|
|
11
|
+
detectedRisks: EffectiveRun["detectedRisks"];
|
|
12
|
+
}
|
|
13
|
+
export interface ConsumedApproval {
|
|
14
|
+
approval: ApprovalRequest;
|
|
15
|
+
run: EffectiveRun;
|
|
16
|
+
}
|
|
17
|
+
export interface ApprovalStoreOptions {
|
|
18
|
+
ttlMs?: number | undefined;
|
|
19
|
+
maxPending?: number | undefined;
|
|
20
|
+
now?: () => Date;
|
|
21
|
+
createId?: () => string;
|
|
22
|
+
onExpired?: ((approval: ApprovalRequest) => void) | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface ApprovalStore {
|
|
25
|
+
prepare(run: EffectiveRun): ApprovalRequest;
|
|
26
|
+
get(approvalId: string): ApprovalRequest | undefined;
|
|
27
|
+
approve(approvalId: string): ConsumedApproval | undefined;
|
|
28
|
+
reject(approvalId: string): ApprovalRequest | undefined;
|
|
29
|
+
cleanup(): number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One-shot, in-process approvals. A stored run is returned exactly once by `approve`; callers must
|
|
33
|
+
* execute that unchanged run rather than accepting new execution options at approval time.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createApprovalStore(options?: ApprovalStoreOptions): ApprovalStore;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { positiveInteger } from "./validation.js";
|
|
3
|
+
const defaultTtlMs = 15 * 60 * 1000;
|
|
4
|
+
const defaultMaxPending = 32;
|
|
5
|
+
/**
|
|
6
|
+
* One-shot, in-process approvals. A stored run is returned exactly once by `approve`; callers must
|
|
7
|
+
* execute that unchanged run rather than accepting new execution options at approval time.
|
|
8
|
+
*/
|
|
9
|
+
export function createApprovalStore(options = {}) {
|
|
10
|
+
const ttlMs = positiveInteger(options.ttlMs, defaultTtlMs, "ttlMs");
|
|
11
|
+
const maxPending = positiveInteger(options.maxPending, defaultMaxPending, "maxPending");
|
|
12
|
+
const now = options.now ?? (() => new Date());
|
|
13
|
+
const createId = options.createId ?? randomUUID;
|
|
14
|
+
const onExpired = options.onExpired;
|
|
15
|
+
const entries = new Map();
|
|
16
|
+
const expire = (entry, current) => {
|
|
17
|
+
if (entry.approval.status === "awaiting_approval" && Date.parse(entry.approval.expiresAt) <= current.getTime()) {
|
|
18
|
+
entry.approval.status = "expired";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const cleanupExpired = () => {
|
|
22
|
+
const current = now();
|
|
23
|
+
let removed = 0;
|
|
24
|
+
for (const [id, entry] of entries) {
|
|
25
|
+
expire(entry, current);
|
|
26
|
+
if (Date.parse(entry.approval.expiresAt) > current.getTime())
|
|
27
|
+
continue;
|
|
28
|
+
if (entry.approval.status === "expired") {
|
|
29
|
+
try {
|
|
30
|
+
onExpired?.(structuredClone(entry.approval));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Expiration cleanup must not crash a long-lived bridge when best-effort telemetry fails.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
entries.delete(id);
|
|
37
|
+
removed += 1;
|
|
38
|
+
}
|
|
39
|
+
return removed;
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
prepare(run) {
|
|
43
|
+
cleanupExpired();
|
|
44
|
+
if (entries.size >= maxPending)
|
|
45
|
+
throw new Error(`Hermes approval store is full (${maxPending} pending)`);
|
|
46
|
+
const created = now();
|
|
47
|
+
const id = createId();
|
|
48
|
+
if (entries.has(id))
|
|
49
|
+
throw new Error("Approval ID collision");
|
|
50
|
+
const approval = {
|
|
51
|
+
id,
|
|
52
|
+
status: "awaiting_approval",
|
|
53
|
+
createdAt: created.toISOString(),
|
|
54
|
+
expiresAt: new Date(created.getTime() + ttlMs).toISOString(),
|
|
55
|
+
requestedMode: run.requestedMode,
|
|
56
|
+
effectiveMode: run.mode,
|
|
57
|
+
presetName: run.presetName,
|
|
58
|
+
detectedRisks: [...run.detectedRisks],
|
|
59
|
+
};
|
|
60
|
+
entries.set(id, { approval, run: structuredClone(run) });
|
|
61
|
+
return structuredClone(approval);
|
|
62
|
+
},
|
|
63
|
+
get(approvalId) {
|
|
64
|
+
const entry = entries.get(approvalId);
|
|
65
|
+
if (!entry)
|
|
66
|
+
return undefined;
|
|
67
|
+
expire(entry, now());
|
|
68
|
+
return structuredClone(entry.approval);
|
|
69
|
+
},
|
|
70
|
+
approve(approvalId) {
|
|
71
|
+
const entry = entries.get(approvalId);
|
|
72
|
+
if (!entry)
|
|
73
|
+
return undefined;
|
|
74
|
+
expire(entry, now());
|
|
75
|
+
if (entry.approval.status !== "awaiting_approval")
|
|
76
|
+
return undefined;
|
|
77
|
+
entry.approval.status = "approved";
|
|
78
|
+
const consumed = { approval: structuredClone(entry.approval), run: structuredClone(entry.run) };
|
|
79
|
+
entries.delete(approvalId);
|
|
80
|
+
return consumed;
|
|
81
|
+
},
|
|
82
|
+
reject(approvalId) {
|
|
83
|
+
const entry = entries.get(approvalId);
|
|
84
|
+
if (!entry)
|
|
85
|
+
return undefined;
|
|
86
|
+
expire(entry, now());
|
|
87
|
+
if (entry.approval.status === "awaiting_approval")
|
|
88
|
+
entry.approval.status = "rejected";
|
|
89
|
+
const rejected = structuredClone(entry.approval);
|
|
90
|
+
entries.delete(approvalId);
|
|
91
|
+
return rejected;
|
|
92
|
+
},
|
|
93
|
+
cleanup: cleanupExpired,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=approvals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approvals.js","sourceRoot":"","sources":["../src/approvals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BridgeMode, RiskCategory } from "./types.js";
|
|
2
|
+
export type AuditPhase = "prepared" | "approved" | "rejected" | "submitted" | "completed" | "cancelled" | "expired";
|
|
3
|
+
/** Deliberately narrow: prompts, context, tool output, credentials and bearer tokens are not representable. */
|
|
4
|
+
export interface AuditEvent {
|
|
5
|
+
timestamp: string;
|
|
6
|
+
phase: AuditPhase;
|
|
7
|
+
requestId: string;
|
|
8
|
+
principalId: string;
|
|
9
|
+
transport: "stdio" | "http";
|
|
10
|
+
requestedMode: BridgeMode;
|
|
11
|
+
effectiveMode: BridgeMode;
|
|
12
|
+
presetName: string;
|
|
13
|
+
detectedRisks: RiskCategory[];
|
|
14
|
+
promptFingerprint: string;
|
|
15
|
+
exitCode?: number | undefined;
|
|
16
|
+
outcome?: "succeeded" | "failed" | "cancelled" | "timed_out" | undefined;
|
|
17
|
+
}
|
|
18
|
+
/** SHA-256 lets related events be correlated without retaining the private handoff. */
|
|
19
|
+
export declare function fingerprintPrompt(prompt: string): string;
|
|
20
|
+
/** Append a restricted audit record as a single JSONL line. The directory and file are owner-only. */
|
|
21
|
+
export declare function appendAuditEvent(file: string, event: AuditEvent): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { appendFileSync, chmodSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
/** SHA-256 lets related events be correlated without retaining the private handoff. */
|
|
5
|
+
export function fingerprintPrompt(prompt) {
|
|
6
|
+
return createHash("sha256").update(prompt, "utf8").digest("hex");
|
|
7
|
+
}
|
|
8
|
+
/** Append a restricted audit record as a single JSONL line. The directory and file are owner-only. */
|
|
9
|
+
export function appendAuditEvent(file, event) {
|
|
10
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
11
|
+
const line = `${JSON.stringify(sanitizeEvent(event))}\n`;
|
|
12
|
+
appendFileSync(file, line, { encoding: "utf8", mode: 0o600 });
|
|
13
|
+
chmodSync(file, 0o600);
|
|
14
|
+
}
|
|
15
|
+
function sanitizeEvent(event) {
|
|
16
|
+
return {
|
|
17
|
+
timestamp: event.timestamp,
|
|
18
|
+
phase: event.phase,
|
|
19
|
+
requestId: event.requestId,
|
|
20
|
+
principalId: event.principalId,
|
|
21
|
+
transport: event.transport,
|
|
22
|
+
requestedMode: event.requestedMode,
|
|
23
|
+
effectiveMode: event.effectiveMode,
|
|
24
|
+
presetName: event.presetName,
|
|
25
|
+
detectedRisks: [...event.detectedRisks],
|
|
26
|
+
promptFingerprint: event.promptFingerprint,
|
|
27
|
+
...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
|
|
28
|
+
...(event.outcome === undefined ? {} : { outcome: event.outcome }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=audit-log.js.map
|