@saadnvd1/agent-os 0.3.461 → 0.3.462
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/README.md
CHANGED
|
@@ -1262,8 +1262,16 @@ descriptors, child processes and spawns per minute; `scripts/perf/execs.mjs
|
|
|
1262
1262
|
spawns with eslogger (macOS) or strace (Linux) and need passwordless sudo for
|
|
1263
1263
|
that one number. Run them on a machine with room to spare.
|
|
1264
1264
|
|
|
1265
|
-
A pre-commit hook
|
|
1266
|
-
|
|
1265
|
+
A pre-commit hook (`.husky/pre-commit`, installed by `npm install`) checks
|
|
1266
|
+
only what is staged, in well under two seconds: `prettier --check`, failing
|
|
1267
|
+
with the `npx prettier --write …` command that fixes it, and a scan of the
|
|
1268
|
+
added lines for token-shaped secrets with the merge gate's own rule
|
|
1269
|
+
(`scripts/hooks/staged-secrets.ts` reads `secretsIn` from
|
|
1270
|
+
`lib/orchestrator/diff.ts`), naming the file and line. Build a test's fake
|
|
1271
|
+
token at runtime (`"gh" + "p_" + "a".repeat(36)`) rather than writing it
|
|
1272
|
+
whole. It runs in the worktrees AgentOS makes too, which have no `.husky/_`
|
|
1273
|
+
until `npm install` runs in them. Typecheck, lint and tests are left to CI.
|
|
1274
|
+
In an emergency, `git commit --no-verify` skips it. CI runs `scripts/check --build` on every pull request and push to main,
|
|
1267
1275
|
which also holds the app's first-load JavaScript to a budget
|
|
1268
1276
|
(`scripts/check-bundle.mjs`; `npx next experimental-analyze` shows what's in
|
|
1269
1277
|
it), and fails a pull request whose body has no Code review section for its head
|
package/lib/worktree-hooks.ts
CHANGED
|
@@ -85,10 +85,19 @@ async function ownHooks(worktree: string): Promise<string> {
|
|
|
85
85
|
return sh(path.join(common, "hooks"));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Husky writes `.husky/_` on `npm install`, and it's gitignored, so a new
|
|
89
|
+
// worktree has none and its hooks would silently not run: run the hook
|
|
90
|
+
// script from `.husky/` the way husky's own runner does.
|
|
88
91
|
const delegate = (own: string, hook: string) =>
|
|
89
92
|
`root="$(git rev-parse --show-toplevel)"
|
|
90
93
|
own=${own}/${hook}
|
|
91
94
|
[ -x "$own" ] && exec "$own" "$@"
|
|
95
|
+
case "$own" in
|
|
96
|
+
*/.husky/_/${hook})
|
|
97
|
+
script="\${own%/_/${hook}}/${hook}"
|
|
98
|
+
[ -f "$script" ] && [ "\${HUSKY-}" != 0 ] && PATH="node_modules/.bin:$PATH" exec sh -e "$script" "$@"
|
|
99
|
+
;;
|
|
100
|
+
esac
|
|
92
101
|
exit 0
|
|
93
102
|
`;
|
|
94
103
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { scanDiff } from "./staged-secrets";
|
|
7
|
+
|
|
8
|
+
// Fakes are built here, never written whole: a literal would be the very
|
|
9
|
+
// thing this scan (and the merge gate) refuses.
|
|
10
|
+
const fake = {
|
|
11
|
+
github: ["gh", "p_", "a1B2".repeat(9)].join(""),
|
|
12
|
+
anthropic: ["sk", "-ant-", "x9Y8".repeat(8)].join(""),
|
|
13
|
+
aws: ["AK", "IA", "Q7".repeat(8)].join(""),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const diffOf = (file: string, start: number, lines: string[]) =>
|
|
17
|
+
[
|
|
18
|
+
`diff --git a/${file} b/${file}`,
|
|
19
|
+
"index 0000000..1111111 100644",
|
|
20
|
+
`--- a/${file}`,
|
|
21
|
+
`+++ b/${file}`,
|
|
22
|
+
`@@ -0,0 +${start},${lines.length} @@`,
|
|
23
|
+
...lines.map((l) => `+${l}`),
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
describe("scanDiff", () => {
|
|
27
|
+
it("names the file and line of each token-shaped string", () => {
|
|
28
|
+
const diff = [
|
|
29
|
+
diffOf("lib/a.ts", 10, ["const ok = 1;", `const t = "${fake.github}";`]),
|
|
30
|
+
diffOf("docs/b.md", 3, [`key: ${fake.anthropic}`, "", fake.aws]),
|
|
31
|
+
].join("\n");
|
|
32
|
+
expect(scanDiff(diff)).toEqual([
|
|
33
|
+
{ file: "lib/a.ts", line: 11, what: expect.stringMatching(/^ghp_/) },
|
|
34
|
+
{ file: "docs/b.md", line: 3, what: expect.stringMatching(/^sk-/) },
|
|
35
|
+
{ file: "docs/b.md", line: 5, what: expect.stringMatching(/^AKIA/) },
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("flags an env-style assignment of a real-looking value, not a placeholder", () => {
|
|
40
|
+
const diff = diffOf(".env.example", 1, [
|
|
41
|
+
`API_TOKEN=${"q7R".repeat(6)}42`,
|
|
42
|
+
"API_KEY=${API_KEY}",
|
|
43
|
+
"SECRET_KEY=changeme",
|
|
44
|
+
]);
|
|
45
|
+
expect(scanDiff(diff)).toEqual([
|
|
46
|
+
{ file: ".env.example", line: 1, what: "API_TOKEN = …" },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("passes code that builds a fake at runtime", () => {
|
|
51
|
+
const diff = diffOf("lib/c.test.ts", 1, [
|
|
52
|
+
'const t = ["gh", "p_", "a".repeat(36)].join("");',
|
|
53
|
+
]);
|
|
54
|
+
expect(scanDiff(diff)).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("doesn't take an added line starting with ++ for a file header", () => {
|
|
58
|
+
const diff = diffOf("lib/d.ts", 1, [`++ ${fake.github}`]);
|
|
59
|
+
expect(scanDiff(diff)).toEqual([
|
|
60
|
+
{ file: "lib/d.ts", line: 1, what: expect.stringMatching(/^ghp_/) },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("fails the commit on a staged secret, and passes once it's gone", () => {
|
|
65
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "staged-secrets-"));
|
|
66
|
+
const git = (...args: string[]) =>
|
|
67
|
+
execFileSync("git", args, { cwd: repo, stdio: "pipe" });
|
|
68
|
+
const scan = () =>
|
|
69
|
+
spawnSync(
|
|
70
|
+
path.resolve("node_modules/.bin/tsx"),
|
|
71
|
+
[path.resolve("scripts/hooks/staged-secrets.ts")],
|
|
72
|
+
{ cwd: repo, encoding: "utf-8" }
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
git("init", "-q");
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(repo, "x.ts"),
|
|
78
|
+
`const t = "${fake.github}";\n`
|
|
79
|
+
);
|
|
80
|
+
git("add", "x.ts");
|
|
81
|
+
const bad = scan();
|
|
82
|
+
expect(bad.status).toBe(1);
|
|
83
|
+
expect(bad.stderr).toContain("x.ts:1");
|
|
84
|
+
expect(bad.stderr).toContain("at runtime");
|
|
85
|
+
fs.writeFileSync(path.join(repo, "x.ts"), "const t = process.env.T;\n");
|
|
86
|
+
git("add", "x.ts");
|
|
87
|
+
expect(scan().status).toBe(0);
|
|
88
|
+
} finally {
|
|
89
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-commit secret scan: the staged diff's added lines, read with the
|
|
3
|
+
* merge gate's own rule (lib/orchestrator/diff.ts secretsIn), so a commit
|
|
4
|
+
* that would fail the gate fails here first, naming the file and line.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from "child_process";
|
|
8
|
+
import { secretsIn } from "../../lib/orchestrator/diff";
|
|
9
|
+
|
|
10
|
+
export interface SecretHit {
|
|
11
|
+
file: string;
|
|
12
|
+
line: number;
|
|
13
|
+
what: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Each added line of a `git diff -U0` with the file and new line number it
|
|
17
|
+
// lands on. Headers are only read before a file's first hunk, so an added
|
|
18
|
+
// line that itself starts with "++" isn't taken for one.
|
|
19
|
+
export function scanDiff(diff: string): SecretHit[] {
|
|
20
|
+
const hits: SecretHit[] = [];
|
|
21
|
+
let file = "";
|
|
22
|
+
let line = 0;
|
|
23
|
+
let header = false;
|
|
24
|
+
for (const l of diff.split("\n")) {
|
|
25
|
+
if (l.startsWith("diff --git ")) {
|
|
26
|
+
header = true;
|
|
27
|
+
} else if (header && l.startsWith("+++ ")) {
|
|
28
|
+
file = l.slice(4).replace(/^b\//, "");
|
|
29
|
+
} else if (l.startsWith("@@")) {
|
|
30
|
+
header = false;
|
|
31
|
+
line = Number(l.match(/\+(\d+)/)?.[1] ?? 0);
|
|
32
|
+
} else if (!header && l.startsWith("+")) {
|
|
33
|
+
const what = secretsIn(l.slice(1));
|
|
34
|
+
if (what) hits.push({ file, line, what });
|
|
35
|
+
line++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return hits;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.argv[1]?.endsWith("staged-secrets.ts")) {
|
|
42
|
+
const diff = execFileSync(
|
|
43
|
+
"git",
|
|
44
|
+
[
|
|
45
|
+
"diff",
|
|
46
|
+
"--cached",
|
|
47
|
+
"-U0",
|
|
48
|
+
"--no-color",
|
|
49
|
+
"--no-ext-diff",
|
|
50
|
+
"--diff-filter=ACMR",
|
|
51
|
+
],
|
|
52
|
+
{ encoding: "utf8", maxBuffer: 256 * 1024 * 1024 }
|
|
53
|
+
);
|
|
54
|
+
const hits = scanDiff(diff);
|
|
55
|
+
for (const h of hits)
|
|
56
|
+
console.error(`✗ ${h.file}:${h.line}: looks like a secret (${h.what})`);
|
|
57
|
+
if (hits.length) {
|
|
58
|
+
console.error(
|
|
59
|
+
"The merge gate refuses token-shaped strings. For a test fixture, build the fake at runtime\n" +
|
|
60
|
+
'(e.g. "gh" + "p_" + "a".repeat(36)) instead of writing it as a literal.'
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|