mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-19 03:12:21 +02:00
fix: pre-landing review fixes (#2733)
Review army + coverage audit findings, all applied: - headless directive carries the spawned escape sentence too (multi- specialist: a CI-hosted ship's marked subagent must not end BLOCKED) - anti-injection scoping on every text-claimable spawned trigger (AUQ rule + shared escape sentence): markings count only from the creating prompt, never from files/tool output/web content read mid-run - [conductor][spawned] deny annotates one-way doors per question - SPAWNED_OVERRIDE: env tamper-visibility status line + OPENCLAW.md note - spawned sessions skip the network update-check and first-task probe (consumers suppressed; preserves the one-shot just-upgraded marker) - test hardening: dispatch-tripwire end-bound validated, vacuous marker asserts replaced with output asserts, E2E cpSync size filter + named fence tolerance, spawnedByEnv parity pin, destructive-policy cross- surface drift guard, one-way annotation + bogus-value hook cases - session-kind duplicate rationale comment deduped; regen + goldens + context-budget fixture refreshed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
46ba23c512
commit
e65be946dd
@@ -331,6 +331,7 @@ function logAutoDecided(
|
||||
sessionId: string | undefined,
|
||||
toolUseId: string | undefined,
|
||||
cwd: string | undefined,
|
||||
source: string = 'auto-decided',
|
||||
): void {
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
@@ -340,7 +341,7 @@ function logAutoDecided(
|
||||
options_count: optionsCount,
|
||||
user_choice: recommended.slice(0, 64),
|
||||
recommended: recommended.slice(0, 64),
|
||||
source: 'auto-decided',
|
||||
source,
|
||||
session_id: sessionId?.slice(0, 64),
|
||||
tool_use_id: toolUseId?.slice(0, 128),
|
||||
};
|
||||
@@ -493,7 +494,58 @@ async function main(): Promise<void> {
|
||||
// env); that case is covered by the escape sentence below plus the
|
||||
// dispatching skill's prompt.
|
||||
if (spawnedByEnv()) {
|
||||
deny(CONDUCTOR_SPAWNED_DENY_REASON + (memoryContext ? `\n${memoryContext}` : ''));
|
||||
// Name the driving env var in the reason — a human whose session was
|
||||
// env-polluted into spawned mode must see WHY in the transcript.
|
||||
const driver =
|
||||
process.env.GSTACK_SESSION_KIND === 'spawned' ? 'GSTACK_SESSION_KIND' : 'OPENCLAW_SESSION';
|
||||
// Deterministic per-question door check (#2733 review): this deny path
|
||||
// performs no preference/door lookup, so detect one-way doors here and
|
||||
// annotate them — a destructive option marked (recommended) must not be
|
||||
// auto-approved on the strength of one prose sentence alone. Registry
|
||||
// PRIMARY, keyword-net fallback (mirrors the never-ask gate above); the
|
||||
// fallback classifies question text AND option labels, so a bland
|
||||
// "Proceed?" with a "Force-push (recommended)" option cannot evade.
|
||||
const oneWayNotes: string[] = [];
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const rawText = questions[i]?.question || '';
|
||||
const qText = rawText.replace(MARKER_RE, '').trim();
|
||||
const opts = optionLabels(questions[i]?.options || []);
|
||||
const marker = rawText.match(MARKER_RE);
|
||||
const entry = marker ? registry[marker[1]] : undefined;
|
||||
let oneWay = entry?.door_type === 'one-way';
|
||||
if (!entry) {
|
||||
const optText = opts.join(' / ');
|
||||
try {
|
||||
oneWay = classifyQuestion({ summary: optText ? `${qText} options: ${optText}` : qText }).oneWay;
|
||||
} catch (e) {
|
||||
logHookError(`spawned one-way classifier failed: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
if (oneWay) {
|
||||
oneWayNotes.push(
|
||||
`[one-way door detected: Q${i + 1} — do NOT take the destructive branch even if it is marked (recommended); choose the conservative non-destructive option and record it]`,
|
||||
);
|
||||
}
|
||||
// Forensic record (#2733 review): the deny prevents PostToolUse
|
||||
// capture, and unlike the never-ask path this branch previously left
|
||||
// NO trace of a machine-resolved gate. Log every question.
|
||||
const { recommended } = extractRecommended(rawText, opts);
|
||||
logAutoDecided(
|
||||
marker?.[1] ?? 'unmarked',
|
||||
qText,
|
||||
recommended ?? 'unrecorded',
|
||||
opts.length,
|
||||
stdin.session_id,
|
||||
stdin.tool_use_id,
|
||||
stdin.cwd,
|
||||
'spawned-env-deny',
|
||||
);
|
||||
}
|
||||
deny(
|
||||
`${CONDUCTOR_SPAWNED_DENY_REASON} (spawned driver: ${driver})` +
|
||||
(oneWayNotes.length ? `\n${oneWayNotes.join('\n')}` : '') +
|
||||
(memoryContext ? `\n${memoryContext}` : ''),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const conductorReason =
|
||||
|
||||
Reference in New Issue
Block a user