@nanhara/hara 0.125.0 → 0.125.1
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 +10 -0
- package/dist/index.js +5 -3
- package/dist/security/subprocess-env.js +30 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.125.1 — 2026-07-18 — installed plugin commands inside agent tasks
|
|
9
|
+
|
|
10
|
+
- **Commands contributed by an installed Hara plugin are now available to Hara's own tool subprocesses.**
|
|
11
|
+
Hara appends the user-approved `~/.hara/bin` directory after the inherited `PATH`, so a plugin command
|
|
12
|
+
such as `hara-video` can be called during an agent task without shadowing a project or system executable.
|
|
13
|
+
Interactive shells remain user-controlled and still receive the existing one-time PATH hint.
|
|
14
|
+
- This closes the failure mode where the Video Skill was loaded but the agent saw `hara-video: command not
|
|
15
|
+
found`, hand-built an ungoverned replacement project, and then bypassed the plugin's verification command.
|
|
16
|
+
- Upgrade with `npm i -g @nanhara/hara@0.125.1`.
|
|
17
|
+
|
|
8
18
|
## 0.125.0 — 2026-07-18 — Claude-compatible specialist orchestration
|
|
9
19
|
|
|
10
20
|
- **Personal Claude Code subagents now work in Hara without copying prompts.** Hara discovers
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { readFileSync, existsSync, realpathSync, statSync, writeFileSync, rmSync
|
|
|
13
13
|
import { homedir, tmpdir } from "node:os";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
16
|
+
import { delimiter, dirname, join, relative, resolve } from "node:path";
|
|
17
17
|
import { loadConfig, configPath, readRawConfig, writeConfigValue, setModelVisionOverride, providerEnvKey, providerDefaultBaseURL, CONFIG_KEYS, APPROVAL_MODES, SANDBOX_MODES, REASONING_EFFORTS, } from "./config.js";
|
|
18
18
|
import { runAgent } from "./agent/loop.js";
|
|
19
19
|
import { formatAgentDuration, parseAgentRunTimeoutMs, MIN_AGENT_RUN_TIMEOUT_MS, MAX_AGENT_RUN_TIMEOUT_MS, MAX_AGENT_MAX_ROUNDS, } from "./agent/limits.js";
|
|
@@ -2655,11 +2655,13 @@ pluginCmd
|
|
|
2655
2655
|
m.mcpServers ? `${Object.keys(m.mcpServers).length} mcp server(s)` : "",
|
|
2656
2656
|
].filter(Boolean);
|
|
2657
2657
|
out(c.green(`Installed ${p.name}@${p.version}${parts.length ? c.dim(" — " + parts.join(", ")) : ""}\n`));
|
|
2658
|
-
//
|
|
2658
|
+
// Hara's model-controlled subprocesses append this directory automatically. The user's independent
|
|
2659
|
+
// interactive shell still needs its own PATH entry when they want to invoke the command directly.
|
|
2659
2660
|
const bins = Object.keys(m.bin ?? {});
|
|
2660
2661
|
if (bins.length) {
|
|
2661
|
-
const onPath = (process.env.PATH ?? "").split(
|
|
2662
|
+
const onPath = (process.env.PATH ?? "").split(delimiter).includes(haraBinDir());
|
|
2662
2663
|
out(c.green(`Linked command(s): ${bins.join(", ")} → ${c.dim(haraBinDir())}\n`));
|
|
2664
|
+
out(c.dim(" available to tool commands started inside Hara automatically\n"));
|
|
2663
2665
|
if (!onPath)
|
|
2664
2666
|
out(c.yellow(` add to PATH once: echo 'export PATH="$HOME/.hara/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc\n`));
|
|
2665
2667
|
}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// A user can explicitly pass selected names with HARA_SUBPROCESS_ENV_ALLOW=NAME,OTHER before launching Hara.
|
|
4
4
|
import { redactSensitiveText } from "./secrets.js";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import { platform } from "node:os";
|
|
8
|
+
import { delimiter, join } from "node:path";
|
|
9
|
+
import { normalizePortableWindowsHome } from "../runtime.js";
|
|
7
10
|
const SECRET_NAME = /(?:^|_)(?:API_?KEY|KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIALS?|COOKIE|JWT|AUTH)(?:_|$)/i;
|
|
8
11
|
const SECRET_EXACT = new Set([
|
|
9
12
|
"DATABASE_URL",
|
|
@@ -27,6 +30,32 @@ function explicitAllow(env) {
|
|
|
27
30
|
.filter((name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
28
31
|
.map((name) => name.toUpperCase()));
|
|
29
32
|
}
|
|
33
|
+
function environmentValue(env, name) {
|
|
34
|
+
const key = Object.keys(env).find((candidate) => candidate.toUpperCase() === name);
|
|
35
|
+
return key ? env[key] : undefined;
|
|
36
|
+
}
|
|
37
|
+
/** Installed plugin commands are user-approved Hara extensions. Make them reachable by model-controlled
|
|
38
|
+
* subprocesses without allowing them to shadow an existing system/project command: ~/.hara/bin is appended,
|
|
39
|
+
* never prepended. The interactive shell still owns its own PATH configuration. */
|
|
40
|
+
function appendHaraPluginBin(env) {
|
|
41
|
+
const explicitHome = environmentValue(env, "HOME");
|
|
42
|
+
const fallbackHome = environmentValue(env, "USERPROFILE");
|
|
43
|
+
// Match Hara's portable-home contract: an explicit Git Bash/MSYS HOME wins on Windows too.
|
|
44
|
+
const home = platform() === "win32" && explicitHome
|
|
45
|
+
? normalizePortableWindowsHome(explicitHome)
|
|
46
|
+
: explicitHome ?? fallbackHome;
|
|
47
|
+
if (!home)
|
|
48
|
+
return;
|
|
49
|
+
const pluginBin = join(home, ".hara", "bin");
|
|
50
|
+
if (!existsSync(pluginBin))
|
|
51
|
+
return;
|
|
52
|
+
const pathKey = Object.keys(env).find((candidate) => candidate.toUpperCase() === "PATH") ?? "PATH";
|
|
53
|
+
const current = env[pathKey] ?? "";
|
|
54
|
+
const comparable = (value) => platform() === "win32" ? value.toLowerCase() : value;
|
|
55
|
+
if (current.split(delimiter).some((entry) => comparable(entry) === comparable(pluginBin)))
|
|
56
|
+
return;
|
|
57
|
+
env[pathKey] = current ? `${current}${delimiter}${pluginBin}` : pluginBin;
|
|
58
|
+
}
|
|
30
59
|
/** Build an own copy so callers never mutate process.env. Explicit overrides (for example an MCP server's
|
|
31
60
|
* configured env) are intentional and win after the inherited environment has been scrubbed. */
|
|
32
61
|
export function toolSubprocessEnv(source = process.env, overrides = {}) {
|
|
@@ -48,6 +77,7 @@ export function toolSubprocessEnv(source = process.env, overrides = {}) {
|
|
|
48
77
|
else
|
|
49
78
|
out[name] = value;
|
|
50
79
|
}
|
|
80
|
+
appendHaraPluginBin(out);
|
|
51
81
|
return out;
|
|
52
82
|
}
|
|
53
83
|
/** Redact both recognizable credential syntax and the exact values of secret-named environment variables.
|