@link-assistant/hive-mind 2.13.5 → 2.15.0
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 +27 -0
- package/README.hi.md +2 -0
- package/README.md +2 -0
- package/README.ru.md +2 -0
- package/README.zh.md +2 -0
- package/package.json +1 -1
- package/src/agent-commander.lib.mjs +8 -0
- package/src/agent-memory-policy.lib.mjs +305 -0
- package/src/claude-quiet-config.lib.mjs +7 -2
- package/src/codex.lib.mjs +7 -0
- package/src/docker-sidecar.lib.mjs +276 -0
- package/src/formal-ai-maintenance.lib.mjs +2 -14
- package/src/formal-ai-sidecar.lib.mjs +17 -137
- package/src/gemini.lib.mjs +5 -0
- package/src/git-push-guard.lib.mjs +230 -0
- package/src/isolation-runner.lib.mjs +90 -10
- package/src/qwen.lib.mjs +4 -0
- package/src/router-isolation.lib.mjs +496 -0
- package/src/router-logs.lib.mjs +143 -0
- package/src/router-maintenance.lib.mjs +77 -0
- package/src/router-session-drain.lib.mjs +153 -0
- package/src/router-sidecar.lib.mjs +516 -0
- package/src/router-task-isolation.lib.mjs +121 -0
- package/src/solve.config.lib.mjs +16 -0
- package/src/task.config.lib.mjs +5 -0
- package/src/task.mjs +1 -0
- package/src/telegram-bot.mjs +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 29612e7: Turn off every AI tool's own cross-task memory and Claude Code's auto-mode classifier by default (issue #2178).
|
|
8
|
+
|
|
9
|
+
A hive-mind task is a disposable container that opens one pull request. The repository — commits, issues, pull requests, case studies — is meant to be the only memory it keeps, because it is the only memory a reviewer can see, correct or revert. Every agentic CLI has since grown a private cross-session store that works against that, and the largest of them (Gemini CLI's auto-memory) is a whole second agent re-reading past sessions on a second model.
|
|
10
|
+
|
|
11
|
+
`--agent-memory-disabled` is new and defaults to `true`. It applies, per tool:
|
|
12
|
+
|
|
13
|
+
- **claude** — `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1`, `CLAUDE_CODE_DISABLE_ORG_MEMORY=1`, `autoMemoryEnabled: false`, and `permissions.disableAutoMode: "disable"`. The last one is what removes the classifier: auto mode is what pays for it, and the settings gate is checked before the provider and model gates, so it holds everywhere the environment-variable opt-in would not. Tasks already run `--dangerously-skip-permissions` inside a disposable container, so a classifier deciding whether an action is safe is answering a question that has already been answered.
|
|
14
|
+
- **codex** — `-c features.memories=false -c features.external_agent_memory_import=false`, on both the `codex exec` path and the `--use-agent-commander` path. Both default to off upstream today, but `memories` is a stable-stage flag, so pinning it per run is what makes the default actually hold.
|
|
15
|
+
- **gemini, qwen** — `tools.exclude: ["save_memory"]` and `experimental.autoMemory: false`, merged into the tool's settings file without disturbing anything else already there.
|
|
16
|
+
- **opencode, agent** — no cross-session memory feature was found in either; recorded explicitly so the claim can be re-checked rather than assumed.
|
|
17
|
+
|
|
18
|
+
`--no-agent-memory-disabled` opts out for codex, gemini and qwen, and when it does the policy adds no arguments at all rather than arguments set to `true`. It does not reach claude: those switches are `ENV` lines in the Docker image and settings written by `configure-claude`, neither of which sees a `solve` argv, so they stay off either way and the flag's description says so.
|
|
19
|
+
|
|
20
|
+
## 2.14.0
|
|
21
|
+
|
|
22
|
+
### Minor Changes
|
|
23
|
+
|
|
24
|
+
- e796988: Add the experimental `--use-router` option, which stops handing a Docker-isolated task the operator's real subscription. With the flag, `~/.claude`, `~/.claude.json`, `~/.codex` and `~/.agents` are mounted only into a reference-counted `hive-mind-router` sidecar (pinned to `ghcr.io/link-assistant/router:0.119.0`) on an internal Docker network; each task is joined to that network and given its own `la_sk_…` token scoped to the one repository it works on, so every model request lands in that token's own redacted log. The sidecar runs while at least one task holds a lease and is stopped when the last one is released, with the Telegram bot reconciling leases against Docker so a killed bot cannot leave it running forever. At end of task the agent's session data is drained into the router's data volume for audit, and that volume is never removed by any code path.
|
|
25
|
+
|
|
26
|
+
GitHub is routed too: the sidecar terminates TLS itself with a certificate covering `api.github.com`, and the task container resolves that name to the router, so an unmodified `gh` is mediated without holding a GitHub credential of its own. `git` pushes through the router's `/git/*` proxy, which refuses remote-ref deletions and, from router `0.110.0`, non-fast-forward updates as well — it asks GitHub's compare API whether the proposed tip is ahead and fails closed when it cannot tell. A read-only `pre-push` hook in the task covers the same ground locally and for non-routed remotes. `--model formal-ai` is served through the router as a stored OpenAI-compatible provider, so it is audited like every other model.
|
|
27
|
+
|
|
28
|
+
Nothing changes without the flag. The remaining limits are documented and printed before every routed run: destructive GitHub API calls are blocked by HTTP method rather than by effect, so `PUT` branch protection, `PUT` rulesets, `POST` transfer and `PATCH` visibility/archived/default_branch are still forwarded (upstream `link-assistant/router#329`) — which puts branch protection itself within reach of a routed token; the Formal AI sidecar's own upstream calls are not routed; and the router resolves exact model ids only, not aliases, so the tier-shaped surface (`--plan`, `--escalate`, fallback chains) needs dated ids on a routed run. New docs: `docs/ROUTER.md`, `docs/COLLECTING-LOGS.md`, `docs/case-studies/issue-2164/` and `examples/collect-logs.mjs`.
|
|
29
|
+
|
|
3
30
|
## 2.13.5
|
|
4
31
|
|
|
5
32
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -88,6 +88,8 @@ Hive Mind में औसत प्रोग्रामर से अलग
|
|
|
88
88
|
|
|
89
89
|
Hive Mind अपने प्रबंधित terminal, log, telemetry, upload और GitHub publication paths में पहचाने गए credentials को sanitize करता है। Scanner विफल होने पर publication रोक दिया जाता है, लेकिन यह किसी autonomous tool को किसी असंबंधित exfiltration path का उपयोग करने से नहीं रोकता। Masking contract, supported formats, maintenance process और incident-response guidance के लिए [Credential sanitization](./docs/CREDENTIAL-SANITIZATION.hi.md) देखें।
|
|
90
90
|
|
|
91
|
+
डिफ़ॉल्ट रूप से Docker में आइसोलेट किए गए कार्यों को ऑपरेटर के Claude/Codex क्रेडेंशियल मिल जाते हैं। `--use-router` उन्हें रोक देता है और मॉडल ट्रैफ़िक को `hive-mind-router` sidecar से भेजता है, जिससे हर कार्य को अपना सीमित दायरे वाला टोकन और अपना request लॉग मिलता है — देखें [राउटर आइसोलेशन](./docs/ROUTER.hi.md) (प्रयोगात्मक) और [लॉग एकत्र करना](./docs/COLLECTING-LOGS.hi.md)।
|
|
92
|
+
|
|
91
93
|
`hive.mjs` चलाने के लिए न्यूनतम सिस्टम आवश्यकताएँ:
|
|
92
94
|
|
|
93
95
|
```
|
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ We strongly recommend:
|
|
|
88
88
|
|
|
89
89
|
Hive Mind sanitizes recognized credentials in its maintained terminal, log, telemetry, upload, and GitHub publication paths. Publication is blocked if the scanner fails, but this does not prevent an autonomous tool from using an unrelated exfiltration path. See [Credential sanitization](./docs/CREDENTIAL-SANITIZATION.md) for the masking contract, supported formats, maintenance process, and incident-response guidance.
|
|
90
90
|
|
|
91
|
+
Docker-isolated tasks receive the operator's Claude/Codex credentials by default. `--use-router` withholds them and routes model traffic through a `hive-mind-router` sidecar instead, giving each task its own scoped token and its own request log — see [Router isolation](./docs/ROUTER.md) (experimental) and [Collecting logs](./docs/COLLECTING-LOGS.md).
|
|
92
|
+
|
|
91
93
|
Minimum system requirements to run `hive.mjs`:
|
|
92
94
|
|
|
93
95
|
```
|
package/README.ru.md
CHANGED
|
@@ -88,6 +88,8 @@ Hive Mind обладает высоким уровнем творчества,
|
|
|
88
88
|
|
|
89
89
|
Hive Mind скрывает распознанные учётные данные в обслуживаемых им путях вывода терминала, журналов, телеметрии, загрузок и публикаций GitHub. При сбое сканера публикация блокируется, однако это не мешает автономному инструменту использовать посторонний канал утечки. Соглашение о маскировании, поддерживаемые форматы, процесс сопровождения и рекомендации по реагированию на инциденты приведены в разделе [Санитизация учётных данных](./docs/CREDENTIAL-SANITIZATION.ru.md).
|
|
90
90
|
|
|
91
|
+
По умолчанию задачи, изолированные в Docker, получают учётные данные Claude/Codex оператора. `--use-router` их не передаёт и вместо этого направляет трафик модели через sidecar `hive-mind-router`, выдавая каждой задаче собственный ограниченный токен и собственный журнал запросов — см. [Изоляция через роутер](./docs/ROUTER.ru.md) (экспериментально) и [Сбор журналов](./docs/COLLECTING-LOGS.ru.md).
|
|
92
|
+
|
|
91
93
|
Минимальные системные требования для запуска `hive.mjs`:
|
|
92
94
|
|
|
93
95
|
```
|
package/README.zh.md
CHANGED
|
@@ -88,6 +88,8 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
|
|
|
88
88
|
|
|
89
89
|
Hive Mind 会在其维护的终端、日志、遥测、上传和 GitHub 发布路径中清理可识别的凭证。如果扫描器失败,发布会被阻止;但这无法阻止自主工具通过不相关的路径泄露数据。有关掩码约定、支持的格式、维护流程和事件响应指南,请参阅[凭证清理](./docs/CREDENTIAL-SANITIZATION.zh.md)。
|
|
90
90
|
|
|
91
|
+
默认情况下,Docker 隔离的任务会拿到操作者的 Claude/Codex 凭据。`--use-router` 不再传入这些凭据,而是让模型流量经由 `hive-mind-router` sidecar 转发,使每个任务获得仅限自身的令牌和自己的请求日志——参见[路由器隔离](./docs/ROUTER.zh.md)(实验性)与[收集日志](./docs/COLLECTING-LOGS.zh.md)。
|
|
92
|
+
|
|
91
93
|
运行 `hive.mjs` 的最低系统要求:
|
|
92
94
|
|
|
93
95
|
```
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import { detectUsageLimit } from './usage-limit.lib.mjs';
|
|
|
14
14
|
import { applyFormalAiPricingOverride } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
15
15
|
import { getCacheReadTokenCount, getCumulativeContextInputTokens, getOutputTokenCount } from './context-fill.lib.mjs';
|
|
16
16
|
import { ensureAiToolScratchIgnored, filterAiToolScratchFromStatus } from './ai-tool-scratch.lib.mjs';
|
|
17
|
+
import { CLAUDE_MEMORY_DISABLE_ENV, buildCodexMemoryDisableConfigArgs, isAgentMemoryDisabled } from './agent-memory-policy.lib.mjs'; // Issue #2178
|
|
17
18
|
|
|
18
19
|
export const AGENT_COMMANDER_TOOLS = new Set(['claude', 'codex', 'opencode', 'agent', 'qwen', 'gemini']);
|
|
19
20
|
|
|
@@ -57,6 +58,12 @@ const buildClaudeToolOptions = (argv = {}) => {
|
|
|
57
58
|
if (argv.showThinkingContent) extraEnv.CLAUDE_CODE_SHOW_THINKING = '1';
|
|
58
59
|
if (argv.planModel) extraEnv.ANTHROPIC_DEFAULT_OPUS_MODEL = argv.planModel;
|
|
59
60
|
if (argv.subAgentModel) extraEnv.CLAUDE_CODE_SUBAGENT_MODEL = mapClaudeSubAgentModelToEnvValue(argv.subAgentModel);
|
|
61
|
+
// Issue #2178: agent-commander spawns `claude` itself, so it needs the memory
|
|
62
|
+
// opt-out in its own env rather than relying on the settings file alone. Not
|
|
63
|
+
// gated on --agent-memory-disabled: for claude these switches are part of the
|
|
64
|
+
// quiet configuration baked into the image, and the flag governs the tools
|
|
65
|
+
// whose memory is applied per run (codex, gemini, qwen).
|
|
66
|
+
Object.assign(extraEnv, CLAUDE_MEMORY_DISABLE_ENV);
|
|
60
67
|
appendExtraEnv(options, extraEnv);
|
|
61
68
|
|
|
62
69
|
return options;
|
|
@@ -73,6 +80,7 @@ const buildCodexToolOptions = (argv = {}) => {
|
|
|
73
80
|
appendExtraArgs(options, reasoningArgs);
|
|
74
81
|
|
|
75
82
|
appendExtraArgs(options, buildCodexDisable1mContextConfigArgs(!!argv.disable1mContext));
|
|
83
|
+
appendExtraArgs(options, buildCodexMemoryDisableConfigArgs(isAgentMemoryDisabled(argv))); // Issue #2178
|
|
76
84
|
try {
|
|
77
85
|
appendExtraArgs(options, buildCodexSubSessionSizeConfigArgs(parseSubSessionSize(argv.subSessionSize)));
|
|
78
86
|
} catch {
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cross-task memory is off for every agentic CLI hive-mind drives (issue #2178).
|
|
4
|
+
*
|
|
5
|
+
* A hive-mind task is a one-shot: a container is created, an issue is solved, a
|
|
6
|
+
* pull request is opened, the container is destroyed. Nothing an agent learns in
|
|
7
|
+
* one task is meant to reach the next one. The repository is the memory — commits,
|
|
8
|
+
* issues, pull requests, `docs/case-studies/` — and it is the only memory a
|
|
9
|
+
* reviewer can see, correct, or revert.
|
|
10
|
+
*
|
|
11
|
+
* Every agentic CLI now ships some form of private cross-session memory that
|
|
12
|
+
* works against that. It is not free:
|
|
13
|
+
*
|
|
14
|
+
* - it burns inference on top of the task (Gemini CLI runs a whole background
|
|
15
|
+
* extraction agent over past sessions; Claude Code loads a memory index into
|
|
16
|
+
* every session);
|
|
17
|
+
* - it carries facts between unrelated repositories with no review step;
|
|
18
|
+
* - it makes a run irreproducible — the same prompt on the same commit behaves
|
|
19
|
+
* differently depending on what the tool happened to remember.
|
|
20
|
+
*
|
|
21
|
+
* The permission classifiers are the same kind of waste for the same reason.
|
|
22
|
+
* Claude Code's "auto" mode pays a classifier call per tool use to decide whether
|
|
23
|
+
* an action is safe. Hive-mind tasks already run with unrestricted access inside a
|
|
24
|
+
* disposable Docker container (`--dangerously-skip-permissions`,
|
|
25
|
+
* `--dangerously-bypass-approvals-and-sandbox`, `--approval-mode yolo`), so the
|
|
26
|
+
* classifier can only ever answer a question nobody asked.
|
|
27
|
+
*
|
|
28
|
+
* This module is the single place that knows which knob turns each of these off,
|
|
29
|
+
* so `solve` and the Docker image baseline stay in agreement and a new tool
|
|
30
|
+
* version cannot quietly re-enable one of them without a test noticing.
|
|
31
|
+
*
|
|
32
|
+
* Verified against claude-code 2.1.246, codex-cli 0.148.0, gemini-cli 0.51.0,
|
|
33
|
+
* qwen-code 0.7.1 and opencode 1.18.5; see `docs/case-studies/issue-2178/`.
|
|
34
|
+
*
|
|
35
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2178
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fsPromises from 'node:fs/promises';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import path from 'node:path';
|
|
41
|
+
|
|
42
|
+
/** Tools `solve --tool` accepts that this policy has something to say about. */
|
|
43
|
+
export const AGENT_MEMORY_POLICY_TOOLS = Object.freeze(['claude', 'codex', 'gemini', 'qwen', 'opencode', 'agent']);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Claude Code environment variables that switch off every cross-session memory
|
|
47
|
+
* store it can reach.
|
|
48
|
+
*
|
|
49
|
+
* `CLAUDE_CODE_DISABLE_AUTO_MEMORY` disables the per-project memory directory,
|
|
50
|
+
* and the same branch also refuses the team stores named by
|
|
51
|
+
* `CLAUDE_MEMORY_STORES`. `CLAUDE_CODE_DISABLE_ORG_MEMORY` disables the
|
|
52
|
+
* organization-wide memory sync, which is gated separately.
|
|
53
|
+
*/
|
|
54
|
+
export const CLAUDE_MEMORY_DISABLE_ENV = Object.freeze({
|
|
55
|
+
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
|
|
56
|
+
CLAUDE_CODE_DISABLE_ORG_MEMORY: '1',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Claude Code settings that switch off memory.
|
|
61
|
+
*
|
|
62
|
+
* `autoMemoryEnabled: false` is the settings-file equivalent of
|
|
63
|
+
* `CLAUDE_CODE_DISABLE_AUTO_MEMORY` — both are checked, and either one is
|
|
64
|
+
* enough, so they are set together rather than one being trusted alone.
|
|
65
|
+
*/
|
|
66
|
+
export const CLAUDE_MEMORY_DISABLE_SETTINGS = Object.freeze({
|
|
67
|
+
autoMemoryEnabled: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Claude Code `permissions` block that removes auto mode.
|
|
72
|
+
*
|
|
73
|
+
* `"disable"` is the only value the setting accepts, and it is checked before
|
|
74
|
+
* the plan-level, provider-level and model-level gates, so it holds regardless
|
|
75
|
+
* of which provider or model a task runs on. Auto mode is what pays for the
|
|
76
|
+
* classifier; with it gone the classifier has no caller.
|
|
77
|
+
*/
|
|
78
|
+
export const CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS = Object.freeze({
|
|
79
|
+
disableAutoMode: 'disable',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Codex feature flags that hold its memory subsystem off.
|
|
84
|
+
*
|
|
85
|
+
* `memories` is the `~/.codex/memories` store Codex writes across sessions;
|
|
86
|
+
* `external_agent_memory_import` pulls another agent's memory in. Both default
|
|
87
|
+
* to off today, but `memories` is stage `stable`, so it can be switched on by a
|
|
88
|
+
* rollout or by an operator's `~/.codex/config.toml` — pinning them per run is
|
|
89
|
+
* what makes the default actually hold.
|
|
90
|
+
*/
|
|
91
|
+
export const CODEX_MEMORY_DISABLE_FEATURES = Object.freeze(['memories', 'external_agent_memory_import']);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `-c key=value` overrides that turn {@link CODEX_MEMORY_DISABLE_FEATURES} off.
|
|
95
|
+
*
|
|
96
|
+
* `-c features.<name>=false` is used rather than the `--disable <name>` alias
|
|
97
|
+
* because `--disable` is documented as exactly equivalent and `-c` is accepted
|
|
98
|
+
* by every `codex exec` version hive-mind supports.
|
|
99
|
+
*
|
|
100
|
+
* @param {boolean} [disabled=true] - false returns [] so `--no-agent-memory-disabled` is a no-op.
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
export const buildCodexMemoryDisableConfigArgs = (disabled = true) => {
|
|
104
|
+
if (!disabled) return [];
|
|
105
|
+
return CODEX_MEMORY_DISABLE_FEATURES.flatMap(feature => ['-c', `features.${feature}=false`]);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Tool name (as the CLI exposes it) of the Gemini-family memory writer.
|
|
110
|
+
*
|
|
111
|
+
* Gemini CLI dropped `save_memory` in 0.51 in favour of the `experimental.autoMemory`
|
|
112
|
+
* background extractor; Qwen Code, forked earlier, still ships the tool. Excluding
|
|
113
|
+
* the name covers the versions that have it and is inert on the versions that do not.
|
|
114
|
+
*/
|
|
115
|
+
export const GEMINI_FAMILY_MEMORY_TOOL = 'save_memory';
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Settings merged into `~/.gemini/settings.json` and `~/.qwen/settings.json`.
|
|
119
|
+
*
|
|
120
|
+
* `tools.exclude` is the nested form both CLIs resolve to (Qwen maps its legacy
|
|
121
|
+
* flat `excludeTools` onto it). `experimental.autoMemory` gates Gemini's
|
|
122
|
+
* background "skill extraction" agent, which re-reads past sessions with a second
|
|
123
|
+
* model and writes a memory index — the most expensive item on this list.
|
|
124
|
+
*/
|
|
125
|
+
export const GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS = Object.freeze({
|
|
126
|
+
tools: Object.freeze({ exclude: Object.freeze([GEMINI_FAMILY_MEMORY_TOOL]) }),
|
|
127
|
+
experimental: Object.freeze({ autoMemory: false }),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/** Where each Gemini-family CLI keeps its user settings. */
|
|
131
|
+
export const GEMINI_FAMILY_SETTINGS_PATHS = Object.freeze({
|
|
132
|
+
gemini: Object.freeze(['.gemini', 'settings.json']),
|
|
133
|
+
qwen: Object.freeze(['.qwen', 'settings.json']),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tools with no cross-session memory feature to switch off.
|
|
138
|
+
*
|
|
139
|
+
* Recorded explicitly rather than left out: "we looked and there was nothing" and
|
|
140
|
+
* "nobody looked" are different states, and only the first one stays true without
|
|
141
|
+
* someone re-checking. The test for this module asserts the list, so a future
|
|
142
|
+
* reader can see the claim was made deliberately.
|
|
143
|
+
*/
|
|
144
|
+
export const TOOLS_WITHOUT_MEMORY_FEATURE = Object.freeze(['opencode', 'agent']);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Is the policy on for this run?
|
|
148
|
+
*
|
|
149
|
+
* Reads `--agent-memory-disabled`, which defaults to true. Only an explicit
|
|
150
|
+
* `--no-agent-memory-disabled` turns it off, so an argv object that predates the
|
|
151
|
+
* flag (or omits it) still gets the policy.
|
|
152
|
+
*
|
|
153
|
+
* @param {Object} [argv]
|
|
154
|
+
*/
|
|
155
|
+
export const isAgentMemoryDisabled = (argv = {}) => argv?.agentMemoryDisabled !== false;
|
|
156
|
+
|
|
157
|
+
const isPlainObject = value => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Merge `desired` into `target` in place, returning the dotted paths that changed.
|
|
161
|
+
*
|
|
162
|
+
* Arrays are unioned rather than replaced so an operator's own `tools.exclude`
|
|
163
|
+
* entries survive; scalars are overwritten, because the whole point is that the
|
|
164
|
+
* policy wins.
|
|
165
|
+
*
|
|
166
|
+
* `__proto__`, `constructor` and `prototype` are skipped outright. Today this
|
|
167
|
+
* function is only ever handed {@link GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS}, a
|
|
168
|
+
* frozen literal whose keys are all ordinary, so none of them can occur — but
|
|
169
|
+
* that is a fact about the caller, not about the function, and callers change
|
|
170
|
+
* (CodeQL `js/prototype-pollution-utility`). The guard is written as explicit
|
|
171
|
+
* comparisons rather than a lookup in a shared set because that is the shape the
|
|
172
|
+
* query recognises as a barrier, and a guard a scanner cannot see is one that
|
|
173
|
+
* gets reported again every time someone touches the file.
|
|
174
|
+
*/
|
|
175
|
+
const mergeSettings = (target, desired, prefix = '') => {
|
|
176
|
+
const changed = [];
|
|
177
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
178
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
|
179
|
+
const dotted = prefix ? `${prefix}.${key}` : key;
|
|
180
|
+
if (isPlainObject(value)) {
|
|
181
|
+
if (!isPlainObject(target[key])) target[key] = {};
|
|
182
|
+
changed.push(...mergeSettings(target[key], value, dotted));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
const existing = Array.isArray(target[key]) ? target[key] : [];
|
|
187
|
+
const merged = [...existing];
|
|
188
|
+
let added = false;
|
|
189
|
+
for (const entry of value) {
|
|
190
|
+
if (!merged.includes(entry)) {
|
|
191
|
+
merged.push(entry);
|
|
192
|
+
added = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (added || !Array.isArray(target[key])) {
|
|
196
|
+
target[key] = merged;
|
|
197
|
+
changed.push(dotted);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (target[key] !== value) {
|
|
202
|
+
target[key] = value;
|
|
203
|
+
changed.push(dotted);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return changed;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resolve the settings file a Gemini-family CLI reads.
|
|
211
|
+
*
|
|
212
|
+
* @param {'gemini'|'qwen'} tool
|
|
213
|
+
* @param {Object} [options]
|
|
214
|
+
* @param {string} [options.homeDir]
|
|
215
|
+
* @returns {string|null} null when the tool has no Gemini-family settings file.
|
|
216
|
+
*/
|
|
217
|
+
export const resolveGeminiFamilySettingsPath = (tool, { homeDir = os.homedir() } = {}) => {
|
|
218
|
+
const segments = GEMINI_FAMILY_SETTINGS_PATHS[tool];
|
|
219
|
+
if (!segments) return null;
|
|
220
|
+
return path.join(homeDir, ...segments);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Write {@link GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS} into a Gemini-family
|
|
225
|
+
* settings file, preserving everything already there.
|
|
226
|
+
*
|
|
227
|
+
* Never throws. A task that cannot write the settings file is still a task worth
|
|
228
|
+
* running: the failure costs inference, not correctness, and the caller logs it.
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} [params]
|
|
231
|
+
* @param {'gemini'|'qwen'} params.tool
|
|
232
|
+
* @param {string} [params.settingsPath] - Overrides the tool's default location (tests).
|
|
233
|
+
* @param {string} [params.homeDir]
|
|
234
|
+
* @param {Function} [params.log]
|
|
235
|
+
* @param {Object} [params.fsImpl] - `node:fs/promises`-shaped, for tests.
|
|
236
|
+
* @returns {Promise<{applied: boolean, path: string|null, changed: string[], error: string|null}>}
|
|
237
|
+
*/
|
|
238
|
+
export const ensureGeminiFamilyMemoryDisabled = async ({ tool, settingsPath, homeDir = os.homedir(), log, fsImpl = fsPromises } = {}) => {
|
|
239
|
+
const resolvedPath = settingsPath || resolveGeminiFamilySettingsPath(tool, { homeDir });
|
|
240
|
+
if (!resolvedPath) return { applied: false, path: null, changed: [], error: null };
|
|
241
|
+
|
|
242
|
+
let settings = {};
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(await fsImpl.readFile(resolvedPath, 'utf-8'));
|
|
245
|
+
if (isPlainObject(parsed)) settings = parsed;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error?.code !== 'ENOENT' && log) {
|
|
248
|
+
await log(`⚠️ Could not read ${resolvedPath}: ${error.message}`, { verbose: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const changed = mergeSettings(settings, GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS);
|
|
253
|
+
try {
|
|
254
|
+
if (changed.length > 0) {
|
|
255
|
+
await fsImpl.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
256
|
+
await fsImpl.writeFile(resolvedPath, JSON.stringify(settings, null, 2));
|
|
257
|
+
}
|
|
258
|
+
if (log) {
|
|
259
|
+
await log(`🧠 Cross-task memory ${changed.length > 0 ? 'disabled' : 'already disabled'} for ${tool} in ${resolvedPath} (issue #2178)`, { verbose: true });
|
|
260
|
+
}
|
|
261
|
+
return { applied: true, path: resolvedPath, changed, error: null };
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const message = error?.message || String(error);
|
|
264
|
+
if (log) await log(`⚠️ Could not write ${resolvedPath}: ${message}`, { verbose: true });
|
|
265
|
+
return { applied: false, path: resolvedPath, changed: [], error: message };
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* One line describing what the policy does for a tool, for `--verbose` logs and
|
|
271
|
+
* for the docs to quote without drifting from the code.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} tool
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
export const describeAgentMemoryPolicy = tool => {
|
|
277
|
+
switch (tool) {
|
|
278
|
+
case 'claude':
|
|
279
|
+
return `settings ${JSON.stringify({ ...CLAUDE_MEMORY_DISABLE_SETTINGS, permissions: CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS })}, env ${Object.keys(CLAUDE_MEMORY_DISABLE_ENV).join(', ')}`;
|
|
280
|
+
case 'codex':
|
|
281
|
+
return buildCodexMemoryDisableConfigArgs(true).join(' ');
|
|
282
|
+
case 'gemini':
|
|
283
|
+
case 'qwen':
|
|
284
|
+
return `settings ${JSON.stringify(GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS)}`;
|
|
285
|
+
default:
|
|
286
|
+
return TOOLS_WITHOUT_MEMORY_FEATURE.includes(tool) ? 'no cross-session memory feature to disable' : 'no policy recorded for this tool';
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export default {
|
|
291
|
+
AGENT_MEMORY_POLICY_TOOLS,
|
|
292
|
+
CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS,
|
|
293
|
+
CLAUDE_MEMORY_DISABLE_ENV,
|
|
294
|
+
CLAUDE_MEMORY_DISABLE_SETTINGS,
|
|
295
|
+
CODEX_MEMORY_DISABLE_FEATURES,
|
|
296
|
+
GEMINI_FAMILY_MEMORY_DISABLE_SETTINGS,
|
|
297
|
+
GEMINI_FAMILY_MEMORY_TOOL,
|
|
298
|
+
GEMINI_FAMILY_SETTINGS_PATHS,
|
|
299
|
+
TOOLS_WITHOUT_MEMORY_FEATURE,
|
|
300
|
+
buildCodexMemoryDisableConfigArgs,
|
|
301
|
+
describeAgentMemoryPolicy,
|
|
302
|
+
ensureGeminiFamilyMemoryDisabled,
|
|
303
|
+
isAgentMemoryDisabled,
|
|
304
|
+
resolveGeminiFamilySettingsPath,
|
|
305
|
+
};
|
|
@@ -4,8 +4,12 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
7
|
+
// Issue #2178: memory and auto mode are governed by one cross-tool policy module
|
|
8
|
+
// so `solve` cannot disagree with the Docker image baseline about what "off" means.
|
|
9
|
+
import { CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS, CLAUDE_MEMORY_DISABLE_ENV, CLAUDE_MEMORY_DISABLE_SETTINGS } from './agent-memory-policy.lib.mjs';
|
|
10
|
+
|
|
7
11
|
export const REQUIRED_CLAUDE_QUIET_ENV = Object.freeze({
|
|
8
|
-
|
|
12
|
+
...CLAUDE_MEMORY_DISABLE_ENV,
|
|
9
13
|
CLAUDE_CODE_DISABLE_CRON: '1',
|
|
10
14
|
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: '1',
|
|
11
15
|
CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1',
|
|
@@ -20,7 +24,7 @@ export const REQUIRED_CLAUDE_QUIET_ENV = Object.freeze({
|
|
|
20
24
|
});
|
|
21
25
|
|
|
22
26
|
export const REQUIRED_CLAUDE_QUIET_SETTINGS = Object.freeze({
|
|
23
|
-
|
|
27
|
+
...CLAUDE_MEMORY_DISABLE_SETTINGS,
|
|
24
28
|
spinnerTipsEnabled: false,
|
|
25
29
|
awaySummaryEnabled: false,
|
|
26
30
|
feedbackSurveyRate: 0,
|
|
@@ -38,6 +42,7 @@ export const REQUIRED_CLAUDE_QUIET_ATTRIBUTION = Object.freeze({
|
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
export const REQUIRED_CLAUDE_QUIET_PERMISSIONS = Object.freeze({
|
|
45
|
+
...CLAUDE_AUTO_MODE_DISABLE_PERMISSIONS,
|
|
41
46
|
defaultMode: 'bypassPermissions',
|
|
42
47
|
});
|
|
43
48
|
|
package/src/codex.lib.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import { buildAuthRemedyLines, buildFormalAiEnvExports, isPrepareOnly, logPrepar
|
|
|
40
40
|
import { buildFormalAiPricingInfo } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
41
41
|
import { classifyRetryableError, createTransientRetryBudget, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
42
42
|
import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
43
|
+
import { buildCodexMemoryDisableConfigArgs, isAgentMemoryDisabled } from './agent-memory-policy.lib.mjs'; // Issue #2178
|
|
43
44
|
import { getCumulativeContextInputTokens } from './context-fill.lib.mjs';
|
|
44
45
|
import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
|
|
45
46
|
import { applyCodexCapabilityEnv, runCodexCapabilityPreflight } from './codex-capability-preflight.lib.mjs'; // Issue #2074
|
|
@@ -708,9 +709,15 @@ export const executeCodexCommand = async params => {
|
|
|
708
709
|
for (const arg of subSessionSizeArgs) {
|
|
709
710
|
codexArgs += ` ${shellQuote(arg)}`;
|
|
710
711
|
}
|
|
712
|
+
// Issue #2178: a hive-mind task must not remember anything a reviewer cannot see.
|
|
713
|
+
const memoryDisableArgs = buildCodexMemoryDisableConfigArgs(isAgentMemoryDisabled(argv));
|
|
714
|
+
for (const arg of memoryDisableArgs) {
|
|
715
|
+
codexArgs += ` ${shellQuote(arg)}`;
|
|
716
|
+
}
|
|
711
717
|
if (argv.verbose) {
|
|
712
718
|
if (disable1mArgs.length) await log(`📊 Codex --disable-1m-context: ${disable1mArgs.join(' ')}`, { verbose: true });
|
|
713
719
|
if (subSessionSizeArgs.length) await log(`📊 Codex --sub-session-size: ${subSessionSizeArgs.join(' ')}`, { verbose: true });
|
|
720
|
+
if (memoryDisableArgs.length) await log(`🧠 Codex cross-task memory disabled: ${memoryDisableArgs.join(' ')} (issue #2178)`, { verbose: true });
|
|
714
721
|
}
|
|
715
722
|
// Issue #2130: re-export the Formal AI environment inside the `sh -lc` script so a
|
|
716
723
|
// stale `formal-ai with --global` block in the operator profile cannot override it.
|