@nanhara/hara 0.124.1 → 0.124.3

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 CHANGED
@@ -5,6 +5,50 @@ All notable changes to `@nanhara/hara`.
5
5
  > Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
6
6
  > **patch** (last) number bumps for **optimizations/fixes of existing features**.
7
7
 
8
+ ## 0.124.3 — 2026-07-17 — project-aware resume and runtime hardening
9
+
10
+ - **Saved sessions now resume in the project they belong to.** `hara resume <id>` validates the persisted
11
+ project root and relaunches there even when invoked from another directory; session lists show each project
12
+ path. Inside Hara, `/resume <id>` switches saved sessions while `/continue` exclusively resumes the active
13
+ task. Missing project roots, corrupt records, concurrent writers, and raw/headless cross-project resumes
14
+ remain fail-closed.
15
+ - **A total run deadline is now a resumable safety pause.** The 80% warning tells the agent to finish or
16
+ checkpoint its current step, the hard-stop notice explains that the task/checklist remains resumable with
17
+ `/continue`, and deadline outcomes persist as `paused` instead of looking irrecoverably blocked. Loop and
18
+ repeated-failure breakers remain blocked until the approach changes.
19
+ - **Home protection now covers recursive ancestor scopes.** Launching from `/`, `/Users`, a symlink alias, or
20
+ any directory that contains Home cannot implicitly enumerate or mutate private user state. Explicit child
21
+ projects remain usable, and interactive `/cd <project>` safely relaunches Hara in the selected project while
22
+ preserving explicit profile/model/approval/sandbox choices.
23
+ - **Windows portable Home is consistent for direct module consumers.** Explicit Git Bash/MSYS `HOME` wins over
24
+ a different `USERPROFILE` even when callers bypass the normal CLI bootstrap, keeping config, sessions,
25
+ indexes, permissions, cron state, and project boundaries on one private root.
26
+ - **Session input is bounded before parsing and recursive processing.** Resume/list reject oversized,
27
+ excessively deep, structure-heavy, symlinked, or hard-linked session files through a verified no-follow
28
+ snapshot; saves fail atomically with compact/new-session guidance before unsafe allocations.
29
+ - Upgrade with `npm i -g @nanhara/hara@0.124.3`.
30
+
31
+ ## 0.124.2 — 2026-07-17 — reliable WeCom gateway transport
32
+
33
+ - **The WeCom gateway is ready only after WeCom accepts its credentials.** Hara now waits for the
34
+ `aibot_subscribe` acknowledgement, stops after five bounded credential failures, monitors heartbeat
35
+ acknowledgements, reconnects half-open sockets with bounded exponential backoff, and stops cleanly when
36
+ `disconnected_event` reports that another active connection replaced it.
37
+ - **A failed WeCom delivery is no longer reported as success.** Correlated requests reject on timeout,
38
+ cancellation, connection loss, malformed/nonzero acknowledgements, or missing upload results. Text and file
39
+ transfers have hard deadlines and credential-scoped per-chat FIFO ordering; chunk uploads retry within a
40
+ fixed bound, and transport diagnostics redact the configured Secret.
41
+ - **WeCom callbacks now participate in Hara's cross-restart execution boundary.** Stable `msgid` and
42
+ `create_time` metadata feed stale-event filtering, deduplication, and cached no-rerun outcomes so a redelivery
43
+ cannot silently launch the same coding/file task again.
44
+ - **Inbound media fails closed and keeps file types separate.** Images remain vision attachments, while files
45
+ and videos become explicit private local-path references. Invalid AES ciphertext/padding is rejected instead
46
+ of returning padded bytes, and unfinished handoffs remove adapter-owned temporary files.
47
+ - A deterministic local WebSocket suite covers auth failure, heartbeat reconnection, request errors, strict
48
+ decryption, media classification, two-chunk upload, and a spawned `hara gateway --platform wecom` allowlist
49
+ round trip without using real enterprise credentials.
50
+ - Upgrade with `npm i -g @nanhara/hara@0.124.2`.
51
+
8
52
  ## 0.124.1 — 2026-07-16 — Windows native private-state and file-identity portability
9
53
 
10
54
  - **Windows private state works in both Node and the native Bun executable.** Exclusive staging creation now
@@ -168,7 +168,9 @@ function warnRun(opts, life) {
168
168
  if (life.disposed || life.warned || life.signal.aborted)
169
169
  return;
170
170
  life.warned = true;
171
- showRunNotice(opts, `⚠ agent still running: ${formatAgentDuration(Date.now() - life.startedAt)} elapsed, round ${life.rounds}/${life.maxRounds}; hard stop at ${formatAgentDuration(life.timeoutMs)}.`);
171
+ const elapsedMs = Date.now() - life.startedAt;
172
+ const remainingMs = Math.max(0, life.timeoutMs - elapsedMs);
173
+ showRunNotice(opts, `⚠ agent still running: ${formatAgentDuration(elapsedMs)} elapsed, round ${life.rounds}/${life.maxRounds}; ${formatAgentDuration(remainingMs)} remains before this turn pauses. Finish the current step or leave a checklist checkpoint; unfinished session work can resume with \`/continue\`.`);
172
174
  }
