@hmharness/kernel 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/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './types.ts';
2
+ export * from './shellgate.ts';
2
3
  export * from './registry.ts';
3
4
  export * from './provider.ts';
4
5
  export * from './window.ts';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./types.js";
2
+ export * from "./shellgate.js";
2
3
  export * from "./registry.js";
3
4
  export * from "./provider.js";
4
5
  export * from "./window.js";
package/dist/loop.js CHANGED
@@ -72,6 +72,18 @@ export async function runLoop(opts) {
72
72
  });
73
73
  usage.promptTokens += chatRes.usage?.prompt_tokens ?? 0;
74
74
  usage.completionTokens += chatRes.usage?.completion_tokens ?? 0;
75
+ // Usage fallback: many OpenAI-compatible gateways never report usage
76
+ // (even with stream_options.include_usage), which used to leave the token
77
+ // valve counting zero - a runaway loop had NO stop. Estimate from the
78
+ // wire text instead (chars/4 heuristic): coarse, but the valve only needs
79
+ // an order of magnitude to trip at 50M.
80
+ if (!chatRes.usage || (!chatRes.usage.prompt_tokens && !chatRes.usage.completion_tokens)) {
81
+ const promptChars = compacted.reduce((n, m) => n + (m.content?.length ?? 0), 0);
82
+ const replyChars = (chatRes.message.content?.length ?? 0)
83
+ + (chatRes.message.tool_calls ?? []).reduce((n, c) => n + c.function.arguments.length, 0);
84
+ usage.promptTokens += Math.ceil(promptChars / 4);
85
+ usage.completionTokens += Math.ceil(replyChars / 4);
86
+ }
75
87
  const { message } = chatRes;
76
88
  events?.onAssistant?.(message);
77
89
  const calls = message.tool_calls ?? [];
@@ -105,7 +117,7 @@ export async function runLoop(opts) {
105
117
  p.isError = true;
106
118
  p.skip = true;
107
119
  }
