@sema-agent/server 7.102.0 → 7.103.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/MIGRATION.md +91 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/USAGE.md +107 -6
- package/dist/background-session-reap.d.ts +4 -1
- package/dist/bake-runner/main.d.ts +21 -3
- package/dist/bake-runner/main.js +18 -11
- package/dist/boot/budget-tracing.d.ts +1 -0
- package/dist/boot/budget-tracing.js +3 -2
- package/dist/boot/config-center.d.ts +12 -2
- package/dist/boot/config-center.js +33 -21
- package/dist/boot/stage-01-config.js +2 -0
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-04-stores.js +2 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +6 -1
- package/dist/brain.d.ts +60 -9
- package/dist/brain.js +44 -8
- package/dist/capabilities/center-plugins.d.ts +9 -3
- package/dist/capabilities/center-plugins.js +5 -10
- package/dist/config-catalog.js +9 -5
- package/dist/config-center/apply-effective.d.ts +40 -2
- package/dist/config-center/apply-effective.js +32 -12
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/mcp-revocation.d.ts +2 -2
- package/dist/config-center/mcp-revocation.js +1 -1
- package/dist/config-center/restart-signal.d.ts +39 -12
- package/dist/config-center/restart-signal.js +13 -24
- package/dist/config-types.d.ts +35 -17
- package/dist/config-types.js +3 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +43 -17
- package/dist/env-name-allowlist-knobs.d.ts +21 -7
- package/dist/env-name-allowlist-knobs.js +7 -2
- package/dist/fleet-client.d.ts +9 -3
- package/dist/fleet-client.js +2 -0
- package/dist/host-lsp-manager.d.ts +4 -3
- package/dist/host-lsp-manager.js +2 -6
- package/dist/http/dispatch.js +4 -0
- package/dist/http/server.d.ts +4 -0
- package/dist/leader/wire.d.ts +8 -1
- package/dist/leader/wire.js +4 -3
- package/dist/model-entry-refusal.d.ts +32 -9
- package/dist/model-entry-refusal.js +22 -10
- package/dist/model-route-endpoint.d.ts +10 -0
- package/dist/model-route-endpoint.js +6 -2
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +15 -2
- package/dist/outbound-dispatcher.d.ts +125 -0
- package/dist/outbound-dispatcher.js +211 -0
- package/dist/plugins/host-platform.d.ts +6 -1
- package/dist/plugins/remote-env-adb.d.ts +10 -3
- package/dist/plugins/remote-env-adb.js +24 -20
- package/dist/plugins/remote-env-host.js +3 -11
- package/dist/plugins/remote-env-k8s.d.ts +8 -1
- package/dist/plugins/remote-env-k8s.js +31 -24
- package/dist/plugins/remote-env-ssh.d.ts +8 -1
- package/dist/plugins/remote-env-ssh.js +24 -18
- package/dist/plugins/remote-shell.d.ts +109 -9
- package/dist/plugins/remote-shell.js +83 -0
- package/dist/project-memory.js +3 -6
- package/dist/run-local.js +2 -0
- package/dist/server-secret-env.d.ts +89 -24
- package/dist/server-secret-env.js +21 -1
- package/dist/trace/core-keyset-guard.d.ts +6 -1
- package/dist/trace/project.js +13 -0
- package/dist/trace/redact.d.ts +11 -0
- package/dist/trace/redact.js +1 -0
- package/package.json +4 -3
- package/skills/update-config.md +23 -5
|
@@ -4,7 +4,7 @@ import https from "node:https";
|
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import { StringDecoder } from "node:string_decoder";
|
|
6
6
|
import WebSocket from "ws";
|
|
7
|
-
import { shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
|
|
7
|
+
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode, numEnvOr } from "./remote-shell.js";
|
|
8
8
|
import { presignS3Url } from "./s3-presign.js";
|
|
9
9
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
10
10
|
import { fileErrorFromExec, classifyFsStderr } from "./remote-env-file-error.js";
|
|
@@ -479,6 +479,15 @@ export class RemoteK8sExecutionEnv {
|
|
|
479
479
|
return onAbort();
|
|
480
480
|
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
481
481
|
}
|
|
482
|
+
const takeStderr = (bytes) => {
|
|
483
|
+
if (bytes.length === 0)
|
|
484
|
+
return;
|
|
485
|
+
errBuf.push(bytes);
|
|
486
|
+
if (detachTee)
|
|
487
|
+
feedMemStream(detachTee.err, bytes, K8S_BG_MEM_CAP);
|
|
488
|
+
if (errDecoder)
|
|
489
|
+
opts.onStderr(errDecoder.write(bytes));
|
|
490
|
+
};
|
|
482
491
|
sock.on("message", (...args) => {
|
|
483
492
|
const data = args[0];
|
|
484
493
|
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
@@ -505,11 +514,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
505
514
|
opts.onStdout(outDecoder.write(payload));
|
|
506
515
|
}
|
|
507
516
|
else if (channel === 2) {
|
|
508
|
-
|
|
509
|
-
if (detachTee)
|
|
510
|
-
feedMemStream(detachTee.err, payload, K8S_BG_MEM_CAP);
|
|
511
|
-
if (errDecoder)
|
|
512
|
-
opts.onStderr(errDecoder.write(payload));
|
|
517
|
+
takeStderr(opts?.stderrFilter ? opts.stderrFilter.filterStderr(payload) : payload);
|
|
513
518
|
}
|
|
514
519
|
else if (channel === 3) {
|
|
515
520
|
statusFrame = payload.toString("utf8");
|
|
@@ -545,6 +550,8 @@ export class RemoteK8sExecutionEnv {
|
|
|
545
550
|
finish({ ok: false, error: new ExecutionError("spawn_error", parsed.error) });
|
|
546
551
|
return;
|
|
547
552
|
}
|
|
553
|
+
if (opts?.stderrFilter)
|
|
554
|
+
takeStderr(opts.stderrFilter.endStderr());
|
|
548
555
|
const err = errBuf.result();
|
|
549
556
|
const stderr = markTruncated(err.text, err.droppedBytes);
|
|
550
557
|
if (boundOutput) {
|
|
@@ -584,31 +591,38 @@ export class RemoteK8sExecutionEnv {
|
|
|
584
591
|
});
|
|
585
592
|
}
|
|
586
593
|
async exec(command, options) {
|
|
594
|
+
return this.execCall(createCwdGuardedCall("k8s", command, this.workingDirOf(options?.cwd), options?.env), options);
|
|
595
|
+
}
|
|
596
|
+
fsExec(command, abortSignal) {
|
|
597
|
+
return this.execCall(createCwdGuardedCall("k8s", command, undefined, undefined), { abortSignal });
|
|
598
|
+
}
|
|
599
|
+
async execCall(call, options) {
|
|
587
600
|
const c = await this.ensureConnected();
|
|
588
601
|
if (!c.ok)
|
|
589
602
|
return c;
|
|
590
603
|
const detachSignal = options?.detachSignal;
|
|
591
604
|
this.inFlightExecs++;
|
|
592
605
|
try {
|
|
593
|
-
const composed = this.withCwdEnv(command, options?.cwd, options?.env);
|
|
594
606
|
if (!detachSignal) {
|
|
595
|
-
return await this.runExec(
|
|
607
|
+
return call.settle(await this.runExec(call.command, {
|
|
596
608
|
signal: options?.abortSignal,
|
|
597
609
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
|
|
598
610
|
boundOutput: true,
|
|
599
611
|
onStdout: options?.onStdout,
|
|
600
612
|
onStderr: options?.onStderr,
|
|
601
|
-
|
|
613
|
+
stderrFilter: call,
|
|
614
|
+
}));
|
|
602
615
|
}
|
|
603
616
|
const adoptId = randomUUID();
|
|
604
617
|
const dir = `${K8S_BG_DIR_ROOT}/exec-${adoptId}`;
|
|
605
618
|
const tee = { out: seedMemStream(""), err: seedMemStream("") };
|
|
606
|
-
return await this.runExec(buildDetachCapableExec(dir,
|
|
619
|
+
return call.settle(await this.runExec(buildDetachCapableExec(dir, call.command), {
|
|
607
620
|
signal: options?.abortSignal,
|
|
608
621
|
timeoutMs: options?.timeout != null ? options.timeout * 1000 : undefined,
|
|
609
622
|
boundOutput: true,
|
|
610
623
|
onStdout: options?.onStdout,
|
|
611
624
|
onStderr: options?.onStderr,
|
|
625
|
+
stderrFilter: call,
|
|
612
626
|
detach: {
|
|
613
627
|
signal: detachSignal,
|
|
614
628
|
tee,
|
|
@@ -624,7 +638,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
624
638
|
return adopted?.shellId;
|
|
625
639
|
},
|
|
626
640
|
},
|
|
627
|
-
});
|
|
641
|
+
}));
|
|
628
642
|
}
|
|
629
643
|
finally {
|
|
630
644
|
this.inFlightExecs--;
|
|
@@ -665,7 +679,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
665
679
|
let bytes = 0;
|
|
666
680
|
const ac = new AbortController();
|
|
667
681
|
const signal = options?.signal ? AbortSignal.any([options.signal, ac.signal]) : ac.signal;
|
|
668
|
-
const done = this.runExec(
|
|
682
|
+
const done = this.runExec(buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command, {
|
|
669
683
|
signal,
|
|
670
684
|
boundOutput: true,
|
|
671
685
|
onStdout: (d) => {
|
|
@@ -752,7 +766,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
752
766
|
throw new Error(`pod connect failed: ${conn.error.message}`);
|
|
753
767
|
const jobId = randomUUID();
|
|
754
768
|
const dir = `${K8S_BG_DIR_ROOT}/${jobId}`;
|
|
755
|
-
const inner =
|
|
769
|
+
const inner = buildCwdEnvCommand(command, self.workingDirOf(options?.cwd), options?.env, "raw").command;
|
|
756
770
|
const cmdB64 = Buffer.from(inner, "utf8").toString("base64");
|
|
757
771
|
const runnerB64 = Buffer.from(buildBgRunnerScript(dir), "utf8").toString("base64");
|
|
758
772
|
const r = await bgExec(buildBgLauncherScript(dir, cmdB64, runnerB64), true);
|
|
@@ -868,7 +882,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
868
882
|
const dir = path.posix.dirname(abs);
|
|
869
883
|
if (!dir || dir === "." || dir === "/")
|
|
870
884
|
return undefined;
|
|
871
|
-
const mk = await this.
|
|
885
|
+
const mk = await this.fsExec(`mkdir -p ${shellQuote(dir)}`, abortSignal);
|
|
872
886
|
if (!mk.ok)
|
|
873
887
|
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
874
888
|
if (mk.value.exitCode !== 0)
|
|
@@ -905,7 +919,7 @@ export class RemoteK8sExecutionEnv {
|
|
|
905
919
|
posixFsInst;
|
|
906
920
|
get posixFs() {
|
|
907
921
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
908
|
-
run: (cmd, abortSignal) => this.
|
|
922
|
+
run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
|
|
909
923
|
resolve: (p) => this.resolve(p),
|
|
910
924
|
tempDir: () => this.cwd,
|
|
911
925
|
}));
|
|
@@ -946,16 +960,9 @@ export class RemoteK8sExecutionEnv {
|
|
|
946
960
|
return c;
|
|
947
961
|
return this.runExec(command, { signal: abortSignal, timeoutMs: this.cfg.dataTimeoutMs });
|
|
948
962
|
}
|
|
949
|
-
|
|
950
|
-
const parts = [];
|
|
963
|
+
workingDirOf(cwd) {
|
|
951
964
|
const dir = cwd ?? this.cwd;
|
|
952
|
-
|
|
953
|
-
parts.push(`cd ${shellQuote(this.resolve(dir))} &&`);
|
|
954
|
-
if (env)
|
|
955
|
-
for (const [k, v] of Object.entries(env))
|
|
956
|
-
parts.push(`export ${k}=${shellQuote(v)};`);
|
|
957
|
-
parts.push(command);
|
|
958
|
-
return parts.join(" ");
|
|
965
|
+
return dir ? this.resolve(dir) : undefined;
|
|
959
966
|
}
|
|
960
967
|
}
|
|
961
968
|
export function k8sExecutionEnvFactory(config) {
|
|
@@ -154,6 +154,11 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
154
154
|
stderr: string;
|
|
155
155
|
exitCode: number;
|
|
156
156
|
}, ExecutionError>>;
|
|
157
|
+
/** 文件面探针的 exec(车PL 验修 ③):只用绝对路径,**不进工作目录**、没有 cwd 守卫 —— 工作目录进不去时探针照样答真话(`permission_denied` /
|
|
158
|
+
* `not_found` 由命令自己的 stderr 归),core 的 `isDeadCwd` 不会把「进不去」读成「没了」。 */
|
|
159
|
+
private fsExec;
|
|
160
|
+
/** buffered exec 的唯一体:`call` 定了整串、stderr 的 started 行剥法与收口(`remote-shell.ts` `createCwdGuardedCall`)。 */
|
|
161
|
+
private execCall;
|
|
157
162
|
execStream(command: string, options?: ExecStreamOptions): AsyncIterable<OutputChunk>;
|
|
158
163
|
private sftp;
|
|
159
164
|
absolutePath(p: string): Promise<Result<string, FileError>>;
|
|
@@ -192,7 +197,9 @@ export declare class RemoteSshExecutionEnv implements RemoteExecutionEnv {
|
|
|
192
197
|
suffix?: string;
|
|
193
198
|
abortSignal?: AbortSignal;
|
|
194
199
|
}): Promise<Result<string, FileError>>;
|
|
195
|
-
|
|
200
|
+
/** 本腿只定工作目录(缺省 cwd 规则 + 绝对化);串的形状归 {@link buildCwdEnvCommand} / {@link createCwdGuardedCall}(S-410:cwd 不可用 ⇒ 命令不跑;
|
|
201
|
+
* 车PL:buffered `exec` 上守卫触发 ⇒ `spawn_error`)。 */
|
|
202
|
+
private workingDirOf;
|
|
196
203
|
private connErr;
|
|
197
204
|
private fileError;
|
|
198
205
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Client } from "ssh2";
|
|
3
3
|
import { createSshHostKeyVerifier } from "../ssh-host-key.js";
|
|
4
|
-
import { shellQuote, kindFromMode } from "./remote-shell.js";
|
|
4
|
+
import { buildCwdEnvCommand, createCwdGuardedCall, shellQuote, kindFromMode } from "./remote-shell.js";
|
|
5
5
|
import { signalNumber } from "./host-platform.js";
|
|
6
6
|
import { createPosixShellFs } from "./posix-shell-fs.js";
|
|
7
7
|
import { FileError, ExecutionError, RemoteExecutionError, withRetry, RollingTailBuffer, markTruncated, } from "@sema-agent/core";
|
|
@@ -234,6 +234,12 @@ export class RemoteSshExecutionEnv {
|
|
|
234
234
|
return this.destroy();
|
|
235
235
|
}
|
|
236
236
|
async exec(command, options) {
|
|
237
|
+
return this.execCall(createCwdGuardedCall("ssh", command, this.workingDirOf(options?.cwd), options?.env), options);
|
|
238
|
+
}
|
|
239
|
+
fsExec(command, abortSignal) {
|
|
240
|
+
return this.execCall(createCwdGuardedCall("ssh", command, undefined, undefined), { abortSignal });
|
|
241
|
+
}
|
|
242
|
+
async execCall(call, options) {
|
|
237
243
|
const ready = await this.ensureConnected(options?.abortSignal);
|
|
238
244
|
if (!ready.ok)
|
|
239
245
|
return ready;
|
|
@@ -269,8 +275,7 @@ export class RemoteSshExecutionEnv {
|
|
|
269
275
|
return onAbort();
|
|
270
276
|
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
271
277
|
}
|
|
272
|
-
|
|
273
|
-
conn.exec(full, (err, stream) => {
|
|
278
|
+
conn.exec(call.command, (err, stream) => {
|
|
274
279
|
if (err)
|
|
275
280
|
return finish({ ok: false, error: new ExecutionError("spawn_error", `ssh exec failed: ${err.message}`, err) });
|
|
276
281
|
streamRef = stream;
|
|
@@ -291,14 +296,22 @@ export class RemoteSshExecutionEnv {
|
|
|
291
296
|
if (code == null && !signal) {
|
|
292
297
|
return finish({ ok: false, error: new ExecutionError("transport_lost", "ssh channel closed without exit status (transport lost / connection dropped)") });
|
|
293
298
|
}
|
|
299
|
+
const tail = call.endStderr();
|
|
300
|
+
if (tail.length > 0) {
|
|
301
|
+
errBuf.push(tail);
|
|
302
|
+
options?.onStderr?.(tail.toString());
|
|
303
|
+
}
|
|
294
304
|
const out = outBuf.result();
|
|
295
305
|
const err = errBuf.result();
|
|
296
306
|
const exitCode = code ?? (signal ? exitCodeForSignalKill(signal) : 1);
|
|
297
|
-
finish(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode }));
|
|
307
|
+
finish(call.settle(ok({ stdout: markTruncated(out.text, out.droppedBytes), stderr: markTruncated(err.text, err.droppedBytes), exitCode })));
|
|
298
308
|
})
|
|
299
309
|
.stderr.on("data", (d) => {
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
const c = call.filterStderr(d);
|
|
311
|
+
if (c.length === 0)
|
|
312
|
+
return;
|
|
313
|
+
errBuf.push(c);
|
|
314
|
+
options?.onStderr?.(c.toString());
|
|
302
315
|
});
|
|
303
316
|
});
|
|
304
317
|
});
|
|
@@ -347,7 +360,7 @@ export class RemoteSshExecutionEnv {
|
|
|
347
360
|
catch {
|
|
348
361
|
}
|
|
349
362
|
};
|
|
350
|
-
const full =
|
|
363
|
+
const full = buildCwdEnvCommand(command, this.workingDirOf(options?.cwd), options?.env, "raw").command;
|
|
351
364
|
conn.exec(full, (err, stream) => {
|
|
352
365
|
if (err) {
|
|
353
366
|
failure = new RemoteExecutionError("unknown", `ssh exec failed: ${err.message}`, err);
|
|
@@ -480,7 +493,7 @@ export class RemoteSshExecutionEnv {
|
|
|
480
493
|
const abs = this.resolve(p);
|
|
481
494
|
const dir = path.posix.dirname(abs);
|
|
482
495
|
if (dir && dir !== "." && dir !== "/") {
|
|
483
|
-
const mk = await this.
|
|
496
|
+
const mk = await this.fsExec(`mkdir -p -- ${shellQuote(dir)}`, abortSignal);
|
|
484
497
|
if (!mk.ok)
|
|
485
498
|
return { ok: false, error: fileErrorFromExec(mk.error, dir) };
|
|
486
499
|
if (mk.value.exitCode !== 0)
|
|
@@ -540,7 +553,7 @@ export class RemoteSshExecutionEnv {
|
|
|
540
553
|
posixFsInst;
|
|
541
554
|
get posixFs() {
|
|
542
555
|
return (this.posixFsInst ??= createPosixShellFs({
|
|
543
|
-
run: (cmd, abortSignal) => this.
|
|
556
|
+
run: (cmd, abortSignal) => this.fsExec(cmd, abortSignal),
|
|
544
557
|
resolve: (p) => this.resolve(p),
|
|
545
558
|
tempDir: () => null,
|
|
546
559
|
}));
|
|
@@ -574,16 +587,9 @@ export class RemoteSshExecutionEnv {
|
|
|
574
587
|
async createTempFile(options) {
|
|
575
588
|
return this.posixFs.createTempFile(options);
|
|
576
589
|
}
|
|
577
|
-
|
|
578
|
-
const parts = [];
|
|
590
|
+
workingDirOf(cwd) {
|
|
579
591
|
const dir = cwd ?? (this.cwd !== "." ? this.cwd : undefined);
|
|
580
|
-
|
|
581
|
-
parts.push(`cd ${shellQuote(this.resolve(dir))} &&`);
|
|
582
|
-
if (env)
|
|
583
|
-
for (const [k, v] of Object.entries(env))
|
|
584
|
-
parts.push(`export ${k}=${shellQuote(v)};`);
|
|
585
|
-
parts.push(command);
|
|
586
|
-
return parts.join(" ");
|
|
592
|
+
return dir ? this.resolve(dir) : undefined;
|
|
587
593
|
}
|
|
588
594
|
connErr(e) {
|
|
589
595
|
const msg = e.message || String(e);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared shell/path helpers for the SSH + ADB `RemoteExecutionEnv` adapters. Kept in ONE place so the
|
|
3
3
|
* security-sensitive quoting can't drift between adapters (council design/#4, #5).
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
5
|
+
import { ExecutionError, type FileError, type FileInfo, type RemoteExecutionError, type Result, type SymlinkChain } from "@sema-agent/core";
|
|
6
6
|
import type { ChildProcess } from "node:child_process";
|
|
7
7
|
/**
|
|
8
8
|
* D5 pipe-destroy grace (parity with core's vendored `EXEC_FORCE_SETTLE_GRACE_MS`, harness/env/nodejs.js):
|
|
@@ -25,6 +25,108 @@ export declare function armPipeDestroyGrace(child: ChildProcess): ReturnType<typ
|
|
|
25
25
|
/** POSIX single-quote a string for safe interpolation into a remote shell command (the only metacharacter
|
|
26
26
|
* inside '' is ' itself, closed/escaped/reopened). */
|
|
27
27
|
export declare function shellQuote(s: string): string;
|
|
28
|
+
/** S-410:cwd 守卫在**远端**的退出码 —— 包装层自己失败、命令没跑(POSIX 包装工具 `env(1)` / `nohup(1)` / `timeout(1)` 的惯用码;126 / 127 是 shell
|
|
29
|
+
* 自己的「不可执行 / 找不到」)。buffered `exec` 经 {@link createCwdGuardedCall} 把它收成 `spawn_error`(车PL);命令自己也可能退 125,判别见那只函数。 */
|
|
30
|
+
export declare const CWD_UNAVAILABLE_EXIT = 125;
|
|
31
|
+
/** S-410:cwd 守卫触发时写进 stderr 的那一行的开头(后接 ` [<nonce>]: <目录> (cd failed; command not run)`),也是 `spawn_error` 消息的主句。 */
|
|
32
|
+
export declare const CWD_UNAVAILABLE_LINE = "sema: working directory unavailable";
|
|
33
|
+
/** 车PL 验修 ②:settled 面上 cd 成功之后、命令之前打到 stderr 的那一行的开头(后接 ` [<同一 nonce>]`)。腿在交出之前把它剥掉。 */
|
|
34
|
+
export declare const CWD_STARTED_LINE = "sema: command started";
|
|
35
|
+
/** 一次调用的 cwd 守卫:被守卫的目录 + 本次调用的两只标记(同一个 nonce)。 */
|
|
36
|
+
export interface CwdGuard {
|
|
37
|
+
/** 被守卫的目录(腿已绝对化)。 */
|
|
38
|
+
readonly dir: string;
|
|
39
|
+
/** `sema: working directory unavailable [<nonce>]` —— 守卫行以它开头。nonce = 每次调用现铸的 8 字节随机数(16 位十六进制)。
|
|
40
|
+
* 🔴 nonce **对命令不保密**:命令能从 `BASH_EXECUTION_STRING` / `/proc/self/cmdline` 读到整串(验修 codex ②)⇒ 守卫行可以被伪造,
|
|
41
|
+
* 判据因此不单靠它(见 {@link CwdGuard.started})。 */
|
|
42
|
+
readonly marker: string;
|
|
43
|
+
/** settled 面:cd 成功之后、命令之前 `echo` 到 stderr 的那一整行 `sema: command started [<同一 nonce>]`;raw 面缺席(见 {@link CwdGuardFace})。
|
|
44
|
+
* 它在命令开跑**之前**就进了流,命令收不回它 ⇒「见到这一行 = 命令跑过了」,伪造的守卫行不再被认。 */
|
|
45
|
+
readonly started: string | undefined;
|
|
46
|
+
}
|
|
47
|
+
/** {@link buildCwdEnvCommand} 的产物(纯数据):交给远端 `sh -c` 的整串 + 本次调用的 cwd 守卫(没给 `dir` ⇒ 缺席)。 */
|
|
48
|
+
export interface CwdEnvCommand {
|
|
49
|
+
readonly command: string;
|
|
50
|
+
readonly cwdGuard: CwdGuard | undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 串是给哪一面用的(闭集,两词):
|
|
54
|
+
* · `settled` —— buffered `exec`:结果要经 {@link createCwdGuardedCall} 判「命令跑没跑」,所以守卫之后多打一行 started 标记,腿再把它剥掉;
|
|
55
|
+
* · `raw` —— `execStream` 与后台 launch:不判(货币是 `RemoteExecutionError` / 落盘的 job 输出),守卫行 + 125 原样交出,**不打** started 行 ——
|
|
56
|
+
* 打了就得在流式的三处与后台的落盘文件里各剥一次,而这两面没有要它的判据。
|
|
57
|
+
*/
|
|
58
|
+
export type CwdGuardFace = "settled" | "raw";
|
|
59
|
+
/**
|
|
60
|
+
* 🔴 S-410(7.103.1)—— 三条远端 shell 腿(ssh / k8s / adb)交给远端 `sh -c` 的那一串**唯一**的组装:进工作目录 →(settled 面)打 started 行 →
|
|
61
|
+
* 导出任务 env → 跑命令。各腿只决定 `dir`(自己的缺省 cwd 规则 + 路径绝对化),串的形状只在这里。
|
|
62
|
+
*
|
|
63
|
+
* 形:`cd '<dir>' || { echo '<marker>: <dir> (cd failed; command not run)' >&2; exit 125; }; echo '<started>' >&2; export K='v'; … <command>`
|
|
64
|
+
* (raw 面没有 `echo '<started>' >&2;` 那一段)
|
|
65
|
+
* · cd 失败(目录不存在 / 无权限 / 不是目录)⇒ 命令**不跑**,整条退 {@link CWD_UNAVAILABLE_EXIT};stderr 先是 shell 自己的 cd 报错,再是守卫行。
|
|
66
|
+
* 修前形 `cd '<dir>' && export K='v'; <command>`:`&&` 只绑到第一只 `export`,`;` 之后的命令照样在**登录目录**里跑,还少了那只 env ——
|
|
67
|
+
* 执行面 fail-open([ref])。core 的 Bash 工具每次都带 `env:{PWD}`,所以修前**每一条**模型命令都是这一形;没有 env 时修前形是
|
|
68
|
+
* `cd … && <command>`,只短路命令的第一段 —— `a; b`、`a || b`、多行命令的尾部照样在登录目录里跑。本形对两者一律不跑(钉:
|
|
69
|
+
* `test/remote-cwd-env-compose.test.ts`「本机 sh 真跑」格的复合命令半)。
|
|
70
|
+
* · 为什么是前缀守卫,不是 `cd … && { export …; <command>; }`:命令串原样留在**最后**,与修前同位。包进 `{ …; }` 会让命令里的尾随注释
|
|
71
|
+
* (`ls # x` 把 `; }` 注释掉)与 heredoc(终止行后面多出 `; }`)改义或语法错。cd 成功时,命令、env、退出码与修前逐字同义;stderr 在 settled 面
|
|
72
|
+
* 多出的 started 行由腿剥掉({@link createCwdGuardedCall}),交出去的 stderr 逐字是命令自己的。
|
|
73
|
+
* · 两行都用 `echo`,不用 `printf`:`echo` 在每一只 POSIX shell 里都是内建(dash / bash / busybox ash / mksh / toybox sh),早期 Android 的
|
|
74
|
+
* toolbox 可能没有 `printf` 可执行件(未实机核)—— 真没有的话,用 `printf` 那种设备上守卫行出不来、腿认不出守卫,命令照样不跑但退回「命令自己退 125」的形。
|
|
75
|
+
* `echo` 在 dash 里会解释参数里的反斜杠转义,只动得了守卫行的目录那一段:判别只读行首的 marker(无反斜杠),消息里的目录取 {@link CwdGuard.dir};
|
|
76
|
+
* started 行里没有目录、没有反斜杠。
|
|
77
|
+
* · export 规则逐字照旧。
|
|
78
|
+
* - 值经 {@link shellQuote}(POSIX 单引号,里面只有 `'` 自己要转义)⇒ 命令读到的是原值,不展开、不分词。
|
|
79
|
+
* - 键原样拼:键与 `command` 出自同一个调用方(引擎交来的 `ExecOpts.env`),而 `command` 本身就是任意 shell 文本、以同一身份跑 ⇒
|
|
80
|
+
* 不引用键不多给任何能力。引用了反而改变「非法键名 ⇒ export 报错」的现状。
|
|
81
|
+
* · ⚠️ 边界:需要远端是 POSIX sh(`{ …; }` 组命令)。k8s 走 `sh -c`、adb 走设备 `sh`;**ssh 腿走远端账户登录 shell 的 `-c`** —— 登录 shell 是
|
|
82
|
+
* csh / tcsh(fish 看版本)的账户不认 `{ …; }`,整条语法错、命令不跑(修前无 env 的 `cd 'X' && cmd` 在 csh 里能跑)。部署前提写在
|
|
83
|
+
* `docs/DEPLOY-PREREQS.md`「ssh / k8s / adb 车道:远端工作目录不可用」节。
|
|
84
|
+
* `dir` 缺席 ⇒ 不进目录、没有守卫、没有 started 行,只有 export + 命令(与修前同)。
|
|
85
|
+
*/
|
|
86
|
+
export declare function buildCwdEnvCommand(command: string, dir: string | undefined, env: Readonly<Record<string, string>> | undefined, face: CwdGuardFace): CwdEnvCommand;
|
|
87
|
+
/** 经 {@link buildCwdEnvCommand} 组串的三条远端 shell 腿(`spawn_error` 消息的腿名,与 host 腿的 `host spawn failed: …` 同一句族)。 */
|
|
88
|
+
export type RemoteShellLeg = "ssh" | "k8s" | "adb";
|
|
89
|
+
/** 远端 stderr 流过的那一半(各腿的传输层按原始字节喂):剥掉本次调用的 started 行(只那一行),其余逐字交出。 */
|
|
90
|
+
export interface CwdStartedStderrFilter {
|
|
91
|
+
/** 一块原始字节 → 该交给下游(缓冲 / 回调 / tee)的字节。 */
|
|
92
|
+
filterStderr(chunk: Buffer): Buffer;
|
|
93
|
+
/** 流结束:交出还压着的字节(started 行之前、还没收到换行的那一截)。 */
|
|
94
|
+
endStderr(): Buffer;
|
|
95
|
+
}
|
|
96
|
+
/** 远端三腿 buffered `exec` 的一次调用(活对象:stderr 流过它,结果经它收口)。 */
|
|
97
|
+
export interface CwdGuardedCall extends CwdStartedStderrFilter {
|
|
98
|
+
/** 交给远端 `sh -c` 的整串(settled 面)。 */
|
|
99
|
+
readonly command: string;
|
|
100
|
+
/** 收口:退 125 ∧ stderr 有以本次 marker 开头的一行 ∧ **没见到** started 行 ⇒ `spawn_error`;其余原样。 */
|
|
101
|
+
settle<T extends {
|
|
102
|
+
readonly stderr: string;
|
|
103
|
+
readonly exitCode: number;
|
|
104
|
+
}>(r: Result<T, ExecutionError>): Result<T, ExecutionError>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 🔴 车PL(7.103.1 验修轮 A [high] + 验修 codex ②)—— 三条远端 shell 腿 buffered `exec` 的**唯一**协议:组串(settled 面)+ 剥 started 行 + 收口。
|
|
108
|
+
* 守卫触发 ⇒ 与 host 腿同形的 `ExecutionError("spawn_error", "<腿> spawn failed: sema: working directory unavailable: <目录> — the command did not run
|
|
109
|
+
* (<cd 自己的话>)")`。
|
|
110
|
+
*
|
|
111
|
+
* 为什么是 spawn_error:core 的 ExecutionEnv 契约把「命令没跑(死 cwd 在列)」给了它;core 的 Bash 工具只在 `spawn_error` / `shell_unavailable` 上
|
|
112
|
+
* 走死 cwd 诊断(`diagnoseDeadCwd`:目录真没了 ⇒ 复位到任务根、答「reset to <root>; re-run」)。S-410 初版把同一形报成 `{ ok:true, exitCode:125 }`,
|
|
113
|
+
* core 读作命令自己失败 ⇒ cwd 不复位,前缀又挡在模型自救的 `cd` / `mkdir` 前面 ⇒ 本轮后续 Bash 全退 125。host 腿(`remote-env-host.ts` 的
|
|
114
|
+
* `child.on("error")`)本就报 `spawn_error`,四条腿一条规则。
|
|
115
|
+
*
|
|
116
|
+
* 判据(一条规则,缺一不可):`r.ok` ∧ 退出码 = {@link CWD_UNAVAILABLE_EXIT} ∧ stderr 有一行以本次 {@link CwdGuard.marker} 开头 ∧ **流里没出现过**
|
|
117
|
+
* 本次 {@link CwdGuard.started} 那一整行。started 行在 cd 成功之后、命令之前写进流,命令收不回它 ⇒ 命令读到 nonce 伪造守卫行再退 125,照样按
|
|
118
|
+
* 「命令跑过了」交出(验修 codex ②:修前这一形被读成「没跑」⇒ 输出丢、调用方可能重试一个非幂等操作)。消息里的目录取 {@link CwdGuard.dir}
|
|
119
|
+
* (不从 stderr 抠),括号里是守卫行之前 shell 自己的 cd 报错(无则 `cd failed`)。
|
|
120
|
+
*
|
|
121
|
+
* started 行的剥法:按原始字节逐行扫,**第一处**与 started 行逐字节相等的整行剥掉(连同换行),其后的字节原样直通;之前的行(ssh 登录 shell 可能
|
|
122
|
+
* 先说几句)原样交出、只压着还没收到换行的那一截。腿在三处都用它:缓冲的 stderr、流式回调、k8s detach 的 tee ⇒ 交出去的 stderr 逐字是命令自己的。
|
|
123
|
+
* 没有守卫(没给 `dir`,如文件面探针)⇒ 直通、收口不判。
|
|
124
|
+
*
|
|
125
|
+
* 辖域 = buffered `exec`(core Bash 工具、git 探针走它;文件面探针也走同一只 exec 体但**不带 cwd**,见各腿)。流式 `execStream` 与后台 launch 用
|
|
126
|
+
* `raw` 面:照旧交出 125 + 守卫行 —— 两面的货币是 `RemoteExecutionError`(没有「命令没跑」这个词),消费方(core exec-gate、leader `pullDiff`)
|
|
127
|
+
* 把任何非 0 退出当失败并带出 stderr,而 core 的后台 Bash 在 launch 前自己先做死 cwd 检查(`fs-bash.js` `run_in_background` 分支)。
|
|
128
|
+
*/
|
|
129
|
+
export declare function createCwdGuardedCall(leg: RemoteShellLeg, command: string, dir: string | undefined, env: Readonly<Record<string, string>> | undefined): CwdGuardedCall;
|
|
28
130
|
/** Derive FileInfo kind from POSIX st_mode bits (raw stat/readdir attrs carry no isDirectory() helper). */
|
|
29
131
|
export declare function kindFromMode(mode?: number): FileInfo["kind"];
|
|
30
132
|
export declare function drainNumEnvWarnings(): Array<{
|
|
@@ -43,17 +145,16 @@ export declare const SYMLINK_CHAIN_MAX_HOPS = 64;
|
|
|
43
145
|
* `unknown` 而不是 `not_found`),于是调用方拿到的是 `ok:false`(core 读作「这一次没有跳」,永不是拒绝)
|
|
44
146
|
* 而不是一个伪造的空链。
|
|
45
147
|
*
|
|
46
|
-
* 🔴 **整条命令包在一个 `{ …; }`
|
|
47
|
-
*
|
|
48
|
-
*
|
|
148
|
+
* 🔴 **整条命令包在一个 `{ …; }` 里**,让它**只有一个退出状态**,不依赖调用方加的前缀是什么形。今天三条远端 shell 腿的文件面探针
|
|
149
|
+
* (本命令经 `PosixShellFs` 发出)**不带 cwd**(车PL 验修 ③:只用绝对路径,不进目录 ⇒ 前面没有 {@link buildCwdEnvCommand} 的守卫前缀);
|
|
150
|
+
* local-docker 用 `docker exec -w <dir>`,e2b 走 SDK 的 `cwd`,都不拼前缀。S-410 之前三条远端腿给它拼过 `cd <dir> && <命令>`,在那个形下
|
|
151
|
+
* 这里连着犯过两次错,记下来是因为它们是**同一个坑的两面**,换任何一种链式前缀都会再来:
|
|
49
152
|
* · 写成 `command -v … || { …; }`:`||` 与前缀的 `&&` 结成一条链(`cd X && command -v … || { … }`),
|
|
50
153
|
* **`cd` 失败时跑的是探测的失败臂** —— 报「readlink 不在」而真相是「那个目录没了」,运维去修一个没坏的东西;
|
|
51
154
|
* · 改成独立语句 `if ! …; fi; c=…; while …`:前缀的 `&&` 只短路**那一条 `if`**,后面的循环照跑 ——
|
|
52
155
|
* 于是「`cd` 失败 ∧ readlink 缺失」这一格里,`|| break` 把工具缺失吞成零跳,脚本 **exit 0 + 空 stdout**,
|
|
53
156
|
* 调用方拿到一条**伪造的空链**。这正是探测本来要挡的那个静默兜底([ref] 轴),换了个入口回来了。
|
|
54
|
-
*
|
|
55
|
-
* `cd` 失败 ⇒ 退出码来自 `cd` 自己、stderr 是它自己的话 ⇒ `ok:false`(core 读作「这一次没有跳」),
|
|
56
|
-
* 诚实且指向真因;`cd` 成功 ⇒ 探测与循环照旧。`{ }` 不是子 shell,里面的 `exit 127` 照样结束整条命令。
|
|
157
|
+
* `{ }` 不是子 shell,里面的 `exit 127` 照样结束整条命令。
|
|
57
158
|
*
|
|
58
159
|
* 词法细节(逐条对着 core 的 Node 实现写,两边同形):绝对目标直接接上;相对目标用参数展开
|
|
59
160
|
* `${c%/*}` 取**当前这条链接**所在目录(不调 `dirname` —— toybox 的 `dirname` 不认 `--`,且这里零外部命令
|
|
@@ -101,8 +202,7 @@ export type ShellChainRun = (cmd: string) => Promise<Result<{
|
|
|
101
202
|
* 「叶不是软链」与「叶不存在」**都是 exit 0 + 空 stdout**(脚本里的 `|| break`),所以任何 exit≠0
|
|
102
203
|
* 都**不是**「这个文件不在」,而是解析器自己出了事(工具缺失 / cwd 没了 / 传输坏 / shell 拒)。
|
|
103
204
|
* 把它折成 `not_found` 会让运维去找一条根本没丢的路径。
|
|
104
|
-
*
|
|
105
|
-
* 归 `not_found` —— 那时真的有一条路径没了(工作目录),所以那个码是对的。
|
|
205
|
+
* 三条远端腿的文件面探针不带 cwd(车PL 验修 ③)⇒ 这条命令不会因为某个工作目录不可用而不跑。
|
|
106
206
|
*/
|
|
107
207
|
export declare function runSymlinkChain(absPath: string, run: ShellChainRun): Promise<Result<SymlinkChain, FileError>>;
|
|
108
208
|
//# sourceMappingURL=remote-shell.d.ts.map
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { ExecutionError } from "@sema-agent/core";
|
|
1
2
|
import { classifyFsStderr, fileErrorFromExec } from "./remote-env-file-error.js";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
2
4
|
export const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
|
|
3
5
|
export function armPipeDestroyGrace(child) {
|
|
4
6
|
const t = setTimeout(() => {
|
|
@@ -15,6 +17,87 @@ export function armPipeDestroyGrace(child) {
|
|
|
15
17
|
export function shellQuote(s) {
|
|
16
18
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
17
19
|
}
|
|
20
|
+
export const CWD_UNAVAILABLE_EXIT = 125;
|
|
21
|
+
export const CWD_UNAVAILABLE_LINE = "sema: working directory unavailable";
|
|
22
|
+
export const CWD_STARTED_LINE = "sema: command started";
|
|
23
|
+
export function buildCwdEnvCommand(command, dir, env, face) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
let cwdGuard;
|
|
26
|
+
if (dir !== undefined && dir !== "") {
|
|
27
|
+
const nonce = randomBytes(8).toString("hex");
|
|
28
|
+
cwdGuard = { dir, marker: `${CWD_UNAVAILABLE_LINE} [${nonce}]`, started: startedLineFor(face, nonce) };
|
|
29
|
+
const line = `${cwdGuard.marker}: ${dir} (cd failed; command not run)`;
|
|
30
|
+
parts.push(`cd ${shellQuote(dir)} || { echo ${shellQuote(line)} >&2; exit ${CWD_UNAVAILABLE_EXIT}; };`);
|
|
31
|
+
if (cwdGuard.started !== undefined)
|
|
32
|
+
parts.push(`echo ${shellQuote(cwdGuard.started)} >&2;`);
|
|
33
|
+
}
|
|
34
|
+
if (env)
|
|
35
|
+
for (const [k, v] of Object.entries(env))
|
|
36
|
+
parts.push(`export ${k}=${shellQuote(v)};`);
|
|
37
|
+
parts.push(command);
|
|
38
|
+
return { command: parts.join(" "), cwdGuard };
|
|
39
|
+
}
|
|
40
|
+
function startedLineFor(face, nonce) {
|
|
41
|
+
switch (face) {
|
|
42
|
+
case "settled":
|
|
43
|
+
return `${CWD_STARTED_LINE} [${nonce}]`;
|
|
44
|
+
case "raw":
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const STARTED_SCAN_CAP_BYTES = 64 * 1024;
|
|
49
|
+
const EMPTY = Buffer.alloc(0);
|
|
50
|
+
export function createCwdGuardedCall(leg, command, dir, env) {
|
|
51
|
+
const sent = buildCwdEnvCommand(command, dir, env, "settled");
|
|
52
|
+
const guard = sent.cwdGuard;
|
|
53
|
+
const startedBytes = guard?.started === undefined ? undefined : Buffer.from(guard.started, "utf8");
|
|
54
|
+
let pending = EMPTY;
|
|
55
|
+
let scanning = startedBytes !== undefined;
|
|
56
|
+
let sawStarted = false;
|
|
57
|
+
return {
|
|
58
|
+
command: sent.command,
|
|
59
|
+
filterStderr(chunk) {
|
|
60
|
+
if (!scanning || startedBytes === undefined)
|
|
61
|
+
return chunk;
|
|
62
|
+
let rest = pending.length === 0 ? chunk : Buffer.concat([pending, chunk]);
|
|
63
|
+
const out = [];
|
|
64
|
+
for (let nl = rest.indexOf(0x0a); nl >= 0; nl = rest.indexOf(0x0a)) {
|
|
65
|
+
if (rest.subarray(0, nl).equals(startedBytes)) {
|
|
66
|
+
sawStarted = true;
|
|
67
|
+
scanning = false;
|
|
68
|
+
out.push(rest.subarray(nl + 1));
|
|
69
|
+
rest = EMPTY;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
out.push(rest.subarray(0, nl + 1));
|
|
73
|
+
rest = rest.subarray(nl + 1);
|
|
74
|
+
}
|
|
75
|
+
if (scanning && rest.length > STARTED_SCAN_CAP_BYTES) {
|
|
76
|
+
scanning = false;
|
|
77
|
+
out.push(rest);
|
|
78
|
+
rest = EMPTY;
|
|
79
|
+
}
|
|
80
|
+
pending = rest;
|
|
81
|
+
return out.length === 1 ? out[0] : Buffer.concat(out);
|
|
82
|
+
},
|
|
83
|
+
endStderr() {
|
|
84
|
+
const p = pending;
|
|
85
|
+
pending = EMPTY;
|
|
86
|
+
scanning = false;
|
|
87
|
+
return p;
|
|
88
|
+
},
|
|
89
|
+
settle(r) {
|
|
90
|
+
if (!r.ok || guard === undefined || sawStarted || r.value.exitCode !== CWD_UNAVAILABLE_EXIT)
|
|
91
|
+
return r;
|
|
92
|
+
const lines = r.value.stderr.split("\n");
|
|
93
|
+
const at = lines.findIndex((l) => l.startsWith(guard.marker));
|
|
94
|
+
if (at < 0)
|
|
95
|
+
return r;
|
|
96
|
+
const said = lines.slice(0, at).join(" ").trim().slice(0, 300) || "cd failed";
|
|
97
|
+
return { ok: false, error: new ExecutionError("spawn_error", `${leg} spawn failed: ${CWD_UNAVAILABLE_LINE}: ${guard.dir} — the command did not run (${said})`) };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
18
101
|
export function kindFromMode(mode) {
|
|
19
102
|
const t = (mode ?? 0) & 0o170000;
|
|
20
103
|
if (t === 0o040000)
|
package/dist/project-memory.js
CHANGED
|
@@ -3,8 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { createHmac, randomBytes } from "node:crypto";
|
|
4
4
|
import hostPath, { isAbsolute, join } from "node:path";
|
|
5
5
|
import { announceInstructionSource } from "./instruction-source-notice.js";
|
|
6
|
-
import {
|
|
7
|
-
import { scrubServerSecrets } from "./server-secret-env.js";
|
|
6
|
+
import { gitSubprocessEnv } from "./server-secret-env.js";
|
|
8
7
|
import { redactSecrets } from "./trace/redact.js";
|
|
9
8
|
const INSTRUCTION_FILE_CHAIN = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
|
|
10
9
|
const INSTRUCTION_READ_CAP = 24 * 1024;
|
|
@@ -72,11 +71,9 @@ async function readChainMember(root, rel, maxBytes, truncatedFlag) {
|
|
|
72
71
|
}
|
|
73
72
|
function git(root, args, maxBytes) {
|
|
74
73
|
const safeArgs = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args];
|
|
75
|
-
const
|
|
76
|
-
const scrubbedEnv = scrubServerSecrets(process.env, scrubFindings);
|
|
77
|
-
recordSecretEnvScrub("server.project-memory.git-subprocess", scrubFindings, []);
|
|
74
|
+
const env = gitSubprocessEnv({ site: "server.project-memory.git-subprocess", isolateConfig: true });
|
|
78
75
|
return new Promise((res) => {
|
|
79
|
-
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env
|
|
76
|
+
execFile("git", safeArgs, { cwd: root, timeout: 5000, maxBuffer: maxBytes, env }, (err, stdout, stderr) => {
|
|
80
77
|
if (!err)
|
|
81
78
|
return res({ out: stdout.trim() });
|
|
82
79
|
const e = err;
|
package/dist/run-local.js
CHANGED
|
@@ -52,6 +52,7 @@ import { createMetrics } from "./observability/metrics.js";
|
|
|
52
52
|
import { installSecretEnvScrubRecorder } from "./observability/secret-env-scrub.js";
|
|
53
53
|
import { createHostLspManager } from "./host-lsp-manager.js";
|
|
54
54
|
import { buildDisplaySafeUrl } from "./trace/redact.js";
|
|
55
|
+
import { installOutboundDispatcher } from "./outbound-dispatcher.js";
|
|
55
56
|
export function parseArgs(argv) {
|
|
56
57
|
let objective;
|
|
57
58
|
let root;
|
|
@@ -267,6 +268,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
267
268
|
const memoryEngineIncident = createMemoryEngineIncidentSink({ logger, metrics });
|
|
268
269
|
installSecretEnvScrubRecorder({ logger, metrics });
|
|
269
270
|
logConfigDiagnostics(logger);
|
|
271
|
+
installOutboundDispatcher(logger);
|
|
270
272
|
const configProvider = createConfigProvider({
|
|
271
273
|
provider: config.configProvider ?? "local",
|
|
272
274
|
...(config.configLocalDir ? { localDir: config.configLocalDir } : { localDir: root }),
|