agy-context-saver 1.2.1 → 1.3.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 +12 -0
- package/mcp/index.js +1 -1
- package/package.json +42 -42
- package/scripts/execution-guard-hook.mjs +90 -25
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to `Agy-Context-Saver` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-10-07
|
|
9
|
+
|
|
10
|
+
### Governance & Loophole Elimination
|
|
11
|
+
- **Proportional Backoff Engine**: Eliminated the "Pendulum Freeze Trap" (where total bans on task polling caused hung/deadlocked background commands to run unmonitored forever) by implementing an exponential backoff curve ($0\text{s} \to 30\text{s} \to 75\text{s} \to 188\text{s} \to 469\text{s} \to 600\text{s}$). Tasks can be safely diagnosed when silent without enabling busy-waiting loops.
|
|
12
|
+
- **Watchdog Timer Coordination**: Coordinated `schedule` watchdog timers with the active proportional backoff window. Timers shorter than the backoff cooldown are rejected with detailed feedback, while legitimate watchdog timers ($\ge 30\text{s}$) are permitted and auto-cancel via native Reactive Wakeup.
|
|
13
|
+
- **Shell Wrapper Unwrapping (LH-01-B)**: Added recursive shell wrapper unwrapping (`cmd /c`, `powershell -Command`, `pwsh -c`, `bash -c`) inside `run_command` so wrapped commands are transparently rewritten to RTK equivalents instead of bypassing token optimization.
|
|
14
|
+
- **Internal State Shell Guard (LH-02-B)**: Added strict shell command guards blocking direct shell access (`cat`, `Get-Content`, `type`) to internal Antigravity execution state (`.system_generated/`), routing transcript queries to MCP tools.
|
|
15
|
+
- **Benchmark Shell Mutation Protection (LH-09-B)**: Extended benchmark and evaluator protections to shell redirect and removal operations (`>`, `rm`, `del`, `Remove-Item`), preventing benchmark tampering via the terminal.
|
|
16
|
+
- **Single Source of Truth Ledger**: Authored comprehensive `LEDGER.md` tracking all 10 historical loopholes (LH-01 to LH-10), remnant escape routes, backoff mathematical formulas, and architectural invariants.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
8
20
|
## [1.2.1] - 2026-10-07
|
|
9
21
|
|
|
10
22
|
### Documentation & Developer Experience
|
package/mcp/index.js
CHANGED
|
@@ -98,7 +98,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
98
98
|
const rulesPath = path.resolve(__dirname, "../rules/AGENTS.md");
|
|
99
99
|
|
|
100
100
|
const SERVER_NAME = "agy-context-saver";
|
|
101
|
-
const SERVER_VERSION = "1.
|
|
101
|
+
const SERVER_VERSION = "1.3.0";
|
|
102
102
|
|
|
103
103
|
// --- Tools Specification ---
|
|
104
104
|
const TOOLS = [
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "agy-context-saver",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Universal Model Context Protocol (MCP) server & lifecycle governor purpose-built for Google Antigravity",
|
|
5
|
-
"main": "mcp/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"agy-context-saver": "mcp/index.js"
|
|
8
|
-
},
|
|
9
|
-
"type": "module",
|
|
10
|
-
"scripts": {
|
|
11
|
-
"start": "node mcp/index.js",
|
|
12
|
-
"setup": "node scripts/install-register.mjs",
|
|
13
|
-
"uninstall": "node scripts/install-register.mjs --uninstall",
|
|
14
|
-
"status": "node scripts/install-register.mjs --status",
|
|
15
|
-
"test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-e2e-probes.mjs && node tests/test-performance-probes.mjs && node tests/test-installed-verification.mjs && node tests/test-lifecycle-rollback.mjs"
|
|
16
|
-
},
|
|
17
|
-
"keywords": [
|
|
18
|
-
"antigravity",
|
|
19
|
-
"mcp",
|
|
20
|
-
"model-context-protocol",
|
|
21
|
-
"gemini",
|
|
22
|
-
"context-window",
|
|
23
|
-
"lifecycle-hooks",
|
|
24
|
-
"agentic-coding"
|
|
25
|
-
],
|
|
26
|
-
"files": [
|
|
27
|
-
"mcp",
|
|
28
|
-
"scripts",
|
|
29
|
-
"rules",
|
|
30
|
-
"skills",
|
|
31
|
-
"hooks.json",
|
|
32
|
-
"plugin.json",
|
|
33
|
-
"mcp_config.json",
|
|
34
|
-
"install.ps1",
|
|
35
|
-
"install.sh",
|
|
36
|
-
"README.md",
|
|
37
|
-
"CHANGELOG.md",
|
|
38
|
-
"LICENSE"
|
|
39
|
-
],
|
|
40
|
-
"author": "paragon-ux",
|
|
41
|
-
"license": "MIT"
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "agy-context-saver",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Universal Model Context Protocol (MCP) server & lifecycle governor purpose-built for Google Antigravity",
|
|
5
|
+
"main": "mcp/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agy-context-saver": "mcp/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node mcp/index.js",
|
|
12
|
+
"setup": "node scripts/install-register.mjs",
|
|
13
|
+
"uninstall": "node scripts/install-register.mjs --uninstall",
|
|
14
|
+
"status": "node scripts/install-register.mjs --status",
|
|
15
|
+
"test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-e2e-probes.mjs && node tests/test-performance-probes.mjs && node tests/test-installed-verification.mjs && node tests/test-lifecycle-rollback.mjs"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"antigravity",
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"gemini",
|
|
22
|
+
"context-window",
|
|
23
|
+
"lifecycle-hooks",
|
|
24
|
+
"agentic-coding"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"mcp",
|
|
28
|
+
"scripts",
|
|
29
|
+
"rules",
|
|
30
|
+
"skills",
|
|
31
|
+
"hooks.json",
|
|
32
|
+
"plugin.json",
|
|
33
|
+
"mcp_config.json",
|
|
34
|
+
"install.ps1",
|
|
35
|
+
"install.sh",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CHANGELOG.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"author": "paragon-ux",
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|
|
@@ -276,6 +276,20 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
276
276
|
return trimmed;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Unwrap shell wrappers (e.g. cmd /c "...", powershell -Command "...", bash -c "...") (LH-01-B)
|
|
280
|
+
const shellWrapperMatch = trimmed.match(
|
|
281
|
+
/^(cmd(?:\.exe)?\s+\/[cC]|(?:powershell|pwsh)(?:\.exe)?\s+(?:-Command|-c)|(?:bash|sh)\s+-c)\s+["']?(.+?)["']?$/i
|
|
282
|
+
);
|
|
283
|
+
if (shellWrapperMatch) {
|
|
284
|
+
const wrapperPrefix = shellWrapperMatch[1];
|
|
285
|
+
const innerCmd = shellWrapperMatch[2].trim();
|
|
286
|
+
const rewrittenInner = rewriteCommandWithRtk(innerCmd);
|
|
287
|
+
if (rewrittenInner !== innerCmd) {
|
|
288
|
+
return `${wrapperPrefix} "${rewrittenInner}"`;
|
|
289
|
+
}
|
|
290
|
+
return trimmed;
|
|
291
|
+
}
|
|
292
|
+
|
|
279
293
|
// Fast-path bypass for shell builtins, non-rewritable binaries, and powershell cmdlets
|
|
280
294
|
const firstWord = (trimmed.match(/^[^\s"']+/)?.[0] || "").toLowerCase();
|
|
281
295
|
if (
|
|
@@ -317,6 +331,17 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
317
331
|
return rewritten;
|
|
318
332
|
}
|
|
319
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Calculates the required minimum cooldown interval (in seconds)
|
|
336
|
+
* proportional to the number of diagnostic status inspections already performed.
|
|
337
|
+
* Formula: Math.min(600, Math.round(30 * Math.pow(2.5, Math.max(0, pollCount - 1))))
|
|
338
|
+
* Levels: Level 0 -> 0s (baseline), Level 1 -> 30s, Level 2 -> 75s, Level 3 -> 188s, Level 4 -> 469s, Level 5+ -> 600s
|
|
339
|
+
*/
|
|
340
|
+
function getRequiredBackoffSeconds(pollCount) {
|
|
341
|
+
if (pollCount <= 0) return 0;
|
|
342
|
+
return Math.min(600, Math.round(30 * Math.pow(2.5, Math.max(0, pollCount - 1))));
|
|
343
|
+
}
|
|
344
|
+
|
|
320
345
|
// --- Main Hook Entrypoint ---
|
|
321
346
|
let isGovernedTool = false;
|
|
322
347
|
|
|
@@ -364,6 +389,29 @@ try {
|
|
|
364
389
|
});
|
|
365
390
|
}
|
|
366
391
|
|
|
392
|
+
// LH-02-B: Direct shell access to internal Antigravity execution state (.system_generated)
|
|
393
|
+
if (isProtectedInternalState(rawCmd)) {
|
|
394
|
+
respond({
|
|
395
|
+
decision: "deny",
|
|
396
|
+
reason:
|
|
397
|
+
"Antigravity Execution Governance: Direct shell access to internal Antigravity execution state (.system_generated) is strictly prohibited to prevent transcript bloat and polling loops.\n" +
|
|
398
|
+
"- To inspect conversation transcripts: Call MCP tool read_transcript(conversationId=\"...\", mode=\"compact\") or query_transcript(...).\n" +
|
|
399
|
+
"- To inspect background tasks: Yield execution turn and rely on native Reactive Wakeup (<SYSTEM_MESSAGE>). Do not read internal task logs."
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// LH-09-B: Shell command mutation of protected benchmark artifacts
|
|
404
|
+
if (
|
|
405
|
+
(rawCmd.includes(">") || /\b(?:rm|del|Remove-Item)\b/i.test(rawCmd)) &&
|
|
406
|
+
isProtectedBenchmarkPath(rawCmd)
|
|
407
|
+
) {
|
|
408
|
+
respond({
|
|
409
|
+
decision: "force_ask",
|
|
410
|
+
reason:
|
|
411
|
+
"[BENCHMARK MUTATION DETECTED] Shell command mutation of protected evaluator/benchmark artifact requires explicit user confirmation to prevent benchmark contamination."
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
367
415
|
const rewritten = rewriteCommandWithRtk(rawCmd);
|
|
368
416
|
const isDaemon = args.IsDaemon === true || args.isDaemon === true;
|
|
369
417
|
const waitMs = args.WaitMsBeforeAsync !== undefined ? args.WaitMsBeforeAsync : args.waitMsBeforeAsync;
|
|
@@ -486,33 +534,43 @@ try {
|
|
|
486
534
|
session.taskIds.push(taskId);
|
|
487
535
|
}
|
|
488
536
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
537
|
+
const now = Date.now();
|
|
538
|
+
const requiredBackoffSec = Math.max(30, getRequiredBackoffSeconds(session.pollCount));
|
|
539
|
+
const requiredBackoffMs = requiredBackoffSec * 1000;
|
|
540
|
+
const elapsedMs = session.lastPollTime ? (now - session.lastPollTime) : Infinity;
|
|
541
|
+
const elapsedSec = Math.floor(elapsedMs / 1000);
|
|
542
|
+
|
|
543
|
+
// Premature check before backoff interval has elapsed
|
|
544
|
+
if (elapsedMs < requiredBackoffMs) {
|
|
545
|
+
session.denials++;
|
|
492
546
|
saveSessionState(convId, session);
|
|
493
|
-
respond({ decision: "allow" });
|
|
494
|
-
}
|
|
495
547
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
548
|
+
if (session.denials >= 5) {
|
|
549
|
+
respond({
|
|
550
|
+
decision: "force_ask",
|
|
551
|
+
reason: `[CIRCUIT BREAKER ACTIVATED] Autonomous loop suspended: manage_task('${action}') called prematurely ${session.denials} times during active backoff window across tasks [${session.taskIds.join(", ")}]. Stop calling tools and yield the turn for native Reactive Wakeup (<SYSTEM_MESSAGE>). User confirmation required to proceed.`
|
|
552
|
+
});
|
|
553
|
+
}
|
|
499
554
|
|
|
500
|
-
|
|
555
|
+
const remainingSec = Math.ceil((requiredBackoffMs - elapsedMs) / 1000);
|
|
501
556
|
respond({
|
|
502
|
-
decision: "
|
|
503
|
-
reason: `
|
|
557
|
+
decision: "deny",
|
|
558
|
+
reason: `Antigravity Execution Governance: Proportional Backoff Active (Level ${session.pollCount}, ${session.denials} denial${session.denials > 1 ? "s" : ""}). Diagnostic check #${session.pollCount + 1} requires at least ${requiredBackoffSec}s elapsed since previous check (${elapsedSec}s elapsed, ${remainingSec}s remaining). Tasks execute asynchronously and notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion. Stop calling tools and yield your turn now.`
|
|
504
559
|
});
|
|
505
560
|
}
|
|
506
561
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
562
|
+
// Backoff window satisfied! Allow diagnostic check and advance level:
|
|
563
|
+
session.pollCount++;
|
|
564
|
+
session.lastPollTime = now;
|
|
565
|
+
session.denials = 0;
|
|
566
|
+
saveSessionState(convId, session);
|
|
567
|
+
respond({ decision: "allow" });
|
|
511
568
|
}
|
|
512
569
|
|
|
513
570
|
if (toolName === "schedule") {
|
|
514
571
|
const prompt = String(args.Prompt || args.prompt || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
515
572
|
const cond = String(args.TimerCondition || args.timerCondition || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
573
|
+
const duration = Number(args.DurationSeconds || args.durationSeconds || args.duration || 0);
|
|
516
574
|
|
|
517
575
|
const isTaskPoll =
|
|
518
576
|
cond.startsWith("task-") ||
|
|
@@ -525,23 +583,30 @@ try {
|
|
|
525
583
|
((prompt.includes("check") || prompt.includes("status") || prompt.includes("poll") || prompt.includes("monitor") || prompt.includes("wait")) &&
|
|
526
584
|
(prompt.includes("task") || prompt.includes("process") || prompt.includes("job") || prompt.includes("test") || prompt.includes("command") || prompt.includes("suite")));
|
|
527
585
|
|
|
528
|
-
// All polling timers are strictly denied regardless of duration (LH-03)
|
|
529
586
|
if (isTaskPoll) {
|
|
530
587
|
const session = getSessionState(convId);
|
|
531
|
-
session.
|
|
532
|
-
|
|
588
|
+
const requiredBackoffSec = Math.max(30, getRequiredBackoffSeconds(session.pollCount));
|
|
589
|
+
|
|
590
|
+
// Task watchdog timers must be AT LEAST the required backoff window!
|
|
591
|
+
if (duration < requiredBackoffSec) {
|
|
592
|
+
session.denials++;
|
|
593
|
+
saveSessionState(convId, session);
|
|
594
|
+
|
|
595
|
+
if (session.denials >= 5) {
|
|
596
|
+
respond({
|
|
597
|
+
decision: "force_ask",
|
|
598
|
+
reason: `[CIRCUIT BREAKER ACTIVATED] Autonomous loop suspended: schedule polling timer has been denied ${session.denials} times. Background tasks notify you automatically upon completion. User confirmation required to proceed.`
|
|
599
|
+
});
|
|
600
|
+
}
|
|
533
601
|
|
|
534
|
-
if (session.denials >= 5) {
|
|
535
602
|
respond({
|
|
536
|
-
decision: "
|
|
537
|
-
reason: `
|
|
603
|
+
decision: "deny",
|
|
604
|
+
reason: `Antigravity Execution Governance: Task watchdog timer duration too short (${duration}s, ${session.denials} denial${session.denials > 1 ? "s" : ""}). Under Proportional Backoff (Level ${session.pollCount}), watchdog timer must be at least ${requiredBackoffSec}s. Tasks notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>). Stop calling tools and yield the turn instead.`
|
|
538
605
|
});
|
|
539
606
|
}
|
|
540
607
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
reason: "Antigravity Execution Governance: Scheduling timers to poll or monitor background tasks is prohibited regardless of duration. Background tasks notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>). Stop calling tools and yield the turn instead."
|
|
544
|
-
});
|
|
608
|
+
// If duration >= requiredBackoffSec, watchdog timer is permitted!
|
|
609
|
+
respond({ decision: "allow" });
|
|
545
610
|
}
|
|
546
611
|
|
|
547
612
|
// Non-polling timers (e.g. user-requested reminders) are allowed
|