108
- else if (tool.needsApproval?.(args)) {
120
+ else if (tool.needsApproval?.(args, ctx)) {
109
121
  // Safe default: with no gate wired in, risky tools are denied.
110
122
  const granted = opts.approval ? await opts.approval.ask(name, args) : false;
111
123
  events?.onApproval?.(name, args, granted);
@@ -15,6 +15,13 @@ export interface ChatResponse {
15
15
  };
16
16
  }
17
17
  export type DeltaKind = 'text' | 'reasoning';
18
+ /** Parse a Retry-After header into a delay in ms (MDN: exactly two legal
19
+ * forms - delta-seconds or HTTP-date), clamped to [0, 120s]. Returns 0 when
20
+ * absent/illegal (caller falls back to exponential backoff). Deliberately
21
+ * does NOT read x-ratelimit-reset: that header is an epoch timestamp in the
22
+ * wild, which naive Number() parsing turned into a ~1.7e12 ms setTimeout
23
+ * that overflowed and fired immediately - a retry storm. */
24
+ export declare function parseRetryAfterMs(raw: string | null | undefined, maxMs?: number): number;
18
25
  export interface ChatOptions {
19
26
  timeoutMs?: number;
20
27
  /** Streaming callback; presence switches the request to stream:true. */
package/dist/provider.js CHANGED
@@ -1,3 +1,20 @@
1
+ /** Parse a Retry-After header into a delay in ms (MDN: exactly two legal
2
+ * forms - delta-seconds or HTTP-date), clamped to [0, 120s]. Returns 0 when
3
+ * absent/illegal (caller falls back to exponential backoff). Deliberately
4
+ * does NOT read x-ratelimit-reset: that header is an epoch timestamp in the
5
+ * wild, which naive Number() parsing turned into a ~1.7e12 ms setTimeout
6
+ * that overflowed and fired immediately - a retry storm. */
7
+ export function parseRetryAfterMs(raw, maxMs = 120_000) {
8
+ if (!raw)
9
+ return 0;
10
+ const v = raw.trim();
11
+ if (/^\d{1,10}$/.test(v))
12
+ return Math.min(maxMs, Number(v) * 1000);
13
+ const at = Date.parse(v);
14
+ if (!Number.isNaN(at))
15
+ return Math.min(maxMs, Math.max(0, at - Date.now()));
16
+ return 0;
17
+ }
1
18
  export async function chat(cfg, messages, tools, opts = {}) {
2
19
  const streaming = typeof opts.onDelta === 'function';
3
20
  const body = { model: cfg.model, messages };
@@ -31,9 +48,10 @@ export async function chat(cfg, messages, tools, opts = {}) {
31
48
  signal: ctrl.signal,
32
49
  });
33
50
  if (res.status === 429) {
34
- // honour server-provided retry delay; fall back to exponential backoff
35
- const retryAfter = Number(res.headers.get('retry-after')) || Number(res.headers.get('x-ratelimit-reset')) || 0;
36
- const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(30_000, 2000 * Math.pow(2, attempt));
51
+ // Retry-After per RFC/MDN has two legal forms: delta-seconds ("120")
52
+ // or HTTP-date ("Wed, 21 Oct 2015 07:28:00 GMT"). See
53
+ // parseRetryAfterMs for why x-ratelimit-reset is deliberately ignored.
54
+ const delay = parseRetryAfterMs(res.headers.get('retry-after')) || Math.min(30_000, 2000 * Math.pow(2, attempt));
37
55
  lastError = `HTTP 429 (rate limited): ${(await res.text()).slice(0, 200)}`;
38
56
  await sleep(delay + Math.random() * 1000); // jitter
39
57
  continue;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @hmharness/kernel - shell fast-path gate
3
+ *
4
+ * One shared implementation for every "run this without an approval card"
5
+ * decision over remotely/exec-ed shell strings (the agent's ssh_run tool and
6
+ * the web /api/ssh proxy). String allowlists are bypassable in general -
7
+ * documented bypass classes include command substitution `$()`/backticks
8
+ * (cli-mcp-server 0.2.5, CVE-2026-28470), `&&`-blind-spots (Claude Code),
9
+ * and argument-level execution such as `find -exec`/`-delete`
10
+ * (GHSA-cv3g-hj65-pcfh). The design here closes all of them at once:
11
+ *
12
+ * fast path = ONE bare read-only verb + plain arguments + ZERO shell
13
+ * metacharacters anywhere (no $ ` ( ) { } < > | ; & \ quotes/newlines)
14
+ * and no verb whose own ARGUMENTS can execute or mutate (find, echo,
15
+ * xargs, awk, date -s are excluded outright).
16
+ *
17
+ * Everything else returns false - i.e. needs an approval card. Fail closed:
18
+ * an unknown or oddly-shaped command is never a fast path.
19
+ */
20
+ /**
21
+ * True only for a bare read-only probe (e.g. `df -h`, `ps aux`, `cat /etc/hosts`).
22
+ * Callers: `if (!isBareProbe(cmd)) → approval required`.
23
+ */
24
+ export declare function isBareProbe(cmd: string): boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @hmharness/kernel - shell fast-path gate
3
+ *
4
+ * One shared implementation for every "run this without an approval card"
5
+ * decision over remotely/exec-ed shell strings (the agent's ssh_run tool and
6
+ * the web /api/ssh proxy). String allowlists are bypassable in general -
7
+ * documented bypass classes include command substitution `$()`/backticks
8
+ * (cli-mcp-server 0.2.5, CVE-2026-28470), `&&`-blind-spots (Claude Code),
9
+ * and argument-level execution such as `find -exec`/`-delete`
10
+ * (GHSA-cv3g-hj65-pcfh). The design here closes all of them at once:
11
+ *
12
+ * fast path = ONE bare read-only verb + plain arguments + ZERO shell
13
+ * metacharacters anywhere (no $ ` ( ) { } < > | ; & \ quotes/newlines)
14
+ * and no verb whose own ARGUMENTS can execute or mutate (find, echo,
15
+ * xargs, awk, date -s are excluded outright).
16
+ *
17
+ * Everything else returns false - i.e. needs an approval card. Fail closed:
18
+ * an unknown or oddly-shaped command is never a fast path.
19
+ */
20
+ /** Metacharacters that enable substitution, chaining, or redirection. */
21
+ const SHELL_METACHARS = /[$`(){}<>|;&'"\\\n\r]/;
22
+ /** Verbs safe to run with plain arguments only (read-only, no exec-in-args). */
23
+ const BARE_PROBE_VERBS = /^(ls|cat|head|tail|df|du|free|uptime|whoami|hostname|uname|ps|grep|wc|id|pwd|date)(\s+[A-Za-z0-9_.:@/=,-]+)*\s*$/;
24
+ /**
25
+ * True only for a bare read-only probe (e.g. `df -h`, `ps aux`, `cat /etc/hosts`).
26
+ * Callers: `if (!isBareProbe(cmd)) → approval required`.
27
+ */
28
+ export function isBareProbe(cmd) {
29
+ if (!cmd || SHELL_METACHARS.test(cmd))
30
+ return false;
31
+ if (/\bdate\b/.test(cmd) && /\s--?s(e[rt])?\b/.test(cmd))
32
+ return false; // clock set
33
+ return BARE_PROBE_VERBS.test(cmd);
34
+ }
package/dist/types.d.ts CHANGED
@@ -33,7 +33,7 @@ export interface Tool {
33
33
  * false means read-only / safe. Remote (MCP) tools default to needing
34
34
  * approval unless their server is marked trusted.
35
35
  */
36
- needsApproval?(args: Record<string, unknown>): boolean;
36
+ needsApproval?(args: Record<string, unknown>, ctx?: ToolContext): boolean;
37
37
  execute(args: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult>;
38
38
  }
39
39
  /** OpenAI-style chat message, reused across provider adapters. */
@@ -106,8 +106,18 @@ export interface HmhConfig {
106
106
  * error self-notes (every task, zero cost), Tier 2 = one model-call
107
107
  * lesson per erroring task (instant reflection), Tier 3 = this - full
108
108
  * cycle with bench gate. Guards unchanged: double-gate, holdout, poison
109
- * screen, writes only under skills/ and memory/. */
109
+ * screen; writes skills/ + memory/ + evolution logs, and repo code only
110
+ * when evolution.autoPatch is explicitly enabled. */
110
111
  autoEvolveEvery?: number;
112
+ /** Evolution-cycle options. autoPatch (default FALSE) opts into code-level
113
+ * self-evolution (the DGM-style sandboxed patch loop). Off by default:
114
+ * self-modifying systems are unsafe by default (the DGM paper's own
115
+ * framing) - with it off, evolution writes only skills/ + memory/ + its
116
+ * own logs, never repo code. */
117
+ evolution?: {
118
+ /** Enable the code-patch (self-modification) step. Default false. */
119
+ autoPatch?: boolean;
120
+ };
111
121
  /** Named vendor endpoints for multi-provider routing. */
112
122
  providers?: Record<string, ProviderConfig>;
113
123
  /** Per-purpose provider names resolved against `providers`. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/kernel",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "hmharness kernel: tool registry, provider adapters, the agent loop, session log, config. Zero runtime dependencies (Node >=22 native fetch).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",