@saadnvd1/agent-os 0.3.733 → 0.3.735
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/lib/orchestrator/write-guard.ts +56 -29
- package/package.json +1 -1
|
@@ -313,14 +313,20 @@ export async function orchestratorWriteBounds(
|
|
|
313
313
|
// Read at most once a minute, not on every write.
|
|
314
314
|
const HOOKS_TTL_MS = 60_000;
|
|
315
315
|
let hooksCache: { key: string; at: number; dirs: string[] } | null = null;
|
|
316
|
+
// The last answer git gave, kept apart from the cache above: a read that
|
|
317
|
+
// times out under load may fall back to one this recent, since a hooks folder
|
|
318
|
+
// rarely moves in minutes. It is never refreshed from a fallback.
|
|
319
|
+
const HOOKS_STALE_OK_MS = 10 * 60_000;
|
|
320
|
+
const lastGoodHooks = new Map<string, { at: number; dirs: string[] }>();
|
|
316
321
|
|
|
317
322
|
export async function hooksPaths(repos: string[]): Promise<string[]> {
|
|
318
323
|
const key = repos.join("\0");
|
|
319
324
|
if (hooksCache?.key === key && Date.now() - hooksCache.at < HOOKS_TTL_MS)
|
|
320
325
|
return hooksCache.dirs;
|
|
321
326
|
// Only "not set" and "not a repository" mean there's nothing to protect;
|
|
322
|
-
// any other failure (a timeout under load)
|
|
323
|
-
//
|
|
327
|
+
// any other failure (a timeout under load) uses the last good answer if
|
|
328
|
+
// it's under ten minutes old, else throws, so the write is refused and
|
|
329
|
+
// nothing is cached.
|
|
324
330
|
const run = (cwd: string, args: string[]) =>
|
|
325
331
|
git(cwd, args, { timeout: 5000 })
|
|
326
332
|
.then((out) => out.trim())
|
|
@@ -336,33 +342,54 @@ export async function hooksPaths(repos: string[]): Promise<string[]> {
|
|
|
336
342
|
// The user's, expanded by git (~ and ~user). A relative one is taken from
|
|
337
343
|
// each repo's top, so it names no one folder: each project's answer
|
|
338
344
|
// below covers it there. Each project's comes as git itself resolves it.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
345
|
+
try {
|
|
346
|
+
return await read();
|
|
347
|
+
} catch (e) {
|
|
348
|
+
const good = lastGoodHooks.get(key);
|
|
349
|
+
if (!good || Date.now() - good.at >= HOOKS_STALE_OK_MS) throw e;
|
|
350
|
+
console.warn(
|
|
351
|
+
`[write-guard] ${e instanceof Error ? e.message : e}; using the answer from ${Math.round((Date.now() - good.at) / 1000)}s ago`
|
|
352
|
+
);
|
|
353
|
+
return good.dirs;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function read(): Promise<string[]> {
|
|
357
|
+
const global = await run(os.homedir(), [
|
|
358
|
+
"config",
|
|
359
|
+
"--global",
|
|
360
|
+
"--type=path",
|
|
361
|
+
"--get",
|
|
362
|
+
"core.hooksPath",
|
|
363
|
+
]);
|
|
364
|
+
const found = await Promise.all(
|
|
365
|
+
repos.map(async (r) => {
|
|
366
|
+
// A project whose folder is gone has no hooks to run.
|
|
367
|
+
if (!fs.existsSync(r)) return "";
|
|
368
|
+
// A task worktree always has .git at its root; a folder there without
|
|
369
|
+
// one is what a removal left behind (a cache dir), not a repository.
|
|
370
|
+
if (
|
|
371
|
+
path.dirname(path.resolve(r)) === path.resolve(WORKTREES_DIR) &&
|
|
372
|
+
!fs.existsSync(path.join(r, ".git"))
|
|
373
|
+
)
|
|
374
|
+
return "";
|
|
375
|
+
const dir = await run(r, [
|
|
376
|
+
"rev-parse",
|
|
377
|
+
"--path-format=absolute",
|
|
378
|
+
"--git-path",
|
|
379
|
+
"hooks",
|
|
380
|
+
]);
|
|
381
|
+
if (dir !== "" && !dir.startsWith("/"))
|
|
382
|
+
throw new Error(`git named no folder for ${r}'s hooks`);
|
|
383
|
+
return dir;
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
const dirs = [
|
|
387
|
+
...new Set([global, ...found].filter((d) => d.startsWith("/"))),
|
|
388
|
+
];
|
|
389
|
+
hooksCache = { key, at: Date.now(), dirs };
|
|
390
|
+
lastGoodHooks.set(key, { at: Date.now(), dirs });
|
|
391
|
+
return dirs;
|
|
392
|
+
}
|
|
366
393
|
}
|
|
367
394
|
|
|
368
395
|
// The check a PreToolUse hook makes: why this tool call is refused, or null.
|