@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 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 BUT the
66
- * extension is checked for path traversal (`..` at the boundary blocks). */
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 BUT the
161
- * extension is checked for path traversal (`..` at the boundary blocks). */
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
- // path traversal: the extension after the approved prefix must not
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 (prevents silent wrong-location
7
- * edits), and does NOT need approval - the blast radius is bounded to the
8
- * declared substring. The model must read the file first to know what to replace. */
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 (prevents silent wrong-location
79
- * edits), and does NOT need approval - the blast radius is bounded to the
80
- * declared substring. The model must read the file first to know what to replace. */
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 in the file)' },
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
- const cmd = String(args.command ?? '');
527
- // read-only probe allowlist - no destructive verbs, no pipes into writes
528
- const probe = /^(ls|cat|head|tail|df|du|free|uptime|whoami|hostname|uname|systemctl (status|list-units)|ps|grep|find|wc|date|echo|id|ip |ifconfig|nmap --version)/.test(cmd);
529
- const writes = /rm|mv|dd|mkfs|reboot|shutdown|kill|pkill|systemctl (start|stop|restart|enable|disable)|apt|yum|docker (rm|rmi|prune)|truncate|>||/i;
530
- return !(probe && !writes);
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 = [readFileTool, editFileTool, writeFileTool, listDirTool, runCommandTool, rememberTool, seeImageTool];
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.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.6",
19
- "@hmharness/evolution": "0.6.6",
20
- "@hmharness/domain-harmony": "0.6.6",
21
- "@hmharness/domain-ops": "0.6.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"