@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 +44 -0
- package/dist/agent/loop.js +4 -2
- package/dist/checkpoints.js +4 -4
- package/dist/context/workspace-scope.js +46 -9
- package/dist/gateway/wecom.js +512 -223
- package/dist/index.js +151 -30
- package/dist/runtime.js +13 -0
- package/dist/sandbox.js +2 -2
- package/dist/search/semindex.js +5 -5
- package/dist/security/permissions.js +2 -2
- package/dist/session/resume.js +52 -0
- package/dist/session/store.js +51 -4
- package/dist/session/task.js +6 -4
- package/dist/tools/cron.js +2 -2
- package/dist/tools/registry.js +2 -2
- package/package.json +1 -1
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
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
?
|
|
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.`;
|
package/dist/checkpoints.js
CHANGED
|
@@ -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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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
|
-
"
|
|
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 (!
|
|
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
|
|
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
|
}
|