agy-context-saver 1.5.0 → 1.6.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/CHANGELOG.md +51 -0
- package/README.md +43 -2
- package/assets/logo.svg +51 -0
- package/install.ps1 +86 -42
- package/mcp/index.js +174 -30
- package/package.json +19 -2
- package/plugin.json +9 -2
- package/rules/AGENTS.md +9 -4
- package/scripts/execution-guard-hook.mjs +174 -22
- package/scripts/install-register.mjs +177 -63
- package/scripts/provision-rtk.mjs +67 -14
- package/scripts/release-preflight.mjs +147 -0
- package/skills/context-governance/SKILL.md +26 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-context-saver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "Universal Model Context Protocol (MCP) server & lifecycle governor purpose-built for Google Antigravity",
|
|
5
5
|
"main": "mcp/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,23 @@
|
|
|
12
12
|
"setup": "node scripts/install-register.mjs",
|
|
13
13
|
"uninstall": "node scripts/install-register.mjs --uninstall",
|
|
14
14
|
"status": "node scripts/install-register.mjs --status",
|
|
15
|
-
"
|
|
15
|
+
"preflight": "node scripts/release-preflight.mjs",
|
|
16
|
+
"test:synergy": "node tests/test-waymark-synergy.mjs",
|
|
17
|
+
"test": "node tests/test-hook.mjs && node tests/test-mcp.mjs && node tests/test-output-governance.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 && node tests/test-subagent-trials.mjs && node tests/test-check-only-readonly.mjs && node tests/test-malformed-config-preservation.mjs && node tests/test-waymark-synergy.mjs"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/paragon-ux/Agy-Context-Saver.git"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://paragon-ux.github.io/agy-context-saver/",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/paragon-ux/Agy-Context-Saver/issues"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
16
32
|
},
|
|
17
33
|
"keywords": [
|
|
18
34
|
"antigravity",
|
|
@@ -24,6 +40,7 @@
|
|
|
24
40
|
"agentic-coding"
|
|
25
41
|
],
|
|
26
42
|
"files": [
|
|
43
|
+
"assets",
|
|
27
44
|
"mcp",
|
|
28
45
|
"scripts",
|
|
29
46
|
"rules",
|
package/plugin.json
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-context-saver",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"displayName": "Antigravity Context Shield & Governor",
|
|
4
|
+
"version": "1.6.1",
|
|
5
|
+
"description": "Deterministic background polling governor, RTK terminal optimizer, and subagent context preservation shield for Antigravity.",
|
|
6
|
+
"logo": "assets/logo.svg",
|
|
7
|
+
"suggestedPrompts": [
|
|
8
|
+
"Audit context health and token consumption for this session",
|
|
9
|
+
"Formulate an isolated subagent brief under Pointer-Over-Wire protocol",
|
|
10
|
+
"Inspect recent conversation turns without raw JSON transcript bloat"
|
|
11
|
+
]
|
|
5
12
|
}
|
package/rules/AGENTS.md
CHANGED
|
@@ -55,12 +55,14 @@
|
|
|
55
55
|
- **Fail-Fast Flags are Opt-In**: Fail-fast flags (`-x`, `--maxfail=1`) are opt-in. Use them when quickly debugging a localized failure, but omit them when assessing the full blast radius across the codebase to avoid the "whack-a-mole" loop.
|
|
56
56
|
- **Coordinated Watchdog Mandatory for Full Sweeps**: When running full multi-minute test sweeps without `-x`, agents **MUST** pair execution with the **Coordinated Watchdog Protocol** (`schedule(DurationSeconds=45, ...)`) and output a **Pre-Yield Status Card**. Never yield silently.
|
|
57
57
|
- **Subagent Delegation for Deep Audits**: When a comprehensive repository-wide test audit is required, delegate the sweep to a subagent (`invoke_subagent`) to keep the main thread responsive.
|
|
58
|
-
- **Subagent Delegation
|
|
58
|
+
- **Subagent Delegation & Pointer-Over-Wire (POW)**:
|
|
59
|
+
- Delegate heavy multi-file exploration, deep research, and repo sweeps to subagents (`invoke_subagent`). Subagents absorb intermediate steps and return high-signal summaries.
|
|
60
|
+
- **POW Protocol**: Payloads or deliverables exceeding 1 KB must be written to workspace scratch files (e.g. `scratch/<task>_findings.md` or artifacts). Transmit **ONLY** an Executive Summary (3–5 sentences), a Scoreboard / Verdict table, and a clickable file link (`file:///...`) in `send_message` to prevent transport truncation and parent context bloat.
|
|
59
61
|
- **Benchmark / Evaluator Integrity**: Never mutate benchmark definitions, prompt templates, or scoring artifacts during an active evaluation without explicit user confirmation.
|
|
60
62
|
|
|
61
63
|
---
|
|
62
64
|
|
|
63
|
-
##
|
|
65
|
+
## Appendix: Zero-Guesswork Testing & Verification Playbook
|
|
64
66
|
When an agent or developer tests this framework, follow this exact sequence without guessing:
|
|
65
67
|
|
|
66
68
|
1. **Verify Live Registration & Health (Fast, non-blocking)**:
|
|
@@ -75,21 +77,24 @@ When an agent or developer tests this framework, follow this exact sequence with
|
|
|
75
77
|
```bash
|
|
76
78
|
npm test
|
|
77
79
|
```
|
|
78
|
-
*Expected*: All
|
|
80
|
+
*Expected*: All 8 test suites pass (Hook unit tests, MCP protocol tests, output governance, E2E probes, performance probes, live system validation, lifecycle rollback, subagent trials).
|
|
79
81
|
|
|
80
82
|
3. **Verify Individual Layers On-Demand**:
|
|
81
83
|
- Governance Hook: `node tests/test-hook.mjs`
|
|
82
84
|
- MCP Protocol & JSON-RPC: `node tests/test-mcp.mjs`
|
|
85
|
+
- Output Governance: `node tests/test-output-governance.mjs`
|
|
83
86
|
- E2E Probes: `node tests/test-e2e-probes.mjs`
|
|
87
|
+
- Performance Probes: `node tests/test-performance-probes.mjs`
|
|
84
88
|
- Live System Checks: `node tests/test-installed-verification.mjs`
|
|
85
89
|
- Lifecycle & Rollback: `node tests/test-lifecycle-rollback.mjs`
|
|
90
|
+
- Subagent Delegation Trials: `node tests/test-subagent-trials.mjs`
|
|
86
91
|
|
|
87
92
|
4. **Verify MCP Tools in Antigravity**:
|
|
88
93
|
- `get_installation_status`: Inspect live integration layers.
|
|
89
94
|
- `check_context_health`: Pass `{ "transcriptPath": "<path-to-transcript.jsonl>" }`.
|
|
90
95
|
- `read_transcript`: Pass `{ "conversationId": "<uuid>", "lastTurns": 3, "mode": "compact" }`.
|
|
91
96
|
- `query_transcript`: Pass `{ "conversationId": "<uuid>", "query": "<pattern>", "summaryOnly": true }`.
|
|
92
|
-
- `subagent_brief`: Pass `{ "objective": "<goal>", "scopeFiles": ["..."] }
|
|
97
|
+
- `subagent_brief`: Pass `{ "objective": "<goal>", "role": "<role>", "scopeFiles": ["..."] }` or pass multiple via `{ "subagents": [...] }` to generate ready-to-use `invoke_subagent` JSON payloads.
|
|
93
98
|
|
|
94
99
|
5. **Operational Anti-Patterns (What NOT to do)**:
|
|
95
100
|
- ❌ **Do NOT run `npm start` or `node mcp/index.js` interactively**: It starts the stdio JSON-RPC daemon and waits indefinitely on stdin.
|
|
@@ -32,6 +32,7 @@ import os from "node:os";
|
|
|
32
32
|
import path from "node:path";
|
|
33
33
|
import process from "node:process";
|
|
34
34
|
import { spawnSync } from "node:child_process";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
35
36
|
|
|
36
37
|
function respond(obj) {
|
|
37
38
|
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
@@ -207,8 +208,12 @@ function getTaskLogMatch(text) {
|
|
|
207
208
|
const TASK_TAIL_BYTES = 262144; // transcript tail scanned first (256 KB)
|
|
208
209
|
const TASK_QUIET_MS = 300000; // log-mtime fallback: quiet for 5 min => treated as finished
|
|
209
210
|
|
|
211
|
+
function getProfileHomeDir() {
|
|
212
|
+
return process.env.AGY_TEST_PROFILE_DIR || os.homedir();
|
|
213
|
+
}
|
|
214
|
+
|
|
210
215
|
function getBrainDir() {
|
|
211
|
-
return process.env.AGY_BRAIN_DIR || path.join(
|
|
216
|
+
return process.env.AGY_BRAIN_DIR || path.join(getProfileHomeDir(), ".gemini", "antigravity", "brain");
|
|
212
217
|
}
|
|
213
218
|
|
|
214
219
|
/** Reads the last `bytes` of a file. Returns { text, whole } or null. */
|
|
@@ -280,19 +285,54 @@ function getTaskState(match, convId) {
|
|
|
280
285
|
return "UNKNOWN";
|
|
281
286
|
}
|
|
282
287
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
function isCompatibleRtkVersion(versionStr) {
|
|
289
|
+
if (!versionStr || typeof versionStr !== "string") return false;
|
|
290
|
+
const match = versionStr.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
291
|
+
if (!match) return false;
|
|
292
|
+
const major = parseInt(match[1], 10);
|
|
293
|
+
const minor = parseInt(match[2], 10);
|
|
294
|
+
return major === 0 && (minor === 50 || minor === 51);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getLocalRtkCandidate() {
|
|
298
|
+
const names = os.platform() === "win32" ? ["rtk.exe", "rtk.cmd"] : ["rtk"];
|
|
299
|
+
const hookDir = path.dirname(fileURLToPath(import.meta.url));
|
|
300
|
+
for (const exeName of names) {
|
|
301
|
+
const pluginBin = path.join(getProfileHomeDir(), ".gemini", "config", "plugins", "agy-context-saver", "bin", exeName);
|
|
302
|
+
if (fs.existsSync(pluginBin)) return pluginBin;
|
|
303
|
+
}
|
|
304
|
+
for (const exeName of names) {
|
|
305
|
+
const repoBin = path.resolve(hookDir, "..", "bin", exeName);
|
|
306
|
+
if (fs.existsSync(repoBin)) return repoBin;
|
|
307
|
+
}
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function findFirstRtkOnPath() {
|
|
312
|
+
const pathEnv = process.env.PATH || process.env.Path || "";
|
|
313
|
+
const dirs = pathEnv.split(path.delimiter).filter(Boolean);
|
|
314
|
+
const exts = os.platform() === "win32" ? [".exe", ".cmd", ".bat"] : [""];
|
|
315
|
+
for (const dir of dirs) {
|
|
316
|
+
for (const ext of exts) {
|
|
317
|
+
const candidate = path.join(dir, `rtk${ext}`);
|
|
318
|
+
try {
|
|
319
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
320
|
+
return candidate;
|
|
321
|
+
}
|
|
322
|
+
} catch {}
|
|
323
|
+
}
|
|
294
324
|
}
|
|
295
|
-
return
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function spawnRtk(bin, args, opts) {
|
|
329
|
+
if (!bin) return { error: Object.assign(new Error("ENOENT"), { code: "ENOENT" }), status: null, stdout: "", stderr: "" };
|
|
330
|
+
if (os.platform() === "win32" && /\.(cmd|bat)$/i.test(bin)) {
|
|
331
|
+
const quotedBin = /\s/.test(bin) ? `"${bin}"` : bin;
|
|
332
|
+
const quotedArgs = args.map((a) => (/\s|["&|<>^]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a)).join(" ");
|
|
333
|
+
return spawnSync("cmd.exe", ["/d", "/s", "/c", `"${quotedBin} ${quotedArgs}"`], { ...opts, windowsVerbatimArguments: true });
|
|
334
|
+
}
|
|
335
|
+
return spawnSync(bin, args, opts);
|
|
296
336
|
}
|
|
297
337
|
|
|
298
338
|
const NON_REWRITABLE_BINARIES = new Set([
|
|
@@ -332,18 +372,77 @@ function loadRewriteCache() {
|
|
|
332
372
|
return inMemoryRewriteCache;
|
|
333
373
|
}
|
|
334
374
|
|
|
375
|
+
function resolveCompatibleRtk() {
|
|
376
|
+
const localCandidate = getLocalRtkCandidate();
|
|
377
|
+
const pathCandidate = findFirstRtkOnPath();
|
|
378
|
+
const envSig = `${pathCandidate}::${localCandidate}`;
|
|
379
|
+
const cache = loadRewriteCache();
|
|
380
|
+
if (cache.__envSig === envSig && cache.__rtkResolved !== undefined) {
|
|
381
|
+
return cache.__rtkResolved;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const k of Object.keys(cache)) delete cache[k];
|
|
385
|
+
|
|
386
|
+
const opts = { encoding: "utf-8", timeout: 2000, windowsHide: true };
|
|
387
|
+
let resolved = null;
|
|
388
|
+
|
|
389
|
+
const pathRes = pathCandidate ? spawnRtk(pathCandidate, ["--version"], opts) : { status: 1, stdout: "" };
|
|
390
|
+
const pathCompatible = !pathRes.error && pathRes.status === 0 && isCompatibleRtkVersion(pathRes.stdout);
|
|
391
|
+
|
|
392
|
+
if (pathCompatible) {
|
|
393
|
+
resolved = { exe: pathCandidate, prefix: "rtk" };
|
|
394
|
+
} else if (localCandidate) {
|
|
395
|
+
const localRes = spawnRtk(localCandidate, ["--version"], opts);
|
|
396
|
+
const localCompatible = !localRes.error && localRes.status === 0 && isCompatibleRtkVersion(localRes.stdout);
|
|
397
|
+
if (localCompatible) {
|
|
398
|
+
const normalizedLocal = localCandidate.replace(/\\/g, "/");
|
|
399
|
+
const prefix = /\s/.test(normalizedLocal) ? `"${normalizedLocal}"` : normalizedLocal;
|
|
400
|
+
resolved = { exe: localCandidate, prefix };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
cache.__envSig = envSig;
|
|
405
|
+
cache.__rtkResolved = resolved;
|
|
406
|
+
try {
|
|
407
|
+
const tmp = `${REWRITE_CACHE_FILE}.${process.pid}.tmp`;
|
|
408
|
+
fs.writeFileSync(tmp, JSON.stringify(cache), "utf-8");
|
|
409
|
+
try {
|
|
410
|
+
fs.renameSync(tmp, REWRITE_CACHE_FILE);
|
|
411
|
+
} catch {
|
|
412
|
+
fs.writeFileSync(REWRITE_CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
413
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
414
|
+
}
|
|
415
|
+
} catch {}
|
|
416
|
+
|
|
417
|
+
return resolved;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function runRtkRewrite(cmd, resolvedRtk) {
|
|
421
|
+
const rtk = resolvedRtk || resolveCompatibleRtk();
|
|
422
|
+
if (!rtk) return { status: 1, stdout: "", stderr: "No compatible RTK binary available" };
|
|
423
|
+
const opts = { encoding: "utf-8", timeout: 2000, windowsHide: true };
|
|
424
|
+
return spawnRtk(rtk.exe, ["rewrite", cmd], opts);
|
|
425
|
+
}
|
|
426
|
+
|
|
335
427
|
/**
|
|
336
428
|
* Persists one rewrite result. Re-reads the file first so concurrent hook processes merge instead
|
|
337
429
|
* of overwriting each other, trims to the newest entries, and writes atomically (temp + rename).
|
|
338
430
|
*/
|
|
339
431
|
function rememberRewrite(cmd, rewritten) {
|
|
340
|
-
loadRewriteCache()
|
|
432
|
+
const cache = loadRewriteCache();
|
|
433
|
+
cache[cmd] = rewritten;
|
|
341
434
|
try {
|
|
342
435
|
const disk = readRewriteCacheFile();
|
|
436
|
+
const envSig = cache.__envSig ?? disk.__envSig;
|
|
437
|
+
const rtkResolved = cache.__rtkResolved ?? disk.__rtkResolved;
|
|
438
|
+
delete disk.__envSig;
|
|
439
|
+
delete disk.__rtkResolved;
|
|
343
440
|
delete disk[cmd];
|
|
344
441
|
disk[cmd] = rewritten;
|
|
345
442
|
const keys = Object.keys(disk);
|
|
346
443
|
for (const k of keys.slice(0, Math.max(0, keys.length - REWRITE_CACHE_MAX))) delete disk[k];
|
|
444
|
+
if (envSig !== undefined) disk.__envSig = envSig;
|
|
445
|
+
if (rtkResolved !== undefined) disk.__rtkResolved = rtkResolved;
|
|
347
446
|
const tmp = `${REWRITE_CACHE_FILE}.${process.pid}.tmp`;
|
|
348
447
|
fs.writeFileSync(tmp, JSON.stringify(disk), "utf-8");
|
|
349
448
|
try {
|
|
@@ -364,7 +463,11 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
364
463
|
if (!trimmed) return rawCmd;
|
|
365
464
|
|
|
366
465
|
// Already prefixed with rtk
|
|
367
|
-
if (/^rtk(?:\.exe)
|
|
466
|
+
if (/^rtk(?:\.exe)?(?=\s|$)/i.test(trimmed)) {
|
|
467
|
+
const rtk = resolveCompatibleRtk();
|
|
468
|
+
if (rtk && rtk.prefix !== "rtk") {
|
|
469
|
+
return trimmed.replace(/^rtk(?:\.exe)?(?=\s|$)/i, rtk.prefix);
|
|
470
|
+
}
|
|
368
471
|
return trimmed;
|
|
369
472
|
}
|
|
370
473
|
|
|
@@ -382,7 +485,7 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
382
485
|
return trimmed;
|
|
383
486
|
}
|
|
384
487
|
|
|
385
|
-
|
|
488
|
+
// Fast-path bypass for shell builtins, non-rewritable binaries, and powershell cmdlets
|
|
386
489
|
const firstWord = (trimmed.match(/^[^\s"']+/)?.[0] || "").toLowerCase();
|
|
387
490
|
if (
|
|
388
491
|
NON_REWRITABLE_BINARIES.has(firstWord) ||
|
|
@@ -397,6 +500,9 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
397
500
|
return trimmed;
|
|
398
501
|
}
|
|
399
502
|
|
|
503
|
+
const rtk = resolveCompatibleRtk();
|
|
504
|
+
if (!rtk) return trimmed;
|
|
505
|
+
|
|
400
506
|
// Check persistent cache
|
|
401
507
|
const cache = loadRewriteCache();
|
|
402
508
|
if (cache[trimmed] !== undefined) {
|
|
@@ -406,7 +512,7 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
406
512
|
let rewritten = trimmed;
|
|
407
513
|
let cacheable = false;
|
|
408
514
|
try {
|
|
409
|
-
const res = runRtkRewrite(trimmed);
|
|
515
|
+
const res = runRtkRewrite(trimmed, rtk);
|
|
410
516
|
// Never cache timeouts, spawn errors or signals: a transient failure must not pin a command
|
|
411
517
|
// to "no rewrite" for the lifetime of the cache.
|
|
412
518
|
cacheable = !res.error && !res.signal && typeof res.status === "number" && res.status >= 0 && res.status <= 3;
|
|
@@ -414,6 +520,9 @@ function rewriteCommandWithRtk(rawCmd) {
|
|
|
414
520
|
// RTK exits 0 or 3 when rewritten, with new command on stdout
|
|
415
521
|
if ((res.status === 0 || res.status === 3) && res.stdout && res.stdout.trim()) {
|
|
416
522
|
rewritten = res.stdout.trim();
|
|
523
|
+
if (rtk.prefix !== "rtk") {
|
|
524
|
+
rewritten = rewritten.replace(/^rtk(?:\.exe)?(?=\s|$)/i, rtk.prefix);
|
|
525
|
+
}
|
|
417
526
|
}
|
|
418
527
|
} catch {}
|
|
419
528
|
|
|
@@ -542,7 +651,7 @@ try {
|
|
|
542
651
|
|
|
543
652
|
const rewritten = rewriteCommandWithRtk(rawCmd);
|
|
544
653
|
const effectiveCmd = rewritten;
|
|
545
|
-
const isRtkRead = /^rtk(?:\.exe)?\s+read\b/i.test(effectiveCmd);
|
|
654
|
+
const isRtkRead = /^(?:["']?[^\s"']*[/\\])?rtk(?:\.exe|\.cmd|\.bat)?["']?\s+read\b/i.test(effectiveCmd);
|
|
546
655
|
|
|
547
656
|
// Check 3: Step Output Spillover Safe Harbor (rtk read only; it clamps + windows output)
|
|
548
657
|
if (isSpillover && !isRtkRead) {
|
|
@@ -655,14 +764,57 @@ try {
|
|
|
655
764
|
respond({ decision: "allow" });
|
|
656
765
|
}
|
|
657
766
|
|
|
658
|
-
//
|
|
659
|
-
|
|
767
|
+
// 2e. Bounded Slice Safe Harbor for view_file
|
|
768
|
+
// Allow view_file IF AND ONLY IF it is a bounded slice inspection (StartLine and EndLine <= 150 lines)
|
|
769
|
+
if (toolName === "view_file") {
|
|
770
|
+
const rawStart = args.StartLine !== undefined ? args.StartLine : args.start_line;
|
|
771
|
+
const rawEnd = args.EndLine !== undefined ? args.EndLine : args.end_line;
|
|
772
|
+
const hasSliceParams = typeof rawStart !== "undefined" || typeof rawEnd !== "undefined";
|
|
773
|
+
|
|
774
|
+
if (hasSliceParams) {
|
|
775
|
+
const startLine = Number(rawStart);
|
|
776
|
+
const endLine = Number(rawEnd);
|
|
777
|
+
|
|
778
|
+
if (
|
|
779
|
+
typeof rawStart !== "undefined" &&
|
|
780
|
+
typeof rawEnd !== "undefined" &&
|
|
781
|
+
Number.isInteger(startLine) &&
|
|
782
|
+
Number.isInteger(endLine) &&
|
|
783
|
+
startLine > 0 &&
|
|
784
|
+
endLine >= startLine
|
|
785
|
+
) {
|
|
786
|
+
const span = endLine - startLine + 1;
|
|
787
|
+
const MAX_SAFE_SLICE_LINES = 150;
|
|
788
|
+
if (span <= MAX_SAFE_SLICE_LINES) {
|
|
789
|
+
respond({ decision: "allow" });
|
|
790
|
+
}
|
|
791
|
+
respond({
|
|
792
|
+
decision: "deny",
|
|
793
|
+
reason: `Antigravity Governance: Oversized file slice via 'view_file' (${span} lines > ${MAX_SAFE_SLICE_LINES} lines max safe window). Reduce line range or run 'rtk read ${target || "<file>"}' via run_command instead.`
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
respond({
|
|
798
|
+
decision: "deny",
|
|
799
|
+
reason: `Antigravity Governance: Invalid slice range via 'view_file'. Ensure StartLine and EndLine are positive integers with EndLine >= StartLine (max 150 lines), or run 'rtk read ${target || "<file>"}' via run_command instead.`
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Default when no slice attempted: keep hook identical to original behavior (zero slice advertisement)
|
|
660
804
|
respond({
|
|
661
805
|
decision: "deny",
|
|
662
806
|
reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target || "<file>"}' via run_command instead.`
|
|
663
807
|
});
|
|
664
808
|
}
|
|
665
809
|
|
|
810
|
+
// 2f. Hard-Route Full-File / Other Workspace Code Inspection to Canonical RTK Commands (LH-07)
|
|
811
|
+
if (toolName === "read_file") {
|
|
812
|
+
respond({
|
|
813
|
+
decision: "deny",
|
|
814
|
+
reason: `Antigravity Governance: Native file inspection via 'read_file' bypasses token optimization. Run 'rtk read ${target || "<file>"}' via run_command instead.`
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
666
818
|
if (toolName === "read_many_files") {
|
|
667
819
|
respond({
|
|
668
820
|
decision: "deny",
|
|
@@ -706,8 +858,8 @@ try {
|
|
|
706
858
|
if (toolName === "manage_task") {
|
|
707
859
|
const action = String(args.Action || args.action || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
708
860
|
|
|
709
|
-
// kill, send_input are always allowed immediately
|
|
710
|
-
if (action !== "status"
|
|
861
|
+
// kill, send_input, and list are always allowed immediately
|
|
862
|
+
if (action !== "status") {
|
|
711
863
|
respond({ decision: "allow" });
|
|
712
864
|
}
|
|
713
865
|
|