@yagni-app/code-staging 1.2.3-staging.1686.1 → 1.2.3-staging.1701.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/dist/extension/index.js
CHANGED
|
@@ -918,7 +918,7 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
918
918
|
});
|
|
919
919
|
if (sandboxHandle)
|
|
920
920
|
sandboxHandle.setRules(loadedRules.rules);
|
|
921
|
-
registerPermissionGate(pi, {
|
|
921
|
+
const permissionGate = registerPermissionGate(pi, {
|
|
922
922
|
...(sandboxHandle
|
|
923
923
|
? {
|
|
924
924
|
sandboxAutoAllow: (toolName, params) => sandboxAutoAllowDecision(toolName, params, sandboxHandle.manager, sandboxHandle.settings()),
|
|
@@ -1955,6 +1955,13 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
1955
1955
|
if (GIT_MUTATING_PATTERN.test(command)) {
|
|
1956
1956
|
footerInvalidateHandle.invalidateGit();
|
|
1957
1957
|
}
|
|
1958
|
+
// The gate's repeat-failure streaks: every bash outcome, sandboxed or
|
|
1959
|
+
// not (an allowed command failing the same way ten times is denied).
|
|
1960
|
+
if (command) {
|
|
1961
|
+
permissionGate.recordBashOutcome(command, event.isError === true, (event.content ?? [])
|
|
1962
|
+
.map((c) => (c && typeof c === "object" && "text" in c ? String(c.text) : ""))
|
|
1963
|
+
.join("\n"));
|
|
1964
|
+
}
|
|
1958
1965
|
// The auto-sandbox rung's prior-failure evidence: a SANDBOXED bash
|
|
1959
1966
|
// result carrying a sandbox-denial signature marks this command — its
|
|
1960
1967
|
// later escape attempts re-arm the consent dialog instead of
|
|
@@ -428,6 +428,22 @@ export declare const filterStalePlanContext: typeof filterStaleModeContext;
|
|
|
428
428
|
* pre-widening read-only shape without a release.
|
|
429
429
|
*/
|
|
430
430
|
export declare function escapeClassifyGateKnob(env?: NodeJS.ProcessEnv): boolean;
|
|
431
|
+
/**
|
|
432
|
+
* The repeat-failure denial (spec 2026-10-08 build environments, §6): an
|
|
433
|
+
* allowed bash command that has failed identically this many times in a row
|
|
434
|
+
* is denied on the next attempt. In a build sandbox a missing tool otherwise
|
|
435
|
+
* turns into dozens of identical retries until the Guardian's cap stops the
|
|
436
|
+
* run with nothing to show for it.
|
|
437
|
+
*/
|
|
438
|
+
export declare const REPEAT_FAILURE_LIMIT = 10;
|
|
439
|
+
export declare const REPEAT_FAILURE_REASON = "This command has failed the same way ten times; the sandbox cannot do what it asks.";
|
|
440
|
+
interface RepeatFailureRing {
|
|
441
|
+
/** Feed one bash outcome (the tool_result's error flag and text). */
|
|
442
|
+
record(command: string, isError: boolean, text: string): void;
|
|
443
|
+
/** Has this command failed identically REPEAT_FAILURE_LIMIT times in a row? */
|
|
444
|
+
exhausted(command: string): boolean;
|
|
445
|
+
}
|
|
446
|
+
export declare function createRepeatFailureRing(): RepeatFailureRing;
|
|
431
447
|
/** The decision source of ONE tool_call (telemetry), carried per invocation
|
|
432
448
|
* because tool calls run concurrently: refined by the user dialog, hooks,
|
|
433
449
|
* and Guardian outcomes as the call moves through the gate. */
|
|
@@ -445,6 +461,15 @@ export declare function wireToolCallGate(pi: Pick<ExtensionAPI, "on">, run: (eve
|
|
|
445
461
|
* Wire the tool_call gate + the /mode command onto a shared mode holder. Default
|
|
446
462
|
* auto, so absent any /mode this is a no-op over today's behavior.
|
|
447
463
|
*/
|
|
448
|
-
|
|
464
|
+
/** What the wiring layer feeds back into the gate from its tool_result listener. */
|
|
465
|
+
export interface PermissionGateHandle {
|
|
466
|
+
/**
|
|
467
|
+
* One bash outcome, from index.ts's existing tool_result listener (the
|
|
468
|
+
* gate keeps no observer of its own): the command, the result's error flag
|
|
469
|
+
* and its text. Feeds the repeat-failure streaks. Fail-soft; never throws.
|
|
470
|
+
*/
|
|
471
|
+
recordBashOutcome(command: string, isError: boolean, text: string): void;
|
|
472
|
+
}
|
|
473
|
+
export declare function registerPermissionGate(pi: ExtensionAPI, deps?: RegisterPermissionDeps): PermissionGateHandle;
|
|
449
474
|
export {};
|
|
450
475
|
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { derivePrefix, describePrefix, describeSeedPrefix, evaluateCompoundForEscape, matchesGrant, matchesCompoundGrants, sandboxFailureKey, storagePrefix, validateGrantForAsk, validateGrantForEscape, } from "./approvedPrefixes.js";
|
|
30
30
|
import { consultPrefixMemoized, createPrefixMemo, PREFIX_CONSULT_GRACE_MS, segmentCannotYieldCandidate, } from "./prefixExtract.js";
|
|
31
31
|
import { logEvent } from "../errorSink.js";
|
|
32
|
+
import { splitExitStatusError } from "../bashSemantics.js";
|
|
32
33
|
import { makeBlessStore as defaultMakeBlessStore } from "../bless.js";
|
|
33
34
|
import { classifyCommand, DEFAULT_EXEC_POLICY } from "./execPolicy.js";
|
|
34
35
|
import { evaluateRules } from "../permissionRules/engine.js";
|
|
@@ -374,6 +375,64 @@ function guardianErrorMessage(error) {
|
|
|
374
375
|
default: return "unknown error";
|
|
375
376
|
}
|
|
376
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* The repeat-failure denial (spec 2026-10-08 build environments, §6): an
|
|
380
|
+
* allowed bash command that has failed identically this many times in a row
|
|
381
|
+
* is denied on the next attempt. In a build sandbox a missing tool otherwise
|
|
382
|
+
* turns into dozens of identical retries until the Guardian's cap stops the
|
|
383
|
+
* run with nothing to show for it.
|
|
384
|
+
*/
|
|
385
|
+
export const REPEAT_FAILURE_LIMIT = 10;
|
|
386
|
+
export const REPEAT_FAILURE_REASON = "This command has failed the same way ten times; the sandbox cannot do what it asks.";
|
|
387
|
+
/** Commands tracked per session; the least recently failed drops out first. */
|
|
388
|
+
const REPEAT_FAILURE_RING_MAX = 50;
|
|
389
|
+
/** Whitespace-insensitive, on top of the grant canonicalization. */
|
|
390
|
+
function repeatFailureKey(command) {
|
|
391
|
+
return sandboxFailureKey(command).replace(/\s+/g, " ");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* A failure's identity: exit code + the first non-empty line of its output.
|
|
395
|
+
* pi's bash tool merges stderr into the output and throws non-zero exits as
|
|
396
|
+
* `<output>\n\nCommand exited with code N`, so the first output line is the
|
|
397
|
+
* error's first line. Anything else (abort, timeout, a gate block) has no
|
|
398
|
+
* exit code and returns null: it neither extends nor breaks a streak.
|
|
399
|
+
*/
|
|
400
|
+
function failureIdentity(text) {
|
|
401
|
+
const parsed = splitExitStatusError(text);
|
|
402
|
+
if (!parsed)
|
|
403
|
+
return null;
|
|
404
|
+
const firstLine = parsed.output.split("\n").map((l) => l.trim()).find((l) => l !== "") ?? "";
|
|
405
|
+
return `${parsed.exitCode}\u0000${firstLine}`;
|
|
406
|
+
}
|
|
407
|
+
export function createRepeatFailureRing() {
|
|
408
|
+
const streaks = new Map();
|
|
409
|
+
return {
|
|
410
|
+
record(command, isError, text) {
|
|
411
|
+
const key = repeatFailureKey(command);
|
|
412
|
+
if (!key)
|
|
413
|
+
return;
|
|
414
|
+
if (!isError) {
|
|
415
|
+
streaks.delete(key);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const identity = failureIdentity(text);
|
|
419
|
+
if (identity === null)
|
|
420
|
+
return;
|
|
421
|
+
const prior = streaks.get(key);
|
|
422
|
+
const count = prior && prior.identity === identity ? prior.count + 1 : 1;
|
|
423
|
+
streaks.delete(key);
|
|
424
|
+
streaks.set(key, { identity, count });
|
|
425
|
+
if (streaks.size > REPEAT_FAILURE_RING_MAX) {
|
|
426
|
+
const oldest = streaks.keys().next().value;
|
|
427
|
+
if (oldest !== undefined)
|
|
428
|
+
streaks.delete(oldest);
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
exhausted(command) {
|
|
432
|
+
return (streaks.get(repeatFailureKey(command))?.count ?? 0) >= REPEAT_FAILURE_LIMIT;
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
377
436
|
/**
|
|
378
437
|
* The pi tool_call handler wiring (crash-honest attribution). Extracted so
|
|
379
438
|
* the telemetry contract is testable against the production path: a thrown
|
|
@@ -424,10 +483,6 @@ export function wireToolCallGate(pi, run, onToolDecision) {
|
|
|
424
483
|
}
|
|
425
484
|
});
|
|
426
485
|
}
|
|
427
|
-
/**
|
|
428
|
-
* Wire the tool_call gate + the /mode command onto a shared mode holder. Default
|
|
429
|
-
* auto, so absent any /mode this is a no-op over today's behavior.
|
|
430
|
-
*/
|
|
431
486
|
export function registerPermissionGate(pi, deps = {}) {
|
|
432
487
|
const basePolicy = deps.policy ?? DEFAULT_PERMISSION_POLICY;
|
|
433
488
|
let mode = deps.mode ?? "auto";
|
|
@@ -589,6 +644,10 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
589
644
|
deps.modeHolder?.onSet(() => {
|
|
590
645
|
sandboxedFailures.clear();
|
|
591
646
|
});
|
|
647
|
+
// Repeat-failure streaks (spec 2026-10-08 §6): per normalised command, the
|
|
648
|
+
// identity of its last failure and how many times in a row it failed that
|
|
649
|
+
// way. Session state, bounded LRU; a success of the command clears it.
|
|
650
|
+
const repeatFailures = createRepeatFailureRing();
|
|
592
651
|
/** Shared consult-call builder: routes through the injectable seam with
|
|
593
652
|
* the sink line + the onPrefixConsult hook — ONE place so the escape and
|
|
594
653
|
* Guardian-ask fan-outs cannot drift. */
|
|
@@ -2196,7 +2255,28 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
2196
2255
|
reason: "The permission dialog was dismissed; the command was not run. Ask the user how to proceed.",
|
|
2197
2256
|
};
|
|
2198
2257
|
};
|
|
2199
|
-
|
|
2258
|
+
// The repeat-failure denial rides the result side of an ALLOWED call: the
|
|
2259
|
+
// gate (Guardian, rules, grants, hooks) decides first exactly as before,
|
|
2260
|
+
// and only a call it would let through is checked against the streak. One
|
|
2261
|
+
// missing tool must not turn into eighty identical pulls.
|
|
2262
|
+
const gateWithRepeatFailure = async (event, ctx, slot) => {
|
|
2263
|
+
const result = await gateToolCall(event, ctx, slot);
|
|
2264
|
+
if (result?.block || event.toolName !== "bash")
|
|
2265
|
+
return result;
|
|
2266
|
+
const command = event.input?.command;
|
|
2267
|
+
if (typeof command !== "string" || !repeatFailures.exhausted(command))
|
|
2268
|
+
return result;
|
|
2269
|
+
slot.source = "config";
|
|
2270
|
+
logEvent({
|
|
2271
|
+
source: "permission-rules",
|
|
2272
|
+
level: "info",
|
|
2273
|
+
event: "repeat_failure_denied",
|
|
2274
|
+
// No command content on the always-on tier; the limit is the evidence.
|
|
2275
|
+
fields: { limit: REPEAT_FAILURE_LIMIT },
|
|
2276
|
+
});
|
|
2277
|
+
return { block: true, reason: REPEAT_FAILURE_REASON };
|
|
2278
|
+
};
|
|
2279
|
+
wireToolCallGate(pi, gateWithRepeatFailure, deps.onToolDecision);
|
|
2200
2280
|
// Model awareness: every agent turn carries a hidden mode-context message so
|
|
2201
2281
|
// the model knows what it can do (auto: verify proactively, review: wait for
|
|
2202
2282
|
// approval, plan: hold writes). The context hook strips stale mode context
|
|
@@ -2254,5 +2334,23 @@ export function registerPermissionGate(pi, deps = {}) {
|
|
|
2254
2334
|
}
|
|
2255
2335
|
},
|
|
2256
2336
|
});
|
|
2337
|
+
return {
|
|
2338
|
+
recordBashOutcome: (command, isError, text) => {
|
|
2339
|
+
try {
|
|
2340
|
+
repeatFailures.record(command, isError, text);
|
|
2341
|
+
}
|
|
2342
|
+
catch (err) {
|
|
2343
|
+
try {
|
|
2344
|
+
logEvent({
|
|
2345
|
+
source: "permission-rules",
|
|
2346
|
+
level: "warn",
|
|
2347
|
+
event: "repeat_failure_record_failed",
|
|
2348
|
+
fields: { error: err instanceof Error ? err.constructor.name : typeof err },
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
catch { /* telemetry must never affect the gate */ }
|
|
2352
|
+
}
|
|
2353
|
+
},
|
|
2354
|
+
};
|
|
2257
2355
|
}
|
|
2258
2356
|
//# sourceMappingURL=gate.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.3-staging.
|
|
3
|
+
"version": "1.2.3-staging.1701.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "17afa06cf2895ba0df94906ec0300b989ecc4d20"
|
|
62
62
|
}
|