@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 formats and lints staged files, then typechecks and runs the
1266
- tests. CI runs `scripts/check --build` on every pull request and push to main,
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.461",
3
+ "version": "0.3.462",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -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
+ }