@stigmer/runner 3.12.1 → 3.12.2
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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +5 -4
- package/dist/activities/execute-cursor/capture-flow.js +5 -4
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/cas-observations.d.ts +10 -4
- package/dist/activities/execute-cursor/cas-observations.js +10 -4
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.js +39 -13
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/skill-resolver.d.ts +8 -0
- package/dist/activities/execute-cursor/skill-resolver.js +77 -26
- package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +3 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +3 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +8 -0
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.js +16 -9
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/generate-session-subject.d.ts +80 -0
- package/dist/activities/generate-session-subject.js +283 -0
- package/dist/activities/generate-session-subject.js.map +1 -0
- package/dist/client/stigmer-client.d.ts +8 -0
- package/dist/client/stigmer-client.js +10 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +25 -10
- package/dist/middleware/approval-gate.js +33 -14
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/otel-spans.d.ts +2 -1
- package/dist/middleware/otel-spans.js +2 -1
- package/dist/middleware/otel-spans.js.map +1 -1
- package/dist/runner-manager.js +3 -1
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +3 -1
- package/dist/runner.js.map +1 -1
- package/dist/shared/attachment-vision.js +9 -0
- package/dist/shared/attachment-vision.js.map +1 -1
- package/dist/shared/mcp-manager.js +8 -0
- package/dist/shared/mcp-manager.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/__tests__/generate-session-subject.test.ts +348 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +60 -9
- package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +163 -7
- package/src/activities/execute-cursor/capture-flow.ts +5 -4
- package/src/activities/execute-cursor/cas-observations.ts +10 -4
- package/src/activities/execute-cursor/hook-script.ts +39 -13
- package/src/activities/execute-cursor/skill-resolver.ts +98 -31
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +3 -1
- package/src/activities/execute-deep-agent/setup.ts +8 -0
- package/src/activities/execute-deep-agent/subagent-wiring.ts +16 -9
- package/src/activities/generate-session-subject.ts +370 -0
- package/src/client/stigmer-client.ts +11 -0
- package/src/middleware/__tests__/approval-gate.test.ts +130 -1
- package/src/middleware/approval-gate.ts +58 -24
- package/src/middleware/otel-spans.ts +2 -1
- package/src/runner-manager.ts +3 -0
- package/src/runner.ts +3 -0
- package/src/shared/__tests__/attachment-vision.test.ts +4 -0
- package/src/shared/__tests__/mcp-manager.test.ts +7 -2
- package/src/shared/attachment-vision.ts +9 -0
- package/src/shared/filereview/__tests__/capture.test.ts +48 -0
- package/src/shared/mcp-manager.ts +8 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Cursor harness's disk-backed observer for
|
|
2
|
+
* The Cursor harness's disk-backed observer for CAS-owned file mutations —
|
|
3
|
+
* gitignored / non-git writes, and (issue #303) pre-delete byte capture — the
|
|
3
4
|
* out-of-process analog of the deep-agent `CasCaptureFilesystemBackend`
|
|
4
5
|
* ({@link ../execute-deep-agent/cas-capture-backend.js}).
|
|
5
6
|
*
|
|
@@ -185,14 +186,19 @@ export function buildSecretClassifyScript(): string {
|
|
|
185
186
|
|
|
186
187
|
/**
|
|
187
188
|
* Build the standalone Node.js script the hook runs to observe a single
|
|
188
|
-
*
|
|
189
|
-
* `recordBefore` plus the DD-E secret gate.
|
|
189
|
+
* CAS-owned mutation — the disk-backed mirror of `CasCaptureFilesystemBackend`'s
|
|
190
|
+
* `recordBefore` plus the DD-E secret gate. Staging is mutation-agnostic: it
|
|
191
|
+
* records the PRE-mutation bytes, so the hook runs it for a write/edit and —
|
|
192
|
+
* issue #303 — for a delete (whose before-bytes exist only until the tool runs;
|
|
193
|
+
* the boundary later reads after=null and authors the DELETE). The hook's
|
|
194
|
+
* delete arm pre-classifies secrets and never invokes this for a secret-like
|
|
195
|
+
* delete (a gated delete must not leave a "secret" marker in the sidecar).
|
|
190
196
|
*
|
|
191
197
|
* Invoked as `printf %s "$SALIENT" | node -e '<this>' <workspaceRoot> <obsDir>`
|
|
192
198
|
* — the (arbitrary) salient path arrives on stdin so no argv escaping is needed;
|
|
193
199
|
* the two absolute paths ride argv. Prints one token on stdout for the hook:
|
|
194
200
|
* - `captured` — non-secret: staged the pre-turn bytes (first-touch-wins), the
|
|
195
|
-
* hook then ALLOWS the
|
|
201
|
+
* hook then ALLOWS the mutation to flow;
|
|
196
202
|
* - `secret` — secret-like: recorded a content-less marker, the hook then
|
|
197
203
|
* DENIES with the security message (nothing is written);
|
|
198
204
|
* - `error` — the path escaped the workspace or a filesystem error occurred,
|
|
@@ -574,9 +574,8 @@ if echo "$STATE" | grep -q '"captureIgnored":true'; then
|
|
|
574
574
|
fi
|
|
575
575
|
# gitWorkspace selects the capture substrate (Slice 2c). Default true when the
|
|
576
576
|
# key is absent (older state files). When false the workspace is NOT a git tree:
|
|
577
|
-
# there is no git snapshot, so EVERY file write is CAS-staged below (not
|
|
578
|
-
# gitignored ones)
|
|
579
|
-
# (no CAS delete-capture path, parity with the deep-agent).
|
|
577
|
+
# there is no git snapshot, so EVERY file write/delete is CAS-staged below (not
|
|
578
|
+
# only gitignored ones) and the git-tracked flow arm is skipped.
|
|
580
579
|
GIT_WORKSPACE=true
|
|
581
580
|
if echo "$STATE" | grep -q '"gitWorkspace":false'; then
|
|
582
581
|
GIT_WORKSPACE=false
|
|
@@ -597,9 +596,10 @@ fi
|
|
|
597
596
|
# secret-like one must be hard-blocked in every mode. WHICH writes are CAS-owned
|
|
598
597
|
# depends on the substrate: in a git tree it is only the GITIGNORED writes (git
|
|
599
598
|
# captures the tracked ones); in a NON-GIT workspace it is EVERY write (there is
|
|
600
|
-
# no git snapshot). Only a built-in write/edit (category "write") takes
|
|
601
|
-
# deletes
|
|
602
|
-
# approval gate). The staging runs on
|
|
599
|
+
# no git snapshot). Only a built-in write/edit (category "write") takes THIS arm;
|
|
600
|
+
# deletes take their own staging arm just below (issue #303), shell/MCP stay on
|
|
601
|
+
# the deny-gate (parity with the deep-agent approval gate). The staging runs on
|
|
602
|
+
# the runner's own Node binary (the
|
|
603
603
|
# disk-backed mirror of CasCaptureFilesystemBackend.recordBefore): the salient
|
|
604
604
|
# path rides stdin (no argv escaping), the workspace root and the per-turn
|
|
605
605
|
# cas-observations dir ride argv. "captured" -> allow (apply-then-review);
|
|
@@ -629,6 +629,32 @@ if [ "$CAPTURE_IGNORED" = "true" ] && [ "$CATEGORY" = "write" ] && [ -n "$SALIEN
|
|
|
629
629
|
fi
|
|
630
630
|
fi
|
|
631
631
|
|
|
632
|
+
# --- Capture mode: observe CAS-owned deletes for review (issue #303) --------
|
|
633
|
+
# The delete twin of the write arm above — a non-secret CAS-owned delete stages
|
|
634
|
+
# its before-bytes (the one moment they still exist on disk) and flows; the
|
|
635
|
+
# turn boundary then reads after=null and authors a reviewable, restorable
|
|
636
|
+
# DELETE entry. One deliberate difference from the write arm: the path is
|
|
637
|
+
# classified for secret-likeness BEFORE any staging touches the sidecar. A
|
|
638
|
+
# secret-like delete must stay on the deny-gate below where a human may still
|
|
639
|
+
# approve it (its args expose no secret content, unlike a write) — staging it
|
|
640
|
+
# would both persist the secret before-bytes and mark the sidecar "secret",
|
|
641
|
+
# making the boundary author a blocking DIFF_UNREVIEWABLE for a merely-gated
|
|
642
|
+
# delete. Every failure mode (secret, classify error, staging error) falls
|
|
643
|
+
# through to the deny-gate rather than denying here, so grants and leases keep
|
|
644
|
+
# applying to a gated delete exactly as before this arm existed; the eventual
|
|
645
|
+
# resolution records its own ledger kind.
|
|
646
|
+
if [ "$CAPTURE_IGNORED" = "true" ] && [ "$CATEGORY" = "delete" ] && [ -n "$SALIENT" ] && { [ "$GIT_WORKSPACE" = "false" ] || __stigmer_is_gitignored "$SALIENT"; }; then
|
|
647
|
+
DELETE_SECRET_RESULT=$(printf '%s' "$SALIENT" | ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e '${secretClassifyScript}' 2>/dev/null || echo error)
|
|
648
|
+
if [ "$DELETE_SECRET_RESULT" = "ok" ]; then
|
|
649
|
+
OBS_DIR="$(dirname "$STATE_FILE")/${CAS_OBSERVATIONS_DIRNAME}"
|
|
650
|
+
OBS_RESULT=$(printf '%s' "$SALIENT" | ELECTRON_RUN_AS_NODE=1 "$NODE_BIN" -e '${observationStagingScript}' "$GIT_ROOT" "$OBS_DIR" 2>/dev/null || echo error)
|
|
651
|
+
if [ "$OBS_RESULT" = "captured" ]; then
|
|
652
|
+
echo '{"permission":"allow"}'
|
|
653
|
+
exit 0
|
|
654
|
+
fi
|
|
655
|
+
fi
|
|
656
|
+
fi
|
|
657
|
+
|
|
632
658
|
# --- 1a. MCP capability manifest: enabled_tools (issue #350) ---
|
|
633
659
|
# Runs BEFORE the auto-approve-all shortcut and every grant check because
|
|
634
660
|
# enabled_tools is a capability manifest, not an approval gate: no bypass may
|
|
@@ -700,13 +726,13 @@ if [ -n "$CATEGORY" ]; then
|
|
|
700
726
|
# Capture mode (GIT tree only): a git-tracked file mutation (write/edit/delete)
|
|
701
727
|
# flows freely — the runner captures the whole change set with git at the turn
|
|
702
728
|
# boundary and gates it per-file for review. A gitignored path is invisible to
|
|
703
|
-
# that git snapshot: a non-secret gitignored WRITE was already
|
|
704
|
-
# (staged + allowed,
|
|
705
|
-
# reaches the deny-gate when captureIgnored
|
|
706
|
-
#
|
|
707
|
-
# (category "shell") never takes this branch
|
|
708
|
-
# NON-GIT workspace this arm is skipped
|
|
709
|
-
#
|
|
729
|
+
# that git snapshot: a non-secret gitignored WRITE or DELETE was already
|
|
730
|
+
# handled above (staged + allowed, hard-blocked, or left to fall through) when
|
|
731
|
+
# captureIgnored is on; here it only reaches the deny-gate when captureIgnored
|
|
732
|
+
# is off (no artifact storage), or it is a secret-like DELETE (approvable, but
|
|
733
|
+
# never staged — issue #303). shell (category "shell") never takes this branch
|
|
734
|
+
# and stays gated as always. In a NON-GIT workspace this arm is skipped
|
|
735
|
+
# entirely (there is no git diff): writes and deletes were CAS-staged above.
|
|
710
736
|
if [ "$CAPTURE_MODE" = "true" ] && [ "$GIT_WORKSPACE" = "true" ] && { [ "$CATEGORY" = "write" ] || [ "$CATEGORY" = "delete" ]; }; then
|
|
711
737
|
if ! __stigmer_is_gitignored "$SALIENT"; then
|
|
712
738
|
echo '{"permission":"allow"}'
|
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
* - Uses a platform-managed directory outside the workspace
|
|
8
8
|
* - Ensures the workspace `.stigmer` symlink (see stigmer-link.ts)
|
|
9
9
|
* - Returns metadata for prompt injection
|
|
10
|
+
*
|
|
11
|
+
* The mount is cached by the skill's content-addressed version hash
|
|
12
|
+
* (stigmer/stigmer#672): metadata is fetched on every execution (that keeps
|
|
13
|
+
* latest-version freshness), but the artifact download and file rewrite are
|
|
14
|
+
* skipped when the mounted content's hash already matches. The session's
|
|
15
|
+
* platform dir survives across executions, so on an active session every
|
|
16
|
+
* message after the first pays a metadata read instead of a full artifact
|
|
17
|
+
* transfer.
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
|
-
import { mkdir, writeFile, rm } from "node:fs/promises";
|
|
20
|
+
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
13
21
|
import { join, dirname } from "node:path";
|
|
14
22
|
import type { StigmerClient } from "../../client/stigmer-client.js";
|
|
15
23
|
import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
@@ -21,6 +29,25 @@ import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/work
|
|
|
21
29
|
|
|
22
30
|
const SKILLS_SUBDIR = "skills";
|
|
23
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Marker recording what a skill's mount directory currently holds. Written
|
|
34
|
+
* LAST, after every file of the mount landed — a crash mid-write leaves no
|
|
35
|
+
* marker, so the next execution remounts instead of trusting a partial tree.
|
|
36
|
+
*/
|
|
37
|
+
const MOUNT_MARKER_FILE = ".stigmer-mount.json";
|
|
38
|
+
|
|
39
|
+
interface MountMarker {
|
|
40
|
+
/** Content-addressed version hash (`Skill.status.version_hash`) of the mounted content. */
|
|
41
|
+
versionHash: string;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the artifact's files are part of the mount. `false` when the
|
|
44
|
+
* skill has no artifact OR when the download failed and the mount fell
|
|
45
|
+
* back to SKILL.md only — the latter makes the next execution retry the
|
|
46
|
+
* download rather than cache the degraded mount.
|
|
47
|
+
*/
|
|
48
|
+
artifactMounted: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
24
51
|
export interface SkillResolverOptions {
|
|
25
52
|
sessionId: string;
|
|
26
53
|
primaryWorkspaceDir: string;
|
|
@@ -61,11 +88,34 @@ export async function resolveSkills(
|
|
|
61
88
|
for (const ref of skillRefs) {
|
|
62
89
|
try {
|
|
63
90
|
const skill = await client.getSkillByReference(ref);
|
|
91
|
+
const spec = skill.spec;
|
|
92
|
+
if (!spec?.skillMd) {
|
|
93
|
+
console.warn(`[resolveSkills] skill ${ref.org}/${ref.slug} fetched but had no skillMd content`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const name = spec.name || skill.metadata?.slug || "unknown";
|
|
98
|
+
const skillDir = join(skillsDir, name);
|
|
99
|
+
const versionHash = skill.status?.versionHash ?? "";
|
|
100
|
+
const wantsArtifact = Boolean(skill.status?.artifactStorageKey);
|
|
101
|
+
const meta: SkillMetadata = {
|
|
102
|
+
name,
|
|
103
|
+
description: spec.description || `Skill: ${name}`,
|
|
104
|
+
path: join(STIGMER_LOCAL_STATE_DIR, SKILLS_SUBDIR, name, "SKILL.md"),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (versionHash !== "" && (await mountIsFresh(skillDir, versionHash, wantsArtifact))) {
|
|
108
|
+
results.push(meta);
|
|
109
|
+
console.log(
|
|
110
|
+
`[resolveSkills] mount cache hit: ${name} (version ${versionHash.slice(0, 12)}) — skipping artifact transfer`,
|
|
111
|
+
);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
64
114
|
|
|
65
115
|
let artifactBytes: Uint8Array | undefined;
|
|
66
|
-
if (
|
|
116
|
+
if (wantsArtifact) {
|
|
67
117
|
try {
|
|
68
|
-
const resp = await client.getSkillArtifact(skill.status
|
|
118
|
+
const resp = await client.getSkillArtifact(skill.status!.artifactStorageKey);
|
|
69
119
|
if (resp.artifact && resp.artifact.length > 0) {
|
|
70
120
|
artifactBytes = resp.artifact;
|
|
71
121
|
}
|
|
@@ -77,13 +127,9 @@ export async function resolveSkills(
|
|
|
77
127
|
}
|
|
78
128
|
}
|
|
79
129
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.log(`[resolveSkills] wrote skill: ${meta.name} -> ${meta.path}`);
|
|
84
|
-
} else {
|
|
85
|
-
console.warn(`[resolveSkills] skill ${ref.org}/${ref.slug} fetched but had no skillMd content`);
|
|
86
|
-
}
|
|
130
|
+
await writeSkillMount(skill, skillDir, artifactBytes);
|
|
131
|
+
results.push(meta);
|
|
132
|
+
console.log(`[resolveSkills] wrote skill: ${name} -> ${meta.path}`);
|
|
87
133
|
} catch (err) {
|
|
88
134
|
console.warn(
|
|
89
135
|
`[resolveSkills] failed to resolve skill ${ref.org}/${ref.slug}: ${err instanceof Error ? err.message : err}`,
|
|
@@ -98,24 +144,47 @@ export async function resolveSkills(
|
|
|
98
144
|
return results;
|
|
99
145
|
}
|
|
100
146
|
|
|
101
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Whether the mount at `skillDir` already holds this version's content.
|
|
149
|
+
*
|
|
150
|
+
* Fresh means: the marker's hash matches AND the mount isn't a degraded
|
|
151
|
+
* SKILL.md-only fallback when the skill does carry an artifact. Any read or
|
|
152
|
+
* parse failure counts as stale — the remount is the safe default.
|
|
153
|
+
*/
|
|
154
|
+
async function mountIsFresh(skillDir: string, versionHash: string, wantsArtifact: boolean): Promise<boolean> {
|
|
155
|
+
try {
|
|
156
|
+
const raw = await readFile(join(skillDir, MOUNT_MARKER_FILE), "utf-8");
|
|
157
|
+
const marker = JSON.parse(raw) as Partial<MountMarker>;
|
|
158
|
+
return marker.versionHash === versionHash && (marker.artifactMounted === true || !wantsArtifact);
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* (Re)write a skill's mount directory from scratch.
|
|
166
|
+
*
|
|
167
|
+
* The directory is removed first so files deleted between versions don't
|
|
168
|
+
* linger in the mount, then SKILL.md and the artifact files are written, and
|
|
169
|
+
* the marker is stamped LAST (see MOUNT_MARKER_FILE for the crash-safety
|
|
170
|
+
* contract). SKILL.md always comes from `spec.skillMd` — the server's
|
|
171
|
+
* authoritative copy — never from the zip; both the zip's SKILL.md and any
|
|
172
|
+
* stray marker-named entry are excluded from extraction so the mount's
|
|
173
|
+
* ownership of those two files is unconditional.
|
|
174
|
+
*/
|
|
175
|
+
async function writeSkillMount(
|
|
102
176
|
skill: Skill,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const spec = skill.spec;
|
|
108
|
-
if (!spec?.skillMd) return null;
|
|
109
|
-
|
|
110
|
-
const name = spec.name || skill.metadata?.slug || "unknown";
|
|
111
|
-
const skillDir = join(skillsDir, name);
|
|
177
|
+
skillDir: string,
|
|
178
|
+
artifactBytes: Uint8Array | undefined,
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
await rm(skillDir, { recursive: true, force: true });
|
|
112
181
|
await mkdir(skillDir, { recursive: true });
|
|
113
182
|
|
|
114
|
-
|
|
115
|
-
await writeFile(skillMdPath, spec.skillMd, "utf-8");
|
|
183
|
+
await writeFile(join(skillDir, "SKILL.md"), skill.spec!.skillMd, "utf-8");
|
|
116
184
|
|
|
117
|
-
|
|
118
|
-
|
|
185
|
+
const artifactMounted = artifactBytes !== undefined && artifactBytes.length > 0;
|
|
186
|
+
if (artifactMounted) {
|
|
187
|
+
const entries = await extractZipFileEntries(artifactBytes, { exclude: ["SKILL.md", MOUNT_MARKER_FILE] });
|
|
119
188
|
for (const entry of entries) {
|
|
120
189
|
const filePath = join(skillDir, entry.path);
|
|
121
190
|
await mkdir(dirname(filePath), { recursive: true });
|
|
@@ -123,13 +192,11 @@ async function writeSkill(
|
|
|
123
192
|
}
|
|
124
193
|
}
|
|
125
194
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
path: relativePath,
|
|
132
|
-
};
|
|
195
|
+
const versionHash = skill.status?.versionHash ?? "";
|
|
196
|
+
if (versionHash !== "") {
|
|
197
|
+
const marker: MountMarker = { versionHash, artifactMounted };
|
|
198
|
+
await writeFile(join(skillDir, MOUNT_MARKER_FILE), JSON.stringify(marker), "utf-8");
|
|
199
|
+
}
|
|
133
200
|
}
|
|
134
201
|
|
|
135
202
|
/**
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
*
|
|
22
22
|
* deepagents mutates only via `write` (create/overwrite) and `edit` (modify);
|
|
23
23
|
* there is no backend delete/rename, so this observes CREATE and MODIFY. A
|
|
24
|
-
*
|
|
24
|
+
* delete-category tool consequently cannot be observed here — the approval gate
|
|
25
|
+
* records its before-bytes at authorization time instead (`captureDeleteBefore`,
|
|
26
|
+
* issue #303). A delete via shell (`rm`) stays on the approval gate as always.
|
|
25
27
|
*
|
|
26
28
|
* @since File-Change HITL Redesign (Phase 3 — CAS deep-agent wiring); sub-agent
|
|
27
29
|
* gitignored capture parity (Session 26, DD-19); shell restore (issue #248)
|
|
@@ -676,6 +676,14 @@ export async function performSetup(deps: SetupDependencies): Promise<SetupResult
|
|
|
676
676
|
isCapturablePath,
|
|
677
677
|
captureIgnored: captureMode && !!artifactStorage,
|
|
678
678
|
recordBlockedSecret: (rawPath: string) => casObserver.recordBlockedSecret(rawPath),
|
|
679
|
+
// Pre-delete byte capture (issue #303): deepagents has no backend
|
|
680
|
+
// delete method for the CAS backend to observe, so a flowing delete's
|
|
681
|
+
// before-bytes are recorded by the gate at authorization time, through
|
|
682
|
+
// the SAME shared observer — the turn boundary then reads after=null
|
|
683
|
+
// and authors a reviewable, restorable DELETE. recordBefore's own
|
|
684
|
+
// ownership predicate (gitignored-only in a git tree, everything in
|
|
685
|
+
// non-git) agrees with the gate's not-capturable condition.
|
|
686
|
+
captureDeleteBefore: (rawPath: string) => casObserver.recordBefore(rawPath),
|
|
679
687
|
unattended,
|
|
680
688
|
unattendedSkips,
|
|
681
689
|
}
|
|
@@ -110,19 +110,26 @@ export function buildSubAgentMiddleware(
|
|
|
110
110
|
stack.push(createToolTruncationMiddleware(options.toolTruncation));
|
|
111
111
|
|
|
112
112
|
if (options.approvalGate) {
|
|
113
|
-
// captureIgnored (DD-19): a sub-agent flows gitignored writes
|
|
114
|
-
// CAS
|
|
113
|
+
// captureIgnored (DD-19): a sub-agent flows gitignored writes — and, since
|
|
114
|
+
// issue #303, non-secret CAS-owned deletes — into CAS iff a CAS observer
|
|
115
|
+
// backs its filesystem backend (compileSubagents passes this as
|
|
115
116
|
// `!!casObserver`). When true, inherit the parent gate verbatim so its
|
|
116
|
-
// captureIgnored + recordBlockedSecret feed the SAME
|
|
117
|
-
// backs the sub-agent's writes. When false (default;
|
|
118
|
-
// observer), force CAS routing off and drop
|
|
119
|
-
// gitignored paths stay on the interrupt gate — a flowed
|
|
120
|
-
// unobserved backend would apply unobserved,
|
|
121
|
-
// git-tracked edits are always captured by
|
|
117
|
+
// captureIgnored + recordBlockedSecret + captureDeleteBefore feed the SAME
|
|
118
|
+
// shared observer that backs the sub-agent's writes. When false (default;
|
|
119
|
+
// non-capture mode, or no observer), force CAS routing off and drop both
|
|
120
|
+
// observer sinks so gitignored paths stay on the interrupt gate — a flowed
|
|
121
|
+
// gitignored edit on an unobserved backend would apply unobserved,
|
|
122
|
+
// unreviewable bytes. Sub-agent git-tracked edits are always captured by
|
|
123
|
+
// the backend-agnostic boundary diff.
|
|
122
124
|
stack.push(createApprovalGateMiddleware(
|
|
123
125
|
options.captureIgnored
|
|
124
126
|
? options.approvalGate
|
|
125
|
-
: {
|
|
127
|
+
: {
|
|
128
|
+
...options.approvalGate,
|
|
129
|
+
captureIgnored: false,
|
|
130
|
+
recordBlockedSecret: undefined,
|
|
131
|
+
captureDeleteBefore: undefined,
|
|
132
|
+
},
|
|
126
133
|
));
|
|
127
134
|
}
|
|
128
135
|
|