@hiro-c/agent-gate 1.0.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.
- package/AGENTS.md +76 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/adapters/Adapter.d.ts +32 -0
- package/dist/adapters/Adapter.d.ts.map +1 -0
- package/dist/adapters/Adapter.js +2 -0
- package/dist/adapters/claude-code/adapter.d.ts +3 -0
- package/dist/adapters/claude-code/adapter.d.ts.map +1 -0
- package/dist/adapters/claude-code/adapter.js +45 -0
- package/dist/adapters/claude-code/transcript.d.ts +16 -0
- package/dist/adapters/claude-code/transcript.d.ts.map +1 -0
- package/dist/adapters/claude-code/transcript.js +104 -0
- package/dist/adapters/cursor/adapter.d.ts +3 -0
- package/dist/adapters/cursor/adapter.d.ts.map +1 -0
- package/dist/adapters/cursor/adapter.js +89 -0
- package/dist/adapters/index.d.ts +8 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +20 -0
- package/dist/cli/agent-gate.d.ts +13 -0
- package/dist/cli/agent-gate.d.ts.map +1 -0
- package/dist/cli/agent-gate.js +226 -0
- package/dist/cli/installer.d.ts +21 -0
- package/dist/cli/installer.d.ts.map +1 -0
- package/dist/cli/installer.js +71 -0
- package/dist/collector/collectClaudeMd.d.ts +3 -0
- package/dist/collector/collectClaudeMd.d.ts.map +1 -0
- package/dist/collector/collectClaudeMd.js +87 -0
- package/dist/collector/collectRuleSources.d.ts +3 -0
- package/dist/collector/collectRuleSources.d.ts.map +1 -0
- package/dist/collector/collectRuleSources.js +151 -0
- package/dist/config/AgentGateConfig.d.ts +18 -0
- package/dist/config/AgentGateConfig.d.ts.map +1 -0
- package/dist/config/AgentGateConfig.js +34 -0
- package/dist/config/Config.d.ts +26 -0
- package/dist/config/Config.d.ts.map +1 -0
- package/dist/config/Config.js +25 -0
- package/dist/config/PluginConfigLoader.d.ts +3 -0
- package/dist/config/PluginConfigLoader.d.ts.map +1 -0
- package/dist/config/PluginConfigLoader.js +85 -0
- package/dist/config/defineConfig.d.ts +23 -0
- package/dist/config/defineConfig.d.ts.map +1 -0
- package/dist/config/defineConfig.js +10 -0
- package/dist/contracts/schemas/hookDataSchema.d.ts +7 -0
- package/dist/contracts/schemas/hookDataSchema.d.ts.map +1 -0
- package/dist/contracts/schemas/hookDataSchema.js +9 -0
- package/dist/contracts/types/Action.d.ts +23 -0
- package/dist/contracts/types/Action.d.ts.map +1 -0
- package/dist/contracts/types/Action.js +2 -0
- package/dist/contracts/types/ClaudeMdFile.d.ts +5 -0
- package/dist/contracts/types/ClaudeMdFile.d.ts.map +1 -0
- package/dist/contracts/types/ClaudeMdFile.js +2 -0
- package/dist/contracts/types/HookData.d.ts +6 -0
- package/dist/contracts/types/HookData.d.ts.map +1 -0
- package/dist/contracts/types/HookData.js +2 -0
- package/dist/contracts/types/ModelClient.d.ts +4 -0
- package/dist/contracts/types/ModelClient.d.ts.map +1 -0
- package/dist/contracts/types/ModelClient.js +2 -0
- package/dist/contracts/types/RuleSource.d.ts +7 -0
- package/dist/contracts/types/RuleSource.d.ts.map +1 -0
- package/dist/contracts/types/RuleSource.js +2 -0
- package/dist/contracts/types/SessionContext.d.ts +25 -0
- package/dist/contracts/types/SessionContext.d.ts.map +1 -0
- package/dist/contracts/types/SessionContext.js +2 -0
- package/dist/contracts/types/ValidationResult.d.ts +5 -0
- package/dist/contracts/types/ValidationResult.d.ts.map +1 -0
- package/dist/contracts/types/ValidationResult.js +2 -0
- package/dist/daemon/client.d.ts +17 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +59 -0
- package/dist/daemon/protocol.d.ts +17 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +8 -0
- package/dist/daemon/server.d.ts +27 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +100 -0
- package/dist/deterministic/defaultRules.d.ts +11 -0
- package/dist/deterministic/defaultRules.d.ts.map +1 -0
- package/dist/deterministic/defaultRules.js +33 -0
- package/dist/deterministic/engine.d.ts +11 -0
- package/dist/deterministic/engine.d.ts.map +1 -0
- package/dist/deterministic/engine.js +12 -0
- package/dist/deterministic/factories.d.ts +20 -0
- package/dist/deterministic/factories.d.ts.map +1 -0
- package/dist/deterministic/factories.js +56 -0
- package/dist/deterministic/rules/preventBashSecretWrite.d.ts +3 -0
- package/dist/deterministic/rules/preventBashSecretWrite.d.ts.map +1 -0
- package/dist/deterministic/rules/preventBashSecretWrite.js +75 -0
- package/dist/deterministic/rules/preventForcePushMain.d.ts +7 -0
- package/dist/deterministic/rules/preventForcePushMain.d.ts.map +1 -0
- package/dist/deterministic/rules/preventForcePushMain.js +85 -0
- package/dist/deterministic/rules/preventRmRfRoot.d.ts +3 -0
- package/dist/deterministic/rules/preventRmRfRoot.d.ts.map +1 -0
- package/dist/deterministic/rules/preventRmRfRoot.js +68 -0
- package/dist/deterministic/rules/preventSecretFileWrite.d.ts +7 -0
- package/dist/deterministic/rules/preventSecretFileWrite.d.ts.map +1 -0
- package/dist/deterministic/rules/preventSecretFileWrite.js +55 -0
- package/dist/deterministic/rules/preventSystemPathWrite.d.ts +3 -0
- package/dist/deterministic/rules/preventSystemPathWrite.d.ts.map +1 -0
- package/dist/deterministic/rules/preventSystemPathWrite.js +38 -0
- package/dist/deterministic/types.d.ts +20 -0
- package/dist/deterministic/types.d.ts.map +1 -0
- package/dist/deterministic/types.js +2 -0
- package/dist/doctor/findings.d.ts +15 -0
- package/dist/doctor/findings.d.ts.map +1 -0
- package/dist/doctor/findings.js +2 -0
- package/dist/doctor/formatFindings.d.ts +3 -0
- package/dist/doctor/formatFindings.d.ts.map +1 -0
- package/dist/doctor/formatFindings.js +37 -0
- package/dist/doctor/lintRuleSources.d.ts +4 -0
- package/dist/doctor/lintRuleSources.d.ts.map +1 -0
- package/dist/doctor/lintRuleSources.js +87 -0
- package/dist/hooks/processHookData.d.ts +37 -0
- package/dist/hooks/processHookData.d.ts.map +1 -0
- package/dist/hooks/processHookData.js +181 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/observability/decisionLogger.d.ts +15 -0
- package/dist/observability/decisionLogger.d.ts.map +1 -0
- package/dist/observability/decisionLogger.js +20 -0
- package/dist/observability/eventBus.d.ts +15 -0
- package/dist/observability/eventBus.d.ts.map +1 -0
- package/dist/observability/eventBus.js +33 -0
- package/dist/observability/sinks/JsonlFileSink.d.ts +13 -0
- package/dist/observability/sinks/JsonlFileSink.d.ts.map +1 -0
- package/dist/observability/sinks/JsonlFileSink.js +36 -0
- package/dist/observability/sinks/Sink.d.ts +46 -0
- package/dist/observability/sinks/Sink.d.ts.map +1 -0
- package/dist/observability/sinks/Sink.js +2 -0
- package/dist/observability/stats.d.ts +14 -0
- package/dist/observability/stats.d.ts.map +1 -0
- package/dist/observability/stats.js +78 -0
- package/dist/validation/models/AnthropicApi.d.ts +9 -0
- package/dist/validation/models/AnthropicApi.d.ts.map +1 -0
- package/dist/validation/models/AnthropicApi.js +44 -0
- package/dist/validation/models/ClaudeCli.d.ts +10 -0
- package/dist/validation/models/ClaudeCli.d.ts.map +1 -0
- package/dist/validation/models/ClaudeCli.js +64 -0
- package/dist/validation/models/CompositeModelClient.d.ts +20 -0
- package/dist/validation/models/CompositeModelClient.d.ts.map +1 -0
- package/dist/validation/models/CompositeModelClient.js +53 -0
- package/dist/validation/prompts/context.d.ts +3 -0
- package/dist/validation/prompts/context.d.ts.map +1 -0
- package/dist/validation/prompts/context.js +29 -0
- package/dist/validation/prompts/response.d.ts +2 -0
- package/dist/validation/prompts/response.d.ts.map +1 -0
- package/dist/validation/prompts/response.js +20 -0
- package/dist/validation/prompts/system-prompt.d.ts +7 -0
- package/dist/validation/prompts/system-prompt.d.ts.map +1 -0
- package/dist/validation/prompts/system-prompt.js +69 -0
- package/dist/validation/validator.d.ts +5 -0
- package/dist/validation/validator.d.ts.map +1 -0
- package/dist/validation/validator.js +98 -0
- package/package.json +67 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.aggregateStats = aggregateStats;
|
|
4
|
+
exports.readStats = readStats;
|
|
5
|
+
exports.formatStats = formatStats;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
function emptyStats() {
|
|
8
|
+
return {
|
|
9
|
+
total: 0,
|
|
10
|
+
blocks: 0,
|
|
11
|
+
allows: 0,
|
|
12
|
+
byRuleId: {},
|
|
13
|
+
bySource: { deterministic: 0, ai: 0, pass: 0 },
|
|
14
|
+
byAdapter: {},
|
|
15
|
+
byTool: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function inc(m, k) {
|
|
19
|
+
m[k] = (m[k] ?? 0) + 1;
|
|
20
|
+
}
|
|
21
|
+
function aggregateStats(entries) {
|
|
22
|
+
const stats = emptyStats();
|
|
23
|
+
for (const e of entries) {
|
|
24
|
+
stats.total++;
|
|
25
|
+
if (e.decision === 'block')
|
|
26
|
+
stats.blocks++;
|
|
27
|
+
else
|
|
28
|
+
stats.allows++;
|
|
29
|
+
if (e.ruleId)
|
|
30
|
+
inc(stats.byRuleId, e.ruleId);
|
|
31
|
+
inc(stats.bySource, e.source);
|
|
32
|
+
inc(stats.byAdapter, e.adapter);
|
|
33
|
+
inc(stats.byTool, e.toolName);
|
|
34
|
+
}
|
|
35
|
+
return stats;
|
|
36
|
+
}
|
|
37
|
+
function readStats(logPath) {
|
|
38
|
+
if (!(0, fs_1.existsSync)(logPath))
|
|
39
|
+
return emptyStats();
|
|
40
|
+
const content = (0, fs_1.readFileSync)(logPath, 'utf-8');
|
|
41
|
+
const entries = [];
|
|
42
|
+
for (const line of content.split('\n')) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed)
|
|
45
|
+
continue;
|
|
46
|
+
try {
|
|
47
|
+
entries.push(JSON.parse(trimmed));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Skip malformed lines.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return aggregateStats(entries);
|
|
54
|
+
}
|
|
55
|
+
function formatRecord(label, rec) {
|
|
56
|
+
const entries = Object.entries(rec).sort((a, b) => b[1] - a[1]);
|
|
57
|
+
if (entries.length === 0)
|
|
58
|
+
return `${label}: (none)`;
|
|
59
|
+
const rendered = entries.map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
60
|
+
return `${label}:\n${rendered}`;
|
|
61
|
+
}
|
|
62
|
+
function formatStats(stats) {
|
|
63
|
+
const blockPct = stats.total > 0 ? Math.round((stats.blocks / stats.total) * 100) : 0;
|
|
64
|
+
const lines = [
|
|
65
|
+
`Total decisions: ${stats.total}`,
|
|
66
|
+
`Blocks: ${stats.blocks} (${blockPct}%)`,
|
|
67
|
+
`Allows: ${stats.allows}`,
|
|
68
|
+
'',
|
|
69
|
+
formatRecord('By source', stats.bySource),
|
|
70
|
+
'',
|
|
71
|
+
formatRecord('By adapter', stats.byAdapter),
|
|
72
|
+
'',
|
|
73
|
+
formatRecord('By tool', stats.byTool),
|
|
74
|
+
'',
|
|
75
|
+
formatRecord('By rule id (deterministic)', stats.byRuleId),
|
|
76
|
+
];
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Config } from '../../config/Config';
|
|
2
|
+
import { IModelClient } from '../../contracts/types/ModelClient';
|
|
3
|
+
export declare class AnthropicApi implements IModelClient {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly client;
|
|
6
|
+
constructor(config: Config);
|
|
7
|
+
ask(prompt: string): Promise<string>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=AnthropicApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnthropicApi.d.ts","sourceRoot":"","sources":["../../../src/validation/models/AnthropicApi.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAKhE,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;gBAEtB,MAAM,EAAE,MAAM;IAOpB,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAe3C"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AnthropicApi = void 0;
|
|
7
|
+
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
8
|
+
const system_prompt_1 = require("../prompts/system-prompt");
|
|
9
|
+
const API_MAX_TOKENS = 1024;
|
|
10
|
+
class AnthropicApi {
|
|
11
|
+
config;
|
|
12
|
+
client;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.client = new sdk_1.default({
|
|
16
|
+
apiKey: this.config.apiKey,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async ask(prompt) {
|
|
20
|
+
const response = await this.client.messages.create({
|
|
21
|
+
model: this.config.model,
|
|
22
|
+
system: (0, system_prompt_1.getSystemPrompt)(this.config.reasonLang),
|
|
23
|
+
max_tokens: API_MAX_TOKENS,
|
|
24
|
+
messages: [
|
|
25
|
+
{
|
|
26
|
+
role: 'user',
|
|
27
|
+
content: prompt,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
return extractTextFromResponse(response);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.AnthropicApi = AnthropicApi;
|
|
35
|
+
function extractTextFromResponse(response) {
|
|
36
|
+
if (response.content.length === 0) {
|
|
37
|
+
throw new Error('No content in response');
|
|
38
|
+
}
|
|
39
|
+
const firstContent = response.content[0];
|
|
40
|
+
if (!('text' in firstContent) || !firstContent.text) {
|
|
41
|
+
throw new Error('Response content does not contain text');
|
|
42
|
+
}
|
|
43
|
+
return firstContent.text;
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IModelClient } from '../../contracts/types/ModelClient';
|
|
2
|
+
import { Config } from '../../config/Config';
|
|
3
|
+
export declare class ClaudeCli implements IModelClient {
|
|
4
|
+
private readonly config;
|
|
5
|
+
private readonly cwd;
|
|
6
|
+
constructor(config: Config, cwd?: string);
|
|
7
|
+
ask(prompt: string): Promise<string>;
|
|
8
|
+
private getClaudeBinary;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=ClaudeCli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ClaudeCli.d.ts","sourceRoot":"","sources":["../../../src/validation/models/ClaudeCli.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAO5C,qBAAa,SAAU,YAAW,YAAY;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEhB,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM;IAKlC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwC1C,OAAO,CAAC,eAAe;CAYxB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClaudeCli = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const system_prompt_1 = require("../prompts/system-prompt");
|
|
9
|
+
const CLI_TIMEOUT_MS = 60_000;
|
|
10
|
+
const CLI_MAX_TURNS = '1';
|
|
11
|
+
const DISALLOWED_TOOLS = 'Edit,Write,Bash,Read,Glob,Grep';
|
|
12
|
+
class ClaudeCli {
|
|
13
|
+
config;
|
|
14
|
+
cwd;
|
|
15
|
+
constructor(config, cwd) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.cwd = cwd ?? process.cwd();
|
|
18
|
+
}
|
|
19
|
+
async ask(prompt) {
|
|
20
|
+
const claudeBinary = this.getClaudeBinary();
|
|
21
|
+
const fullPrompt = `${(0, system_prompt_1.getSystemPrompt)(this.config.reasonLang)}\n\n${prompt}`;
|
|
22
|
+
const args = [
|
|
23
|
+
'-',
|
|
24
|
+
'--output-format',
|
|
25
|
+
'json',
|
|
26
|
+
'--max-turns',
|
|
27
|
+
CLI_MAX_TURNS,
|
|
28
|
+
'--model',
|
|
29
|
+
this.config.model,
|
|
30
|
+
'--disallowed-tools',
|
|
31
|
+
DISALLOWED_TOOLS,
|
|
32
|
+
'--strict-mcp-config',
|
|
33
|
+
];
|
|
34
|
+
const claudeDir = (0, path_1.join)(this.cwd, '.claude');
|
|
35
|
+
if (!(0, fs_1.existsSync)(claudeDir)) {
|
|
36
|
+
(0, fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
const output = (0, child_process_1.execFileSync)(claudeBinary, args, {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
timeout: CLI_TIMEOUT_MS,
|
|
41
|
+
input: fullPrompt,
|
|
42
|
+
cwd: claudeDir,
|
|
43
|
+
// shell is needed on Windows to invoke the `.cmd` shim that npm creates
|
|
44
|
+
// for installed bins. On Unix the binary is invoked directly via execve,
|
|
45
|
+
// so shell interpretation (and any associated injection surface) is off.
|
|
46
|
+
// All args passed here are constants or validated config values, so this
|
|
47
|
+
// flag does not widen the injection surface in practice.
|
|
48
|
+
shell: process.platform === 'win32',
|
|
49
|
+
});
|
|
50
|
+
const response = JSON.parse(output);
|
|
51
|
+
return response.result;
|
|
52
|
+
}
|
|
53
|
+
getClaudeBinary() {
|
|
54
|
+
if (this.config.useSystemClaude) {
|
|
55
|
+
return 'claude';
|
|
56
|
+
}
|
|
57
|
+
const localBinary = (0, path_1.join)((0, os_1.homedir)(), '.claude', 'local', 'claude');
|
|
58
|
+
if ((0, fs_1.existsSync)(localBinary)) {
|
|
59
|
+
return localBinary;
|
|
60
|
+
}
|
|
61
|
+
return 'claude';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.ClaudeCli = ClaudeCli;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IModelClient } from '../../contracts/types/ModelClient';
|
|
2
|
+
export interface CompositeModelClientOptions {
|
|
3
|
+
/** Per-client timeout in ms. A client that exceeds this is treated as failed. */
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Tries each client in order, returning the first successful response.
|
|
8
|
+
* If a client throws or exceeds the optional timeout, the composite
|
|
9
|
+
* falls back to the next client. When every client fails, the composite
|
|
10
|
+
* itself throws an aggregate error so the AI validator's outer
|
|
11
|
+
* try/catch can fail-open the request.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CompositeModelClient implements IModelClient {
|
|
14
|
+
private readonly clients;
|
|
15
|
+
private readonly timeoutMs?;
|
|
16
|
+
constructor(clients: IModelClient[], opts?: CompositeModelClientOptions);
|
|
17
|
+
ask(prompt: string): Promise<string>;
|
|
18
|
+
private askWithOptionalTimeout;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=CompositeModelClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CompositeModelClient.d.ts","sourceRoot":"","sources":["../../../src/validation/models/CompositeModelClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAA;AAEhE,MAAM,WAAW,2BAA2B;IAC1C,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,YAAY;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAQ;gBAEvB,OAAO,EAAE,YAAY,EAAE,EAAE,IAAI,CAAC,EAAE,2BAA2B;IAQjE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAgB5B,sBAAsB;CAuBrC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompositeModelClient = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Tries each client in order, returning the first successful response.
|
|
6
|
+
* If a client throws or exceeds the optional timeout, the composite
|
|
7
|
+
* falls back to the next client. When every client fails, the composite
|
|
8
|
+
* itself throws an aggregate error so the AI validator's outer
|
|
9
|
+
* try/catch can fail-open the request.
|
|
10
|
+
*/
|
|
11
|
+
class CompositeModelClient {
|
|
12
|
+
clients;
|
|
13
|
+
timeoutMs;
|
|
14
|
+
constructor(clients, opts) {
|
|
15
|
+
if (clients.length === 0) {
|
|
16
|
+
throw new Error('CompositeModelClient requires at least one IModelClient');
|
|
17
|
+
}
|
|
18
|
+
this.clients = clients;
|
|
19
|
+
this.timeoutMs = opts?.timeoutMs;
|
|
20
|
+
}
|
|
21
|
+
async ask(prompt) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
for (let i = 0; i < this.clients.length; i++) {
|
|
24
|
+
const c = this.clients[i];
|
|
25
|
+
try {
|
|
26
|
+
return await this.askWithOptionalTimeout(c, prompt);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
30
|
+
errors.push(`client[${i}]: ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`CompositeModelClient: all ${this.clients.length} clients failed. ${errors.join(' | ')}`);
|
|
34
|
+
}
|
|
35
|
+
async askWithOptionalTimeout(client, prompt) {
|
|
36
|
+
if (this.timeoutMs === undefined) {
|
|
37
|
+
return client.ask(prompt);
|
|
38
|
+
}
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
reject(new Error(`client timeout after ${this.timeoutMs}ms`));
|
|
42
|
+
}, this.timeoutMs);
|
|
43
|
+
client.ask(prompt).then((result) => {
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
resolve(result);
|
|
46
|
+
}, (err) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
exports.CompositeModelClient = CompositeModelClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/validation/prompts/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAkB,MAAM,kCAAkC,CAAA;AAc7E,wBAAgB,WAAW,CACzB,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,MAAM,CAoBR"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildPrompt = buildPrompt;
|
|
4
|
+
const response_1 = require("./response");
|
|
5
|
+
const KIND_LABEL = {
|
|
6
|
+
'claude-md': 'CLAUDE.md',
|
|
7
|
+
'agents-md': 'AGENTS.md',
|
|
8
|
+
cursorrules: '.cursorrules (Cursor legacy)',
|
|
9
|
+
'cursor-mdc': '.cursor/rules/*.mdc (Cursor)',
|
|
10
|
+
clinerules: '.clinerules/*.md (Cline)',
|
|
11
|
+
'windsurf-rule': '.windsurf/rules/*.md (Windsurf)',
|
|
12
|
+
'copilot-instructions': '.github/copilot-instructions.md (Copilot)',
|
|
13
|
+
'aider-conventions': 'CONVENTIONS.md (Aider)',
|
|
14
|
+
};
|
|
15
|
+
function buildPrompt(rules, toolName, toolInput) {
|
|
16
|
+
const rulesSection = rules
|
|
17
|
+
.map((r) => `## ${KIND_LABEL[r.kind]} (${r.path})\n\n${r.content}`)
|
|
18
|
+
.join('\n\n');
|
|
19
|
+
const toolSection = JSON.stringify({ tool_name: toolName, tool_input: toolInput }, null, 2);
|
|
20
|
+
return `# プロジェクトルール (集約された指示ファイル)
|
|
21
|
+
|
|
22
|
+
${rulesSection}
|
|
23
|
+
|
|
24
|
+
# 実行されるツール操作
|
|
25
|
+
|
|
26
|
+
${toolSection}
|
|
27
|
+
|
|
28
|
+
${response_1.RESPONSE_FORMAT}`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const RESPONSE_FORMAT = "\nRespond in the following JSON format only. Do not include any other text.\n\nIf there is a violation:\n```json\n{\"decision\": \"block\", \"reason\": \"Rule \\\"<quote>\\\" forbids this. Next correct step: <what to do instead>.\"}\n```\n\nExample block reason:\n\"CLAUDE.md states \\\"Do not delete production data without confirmation\\\".\nNext correct step: stop the destructive command and ask the user before\nrunning anything that targets the production database.\"\n\nIf there is no violation:\n```json\n{\"decision\": null, \"reason\": \"No violation found\"}\n```";
|
|
2
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../../src/validation/prompts/response.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,mkBAgBrB,CAAA"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESPONSE_FORMAT = void 0;
|
|
4
|
+
exports.RESPONSE_FORMAT = `
|
|
5
|
+
Respond in the following JSON format only. Do not include any other text.
|
|
6
|
+
|
|
7
|
+
If there is a violation:
|
|
8
|
+
\`\`\`json
|
|
9
|
+
{"decision": "block", "reason": "Rule \\"<quote>\\" forbids this. Next correct step: <what to do instead>."}
|
|
10
|
+
\`\`\`
|
|
11
|
+
|
|
12
|
+
Example block reason:
|
|
13
|
+
"CLAUDE.md states \\"Do not delete production data without confirmation\\".
|
|
14
|
+
Next correct step: stop the destructive command and ask the user before
|
|
15
|
+
running anything that targets the production database."
|
|
16
|
+
|
|
17
|
+
If there is no violation:
|
|
18
|
+
\`\`\`json
|
|
19
|
+
{"decision": null, "reason": "No violation found"}
|
|
20
|
+
\`\`\``;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getSystemPrompt(reasonLang?: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Backwards-compatible constant export used by older callers and tests.
|
|
4
|
+
* Equivalent to getSystemPrompt() with auto language behavior.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SYSTEM_PROMPT: string;
|
|
7
|
+
//# sourceMappingURL=system-prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../../src/validation/prompts/system-prompt.ts"],"names":[],"mappings":"AA2DA,wBAAgB,eAAe,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAI3D;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAoB,CAAA"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SYSTEM_PROMPT = void 0;
|
|
4
|
+
exports.getSystemPrompt = getSystemPrompt;
|
|
5
|
+
const BASE_PROMPT = `You are a guardrail for an AI coding agent.
|
|
6
|
+
Your job is to evaluate whether the agent's upcoming tool operation
|
|
7
|
+
violates any project rules.
|
|
8
|
+
|
|
9
|
+
The rules come from one or more instruction files in the project:
|
|
10
|
+
CLAUDE.md, AGENTS.md, .cursorrules, .cursor/rules/*.mdc,
|
|
11
|
+
.clinerules/*.md, .windsurf/rules/*.md, .github/copilot-instructions.md,
|
|
12
|
+
or CONVENTIONS.md. Treat them all as a single combined rule set,
|
|
13
|
+
attributing context by their headings when relevant.
|
|
14
|
+
|
|
15
|
+
Criteria for blocking:
|
|
16
|
+
- Only block clear, specific rule violations.
|
|
17
|
+
- When ambiguous, allow the operation (avoid over-blocking).
|
|
18
|
+
- Anything not mentioned in the rules is NOT a violation.
|
|
19
|
+
|
|
20
|
+
When you do block, the "reason" field MUST do two things:
|
|
21
|
+
1. Name the violated rule (or quote the relevant line from the
|
|
22
|
+
instruction file).
|
|
23
|
+
2. Describe the next correct step the agent should take instead.
|
|
24
|
+
Treat the reason as guidance, not just a denial. The agent reads
|
|
25
|
+
the reason and uses it to decide what to do next, so the more
|
|
26
|
+
actionable the guidance, the better.`;
|
|
27
|
+
const HUMANIZED_LANGS = {
|
|
28
|
+
en: 'English',
|
|
29
|
+
ja: 'Japanese',
|
|
30
|
+
zh: 'Chinese',
|
|
31
|
+
ko: 'Korean',
|
|
32
|
+
fr: 'French',
|
|
33
|
+
de: 'German',
|
|
34
|
+
es: 'Spanish',
|
|
35
|
+
pt: 'Portuguese',
|
|
36
|
+
it: 'Italian',
|
|
37
|
+
ru: 'Russian',
|
|
38
|
+
tr: 'Turkish',
|
|
39
|
+
ar: 'Arabic',
|
|
40
|
+
};
|
|
41
|
+
function humanizeLang(code) {
|
|
42
|
+
const lower = code.toLowerCase();
|
|
43
|
+
return HUMANIZED_LANGS[lower] ?? code;
|
|
44
|
+
}
|
|
45
|
+
function buildLanguageDirective(reasonLang) {
|
|
46
|
+
if (reasonLang && reasonLang.toLowerCase() !== 'auto') {
|
|
47
|
+
return `Output language:
|
|
48
|
+
- Always write the "reason" field in ${humanizeLang(reasonLang)},
|
|
49
|
+
regardless of the language used in the instruction files. If you
|
|
50
|
+
quote a rule that was written in a different language, keep the
|
|
51
|
+
quote in its original language but write the surrounding guidance
|
|
52
|
+
in ${humanizeLang(reasonLang)}.`;
|
|
53
|
+
}
|
|
54
|
+
return `Output language:
|
|
55
|
+
- Match the dominant language of the instruction files when writing
|
|
56
|
+
the "reason" field. If the files are mixed or ambiguous, default to
|
|
57
|
+
English. This default keeps reasons grep-friendly for users who
|
|
58
|
+
share logs across teams.`;
|
|
59
|
+
}
|
|
60
|
+
function getSystemPrompt(reasonLang) {
|
|
61
|
+
return `${BASE_PROMPT}
|
|
62
|
+
|
|
63
|
+
${buildLanguageDirective(reasonLang)}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Backwards-compatible constant export used by older callers and tests.
|
|
67
|
+
* Equivalent to getSystemPrompt() with auto language behavior.
|
|
68
|
+
*/
|
|
69
|
+
exports.SYSTEM_PROMPT = getSystemPrompt();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ValidationResult } from '../contracts/types/ValidationResult';
|
|
2
|
+
import { RuleSource } from '../contracts/types/RuleSource';
|
|
3
|
+
import { IModelClient } from '../contracts/types/ModelClient';
|
|
4
|
+
export declare function validator(rules: RuleSource[], toolName: string, toolInput: Record<string, unknown>, modelClient: IModelClient): Promise<ValidationResult>;
|
|
5
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,qCAAqC,CAAA;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAA;AAQ7D,wBAAsB,SAAS,CAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,WAAW,EAAE,YAAY,GACxB,OAAO,CAAC,gBAAgB,CAAC,CAa3B"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validator = validator;
|
|
4
|
+
const context_1 = require("./prompts/context");
|
|
5
|
+
async function validator(rules, toolName, toolInput, modelClient) {
|
|
6
|
+
try {
|
|
7
|
+
const prompt = (0, context_1.buildPrompt)(rules, toolName, toolInput);
|
|
8
|
+
const response = await modelClient.ask(prompt);
|
|
9
|
+
return parseModelResponse(response);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
13
|
+
return {
|
|
14
|
+
decision: undefined,
|
|
15
|
+
reason: `Validation error (allowing operation): ${errorMessage}`,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function parseModelResponse(response) {
|
|
20
|
+
const jsonString = extractJsonString(response);
|
|
21
|
+
const parsed = JSON.parse(jsonString);
|
|
22
|
+
return {
|
|
23
|
+
decision: parsed.decision === 'block' ? 'block' : undefined,
|
|
24
|
+
reason: parsed.reason ?? '',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function extractJsonString(response) {
|
|
28
|
+
if (!response) {
|
|
29
|
+
throw new Error('No response from model');
|
|
30
|
+
}
|
|
31
|
+
const jsonFromCodeBlock = extractFromJsonCodeBlock(response);
|
|
32
|
+
if (jsonFromCodeBlock)
|
|
33
|
+
return jsonFromCodeBlock;
|
|
34
|
+
const jsonFromGenericBlock = extractFromGenericCodeBlock(response);
|
|
35
|
+
if (jsonFromGenericBlock)
|
|
36
|
+
return jsonFromGenericBlock;
|
|
37
|
+
const plainJson = extractPlainJson(response);
|
|
38
|
+
if (plainJson)
|
|
39
|
+
return plainJson;
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
function extractFromJsonCodeBlock(response) {
|
|
43
|
+
const startPattern = '```json';
|
|
44
|
+
const endPattern = '```';
|
|
45
|
+
let startIndex = 0;
|
|
46
|
+
let lastBlock = null;
|
|
47
|
+
let blockStart = response.indexOf(startPattern, startIndex);
|
|
48
|
+
while (blockStart !== -1) {
|
|
49
|
+
const contentStart = blockStart + startPattern.length;
|
|
50
|
+
const blockEnd = response.indexOf(endPattern, contentStart);
|
|
51
|
+
if (blockEnd === -1)
|
|
52
|
+
break;
|
|
53
|
+
lastBlock = response.substring(contentStart, blockEnd).trim();
|
|
54
|
+
startIndex = blockEnd + endPattern.length;
|
|
55
|
+
blockStart = response.indexOf(startPattern, startIndex);
|
|
56
|
+
}
|
|
57
|
+
return lastBlock;
|
|
58
|
+
}
|
|
59
|
+
function extractFromGenericCodeBlock(response) {
|
|
60
|
+
const startPattern = '```';
|
|
61
|
+
let lastValidBlock = null;
|
|
62
|
+
let searchFrom = 0;
|
|
63
|
+
while (true) {
|
|
64
|
+
const blockStart = response.indexOf(startPattern, searchFrom);
|
|
65
|
+
if (blockStart === -1)
|
|
66
|
+
break;
|
|
67
|
+
let contentStart = blockStart + startPattern.length;
|
|
68
|
+
while (contentStart < response.length && /\s/.test(response[contentStart])) {
|
|
69
|
+
contentStart++;
|
|
70
|
+
}
|
|
71
|
+
const blockEnd = response.indexOf(startPattern, contentStart);
|
|
72
|
+
if (blockEnd === -1)
|
|
73
|
+
break;
|
|
74
|
+
const content = response.substring(contentStart, blockEnd).trim();
|
|
75
|
+
if (isValidJson(content)) {
|
|
76
|
+
lastValidBlock = content;
|
|
77
|
+
}
|
|
78
|
+
searchFrom = blockEnd + startPattern.length;
|
|
79
|
+
}
|
|
80
|
+
return lastValidBlock;
|
|
81
|
+
}
|
|
82
|
+
function extractPlainJson(response) {
|
|
83
|
+
const pattern = /\{[^{}]*"decision"[^{}]*"reason"[^{}]*}|\{[^{}]*"reason"[^{}]*"decision"[^{}]*}/g;
|
|
84
|
+
const matches = response.match(pattern);
|
|
85
|
+
if (!matches)
|
|
86
|
+
return null;
|
|
87
|
+
const lastMatch = matches[matches.length - 1];
|
|
88
|
+
return isValidJson(lastMatch) ? lastMatch : null;
|
|
89
|
+
}
|
|
90
|
+
function isValidJson(str) {
|
|
91
|
+
try {
|
|
92
|
+
JSON.parse(str);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hiro-c/agent-gate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Runtime rule enforcer for AI coding agents. Reads CLAUDE.md / AGENTS.md / .cursorrules and enforces them via Claude Code and Cursor hooks, with a deterministic safety baseline.",
|
|
5
|
+
"author": "Hiro-Chiba",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/Hiro-Chiba/agent-gate#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Hiro-Chiba/agent-gate.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Hiro-Chiba/agent-gate/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"claude-md",
|
|
19
|
+
"agents-md",
|
|
20
|
+
"cursor",
|
|
21
|
+
"cursor-hooks",
|
|
22
|
+
"cursor-rules",
|
|
23
|
+
"ai-agent",
|
|
24
|
+
"ai-tools",
|
|
25
|
+
"hooks",
|
|
26
|
+
"guardrail",
|
|
27
|
+
"linter",
|
|
28
|
+
"code-quality"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.0.0"
|
|
32
|
+
},
|
|
33
|
+
"main": "dist/index.js",
|
|
34
|
+
"types": "dist/index.d.ts",
|
|
35
|
+
"bin": {
|
|
36
|
+
"agent-gate": "dist/cli/agent-gate.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"!dist/*.tsbuildinfo",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"AGENTS.md"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"provenance": true
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc --build tsconfig.build.json",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"checks": "npm run typecheck && npm run test",
|
|
55
|
+
"prepublishOnly": "npm run checks && npm run build"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@anthropic-ai/sdk": "^0.90.0",
|
|
59
|
+
"jiti": "^2.7.0",
|
|
60
|
+
"zod": "^4.3.6"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^25.6.0",
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"vitest": "^4.1.4"
|
|
66
|
+
}
|
|
67
|
+
}
|