@0xmaxma/claude-gateway 1.8.3 → 1.8.5
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/README.md +58 -1
- package/config.template.json +5 -3
- package/dist/agent/model-catalog.d.ts.map +1 -1
- package/dist/agent/model-catalog.js +5 -13
- package/dist/agent/model-catalog.js.map +1 -1
- package/dist/agent/runner.d.ts +1 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +22 -3
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/apps-router.d.ts.map +1 -1
- package/dist/api/apps-router.js +31 -9
- package/dist/api/apps-router.js.map +1 -1
- package/dist/api/gateway-router.js +1 -1
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/share-router.d.ts +8 -0
- package/dist/api/share-router.d.ts.map +1 -1
- package/dist/api/share-router.js +100 -10
- package/dist/api/share-router.js.map +1 -1
- package/dist/apps/agent-manager.d.ts +35 -2
- package/dist/apps/agent-manager.d.ts.map +1 -1
- package/dist/apps/agent-manager.js +131 -15
- package/dist/apps/agent-manager.js.map +1 -1
- package/dist/apps/installer.d.ts +41 -1
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +52 -3
- package/dist/apps/installer.js.map +1 -1
- package/dist/cli/commands/debug-bundle.d.ts.map +1 -1
- package/dist/cli/commands/debug-bundle.js +9 -52
- package/dist/cli/commands/debug-bundle.js.map +1 -1
- package/dist/cli/commands/gateway.d.ts +6 -1
- package/dist/cli/commands/gateway.d.ts.map +1 -1
- package/dist/cli/commands/gateway.js +14 -3
- package/dist/cli/commands/gateway.js.map +1 -1
- package/dist/cli/commands/logs.d.ts +37 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +429 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +21 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs-dir.d.ts +69 -0
- package/dist/cli/logs-dir.d.ts.map +1 -0
- package/dist/cli/logs-dir.js +166 -0
- package/dist/cli/logs-dir.js.map +1 -0
- package/dist/cli/output.d.ts +34 -0
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +56 -10
- package/dist/cli/output.js.map +1 -1
- package/dist/config/claude-settings.d.ts +41 -0
- package/dist/config/claude-settings.d.ts.map +1 -0
- package/dist/config/claude-settings.js +102 -0
- package/dist/config/claude-settings.js.map +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +5 -0
- package/dist/config/watcher.js.map +1 -1
- package/dist/index.js +36 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +52 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +264 -2
- package/dist/logger.js.map +1 -1
- package/dist/session/process.d.ts +43 -1
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +154 -9
- package/dist/session/process.js.map +1 -1
- package/dist/share/share-store.d.ts +45 -4
- package/dist/share/share-store.d.ts.map +1 -1
- package/dist/share/share-store.js +127 -23
- package/dist/share/share-store.js.map +1 -1
- package/dist/shell/bypass-dialog.d.ts +104 -12
- package/dist/shell/bypass-dialog.d.ts.map +1 -1
- package/dist/shell/bypass-dialog.js +146 -14
- package/dist/shell/bypass-dialog.js.map +1 -1
- package/dist/shell/claude-pty-shell.js +29 -4
- package/dist/shell/claude-pty-shell.js.map +1 -1
- package/dist/shell/screen.d.ts +15 -11
- package/dist/shell/screen.d.ts.map +1 -1
- package/dist/shell/screen.js +42 -33
- package/dist/shell/screen.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/mcp/server.ts +2 -2
- package/mcp/tools/image/module.ts +10 -3
- package/mcp/tools/share-file/module.ts +169 -0
- package/mcp/tools/shared/share-client.ts +8 -4
- package/mcp/tools/telegram/receiver-server.ts +12 -2
- package/package.json +1 -1
- package/mcp/tools/share-image/module.ts +0 -120
package/dist/logger.js
CHANGED
|
@@ -33,16 +33,201 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LOGS_DEFAULTS = void 0;
|
|
37
|
+
exports.isLogLevel = isLogLevel;
|
|
38
|
+
exports.configureLogging = configureLogging;
|
|
39
|
+
exports.loggingConfig = loggingConfig;
|
|
40
|
+
exports.resetLoggingForTests = resetLoggingForTests;
|
|
36
41
|
exports.createLogger = createLogger;
|
|
42
|
+
exports.sweepOldLogs = sweepOldLogs;
|
|
43
|
+
exports.startLogRetentionSweep = startLogRetentionSweep;
|
|
37
44
|
const fs = __importStar(require("fs"));
|
|
38
45
|
const path = __importStar(require("path"));
|
|
46
|
+
/** Ordering for the level gate. Only the relative order matters. */
|
|
47
|
+
const LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
48
|
+
function isLogLevel(value) {
|
|
49
|
+
return typeof value === 'string' && value in LEVEL_RANK;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Defaults applied when `gateway.logs` is absent or partial.
|
|
53
|
+
*
|
|
54
|
+
* `level: 'info'` is the load-bearing one. Session processes log every stream
|
|
55
|
+
* event at debug (src/session/process.ts), which on a live host measured as
|
|
56
|
+
* 19,995 debug lines to 5 info lines in a single 217 MB file — so `debug`
|
|
57
|
+
* on-by-default was, in practice, the whole log directory. Rotation alone would
|
|
58
|
+
* not have helped: it bounds what is *kept*, not what is *written*.
|
|
59
|
+
*/
|
|
60
|
+
exports.LOGS_DEFAULTS = {
|
|
61
|
+
level: 'info',
|
|
62
|
+
maxFileBytes: 16 * 1024 * 1024,
|
|
63
|
+
maxFiles: 3,
|
|
64
|
+
retentionDays: 14,
|
|
65
|
+
};
|
|
66
|
+
let active = { ...exports.LOGS_DEFAULTS };
|
|
67
|
+
/**
|
|
68
|
+
* Install the process-wide logging policy. Called once at boot, before the
|
|
69
|
+
* first logger exists.
|
|
70
|
+
*
|
|
71
|
+
* Policy is module state rather than a per-logger argument because there is one
|
|
72
|
+
* of it per process, and `createLogger()` has 13 call sites — several of which
|
|
73
|
+
* (routers, the session process) have no access to the gateway config. Threading
|
|
74
|
+
* an options bag through all of them would put the same value in 13 places and
|
|
75
|
+
* let them drift.
|
|
76
|
+
*/
|
|
77
|
+
function configureLogging(cfg) {
|
|
78
|
+
const level = isLogLevel(cfg?.level) ? cfg.level : exports.LOGS_DEFAULTS.level;
|
|
79
|
+
active = {
|
|
80
|
+
level,
|
|
81
|
+
maxFileBytes: positiveOrDefault(cfg?.maxFileBytes, exports.LOGS_DEFAULTS.maxFileBytes),
|
|
82
|
+
maxFiles: nonNegativeOrDefault(cfg?.maxFiles, exports.LOGS_DEFAULTS.maxFiles),
|
|
83
|
+
retentionDays: nonNegativeOrDefault(cfg?.retentionDays, exports.LOGS_DEFAULTS.retentionDays),
|
|
84
|
+
};
|
|
85
|
+
return { ...active };
|
|
86
|
+
}
|
|
87
|
+
/** The policy currently in force (a copy — callers must not mutate it). */
|
|
88
|
+
function loggingConfig() {
|
|
89
|
+
return { ...active };
|
|
90
|
+
}
|
|
91
|
+
/** Test hook: restore defaults and forget per-file state. */
|
|
92
|
+
function resetLoggingForTests() {
|
|
93
|
+
active = { ...exports.LOGS_DEFAULTS };
|
|
94
|
+
fileSizes.clear();
|
|
95
|
+
writeFailureReported = false;
|
|
96
|
+
}
|
|
97
|
+
/** 0 is meaningful for `maxFiles`/`retentionDays` (= disabled) but not for a
|
|
98
|
+
* size threshold, where it would rotate on every line. */
|
|
99
|
+
function positiveOrDefault(value, fallback) {
|
|
100
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
101
|
+
}
|
|
102
|
+
function nonNegativeOrDefault(value, fallback) {
|
|
103
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Live size per log file, keyed by absolute path.
|
|
107
|
+
*
|
|
108
|
+
* Kept at module scope rather than on the instance because one file can have
|
|
109
|
+
* several loggers writing to it — `createLogger(agentConfig.id, …)` is called
|
|
110
|
+
* from both the boot path and the runner constructor. Per-instance counters
|
|
111
|
+
* would each see only their own share of the file and rotate late. Seeded from
|
|
112
|
+
* `statSync` once per file, then maintained by arithmetic so the common path
|
|
113
|
+
* costs no syscall.
|
|
114
|
+
*
|
|
115
|
+
* One entry per log file, and session logs create a new file each. The
|
|
116
|
+
* retention sweep deletes its entry along with the file, which bounds the map
|
|
117
|
+
* for any configuration that keeps retention on; with `retentionDays: 0` it
|
|
118
|
+
* grows with the session count, at roughly a path string per session. That is
|
|
119
|
+
* small enough not to be worth a second eviction mechanism whose only job would
|
|
120
|
+
* be to duplicate the sweep.
|
|
121
|
+
*/
|
|
122
|
+
const fileSizes = new Map();
|
|
123
|
+
/**
|
|
124
|
+
* A failed append is latched and reported once per process.
|
|
125
|
+
*
|
|
126
|
+
* This used to be a bare `catch {}` — a full disk or a permission change
|
|
127
|
+
* silently stopped all file logging, which is precisely the failure that makes
|
|
128
|
+
* the next incident undiagnosable. Reporting every occurrence would be its own
|
|
129
|
+
* denial of service (the failing call is in the logger), so it is reported once
|
|
130
|
+
* and then suppressed.
|
|
131
|
+
*/
|
|
132
|
+
let writeFailureReported = false;
|
|
133
|
+
function reportWriteFailure(file, err) {
|
|
134
|
+
if (writeFailureReported)
|
|
135
|
+
return;
|
|
136
|
+
writeFailureReported = true;
|
|
137
|
+
const reason = err?.code ?? err?.message ?? String(err);
|
|
138
|
+
process.stderr.write(`[logger] cannot write to ${file} (${reason}) — file logging is degraded for the rest of this process. ` +
|
|
139
|
+
'Further write failures will not be reported.\n');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Remove every rotated generation at or above `maxFiles`.
|
|
143
|
+
*
|
|
144
|
+
* Deleting only `<file>.<maxFiles>` would be dead code: `renameSync` clobbers
|
|
145
|
+
* its destination, so the top generation is overwritten by the shift below
|
|
146
|
+
* anyway — but only while `maxFiles` stays where it was. Lower it in the config
|
|
147
|
+
* (5 → 2) and generations 2..5 are orphaned: nothing renames onto them, and the
|
|
148
|
+
* age sweep is the only thing that would ever collect them. A rotation happens
|
|
149
|
+
* once per `maxFileBytes`, so it can afford one readdir to stay bounded.
|
|
150
|
+
*/
|
|
151
|
+
function pruneGenerations(file, maxFiles) {
|
|
152
|
+
const dir = path.dirname(file);
|
|
153
|
+
const prefix = `${path.basename(file)}.`;
|
|
154
|
+
let names;
|
|
155
|
+
try {
|
|
156
|
+
names = fs.readdirSync(dir);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return; // best-effort
|
|
160
|
+
}
|
|
161
|
+
for (const name of names) {
|
|
162
|
+
if (!name.startsWith(prefix))
|
|
163
|
+
continue;
|
|
164
|
+
const gen = Number(name.slice(prefix.length));
|
|
165
|
+
if (!Number.isInteger(gen) || gen < maxFiles)
|
|
166
|
+
continue;
|
|
167
|
+
try {
|
|
168
|
+
fs.rmSync(path.join(dir, name), { force: true });
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
/* best-effort */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** `<name>.log` → `<name>.log.1`, dropping generations past `maxFiles`.
|
|
176
|
+
*
|
|
177
|
+
* Synchronous and best-effort by design: it runs inside a logging call, so it
|
|
178
|
+
* must neither block on I/O the caller did not ask for nor throw into code
|
|
179
|
+
* that was only trying to log. A rotation that fails leaves the live file in
|
|
180
|
+
* place and logging simply continues into it. */
|
|
181
|
+
function rotate(file, maxFiles) {
|
|
182
|
+
if (maxFiles <= 0) {
|
|
183
|
+
// No generations kept: the live file is the only file, so start it over.
|
|
184
|
+
try {
|
|
185
|
+
fs.unlinkSync(file);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* best-effort */
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
pruneGenerations(file, maxFiles);
|
|
193
|
+
for (let gen = maxFiles - 1; gen >= 1; gen--) {
|
|
194
|
+
try {
|
|
195
|
+
fs.renameSync(`${file}.${gen}`, `${file}.${gen + 1}`);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* a generation that does not exist yet is the normal case */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
fs.renameSync(file, `${file}.1`);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* best-effort */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
39
208
|
class AgentLogger {
|
|
40
209
|
constructor(agentId, logDir) {
|
|
41
210
|
this.agentId = agentId;
|
|
42
211
|
fs.mkdirSync(logDir, { recursive: true });
|
|
43
212
|
this.logFilePath = path.join(logDir, `${agentId}.log`);
|
|
44
213
|
}
|
|
214
|
+
currentSize() {
|
|
215
|
+
const known = fileSizes.get(this.logFilePath);
|
|
216
|
+
if (known !== undefined)
|
|
217
|
+
return known;
|
|
218
|
+
let size = 0;
|
|
219
|
+
try {
|
|
220
|
+
size = fs.statSync(this.logFilePath).size;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
size = 0; // not created yet
|
|
224
|
+
}
|
|
225
|
+
fileSizes.set(this.logFilePath, size);
|
|
226
|
+
return size;
|
|
227
|
+
}
|
|
45
228
|
log(level, message, data) {
|
|
229
|
+
if (LEVEL_RANK[level] < LEVEL_RANK[active.level])
|
|
230
|
+
return;
|
|
46
231
|
const entry = {
|
|
47
232
|
ts: new Date().toISOString(),
|
|
48
233
|
agentId: this.agentId,
|
|
@@ -55,11 +240,18 @@ class AgentLogger {
|
|
|
55
240
|
// Write to stdout (pretty-printed for readability)
|
|
56
241
|
process.stdout.write(pretty + '\n');
|
|
57
242
|
// Write to log file (compact, one entry per line)
|
|
243
|
+
const bytes = Buffer.byteLength(line, 'utf-8') + 1;
|
|
58
244
|
try {
|
|
245
|
+
if (active.maxFileBytes > 0 && this.currentSize() + bytes > active.maxFileBytes) {
|
|
246
|
+
rotate(this.logFilePath, active.maxFiles);
|
|
247
|
+
fileSizes.set(this.logFilePath, 0);
|
|
248
|
+
}
|
|
59
249
|
fs.appendFileSync(this.logFilePath, line + '\n', 'utf-8');
|
|
250
|
+
fileSizes.set(this.logFilePath, this.currentSize() + bytes);
|
|
60
251
|
}
|
|
61
|
-
catch {
|
|
62
|
-
//
|
|
252
|
+
catch (err) {
|
|
253
|
+
// Logging must never throw into a caller, but it must not vanish either.
|
|
254
|
+
reportWriteFailure(this.logFilePath, err);
|
|
63
255
|
}
|
|
64
256
|
}
|
|
65
257
|
info(message, data) {
|
|
@@ -78,4 +270,74 @@ class AgentLogger {
|
|
|
78
270
|
function createLogger(agentId, logDir) {
|
|
79
271
|
return new AgentLogger(agentId, logDir);
|
|
80
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Delete log files (live and rotated generations) last modified more than
|
|
275
|
+
* `retentionDays` ago. Returns the paths removed.
|
|
276
|
+
*
|
|
277
|
+
* Age, not count, is what bounds a directory of *session* logs: each session
|
|
278
|
+
* gets its own `<agent>:session:<uuid>.log` that is never written again once the
|
|
279
|
+
* session ends, so `maxFiles` — which only prunes generations of one stream —
|
|
280
|
+
* never touches them.
|
|
281
|
+
*/
|
|
282
|
+
function sweepOldLogs(logDir, retentionDays, now = Date.now()) {
|
|
283
|
+
const removed = [];
|
|
284
|
+
if (!(retentionDays > 0))
|
|
285
|
+
return removed; // 0 = keep forever
|
|
286
|
+
const cutoff = now - retentionDays * 24 * 60 * 60 * 1000;
|
|
287
|
+
let names;
|
|
288
|
+
try {
|
|
289
|
+
names = fs.readdirSync(logDir);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return removed; // no directory yet, or unreadable — nothing to sweep
|
|
293
|
+
}
|
|
294
|
+
for (const name of names) {
|
|
295
|
+
// `.log` and its rotated generations (`.log.1`), and nothing else: the
|
|
296
|
+
// directory is not exclusively ours to delete from.
|
|
297
|
+
if (!/\.log(\.\d+)?$/.test(name))
|
|
298
|
+
continue;
|
|
299
|
+
const full = path.join(logDir, name);
|
|
300
|
+
try {
|
|
301
|
+
if (fs.statSync(full).mtimeMs >= cutoff)
|
|
302
|
+
continue;
|
|
303
|
+
fs.rmSync(full, { force: true });
|
|
304
|
+
fileSizes.delete(full);
|
|
305
|
+
removed.push(full);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
/* best-effort — a file that vanished or cannot be read is not fatal */
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return removed;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Sweep now, then once a day. Returns a stop function.
|
|
315
|
+
*
|
|
316
|
+
* Modelled on `AppInstaller.startBackupCleanup()`: the timer is unref'd so it
|
|
317
|
+
* never keeps the process alive, and a failing sweep is swallowed rather than
|
|
318
|
+
* escalated — retention is housekeeping, not a reason to take the gateway down.
|
|
319
|
+
*
|
|
320
|
+
* The timer is created unconditionally, including when retention is currently
|
|
321
|
+
* off. `retentionDays` is read on each run rather than captured, so a policy
|
|
322
|
+
* reloaded from `gateway.logs` takes effect at the next sweep — and a timer
|
|
323
|
+
* that was never started because retention happened to be 0 at boot could not
|
|
324
|
+
* do that. A disabled sweep costs one no-op call a day.
|
|
325
|
+
*/
|
|
326
|
+
function startLogRetentionSweep(logDir, onSwept) {
|
|
327
|
+
const run = () => {
|
|
328
|
+
try {
|
|
329
|
+
const removed = sweepOldLogs(logDir, active.retentionDays);
|
|
330
|
+
if (removed.length > 0)
|
|
331
|
+
onSwept?.(removed);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
/* best-effort */
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
run();
|
|
338
|
+
const timer = setInterval(run, 24 * 60 * 60 * 1000);
|
|
339
|
+
if (typeof timer.unref === 'function')
|
|
340
|
+
timer.unref();
|
|
341
|
+
return () => clearInterval(timer);
|
|
342
|
+
}
|
|
81
343
|
//# sourceMappingURL=logger.js.map
|
package/dist/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -87,10 +87,52 @@ export declare class SessionProcess extends EventEmitter {
|
|
|
87
87
|
private setupRestartWatcher;
|
|
88
88
|
private buildInitialPrompt;
|
|
89
89
|
/**
|
|
90
|
-
* Read stdio MCP servers from Claude Code's user-scoped config
|
|
90
|
+
* Read stdio MCP servers from Claude Code's user-scoped config
|
|
91
|
+
* (`$CLAUDE_CONFIG_DIR/settings.json`, else `~/.claude/settings.json`).
|
|
91
92
|
* Returns empty object if file doesn't exist, can't be parsed, or has no mcpServers.
|
|
92
93
|
*/
|
|
93
94
|
private readUserScopedMcp;
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the Claude credentials handed to an app-agent container this spawn.
|
|
97
|
+
*
|
|
98
|
+
* Container agents used to receive their credentials as a read-only bind mount
|
|
99
|
+
* of the host ~/.claude/settings.json. A Docker *file* bind mount pins an
|
|
100
|
+
* inode rather than a path, and Claude Code rewrites that file by atomic
|
|
101
|
+
* rename — so the first host settings change left every long-lived container
|
|
102
|
+
* reading a deleted inode. Claude Code silently ignores a settings.json whose
|
|
103
|
+
* inode is unlinked, so the agent stayed healthy and answered every real user
|
|
104
|
+
* "Not logged in · Please run /login" until someone recreated the container.
|
|
105
|
+
*
|
|
106
|
+
* That mechanism was verified directly rather than inferred: with content held
|
|
107
|
+
* byte-identical (same md5) and only the link count differing, a container
|
|
108
|
+
* reading the unlinked inode reported "Not logged in" and sent no request,
|
|
109
|
+
* while the same file at nlink=1 authenticated normally.
|
|
110
|
+
*
|
|
111
|
+
* Resolving by path here is immune to that: the container is handed the
|
|
112
|
+
* current credentials every time a session subprocess is spawned. Because the
|
|
113
|
+
* unlinked file is *ignored* rather than merely stale, the forwarded env is
|
|
114
|
+
* what the CLI ends up using — so a container stuck on a stale mount recovers
|
|
115
|
+
* on its next session spawn, with no container recreate. Note the cadence is
|
|
116
|
+
* per spawn, not per turn: one `docker exec` serves a whole session and later
|
|
117
|
+
* turns are written to its stdin, so a rotated credential reaches a live
|
|
118
|
+
* session only when it is respawned (idle reap, restart or config change).
|
|
119
|
+
*
|
|
120
|
+
* settings.json wins over the gateway's own environment. That matches Claude
|
|
121
|
+
* Code, which applies the file's `env` block over whatever it inherited (also
|
|
122
|
+
* verified: with both sources set to different endpoints, every request went
|
|
123
|
+
* to the file's), and it keeps the live file as the single source of truth — a
|
|
124
|
+
* long-lived gateway started with an exported token would otherwise pin the
|
|
125
|
+
* container to a credential that can never be rotated without restarting the
|
|
126
|
+
* service.
|
|
127
|
+
*
|
|
128
|
+
* Credentials resolve as a GROUP, not key by key. If the file declares an
|
|
129
|
+
* identity at all, the gateway's environment must not contribute a *different*
|
|
130
|
+
* one: forwarding a file OAuth token alongside an environment
|
|
131
|
+
* ANTHROPIC_API_KEY would hand the container two identities and let the CLI
|
|
132
|
+
* choose between them silently. A base URL only selects an endpoint, so it is
|
|
133
|
+
* not part of that group and keeps its own independent fallback.
|
|
134
|
+
*/
|
|
135
|
+
private resolveContainerAuthEnv;
|
|
94
136
|
/**
|
|
95
137
|
* Read stdio MCP servers from Claude Code's project-scoped config (~/.claude.json).
|
|
96
138
|
* Looks up projects[workspace].mcpServers for the agent's workspace path.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/session/process.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAKtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGtD,OAAO,EAAmC,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../src/session/process.ts"],"names":[],"mappings":"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"}
|
package/dist/session/process.js
CHANGED
|
@@ -51,8 +51,35 @@ const logger_1 = require("../logger");
|
|
|
51
51
|
const pty_stream_registry_1 = require("../shell/pty-stream-registry");
|
|
52
52
|
const screen_1 = require("../shell/screen");
|
|
53
53
|
const claude_bin_1 = require("./claude-bin");
|
|
54
|
+
const claude_settings_1 = require("../config/claude-settings");
|
|
54
55
|
const tool_labels_1 = require("../utils/tool-labels");
|
|
55
56
|
exports.MAX_HISTORY_MESSAGES = 50;
|
|
57
|
+
/**
|
|
58
|
+
* Claude credential / API-routing env vars forwarded from the host into an
|
|
59
|
+
* app-agent container. Deliberately narrow — auth and endpoint only. Model and
|
|
60
|
+
* behaviour settings come from the agent's own config, not the host env.
|
|
61
|
+
*
|
|
62
|
+
* The keys that prove an identity, as opposed to merely selecting an endpoint.
|
|
63
|
+
* These resolve as a group so a container is never handed two credentials from
|
|
64
|
+
* two different sources — see resolveContainerAuthEnv().
|
|
65
|
+
*/
|
|
66
|
+
const CONTAINER_CREDENTIAL_KEYS = [
|
|
67
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
68
|
+
'ANTHROPIC_API_KEY',
|
|
69
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* The credential keys plus the endpoint selector. Derived from
|
|
73
|
+
* CONTAINER_CREDENTIAL_KEYS rather than repeated, so the two lists cannot drift:
|
|
74
|
+
* a credential added to one is automatically forwarded by the other.
|
|
75
|
+
*
|
|
76
|
+
* ANTHROPIC_BASE_URL is not a credential — it only selects an endpoint — so it
|
|
77
|
+
* sits outside the group that resolves atomically in resolveContainerAuthEnv().
|
|
78
|
+
*/
|
|
79
|
+
const CONTAINER_AUTH_ENV_KEYS = [
|
|
80
|
+
...CONTAINER_CREDENTIAL_KEYS,
|
|
81
|
+
'ANTHROPIC_BASE_URL',
|
|
82
|
+
];
|
|
56
83
|
// The MCP reply tools that deliver an agent's user-facing message to a channel.
|
|
57
84
|
// Their text lives in the tool_use `input.text`, not in an assistant text block,
|
|
58
85
|
// so it is never captured by `assistantBuffer` — the reply is delivered to the
|
|
@@ -426,18 +453,117 @@ class SessionProcess extends events_1.EventEmitter {
|
|
|
426
453
|
};
|
|
427
454
|
}
|
|
428
455
|
/**
|
|
429
|
-
* Read stdio MCP servers from Claude Code's user-scoped config
|
|
456
|
+
* Read stdio MCP servers from Claude Code's user-scoped config
|
|
457
|
+
* (`$CLAUDE_CONFIG_DIR/settings.json`, else `~/.claude/settings.json`).
|
|
430
458
|
* Returns empty object if file doesn't exist, can't be parsed, or has no mcpServers.
|
|
431
459
|
*/
|
|
432
460
|
readUserScopedMcp() {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
436
|
-
return parsed?.mcpServers ?? {};
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
461
|
+
const servers = (0, claude_settings_1.readClaudeSettings)()?.mcpServers;
|
|
462
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers))
|
|
439
463
|
return {};
|
|
464
|
+
return servers;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Resolve the Claude credentials handed to an app-agent container this spawn.
|
|
468
|
+
*
|
|
469
|
+
* Container agents used to receive their credentials as a read-only bind mount
|
|
470
|
+
* of the host ~/.claude/settings.json. A Docker *file* bind mount pins an
|
|
471
|
+
* inode rather than a path, and Claude Code rewrites that file by atomic
|
|
472
|
+
* rename — so the first host settings change left every long-lived container
|
|
473
|
+
* reading a deleted inode. Claude Code silently ignores a settings.json whose
|
|
474
|
+
* inode is unlinked, so the agent stayed healthy and answered every real user
|
|
475
|
+
* "Not logged in · Please run /login" until someone recreated the container.
|
|
476
|
+
*
|
|
477
|
+
* That mechanism was verified directly rather than inferred: with content held
|
|
478
|
+
* byte-identical (same md5) and only the link count differing, a container
|
|
479
|
+
* reading the unlinked inode reported "Not logged in" and sent no request,
|
|
480
|
+
* while the same file at nlink=1 authenticated normally.
|
|
481
|
+
*
|
|
482
|
+
* Resolving by path here is immune to that: the container is handed the
|
|
483
|
+
* current credentials every time a session subprocess is spawned. Because the
|
|
484
|
+
* unlinked file is *ignored* rather than merely stale, the forwarded env is
|
|
485
|
+
* what the CLI ends up using — so a container stuck on a stale mount recovers
|
|
486
|
+
* on its next session spawn, with no container recreate. Note the cadence is
|
|
487
|
+
* per spawn, not per turn: one `docker exec` serves a whole session and later
|
|
488
|
+
* turns are written to its stdin, so a rotated credential reaches a live
|
|
489
|
+
* session only when it is respawned (idle reap, restart or config change).
|
|
490
|
+
*
|
|
491
|
+
* settings.json wins over the gateway's own environment. That matches Claude
|
|
492
|
+
* Code, which applies the file's `env` block over whatever it inherited (also
|
|
493
|
+
* verified: with both sources set to different endpoints, every request went
|
|
494
|
+
* to the file's), and it keeps the live file as the single source of truth — a
|
|
495
|
+
* long-lived gateway started with an exported token would otherwise pin the
|
|
496
|
+
* container to a credential that can never be rotated without restarting the
|
|
497
|
+
* service.
|
|
498
|
+
*
|
|
499
|
+
* Credentials resolve as a GROUP, not key by key. If the file declares an
|
|
500
|
+
* identity at all, the gateway's environment must not contribute a *different*
|
|
501
|
+
* one: forwarding a file OAuth token alongside an environment
|
|
502
|
+
* ANTHROPIC_API_KEY would hand the container two identities and let the CLI
|
|
503
|
+
* choose between them silently. A base URL only selects an endpoint, so it is
|
|
504
|
+
* not part of that group and keeps its own independent fallback.
|
|
505
|
+
*/
|
|
506
|
+
resolveContainerAuthEnv() {
|
|
507
|
+
// Resolved through the shared helper so an operator who relocated the config
|
|
508
|
+
// with CLAUDE_CONFIG_DIR is honoured here too. Hardcoding ~/.claude would
|
|
509
|
+
// read a path that does not exist and forward nothing — reproducing the very
|
|
510
|
+
// "Not logged in" failure this method was written to prevent.
|
|
511
|
+
const fileEnv = (0, claude_settings_1.claudeSettingsEnv)();
|
|
512
|
+
// Presence, not validity: a credential key spelled out in settings.json is
|
|
513
|
+
// authoritative for the whole group even when its value is malformed, so a
|
|
514
|
+
// broken token can never silently fall back to a different identity.
|
|
515
|
+
const fileDeclaresCredential = CONTAINER_CREDENTIAL_KEYS.some((k) => k in fileEnv);
|
|
516
|
+
const resolved = {};
|
|
517
|
+
const dropped = [];
|
|
518
|
+
for (const key of CONTAINER_AUTH_ENV_KEYS) {
|
|
519
|
+
let value;
|
|
520
|
+
let origin;
|
|
521
|
+
if (key in fileEnv) {
|
|
522
|
+
value = fileEnv[key];
|
|
523
|
+
origin = 'settings.json';
|
|
524
|
+
}
|
|
525
|
+
else if (fileDeclaresCredential && CONTAINER_CREDENTIAL_KEYS.includes(key)) {
|
|
526
|
+
continue; // the file owns the identity — don't mix in another one
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
value = process.env[key];
|
|
530
|
+
origin = 'gateway env';
|
|
531
|
+
}
|
|
532
|
+
// settings.json is untrusted input: forward only a clean, non-empty
|
|
533
|
+
// string. A NUL byte throws at spawn, and a newline is never legitimate
|
|
534
|
+
// in a credential or a base URL.
|
|
535
|
+
if (value === undefined)
|
|
536
|
+
continue; // simply not set anywhere — not a defect
|
|
537
|
+
if (typeof value !== 'string') {
|
|
538
|
+
dropped.push({ key, reason: `${origin} value is ${typeof value}, not a string` });
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (value.length === 0) {
|
|
542
|
+
dropped.push({ key, reason: `${origin} value is empty` });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (/[\0\r\n]/.test(value)) {
|
|
546
|
+
dropped.push({ key, reason: `${origin} value contains a NUL or newline` });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
resolved[key] = value;
|
|
550
|
+
}
|
|
551
|
+
// Never fail silently. The bug this whole path exists to fix presented as a
|
|
552
|
+
// container that looked healthy while answering real users "Not logged in",
|
|
553
|
+
// so a spawn that forwards no credential at all must say so — otherwise a
|
|
554
|
+
// malformed token in settings.json reproduces the original symptom with the
|
|
555
|
+
// original silence. `dropped` names the reason without ever logging a value.
|
|
556
|
+
const forwardedCredentials = CONTAINER_CREDENTIAL_KEYS.filter((k) => k in resolved);
|
|
557
|
+
if (forwardedCredentials.length === 0) {
|
|
558
|
+
this.logger.warn('No Claude credential could be resolved for this container agent — it will likely report "Not logged in"', { sessionId: this.sessionId, dropped, checked: CONTAINER_AUTH_ENV_KEYS });
|
|
440
559
|
}
|
|
560
|
+
else if (dropped.length > 0) {
|
|
561
|
+
this.logger.debug('Some container auth env vars were not forwarded', {
|
|
562
|
+
sessionId: this.sessionId,
|
|
563
|
+
dropped,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return resolved;
|
|
441
567
|
}
|
|
442
568
|
/**
|
|
443
569
|
* Read stdio MCP servers from Claude Code's project-scoped config (~/.claude.json).
|
|
@@ -547,6 +673,11 @@ class SessionProcess extends events_1.EventEmitter {
|
|
|
547
673
|
IMAGE_API_KEY: process.env.IMAGE_API_KEY ?? process.env.ANTHROPIC_AUTH_TOKEN ?? '',
|
|
548
674
|
IMAGE_DISABLED: process.env.IMAGE_DISABLED ?? '',
|
|
549
675
|
IMAGE_POLL_TIMEOUT_MS: process.env.IMAGE_POLL_TIMEOUT_MS ?? '',
|
|
676
|
+
// #444: the share bridge's env vars were renamed IMAGE_SHARE_* ->
|
|
677
|
+
// SHARE_* when it stopped being image-only. Forward BOTH so an
|
|
678
|
+
// operator who set either name keeps working; the MCP side reads
|
|
679
|
+
// the neutral name first and falls back to the legacy one.
|
|
680
|
+
SHARE_MAX_REFS: process.env.SHARE_MAX_REFS ?? '',
|
|
550
681
|
IMAGE_SHARE_MAX_REFS: process.env.IMAGE_SHARE_MAX_REFS ?? '',
|
|
551
682
|
// Short-lived public media URLs (LINE image delivery). line_image
|
|
552
683
|
// mints a share token via the gateway share bridge (using
|
|
@@ -732,12 +863,25 @@ class SessionProcess extends events_1.EventEmitter {
|
|
|
732
863
|
const containerEnv = {
|
|
733
864
|
HOME: os.homedir(),
|
|
734
865
|
CLAUDE_WORKSPACE: '/workspace',
|
|
735
|
-
TELEGRAM_BOT_TOKEN: this.agentConfig.telegram?.botToken ?? '',
|
|
736
866
|
GATEWAY_RESTART_SIGNAL_PATH: containerRestartPath,
|
|
737
867
|
};
|
|
738
868
|
if (process.env.GATEWAY_API_URL)
|
|
739
869
|
containerEnv.GATEWAY_API_URL = process.env.GATEWAY_API_URL;
|
|
740
|
-
|
|
870
|
+
// Secrets are forwarded by NAME only (`-e KEY`, no `=value`), which makes
|
|
871
|
+
// Docker read the value from this process's own environment (set on the spawn
|
|
872
|
+
// below). The value therefore never lands on the docker argv, where any local
|
|
873
|
+
// user could read it out of `ps`: /proc/<pid>/cmdline is world-readable on a
|
|
874
|
+
// stock kernel, whereas /proc/<pid>/environ is owner-only.
|
|
875
|
+
//
|
|
876
|
+
// TELEGRAM_BOT_TOKEN travels by name for exactly the same reason as the
|
|
877
|
+
// Claude credentials — it is a bearer token for the agent's whole bot
|
|
878
|
+
// account. It is already placed in the spawn env below, unconditionally.
|
|
879
|
+
const containerAuthEnv = isAppAgent ? this.resolveContainerAuthEnv() : {};
|
|
880
|
+
const byNameKeys = ['TELEGRAM_BOT_TOKEN', ...Object.keys(containerAuthEnv)];
|
|
881
|
+
const dockerEnvFlags = [
|
|
882
|
+
...Object.entries(containerEnv).flatMap(([k, v]) => ['-e', `${k}=${v}`]),
|
|
883
|
+
...byNameKeys.flatMap((k) => ['-e', k]),
|
|
884
|
+
];
|
|
741
885
|
const spawnArgs = isAppAgent
|
|
742
886
|
? [
|
|
743
887
|
'exec', '--workdir', '/workspace', '--user', String(containerUid), '-i',
|
|
@@ -761,6 +905,7 @@ class SessionProcess extends events_1.EventEmitter {
|
|
|
761
905
|
const proc = (0, child_process_1.spawn)(spawnBin, spawnArgs, {
|
|
762
906
|
env: {
|
|
763
907
|
...process.env,
|
|
908
|
+
...containerAuthEnv,
|
|
764
909
|
...(hardenedPath ? { PATH: hardenedPath } : {}),
|
|
765
910
|
CLAUDE_WORKSPACE: isAppAgent ? '/workspace' : this.agentConfig.workspace,
|
|
766
911
|
TELEGRAM_BOT_TOKEN: this.agentConfig.telegram?.botToken ?? '',
|