@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,75 @@
1
+ import { decide } from "./policy.js";
2
+ import { matchesTier1Deny, DEFAULT_TIER1_DENY } from "./tier1.js";
3
+ import { policyVersion } from "./policy-version.js";
4
+ import { redact } from "./redact.js";
5
+ import { cacheKey } from "./cache.js";
6
+ import { appendAudit } from "./audit.js";
7
+ import { isGateSession } from "./scope.js";
8
+ const YIELD = (reason) => ({ decision: "yield", tier: 2, reason });
9
+ /** The one pipeline (§4): scope → redact → cache → Tier-1 → classify → policy → audit. */
10
+ export async function decideRequest(req, ctx) {
11
+ const started = (ctx.now ?? Date.now)();
12
+ const env = ctx.env ?? process.env;
13
+ // 1. Scope — never gate the operator's own session (§10)
14
+ const markers = env.AUTO_GATE_SESSION === "1" || req.sessionKind !== "standalone";
15
+ if (ctx.config.mode === "off" || !isGateSession(env) && !markers)
16
+ return YIELD("out of scope");
17
+ if (req.permissionMode === "auto")
18
+ return YIELD("agent permission mode is auto");
19
+ // 2. Redact BEFORE anything leaves the process (§6.2)
20
+ const payload = redact(req.toolPayload);
21
+ const intent = req.userIntent ? redact(req.userIntent) : undefined;
22
+ // 3. Cache (only conclusive outcomes are cached — §9)
23
+ const pv = policyVersion(ctx.config.policy);
24
+ const key = cacheKeyOf(req, payload.text, intent?.text ?? null, pv, ctx);
25
+ if (ctx.config.cache) {
26
+ const hit = ctx.cache.get(key);
27
+ if (hit) {
28
+ // A hit bypasses the classifier, but it MUST still be audited — otherwise
29
+ // the cache-hit rate (§9) is structurally unmeasurable.
30
+ appendAudit(ctx.config.audit.path, {
31
+ ts: new Date(started).toISOString(),
32
+ agent: req.agent, tool: req.toolName, cwd: req.cwd,
33
+ decision: hit.decision, verdict: "cached", classifier: "cache", tier: hit.tier,
34
+ cacheHit: true, latencyMs: (ctx.now ?? Date.now)() - started,
35
+ redactions: [...payload.fired, ...(intent?.fired ?? [])],
36
+ });
37
+ return hit;
38
+ }
39
+ }
40
+ // 4. Tier-1 static deny — model-free, ~0 ms
41
+ const tier1 = matchesTier1Deny(payload.text, ctx.config.deny?.map((s) => new RegExp(s)) ?? DEFAULT_TIER1_DENY);
42
+ // 5. Tier-2 classify (never throws) → 6. policy
43
+ let assessment;
44
+ if (!tier1)
45
+ assessment = await ctx.classifier.classify(req);
46
+ const outcome = assessment
47
+ ? decide(req, assessment, ctx.config.policy, tier1)
48
+ : decide(req, { verdict: "ask", classifier: "skipped", tier: 2 }, ctx.config.policy, tier1);
49
+ // 7. Persist + audit
50
+ if (ctx.config.cache)
51
+ ctx.cache.set(key, outcome);
52
+ appendAudit(ctx.config.audit.path, {
53
+ ts: new Date(started).toISOString(),
54
+ agent: req.agent, tool: req.toolName, cwd: req.cwd,
55
+ decision: outcome.decision, verdict: assessment?.verdict ?? "n/a",
56
+ classifier: assessment?.classifier ?? "tier1", tier: outcome.tier,
57
+ cacheHit: false, latencyMs: (ctx.now ?? Date.now)() - started,
58
+ ...(assessment?.probabilities ? { probabilities: assessment.probabilities } : {}),
59
+ ...(assessment?.hazards ? { hazards: assessment.hazards } : {}),
60
+ ...(assessment?.severity !== undefined ? { severity: assessment.severity } : {}),
61
+ ...(assessment?.confidence !== undefined ? { confidence: assessment.confidence } : {}),
62
+ ...(assessment?.usage?.inputTokens !== undefined ? { inputTokens: assessment.usage.inputTokens } : {}),
63
+ redactions: [...payload.fired, ...(intent?.fired ?? [])],
64
+ });
65
+ return outcome;
66
+ }
67
+ function cacheKeyOf(req, redactedPayload, redactedIntent, pv, ctx) {
68
+ // Reuse P1's cacheKey by constructing its parts (single source of truth).
69
+ return cacheKey({
70
+ agent: req.agent, toolName: req.toolName, cwd: req.cwd, redactedPayload,
71
+ redactedUserIntent: redactedIntent, policyVersion: pv,
72
+ toolAliasesVersion: ctx.config.toolAliasesVersion,
73
+ classifierId: ctx.classifier.id, model: ctx.classifier.model ?? "",
74
+ });
75
+ }
@@ -0,0 +1,2 @@
1
+ import type { PolicyConfig } from "./types.js";
2
+ export declare function policyVersion(p: PolicyConfig): string;
@@ -0,0 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ /** Deterministic (sorted-key) JSON, so key order can never change the hash. */
3
+ function canonical(v) {
4
+ if (Array.isArray(v))
5
+ return `[${v.map(canonical).join(",")}]`;
6
+ if (v && typeof v === "object") {
7
+ const o = v;
8
+ return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonical(o[k])}`).join(",")}}`;
9
+ }
10
+ return JSON.stringify(v);
11
+ }
12
+ export function policyVersion(p) {
13
+ return createHash("sha256").update(canonical(p)).digest("hex");
14
+ }
@@ -0,0 +1,2 @@
1
+ import type { GateAssessment, GateRequest, GateOutcome, PolicyConfig } from "./types.js";
2
+ export declare function decide(req: GateRequest, a: GateAssessment, p: PolicyConfig, tier1Matched: boolean): GateOutcome;
@@ -0,0 +1,36 @@
1
+ const GENERATIVE = new Set(["generative", "router"]);
2
+ export function decide(req, a, p, tier1Matched) {
3
+ const P = (k) => a.probabilities?.[k];
4
+ const H = (k) => a.hazards?.[k];
5
+ const sev = a.severity;
6
+ // 1. DENY
7
+ if (tier1Matched)
8
+ return { decision: "deny", tier: 1, reason: "tier-1 static deny" };
9
+ if ((P("deny") ?? 0) >= p.denyThreshold)
10
+ return { decision: "deny", tier: 2, reason: "P(deny) ≥ denyThreshold" };
11
+ if ((H("destructive") ?? 0) >= p.actionThreshold && (sev ?? 0) >= p.severityBlock)
12
+ return { decision: "deny", tier: 2, reason: "destructive ≥ action ∧ severity ≥ block" };
13
+ if ((H("secrets") ?? 0) >= p.actionThreshold && (H("scope_escape") ?? 0) >= p.actionThreshold)
14
+ return { decision: "deny", tier: 2, reason: "secrets ≥ action ∧ scope_escape ≥ action" };
15
+ if ((H("scope_escape") ?? 0) >= p.actionThreshold && (sev ?? 0) >= p.severityBlock)
16
+ return { decision: "deny", tier: 2, reason: "scope_escape ≥ action ∧ severity ≥ block" };
17
+ // 2. GENERATIVE FALLBACK (only if explicitly opted in)
18
+ const isGenerative = [...GENERATIVE].some((g) => a.classifier.startsWith(g));
19
+ if (p.fallbackAcceptsVerdict && isGenerative && (a.verdict === "allow" || a.verdict === "deny"))
20
+ return { decision: a.verdict, tier: 2, reason: "generative verdict (fallbackAcceptsVerdict)" };
21
+ // 3. ALLOW — every condition must hold
22
+ const confidenceOk = a.confidence !== undefined && a.confidence >= p.minConfidence;
23
+ const intentOk = req.intentSupport === "unsupported" ? true
24
+ : req.intentSupport === "pending" ? false
25
+ : (H("intent_match") ?? 0) >= p.intentThreshold;
26
+ if ((P("allow") ?? 0) >= p.allowThreshold &&
27
+ confidenceOk &&
28
+ (H("destructive") ?? 1) < p.reviewThreshold &&
29
+ (H("secrets") ?? 1) < p.reviewThreshold &&
30
+ (H("scope_escape") ?? 1) < p.reviewThreshold &&
31
+ intentOk &&
32
+ (sev ?? 1) < p.reviewSeverity)
33
+ return { decision: "allow", tier: 2, reason: "allow conditions satisfied" };
34
+ // 4. ASK — catch-all
35
+ return { decision: "ask", tier: 2, reason: "catch-all ask" };
36
+ }
@@ -0,0 +1,9 @@
1
+ import type { PolicyConfig } from "./types.js";
2
+ export declare const STRICT: PolicyConfig;
3
+ export declare const BALANCED: PolicyConfig;
4
+ export declare const PERMISSIVE: PolicyConfig;
5
+ export declare const PRESETS: {
6
+ readonly strict: PolicyConfig;
7
+ readonly balanced: PolicyConfig;
8
+ readonly permissive: PolicyConfig;
9
+ };
@@ -0,0 +1,15 @@
1
+ const hazards = {
2
+ destructive: { review: 0.35, action: 0.8, vetoesAllow: true },
3
+ secrets: { review: 0.35, action: 0.8, vetoesAllow: true },
4
+ scope_escape: { review: 0.35, action: 0.8, vetoesAllow: true },
5
+ intent_match: { allowFloor: 0.5, vetoesAllow: false },
6
+ };
7
+ export const STRICT = {
8
+ name: "strict", reviewThreshold: 0.35, actionThreshold: 0.8, allowThreshold: 0.8,
9
+ denyThreshold: 0.75, minConfidence: 0.75, intentThreshold: 0.5, reviewSeverity: 1.0,
10
+ severityBlock: 2.0, fallbackAcceptsVerdict: false, hazards,
11
+ tools: ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit"],
12
+ };
13
+ export const BALANCED = { ...STRICT, name: "balanced", allowThreshold: 0.75, denyThreshold: 0.8, minConfidence: 0.65, intentThreshold: 0.4 };
14
+ export const PERMISSIVE = { ...STRICT, name: "permissive", allowThreshold: 0.7, denyThreshold: 0.85, minConfidence: 0.55, intentThreshold: 0.3, actionThreshold: 0.85 };
15
+ export const PRESETS = { strict: STRICT, balanced: BALANCED, permissive: PERMISSIVE };
@@ -0,0 +1,7 @@
1
+ export declare const REDACTION_PATTERNS_VERSION = "v1";
2
+ export interface RedactResult {
3
+ text: string;
4
+ fired: string[];
5
+ }
6
+ /** Redact secret patterns. Pure; `fired` names the rules that matched (never the secret). */
7
+ export declare function redact(input: string): RedactResult;
@@ -0,0 +1,26 @@
1
+ export const REDACTION_PATTERNS_VERSION = "v1";
2
+ // Order matters: the more specific token shapes run before the generic KEY=value rule.
3
+ const RULES = [
4
+ { id: "bearer", re: /\b(Bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi, replace: "$1 [REDACTED]" },
5
+ { id: "gh-token", re: /\bgh[pousr]_[A-Za-z0-9]{20,}/g, replace: "[REDACTED]" },
6
+ { id: "openai-key", re: /\bsk-[A-Za-z0-9_-]{16,}/g, replace: "[REDACTED]" },
7
+ { id: "aws-id", re: /\b(AKIA|ASIA)[A-Z0-9]{16}\b/g, replace: "[REDACTED]" },
8
+ { id: "private-key", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, replace: "[REDACTED PRIVATE KEY]" },
9
+ { id: "pem-body", re: /MII[A-Za-z0-9+/=]{40,}/g, replace: "[REDACTED]" },
10
+ { id: "env-assign", re: /\b([A-Z][A-Z0-9_]{2,})=(["']?)([^\s"']{6,})\2/g, replace: "$1=[REDACTED]" },
11
+ ];
12
+ /** Redact secret patterns. Pure; `fired` names the rules that matched (never the secret). */
13
+ export function redact(input) {
14
+ if (typeof input !== "string" || input.length === 0)
15
+ return { text: "", fired: [] };
16
+ let text = input;
17
+ const fired = [];
18
+ for (const rule of RULES) {
19
+ if (rule.re.test(text)) {
20
+ fired.push(rule.id);
21
+ text = text.replace(rule.re, rule.replace);
22
+ }
23
+ rule.re.lastIndex = 0; // /g regexes are stateful — reset between calls
24
+ }
25
+ return { text, fired };
26
+ }
@@ -0,0 +1,2 @@
1
+ /** Positive session markers only — never scope the operator's own session (§10). */
2
+ export declare function isGateSession(env: Record<string, string | undefined>): boolean;
@@ -0,0 +1,7 @@
1
+ /** Positive session markers only — never scope the operator's own session (§10). */
2
+ export function isGateSession(env) {
3
+ return Boolean(env.SQUADRANT_CREW_TASK_ID ||
4
+ env.SQUADRANT_SIDE_SESSION === "1" ||
5
+ env.SQUADRANT_ROLE === "captain" ||
6
+ env.AUTO_GATE_SESSION === "1");
7
+ }
@@ -0,0 +1,3 @@
1
+ /** ~0 ms, model-free first pass. The ONLY independent backstop (spec §12). */
2
+ export declare const DEFAULT_TIER1_DENY: RegExp[];
3
+ export declare function matchesTier1Deny(payload: string, deny?: RegExp[]): boolean;
@@ -0,0 +1,16 @@
1
+ /** ~0 ms, model-free first pass. The ONLY independent backstop (spec §12). */
2
+ export const DEFAULT_TIER1_DENY = [
3
+ /(^|[;&|]\s*)sudo\b/,
4
+ // rm with recursive+force in any flag order, targeting / or a system path
5
+ /\brm\s+(-[a-z]*[rf][a-z]*\s+)+(-[a-z]*[rf][a-z]*\s+)*\/(\s|$)/i,
6
+ /\brm\s+--(recursive|force)\b[^\n]*\/(\s|$)/i,
7
+ /\b(mkfs(\.\w+)?|dd\s+[^\n]*of=\/dev\/)\b/,
8
+ /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, // fork bomb
9
+ /\bchmod\s+(-R\s+)?0?777\s+\/(\s|$)/,
10
+ /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(ba|z|da)?sh\b/,
11
+ ];
12
+ export function matchesTier1Deny(payload, deny = DEFAULT_TIER1_DENY) {
13
+ if (typeof payload !== "string" || payload.length === 0)
14
+ return false;
15
+ return deny.some((re) => re.test(payload));
16
+ }
@@ -0,0 +1,5 @@
1
+ import type { ToolAliases } from "./types.js";
2
+ /** Canonical names for a native tool name. Empty ⇒ unmapped ⇒ out of scope. */
3
+ export declare function canonicalNames(agent: string, nativeName: string, aliases: ToolAliases): string[];
4
+ /** Scope match: ANY canonical name in the set is in scope (§15#6, any-of). */
5
+ export declare function isInScope(agent: string, nativeName: string, aliases: ToolAliases, tools: string[]): boolean;
@@ -0,0 +1,21 @@
1
+ const CLAUDE_CANONICAL = new Set(["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit"]);
2
+ /** Canonical names for a native tool name. Empty ⇒ unmapped ⇒ out of scope. */
3
+ export function canonicalNames(agent, nativeName, aliases) {
4
+ const perAgent = aliases[agent];
5
+ if (perAgent) {
6
+ const mapped = perAgent[nativeName];
7
+ if (mapped)
8
+ return mapped;
9
+ // A claude-shaped name passed through an aliased agent still matches.
10
+ if (CLAUDE_CANONICAL.has(nativeName))
11
+ return [nativeName];
12
+ return [];
13
+ }
14
+ // No alias table for this agent: claude-shaped names are canonical; others out of scope.
15
+ return CLAUDE_CANONICAL.has(nativeName) ? [nativeName] : [];
16
+ }
17
+ /** Scope match: ANY canonical name in the set is in scope (§15#6, any-of). */
18
+ export function isInScope(agent, nativeName, aliases, tools) {
19
+ const canon = canonicalNames(agent, nativeName, aliases);
20
+ return canon.some((c) => tools.includes(c));
21
+ }
@@ -0,0 +1,68 @@
1
+ export type ClassifierVerdict = "allow" | "deny" | "ask";
2
+ export type GateOutcomeDecision = "allow" | "deny" | "ask" | "yield";
3
+ export type IntentSupport = "available" | "pending" | "unsupported";
4
+ /** native tool name → set of canonical (claude-shaped) names */
5
+ export type ToolAliases = Record<string, Record<string, string[]>>;
6
+ export interface GateRequest {
7
+ agent: string;
8
+ toolName: string;
9
+ toolPayload: string;
10
+ cwd: string;
11
+ userIntent?: string | null;
12
+ intentSupport: IntentSupport;
13
+ permissionMode?: string;
14
+ sessionKind: "crew" | "side" | "captain" | "standalone";
15
+ raw?: unknown;
16
+ }
17
+ export interface GateAssessment {
18
+ verdict: ClassifierVerdict;
19
+ confidence?: number;
20
+ probabilities?: Record<string, number>;
21
+ hazards?: Record<string, number>;
22
+ severity?: number;
23
+ classifier: string;
24
+ tier: 2;
25
+ usage?: {
26
+ inputTokens?: number;
27
+ };
28
+ }
29
+ export interface GateOutcome {
30
+ decision: GateOutcomeDecision;
31
+ tier: 1 | 2;
32
+ reason: string;
33
+ cached?: boolean;
34
+ }
35
+ export interface PolicyConfig {
36
+ name: "strict" | "balanced" | "permissive";
37
+ reviewThreshold: number;
38
+ actionThreshold: number;
39
+ allowThreshold: number;
40
+ denyThreshold: number;
41
+ minConfidence: number;
42
+ intentThreshold: number;
43
+ reviewSeverity: number;
44
+ severityBlock: number;
45
+ fallbackAcceptsVerdict: boolean;
46
+ hazards: {
47
+ destructive: {
48
+ review: number;
49
+ action: number;
50
+ vetoesAllow: boolean;
51
+ };
52
+ secrets: {
53
+ review: number;
54
+ action: number;
55
+ vetoesAllow: boolean;
56
+ };
57
+ scope_escape: {
58
+ review: number;
59
+ action: number;
60
+ vetoesAllow: boolean;
61
+ };
62
+ intent_match: {
63
+ allowFloor: number;
64
+ vetoesAllow: boolean;
65
+ };
66
+ };
67
+ tools: string[];
68
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ export * from "./core/types.js";
2
+ export * from "./core/tools.js";
3
+ export * from "./core/tier1.js";
4
+ export * from "./core/policy.js";
5
+ export * from "./core/presets.js";
6
+ export * from "./core/policy-version.js";
7
+ export * from "./core/cache.js";
8
+ export * from "./core/scope.js";
9
+ export * from "./core/redact.js";
10
+ export * from "./classifiers/classify.js";
11
+ export * from "./classifiers/jev.js";
12
+ export * from "./classifiers/generative.js";
13
+ export * from "./classifiers/null.js";
14
+ export * from "./config/schema.js";
15
+ export * from "./config/resolve.js";
16
+ export * from "./config/keys.js";
17
+ export * from "./core/audit.js";
18
+ export * from "./core/decide.js";
19
+ export * from "./adapters/claude/payload.js";
20
+ export * from "./adapters/claude/output.js";
21
+ export * from "./adapters/claude/settings.js";
22
+ export * from "./adapters/claude/blocked-signal.js";
23
+ export * from "./adapters/opencode/lock.js";
24
+ export * from "./adapters/opencode/port.js";
25
+ export * from "./adapters/opencode/sse.js";
26
+ export * from "./adapters/opencode/intent.js";
27
+ export * from "./adapters/opencode/answer.js";
28
+ export * from "./adapters/opencode/install.js";
29
+ export * from "./integration/project.js";
30
+ export * from "./integration/host.js";
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ export * from "./core/types.js";
2
+ export * from "./core/tools.js";
3
+ export * from "./core/tier1.js";
4
+ export * from "./core/policy.js";
5
+ export * from "./core/presets.js";
6
+ export * from "./core/policy-version.js";
7
+ export * from "./core/cache.js";
8
+ export * from "./core/scope.js";
9
+ export * from "./core/redact.js";
10
+ export * from "./classifiers/classify.js";
11
+ export * from "./classifiers/jev.js";
12
+ export * from "./classifiers/generative.js";
13
+ export * from "./classifiers/null.js";
14
+ export * from "./config/schema.js";
15
+ export * from "./config/resolve.js";
16
+ export * from "./config/keys.js";
17
+ export * from "./core/audit.js";
18
+ export * from "./core/decide.js";
19
+ export * from "./adapters/claude/payload.js";
20
+ export * from "./adapters/claude/output.js";
21
+ export * from "./adapters/claude/settings.js";
22
+ export * from "./adapters/claude/blocked-signal.js";
23
+ export * from "./adapters/opencode/lock.js";
24
+ export * from "./adapters/opencode/port.js";
25
+ export * from "./adapters/opencode/sse.js";
26
+ export * from "./adapters/opencode/intent.js";
27
+ export * from "./adapters/opencode/answer.js";
28
+ export * from "./adapters/opencode/install.js";
29
+ export * from "./integration/project.js";
30
+ export * from "./integration/host.js";
@@ -0,0 +1,28 @@
1
+ import type { GateOutcome } from "../core/types.js";
2
+ import type { AutoGateConfig } from "../config/schema.js";
3
+ export interface BlockedSignalCtx {
4
+ agent: string;
5
+ tool: string;
6
+ reason: string;
7
+ project?: string;
8
+ gateId?: string;
9
+ }
10
+ export interface AutoGateHost {
11
+ /** Interface 1 — a partial projection of squadrant's defaults.gate, or a full config. */
12
+ config?: Partial<AutoGateConfig>;
13
+ /** Interface 2 — supplied by the host; the package never reads a transcript itself. */
14
+ readIntent: (transcriptPath: string | undefined) => string | undefined;
15
+ /** Interface 3 — the ask/yield callback. squadrant maps this to #560 task.blocked. */
16
+ blockedSignal?: (ctx: BlockedSignalCtx) => Promise<void>;
17
+ /** Injectable for tests; defaults to the real P3 runtime. */
18
+ decide?: (req: unknown) => Promise<GateOutcome>;
19
+ project?: string;
20
+ env?: Record<string, string | undefined>;
21
+ log?: (m: string) => void;
22
+ }
23
+ /** The single entry squadrant consumes (§3). It owns NOTHING of squadrant's config:
24
+ * the host injects the resolved config, the intent reader, and the blocked signal. */
25
+ export declare function createAutoGate(host: AutoGateHost): {
26
+ config: AutoGateConfig;
27
+ decideClaudeHookPayload(raw: string): Promise<string | undefined>;
28
+ };
@@ -0,0 +1,37 @@
1
+ import { mapClaudeHookPayload } from "../adapters/claude/payload.js";
2
+ import { formatClaudeHookOutput } from "../adapters/claude/output.js";
3
+ import { resolveConfig, mergeConfig } from "../config/resolve.js";
4
+ import { buildRuntime } from "../cli/runtime.js";
5
+ /** The single entry squadrant consumes (§3). It owns NOTHING of squadrant's config:
6
+ * the host injects the resolved config, the intent reader, and the blocked signal. */
7
+ export function createAutoGate(host) {
8
+ // D-A2: compose through the same tested precedence as resolveConfig, so a
9
+ // partial projected section (notably a named `policy` preset) re-expands
10
+ // instead of replacing the base section.
11
+ const config = mergeConfig(resolveConfig(), host.config ?? {});
12
+ async function decide(req) {
13
+ if (host.decide)
14
+ return host.decide(req);
15
+ const rt = await buildRuntime();
16
+ return rt.decide(req);
17
+ }
18
+ return {
19
+ config,
20
+ async decideClaudeHookPayload(raw) {
21
+ try {
22
+ const payload = JSON.parse(raw);
23
+ const req = mapClaudeHookPayload(payload, host.env ?? process.env, host.readIntent);
24
+ const outcome = await decide(req);
25
+ const out = formatClaudeHookOutput(outcome);
26
+ if (out)
27
+ return out;
28
+ await host.blockedSignal?.({ agent: "claude", tool: req.toolName, reason: outcome.reason, ...(host.project ? { project: host.project } : {}) });
29
+ return undefined;
30
+ }
31
+ catch (e) {
32
+ host.log?.(`auto-gate host: decide failed — ${e.message}`); // print nothing ⇒ dialog appears
33
+ return undefined;
34
+ }
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,24 @@
1
+ import type { AutoGateConfig } from "../config/schema.js";
2
+ /** The subset of squadrant's `defaults.gate` this projection understands. */
3
+ export interface SquadrantGateShape {
4
+ mode?: "on" | "off" | "auto";
5
+ policy?: {
6
+ name?: string;
7
+ [k: string]: unknown;
8
+ };
9
+ ask?: {
10
+ onAsk?: string | null;
11
+ timeoutMs?: number;
12
+ };
13
+ tools?: string[];
14
+ deny?: string[];
15
+ }
16
+ export interface SquadrantConfigShape {
17
+ gate?: SquadrantGateShape;
18
+ }
19
+ type Projected = Partial<AutoGateConfig>;
20
+ /** §3 bridge interface 1. The package NEVER reads squadrant's config file — the
21
+ * host hands it a projected object. `auto` is squadrant's documented no-op, so
22
+ * it maps to the package's `off`; only an explicit `on` enables the gate. */
23
+ export declare function projectGateConfig(squadrant: SquadrantConfigShape): Projected;
24
+ export {};
@@ -0,0 +1,19 @@
1
+ /** §3 bridge interface 1. The package NEVER reads squadrant's config file — the
2
+ * host hands it a projected object. `auto` is squadrant's documented no-op, so
3
+ * it maps to the package's `off`; only an explicit `on` enables the gate. */
4
+ export function projectGateConfig(squadrant) {
5
+ const g = squadrant.gate;
6
+ if (!g)
7
+ return { mode: "off" };
8
+ const mode = g.mode === "on" ? "on" : "off"; // "auto" and undefined are no-ops
9
+ const out = { mode };
10
+ if (g.policy)
11
+ out.policy = g.policy;
12
+ if (g.ask)
13
+ out.ask = { onAsk: g.ask.onAsk ?? null, timeoutMs: g.ask.timeoutMs ?? 120000 };
14
+ if (g.tools)
15
+ out.tools = g.tools;
16
+ if (g.deny)
17
+ out.deny = g.deny;
18
+ return out;
19
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@squadrant-ai/auto-gate",
3
+ "version": "0.1.0",
4
+ "description": "Provider-agnostic auto permission gate",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "auto-gate": "dist/cli/index.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "test": "vitest run"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "license": "MIT",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "packageManager": "pnpm@10.33.4",
32
+ "devDependencies": {
33
+ "@types/node": "^26.6.2",
34
+ "typescript": "^7.0.2",
35
+ "vitest": "^5.0.1"
36
+ }
37
+ }