@squadrant-ai/auto-gate 0.1.0

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.
Files changed (90) hide show
  1. package/README.md +2 -0
  2. package/dist/adapters/claude/blocked-signal.d.ts +16 -0
  3. package/dist/adapters/claude/blocked-signal.js +21 -0
  4. package/dist/adapters/claude/install.d.ts +12 -0
  5. package/dist/adapters/claude/install.js +52 -0
  6. package/dist/adapters/claude/output.d.ts +5 -0
  7. package/dist/adapters/claude/output.js +16 -0
  8. package/dist/adapters/claude/payload.d.ts +14 -0
  9. package/dist/adapters/claude/payload.js +36 -0
  10. package/dist/adapters/claude/settings.d.ts +14 -0
  11. package/dist/adapters/claude/settings.js +48 -0
  12. package/dist/adapters/opencode/answer.d.ts +24 -0
  13. package/dist/adapters/opencode/answer.js +25 -0
  14. package/dist/adapters/opencode/install.d.ts +5 -0
  15. package/dist/adapters/opencode/install.js +30 -0
  16. package/dist/adapters/opencode/intent.d.ts +10 -0
  17. package/dist/adapters/opencode/intent.js +11 -0
  18. package/dist/adapters/opencode/lock.d.ts +18 -0
  19. package/dist/adapters/opencode/lock.js +72 -0
  20. package/dist/adapters/opencode/port.d.ts +17 -0
  21. package/dist/adapters/opencode/port.js +29 -0
  22. package/dist/adapters/opencode/run.d.ts +40 -0
  23. package/dist/adapters/opencode/run.js +69 -0
  24. package/dist/adapters/opencode/sse.d.ts +18 -0
  25. package/dist/adapters/opencode/sse.js +29 -0
  26. package/dist/adapters/opencode/watch.d.ts +43 -0
  27. package/dist/adapters/opencode/watch.js +138 -0
  28. package/dist/classifiers/battery.d.ts +26 -0
  29. package/dist/classifiers/battery.js +27 -0
  30. package/dist/classifiers/classify.d.ts +15 -0
  31. package/dist/classifiers/classify.js +25 -0
  32. package/dist/classifiers/generative.d.ts +16 -0
  33. package/dist/classifiers/generative.js +50 -0
  34. package/dist/classifiers/jev-parse.d.ts +6 -0
  35. package/dist/classifiers/jev-parse.js +35 -0
  36. package/dist/classifiers/jev-state.d.ts +10 -0
  37. package/dist/classifiers/jev-state.js +13 -0
  38. package/dist/classifiers/jev.d.ts +15 -0
  39. package/dist/classifiers/jev.js +66 -0
  40. package/dist/classifiers/null.d.ts +6 -0
  41. package/dist/classifiers/null.js +9 -0
  42. package/dist/cli/claude.d.ts +21 -0
  43. package/dist/cli/claude.js +20 -0
  44. package/dist/cli/decide.d.ts +17 -0
  45. package/dist/cli/decide.js +16 -0
  46. package/dist/cli/doctor.d.ts +4 -0
  47. package/dist/cli/doctor.js +26 -0
  48. package/dist/cli/index.d.ts +2 -0
  49. package/dist/cli/index.js +109 -0
  50. package/dist/cli/runtime.d.ts +19 -0
  51. package/dist/cli/runtime.js +86 -0
  52. package/dist/cli/stats.d.ts +7 -0
  53. package/dist/cli/stats.js +29 -0
  54. package/dist/cli/test.d.ts +2 -0
  55. package/dist/cli/test.js +9 -0
  56. package/dist/config/keys.d.ts +8 -0
  57. package/dist/config/keys.js +17 -0
  58. package/dist/config/resolve.d.ts +17 -0
  59. package/dist/config/resolve.js +52 -0
  60. package/dist/config/schema.d.ts +38 -0
  61. package/dist/config/schema.js +19 -0
  62. package/dist/core/audit.d.ts +21 -0
  63. package/dist/core/audit.js +12 -0
  64. package/dist/core/cache.d.ts +22 -0
  65. package/dist/core/cache.js +59 -0
  66. package/dist/core/decide.d.ts +26 -0
  67. package/dist/core/decide.js +75 -0
  68. package/dist/core/policy-version.d.ts +2 -0
  69. package/dist/core/policy-version.js +14 -0
  70. package/dist/core/policy.d.ts +2 -0
  71. package/dist/core/policy.js +36 -0
  72. package/dist/core/presets.d.ts +9 -0
  73. package/dist/core/presets.js +15 -0
  74. package/dist/core/redact.d.ts +7 -0
  75. package/dist/core/redact.js +26 -0
  76. package/dist/core/scope.d.ts +2 -0
  77. package/dist/core/scope.js +7 -0
  78. package/dist/core/tier1.d.ts +3 -0
  79. package/dist/core/tier1.js +16 -0
  80. package/dist/core/tools.d.ts +5 -0
  81. package/dist/core/tools.js +21 -0
  82. package/dist/core/types.d.ts +68 -0
  83. package/dist/core/types.js +1 -0
  84. package/dist/index.d.ts +30 -0
  85. package/dist/index.js +30 -0
  86. package/dist/integration/host.d.ts +28 -0
  87. package/dist/integration/host.js +37 -0
  88. package/dist/integration/project.d.ts +24 -0
  89. package/dist/integration/project.js +19 -0
  90. package/package.json +37 -0
