agy-context-saver 0.0.0-stage → 1.2.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 +60 -0
- package/LICENSE +21 -0
- package/README.md +172 -2
- package/hooks.json +16 -0
- package/install.ps1 +42 -0
- package/install.sh +6 -0
- package/mcp/index.js +1085 -0
- package/mcp/instructions.md +30 -0
- package/mcp_config.json +8 -0
- package/package.json +42 -6
- package/plugin.json +5 -0
- package/rules/AGENTS.md +78 -0
- package/scripts/execution-guard-hook.mjs +575 -0
- package/scripts/install-register.mjs +620 -0
- package/scripts/provision-rtk.mjs +120 -0
- package/skills/context-governance/SKILL.md +130 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agy-Context-Saver: Intelligent Execution Governor Hook for Antigravity
|
|
5
|
+
*
|
|
6
|
+
* Closed Execution Topology & Governance Invariants:
|
|
7
|
+
* 1. RTK Transparent Command Optimization:
|
|
8
|
+
* - Offloads 100% of command output reduction to RTK (Rust Token Killer).
|
|
9
|
+
* - Intercepts native run_command and calls `rtk rewrite <cmd>`.
|
|
10
|
+
* - Non-daemon commands maintain WaitMsBeforeAsync=10000 for synchronous completion.
|
|
11
|
+
* 2. Hard-Routed Native File/Search Inspection (LH-07):
|
|
12
|
+
* - view_file, read_file, read_many_files -> Deny and route to `rtk read <file>`.
|
|
13
|
+
* - grep_search -> Deny and route to `rtk grep <pattern>`.
|
|
14
|
+
* - find_by_name -> Deny and route to `rtk find <path>`.
|
|
15
|
+
* - list_dir -> Deny and route to `rtk ls` or `rtk tree`.
|
|
16
|
+
* - Safe Harbor: Documented Antigravity special files (SKILL.md, brain artifacts, configs) are allowed.
|
|
17
|
+
* 3. Root-Based Antigravity Internal State Protection (LH-02):
|
|
18
|
+
* - Hard denial for any inspection of `.system_generated/` (transcripts, task logs, progress).
|
|
19
|
+
* - Directed to Agy MCP tools `read_transcript` / `query_transcript`.
|
|
20
|
+
* 4. Closed Lifecycle Governance (LH-03, LH-04, LH-05, LH-06):
|
|
21
|
+
* - Session-wide ledger without cooldown resets, task-ID resets, or duration exemptions.
|
|
22
|
+
* - Enforces native Reactive Wakeup (<SYSTEM_MESSAGE>) for background tasks.
|
|
23
|
+
* - Circuit Breaker: 5 cumulative denials trigger `force_ask` to halt runaway autonomous loops.
|
|
24
|
+
* 5. Benchmark & Evaluator Mutation Protection (LH-09):
|
|
25
|
+
* - Requires user approval before mutating benchmark definitions, evaluators, or scoring files.
|
|
26
|
+
* 6. Fail-Closed on Governed Operations (LH-10):
|
|
27
|
+
* - Governed tool failures deny instead of silently allowing.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import os from "node:os";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
import process from "node:process";
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
|
|
36
|
+
function respond(obj) {
|
|
37
|
+
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const GOVERNED_TOOLS = new Set([
|
|
42
|
+
"manage_task",
|
|
43
|
+
"schedule",
|
|
44
|
+
"run_command",
|
|
45
|
+
"view_file",
|
|
46
|
+
"read_file",
|
|
47
|
+
"read_many_files",
|
|
48
|
+
"grep_search",
|
|
49
|
+
"find_by_name",
|
|
50
|
+
"list_dir",
|
|
51
|
+
"replace_file_content",
|
|
52
|
+
"write_to_file"
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const STATE_TTL_MS = 3600000; // 1 hour TTL
|
|
56
|
+
const MAX_STATE_ENTRIES = 50;
|
|
57
|
+
|
|
58
|
+
function getStateFilePath(convId = "global") {
|
|
59
|
+
if (!convId || convId === "global") {
|
|
60
|
+
return path.join(os.tmpdir(), "agy-session-governance-state.json");
|
|
61
|
+
}
|
|
62
|
+
const safeId = String(convId).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
63
|
+
return path.join(os.tmpdir(), `agy-session-${safeId}.json`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getSessionState(convId = "global") {
|
|
67
|
+
const file = getStateFilePath(convId);
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(file)) {
|
|
70
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
71
|
+
const entry = (parsed && typeof parsed.pollCount === "number") ? parsed : (parsed && parsed[convId]);
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (entry && typeof entry.lastPollTime === "number" && (now - entry.lastPollTime) < STATE_TTL_MS) {
|
|
74
|
+
return entry;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
return { pollCount: 0, denials: 0, taskIds: [], lastPollTime: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function saveSessionState(convId = "global", entry) {
|
|
82
|
+
const file = getStateFilePath(convId);
|
|
83
|
+
try {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
entry.lastPollTime = now;
|
|
86
|
+
let toWrite;
|
|
87
|
+
if (!convId || convId === "global") {
|
|
88
|
+
let state = {};
|
|
89
|
+
if (fs.existsSync(file)) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
92
|
+
if (parsed && typeof parsed === "object" && typeof parsed.pollCount !== "number") {
|
|
93
|
+
state = parsed;
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
state[convId] = entry;
|
|
98
|
+
|
|
99
|
+
// Prune stale entries
|
|
100
|
+
const entries = Object.entries(state)
|
|
101
|
+
.filter(([_, v]) => v && typeof v.lastPollTime === "number" && (now - v.lastPollTime) < STATE_TTL_MS)
|
|
102
|
+
.sort((a, b) => (b[1].lastPollTime || 0) - (a[1].lastPollTime || 0))
|
|
103
|
+
.slice(0, MAX_STATE_ENTRIES);
|
|
104
|
+
|
|
105
|
+
toWrite = JSON.stringify(Object.fromEntries(entries));
|
|
106
|
+
} else {
|
|
107
|
+
toWrite = JSON.stringify(entry);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const tmpFile = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
111
|
+
fs.writeFileSync(tmpFile, toWrite, "utf-8");
|
|
112
|
+
try {
|
|
113
|
+
fs.renameSync(tmpFile, file);
|
|
114
|
+
} catch {
|
|
115
|
+
fs.writeFileSync(file, toWrite, "utf-8");
|
|
116
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
117
|
+
}
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Checks if target path references Antigravity internal state by root directory.
|
|
123
|
+
*/
|
|
124
|
+
function isProtectedInternalState(targetPath) {
|
|
125
|
+
if (!targetPath || typeof targetPath !== "string") return false;
|
|
126
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
127
|
+
return (
|
|
128
|
+
normalized.includes("/.system_generated/") ||
|
|
129
|
+
normalized.endsWith("/.system_generated") ||
|
|
130
|
+
/\.system_generated(?:$|[\/\\])/i.test(normalized)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Checks if target file is a documented Antigravity special file (skills, brain artifacts, configs).
|
|
136
|
+
*/
|
|
137
|
+
function isAntigravitySpecialFile(targetPath) {
|
|
138
|
+
if (!targetPath || typeof targetPath !== "string") return false;
|
|
139
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
140
|
+
|
|
141
|
+
// If inside .system_generated, it's ALWAYS protected internal state, never a special file
|
|
142
|
+
if (isProtectedInternalState(normalized)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Antigravity skill definitions
|
|
147
|
+
if (normalized.endsWith("/SKILL.md") || normalized === "SKILL.md" || normalized.includes("/skills/")) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Antigravity brain markdown artifacts or scratch scripts (outside .system_generated)
|
|
152
|
+
if (normalized.includes("/.gemini/antigravity/brain/")) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Gemini / Antigravity plugin, MCP, or configuration files
|
|
157
|
+
if (normalized.includes("/.gemini/config/") || normalized.includes("/.gemini/antigravity/mcp/")) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Governance rulebooks
|
|
162
|
+
if (
|
|
163
|
+
normalized.endsWith("/AGENTS.md") ||
|
|
164
|
+
normalized === "AGENTS.md" ||
|
|
165
|
+
normalized.endsWith("/CLAUDE.md") ||
|
|
166
|
+
normalized === "CLAUDE.md" ||
|
|
167
|
+
normalized.includes("/.agents/rules/")
|
|
168
|
+
) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Checks if a target path is a protected benchmark or evaluation artifact.
|
|
177
|
+
*/
|
|
178
|
+
function isProtectedBenchmarkPath(targetPath) {
|
|
179
|
+
if (!targetPath || typeof targetPath !== "string") return false;
|
|
180
|
+
const normalized = targetPath.replace(/\\/g, "/");
|
|
181
|
+
return (
|
|
182
|
+
/\/(?:benchmarks?|evaluators?|evaluation|scoring|ground_truth|prompt_templates)\//i.test(normalized) ||
|
|
183
|
+
/\/(?:eval_[^\/]+\.(?:py|json|toml)|bench_[^\/]+\.(?:py|json|toml))$/i.test(normalized)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let resolvedRtkPath = null;
|
|
188
|
+
function getRtkBinary() {
|
|
189
|
+
if (resolvedRtkPath) return resolvedRtkPath;
|
|
190
|
+
|
|
191
|
+
// 1. Try global rtk on PATH
|
|
192
|
+
try {
|
|
193
|
+
const check = spawnSync("rtk", ["--version"], { timeout: 1000, windowsHide: true });
|
|
194
|
+
if (check.status === 0) {
|
|
195
|
+
resolvedRtkPath = "rtk";
|
|
196
|
+
return resolvedRtkPath;
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
|
|
200
|
+
// 2. Try plugin local bin
|
|
201
|
+
const isWin = os.platform() === "win32";
|
|
202
|
+
const exeName = isWin ? "rtk.exe" : "rtk";
|
|
203
|
+
const homeDir = os.homedir();
|
|
204
|
+
const pluginBin = path.join(homeDir, ".gemini", "config", "plugins", "agy-context-saver", "bin", exeName);
|
|
205
|
+
if (fs.existsSync(pluginBin)) {
|
|
206
|
+
resolvedRtkPath = pluginBin;
|
|
207
|
+
return resolvedRtkPath;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
resolvedRtkPath = "rtk";
|
|
211
|
+
return resolvedRtkPath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const NON_REWRITABLE_BINARIES = new Set([
|
|
215
|
+
"node", "node.exe",
|
|
216
|
+
"powershell", "powershell.exe",
|
|
217
|
+
"pwsh", "pwsh.exe",
|
|
218
|
+
"cmd", "cmd.exe",
|
|
219
|
+
"dir",
|
|
220
|
+
"echo",
|
|
221
|
+
"cd",
|
|
222
|
+
"del",
|
|
223
|
+
"mkdir",
|
|
224
|
+
"rmdir",
|
|
225
|
+
"copy",
|
|
226
|
+
"move",
|
|
227
|
+
"type",
|
|
228
|
+
"cls",
|
|
229
|
+
"clear",
|
|
230
|
+
"set",
|
|
231
|
+
"export"
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const REWRITE_CACHE_FILE = path.join(os.tmpdir(), "agy-rtk-rewrite-cache.json");
|
|
235
|
+
const REWRITE_CACHE_MAX = 100;
|
|
236
|
+
let inMemoryRewriteCache = null;
|
|
237
|
+
|
|
238
|
+
function loadRewriteCache() {
|
|
239
|
+
if (inMemoryRewriteCache) return inMemoryRewriteCache;
|
|
240
|
+
try {
|
|
241
|
+
if (fs.existsSync(REWRITE_CACHE_FILE)) {
|
|
242
|
+
const data = JSON.parse(fs.readFileSync(REWRITE_CACHE_FILE, "utf-8"));
|
|
243
|
+
if (data && typeof data === "object") {
|
|
244
|
+
inMemoryRewriteCache = data;
|
|
245
|
+
return inMemoryRewriteCache;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
inMemoryRewriteCache = {};
|
|
250
|
+
return inMemoryRewriteCache;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function saveRewriteCache(cache) {
|
|
254
|
+
try {
|
|
255
|
+
const keys = Object.keys(cache);
|
|
256
|
+
let toSave = cache;
|
|
257
|
+
if (keys.length > REWRITE_CACHE_MAX) {
|
|
258
|
+
const slice = keys.slice(keys.length - REWRITE_CACHE_MAX);
|
|
259
|
+
toSave = {};
|
|
260
|
+
for (const k of slice) toSave[k] = cache[k];
|
|
261
|
+
}
|
|
262
|
+
fs.writeFileSync(REWRITE_CACHE_FILE, JSON.stringify(toSave), "utf-8");
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Rewrite raw shell command using RTK (Rust Token Killer).
|
|
268
|
+
*/
|
|
269
|
+
function rewriteCommandWithRtk(rawCmd) {
|
|
270
|
+
if (!rawCmd || typeof rawCmd !== "string") return rawCmd;
|
|
271
|
+
const trimmed = rawCmd.trim();
|
|
272
|
+
if (!trimmed) return rawCmd;
|
|
273
|
+
|
|
274
|
+
// Already prefixed with rtk
|
|
275
|
+
if (/^rtk(?:\.exe)?\s+/i.test(trimmed)) {
|
|
276
|
+
return trimmed;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Fast-path bypass for shell builtins, non-rewritable binaries, and powershell cmdlets
|
|
280
|
+
const firstWord = (trimmed.match(/^[^\s"']+/)?.[0] || "").toLowerCase();
|
|
281
|
+
if (
|
|
282
|
+
NON_REWRITABLE_BINARIES.has(firstWord) ||
|
|
283
|
+
firstWord.startsWith("get-") ||
|
|
284
|
+
firstWord.startsWith("set-") ||
|
|
285
|
+
firstWord.startsWith("start-") ||
|
|
286
|
+
firstWord.startsWith("stop-") ||
|
|
287
|
+
firstWord.startsWith("new-") ||
|
|
288
|
+
firstWord.startsWith("remove-") ||
|
|
289
|
+
firstWord.startsWith("test-")
|
|
290
|
+
) {
|
|
291
|
+
return trimmed;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Check persistent cache
|
|
295
|
+
const cache = loadRewriteCache();
|
|
296
|
+
if (cache[trimmed] !== undefined) {
|
|
297
|
+
return cache[trimmed];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let rewritten = trimmed;
|
|
301
|
+
try {
|
|
302
|
+
const rtkBin = getRtkBinary();
|
|
303
|
+
const res = spawnSync(rtkBin, ["rewrite", trimmed], {
|
|
304
|
+
encoding: "utf-8",
|
|
305
|
+
timeout: 2000,
|
|
306
|
+
windowsHide: true
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// RTK exits 0 or 3 when rewritten, with new command on stdout
|
|
310
|
+
if ((res.status === 0 || res.status === 3) && res.stdout && res.stdout.trim()) {
|
|
311
|
+
rewritten = res.stdout.trim();
|
|
312
|
+
}
|
|
313
|
+
} catch {}
|
|
314
|
+
|
|
315
|
+
cache[trimmed] = rewritten;
|
|
316
|
+
saveRewriteCache(cache);
|
|
317
|
+
return rewritten;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- Main Hook Entrypoint ---
|
|
321
|
+
let isGovernedTool = false;
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const inputRaw = fs.readFileSync(0, "utf-8");
|
|
325
|
+
if (!inputRaw || !inputRaw.trim()) {
|
|
326
|
+
respond({ decision: "allow" });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Fast-path string scan (< 1ms)
|
|
330
|
+
let foundGoverned = false;
|
|
331
|
+
for (const name of GOVERNED_TOOLS) {
|
|
332
|
+
if (inputRaw.includes(name)) {
|
|
333
|
+
foundGoverned = true;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!foundGoverned) {
|
|
339
|
+
respond({ decision: "allow" });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
isGovernedTool = true;
|
|
343
|
+
const payload = JSON.parse(inputRaw);
|
|
344
|
+
const toolCall = payload.toolCall || payload.tool_call || {};
|
|
345
|
+
const toolName = String(toolCall.name || toolCall.tool_name || "").trim().toLowerCase();
|
|
346
|
+
const args = toolCall.args || toolCall.arguments || {};
|
|
347
|
+
const convId = payload.conversationId || "global";
|
|
348
|
+
|
|
349
|
+
if (!GOVERNED_TOOLS.has(toolName)) {
|
|
350
|
+
respond({ decision: "allow" });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
isGovernedTool = true;
|
|
354
|
+
|
|
355
|
+
// =========================================================================
|
|
356
|
+
// 1. run_command: Transparent RTK Command Optimization & Synchronous Wait
|
|
357
|
+
// =========================================================================
|
|
358
|
+
if (toolName === "run_command") {
|
|
359
|
+
const rawCmd = args.CommandLine !== undefined ? args.CommandLine : args.command;
|
|
360
|
+
if (typeof rawCmd !== "string" || !rawCmd.trim()) {
|
|
361
|
+
respond({
|
|
362
|
+
decision: "deny",
|
|
363
|
+
reason: "Antigravity Governance: Missing or empty CommandLine in run_command."
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const rewritten = rewriteCommandWithRtk(rawCmd);
|
|
368
|
+
const isDaemon = args.IsDaemon === true || args.isDaemon === true;
|
|
369
|
+
const waitMs = args.WaitMsBeforeAsync !== undefined ? args.WaitMsBeforeAsync : args.waitMsBeforeAsync;
|
|
370
|
+
const targetMaxWait = Number(process.env.AGY_MAX_WAIT_MS) || 10000;
|
|
371
|
+
|
|
372
|
+
const overwrite = {};
|
|
373
|
+
if (rewritten !== rawCmd) {
|
|
374
|
+
overwrite.CommandLine = rewritten;
|
|
375
|
+
}
|
|
376
|
+
if (!isDaemon && (waitMs === undefined || waitMs < targetMaxWait)) {
|
|
377
|
+
overwrite.WaitMsBeforeAsync = targetMaxWait;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (Object.keys(overwrite).length > 0) {
|
|
381
|
+
respond({ decision: "allow", overwrite });
|
|
382
|
+
}
|
|
383
|
+
respond({ decision: "allow" });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// =========================================================================
|
|
387
|
+
// 2. Native File & Inspection Tools: Root Protection & RTK Hard-Routing
|
|
388
|
+
// =========================================================================
|
|
389
|
+
const INSPECTION_TOOLS = new Set([
|
|
390
|
+
"view_file",
|
|
391
|
+
"read_file",
|
|
392
|
+
"read_many_files",
|
|
393
|
+
"grep_search",
|
|
394
|
+
"find_by_name",
|
|
395
|
+
"list_dir"
|
|
396
|
+
]);
|
|
397
|
+
|
|
398
|
+
if (INSPECTION_TOOLS.has(toolName)) {
|
|
399
|
+
const rawTarget =
|
|
400
|
+
args.AbsolutePath ||
|
|
401
|
+
args.absolutePath ||
|
|
402
|
+
args.targetFile ||
|
|
403
|
+
args.target_file ||
|
|
404
|
+
args.path ||
|
|
405
|
+
args.dirPath ||
|
|
406
|
+
args.dir_path ||
|
|
407
|
+
args.directory ||
|
|
408
|
+
"";
|
|
409
|
+
const target = String(rawTarget).trim();
|
|
410
|
+
|
|
411
|
+
// 2a. Internal State Protection by Root (LH-02)
|
|
412
|
+
if (isProtectedInternalState(target) || isProtectedInternalState(JSON.stringify(args))) {
|
|
413
|
+
respond({
|
|
414
|
+
decision: "deny",
|
|
415
|
+
reason:
|
|
416
|
+
"Antigravity Execution Governance: Direct access to internal Antigravity execution state (.system_generated) is strictly prohibited to prevent transcript bloat and polling loops.\n" +
|
|
417
|
+
"- To inspect conversation transcripts: Call MCP tool read_transcript(conversationId=\"...\", mode=\"compact\") or query_transcript(...).\n" +
|
|
418
|
+
"- To inspect background tasks: Yield execution turn and rely on native Reactive Wakeup (<SYSTEM_MESSAGE>). Do not read internal task logs."
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 2b. Allow Antigravity Special Files (SKILL.md, brain artifacts, configs)
|
|
423
|
+
if (isAntigravitySpecialFile(target)) {
|
|
424
|
+
respond({ decision: "allow" });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 2c. Hard-Route Workspace Code/Data Inspection to Canonical RTK Commands (LH-07)
|
|
428
|
+
if (toolName === "view_file" || toolName === "read_file") {
|
|
429
|
+
respond({
|
|
430
|
+
decision: "deny",
|
|
431
|
+
reason: `Antigravity Governance: Native file inspection via '${toolName}' bypasses token optimization. Run 'rtk read ${target || "<file>"}' via run_command instead.`
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (toolName === "read_many_files") {
|
|
436
|
+
respond({
|
|
437
|
+
decision: "deny",
|
|
438
|
+
reason: "Antigravity Governance: Native multi-file reading via 'read_many_files' bypasses token optimization. Run 'rtk read <file>' via run_command instead."
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (toolName === "grep_search") {
|
|
443
|
+
const pattern = args.query || args.pattern || "";
|
|
444
|
+
const isWin = os.platform() === "win32";
|
|
445
|
+
const cmdHint = isWin ? `rtk rg "${pattern}" .` : `rtk grep "${pattern}"`;
|
|
446
|
+
respond({
|
|
447
|
+
decision: "deny",
|
|
448
|
+
reason: `Antigravity Governance: Native grep inspection via 'grep_search' bypasses token optimization. Run '${cmdHint}' via run_command instead.`
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (toolName === "find_by_name") {
|
|
453
|
+
const searchTarget = args.query || args.pattern || target || ".";
|
|
454
|
+
respond({
|
|
455
|
+
decision: "deny",
|
|
456
|
+
reason: `Antigravity Governance: Native file search via 'find_by_name' bypasses token optimization. Run 'rtk find ${searchTarget}' via run_command instead.`
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (toolName === "list_dir") {
|
|
461
|
+
const isWin = os.platform() === "win32";
|
|
462
|
+
const cmdHint = isWin ? `rtk find ${target || "."}` : `rtk ls ${target || ""}`;
|
|
463
|
+
respond({
|
|
464
|
+
decision: "deny",
|
|
465
|
+
reason: `Antigravity Governance: Native directory listing via 'list_dir' bypasses token optimization. Run '${cmdHint}' via run_command instead.`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
respond({ decision: "allow" });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// =========================================================================
|
|
473
|
+
// 3. Background Task Lifecycle: Closed Session Ledger & Reactive Wakeup
|
|
474
|
+
// =========================================================================
|
|
475
|
+
if (toolName === "manage_task") {
|
|
476
|
+
const action = String(args.Action || args.action || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
477
|
+
|
|
478
|
+
// kill, send_input are always allowed immediately
|
|
479
|
+
if (action !== "status" && action !== "list") {
|
|
480
|
+
respond({ decision: "allow" });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const session = getSessionState(convId);
|
|
484
|
+
const taskId = String(args.TaskId || args.taskId || "").trim();
|
|
485
|
+
if (taskId && !session.taskIds.includes(taskId)) {
|
|
486
|
+
session.taskIds.push(taskId);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// First inspection check in the session is permitted
|
|
490
|
+
if (session.pollCount === 0) {
|
|
491
|
+
session.pollCount = 1;
|
|
492
|
+
saveSessionState(convId, session);
|
|
493
|
+
respond({ decision: "allow" });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Any subsequent polling checks in the session are strictly denied
|
|
497
|
+
session.denials++;
|
|
498
|
+
saveSessionState(convId, session);
|
|
499
|
+
|
|
500
|
+
if (session.denials >= 5) {
|
|
501
|
+
respond({
|
|
502
|
+
decision: "force_ask",
|
|
503
|
+
reason: `[CIRCUIT BREAKER ACTIVATED] Autonomous loop suspended: manage_task('${action}') has been denied ${session.denials} times across tasks [${session.taskIds.join(", ")}]. Stop calling tools and yield the turn for native Reactive Wakeup (<SYSTEM_MESSAGE>). User confirmation required to proceed.`
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
respond({
|
|
508
|
+
decision: "deny",
|
|
509
|
+
reason: `Antigravity Execution Governance: Background task polling is prohibited (${session.denials} denial). Tasks execute asynchronously and notify you automatically via native Reactive Wakeup (<SYSTEM_MESSAGE>) upon completion. Stop calling tools and yield your turn now.`
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (toolName === "schedule") {
|
|
514
|
+
const prompt = String(args.Prompt || args.prompt || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
515
|
+
const cond = String(args.TimerCondition || args.timerCondition || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
516
|
+
|
|
517
|
+
const isTaskPoll =
|
|
518
|
+
cond.startsWith("task-") ||
|
|
519
|
+
cond.includes("task") ||
|
|
520
|
+
prompt.includes("check on") ||
|
|
521
|
+
prompt.includes("check task") ||
|
|
522
|
+
prompt.includes("background task") ||
|
|
523
|
+
prompt.includes("still running") ||
|
|
524
|
+
prompt.includes("test suite") ||
|
|
525
|
+
((prompt.includes("check") || prompt.includes("status") || prompt.includes("poll") || prompt.includes("monitor") || prompt.includes("wait")) &&
|
|
526
|
+
(prompt.includes("task") || prompt.includes("process") || prompt.includes("job") || prompt.includes("test") || prompt.includes("command") || prompt.includes("suite")));
|
|
527
|
+
|
|
528
|
+
// All polling timers are strictly denied regardless of duration (LH-03)
|
|
529
|
+
if (isTaskPoll) {
|
|
530
|
+
const session = getSessionState(convId);
|
|
531
|
+
session.denials++;
|
|
532
|
+
saveSessionState(convId, session);
|
|
533
|
+
|
|
534
|
+
if (session.denials >= 5) {
|
|
535
|
+
respond({
|
|
536
|
+
decision: "force_ask",
|
|
537
|
+
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.`
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
respond({
|
|
542
|
+
decision: "deny",
|
|
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
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Non-polling timers (e.g. user-requested reminders) are allowed
|
|
548
|
+
respond({ decision: "allow" });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// =========================================================================
|
|
552
|
+
// 4. Benchmark & Evaluator Mutation Protection (LH-09)
|
|
553
|
+
// =========================================================================
|
|
554
|
+
if (toolName === "replace_file_content" || toolName === "write_to_file") {
|
|
555
|
+
const targetFile = String(args.TargetFile || args.targetFile || args.path || "").trim();
|
|
556
|
+
if (isProtectedBenchmarkPath(targetFile)) {
|
|
557
|
+
respond({
|
|
558
|
+
decision: "force_ask",
|
|
559
|
+
reason: `[BENCHMARK MUTATION DETECTED] Mutation of protected evaluator/benchmark artifact '${targetFile}' requires explicit user confirmation to prevent benchmark contamination.`
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
respond({ decision: "allow" });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
respond({ decision: "allow" });
|
|
566
|
+
} catch (err) {
|
|
567
|
+
// Fail-closed for governed tools, fail-open for unclassified non-governed tools (LH-10)
|
|
568
|
+
if (isGovernedTool) {
|
|
569
|
+
respond({
|
|
570
|
+
decision: "deny",
|
|
571
|
+
reason: `Antigravity Governance: Exception encountered while evaluating governed tool call: ${err.message}. Denied for session integrity.`
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
respond({ decision: "allow" });
|
|
575
|
+
}
|