@hmharness/agent 0.6.6 → 0.6.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/dist/runner.d.ts +3 -2
- package/dist/runner.js +4 -5
- package/dist/tools.d.ts +7 -4
- package/dist/tools.js +33 -12
- package/package.json +5 -5
package/dist/runner.d.ts
CHANGED
|
@@ -62,8 +62,9 @@ export interface ApprovedRule {
|
|
|
62
62
|
export declare function loadApprovedRules(home: string): ApprovedRule[];
|
|
63
63
|
/** Structured rule matching (review fix: raw string prefix was exploitable -
|
|
64
64
|
* `node scripts/` prefix was hit by `node scripts/../../evil.js`). Now:
|
|
65
|
-
* parses the rule as structured args, string values match by prefix
|
|
66
|
-
*
|
|
65
|
+
* parses the rule as structured args, string values match by prefix AND no
|
|
66
|
+
* path SEGMENT of the extension may be `..` (a boundary-only check missed
|
|
67
|
+
* `node scripts/sub/../../evil.js`, whose first differing segment is `sub`). */
|
|
67
68
|
export declare function matchesRule(rules: ApprovedRule[], toolName: string, args: Record<string, unknown>): boolean;
|
|
68
69
|
export declare function makeApproval(cfg: HmhConfig, yes: boolean, sharedRl?: readline.Interface): LoopApproval;
|
|
69
70
|
export interface RunnerEvents {
|
package/dist/runner.js
CHANGED
|
@@ -157,8 +157,9 @@ function saveApprovedRules(home, rules) {
|
|
|
157
157
|
}
|
|
158
158
|
/** Structured rule matching (review fix: raw string prefix was exploitable -
|
|
159
159
|
* `node scripts/` prefix was hit by `node scripts/../../evil.js`). Now:
|
|
160
|
-
* parses the rule as structured args, string values match by prefix
|
|
161
|
-
*
|
|
160
|
+
* parses the rule as structured args, string values match by prefix AND no
|
|
161
|
+
* path SEGMENT of the extension may be `..` (a boundary-only check missed
|
|
162
|
+
* `node scripts/sub/../../evil.js`, whose first differing segment is `sub`). */
|
|
162
163
|
export function matchesRule(rules, toolName, args) {
|
|
163
164
|
return rules.some((r) => {
|
|
164
165
|
if (r.tool !== toolName)
|
|
@@ -170,9 +171,7 @@ export function matchesRule(rules, toolName, args) {
|
|
|
170
171
|
if (typeof rv === 'string' && typeof av === 'string') {
|
|
171
172
|
if (!av.startsWith(rv))
|
|
172
173
|
return false;
|
|
173
|
-
|
|
174
|
-
// start with `..` (blocks `node scripts/` → `node scripts/../../x`)
|
|
175
|
-
if (av.slice(rv.length).startsWith('..'))
|
|
174
|
+
if (av.slice(rv.length).split(/[\\/]+/).includes('..'))
|
|
176
175
|
return false;
|
|
177
176
|
}
|
|
178
177
|
else if (rv !== av) {
|
package/dist/tools.d.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { type Tool } from '@hmharness/kernel';
|
|
|
2
2
|
export declare const readFileTool: Tool;
|
|
3
3
|
export declare const writeFileTool: Tool;
|
|
4
4
|
/** Surgical search-replace tool (adopted from Codex's apply_patch philosophy).
|
|
5
|
-
* Safer than write_file for targeted edits: only changes the declared fragment
|
|
6
|
-
* refuses to run when old_string is not unique
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Safer than write_file for targeted edits: only changes the declared fragment
|
|
6
|
+
* and refuses to run when old_string is not unique. Approval follows the
|
|
7
|
+
* Codex workspace-write tier model: inside the working directory an edit is
|
|
8
|
+
* the agent's normal job (no card per edit); anything that resolves into
|
|
9
|
+
* HMH_HOME is ALWAYS gated - config.json/workspaces.json/approved-rules.json
|
|
10
|
+
* live there, and an ungated edit to them could rewrite approval policy
|
|
11
|
+
* itself (e.g. approval:'auto', trusted MCP) and disarm every other gate. */
|
|
9
12
|
export declare const editFileTool: Tool;
|
|
10
13
|
export declare const listDirTool: Tool;
|
|
11
14
|
/** Host-shell pipe preflight: on Windows cmd, Unix-isms fail with cryptic
|
package/dist/tools.js
CHANGED
|
@@ -8,7 +8,7 @@ import { exec } from 'node:child_process';
|
|
|
8
8
|
import { copyFile, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
9
9
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
10
10
|
import { promisify } from 'node:util';
|
|
11
|
-
import { chatVision, homeDir, isVisionRefusal, loadConfig, visionProviderChain } from '@hmharness/kernel';
|
|
11
|
+
import { chatVision, homeDir, isBareProbe, isVisionRefusal, loadConfig, visionProviderChain } from '@hmharness/kernel';
|
|
12
12
|
const execCb = promisify(exec);
|
|
13
13
|
const DENY_PATTERNS = [
|
|
14
14
|
// recursive deletes aimed at ROOT/HOME/SYSTEM targets only - relative
|
|
@@ -74,10 +74,13 @@ export const writeFileTool = {
|
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
/** Surgical search-replace tool (adopted from Codex's apply_patch philosophy).
|
|
77
|
-
* Safer than write_file for targeted edits: only changes the declared fragment
|
|
78
|
-
* refuses to run when old_string is not unique
|
|
79
|
-
*
|
|
80
|
-
*
|
|
77
|
+
* Safer than write_file for targeted edits: only changes the declared fragment
|
|
78
|
+
* and refuses to run when old_string is not unique. Approval follows the
|
|
79
|
+
* Codex workspace-write tier model: inside the working directory an edit is
|
|
80
|
+
* the agent's normal job (no card per edit); anything that resolves into
|
|
81
|
+
* HMH_HOME is ALWAYS gated - config.json/workspaces.json/approved-rules.json
|
|
82
|
+
* live there, and an ungated edit to them could rewrite approval policy
|
|
83
|
+
* itself (e.g. approval:'auto', trusted MCP) and disarm every other gate. */
|
|
81
84
|
export const editFileTool = {
|
|
82
85
|
name: 'edit_file',
|
|
83
86
|
description: 'Surgical search-replace edit. Provide old_string (must appear exactly once in the file) and new_string to replace it. Prefer this over write_file for changes to existing files. old_string must be unique - if it appears more than once, the edit is refused (make it more specific).',
|
|
@@ -85,11 +88,25 @@ export const editFileTool = {
|
|
|
85
88
|
type: 'object',
|
|
86
89
|
properties: {
|
|
87
90
|
path: { type: 'string', description: 'file path' },
|
|
88
|
-
old_string: { type: 'string', description: 'the exact string to find and replace (must appear exactly once
|
|
91
|
+
old_string: { type: 'string', description: 'the exact string to find and replace (must appear exactly once)' },
|
|
89
92
|
new_string: { type: 'string', description: 'the replacement string' },
|
|
90
93
|
},
|
|
91
94
|
required: ['path', 'old_string', 'new_string'],
|
|
92
95
|
},
|
|
96
|
+
needsApproval(args, ctx) {
|
|
97
|
+
if (!ctx)
|
|
98
|
+
return true; // no context to bound the blast radius - gate it
|
|
99
|
+
const p = safePath(String(args.path ?? ''), ctx.cwd).replace(/\\/g, '/').toLowerCase();
|
|
100
|
+
const under = (base) => {
|
|
101
|
+
const b = base.replace(/\\/g, '/').toLowerCase();
|
|
102
|
+
return p === b || p.startsWith(b + '/');
|
|
103
|
+
};
|
|
104
|
+
if (under(ctx.home))
|
|
105
|
+
return true; // the agent's own state: always a card
|
|
106
|
+
if (under(ctx.cwd))
|
|
107
|
+
return false; // workspace-write: silent within cwd
|
|
108
|
+
return true; // outside the workspace: a card
|
|
109
|
+
},
|
|
93
110
|
async execute(args, ctx) {
|
|
94
111
|
try {
|
|
95
112
|
const p = safePath(String(args.path), ctx.cwd);
|
|
@@ -523,11 +540,12 @@ export const sshRunTool = {
|
|
|
523
540
|
required: ['host', 'command'],
|
|
524
541
|
},
|
|
525
542
|
needsApproval(args) {
|
|
526
|
-
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
543
|
+
// shared fast path (kernel shellgate): a BARE read-only probe runs without
|
|
544
|
+
// the card; everything else - substitution, redirects, find -exec,
|
|
545
|
+
// date -s, unknown verbs - asks. The old local regex ended in an empty
|
|
546
|
+
// alternation (`|>||`) which matched the empty string, so EVERY command
|
|
547
|
+
// tripped the card and the documented probe fast path was dead code.
|
|
548
|
+
return !isBareProbe(String(args.command ?? ''));
|
|
531
549
|
},
|
|
532
550
|
async execute(args) {
|
|
533
551
|
const cfg = await loadConfig();
|
|
@@ -641,4 +659,7 @@ export const seeImageTool = {
|
|
|
641
659
|
};
|
|
642
660
|
},
|
|
643
661
|
};
|
|
644
|
-
export const baseTools = [
|
|
662
|
+
export const baseTools = [
|
|
663
|
+
readFileTool, editFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool,
|
|
664
|
+
webSearchTool, webFetchTool, browserOpenTool, desktopScreenshotTool, desktopClickTool, desktopTypeTool, sshRunTool,
|
|
665
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "hmharness agent execution layer: base tools, system prompt, sub-agent spawn, and the shared task runner that frontends (cli, web) drive.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"build": "tsc -p tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@hmharness/kernel": "0.6.
|
|
19
|
-
"@hmharness/evolution": "0.6.
|
|
20
|
-
"@hmharness/domain-harmony": "0.6.
|
|
21
|
-
"@hmharness/domain-ops": "0.6.
|
|
18
|
+
"@hmharness/kernel": "0.6.7",
|
|
19
|
+
"@hmharness/evolution": "0.6.7",
|
|
20
|
+
"@hmharness/domain-harmony": "0.6.7",
|
|
21
|
+
"@hmharness/domain-ops": "0.6.7"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist"
|