@@ -0,0 +1,26 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { resolveConfig } from "../config/resolve.js";
5
+ import { resolveApiKey } from "../config/keys.js";
6
+ import { foreignOwnerOf } from "../adapters/claude/settings.js";
7
+ export function doctor() {
8
+ const cfg = resolveConfig();
9
+ const p = join(homedir(), ".claude", "settings.json");
10
+ let owner = null;
11
+ let ours = false;
12
+ try {
13
+ const s = JSON.parse(readFileSync(p, "utf8"));
14
+ owner = foreignOwnerOf(s);
15
+ ours = JSON.stringify(s).includes("auto-gate-managed");
16
+ }
17
+ catch { /* missing/unreadable settings is reported below */ }
18
+ const key = resolveApiKey({ apiKeyEnv: cfg.classifier.jev.apiKeyEnv });
19
+ return { lines: [
20
+ `mode: ${cfg.mode}`,
21
+ `classifier: ${cfg.classifier.kind} (${cfg.classifier.jev.model})`,
22
+ `api key: ${key ? "present" : "MISSING — the gate will ask (fail-open)"}`,
23
+ `claude PermissionRequest: ${owner ? `owned by ${owner} (we defer)` : ours ? "owned by auto-gate" : "not installed"}`,
24
+ `settings file: ${existsSync(p) ? p : "(none)"}`,
25
+ ] };
26
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ const [, , cmd, arg3, arg4] = process.argv;
4
+ if (cmd === "decide" && arg3 === "--agent" && arg4 === "claude") {
5
+ const { runClaudeDecide } = await import("./claude.js");
6
+ const { buildRuntime, readLastHumanMessage } = await import("./runtime.js");
7
+ const { createBlockedSignal } = await import("../adapters/claude/blocked-signal.js");
8
+ const { resolveConfig } = await import("../config/resolve.js");
9
+ const { exec } = await import("node:child_process");
10
+ const { promisify } = await import("node:util");
11
+ const execAsync = promisify(exec);
12
+ await runClaudeDecide({
13
+ stdin: { read: async () => readFileSync(0, "utf8") },
14
+ stdout: process.stdout, stderr: process.stderr,
15
+ decide: (req) => buildRuntime().then((rt) => rt.decide(req)),
16
+ readIntent: readLastHumanMessage,
17
+ blockedSignal: createBlockedSignal({
18
+ command: resolveConfig().ask.onAsk,
19
+ exec: async (command, env) => { await execAsync(command, { env: { ...process.env, ...env } }); },
20
+ log: (m) => process.stderr.write(m + "\n"),
21
+ }),
22
+ log: (m) => process.stderr.write(m + "\n"),
23
+ });
24
+ }
25
+ else if (cmd === "decide") {
26
+ const { runDecide } = await import("./decide.js");
27
+ const { buildRuntime } = await import("./runtime.js");
28
+ await runDecide({
29
+ stdin: { read: async () => readFileSync(0, "utf8") },
30
+ stdout: process.stdout, stderr: process.stderr,
31
+ decide: (req) => buildRuntime().then((rt) => rt.decide(req)),
32
+ log: (m) => process.stderr.write(m + "\n"),
33
+ });
34
+ }
35
+ else if (cmd === "install") {
36
+ const { installClaude } = await import("../adapters/claude/install.js");
37
+ const r = installClaude();
38
+ if (r.installed)
39
+ process.stdout.write("auto-gate: installed claude PermissionRequest hook\n");
40
+ else if (r.deferredTo)
41
+ process.stdout.write(`auto-gate: deferred — ${r.deferredTo} already owns the claude PermissionRequest hook\n`);
42
+ else
43
+ process.stdout.write(`auto-gate: not installed — ${r.reason ?? "unknown reason"}\n`);
44
+ }
45
+ else if (cmd === "uninstall") {
46
+ const { uninstallClaude } = await import("../adapters/claude/install.js");
47
+ const r = uninstallClaude();
48
+ process.stdout.write(r.installed
49
+ ? "auto-gate: removed claude PermissionRequest hook\n"
50
+ : `auto-gate: nothing to uninstall — ${r.reason ?? "no settings"}\n`);
51
+ }
52
+ else if (cmd === "doctor") {
53
+ const { doctor } = await import("./doctor.js");
54
+ for (const line of doctor().lines)
55
+ process.stdout.write(line + "\n");
56
+ }
57
+ else if (cmd === "opencode" && arg3 === "run") {
58
+ const { buildRunPlan, supervise } = await import("../adapters/opencode/run.js");
59
+ const plan = await buildRunPlan({ args: process.argv.slice(4) });
60
+ const code = await supervise(plan, { log: (m) => process.stderr.write(m + "\n") });
61
+ process.exit(code);
62
+ }
63
+ else if (cmd === "opencode" && arg3 === "watch") {
64
+ const { resolvePort } = await import("../adapters/opencode/port.js");
65
+ const { acquireLock } = await import("../adapters/opencode/lock.js");
66
+ const { createWatcher, subscribeSse, answerPermission } = await import("../adapters/opencode/watch.js");
67
+ const { buildRuntime } = await import("./runtime.js");
68
+ const at = process.argv.indexOf("--port", 4);
69
+ const flag = at >= 0 ? Number(process.argv[at + 1]) : undefined;
70
+ const resolved = resolvePort({ flag, env: process.env });
71
+ if ("error" in resolved) {
72
+ process.stderr.write(`auto-gate opencode: ${resolved.error}\n`);
73
+ process.exit(4);
74
+ }
75
+ const host = "127.0.0.1";
76
+ const rt = await buildRuntime();
77
+ const watcher = createWatcher({
78
+ host, port: resolved.port,
79
+ acquire: () => acquireLock({ host, port: resolved.port }),
80
+ subscribe: subscribeSse,
81
+ decide: rt.decide,
82
+ answer: (a) => answerPermission({ host, port: resolved.port, ...a }),
83
+ log: (m) => process.stderr.write(m + "\n"),
84
+ });
85
+ const outcome = await watcher.start();
86
+ if (outcome === "yielded")
87
+ process.exit(0);
88
+ if (outcome === "unrecoverable")
89
+ process.exit(1);
90
+ await watcher.stopped;
91
+ }
92
+ else if (cmd === "test") {
93
+ const { runTest } = await import("./test.js");
94
+ process.stdout.write(await runTest(readFileSync(0, "utf8")) + "\n");
95
+ }
96
+ else if (cmd === "stats") {
97
+ const { summarizeAudit } = await import("./stats.js");
98
+ const { resolveConfig } = await import("../config/resolve.js");
99
+ const s = summarizeAudit(resolveConfig().audit.path);
100
+ process.stdout.write(`auto-gate stats — ${s.total} decisions\n`);
101
+ for (const [decision, n] of Object.entries(s.byDecision).sort())
102
+ process.stdout.write(` ${decision}: ${n}\n`);
103
+ process.stdout.write(` cacheHits: ${s.cacheHits}\n`);
104
+ process.stdout.write(` redactionClasses: ${s.redactionClasses.join(", ") || "(none)"}\n`);
105
+ }
106
+ else {
107
+ process.stderr.write(`auto-gate: unknown command '${cmd}'\n`);
108
+ process.exit(2);
109
+ }
@@ -0,0 +1,19 @@
1
+ import { GateCache } from "../core/cache.js";
2
+ import type { GateRequest } from "../core/types.js";
3
+ /** The claude transcript's most recent real human prompt (§11). Best-effort: a
4
+ * missing/unreadable path, malformed lines, or a transcript with no human
5
+ * turns all yield undefined and NEVER throw. Scans JSONL from the last line
6
+ * backwards, skipping assistant turns and tool-result-only user entries. */
7
+ export declare function readLastHumanMessage(transcriptPath: string | undefined): string | undefined;
8
+ /** Assembles config + classifier + cache, and exposes the resolved model id so the
9
+ * decision cache key invalidates when the model changes (§9). */
10
+ export declare function buildRuntime(): Promise<{
11
+ config: import("../index.js").AutoGateConfig;
12
+ classifier: {
13
+ id: "generative" | "jev";
14
+ model: string;
15
+ classify: (r: GateRequest) => Promise<import("../core/types.js").GateAssessment>;
16
+ };
17
+ cache: GateCache;
18
+ decide: (req: GateRequest) => Promise<import("../core/types.js").GateOutcome>;
19
+ }>;
@@ -0,0 +1,86 @@
1
+ import { resolveConfig } from "../config/resolve.js";
2
+ import { resolveApiKey } from "../config/keys.js";
3
+ import { createJevClassifier } from "../classifiers/jev.js";
4
+ import { createGenerativeClassifier } from "../classifiers/generative.js";
5
+ import { classify } from "../classifiers/classify.js";
6
+ import { GateCache } from "../core/cache.js";
7
+ import { decideRequest } from "../core/decide.js";
8
+ import { homedir } from "node:os";
9
+ import { readFileSync } from "node:fs";
10
+ /** Pull the human-readable text out of a transcript `message.content`: a bare
11
+ * string, or the joined `.text` of blocks whose `type === "text"`. Anything
12
+ * else (notably `tool_result`) contributes nothing, so a tool-result-only turn
13
+ * yields undefined and is treated as not-a-human-message. */
14
+ function textFromContent(content) {
15
+ if (typeof content === "string") {
16
+ const t = content.trim();
17
+ return t || undefined;
18
+ }
19
+ if (Array.isArray(content)) {
20
+ const parts = content
21
+ .filter((b) => !!b && typeof b === "object" && b.type === "text" && typeof b.text === "string")
22
+ .map((b) => b.text);
23
+ const joined = parts.join("\n").trim();
24
+ return joined || undefined;
25
+ }
26
+ return undefined;
27
+ }
28
+ /** The claude transcript's most recent real human prompt (§11). Best-effort: a
29
+ * missing/unreadable path, malformed lines, or a transcript with no human
30
+ * turns all yield undefined and NEVER throw. Scans JSONL from the last line
31
+ * backwards, skipping assistant turns and tool-result-only user entries. */
32
+ export function readLastHumanMessage(transcriptPath) {
33
+ if (!transcriptPath)
34
+ return undefined;
35
+ let raw;
36
+ try {
37
+ raw = readFileSync(transcriptPath, "utf8");
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ const lines = raw.split(/\r?\n/);
43
+ for (let i = lines.length - 1; i >= 0; i--) {
44
+ const line = lines[i].trim();
45
+ if (!line)
46
+ continue;
47
+ let entry;
48
+ try {
49
+ const parsed = JSON.parse(line);
50
+ if (!parsed || typeof parsed !== "object")
51
+ continue;
52
+ entry = parsed;
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ if (entry.type !== "user")
58
+ continue;
59
+ const message = entry.message;
60
+ if (message && message.role && message.role !== "user")
61
+ continue;
62
+ const text = textFromContent(message?.content);
63
+ if (text)
64
+ return text;
65
+ }
66
+ return undefined;
67
+ }
68
+ /** Assembles config + classifier + cache, and exposes the resolved model id so the
69
+ * decision cache key invalidates when the model changes (§9). */
70
+ export async function buildRuntime() {
71
+ const config = resolveConfig();
72
+ const jev = createJevClassifier({
73
+ apiKey: resolveApiKey({ apiKeyEnv: config.classifier.jev.apiKeyEnv }) ?? "",
74
+ model: config.classifier.jev.model, baseUrl: config.classifier.jev.baseUrl,
75
+ timeoutMs: config.classifier.jev.timeoutMs, log: (m) => process.stderr.write(m + "\n"),
76
+ });
77
+ const generative = createGenerativeClassifier({ ...config.classifier.generative });
78
+ const primary = config.classifier.kind === "jev" ? jev : generative;
79
+ const model = config.classifier.kind === "jev" ? config.classifier.jev.model : config.classifier.generative.model;
80
+ const classifier = {
81
+ id: config.classifier.kind, model,
82
+ classify: (r) => classify(r, { kind: config.classifier.kind, primary, fallback: config.classifier.kind === "jev" ? generative : undefined }),
83
+ };
84
+ const cache = new GateCache(`${homedir()}/.auto-gate/gate-cache.json`);
85
+ return { config, classifier, cache, decide: (req) => decideRequest(req, { config, classifier, cache }) };
86
+ }
@@ -0,0 +1,7 @@
1
+ export interface AuditSummary {
2
+ total: number;
3
+ byDecision: Record<string, number>;
4
+ cacheHits: number;
5
+ redactionClasses: string[];
6
+ }
7
+ export declare function summarizeAudit(path: string): AuditSummary;
@@ -0,0 +1,29 @@
1
+ import { readFileSync } from "node:fs";
2
+ export function summarizeAudit(path) {
3
+ const s = { total: 0, byDecision: {}, cacheHits: 0, redactionClasses: [] };
4
+ let raw = "";
5
+ try {
6
+ raw = readFileSync(path, "utf8");
7
+ }
8
+ catch {
9
+ return s;
10
+ }
11
+ const redactions = new Set();
12
+ for (const line of raw.split("\n")) {
13
+ if (!line.trim())
14
+ continue;
15
+ try {
16
+ const e = JSON.parse(line);
17
+ s.total++;
18
+ if (e.decision)
19
+ s.byDecision[e.decision] = (s.byDecision[e.decision] ?? 0) + 1;
20
+ if (e.cacheHit)
21
+ s.cacheHits++;
22
+ for (const r of e.redactions ?? [])
23
+ redactions.add(r);
24
+ }
25
+ catch { /* skip a corrupt line rather than failing the whole report */ }
26
+ }
27
+ s.redactionClasses = [...redactions].sort();
28
+ return s;
29
+ }
@@ -0,0 +1,2 @@
1
+ /** Dry run: same pipeline, cache bypassed (config.cache=false), human-readable output. */
2
+ export declare function runTest(reqJson: string): Promise<string>;
@@ -0,0 +1,9 @@
1
+ import { decideRequest } from "../core/decide.js";
2
+ import { buildRuntime } from "./runtime.js";
3
+ /** Dry run: same pipeline, cache bypassed (config.cache=false), human-readable output. */
4
+ export async function runTest(reqJson) {
5
+ const { config, classifier, cache } = await buildRuntime();
6
+ const req = JSON.parse(reqJson);
7
+ const o = await decideRequest(req, { config: { ...config, cache: false }, classifier, cache });
8
+ return `${o.decision} (tier ${o.tier}) ${o.reason}`;
9
+ }
@@ -0,0 +1,8 @@
1
+ export interface KeyInput {
2
+ env?: Record<string, string | undefined>;
3
+ home?: string;
4
+ apiKeyEnv?: string;
5
+ keyFile?: string;
6
+ }
7
+ /** env → key file (§12). Never logged. Absent ⇒ undefined (caller asks). */
8
+ export declare function resolveApiKey(i?: KeyInput): string | undefined;
@@ -0,0 +1,17 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ /** env → key file (§12). Never logged. Absent ⇒ undefined (caller asks). */
4
+ export function resolveApiKey(i = {}) {
5
+ const env = i.env ?? process.env;
6
+ const fromEnv = env[i.apiKeyEnv ?? "TYPESAFE_API_KEY"];
7
+ if (fromEnv)
8
+ return fromEnv;
9
+ const file = i.keyFile ?? `${i.home ?? homedir()}/.auto-gate-key`;
10
+ try {
11
+ const v = readFileSync(file, "utf8").trim();
12
+ return v || undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
@@ -0,0 +1,17 @@
1
+ import { type AutoGateConfig } from "./schema.js";
2
+ export interface ResolveInput {
3
+ home?: string;
4
+ cwd?: string;
5
+ env?: Record<string, string | undefined>;
6
+ explicitPath?: string;
7
+ }
8
+ export type DeepPartial<T> = {
9
+ [K in keyof T]?: T[K] extends readonly (infer U)[] ? U[] : T[K] extends object ? DeepPartial<T[K]> : T[K];
10
+ };
11
+ /** Shallow-merge per section, with policy resolved against its named preset so
12
+ * a preset change plus a per-key override compose predictably (§12). Exported
13
+ * so the §3 host API (integration/host.ts) can compose squadrant's projected
14
+ * config with the same tested precedence (D-A2). */
15
+ export declare function mergeConfig(base: AutoGateConfig, over: DeepPartial<AutoGateConfig>): AutoGateConfig;
16
+ /** Precedence (low → high): defaults ← user ← repo ← explicit/--config/env. */
17
+ export declare function resolveConfig(input?: ResolveInput): AutoGateConfig;
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { DEFAULT_CONFIG } from "./schema.js";
5
+ import { PRESETS } from "../core/presets.js";
6
+ /** Shallow-merge per section, with policy resolved against its named preset so
7
+ * a preset change plus a per-key override compose predictably (§12). Exported
8
+ * so the §3 host API (integration/host.ts) can compose squadrant's projected
9
+ * config with the same tested precedence (D-A2). */
10
+ export function mergeConfig(base, over) {
11
+ const policyName = (over.policy?.name ?? base.policy.name);
12
+ const preset = PRESETS[policyName] ?? PRESETS.strict;
13
+ return {
14
+ ...base,
15
+ ...over,
16
+ classifier: { ...base.classifier, ...over.classifier,
17
+ jev: { ...base.classifier.jev, ...over.classifier?.jev },
18
+ generative: { ...base.classifier.generative, ...over.classifier?.generative } },
19
+ // Re-resolve the preset ONLY when this layer touches policy; otherwise keep the
20
+ // already-composed base (re-expanding on every layer would reset per-key overrides).
21
+ policy: (over.policy
22
+ ? { ...preset, ...over.policy, name: policyName, hazards: { ...preset.hazards, ...over.policy.hazards } }
23
+ : base.policy),
24
+ agents: { ...base.agents, ...over.agents },
25
+ ask: { ...base.ask, ...over.ask },
26
+ tools: over.tools ?? base.tools,
27
+ toolAliases: (over.toolAliases ?? base.toolAliases),
28
+ audit: { ...base.audit, ...over.audit },
29
+ };
30
+ }
31
+ function readJsonLayer(path) {
32
+ try {
33
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
34
+ return parsed && typeof parsed === "object" ? parsed : {};
35
+ }
36
+ catch {
37
+ return {}; // missing or malformed ⇒ this layer contributes nothing (never throws)
38
+ }
39
+ }
40
+ /** Precedence (low → high): defaults ← user ← repo ← explicit/--config/env. */
41
+ export function resolveConfig(input = {}) {
42
+ const home = input.home ?? homedir();
43
+ const cwd = input.cwd ?? process.cwd();
44
+ const env = input.env ?? process.env;
45
+ const explicit = input.explicitPath ?? env.AUTO_GATE_CONFIG;
46
+ let cfg = mergeConfig({ ...DEFAULT_CONFIG, policy: PRESETS.strict }, {});
47
+ cfg = mergeConfig(cfg, readJsonLayer(join(home, ".config", "auto-gate", "config.json")));
48
+ cfg = mergeConfig(cfg, readJsonLayer(join(cwd, ".auto-gate.json")));
49
+ if (explicit)
50
+ cfg = mergeConfig(cfg, readJsonLayer(explicit));
51
+ return cfg;
52
+ }
@@ -0,0 +1,38 @@
1
+ import type { PolicyConfig, ToolAliases } from "../core/types.js";
2
+ export interface AutoGateConfig {
3
+ mode: "on" | "off" | "auto";
4
+ agents: Record<string, {
5
+ enabled?: boolean;
6
+ }>;
7
+ classifier: {
8
+ kind: "jev" | "generative";
9
+ jev: {
10
+ model: string;
11
+ baseUrl: string;
12
+ apiKeyEnv: string;
13
+ timeoutMs: number;
14
+ };
15
+ generative: {
16
+ baseUrl: string;
17
+ model: string;
18
+ apiKeyEnv?: string;
19
+ timeoutMs: number;
20
+ };
21
+ };
22
+ policy: PolicyConfig;
23
+ tools: string[];
24
+ toolAliasesVersion: number;
25
+ toolAliases: ToolAliases;
26
+ deny?: string[];
27
+ ask: {
28
+ onAsk: string | null;
29
+ timeoutMs: number;
30
+ };
31
+ cache: boolean;
32
+ audit: {
33
+ path: string;
34
+ };
35
+ }
36
+ /** Standalone default is ON (a user who installs a gate wants it on). This
37
+ * differs from squadrant's `defaults.gate.mode` default of "auto" (§12). */
38
+ export declare const DEFAULT_CONFIG: AutoGateConfig;
@@ -0,0 +1,19 @@
1
+ /** Standalone default is ON (a user who installs a gate wants it on). This
2
+ * differs from squadrant's `defaults.gate.mode` default of "auto" (§12). */
3
+ export const DEFAULT_CONFIG = {
4
+ mode: "on",
5
+ agents: { claude: { enabled: true }, opencode: { enabled: true } },
6
+ classifier: {
7
+ kind: "jev",
8
+ jev: { model: "jev-latest", baseUrl: "https://api.typesafe.ai", apiKeyEnv: "TYPESAFE_API_KEY", timeoutMs: 4000 },
9
+ generative: { baseUrl: "", model: "", timeoutMs: 3000 },
10
+ },
11
+ // policy defaults come from presets.ts so there is exactly one source of truth
12
+ policy: {}, // replaced in resolve.ts with PRESETS.strict
13
+ tools: ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit"],
14
+ toolAliasesVersion: 1,
15
+ toolAliases: { opencode: { bash: ["Bash"], edit: ["Edit", "Write", "MultiEdit"] } },
16
+ ask: { onAsk: null, timeoutMs: 120000 },
17
+ cache: true,
18
+ audit: { path: "~/.auto-gate/decisions.jsonl" },
19
+ };
@@ -0,0 +1,21 @@
1
+ export interface AuditEntry {
2
+ ts: string;
3
+ agent: string;
4
+ tool: string;
5
+ cwd: string;
6
+ decision: string;
7
+ verdict: string;
8
+ classifier: string;
9
+ tier: 1 | 2;
10
+ cacheHit: boolean;
11
+ latencyMs: number;
12
+ probabilities?: Record<string, number>;
13
+ hazards?: Record<string, number>;
14
+ severity?: number;
15
+ confidence?: number;
16
+ inputTokens?: number;
17
+ redactions: string[];
18
+ }
19
+ /** Append-only JSONL, 0600. NEVER logs raw payloads/secrets — only the redaction
20
+ * ids that fired (§9/§12). Best-effort: an audit write never breaks a decision. */
21
+ export declare function appendAudit(path: string, entry: AuditEntry): void;
@@ -0,0 +1,12 @@
1
+ import { appendFileSync, chmodSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ /** Append-only JSONL, 0600. NEVER logs raw payloads/secrets — only the redaction
4
+ * ids that fired (§9/§12). Best-effort: an audit write never breaks a decision. */
5
+ export function appendAudit(path, entry) {
6
+ try {
7
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
8
+ appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
9
+ chmodSync(path, 0o600);
10
+ }
11
+ catch { /* best-effort */ }
12
+ }
@@ -0,0 +1,22 @@
1
+ import type { GateOutcome } from "./types.js";
2
+ export interface CacheKeyParts {
3
+ agent: string;
4
+ toolName: string;
5
+ cwd: string;
6
+ redactedPayload: string;
7
+ redactedUserIntent: string | null;
8
+ policyVersion: string;
9
+ toolAliasesVersion: number;
10
+ classifierId: string;
11
+ model: string;
12
+ }
13
+ export declare function cacheKey(k: CacheKeyParts): string;
14
+ export declare class GateCache {
15
+ private file;
16
+ private entries;
17
+ constructor(file: string);
18
+ get(key: string): GateOutcome | undefined;
19
+ set(key: string, outcome: GateOutcome): void;
20
+ private load;
21
+ private save;
22
+ }
@@ -0,0 +1,59 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, writeFileSync, renameSync, chmodSync, mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ export function cacheKey(k) {
5
+ const intentHash = createHash("sha256").update(k.redactedUserIntent ?? "").digest("hex");
6
+ return createHash("sha256")
7
+ .update([k.agent, k.toolName, k.cwd, k.redactedPayload, intentHash, k.policyVersion,
8
+ String(k.toolAliasesVersion), k.classifierId, k.model].join("\n"))
9
+ .digest("hex");
10
+ }
11
+ const TTL_MS = 10 * 60_000;
12
+ const MAX = 500;
13
+ export class GateCache {
14
+ file;
15
+ entries = new Map();
16
+ constructor(file) {
17
+ this.file = file;
18
+ this.load();
19
+ }
20
+ get(key) {
21
+ const e = this.entries.get(key);
22
+ if (!e)
23
+ return undefined;
24
+ if (Date.now() - e.at > TTL_MS) {
25
+ this.entries.delete(key);
26
+ return undefined;
27
+ }
28
+ return { ...e.outcome, cached: true };
29
+ }
30
+ set(key, outcome) {
31
+ if (outcome.decision !== "allow" && outcome.decision !== "deny")
32
+ return; // never cache ask
33
+ this.entries.set(key, { at: Date.now(), outcome: { ...outcome, cached: undefined } });
34
+ if (this.entries.size > MAX) {
35
+ const oldest = [...this.entries.entries()].sort((a, b) => a[1].at - b[1].at)[0];
36
+ if (oldest)
37
+ this.entries.delete(oldest[0]);
38
+ }
39
+ this.save();
40
+ }
41
+ load() {
42
+ try {
43
+ const raw = JSON.parse(readFileSync(this.file, "utf8"));
44
+ for (const [k, v] of Object.entries(raw))
45
+ this.entries.set(k, v);
46
+ }
47
+ catch { /* missing/corrupt cache is not an error */ }
48
+ }
49
+ save() {
50
+ try {
51
+ mkdirSync(dirname(this.file), { recursive: true, mode: 0o700 });
52
+ const tmp = `${this.file}.${process.pid}.tmp`;
53
+ writeFileSync(tmp, JSON.stringify(Object.fromEntries(this.entries)), { mode: 0o600 });
54
+ renameSync(tmp, this.file);
55
+ chmodSync(this.file, 0o600);
56
+ }
57
+ catch { /* cache write failure must never break a decision */ }
58
+ }
59
+ }
@@ -0,0 +1,26 @@
1
+ import type { GateAssessment, GateOutcome, GateRequest } from "./types.js";
2
+ import { GateCache } from "./cache.js";
3
+ export interface DecideContext {
4
+ config: {
5
+ mode: "on" | "off" | "auto";
6
+ policy: import("./types.js").PolicyConfig;
7
+ tools: string[];
8
+ toolAliasesVersion: number;
9
+ toolAliases: import("./types.js").ToolAliases;
10
+ deny?: string[];
11
+ cache: boolean;
12
+ audit: {
13
+ path: string;
14
+ };
15
+ };
16
+ classifier: {
17
+ id: string;
18
+ model?: string;
19
+ classify(req: GateRequest): Promise<GateAssessment>;
20
+ };
21
+ cache: GateCache;
22
+ env?: Record<string, string | undefined>;
23
+ now?: () => number;
24
+ }
25
+ /** The one pipeline (§4): scope → redact → cache → Tier-1 → classify → policy → audit. */
26
+ export declare function decideRequest(req: GateRequest, ctx: DecideContext): Promise<GateOutcome>;