@nanhara/hara 0.121.1 → 0.122.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 +120 -0
- package/README.md +57 -10
- package/SECURITY.md +48 -9
- package/dist/agent/loop.js +169 -31
- package/dist/agent/reminders.js +22 -7
- package/dist/agent/repeat-guard.js +26 -7
- package/dist/agent/structured.js +231 -0
- package/dist/agent/touched.js +24 -6
- package/dist/checkpoints.js +103 -17
- package/dist/cli.js +16 -0
- package/dist/config.js +173 -34
- package/dist/context/agents-md.js +44 -9
- package/dist/context/mentions.js +10 -4
- package/dist/context/subdir-hints.js +40 -7
- package/dist/cron/deliver.js +37 -3
- package/dist/cron/runner.js +372 -37
- package/dist/cron/store.js +11 -3
- package/dist/exec/jobs.js +88 -20
- package/dist/feedback.js +3 -2
- package/dist/fs-read.js +421 -12
- package/dist/fs-walk.js +8 -2
- package/dist/fs-write.js +433 -21
- package/dist/gateway/dingtalk.js +4 -1
- package/dist/gateway/discord.js +53 -20
- package/dist/gateway/feishu.js +157 -58
- package/dist/gateway/flows-pending.js +727 -0
- package/dist/gateway/flows.js +391 -16
- package/dist/gateway/matrix.js +81 -18
- package/dist/gateway/mattermost.js +44 -34
- package/dist/gateway/media.js +659 -0
- package/dist/gateway/outbound-files.js +379 -0
- package/dist/gateway/serve.js +712 -169
- package/dist/gateway/sessions.js +475 -78
- package/dist/gateway/signal.js +31 -28
- package/dist/gateway/slack.js +28 -21
- package/dist/gateway/telegram.js +33 -21
- package/dist/gateway/tmux-routes.js +11 -3
- package/dist/gateway/wecom.js +38 -31
- package/dist/gateway/weixin.js +147 -59
- package/dist/hooks.js +41 -23
- package/dist/index.js +763 -273
- package/dist/mcp/client.js +164 -12
- package/dist/memory/store.js +68 -22
- package/dist/org/planner.js +36 -10
- package/dist/org/projects.js +347 -0
- package/dist/org/review-chain.js +360 -24
- package/dist/org/roles.js +42 -13
- package/dist/profile/profile.js +152 -27
- package/dist/recall.js +4 -2
- package/dist/runtime.js +37 -0
- package/dist/sandbox.js +142 -33
- package/dist/search/semindex.js +182 -53
- package/dist/search/zvec-store.js +121 -42
- package/dist/security/permissions.js +326 -19
- package/dist/security/private-state.js +299 -0
- package/dist/security/project-trust.js +6 -0
- package/dist/security/secrets.js +84 -9
- package/dist/security/sensitive-files.js +723 -0
- package/dist/security/subprocess-env.js +210 -0
- package/dist/serve/server.js +774 -318
- package/dist/serve/sessions.js +113 -33
- package/dist/session/store.js +298 -47
- package/dist/skills/skills.js +16 -7
- package/dist/tools/all.js +1 -0
- package/dist/tools/builtin.js +77 -49
- package/dist/tools/codebase.js +3 -1
- package/dist/tools/computer.js +98 -92
- package/dist/tools/cron.js +6 -0
- package/dist/tools/edit.js +22 -9
- package/dist/tools/external_agent.js +110 -16
- package/dist/tools/memory.js +38 -8
- package/dist/tools/patch.js +253 -34
- package/dist/tools/search.js +543 -73
- package/dist/tools/send.js +11 -5
- package/dist/tools/task.js +453 -0
- package/dist/tools/todo.js +67 -16
- package/dist/tools/web.js +168 -54
- package/dist/undo.js +83 -7
- package/package.json +11 -10
- package/runtime-bootstrap.cjs +72 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Environment boundary for model-controlled subprocesses. Provider credentials stay in the Hara process;
|
|
2
|
+
// Bash/jobs/hooks/MCP/external agents receive ordinary build/runtime variables but not secret-shaped values.
|
|
3
|
+
// A user can explicitly pass selected names with HARA_SUBPROCESS_ENV_ALLOW=NAME,OTHER before launching Hara.
|
|
4
|
+
import { redactSensitiveText } from "./secrets.js";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { platform } from "node:os";
|
|
7
|
+
const SECRET_NAME = /(?:^|_)(?:API_?KEY|KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIALS?|COOKIE|JWT|AUTH)(?:_|$)/i;
|
|
8
|
+
const SECRET_EXACT = new Set([
|
|
9
|
+
"DATABASE_URL",
|
|
10
|
+
"REDIS_URL",
|
|
11
|
+
"MONGODB_URI",
|
|
12
|
+
"MONGO_URL",
|
|
13
|
+
"PGPASSWORD",
|
|
14
|
+
]);
|
|
15
|
+
const INJECTION_NAME = /^(?:BASH_ENV|ENV|NODE_OPTIONS|NODE_PATH|PYTHONPATH|PYTHONHOME|PYTHONSTARTUP|RUBYOPT|RUBYLIB|PERL5OPT|PERL5LIB|JAVA_TOOL_OPTIONS|_JAVA_OPTIONS|LD_PRELOAD|LD_LIBRARY_PATH|DYLD_.*|GIT_ASKPASS|SSH_ASKPASS|SUDO_ASKPASS|GIT_SSH_COMMAND|GIT_CONFIG(?:_.*)?|GIT_EXEC_PATH|GIT_EXTERNAL_DIFF|GIT_EDITOR|GIT_SEQUENCE_EDITOR|GIT_PAGER|PAGER|MANPAGER|EDITOR|VISUAL|LESSOPEN|LESSCLOSE|NPM_CONFIG_SCRIPT_SHELL|NPM_CONFIG_NODE_GYP|NPM_CONFIG_USERCONFIG|PIP_CONFIG_FILE|AWS_SHARED_CREDENTIALS_FILE|AWS_CONFIG_FILE|DOCKER_CONFIG|KUBECONFIG|NETRC|AZURE_CONFIG_DIR)$/;
|
|
16
|
+
const SAFE_EXCEPTIONS = new Set(["SSH_AUTH_SOCK"]); // socket path, not the credential itself
|
|
17
|
+
export function isSecretEnvironmentName(name) {
|
|
18
|
+
const normalized = name.toUpperCase(); // Windows environment names are case-insensitive
|
|
19
|
+
if (SAFE_EXCEPTIONS.has(normalized))
|
|
20
|
+
return false;
|
|
21
|
+
return SECRET_EXACT.has(normalized) || SECRET_NAME.test(normalized) || INJECTION_NAME.test(normalized);
|
|
22
|
+
}
|
|
23
|
+
function explicitAllow(env) {
|
|
24
|
+
return new Set(String(env.HARA_SUBPROCESS_ENV_ALLOW ?? "")
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((name) => name.trim())
|
|
27
|
+
.filter((name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
28
|
+
.map((name) => name.toUpperCase()));
|
|
29
|
+
}
|
|
30
|
+
/** Build an own copy so callers never mutate process.env. Explicit overrides (for example an MCP server's
|
|
31
|
+
* configured env) are intentional and win after the inherited environment has been scrubbed. */
|
|
32
|
+
export function toolSubprocessEnv(source = process.env, overrides = {}) {
|
|
33
|
+
const allow = explicitAllow(source);
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [name, value] of Object.entries(source)) {
|
|
36
|
+
if (value === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
const normalized = name.toUpperCase();
|
|
39
|
+
if (normalized === "HARA_SUBPROCESS_ENV_ALLOW")
|
|
40
|
+
continue;
|
|
41
|
+
if (isSecretEnvironmentName(normalized) && !allow.has(normalized))
|
|
42
|
+
continue;
|
|
43
|
+
out[name] = value;
|
|
44
|
+
}
|
|
45
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
delete out[name];
|
|
48
|
+
else
|
|
49
|
+
out[name] = value;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Redact both recognizable credential syntax and the exact values of secret-named environment variables.
|
|
54
|
+
* Exact-value matching closes the gap for opaque provider tokens that have no standard prefix. */
|
|
55
|
+
export function redactToolSubprocessOutput(text, source = process.env, explicit = {}) {
|
|
56
|
+
let out = redactSensitiveText(text).text;
|
|
57
|
+
const values = new Set();
|
|
58
|
+
for (const env of [source, explicit]) {
|
|
59
|
+
for (const [name, value] of Object.entries(env)) {
|
|
60
|
+
if (!value || value.length < 6 || !isSecretEnvironmentName(name))
|
|
61
|
+
continue;
|
|
62
|
+
values.add(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const value of [...values].sort((a, b) => b.length - a.length)) {
|
|
66
|
+
if (out.includes(value))
|
|
67
|
+
out = out.split(value).join("***");
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Line-buffered streaming redactor. Redacting each transport chunk independently is unsafe because a token
|
|
72
|
+
* may be split between two chunks. Holding the partial line until the next newline/end closes that gap. */
|
|
73
|
+
export function createToolOutputLineRedactor(emit, source = process.env, explicit = {}, maxPendingCharacters = 64 * 1024) {
|
|
74
|
+
if (!Number.isSafeInteger(maxPendingCharacters) || maxPendingCharacters < 1024) {
|
|
75
|
+
throw new Error("streaming redactor maxPendingCharacters must be an integer >= 1024");
|
|
76
|
+
}
|
|
77
|
+
let pending = "";
|
|
78
|
+
let droppingLongLine = false;
|
|
79
|
+
const feed = (piece, endsLine) => {
|
|
80
|
+
if (droppingLongLine) {
|
|
81
|
+
if (endsLine)
|
|
82
|
+
droppingLongLine = false;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (pending.length + piece.length > maxPendingCharacters) {
|
|
86
|
+
// Never release a prefix of an overlong line: a credential could straddle that release boundary.
|
|
87
|
+
// Drop through the newline instead, keeping memory fixed and preserving only a safe diagnostic.
|
|
88
|
+
pending = "";
|
|
89
|
+
droppingLongLine = !endsLine;
|
|
90
|
+
emit(`[output line omitted — exceeded ${maxPendingCharacters} characters]\n`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
pending += piece;
|
|
94
|
+
if (endsLine) {
|
|
95
|
+
emit(redactToolSubprocessOutput(pending, source, explicit));
|
|
96
|
+
pending = "";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
push(chunk) {
|
|
101
|
+
let start = 0;
|
|
102
|
+
let newline;
|
|
103
|
+
while ((newline = chunk.indexOf("\n", start)) >= 0) {
|
|
104
|
+
feed(chunk.slice(start, newline + 1), true);
|
|
105
|
+
start = newline + 1;
|
|
106
|
+
}
|
|
107
|
+
if (start < chunk.length)
|
|
108
|
+
feed(chunk.slice(start), false);
|
|
109
|
+
},
|
|
110
|
+
flush() {
|
|
111
|
+
if (pending)
|
|
112
|
+
emit(redactToolSubprocessOutput(pending, source, explicit));
|
|
113
|
+
pending = "";
|
|
114
|
+
droppingLongLine = false;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Terminate a spawned tool and, when it owns a POSIX process group, all descendants. On Windows,
|
|
119
|
+
* taskkill /T is the supported process-tree primitive. Callers should spawn with `detached: true` on POSIX
|
|
120
|
+
* and pass `processGroup: true`; the explicit flag avoids ever signaling Hara's own process group. */
|
|
121
|
+
export function terminateSubprocessTree(child, options = {}) {
|
|
122
|
+
const force = options.force === true;
|
|
123
|
+
const pid = child.pid;
|
|
124
|
+
if (!pid)
|
|
125
|
+
return () => { };
|
|
126
|
+
const signal = (hard) => {
|
|
127
|
+
const direct = () => {
|
|
128
|
+
try {
|
|
129
|
+
child.kill(hard ? "SIGKILL" : "SIGTERM");
|
|
130
|
+
}
|
|
131
|
+
catch { /* already gone */ }
|
|
132
|
+
};
|
|
133
|
+
if (platform() === "win32") {
|
|
134
|
+
try {
|
|
135
|
+
// Asynchronous taskkill preserves the caller's wall-clock fallback; a synchronous 5s taskkill
|
|
136
|
+
// timeout here could itself make a 200ms tool timeout take ten seconds across TERM + force passes.
|
|
137
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/t", ...(hard ? ["/f"] : [])], {
|
|
138
|
+
stdio: "ignore",
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
env: toolSubprocessEnv(),
|
|
141
|
+
});
|
|
142
|
+
killer.once("error", direct);
|
|
143
|
+
killer.once("close", (code) => { if (code !== 0)
|
|
144
|
+
direct(); });
|
|
145
|
+
killer.unref();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Fall back to Node's direct-child signal below.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (options.processGroup) {
|
|
153
|
+
try {
|
|
154
|
+
process.kill(-pid, hard ? "SIGKILL" : "SIGTERM");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// The group may have exited between the check and signal; direct-child kill is a safe fallback.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
direct();
|
|
162
|
+
};
|
|
163
|
+
signal(force);
|
|
164
|
+
if (force)
|
|
165
|
+
options.onForce?.();
|
|
166
|
+
const graceMs = options.graceMs;
|
|
167
|
+
if (force || graceMs === undefined)
|
|
168
|
+
return () => { };
|
|
169
|
+
const grace = Math.max(0, Math.min(graceMs, 30_000));
|
|
170
|
+
const fallback = Math.max(0, Math.min(options.fallbackMs ?? 1_000, 30_000));
|
|
171
|
+
let fallbackActive = true;
|
|
172
|
+
let forceTimer;
|
|
173
|
+
let fallbackTimer;
|
|
174
|
+
const cancelFallback = () => {
|
|
175
|
+
if (!fallbackActive)
|
|
176
|
+
return;
|
|
177
|
+
fallbackActive = false;
|
|
178
|
+
if (fallbackTimer)
|
|
179
|
+
clearTimeout(fallbackTimer);
|
|
180
|
+
child.off("close", cancelFallback);
|
|
181
|
+
child.off("error", cancelFallback);
|
|
182
|
+
};
|
|
183
|
+
// `close` only proves the direct child's stdio is closed. A grandchild can redirect stdio, ignore TERM,
|
|
184
|
+
// and remain in the owned process group, so the forced group kill MUST NOT be cancelled by child close.
|
|
185
|
+
// We cancel only the API hard-settle fallback; the short force timer is intentionally kept referenced so
|
|
186
|
+
// natural process exit cannot orphan a resistant descendant before escalation runs.
|
|
187
|
+
child.once("close", cancelFallback);
|
|
188
|
+
child.once("error", cancelFallback);
|
|
189
|
+
forceTimer = setTimeout(() => {
|
|
190
|
+
forceTimer = undefined;
|
|
191
|
+
signal(true);
|
|
192
|
+
options.onForce?.();
|
|
193
|
+
}, grace);
|
|
194
|
+
if (options.onFallback) {
|
|
195
|
+
fallbackTimer = setTimeout(() => {
|
|
196
|
+
if (!fallbackActive)
|
|
197
|
+
return;
|
|
198
|
+
const callback = options.onFallback;
|
|
199
|
+
cancelFallback();
|
|
200
|
+
callback?.();
|
|
201
|
+
}, grace + fallback);
|
|
202
|
+
}
|
|
203
|
+
return (cancelForce = false) => {
|
|
204
|
+
cancelFallback();
|
|
205
|
+
if (cancelForce && forceTimer) {
|
|
206
|
+
clearTimeout(forceTimer);
|
|
207
|
+
forceTimer = undefined;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|