@idevelopers/agentgate 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.
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "agentgate-marketplace",
4
+ "description": "Local marketplace for AgentGate, a validation gate and budget guardrail plugin for autonomous coding agents.",
5
+ "owner": {
6
+ "name": "iDevelopers"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "agentgate",
11
+ "description": "Validation gates + token-budget kill switch for autonomous coding agents",
12
+ "author": {
13
+ "name": "iDevelopers"
14
+ },
15
+ "category": "development",
16
+ "source": "./",
17
+ "version": "0.1.0",
18
+ "homepage": "https://www.npmjs.com/package/@idevelopers/agentgate"
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "agentgate",
3
+ "version": "0.1.0",
4
+ "description": "Validation gates + token-budget kill switch for autonomous coding agents",
5
+ "author": {
6
+ "name": "iDevelopers",
7
+ "url": "https://github.com/manish-1988"
8
+ },
9
+ "repository": "https://www.npmjs.com/package/@idevelopers/agentgate",
10
+ "license": "MIT"
11
+ }
package/.mcp.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "agentgate": {
4
+ "command": "npx",
5
+ "args": [
6
+ "-y",
7
+ "@idevelopers/agentgate"
8
+ ]
9
+ }
10
+ }
11
+ }
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # AgentGate
2
+
3
+ Stop autonomous coding agents from burning tokens and marking broken code as done.
4
+
5
+ AgentGate is an MCP server plus Claude Code and Codex plugin packaging. It wraps agent work in explicit slices, runs real validation gates, blocks done status while gates fail, and adds Pro guardrails for multi-slice plans, token budgets, advanced gates, and analytics.
6
+
7
+ ## Install
8
+
9
+ Install the MCP server directly from npm in Claude Code:
10
+
11
+ ```text
12
+ claude mcp add agentgate -- npx -y @idevelopers/agentgate
13
+ ```
14
+
15
+ Or install the Claude Code plugin from the public installer marketplace:
16
+
17
+ ```text
18
+ /plugin marketplace add https://github.com/manish-1988/agentgate-installer
19
+ /plugin install agentgate@agentgate-marketplace
20
+ ```
21
+
22
+ For local source development from this private repository:
23
+
24
+ ```sh
25
+ npm install
26
+ npm run build
27
+ node dist/index.js
28
+ ```
29
+
30
+ Polar handles paid licenses; npm distributes the runtime package. The public installer repository contains only plugin metadata and points Claude/Codex at `npx -y @idevelopers/agentgate`.
31
+
32
+ ## Free, Pro, Team
33
+
34
+ | Capability | Free | Pro | Team |
35
+ | --- | --- | --- | --- |
36
+ | Single-slice run + basic gates | Yes | Yes | Yes |
37
+ | Multi-slice orchestration | No | Yes | Yes |
38
+ | Token-budget kill switch | No | Yes | Yes |
39
+ | Advanced gates: typecheck, build, custom | No | Yes | Yes |
40
+ | Run analytics export | No | Yes | Yes |
41
+ | Shared policies and seats | No | No | Yes |
42
+
43
+ Pro is intended for `$19/mo` or `$190/yr`. Team is intended for `$49/mo` with seats.
44
+ Buy Pro or Team through the live Polar checkout:
45
+
46
+ ```text
47
+ https://buy.polar.sh/polar_cl_VxlhG8lCO6IUcLcveJ51YOXYXMHWdUNorKr0U1bhyqm
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ Create `agentgate.config.json` in your repo. Start from `agentgate.config.example.json`:
53
+
54
+ ```json
55
+ {
56
+ "version": 1,
57
+ "gates": [
58
+ {
59
+ "id": "test",
60
+ "type": "test",
61
+ "command": "npm",
62
+ "args": ["test", "--", "--run"]
63
+ }
64
+ ],
65
+ "slices": [
66
+ {
67
+ "id": "slice-1",
68
+ "description": "Implement one focused change",
69
+ "fileScope": ["src/**", "test/**"],
70
+ "acceptance": ["Tests pass"],
71
+ "gates": ["test"]
72
+ }
73
+ ]
74
+ }
75
+ ```
76
+
77
+ For smoke tests or nonstandard filenames, point AgentGate at a specific config:
78
+
79
+ ```sh
80
+ export AGENTGATE_CONFIG=agentgate.config.example.json
81
+ ```
82
+
83
+ ## Tools
84
+
85
+ - `agentgate_define_slice`: register or update a slice in the active run.
86
+ - `agentgate_run_gate`: run gates for one slice.
87
+ - `agentgate_run_plan`: Pro multi-slice DAG execution with bounded retries.
88
+ - `agentgate_budget_status`: report token usage, remaining budget, estimated cost, and state.
89
+ - `agentgate_activate_license`: store and verify a Polar license key.
90
+
91
+ ## License Activation
92
+
93
+ AgentGate ships with the public Polar organization id for production license validation:
94
+
95
+ ```sh
96
+ POLAR_ORGANIZATION_ID=534c0711-1ded-497c-aa46-d1a7dfa46d6e
97
+ ```
98
+
99
+ You only need to override it when testing against a different Polar organization. For sandbox checks, set both values:
100
+
101
+ ```sh
102
+ export POLAR_ENVIRONMENT=sandbox
103
+ export POLAR_ORGANIZATION_ID="your_sandbox_org_id"
104
+ ```
105
+
106
+ Then call `agentgate_activate_license` with the Polar license key. AgentGate caches successful validation in `~/.agentgate/license.json` and allows a 72-hour offline grace window for Pro and Team licenses.
107
+ Because AgentGate license keys use Polar activation limits, activation also caches a local activation id. By default the activation label is `agentgate-local`; override it with `AGENTGATE_ACTIVATION_LABEL` if you want a more specific device label in Polar.
108
+ When a user hits a paid feature without a Pro or Team license, AgentGate returns the live Polar checkout URL and tells them to activate the issued key with `agentgate_activate_license`.
109
+
110
+ ## Launch Status
111
+
112
+ Soft-launch validation was completed on 2026-06-11:
113
+
114
+ - Polar sandbox checkout issued a real sandbox license key.
115
+ - `agentgate_activate_license` activated that key and cached a Polar activation id.
116
+ - `agentgate_run_plan` passed the example Pro plan with `config` and `typecheck` slices.
117
+ - Free users can run non-Pro gates, while Pro plan execution is blocked without a valid license.
118
+ - Dogfood runs passed against AgentGate, Shipnote, PocketScan, and the mushroom tracker syntax check.
119
+ - The live Polar finance page showed account approval complete, payout account setup complete, and identity verification complete.
120
+ - A production free smoke checkout issued a live Pro license key and AgentGate activated it successfully.
121
+
122
+ Current distribution model: private source repository, public npm package, public installer marketplace, and Polar license monetization.
123
+
124
+ ## Security
125
+
126
+ AgentGate runs locally and does not send repo contents to an AgentGate server. It does spawn the commands listed in `agentgate.config.json`, using `child_process.spawn` with `shell:false`. Treat the config like a CI file: review it before running, and only run trusted gate commands.
127
+
128
+ ## Development
129
+
130
+ ```sh
131
+ npm install
132
+ npm run build
133
+ npx vitest run
134
+ ```
135
+
136
+ The package binary is `agentgate`, mapped to `dist/index.js`.
@@ -0,0 +1,63 @@
1
+ {
2
+ "version": 1,
3
+ "gates": [
4
+ {
5
+ "id": "test",
6
+ "type": "test",
7
+ "command": "npm",
8
+ "args": [
9
+ "test",
10
+ "--",
11
+ "--run"
12
+ ]
13
+ },
14
+ {
15
+ "id": "typecheck",
16
+ "type": "typecheck",
17
+ "command": "tsc",
18
+ "args": [
19
+ "--noEmit"
20
+ ],
21
+ "proOnly": true
22
+ }
23
+ ],
24
+ "slices": [
25
+ {
26
+ "id": "config",
27
+ "description": "Validate the AgentGate config loader and schema.",
28
+ "fileScope": [
29
+ "src/config/**",
30
+ "test/config.test.ts"
31
+ ],
32
+ "acceptance": [
33
+ "Valid configs parse.",
34
+ "Invalid configs report readable field paths."
35
+ ],
36
+ "gates": [
37
+ "test"
38
+ ]
39
+ },
40
+ {
41
+ "id": "typecheck",
42
+ "description": "Run strict TypeScript checks.",
43
+ "fileScope": [
44
+ "src/**",
45
+ "test/**"
46
+ ],
47
+ "acceptance": [
48
+ "TypeScript emits without errors."
49
+ ],
50
+ "gates": [
51
+ "typecheck"
52
+ ],
53
+ "dependsOn": [
54
+ "config"
55
+ ]
56
+ }
57
+ ],
58
+ "budget": {
59
+ "maxTokens": 200000,
60
+ "warnAtPct": 0.8,
61
+ "model": "opus"
62
+ }
63
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "agentgate",
3
+ "version": "0.1.0",
4
+ "description": "Validation gates + token-budget kill switch for autonomous coding agents",
5
+ "author": "iDevelopers",
6
+ "license": "MIT",
7
+ "mcpServers": {
8
+ "agentgate": {
9
+ "command": "npx",
10
+ "args": [
11
+ "-y",
12
+ "@idevelopers/agentgate"
13
+ ]
14
+ }
15
+ },
16
+ "skills": [
17
+ "skills/agentgate-workflow/SKILL.md"
18
+ ],
19
+ "commands": [
20
+ "commands/gate.md",
21
+ "commands/plan.md"
22
+ ]
23
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Run AgentGate validation gates for a slice
3
+ argument-hint: "[sliceId]"
4
+ ---
5
+
6
+ Run `agentgate_run_gate` for the provided slice id. Inspect every returned gate result. If any gate failed, errored, or timed out, fix the issue within the configured file scope and rerun this command.
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Run the AgentGate Pro multi-slice plan
3
+ argument-hint: ""
4
+ ---
5
+
6
+ Run `agentgate_run_plan`. This is a Pro command. Stop on the first failed slice, inspect the returned gate results, and rerun after fixing the cause.
@@ -0,0 +1,39 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { requirePro } from "../license/gate.js";
4
+ import { runsDir } from "../util/paths.js";
5
+ export async function toJSON(runId, tier) {
6
+ requirePro("analytics_export", tier);
7
+ const raw = await readFile(path.join(runsDir(), `${runId}.jsonl`), "utf8");
8
+ return raw
9
+ .split("\n")
10
+ .filter(Boolean)
11
+ .map((line) => JSON.parse(line));
12
+ }
13
+ export async function toCSV(runId, tier) {
14
+ const events = await toJSON(runId, tier);
15
+ const rows = events.map(flattenRecord);
16
+ const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))];
17
+ const lines = [
18
+ headers.join(","),
19
+ ...rows.map((row) => headers.map((header) => csvEscape(row[header] ?? "")).join(",")),
20
+ ];
21
+ return lines.join("\n");
22
+ }
23
+ function flattenRecord(value) {
24
+ const record = typeof value === "object" && value !== null
25
+ ? value
26
+ : { value };
27
+ return Object.fromEntries(Object.entries(record).map(([key, nestedValue]) => [
28
+ key,
29
+ typeof nestedValue === "string"
30
+ ? nestedValue
31
+ : JSON.stringify(nestedValue),
32
+ ]));
33
+ }
34
+ function csvEscape(value) {
35
+ if (!/[",\n]/.test(value)) {
36
+ return value;
37
+ }
38
+ return `"${value.replaceAll("\"", "\"\"")}"`;
39
+ }
@@ -0,0 +1,12 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { runsDir } from "../util/paths.js";
4
+ export async function appendEvent(runId, event) {
5
+ const dir = runsDir();
6
+ await mkdir(dir, { recursive: true });
7
+ const eventWithTime = {
8
+ at: new Date().toISOString(),
9
+ ...event,
10
+ };
11
+ await appendFile(path.join(dir, `${runId}.jsonl`), `${JSON.stringify(eventWithTime)}\n`, "utf8");
12
+ }
@@ -0,0 +1,11 @@
1
+ export class BudgetExceeded extends Error {
2
+ constructor(message = "AgentGate token budget exceeded.") {
3
+ super(message);
4
+ this.name = "BudgetExceeded";
5
+ }
6
+ }
7
+ export function enforceBudget(ledger, budget) {
8
+ if (ledger.used() >= budget.maxTokens) {
9
+ throw new BudgetExceeded(`Token budget exceeded: used ${ledger.used()} of ${budget.maxTokens}.`);
10
+ }
11
+ }
@@ -0,0 +1,24 @@
1
+ export class TokenLedger {
2
+ tokensUsed = 0;
3
+ add(tokens) {
4
+ if (!Number.isFinite(tokens) || tokens < 0) {
5
+ throw new Error("Token count must be a non-negative finite number.");
6
+ }
7
+ this.tokensUsed += Math.floor(tokens);
8
+ }
9
+ used() {
10
+ return this.tokensUsed;
11
+ }
12
+ remaining(maxTokens) {
13
+ return Math.max(0, maxTokens - this.tokensUsed);
14
+ }
15
+ check(budget) {
16
+ if (this.tokensUsed >= budget.maxTokens) {
17
+ return "stopped";
18
+ }
19
+ if (this.tokensUsed >= budget.warnAtPct * budget.maxTokens) {
20
+ return "warn";
21
+ }
22
+ return "ok";
23
+ }
24
+ }
@@ -0,0 +1,38 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ZodError } from "zod";
4
+ import { configSchema } from "./schema.js";
5
+ export async function loadConfig(cwd = process.cwd()) {
6
+ const configPath = process.env.AGENTGATE_CONFIG
7
+ ? path.resolve(cwd, process.env.AGENTGATE_CONFIG)
8
+ : path.join(cwd, "agentgate.config.json");
9
+ const raw = await readFile(configPath, "utf8");
10
+ try {
11
+ return configSchema.parse(JSON.parse(raw));
12
+ }
13
+ catch (error) {
14
+ if (error instanceof SyntaxError) {
15
+ throw new Error(`Invalid JSON in ${configPath}: ${error.message}`);
16
+ }
17
+ if (error instanceof ZodError) {
18
+ const details = error.issues
19
+ .map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
20
+ .join("; ");
21
+ throw new Error(`Invalid AgentGate config at ${configPath}: ${details}`);
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ export async function loadPolicy(policyPath) {
27
+ const resolvedPath = path.resolve(policyPath);
28
+ const raw = await readFile(resolvedPath, "utf8");
29
+ try {
30
+ return JSON.parse(raw);
31
+ }
32
+ catch (error) {
33
+ if (error instanceof SyntaxError) {
34
+ throw new Error(`Invalid JSON in policy file ${resolvedPath}: ${error.message}`);
35
+ }
36
+ throw error;
37
+ }
38
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ export const gateTypeSchema = z.enum([
3
+ "test",
4
+ "lint",
5
+ "typecheck",
6
+ "build",
7
+ "custom",
8
+ ]);
9
+ export const gateSchema = z.object({
10
+ id: z.string().min(1),
11
+ type: gateTypeSchema,
12
+ command: z.string().min(1),
13
+ args: z.array(z.string()),
14
+ cwd: z.string().optional(),
15
+ timeoutMs: z.number().int().positive().default(120000),
16
+ proOnly: z.boolean().optional(),
17
+ });
18
+ export const sliceSchema = z.object({
19
+ id: z.string().min(1),
20
+ description: z.string().min(1),
21
+ fileScope: z.array(z.string()),
22
+ acceptance: z.array(z.string()),
23
+ gates: z.array(z.string()),
24
+ dependsOn: z.array(z.string()).optional(),
25
+ maxRetries: z.number().int().nonnegative().default(2),
26
+ });
27
+ export const budgetSchema = z.object({
28
+ maxTokens: z.number().int().positive(),
29
+ warnAtPct: z.number().min(0).max(1),
30
+ model: z.string().min(1),
31
+ });
32
+ export const configSchema = z.object({
33
+ version: z.literal(1),
34
+ gates: z.array(gateSchema),
35
+ slices: z.array(sliceSchema),
36
+ budget: budgetSchema.optional(),
37
+ policyFile: z.string().optional(),
38
+ });
@@ -0,0 +1,77 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ const OUTPUT_TAIL_BYTES = 4096;
4
+ const GATE_ENV_DENYLIST = ["AGENTGATE_CONFIG", "AGENTGATE_ACTIVATION_LABEL"];
5
+ function appendTail(current, chunk) {
6
+ const next = current + chunk.toString("utf8");
7
+ if (Buffer.byteLength(next, "utf8") <= OUTPUT_TAIL_BYTES) {
8
+ return next;
9
+ }
10
+ return Buffer.from(next, "utf8")
11
+ .subarray(-OUTPUT_TAIL_BYTES)
12
+ .toString("utf8");
13
+ }
14
+ export class GateRunner {
15
+ async run(gate) {
16
+ const startedAt = Date.now();
17
+ const timeoutMs = gate.timeoutMs ?? 120000;
18
+ let stdoutTail = "";
19
+ let stderrTail = "";
20
+ let settled = false;
21
+ let timedOut = false;
22
+ return new Promise((resolve) => {
23
+ const finish = (status, exitCode) => {
24
+ if (settled) {
25
+ return;
26
+ }
27
+ settled = true;
28
+ clearTimeout(timer);
29
+ resolve({
30
+ gateId: gate.id,
31
+ status,
32
+ exitCode,
33
+ durationMs: Date.now() - startedAt,
34
+ stdoutTail,
35
+ stderrTail,
36
+ });
37
+ };
38
+ const child = spawn(gate.command, gate.args, {
39
+ cwd: gate.cwd,
40
+ env: gateEnvironment(gate.cwd),
41
+ shell: false,
42
+ });
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ child.kill();
46
+ }, timeoutMs);
47
+ child.stdout?.on("data", (chunk) => {
48
+ stdoutTail = appendTail(stdoutTail, chunk);
49
+ });
50
+ child.stderr?.on("data", (chunk) => {
51
+ stderrTail = appendTail(stderrTail, chunk);
52
+ });
53
+ child.on("error", (error) => {
54
+ stderrTail = appendTail(stderrTail, Buffer.from(error.message));
55
+ finish("error", null);
56
+ });
57
+ child.on("close", (code) => {
58
+ if (timedOut) {
59
+ finish("timeout", code);
60
+ return;
61
+ }
62
+ finish(code === 0 ? "pass" : "fail", code);
63
+ });
64
+ });
65
+ }
66
+ }
67
+ function gateEnvironment(cwd) {
68
+ const env = { ...process.env };
69
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
70
+ const currentPath = env[pathKey] ?? "";
71
+ const localBin = path.join(cwd ?? process.cwd(), "node_modules", ".bin");
72
+ env[pathKey] = currentPath ? `${localBin}${path.delimiter}${currentPath}` : localBin;
73
+ for (const key of GATE_ENV_DENYLIST) {
74
+ delete env[key];
75
+ }
76
+ return env;
77
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerTools } from "./server.js";
5
+ async function main() {
6
+ const server = new McpServer({
7
+ name: "agentgate",
8
+ version: "0.1.0",
9
+ });
10
+ registerTools(server);
11
+ const transport = new StdioServerTransport();
12
+ await server.connect(transport);
13
+ }
14
+ main().catch((error) => {
15
+ console.error(error);
16
+ process.exit(1);
17
+ });
@@ -0,0 +1 @@
1
+ export const AGENTGATE_CHECKOUT_URL = "https://buy.polar.sh/polar_cl_VxlhG8lCO6IUcLcveJ51YOXYXMHWdUNorKr0U1bhyqm";
@@ -0,0 +1,6 @@
1
+ import { AGENTGATE_CHECKOUT_URL } from "./constants.js";
2
+ export function requirePro(feature, tier) {
3
+ if (tier === "free") {
4
+ throw new Error(`Pro license required: ${feature}. Buy a license at ${AGENTGATE_CHECKOUT_URL}, then activate it with agentgate_activate_license.`);
5
+ }
6
+ }
@@ -0,0 +1,187 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Polar } from "@polar-sh/sdk";
4
+ import { licenseCachePath } from "../util/paths.js";
5
+ const OFFLINE_GRACE_MS = 72 * 60 * 60 * 1000;
6
+ export const AGENTGATE_POLAR_ORGANIZATION_ID = "534c0711-1ded-497c-aa46-d1a7dfa46d6e";
7
+ export class PolarLicenseValidator {
8
+ options;
9
+ cachePath;
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ this.cachePath = options.cachePath ?? licenseCachePath();
13
+ }
14
+ async verify() {
15
+ const cache = await readLicenseCache(this.cachePath);
16
+ if (!cache.key) {
17
+ return { tier: "free" };
18
+ }
19
+ const organizationId = this.options.organizationId ??
20
+ process.env.POLAR_ORGANIZATION_ID ??
21
+ defaultOrganizationId();
22
+ if (!organizationId) {
23
+ return cachedWithinGrace(cache) ?? { tier: "free" };
24
+ }
25
+ try {
26
+ const polar = new Polar({
27
+ server: process.env.POLAR_ENVIRONMENT === "sandbox" ? "sandbox" : "production",
28
+ });
29
+ const activationId = cache.activationId ??
30
+ (await activateLicenseKey(polar, cache.key, organizationId, this.cachePath, cache));
31
+ const validated = await polar.customerPortal.licenseKeys.validate({
32
+ key: cache.key,
33
+ organizationId,
34
+ activationId,
35
+ }, {
36
+ timeoutMs: 10000,
37
+ });
38
+ if (!isGrantedLicense(validated)) {
39
+ return { tier: "free" };
40
+ }
41
+ const verification = verificationFromPolarResponse(validated);
42
+ await writeLicenseCache(this.cachePath, {
43
+ ...cache,
44
+ activationId,
45
+ ...verification,
46
+ validatedAt: new Date().toISOString(),
47
+ });
48
+ return verification;
49
+ }
50
+ catch (error) {
51
+ if (isNetworkError(error)) {
52
+ return cachedWithinGrace(cache) ?? { tier: "free" };
53
+ }
54
+ return { tier: "free" };
55
+ }
56
+ }
57
+ }
58
+ function defaultOrganizationId() {
59
+ if (process.env.POLAR_ENVIRONMENT === "sandbox") {
60
+ return undefined;
61
+ }
62
+ return AGENTGATE_POLAR_ORGANIZATION_ID;
63
+ }
64
+ export async function storeLicenseKey(key, cachePath = licenseCachePath()) {
65
+ const existing = await readLicenseCache(cachePath);
66
+ if (existing.key === key) {
67
+ await writeLicenseCache(cachePath, {
68
+ ...existing,
69
+ key,
70
+ });
71
+ return;
72
+ }
73
+ await writeLicenseCache(cachePath, { key });
74
+ }
75
+ async function activateLicenseKey(polar, key, organizationId, cachePath, cache) {
76
+ const activation = await polar.customerPortal.licenseKeys.activate({
77
+ key,
78
+ organizationId,
79
+ label: process.env.AGENTGATE_ACTIVATION_LABEL ?? "agentgate-local",
80
+ meta: {
81
+ app: "agentgate",
82
+ },
83
+ }, {
84
+ timeoutMs: 10000,
85
+ });
86
+ await writeLicenseCache(cachePath, {
87
+ ...cache,
88
+ activationId: activation.id,
89
+ });
90
+ return activation.id;
91
+ }
92
+ async function readLicenseCache(cachePath) {
93
+ try {
94
+ const raw = await readFile(cachePath, "utf8");
95
+ return JSON.parse(raw);
96
+ }
97
+ catch (error) {
98
+ if (isNodeError(error) && error.code === "ENOENT") {
99
+ return {};
100
+ }
101
+ throw error;
102
+ }
103
+ }
104
+ async function writeLicenseCache(cachePath, cache) {
105
+ await mkdir(path.dirname(cachePath), { recursive: true });
106
+ await writeFile(cachePath, JSON.stringify(cache, null, 2), "utf8");
107
+ }
108
+ function cachedWithinGrace(cache) {
109
+ if (!cache.tier || cache.tier === "free" || !cache.validatedAt) {
110
+ return undefined;
111
+ }
112
+ const validatedAt = new Date(cache.validatedAt).getTime();
113
+ if (Number.isNaN(validatedAt) || Date.now() - validatedAt > OFFLINE_GRACE_MS) {
114
+ return undefined;
115
+ }
116
+ return {
117
+ tier: cache.tier,
118
+ validUntil: cache.validUntil,
119
+ seats: cache.seats,
120
+ };
121
+ }
122
+ function isGrantedLicense(response) {
123
+ const record = asRecord(response);
124
+ if (record["valid"] === false) {
125
+ return false;
126
+ }
127
+ const status = record["status"];
128
+ if (typeof status === "string" && status !== "granted") {
129
+ return false;
130
+ }
131
+ return true;
132
+ }
133
+ function verificationFromPolarResponse(response) {
134
+ const record = asRecord(response);
135
+ const tier = inferTier(record);
136
+ const expiresAt = record["expiresAt"];
137
+ const limitActivations = numberValue(record["limitActivations"]);
138
+ return {
139
+ tier,
140
+ validUntil: expiresAt instanceof Date
141
+ ? expiresAt.toISOString()
142
+ : typeof expiresAt === "string"
143
+ ? expiresAt
144
+ : undefined,
145
+ seats: tier === "team" ? limitActivations ?? undefined : undefined,
146
+ };
147
+ }
148
+ function inferTier(record) {
149
+ const explicitTier = nestedString(record, ["metadata", "tier"]) ??
150
+ nestedString(record, ["benefit", "metadata", "tier"]) ??
151
+ nestedString(record, ["product", "metadata", "tier"]);
152
+ if (explicitTier === "team") {
153
+ return "team";
154
+ }
155
+ if (explicitTier === "pro") {
156
+ return "pro";
157
+ }
158
+ const limitActivations = numberValue(record["limitActivations"]);
159
+ if (limitActivations && limitActivations > 1) {
160
+ return "team";
161
+ }
162
+ return "pro";
163
+ }
164
+ function nestedString(record, pathParts) {
165
+ let current = record;
166
+ for (const part of pathParts) {
167
+ current = asRecord(current)[part];
168
+ }
169
+ return typeof current === "string" ? current.toLowerCase() : undefined;
170
+ }
171
+ function numberValue(value) {
172
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
173
+ }
174
+ function asRecord(value) {
175
+ return typeof value === "object" && value !== null
176
+ ? value
177
+ : {};
178
+ }
179
+ function isNetworkError(error) {
180
+ const record = asRecord(error);
181
+ const name = String(record["name"] ?? "");
182
+ const message = String(record["message"] ?? "");
183
+ return /(connection|network|timeout|abort|econn|enotfound|etimedout)/i.test(`${name} ${message}`);
184
+ }
185
+ function isNodeError(error) {
186
+ return error instanceof Error && "code" in error;
187
+ }
@@ -0,0 +1,145 @@
1
+ import { enforceBudget } from "../budget/killswitch.js";
2
+ import { TokenLedger } from "../budget/ledger.js";
3
+ import { requirePro } from "../license/gate.js";
4
+ import { appendEvent } from "../analytics/logger.js";
5
+ import { estimateCostUsd } from "../util/cost.js";
6
+ export class Orchestrator {
7
+ config;
8
+ gateRunner;
9
+ ledger;
10
+ licenseTier;
11
+ runId;
12
+ constructor(config, gateRunner, ledger = new TokenLedger(), licenseTier = "free", runId) {
13
+ this.config = config;
14
+ this.gateRunner = gateRunner;
15
+ this.ledger = ledger;
16
+ this.licenseTier = licenseTier;
17
+ this.runId = runId;
18
+ }
19
+ async runSlice(sliceId) {
20
+ const slice = this.config.slices.find((candidate) => candidate.id === sliceId);
21
+ if (!slice) {
22
+ throw new Error(`Unknown slice: ${sliceId}`);
23
+ }
24
+ await this.log({
25
+ type: "slice_start",
26
+ sliceId: slice.id,
27
+ });
28
+ const gateResults = [];
29
+ for (const gateId of slice.gates) {
30
+ if (this.config.budget) {
31
+ enforceBudget(this.ledger, this.config.budget);
32
+ }
33
+ const gate = this.findGate(gateId);
34
+ if (this.gateRequiresPro(gate)) {
35
+ requirePro("advanced_gates", this.licenseTier);
36
+ }
37
+ const gateResult = await this.gateRunner.run(gate);
38
+ gateResults.push(gateResult);
39
+ await this.log({
40
+ type: "gate_result",
41
+ sliceId: slice.id,
42
+ gateResult,
43
+ });
44
+ }
45
+ const tokensUsed = this.ledger.used();
46
+ const result = {
47
+ sliceId: slice.id,
48
+ passed: gateResults.every((result) => result.status === "pass"),
49
+ attempts: 1,
50
+ gateResults,
51
+ tokensUsed,
52
+ costUsd: this.config.budget
53
+ ? estimateCostUsd(tokensUsed, this.config.budget.model)
54
+ : 0,
55
+ };
56
+ await this.log({
57
+ type: "slice_end",
58
+ sliceId: slice.id,
59
+ passed: result.passed,
60
+ tokensUsed: result.tokensUsed,
61
+ costUsd: result.costUsd,
62
+ });
63
+ return result;
64
+ }
65
+ async runPlan() {
66
+ requirePro("run_plan", this.licenseTier);
67
+ const orderedSlices = this.topologicalSlices();
68
+ const results = [];
69
+ for (const slice of orderedSlices) {
70
+ const maxRetries = slice.maxRetries ?? 2;
71
+ const allGateResults = [];
72
+ let attempts = 0;
73
+ let latestResult;
74
+ while (attempts <= maxRetries) {
75
+ attempts += 1;
76
+ latestResult = await this.runSlice(slice.id);
77
+ allGateResults.push(...latestResult.gateResults);
78
+ if (latestResult.passed) {
79
+ break;
80
+ }
81
+ }
82
+ if (!latestResult) {
83
+ throw new Error(`No attempts ran for slice: ${slice.id}`);
84
+ }
85
+ const result = {
86
+ ...latestResult,
87
+ attempts,
88
+ gateResults: allGateResults,
89
+ };
90
+ results.push(result);
91
+ if (!result.passed) {
92
+ break;
93
+ }
94
+ }
95
+ return results;
96
+ }
97
+ findGate(gateId) {
98
+ const gate = this.config.gates.find((candidate) => candidate.id === gateId);
99
+ if (!gate) {
100
+ throw new Error(`Unknown gate: ${gateId}`);
101
+ }
102
+ return gate;
103
+ }
104
+ gateRequiresPro(gate) {
105
+ return (gate.proOnly === true ||
106
+ gate.type === "typecheck" ||
107
+ gate.type === "build" ||
108
+ gate.type === "custom");
109
+ }
110
+ topologicalSlices() {
111
+ const sliceById = new Map(this.config.slices.map((slice) => [slice.id, slice]));
112
+ const permanent = new Set();
113
+ const temporary = new Set();
114
+ const ordered = [];
115
+ const visit = (sliceId) => {
116
+ if (permanent.has(sliceId)) {
117
+ return;
118
+ }
119
+ if (temporary.has(sliceId)) {
120
+ throw new Error(`Cycle detected in slice dependencies at ${sliceId}`);
121
+ }
122
+ const slice = sliceById.get(sliceId);
123
+ if (!slice) {
124
+ throw new Error(`Unknown slice dependency: ${sliceId}`);
125
+ }
126
+ temporary.add(sliceId);
127
+ for (const dependency of slice.dependsOn ?? []) {
128
+ visit(dependency);
129
+ }
130
+ temporary.delete(sliceId);
131
+ permanent.add(sliceId);
132
+ ordered.push(slice);
133
+ };
134
+ for (const slice of this.config.slices) {
135
+ visit(slice.id);
136
+ }
137
+ return ordered;
138
+ }
139
+ async log(event) {
140
+ if (!this.runId) {
141
+ return;
142
+ }
143
+ await appendEvent(this.runId, event);
144
+ }
145
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,12 @@
1
+ import { registerActivateLicenseTool } from "./tools/activate_license.js";
2
+ import { registerBudgetStatusTool } from "./tools/budget_status.js";
3
+ import { registerDefineSliceTool } from "./tools/define_slice.js";
4
+ import { registerRunGateTool } from "./tools/run_gate.js";
5
+ import { registerRunPlanTool } from "./tools/run_plan.js";
6
+ export function registerTools(server) {
7
+ registerDefineSliceTool(server);
8
+ registerRunGateTool(server);
9
+ registerBudgetStatusTool(server);
10
+ registerActivateLicenseTool(server);
11
+ registerRunPlanTool(server);
12
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { PolarLicenseValidator, storeLicenseKey, } from "../license/validator.js";
3
+ import { AGENTGATE_CHECKOUT_URL } from "../license/constants.js";
4
+ import { jsonToolResult } from "./response.js";
5
+ const activateLicenseInputSchema = {
6
+ key: z.string().min(1),
7
+ };
8
+ export function registerActivateLicenseTool(server) {
9
+ server.registerTool("agentgate_activate_license", {
10
+ title: "Activate AgentGate License",
11
+ description: "Store and verify a Polar license key.",
12
+ inputSchema: activateLicenseInputSchema,
13
+ }, async ({ key }) => {
14
+ await storeLicenseKey(key);
15
+ const verification = await new PolarLicenseValidator().verify();
16
+ return jsonToolResult({
17
+ tier: verification.tier,
18
+ validUntil: verification.validUntil,
19
+ checkoutUrl: verification.tier === "free" ? AGENTGATE_CHECKOUT_URL : undefined,
20
+ });
21
+ });
22
+ }
@@ -0,0 +1,31 @@
1
+ import { loadConfig } from "../config/loader.js";
2
+ import { estimateCostUsd } from "../util/cost.js";
3
+ import { jsonToolResult } from "./response.js";
4
+ import { activeTokenLedger } from "./runtime.js";
5
+ export function registerBudgetStatusTool(server) {
6
+ server.registerTool("agentgate_budget_status", {
7
+ title: "AgentGate Budget Status",
8
+ description: "Report token usage, estimated cost, and budget state.",
9
+ }, async () => {
10
+ const config = await loadConfig(process.cwd());
11
+ const tokensUsed = activeTokenLedger.used();
12
+ if (!config.budget) {
13
+ return jsonToolResult({
14
+ tokensUsed,
15
+ maxTokens: null,
16
+ remaining: null,
17
+ costUsd: 0,
18
+ model: null,
19
+ state: "ok",
20
+ });
21
+ }
22
+ return jsonToolResult({
23
+ tokensUsed,
24
+ maxTokens: config.budget.maxTokens,
25
+ remaining: activeTokenLedger.remaining(config.budget.maxTokens),
26
+ costUsd: estimateCostUsd(tokensUsed, config.budget.model),
27
+ model: config.budget.model,
28
+ state: activeTokenLedger.check(config.budget),
29
+ });
30
+ });
31
+ }
@@ -0,0 +1,19 @@
1
+ import { loadConfig } from "../config/loader.js";
2
+ import { listActiveSlices } from "./define_slice.js";
3
+ export async function loadRuntimeConfig(cwd = process.cwd()) {
4
+ return withActiveSlices(await loadConfig(cwd));
5
+ }
6
+ export function withActiveSlices(config) {
7
+ const activeSlices = listActiveSlices();
8
+ if (activeSlices.length === 0) {
9
+ return config;
10
+ }
11
+ const slices = new Map(config.slices.map((slice) => [slice.id, slice]));
12
+ for (const slice of activeSlices) {
13
+ slices.set(slice.id, slice);
14
+ }
15
+ return {
16
+ ...config,
17
+ slices: [...slices.values()],
18
+ };
19
+ }
@@ -0,0 +1,24 @@
1
+ import { sliceSchema } from "../config/schema.js";
2
+ import { jsonToolResult } from "./response.js";
3
+ const activeSlices = new Map();
4
+ export function upsertActiveSlice(slice) {
5
+ activeSlices.set(slice.id, slice);
6
+ return slice;
7
+ }
8
+ export function listActiveSlices() {
9
+ return [...activeSlices.values()];
10
+ }
11
+ export function registerDefineSliceTool(server) {
12
+ server.registerTool("agentgate_define_slice", {
13
+ title: "Define AgentGate Slice",
14
+ description: "Register or update a slice in the active AgentGate run.",
15
+ inputSchema: sliceSchema.shape,
16
+ }, async (input) => {
17
+ const slice = sliceSchema.parse(input);
18
+ upsertActiveSlice(slice);
19
+ return jsonToolResult({
20
+ ok: true,
21
+ slice,
22
+ });
23
+ });
24
+ }
@@ -0,0 +1,10 @@
1
+ export function jsonToolResult(value) {
2
+ return {
3
+ content: [
4
+ {
5
+ type: "text",
6
+ text: JSON.stringify(value, null, 2),
7
+ },
8
+ ],
9
+ };
10
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+ import { GateRunner } from "../gates/runner.js";
3
+ import { PolarLicenseValidator } from "../license/validator.js";
4
+ import { Orchestrator } from "../orchestrator/orchestrator.js";
5
+ import { loadRuntimeConfig } from "./config.js";
6
+ import { jsonToolResult } from "./response.js";
7
+ import { activeRunId, activeTokenLedger } from "./runtime.js";
8
+ const runGateInputSchema = {
9
+ sliceId: z.string().min(1),
10
+ };
11
+ export function registerRunGateTool(server) {
12
+ server.registerTool("agentgate_run_gate", {
13
+ title: "Run AgentGate Gates",
14
+ description: "Run the configured validation gates for a slice.",
15
+ inputSchema: runGateInputSchema,
16
+ }, async ({ sliceId }) => {
17
+ const config = await loadRuntimeConfig(process.cwd());
18
+ const license = await new PolarLicenseValidator().verify();
19
+ const orchestrator = new Orchestrator(config, new GateRunner(), activeTokenLedger, license.tier, activeRunId);
20
+ const result = await orchestrator.runSlice(sliceId);
21
+ return jsonToolResult({
22
+ passed: result.passed,
23
+ gateResults: result.gateResults,
24
+ });
25
+ });
26
+ }
@@ -0,0 +1,17 @@
1
+ import { GateRunner } from "../gates/runner.js";
2
+ import { PolarLicenseValidator } from "../license/validator.js";
3
+ import { Orchestrator } from "../orchestrator/orchestrator.js";
4
+ import { loadRuntimeConfig } from "./config.js";
5
+ import { jsonToolResult } from "./response.js";
6
+ import { activeRunId, activeTokenLedger } from "./runtime.js";
7
+ export function registerRunPlanTool(server) {
8
+ server.registerTool("agentgate_run_plan", {
9
+ title: "Run AgentGate Plan",
10
+ description: "Run the full Pro multi-slice plan with retries.",
11
+ }, async () => {
12
+ const config = await loadRuntimeConfig(process.cwd());
13
+ const license = await new PolarLicenseValidator().verify();
14
+ const orchestrator = new Orchestrator(config, new GateRunner(), activeTokenLedger, license.tier, activeRunId);
15
+ return jsonToolResult(await orchestrator.runPlan());
16
+ });
17
+ }
@@ -0,0 +1,3 @@
1
+ import { TokenLedger } from "../budget/ledger.js";
2
+ export const activeTokenLedger = new TokenLedger();
3
+ export const activeRunId = `run-${Date.now()}`;
@@ -0,0 +1,15 @@
1
+ const MODEL_RATES_PER_1K_TOKENS_USD = {
2
+ opus: 0.015,
3
+ sonnet: 0.003,
4
+ haiku: 0.0008,
5
+ "gpt-5": 0.01,
6
+ "gpt-4.1": 0.005,
7
+ default: 0.005,
8
+ };
9
+ export function estimateCostUsd(tokens, model) {
10
+ const normalizedModel = model.toLowerCase();
11
+ const exactRate = MODEL_RATES_PER_1K_TOKENS_USD[normalizedModel];
12
+ const fuzzyRate = Object.entries(MODEL_RATES_PER_1K_TOKENS_USD).find(([key]) => normalizedModel.includes(key))?.[1];
13
+ const rate = exactRate ?? fuzzyRate ?? MODEL_RATES_PER_1K_TOKENS_USD.default;
14
+ return Number(((tokens / 1000) * rate).toFixed(4));
15
+ }
@@ -0,0 +1,11 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function agentgateHome() {
4
+ return path.join(os.homedir(), ".agentgate");
5
+ }
6
+ export function runsDir() {
7
+ return path.join(agentgateHome(), "runs");
8
+ }
9
+ export function licenseCachePath() {
10
+ return path.join(agentgateHome(), "license.json");
11
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@idevelopers/agentgate",
3
+ "version": "0.1.0",
4
+ "description": "Validation gates and token-budget guardrails for autonomous coding agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentgate": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepare": "npm run build",
12
+ "test": "vitest run"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "agentgate.config.example.json",
18
+ ".mcp.json",
19
+ ".claude-plugin",
20
+ "codex-plugin",
21
+ "skills",
22
+ "commands"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "codex",
27
+ "claude-code",
28
+ "agents",
29
+ "validation"
30
+ ],
31
+ "license": "MIT",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.13.0",
37
+ "@polar-sh/sdk": "^0.48.1",
38
+ "zod": "^3.25.67"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.19.1",
42
+ "typescript": "^5.8.3",
43
+ "vitest": "^3.2.4"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ }
48
+ }
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: agentgate-workflow
3
+ description: Use when implementing code under AgentGate validation gates, slice scopes, token budgets, or Pro multi-slice plans.
4
+ version: 0.1.0
5
+ ---
6
+
7
+ # AgentGate Workflow
8
+
9
+ Use this skill when implementing work under AgentGate guardrails.
10
+
11
+ 1. Load `agentgate.config.json` and identify the slice to run.
12
+ 2. If the slice is created during the session, call `agentgate_define_slice` with its `id`, `description`, `fileScope`, `acceptance`, and `gates`.
13
+ 3. Implement only inside the slice's `fileScope`.
14
+ 4. Call `agentgate_run_gate` with the slice id after implementation.
15
+ 5. Treat any failed, errored, or timed-out gate as blocking. Read the returned tails, fix the issue, and rerun the gate.
16
+ 6. Do not mark a slice complete until all returned gates pass.
17
+ 7. Between slices, call `agentgate_budget_status` and stop if the state is `stopped`.
18
+ 8. For Pro multi-slice work, call `agentgate_run_plan` to execute slices in dependency order with bounded retries.