@sema-agent/core 5.42.0 → 5.43.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 +48 -0
- package/dist/core/runner/assemble-result.d.ts +3 -0
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +6 -4
- package/dist/core/runner/runtask.js +2 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +8 -0
- package/dist/engine/execution-env/node-execution-env.js +41 -4
- package/dist/tools/fs/fs-search-tools.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.js +8 -5
- package/dist/tools/fs/search.d.ts +11 -0
- package/dist/tools/fs/search.js +21 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.43.0 — 2026-08-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `TaskResult.checkpointId` — the non-secret checkpoint identity now rides the suspended /
|
|
7
|
+
needs_review terminals beside `checkpointToken` (full chain: mint → publishCommittedSuspend →
|
|
8
|
+
suspend/review ref → runTask ref copy → assemble-result; the ref copy is an explicit-key
|
|
9
|
+
whitelist and the new key crosses it). **Erratum for the 5.42.0 entry**: that release minted
|
|
10
|
+
`checkpointId` on `Checkpoint`/`CheckpointSummary` only — it did NOT yet reach `TaskResult`;
|
|
11
|
+
the entry omitted that boundary. This release completes the face.
|
|
12
|
+
- `NodeExecutionEnv` constructor seat `onSecretEnvScrub` (#316): the Bash exec site's secret-env
|
|
13
|
+
scrub decision is observable. Findings name the denylist-matched keys withheld from the child
|
|
14
|
+
env; a caller-overridden key (constructor `shellEnv` or per-exec `env`) is NOT reported withheld —
|
|
15
|
+
the child has it by the caller's explicit choice; an async seat's rejection is consumed (an announcement must
|
|
16
|
+
never become an exec failure); unwired builds print a once-per-process console summary.
|
|
17
|
+
Companion doc fix (#315, doc half): the scrub matches key NAMES against the denylist — a secret
|
|
18
|
+
VALUE under an innocent name is not detected; the doc now says so where the denylist lives.
|
|
19
|
+
- Grep structured rows (#313, first stage): the ripgrep legs hand their served rows
|
|
20
|
+
(`{path?, text}`, post cap/offset) to the tool layer, and `grepDetailFields` prefers row
|
|
21
|
+
identity when every served row carries it — `details.filenames` on rg legs no longer re-parses
|
|
22
|
+
paths out of formatted text (a path containing `:digits:` can no longer split wrong). Text
|
|
23
|
+
parsing remains the honest fallback (JS fallback leg unchanged; its rows stage is a later
|
|
24
|
+
window).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Grep's ripgrep legs always pass `-H` (#311): a single explicit file target keeps its path
|
|
28
|
+
field, so a deny-guarded single-file grep keeps the rg fast path instead of abandoning rg for
|
|
29
|
+
the JS scanner's explicit-file lane (an engine swap over that one file, losing rg's
|
|
30
|
+
streaming/large-file handling). Observable flip, both arms: content-mode output for a single
|
|
31
|
+
explicit file target now carries a path prefix in the TARGET's spelling as passed (the tool
|
|
32
|
+
layer hands an absolute canonical path, so the prefix is ABSOLUTE); without a deny judge the
|
|
33
|
+
line previously had no prefix, and WITH one it previously came from the JS lane with a
|
|
34
|
+
root-relative prefix — both spellings change. Multi-file and directory targets: byte-identical
|
|
35
|
+
(measured). The two engines still disagree on path shape for the same call (jsGrep emits
|
|
36
|
+
root-relative) — that alignment is tracked separately (#318), not silently changed here.
|
|
37
|
+
- The deny-withholding note no longer fabricates on single-file scopes (5.43 rescan): `rg --files`
|
|
38
|
+
lists an explicitly named file argument regardless of glob filters, so the existence probe
|
|
39
|
+
behind the note would have claimed "entries were excluded" on every deny-wired single-file grep
|
|
40
|
+
the `-H` change made reachable. A target the stat probe proves is a FILE is judged clean without
|
|
41
|
+
the probe — the tool layer already judged that one target against the same deny table before
|
|
42
|
+
the engine ran, and a scope of exactly one served file has nothing else to withhold. Directory
|
|
43
|
+
scopes keep the probe unchanged.
|
|
44
|
+
- A clean no-match ripgrep run now carries `rows: []` (5.43 rescan): the one rg terminal that
|
|
45
|
+
omitted the served-rows key, breaking the "rows present ⟺ ripgrep leg" reading.
|
|
46
|
+
- `onSecretEnvScrub` judges its seat by FUNCTION-ness (5.43 rescan): a present non-function seat
|
|
47
|
+
(null, an untyped host's JSON wiring) no longer silences both channels at once — the findings
|
|
48
|
+
keep the seatless once-per-process console summary and the seat defect itself is announced once
|
|
49
|
+
per process (the #170 shape; loud-bad-value law).
|
|
50
|
+
|
|
3
51
|
## 5.42.0 — 2026-08-18
|
|
4
52
|
|
|
5
53
|
### Added
|
|
@@ -170,6 +170,8 @@ export interface ResultFlags {
|
|
|
170
170
|
* but does not set `threw`, so it never reaches the `flags.threw` branch. */
|
|
171
171
|
suspendRef?: {
|
|
172
172
|
token: import("../checkpoint-store.js").CheckpointToken;
|
|
173
|
+
/** The non-secret identity twin ({@link import("../checkpoint-store.js").Checkpoint.checkpointId}). */
|
|
174
|
+
checkpointId?: string;
|
|
173
175
|
gate: import("../checkpoint-store.js").CheckpointGate;
|
|
174
176
|
/** RB-439-b: how the paused workspace comes back (`"park_only"` = a non-suspendable target that was
|
|
175
177
|
* never actually paused). Echoed on `TaskResult.workspaceRestoreMode`; absent for a process-local
|
|
@@ -190,6 +192,7 @@ export interface ResultFlags {
|
|
|
190
192
|
* so it never hits the failure branches above. */
|
|
191
193
|
reviewRef?: {
|
|
192
194
|
token: import("../checkpoint-store.js").CheckpointToken;
|
|
195
|
+
checkpointId?: string;
|
|
193
196
|
gate: import("../checkpoint-store.js").CheckpointGate;
|
|
194
197
|
/** RB-439-b: the review pause pauses the same workspace the approval pause does — same discriminant. */
|
|
195
198
|
restoreMode?: "snapshot" | "park_only";
|
|
@@ -60,6 +60,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
60
60
|
let blockedReason;
|
|
61
61
|
let salvagedOutput;
|
|
62
62
|
let checkpointToken;
|
|
63
|
+
let checkpointId;
|
|
63
64
|
let checkpointGate;
|
|
64
65
|
let workspaceRestoreMode;
|
|
65
66
|
const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
|
|
@@ -114,6 +115,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
114
115
|
else if (flags.suspendRef) {
|
|
115
116
|
status = "suspended";
|
|
116
117
|
checkpointToken = flags.suspendRef.token;
|
|
118
|
+
checkpointId = flags.suspendRef.checkpointId;
|
|
117
119
|
checkpointGate = flags.suspendRef.gate;
|
|
118
120
|
workspaceRestoreMode = flags.suspendRef.restoreMode;
|
|
119
121
|
}
|
|
@@ -121,6 +123,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
121
123
|
status = "needs_review";
|
|
122
124
|
errorCode = "review.pending";
|
|
123
125
|
checkpointToken = flags.reviewRef.token;
|
|
126
|
+
checkpointId = flags.reviewRef.checkpointId;
|
|
124
127
|
checkpointGate = flags.reviewRef.gate;
|
|
125
128
|
workspaceRestoreMode = flags.reviewRef.restoreMode;
|
|
126
129
|
}
|
|
@@ -162,5 +165,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
162
165
|
void _internalCompaction;
|
|
163
166
|
if (flags.unpricedSpend)
|
|
164
167
|
delete publicStats.costMicroUsd;
|
|
165
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), stats: publicStats };
|
|
168
|
+
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), stats: publicStats };
|
|
166
169
|
}
|
|
@@ -433,6 +433,7 @@ export interface Prepared {
|
|
|
433
433
|
* fired this run. */
|
|
434
434
|
suspendRef: {
|
|
435
435
|
token?: CheckpointToken;
|
|
436
|
+
checkpointId?: string;
|
|
436
437
|
gate?: CheckpointGate;
|
|
437
438
|
scope?: string;
|
|
438
439
|
restoreMode?: "snapshot" | "park_only";
|
|
@@ -453,6 +454,7 @@ export interface Prepared {
|
|
|
453
454
|
* `status:"needs_review"`. Empty unless a review pause fired this run. */
|
|
454
455
|
reviewRef: {
|
|
455
456
|
token?: CheckpointToken;
|
|
457
|
+
checkpointId?: string;
|
|
456
458
|
gate?: CheckpointGate;
|
|
457
459
|
scope?: string;
|
|
458
460
|
restoreMode?: "snapshot" | "park_only";
|
|
@@ -3537,9 +3537,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3537
3537
|
return { ok: false, reason };
|
|
3538
3538
|
}
|
|
3539
3539
|
};
|
|
3540
|
-
const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
|
|
3540
|
+
const publishCommittedSuspend = (token, gate, scope, remoteHandle, checkpointId) => {
|
|
3541
3541
|
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? reviewRef : suspendRef;
|
|
3542
3542
|
ref.token = token;
|
|
3543
|
+
if (checkpointId !== undefined)
|
|
3544
|
+
ref.checkpointId = checkpointId;
|
|
3543
3545
|
ref.gate = gate;
|
|
3544
3546
|
if (remoteHandle !== undefined)
|
|
3545
3547
|
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
@@ -3625,7 +3627,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3625
3627
|
};
|
|
3626
3628
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
|
|
3627
3629
|
return false;
|
|
3628
|
-
publishCommittedSuspend(token, gate, scope, remoteHandle);
|
|
3630
|
+
publishCommittedSuspend(token, gate, scope, remoteHandle, cp.checkpointId);
|
|
3629
3631
|
try {
|
|
3630
3632
|
await sessions.pin?.(sessionId);
|
|
3631
3633
|
}
|
|
@@ -3711,7 +3713,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3711
3713
|
};
|
|
3712
3714
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
|
|
3713
3715
|
return false;
|
|
3714
|
-
publishCommittedSuspend(token, gate, scope, remoteHandle);
|
|
3716
|
+
publishCommittedSuspend(token, gate, scope, remoteHandle, cp.checkpointId);
|
|
3715
3717
|
try {
|
|
3716
3718
|
await sessions.pin?.(sessionId);
|
|
3717
3719
|
}
|
|
@@ -4006,7 +4008,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4006
4008
|
if (!committed.ok) {
|
|
4007
4009
|
return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
|
|
4008
4010
|
}
|
|
4009
|
-
publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
|
|
4011
|
+
publishCommittedSuspend(token, gate, cp.scope, remoteHandle, cp.checkpointId);
|
|
4010
4012
|
try {
|
|
4011
4013
|
await sessions.pin?.(sessionId);
|
|
4012
4014
|
}
|
|
@@ -3472,6 +3472,7 @@ export class Runner {
|
|
|
3472
3472
|
suspendRef: prepared.suspendRef.token !== undefined && prepared.suspendRef.gate !== undefined
|
|
3473
3473
|
? {
|
|
3474
3474
|
token: prepared.suspendRef.token,
|
|
3475
|
+
...(prepared.suspendRef.checkpointId !== undefined ? { checkpointId: prepared.suspendRef.checkpointId } : {}),
|
|
3475
3476
|
gate: prepared.suspendRef.gate,
|
|
3476
3477
|
...(prepared.suspendRef.restoreMode !== undefined ? { restoreMode: prepared.suspendRef.restoreMode } : {}),
|
|
3477
3478
|
}
|
|
@@ -3479,6 +3480,7 @@ export class Runner {
|
|
|
3479
3480
|
reviewRef: prepared.reviewRef.token !== undefined && prepared.reviewRef.gate !== undefined
|
|
3480
3481
|
? {
|
|
3481
3482
|
token: prepared.reviewRef.token,
|
|
3483
|
+
...(prepared.reviewRef.checkpointId !== undefined ? { checkpointId: prepared.reviewRef.checkpointId } : {}),
|
|
3482
3484
|
gate: prepared.reviewRef.gate,
|
|
3483
3485
|
...(prepared.reviewRef.restoreMode !== undefined ? { restoreMode: prepared.reviewRef.restoreMode } : {}),
|
|
3484
3486
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -2881,6 +2881,10 @@ export interface TaskResult {
|
|
|
2881
2881
|
* the resume capability, §6). Undefined for every other status.
|
|
2882
2882
|
*/
|
|
2883
2883
|
checkpointToken?: import("./checkpoint-store.js").CheckpointToken;
|
|
2884
|
+
/** The suspend's NON-SECRET stable identity ({@link import("./checkpoint-store.js").Checkpoint.checkpointId}),
|
|
2885
|
+
* set beside `checkpointToken` on the suspended/needs_review terminals — the display/correlation key a
|
|
2886
|
+
* consumer may log or render where the token must not travel. Absent on pre-identity checkpoints. */
|
|
2887
|
+
checkpointId?: string;
|
|
2884
2888
|
/** Set with `checkpointToken`: who/what must resume (e.g. `{kind:"human", reason, toolName}`), so the
|
|
2885
2889
|
* caller knows what decision the suspension is awaiting. */
|
|
2886
2890
|
checkpointGate?: import("./checkpoint-store.js").CheckpointGate;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExecResult, type ExecutionEnv, ExecutionError, type ExecutionEnvExecOptions, FileError, type FileInfo, type Result } from "../harness/types.js";
|
|
2
|
+
import { type SecretEnvFinding } from "../../core/secret-env.js";
|
|
2
3
|
import type { BackgroundShellCapability, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions } from "../../core/background-shell.js";
|
|
3
4
|
import { BackgroundShellError } from "../../core/background-shell.js";
|
|
4
5
|
import type { SchedulerCapability, ScheduledIntent, SchedulerContext, ScheduledTaskId, ScheduledTaskSummary } from "../../core/scheduler.js";
|
|
@@ -18,6 +19,8 @@ export declare function getShellConfig(customShellPath?: string): Promise<Result
|
|
|
18
19
|
shell: string;
|
|
19
20
|
args: string[];
|
|
20
21
|
}, ExecutionError>>;
|
|
22
|
+
/** test seam (mirrors the notice-seat announce reset): the once-per-process fallback latches. */
|
|
23
|
+
export declare function __resetSecretScrubAnnouncement(): void;
|
|
21
24
|
/** RB-235 ([1937] BB2): the spool-reclaim decision, extracted PURE so every rule is unit-pinnable.
|
|
22
25
|
* Both reclaim sites (foreground `reclaimSpool`, background `syncSpool`) route through this one
|
|
23
26
|
* function so they cannot drift apart.
|
|
@@ -83,6 +86,7 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
83
86
|
private readonly bgShells;
|
|
84
87
|
private bgCounter;
|
|
85
88
|
private readonly retainBackgroundProcesses;
|
|
89
|
+
private readonly onSecretEnvScrub?;
|
|
86
90
|
/** design/128 T1-1: lazily-created env-owned spool dir (mkdtemp under os tmp — unpredictable, never the cwd). */
|
|
87
91
|
private bgSpoolDir?;
|
|
88
92
|
readonly backgroundCapabilities: BackgroundShellCapability["backgroundCapabilities"];
|
|
@@ -108,6 +112,10 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
108
112
|
* deployments (their pre-suspend dispose becomes a no-op too — do not combine).
|
|
109
113
|
*/
|
|
110
114
|
retainBackgroundProcesses?: boolean;
|
|
115
|
+
/** The D1 scrub decision's observation seat: called with every NON-EMPTY finding set the default
|
|
116
|
+
* "scrub" leg withholds from a shell child. Absent ⇒ a once-per-process console summary (loud
|
|
117
|
+
* enough to notice, quiet enough not to spam). Never called on the "all"/allowlist legs. */
|
|
118
|
+
onSecretEnvScrub?: (findings: readonly SecretEnvFinding[]) => void;
|
|
111
119
|
});
|
|
112
120
|
get schedulerCapabilities(): SchedulerCapability["schedulerCapabilities"];
|
|
113
121
|
schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{
|
|
@@ -188,7 +188,13 @@ export async function getShellConfig(customShellPath) {
|
|
|
188
188
|
return ok({ shell: "sh", args: ["-c"] });
|
|
189
189
|
}
|
|
190
190
|
const EXEC_FORCE_SETTLE_GRACE_MS = 3000;
|
|
191
|
-
|
|
191
|
+
let secretScrubConsoleAnnounced = false;
|
|
192
|
+
let secretScrubSeatDefectAnnounced = false;
|
|
193
|
+
export function __resetSecretScrubAnnouncement() {
|
|
194
|
+
secretScrubConsoleAnnounced = false;
|
|
195
|
+
secretScrubSeatDefectAnnounced = false;
|
|
196
|
+
}
|
|
197
|
+
function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub) {
|
|
192
198
|
let inherited;
|
|
193
199
|
if (inheritEnv === "all") {
|
|
194
200
|
inherited = process.env;
|
|
@@ -200,7 +206,36 @@ function getShellEnv(inheritEnv, baseEnv, extraEnv) {
|
|
|
200
206
|
inherited[k] = process.env[k];
|
|
201
207
|
}
|
|
202
208
|
else {
|
|
203
|
-
|
|
209
|
+
const scrubbed = [];
|
|
210
|
+
inherited = scrubSecretEnv(process.env, scrubbed);
|
|
211
|
+
const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
|
|
212
|
+
if (findings.length > 0) {
|
|
213
|
+
if (typeof onScrub === "function") {
|
|
214
|
+
try {
|
|
215
|
+
const r = onScrub(findings);
|
|
216
|
+
if (typeof r?.then === "function") {
|
|
217
|
+
r.then(undefined, () => {
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
|
|
226
|
+
secretScrubSeatDefectAnnounced = true;
|
|
227
|
+
try {
|
|
228
|
+
console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (!secretScrubConsoleAnnounced) {
|
|
234
|
+
secretScrubConsoleAnnounced = true;
|
|
235
|
+
console.warn(`secret_env_scrubbed: ${findings.length} env key(s) withheld from the shell child (${findings.map((f) => f.key).join(", ")}) — once per process; wire onSecretEnvScrub for per-exec telemetry`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
204
239
|
}
|
|
205
240
|
return {
|
|
206
241
|
...inherited,
|
|
@@ -254,6 +289,7 @@ export class NodeExecutionEnv {
|
|
|
254
289
|
bgShells = new Map();
|
|
255
290
|
bgCounter = 0;
|
|
256
291
|
retainBackgroundProcesses;
|
|
292
|
+
onSecretEnvScrub;
|
|
257
293
|
bgSpoolDir;
|
|
258
294
|
backgroundCapabilities;
|
|
259
295
|
constructor(options) {
|
|
@@ -263,6 +299,7 @@ export class NodeExecutionEnv {
|
|
|
263
299
|
this.inheritEnv = options.inheritEnv ?? "scrub";
|
|
264
300
|
this.scheduler = options.scheduler;
|
|
265
301
|
this.retainBackgroundProcesses = options.retainBackgroundProcesses ?? false;
|
|
302
|
+
this.onSecretEnvScrub = options.onSecretEnvScrub;
|
|
266
303
|
this.backgroundCapabilities = {
|
|
267
304
|
supported: true,
|
|
268
305
|
maxConcurrent: 8,
|
|
@@ -420,7 +457,7 @@ export class NodeExecutionEnv {
|
|
|
420
457
|
child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
|
|
421
458
|
cwd,
|
|
422
459
|
detached: process.platform !== "win32",
|
|
423
|
-
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env),
|
|
460
|
+
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub),
|
|
424
461
|
stdio: execSpoolFds ? ["ignore", execSpoolFds[0], execSpoolFds[1]] : ["ignore", "pipe", "pipe"],
|
|
425
462
|
windowsHide: true,
|
|
426
463
|
});
|
|
@@ -1114,7 +1151,7 @@ export class NodeExecutionEnv {
|
|
|
1114
1151
|
child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
|
|
1115
1152
|
cwd: options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd,
|
|
1116
1153
|
detached: process.platform !== "win32",
|
|
1117
|
-
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env),
|
|
1154
|
+
env: getShellEnv(this.inheritEnv, this.shellEnv, options?.env, this.onSecretEnvScrub),
|
|
1118
1155
|
stdio: spoolFds ? ["ignore", spoolFds[0], spoolFds[1]] : ["ignore", "pipe", "pipe"],
|
|
1119
1156
|
windowsHide: true,
|
|
1120
1157
|
});
|
|
@@ -9,7 +9,7 @@ import type { ReadFace } from "./read-face.js";
|
|
|
9
9
|
* replace the text parsing with structured rows from runGrep; until then this seam is the honesty
|
|
10
10
|
* boundary.
|
|
11
11
|
*/
|
|
12
|
-
export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number): Record<string, unknown>;
|
|
12
|
+
export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
|
|
13
13
|
export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|
|
14
14
|
export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace): AgentTool;
|
|
15
15
|
/** Static side-effect class of every hand tool, by name (design/44 §3). Used by prepare-task to (a) feed
|
|
@@ -2,7 +2,9 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
3
|
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
4
4
|
import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern, invalidGlobTokens } from "./search.js";
|
|
5
|
-
export function grepDetailFields(text, mode, offset) {
|
|
5
|
+
export function grepDetailFields(text, mode, offset, structuredRows) {
|
|
6
|
+
const structuredIdentityComplete = structuredRows !== undefined && structuredRows.every((r) => r.path !== undefined || r.text === "--");
|
|
7
|
+
const structuredPaths = structuredIdentityComplete && structuredRows !== undefined ? [...new Set(structuredRows.filter((r) => r.path !== undefined).map((r) => r.path))] : undefined;
|
|
6
8
|
const rows = text.startsWith("No matches.")
|
|
7
9
|
? []
|
|
8
10
|
: text
|
|
@@ -28,10 +30,11 @@ export function grepDetailFields(text, mode, offset) {
|
|
|
28
30
|
const appliedLimit = cappedAt !== undefined ? { appliedLimit: cappedAt } : {};
|
|
29
31
|
let detailFields;
|
|
30
32
|
if (mode === "files_with_matches") {
|
|
31
|
-
|
|
33
|
+
const fileNames = structuredPaths ?? rows;
|
|
34
|
+
detailFields = { filenames: fileNames, numFiles: fileNames.length, totalFiles: capTotal ?? fileNames.length, ...appliedLimit, ...appliedOffset };
|
|
32
35
|
}
|
|
33
36
|
else if (mode === "count") {
|
|
34
|
-
const filenames = [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
37
|
+
const filenames = structuredPaths ?? [...new Set(rows.map((l) => (l.includes(":") ? l.slice(0, l.lastIndexOf(":")) : l)))];
|
|
35
38
|
let numMatches = 0;
|
|
36
39
|
let malformed = false;
|
|
37
40
|
for (const l of rows) {
|
|
@@ -52,7 +55,7 @@ export function grepDetailFields(text, mode, offset) {
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
else {
|
|
55
|
-
const filenames = [...new Set(rows.map(contentPathOf))];
|
|
58
|
+
const filenames = structuredPaths ?? [...new Set(rows.map(contentPathOf))];
|
|
56
59
|
const joined = rows.join("\n");
|
|
57
60
|
const GREP_CONTENT_PREVIEW_CHARS = 16_000;
|
|
58
61
|
detailFields = {
|
|
@@ -161,7 +164,7 @@ export function createGrepTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
161
164
|
if (text.startsWith("Error (grep)") || text.startsWith("Error (Grep)"))
|
|
162
165
|
return errorResult(text);
|
|
163
166
|
const mode = a.output_mode ?? "files_with_matches";
|
|
164
|
-
const detailFields = grepDetailFields(text, mode, a.offset);
|
|
167
|
+
const detailFields = grepDetailFields(text, mode, a.offset, grepRun.rows);
|
|
165
168
|
return {
|
|
166
169
|
content: text,
|
|
167
170
|
details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}), ...(grepRun.withheld !== undefined ? { withheld: grepRun.withheld } : {}) },
|
|
@@ -210,8 +210,19 @@ export type GrepDegradation = {
|
|
|
210
210
|
};
|
|
211
211
|
/** Structured grep result: the model-facing text plus the degradation facts, so the tool layer can
|
|
212
212
|
* ship them on the structured frame instead of leaving them prose-only. */
|
|
213
|
+
/** #313 (first stage, rg legs) — one SERVED result row with its path read from ripgrep's own
|
|
214
|
+
* field ({@link parseRgRecords}), exactly the window `text` shows (post cap/offset). The tool
|
|
215
|
+
* layer prefers these over re-parsing `text` (whose `path:line:text` split mis-cuts a path that
|
|
216
|
+
* itself contains `:digits:`); absent = a leg that has no structured rows yet (the JS scanner —
|
|
217
|
+
* its rows stage is the ticket's remainder) and the text parse is the honest fallback. */
|
|
218
|
+
export interface GrepRow {
|
|
219
|
+
path?: string;
|
|
220
|
+
text: string;
|
|
221
|
+
}
|
|
213
222
|
export interface GrepRunResult {
|
|
214
223
|
text: string;
|
|
224
|
+
/** Served rows (#313): present on the ripgrep legs, absent on the JS-scanner legs. */
|
|
225
|
+
rows?: readonly GrepRow[];
|
|
215
226
|
degraded?: GrepDegradation;
|
|
216
227
|
/** design/199 件B — deny-list withholding facts (see {@link ReadDenyWithheld}); absent = nothing
|
|
217
228
|
* withheld / no deny judge in play. */
|
package/dist/tools/fs/search.js
CHANGED
|
@@ -1096,12 +1096,16 @@ function formatRgRecords(records, p, caveat = "") {
|
|
|
1096
1096
|
const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
|
|
1097
1097
|
const off = Math.max(0, Math.floor(p.offset ?? 0));
|
|
1098
1098
|
const capped = records.slice(off, off + cap);
|
|
1099
|
+
const served = capped.map((r) => (r.path !== undefined ? { path: r.path, text: r.text } : { text: r.text }));
|
|
1099
1100
|
if (capped.length === 0)
|
|
1100
|
-
return NO_MATCHES + (off > 0 ? `\n[offset ${off}]` : "") + caveat;
|
|
1101
|
-
return
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1101
|
+
return { text: NO_MATCHES + (off > 0 ? `\n[offset ${off}]` : "") + caveat, served };
|
|
1102
|
+
return {
|
|
1103
|
+
text: capped.map((r) => r.text).join("\n") +
|
|
1104
|
+
(records.length > off + cap ? `\n…[capped at ${cap} of ${records.length}]` : "") +
|
|
1105
|
+
(off > 0 ? `\n[offset ${off}]` : "") +
|
|
1106
|
+
caveat,
|
|
1107
|
+
served,
|
|
1108
|
+
};
|
|
1105
1109
|
}
|
|
1106
1110
|
export function rgOutputDenyTripwire(stdout, mode, judge, opts = {}) {
|
|
1107
1111
|
if (stdout.length === 0)
|
|
@@ -1143,7 +1147,7 @@ async function jsGrepFallback(env, root, p, signal, reason, deny) {
|
|
|
1143
1147
|
}
|
|
1144
1148
|
export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
1145
1149
|
const mode = p.output_mode ?? "files_with_matches";
|
|
1146
|
-
const flags = ["--null", "--no-messages", "--no-require-git", "--hidden"];
|
|
1150
|
+
const flags = ["--null", "-H", "--no-messages", "--no-require-git", "--hidden"];
|
|
1147
1151
|
for (const d of VCS_DIRS)
|
|
1148
1152
|
flags.push("--glob", `!${d}`);
|
|
1149
1153
|
flags.push("--max-columns", "500", "--max-columns-preview");
|
|
@@ -1193,7 +1197,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1193
1197
|
if (records.length === 0)
|
|
1194
1198
|
return jsGrepFallback(env, root, p, signal, "timed out before completing a result", deny);
|
|
1195
1199
|
return {
|
|
1196
|
-
text: `${delimitUntrusted("ripgrep partial output",
|
|
1200
|
+
...(() => { const f = formatRgRecords(records, p); return { text: `${delimitUntrusted("ripgrep partial output", f.text)}\n…[ripgrep timed out after producing partial output — results may be incomplete]`, rows: f.served }; })(),
|
|
1197
1201
|
degraded: { partial: true, reason: "ripgrep timed out" },
|
|
1198
1202
|
};
|
|
1199
1203
|
}
|
|
@@ -1205,6 +1209,12 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1205
1209
|
const denyDisclosure = async () => {
|
|
1206
1210
|
if (deny === undefined || deny.rgExclusionGlobs.length === 0)
|
|
1207
1211
|
return { note: "" };
|
|
1212
|
+
if (p.path) {
|
|
1213
|
+
const startPath = isAbsolutePathForm(p.path) ? p.path : `${root.replace(/[\\/]+$/, "")}${root.includes("\\") ? "\\" : "/"}${p.path}`;
|
|
1214
|
+
const info = await env.fileInfo(startPath, signal);
|
|
1215
|
+
if (info.ok && info.value.kind === "file")
|
|
1216
|
+
return { note: "" };
|
|
1217
|
+
}
|
|
1208
1218
|
const withheld = await rgDenyExistenceProbe(env, root, deny, target, signal);
|
|
1209
1219
|
if (withheld === undefined)
|
|
1210
1220
|
return { note: "" };
|
|
@@ -1215,7 +1225,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1215
1225
|
};
|
|
1216
1226
|
if (exitCode === 1) {
|
|
1217
1227
|
const d = await denyDisclosure();
|
|
1218
|
-
return { text: NO_MATCHES + d.note, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1228
|
+
return { text: NO_MATCHES + d.note, rows: [], ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1219
1229
|
}
|
|
1220
1230
|
if (exitCode >= 2) {
|
|
1221
1231
|
if (stdout.trim().length > 0) {
|
|
@@ -1228,7 +1238,7 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1228
1238
|
if (records.length === 0)
|
|
1229
1239
|
return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no complete result`, deny);
|
|
1230
1240
|
return {
|
|
1231
|
-
text: `${delimitUntrusted("ripgrep partial output",
|
|
1241
|
+
...(() => { const f = formatRgRecords(records, p); return { text: `${delimitUntrusted("ripgrep partial output", f.text)}\n…[ripgrep exited with an error after producing partial output — results may be incomplete]`, rows: f.served }; })(),
|
|
1232
1242
|
degraded: { partial: true, reason: `ripgrep exited with code ${exitCode}` },
|
|
1233
1243
|
};
|
|
1234
1244
|
}
|
|
@@ -1242,7 +1252,8 @@ export async function rgGrepDetailed(env, root, p, signal, deny) {
|
|
|
1242
1252
|
const parsed = parseRgRecords(stdout, mode);
|
|
1243
1253
|
const ordered = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, parsed, signal) : parsed;
|
|
1244
1254
|
const d = await denyDisclosure();
|
|
1245
|
-
|
|
1255
|
+
const formatted = formatRgRecords(ordered, p);
|
|
1256
|
+
return { text: formatted.text + d.note, rows: formatted.served, ...(d.withheld !== undefined ? { withheld: d.withheld } : {}) };
|
|
1246
1257
|
}
|
|
1247
1258
|
async function rgDenyExistenceProbe(env, root, deny, target, signal) {
|
|
1248
1259
|
const probeFlags = ["--files", "--hidden", "--no-require-git", "--no-messages"];
|