173
175
  function createRunLifecycle(opts) {
174
176
  const timeoutMs = agentRunTimeoutMs(opts.timeoutMs);
@@ -226,7 +228,7 @@ function disposeRunLifecycle(life) {
226
228
  function hardStop(opts, life, kind, detail) {
227
229
  const elapsedMs = Date.now() - life.startedAt;
228
230
  const message = kind === "deadline"
229
- ? `⛔ agent run stopped: total deadline ${formatAgentDuration(life.timeoutMs)} reached after ${life.rounds} round(s). No further model or tool calls will start. Increase it with \`hara config set runTimeoutMs 45m\` (maximum 2h) only for intentional long work.`
231
+ ? `⏸ agent run paused: total deadline ${formatAgentDuration(life.timeoutMs)} reached after ${life.rounds} round(s). No further model or tool calls will start in this turn. Session-backed work keeps its task and checklist checkpoint; type \`/continue\` to resume in a fresh bounded turn. Only for intentionally long single turns, use \`hara config set runTimeoutMs 45m\` (maximum 2h).`
230
232
  : kind === "max_rounds"
231
233
  ? `⛔ agent run stopped: ${life.maxRounds}-round safety limit reached after ${formatAgentDuration(elapsedMs)}. This usually means the model is looping. Increase it with \`hara config set maxAgentRounds <n>\` (maximum 256) only if the extra rounds are intentional.`
232
234
  : `⛔ agent run stopped: the same failing ${detail?.tool ?? "tool"} call repeated ${detail?.count ?? REPEATED_FAILURE_LIMIT} times. Change the approach or fix the reported cause before retrying.`;
@@ -10,7 +10,7 @@ import { dirname, join } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { createHash } from "node:crypto";
12
12
  import { findProjectRoot } from "./context/agents-md.js";
13
- import { isHomeWorkspace } from "./context/workspace-scope.js";
13
+ import { isUnsafeProjectWorkspace } from "./context/workspace-scope.js";
14
14
  import { toolSubprocessEnv } from "./security/subprocess-env.js";
15
15
  import { sensitiveFileReason } from "./security/sensitive-files.js";
16
16
  import { redactSensitiveText } from "./security/secrets.js";
@@ -107,7 +107,7 @@ function dropSensitiveIndexEntries(root, gitDir) {
107
107
  export function checkpoint(cwd, label) {
108
108
  // A shadow `git add -A` at ~/ would traverse the user's entire personal/control-data scope. Home is
109
109
  // deliberately not an implicit project, including when reached through a symlink alias.
110
- if (isHomeWorkspace(cwd))
110
+ if (isUnsafeProjectWorkspace(cwd))
111
111
  return null;
112
112
  const root = findProjectRoot(cwd);
113
113
  const gitDir = shadowGitDir(root);
@@ -138,7 +138,7 @@ export function checkpoint(cwd, label) {
138
138
  }
139
139
  /** Recent checkpoints, newest first. */
140
140
  export function listCheckpoints(cwd, n = 15) {
141
- if (isHomeWorkspace(cwd))
141
+ if (isUnsafeProjectWorkspace(cwd))
142
142
  return [];
143
143
  const root = findProjectRoot(cwd);
144
144
  const gitDir = shadowGitDir(root);
@@ -163,7 +163,7 @@ export function listCheckpoints(cwd, n = 15) {
163
163
  export function restoreCheckpoint(cwd, ref) {
164
164
  if (!/^[0-9a-f]{4,64}$/i.test(ref))
165
165
  return null; // checkpoint refs are hashes; reject option/ref injection
166
- if (isHomeWorkspace(cwd))
166
+ if (isUnsafeProjectWorkspace(cwd))
167
167
  return null;
168
168
  const root = findProjectRoot(cwd);
169
169
  const gitDir = shadowGitDir(root);
@@ -1,6 +1,6 @@
1
- import { realpathSync } from "node:fs";
2
- import { homedir } from "node:os";
1
+ import { realpathSync, statSync } from "node:fs";
3
2
  import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { effectiveHomeDir } from "../runtime.js";
4
4
  /** Resolve existing paths through symlinks before comparing security/workspace scopes. Falling back to
5
5
  * resolve() keeps messages deterministic for a path that disappears during startup; normal cwd/home paths
6
6
  * exist and therefore take the realpath branch. */
@@ -14,21 +14,25 @@ export function canonicalWorkspacePath(path) {
14
14
  }
15
15
  }
16
16
  /** The user's home directory is a control/personal-data scope, not an implicit project workspace. */
17
- export function isHomeWorkspace(cwd, home = homedir()) {
17
+ export function isHomeWorkspace(cwd, home = effectiveHomeDir()) {
18
18
  return canonicalWorkspacePath(cwd) === canonicalWorkspacePath(home);
19
19
  }
20
20
  /** A recursive root is unsafe when it is Home itself OR an ancestor that would descend into Home. This
21
21
  * closes `path: ".."`, filesystem-root, and symlink-alias bypasses while keeping an explicitly selected
22
22
  * project child under Home usable. */
23
- export function recursiveRootContainsHome(root, home = homedir()) {
23
+ export function recursiveRootContainsHome(root, home = effectiveHomeDir()) {
24
24
  const canonicalRoot = canonicalWorkspacePath(root);
25
25
  const canonicalHome = canonicalWorkspacePath(home);
26
26
  const rel = relative(canonicalRoot, canonicalHome);
27
27
  return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
28
28
  }
29
+ /** Project-scoped execution from Home OR one of its ancestors can implicitly reach private user state. */
30
+ export function isUnsafeProjectWorkspace(cwd, home = effectiveHomeDir()) {
31
+ return recursiveRootContainsHome(cwd, home);
32
+ }
29
33
  export function homeWorkspaceActionError(action) {
30
- return (`Refusing to ${action} from the home directory: it is not an implicit project workspace. ` +
31
- "Run `cd /path/to/project` first, or launch with `hara --cwd /path/to/project`.");
34
+ return (`Refusing to ${action} from a workspace that is the home directory or contains it: it is not an implicit project workspace. ` +
35
+ "Use `/cd /path/to/project`, run `cd /path/to/project` first, or launch with `hara --cwd /path/to/project`.");
32
36
  }
33
37
  export function recursiveHomeSearchError(tool) {
34
38
  return (`Error: ${tool} will not recursively scan the home directory. ` +
@@ -43,11 +47,44 @@ export function homeWorkspaceDirectoryScanError(tool) {
43
47
  /** Injected into model context when Hara was intentionally launched at ~/. Runtime checks enforce the same
44
48
  * policy, but guidance avoids wasting turns on calls that are guaranteed to be rejected. */
45
49
  export function homeWorkspaceGuidance(cwd) {
46
- if (!isHomeWorkspace(cwd))
50
+ if (!isUnsafeProjectWorkspace(cwd))
47
51
  return "";
48
52
  return ("# Home-directory workspace boundary\n" +
49
- "The working directory resolves to the user's home directory, which is not an implicit project. " +
53
+ "The working directory resolves to the user's home directory or an ancestor that contains it, which is not an implicit project. " +
50
54
  "Do not initialize a project, create or modify files, build a repository index, run shell/external " +
51
55
  "executable tools, enumerate directories, or grep/glob/search Home or one of its child directories. " +
52
- "Ask the user to run `cd /path/to/project` or `hara --cwd /path/to/project` for project work. Only explicitly named single-file reads remain available.");
56
+ "Ask the user to switch with `/cd /path/to/project`, run `cd /path/to/project`, or launch with " +
57
+ "`hara --cwd /path/to/project` for project work. Only explicitly named single-file reads remain available.");
58
+ }
59
+ /** Resolve an explicit interactive workspace handoff without accepting Home/ancestor scopes. */
60
+ export function resolveWorkspaceSwitch(input, currentCwd, home = effectiveHomeDir()) {
61
+ let requested = input.trim();
62
+ if (!requested)
63
+ return { ok: false, error: "usage: /cd <project-directory>" };
64
+ if (requested.length >= 2
65
+ && ((requested.startsWith('"') && requested.endsWith('"')) || (requested.startsWith("'") && requested.endsWith("'"))))
66
+ requested = requested.slice(1, -1).trim();
67
+ if (!requested)
68
+ return { ok: false, error: "usage: /cd <project-directory>" };
69
+ if (requested === "~")
70
+ requested = home;
71
+ else if (/^~[\\/]/u.test(requested)) {
72
+ const parts = requested.slice(2).split(/[\\/]+/u).filter(Boolean);
73
+ requested = resolve(home, ...parts);
74
+ }
75
+ try {
76
+ const target = realpathSync.native(resolve(currentCwd, requested));
77
+ if (!statSync(target).isDirectory())
78
+ return { ok: false, error: `not a directory: ${requested}` };
79
+ if (isUnsafeProjectWorkspace(target, home)) {
80
+ return { ok: false, error: homeWorkspaceActionError("switch project scope") };
81
+ }
82
+ return { ok: true, cwd: target };
83
+ }
84
+ catch (error) {
85
+ return {
86
+ ok: false,
87
+ error: `cannot switch to '${requested}': ${error instanceof Error ? error.message : String(error)}`,
88
+ };
89
+ }
53
90
  }