@agentwares/agentguard 0.1.9 → 0.1.10
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/CHANGELOG.md +33 -0
- package/dist/audit-5LUDXZLG.js +18 -0
- package/dist/chunk-2QDPPTPY.js +179 -0
- package/dist/chunk-2QDPPTPY.js.map +1 -0
- package/dist/{chunk-SUXQTT3X.js → chunk-4WTFMFRY.js} +18 -16
- package/dist/chunk-4WTFMFRY.js.map +1 -0
- package/dist/{chunk-VQWQ5TOM.js → chunk-DEVXG3IB.js} +4 -4
- package/dist/chunk-DEVXG3IB.js.map +1 -0
- package/dist/chunk-F73VDY3W.js +202 -0
- package/dist/chunk-F73VDY3W.js.map +1 -0
- package/dist/{chunk-X4M7YWQA.js → chunk-IEJK7N3B.js} +32 -27
- package/dist/chunk-IEJK7N3B.js.map +1 -0
- package/dist/{chunk-ZSLWPKXI.js → chunk-IS3DAHZI.js} +562 -8
- package/dist/chunk-IS3DAHZI.js.map +1 -0
- package/dist/{chunk-KRKZGAJ3.js → chunk-JSELR4SG.js} +66 -55
- package/dist/chunk-JSELR4SG.js.map +1 -0
- package/dist/{chunk-TW5KJREH.js → chunk-OM5L7A32.js} +20 -11
- package/dist/chunk-OM5L7A32.js.map +1 -0
- package/dist/{chunk-HPYZ3JDX.js → chunk-SWZQMPDG.js} +3 -3
- package/dist/{chunk-HPYZ3JDX.js.map → chunk-SWZQMPDG.js.map} +1 -1
- package/dist/{chunk-D66U3PVB.js → chunk-TJXZBOHL.js} +6 -6
- package/dist/chunk-TJXZBOHL.js.map +1 -0
- package/dist/{chunk-BKXXTWIS.js → chunk-XOBFANL4.js} +5 -5
- package/dist/chunk-XOBFANL4.js.map +1 -0
- package/dist/chunk-XPNBYSIN.js +23 -0
- package/dist/chunk-XPNBYSIN.js.map +1 -0
- package/dist/{chunk-6S7LYVOC.js → chunk-YBVD6HTT.js} +27 -24
- package/dist/chunk-YBVD6HTT.js.map +1 -0
- package/dist/{chunk-6TUQD666.js → chunk-ZG4LGUJE.js} +100 -81
- package/dist/chunk-ZG4LGUJE.js.map +1 -0
- package/dist/cli.js +6 -4
- package/dist/fixtures/crm-server.js +2 -2
- package/dist/fixtures/demo-agent.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +23 -21
- package/dist/index.js.map +1 -1
- package/dist/init-MKPQU2JG.js +17 -0
- package/dist/{proxy-3Z5YEZ4R.js → proxy-6GP63TLD.js} +21 -29
- package/dist/proxy-6GP63TLD.js.map +1 -0
- package/dist/tools-XS7PMTLT.js +15 -0
- package/llms.txt +8 -6
- package/package.json +10 -4
- package/server.json +9 -9
- package/dist/audit-2YCMHMCP.js +0 -17
- package/dist/chunk-3O6KWT3E.js +0 -345
- package/dist/chunk-3O6KWT3E.js.map +0 -1
- package/dist/chunk-6S7LYVOC.js.map +0 -1
- package/dist/chunk-6TUQD666.js.map +0 -1
- package/dist/chunk-BKXXTWIS.js.map +0 -1
- package/dist/chunk-D66U3PVB.js.map +0 -1
- package/dist/chunk-KRKZGAJ3.js.map +0 -1
- package/dist/chunk-SUXQTT3X.js.map +0 -1
- package/dist/chunk-TW5KJREH.js.map +0 -1
- package/dist/chunk-VQWQ5TOM.js.map +0 -1
- package/dist/chunk-X4M7YWQA.js.map +0 -1
- package/dist/chunk-ZSLWPKXI.js.map +0 -1
- package/dist/init-CWROGV5I.js +0 -15
- package/dist/proxy-3Z5YEZ4R.js.map +0 -1
- package/dist/tools-3LLDG3XQ.js +0 -15
- /package/dist/{audit-2YCMHMCP.js.map → audit-5LUDXZLG.js.map} +0 -0
- /package/dist/{init-CWROGV5I.js.map → init-MKPQU2JG.js.map} +0 -0
- /package/dist/{tools-3LLDG3XQ.js.map → tools-XS7PMTLT.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
`@agentwares/agentguard`, the CLI and MCP proxy. A tag `agentguard-v<version>` publishes the
|
|
4
4
|
GitHub Release for a version with its section below as the notes.
|
|
5
5
|
|
|
6
|
+
## 0.1.10 — 2026-10-08
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **agentguard is now okgate.** "Nothing irreversible without an OK." The CLI calls itself
|
|
11
|
+
`okgate`, and npm serves it as a second bin of this package until `@agentwares/okgate` is
|
|
12
|
+
published: `npx -p @agentwares/agentguard okgate init`. Everything it writes from now on uses
|
|
13
|
+
the new names: `okgate.yaml`, `.okgate/`, `OKGATE_*`, the `okgate` MCP server entry, the
|
|
14
|
+
`okgate_get_status` / `okgate_get_report` / `okgate_verify_audit_log` tools, `X-Okgate-Key`,
|
|
15
|
+
and hook commands that run `npx -y -p @agentwares/agentguard@<version> okgate hook pre-tool-use`.
|
|
16
|
+
- The server registers in the MCP Registry as `io.github.agentwares/okgate` (`mcpName`).
|
|
17
|
+
|
|
18
|
+
### Kept working (until at least 2027-01-15)
|
|
19
|
+
|
|
20
|
+
- The `agentguard` bin, and `npx @agentwares/agentguard`, which runs it.
|
|
21
|
+
- `agentguard.yaml` is read when there is no `okgate.yaml`, including walking up from a coding
|
|
22
|
+
agent's working directory; creating an `okgate.yaml` beside it counts as editing the guard.
|
|
23
|
+
- An existing `.agentguard/` stays the state directory when there is no `.okgate/`, so counters,
|
|
24
|
+
pending approvals and the audit log's hash chain carry on: `okgate verify` passes across the
|
|
25
|
+
rename. Both directories are the guard's own and refused to a coding agent.
|
|
26
|
+
- Each `AGENTGUARD_*` variable applies when its `OKGATE_*` twin is unset. **`AGENTGUARD_KILL=1`
|
|
27
|
+
and `OKGATE_KILL=1` both halt everything, whatever `kill.env` the policy names**, and a KILL
|
|
28
|
+
file under either directory halts; `okgate kill` writes both that exist and `resume` clears both.
|
|
29
|
+
- `hooks status`, `hooks uninstall` and `hooks install` recognise agentguard-era hook entries
|
|
30
|
+
(install replaces one, it never adds a second); `init` recognises the old `agentguard` MCP entry
|
|
31
|
+
and `init --undo` restores a `*.agentguard-backup`; `connect` replaces an `agentguard` entry.
|
|
32
|
+
- `tools/list` shows only `okgate_*`; a `tools/call` for an `agentguard_*` name runs its twin with
|
|
33
|
+
a deprecation note in `_meta["okgate/deprecation"]` until 2027-01-15, then answers
|
|
34
|
+
`TOOL_RENAMED` (`retryable: false`, with the new name in `fix`). Tool results carry the verdict
|
|
35
|
+
under `_meta.okgate` and, for the window, `_meta.agentguard`.
|
|
36
|
+
- `<!-- agentguard: … -->` inline checks, `X-Agentguard-Key`, and `_meta["agentguard/runId"]`.
|
|
37
|
+
- `permission-diff` (and the GitHub Action) diffs `okgate.yaml` and `agentguard.yaml` alike.
|
|
38
|
+
|
|
6
39
|
## 0.1.9 — 2026-10-07
|
|
7
40
|
|
|
8
41
|
### Added
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TEAM_HISTORY_LINE,
|
|
3
|
+
TEAM_HISTORY_URL,
|
|
4
|
+
auditCommand,
|
|
5
|
+
parseSince
|
|
6
|
+
} from "./chunk-YBVD6HTT.js";
|
|
7
|
+
import "./chunk-F73VDY3W.js";
|
|
8
|
+
import "./chunk-2QDPPTPY.js";
|
|
9
|
+
import "./chunk-XPNBYSIN.js";
|
|
10
|
+
import "./chunk-4WTFMFRY.js";
|
|
11
|
+
import "./chunk-5WRI5ZAA.js";
|
|
12
|
+
export {
|
|
13
|
+
TEAM_HISTORY_LINE,
|
|
14
|
+
TEAM_HISTORY_URL,
|
|
15
|
+
auditCommand,
|
|
16
|
+
parseSince
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=audit-5LUDXZLG.js.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/hooks/clients.ts
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import {
|
|
5
|
+
LEGACY_POLICY_FILE,
|
|
6
|
+
LEGACY_STATE_DIR,
|
|
7
|
+
POLICY_FILE,
|
|
8
|
+
STATE_DIR
|
|
9
|
+
} from "@agentwares/agentguard-core";
|
|
10
|
+
var MATCHERS = {
|
|
11
|
+
"claude-code": "^(Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit)$|^mcp__",
|
|
12
|
+
codex: "^(Bash|apply_patch)$|^mcp__",
|
|
13
|
+
"gemini-cli": "^(run_shell_command|write_file|replace)$|^mcp_"
|
|
14
|
+
};
|
|
15
|
+
var CLIENT_ALIASES = {
|
|
16
|
+
claude: "claude-code",
|
|
17
|
+
"claude-code": "claude-code",
|
|
18
|
+
codex: "codex",
|
|
19
|
+
gemini: "gemini-cli",
|
|
20
|
+
"gemini-cli": "gemini-cli"
|
|
21
|
+
};
|
|
22
|
+
function hookTarget(client, scope, where, command, timeoutS = 30) {
|
|
23
|
+
const root = scope === "project" ? where.cwd : where.home;
|
|
24
|
+
if (client === "claude-code")
|
|
25
|
+
return {
|
|
26
|
+
client,
|
|
27
|
+
label: "Claude Code",
|
|
28
|
+
scope,
|
|
29
|
+
settingsPath: join(root, ".claude", "settings.json"),
|
|
30
|
+
event: "PreToolUse",
|
|
31
|
+
matcher: MATCHERS[client],
|
|
32
|
+
handler: { type: "command", command, timeout: timeoutS, statusMessage: "okgate" }
|
|
33
|
+
};
|
|
34
|
+
if (client === "codex")
|
|
35
|
+
return {
|
|
36
|
+
client,
|
|
37
|
+
label: "Codex CLI",
|
|
38
|
+
scope,
|
|
39
|
+
settingsPath: join(root, ".codex", "hooks.json"),
|
|
40
|
+
event: "PreToolUse",
|
|
41
|
+
matcher: MATCHERS[client],
|
|
42
|
+
handler: { type: "command", command, timeout: timeoutS, statusMessage: "okgate" },
|
|
43
|
+
note: "Codex runs a new hook only after you trust it: open /hooks in Codex and trust the okgate entry."
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
client,
|
|
47
|
+
label: "Gemini CLI",
|
|
48
|
+
scope,
|
|
49
|
+
settingsPath: join(root, ".gemini", "settings.json"),
|
|
50
|
+
event: "BeforeTool",
|
|
51
|
+
matcher: MATCHERS[client],
|
|
52
|
+
handler: {
|
|
53
|
+
name: "okgate",
|
|
54
|
+
type: "command",
|
|
55
|
+
command,
|
|
56
|
+
timeout: timeoutS * 1e3,
|
|
57
|
+
description: "okgate: one rulebook for shell commands and file edits"
|
|
58
|
+
},
|
|
59
|
+
note: scope === "project" ? "Gemini CLI fingerprints project hooks: confirm the okgate hook the first time it asks." : void 0
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isGuardHookCommand(command) {
|
|
63
|
+
return typeof command === "string" && /okgate|agentguard/.test(command) && /\bhook\s+pre-tool-use\b/.test(command);
|
|
64
|
+
}
|
|
65
|
+
function backupFor(path) {
|
|
66
|
+
const legacy = `${path}.agentguard-backup`;
|
|
67
|
+
const next = `${path}.okgate-backup`;
|
|
68
|
+
return { existing: [legacy, next].find((b) => existsSync(b)), next };
|
|
69
|
+
}
|
|
70
|
+
function readSettings(path) {
|
|
71
|
+
if (!existsSync(path)) return {};
|
|
72
|
+
const text = readFileSync(path, "utf8");
|
|
73
|
+
if (!text.trim()) return {};
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(text);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}); fix it or move it aside \u2014 okgate will not overwrite it`,
|
|
80
|
+
{ cause: err }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
84
|
+
throw new Error(`${path} is not a JSON object; okgate will not overwrite it`);
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
function groupsOf(settings, event) {
|
|
88
|
+
const hooks = settings.hooks;
|
|
89
|
+
if (!hooks || typeof hooks !== "object") return [];
|
|
90
|
+
const groups = hooks[event];
|
|
91
|
+
return Array.isArray(groups) ? groups : [];
|
|
92
|
+
}
|
|
93
|
+
function installedHandlers(target) {
|
|
94
|
+
const settings = readSettings(target.settingsPath);
|
|
95
|
+
return groupsOf(settings, target.event).flatMap(
|
|
96
|
+
(g) => (g.hooks ?? []).filter((h) => isGuardHookCommand(h.command))
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
function installHook(target) {
|
|
100
|
+
const existed = existsSync(target.settingsPath);
|
|
101
|
+
const settings = readSettings(target.settingsPath);
|
|
102
|
+
const hooks = settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {};
|
|
103
|
+
const groups = groupsOf(settings, target.event);
|
|
104
|
+
const isOurs = (h) => isGuardHookCommand(h.command);
|
|
105
|
+
const ours = groups.flatMap((g) => (g.hooks ?? []).filter(isOurs));
|
|
106
|
+
const exact = groups.some(
|
|
107
|
+
(g) => g.matcher === target.matcher && g.hooks?.length === 1 && JSON.stringify(g.hooks[0]) === JSON.stringify(target.handler)
|
|
108
|
+
);
|
|
109
|
+
if (exact && ours.length === 1) return { status: "unchanged" };
|
|
110
|
+
const kept = [];
|
|
111
|
+
for (const group of groups) {
|
|
112
|
+
if (!(group.hooks ?? []).some(isOurs)) {
|
|
113
|
+
kept.push(group);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const others = (group.hooks ?? []).filter((h) => !isOurs(h));
|
|
117
|
+
if (others.length > 0) kept.push({ ...group, hooks: others });
|
|
118
|
+
}
|
|
119
|
+
kept.push({ matcher: target.matcher, hooks: [target.handler] });
|
|
120
|
+
const status = ours.length > 0 ? "updated" : "added";
|
|
121
|
+
hooks[target.event] = kept;
|
|
122
|
+
settings.hooks = hooks;
|
|
123
|
+
let backup;
|
|
124
|
+
if (existed) {
|
|
125
|
+
const { existing, next } = backupFor(target.settingsPath);
|
|
126
|
+
backup = existing ?? next;
|
|
127
|
+
if (!existing) copyFileSync(target.settingsPath, backup);
|
|
128
|
+
}
|
|
129
|
+
mkdirSync(dirname(target.settingsPath), { recursive: true });
|
|
130
|
+
writeFileSync(target.settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
131
|
+
return { status, backup };
|
|
132
|
+
}
|
|
133
|
+
function uninstallHook(target) {
|
|
134
|
+
if (!existsSync(target.settingsPath)) return 0;
|
|
135
|
+
const settings = readSettings(target.settingsPath);
|
|
136
|
+
const groups = groupsOf(settings, target.event);
|
|
137
|
+
let removed = 0;
|
|
138
|
+
const kept = [];
|
|
139
|
+
for (const group of groups) {
|
|
140
|
+
const others = (group.hooks ?? []).filter((h) => !isGuardHookCommand(h.command));
|
|
141
|
+
removed += (group.hooks ?? []).length - others.length;
|
|
142
|
+
if (others.length > 0) kept.push({ ...group, hooks: others });
|
|
143
|
+
}
|
|
144
|
+
if (removed === 0) return 0;
|
|
145
|
+
const hooks = settings.hooks;
|
|
146
|
+
if (kept.length > 0) hooks[target.event] = kept;
|
|
147
|
+
else delete hooks[target.event];
|
|
148
|
+
if (Object.keys(hooks).length === 0) delete settings.hooks;
|
|
149
|
+
writeFileSync(target.settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
function hookSettingsFiles(where) {
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const root of [where.cwd, where.home]) {
|
|
155
|
+
out.push(
|
|
156
|
+
join(root, ".claude", "settings.json"),
|
|
157
|
+
join(root, ".claude", "settings.local.json"),
|
|
158
|
+
join(root, ".codex", "hooks.json"),
|
|
159
|
+
join(root, ".codex", "config.toml"),
|
|
160
|
+
join(root, ".gemini", "settings.json")
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
out.push(
|
|
164
|
+
join(where.home, STATE_DIR, POLICY_FILE),
|
|
165
|
+
join(where.home, LEGACY_STATE_DIR, LEGACY_POLICY_FILE)
|
|
166
|
+
);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export {
|
|
171
|
+
CLIENT_ALIASES,
|
|
172
|
+
hookTarget,
|
|
173
|
+
backupFor,
|
|
174
|
+
installedHandlers,
|
|
175
|
+
installHook,
|
|
176
|
+
uninstallHook,
|
|
177
|
+
hookSettingsFiles
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=chunk-2QDPPTPY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/clients.ts"],"sourcesContent":["/**\n * Where each coding agent reads its pre-execution hooks, and the entry okgate writes there.\n * Verified 7 October 2026 against:\n *\n * - Claude Code: https://code.claude.com/docs/en/hooks — `hooks.PreToolUse[]` in\n * `.claude/settings.json` (project) or `~/.claude/settings.json` (user); a matcher with\n * characters beyond letters, digits, `_`, `-`, `,` and `|` is an unanchored regex; `timeout`\n * in seconds.\n * - Codex CLI: https://developers.openai.com/codex/hooks — `hooks.PreToolUse[]` in\n * `.codex/hooks.json` or `~/.codex/hooks.json`; the matcher is a regex over the tool name\n * (`Bash`, `apply_patch`, `mcp__…`); `timeout` in seconds; a new hook runs only after the user\n * trusts it in `/hooks`.\n * - Gemini CLI: https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md —\n * `hooks.BeforeTool[]` in `.gemini/settings.json` or `~/.gemini/settings.json`; regex matcher\n * over `run_shell_command`, `write_file`, `replace`, `mcp_…`; `timeout` in milliseconds.\n */\nimport { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n LEGACY_POLICY_FILE,\n LEGACY_STATE_DIR,\n POLICY_FILE,\n STATE_DIR,\n type HookClient,\n} from \"@agentwares/agentguard-core\";\n\nexport type InstallScope = \"project\" | \"user\";\n\nexport interface HookTarget {\n client: HookClient;\n label: string;\n scope: InstallScope;\n settingsPath: string;\n event: \"PreToolUse\" | \"BeforeTool\";\n matcher: string;\n handler: Record<string, unknown>;\n /** what the user has to do in the client before the hook runs */\n note?: string;\n}\n\n/** Tools worth a check: the shell, file writes and MCP tools. Reads are left alone. */\nexport const MATCHERS: Record<HookClient, string> = {\n \"claude-code\": \"^(Bash|PowerShell|Write|Edit|MultiEdit|NotebookEdit)$|^mcp__\",\n codex: \"^(Bash|apply_patch)$|^mcp__\",\n \"gemini-cli\": \"^(run_shell_command|write_file|replace)$|^mcp_\",\n};\n\nexport const CLIENT_ALIASES: Record<string, HookClient> = {\n claude: \"claude-code\",\n \"claude-code\": \"claude-code\",\n codex: \"codex\",\n gemini: \"gemini-cli\",\n \"gemini-cli\": \"gemini-cli\",\n};\n\nexport function hookTarget(\n client: HookClient,\n scope: InstallScope,\n where: { cwd: string; home: string },\n command: string,\n timeoutS = 30,\n): HookTarget {\n const root = scope === \"project\" ? where.cwd : where.home;\n if (client === \"claude-code\")\n return {\n client,\n label: \"Claude Code\",\n scope,\n settingsPath: join(root, \".claude\", \"settings.json\"),\n event: \"PreToolUse\",\n matcher: MATCHERS[client],\n handler: { type: \"command\", command, timeout: timeoutS, statusMessage: \"okgate\" },\n };\n if (client === \"codex\")\n return {\n client,\n label: \"Codex CLI\",\n scope,\n settingsPath: join(root, \".codex\", \"hooks.json\"),\n event: \"PreToolUse\",\n matcher: MATCHERS[client],\n handler: { type: \"command\", command, timeout: timeoutS, statusMessage: \"okgate\" },\n note: \"Codex runs a new hook only after you trust it: open /hooks in Codex and trust the okgate entry.\",\n };\n return {\n client,\n label: \"Gemini CLI\",\n scope,\n settingsPath: join(root, \".gemini\", \"settings.json\"),\n event: \"BeforeTool\",\n matcher: MATCHERS[client],\n handler: {\n name: \"okgate\",\n type: \"command\",\n command,\n timeout: timeoutS * 1000,\n description: \"okgate: one rulebook for shell commands and file edits\",\n },\n note:\n scope === \"project\"\n ? \"Gemini CLI fingerprints project hooks: confirm the okgate hook the first time it asks.\"\n : undefined,\n };\n}\n\n/** Is this hook command one okgate wrote, under either name (agentguard before the rename)? */\nexport function isGuardHookCommand(command: unknown): boolean {\n return (\n typeof command === \"string\" &&\n /okgate|agentguard/.test(command) &&\n /\\bhook\\s+pre-tool-use\\b/.test(command)\n );\n}\n\n/**\n * The backup of a file okgate rewrites: `<file>.okgate-backup`, or the `<file>.agentguard-backup`\n * an okgate-era install left, which holds the older original and so wins.\n */\nexport function backupFor(path: string): { existing?: string; next: string } {\n const legacy = `${path}.agentguard-backup`;\n const next = `${path}.okgate-backup`;\n return { existing: [legacy, next].find((b) => existsSync(b)), next };\n}\n\ntype Json = Record<string, unknown>;\ninterface MatcherGroup {\n matcher?: string;\n hooks?: Json[];\n [k: string]: unknown;\n}\n\nfunction readSettings(path: string): Json {\n if (!existsSync(path)) return {};\n const text = readFileSync(path, \"utf8\");\n if (!text.trim()) return {};\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch (err) {\n throw new Error(\n `${path} is not valid JSON (${err instanceof Error ? err.message : String(err)}); fix it or move it aside — okgate will not overwrite it`,\n { cause: err },\n );\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed))\n throw new Error(`${path} is not a JSON object; okgate will not overwrite it`);\n return parsed as Json;\n}\n\nfunction groupsOf(settings: Json, event: string): MatcherGroup[] {\n const hooks = settings.hooks;\n if (!hooks || typeof hooks !== \"object\") return [];\n const groups = (hooks as Json)[event];\n return Array.isArray(groups) ? (groups as MatcherGroup[]) : [];\n}\n\n/** The okgate handlers currently in a settings file. */\nexport function installedHandlers(target: HookTarget): Json[] {\n const settings = readSettings(target.settingsPath);\n return groupsOf(settings, target.event).flatMap((g) =>\n (g.hooks ?? []).filter((h) => isGuardHookCommand(h.command)),\n );\n}\n\n/**\n * Write (or update in place) the okgate entry, keeping every other setting and hook. The\n * first change to an existing file leaves a `*.okgate-backup` next to it (or keeps an agentguard-era `*.agentguard-backup`).\n */\nexport function installHook(target: HookTarget): {\n status: \"added\" | \"updated\" | \"unchanged\";\n backup?: string;\n} {\n const existed = existsSync(target.settingsPath);\n const settings = readSettings(target.settingsPath);\n const hooks = (\n settings.hooks && typeof settings.hooks === \"object\" ? settings.hooks : {}\n ) as Json;\n const groups = groupsOf(settings, target.event);\n const isOurs = (h: Json): boolean => isGuardHookCommand(h.command);\n const ours = groups.flatMap((g) => (g.hooks ?? []).filter(isOurs));\n const exact = groups.some(\n (g) =>\n g.matcher === target.matcher &&\n g.hooks?.length === 1 &&\n JSON.stringify(g.hooks[0]) === JSON.stringify(target.handler),\n );\n if (exact && ours.length === 1) return { status: \"unchanged\" };\n const kept: MatcherGroup[] = [];\n for (const group of groups) {\n if (!(group.hooks ?? []).some(isOurs)) {\n kept.push(group);\n continue;\n }\n const others = (group.hooks ?? []).filter((h) => !isOurs(h));\n if (others.length > 0) kept.push({ ...group, hooks: others });\n }\n kept.push({ matcher: target.matcher, hooks: [target.handler] });\n const status = ours.length > 0 ? \"updated\" : \"added\";\n hooks[target.event] = kept;\n settings.hooks = hooks;\n let backup: string | undefined;\n if (existed) {\n const { existing, next } = backupFor(target.settingsPath);\n backup = existing ?? next;\n if (!existing) copyFileSync(target.settingsPath, backup);\n }\n mkdirSync(dirname(target.settingsPath), { recursive: true });\n writeFileSync(target.settingsPath, JSON.stringify(settings, null, 2) + \"\\n\");\n return { status, backup };\n}\n\n/** Remove okgate's entries; other hooks and settings stay. Returns how many were removed. */\nexport function uninstallHook(target: HookTarget): number {\n if (!existsSync(target.settingsPath)) return 0;\n const settings = readSettings(target.settingsPath);\n const groups = groupsOf(settings, target.event);\n let removed = 0;\n const kept: MatcherGroup[] = [];\n for (const group of groups) {\n const others = (group.hooks ?? []).filter((h) => !isGuardHookCommand(h.command));\n removed += (group.hooks ?? []).length - others.length;\n if (others.length > 0) kept.push({ ...group, hooks: others });\n }\n if (removed === 0) return 0;\n const hooks = settings.hooks as Json;\n if (kept.length > 0) hooks[target.event] = kept;\n else delete hooks[target.event];\n if (Object.keys(hooks).length === 0) delete settings.hooks;\n writeFileSync(target.settingsPath, JSON.stringify(settings, null, 2) + \"\\n\");\n return removed;\n}\n\n/** Every settings file that can hold a hook, project and user level: edits to them are guard-config. */\nexport function hookSettingsFiles(where: { cwd: string; home: string }): string[] {\n const out: string[] = [];\n for (const root of [where.cwd, where.home]) {\n out.push(\n join(root, \".claude\", \"settings.json\"),\n join(root, \".claude\", \"settings.local.json\"),\n join(root, \".codex\", \"hooks.json\"),\n join(root, \".codex\", \"config.toml\"),\n join(root, \".gemini\", \"settings.json\"),\n );\n }\n out.push(\n join(where.home, STATE_DIR, POLICY_FILE),\n join(where.home, LEGACY_STATE_DIR, LEGACY_POLICY_FILE),\n );\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1031,7 +1031,7 @@ async function sendWebhook(url, payload, opts = {}) {
|
|
|
1031
1031
|
function approvalSlackMessage(event, url) {
|
|
1032
1032
|
const a = event.approval;
|
|
1033
1033
|
const argsText = JSON.stringify(a.args ?? {}, null, 2).slice(0, 1500);
|
|
1034
|
-
const text = `[
|
|
1034
|
+
const text = `[okgate] approval needed: ${a.tool} (run ${a.run_id}) \u2014 run \`okgate approve ${a.id}\`${url ? ` or open ${url}` : ""}`;
|
|
1035
1035
|
const blocks = [
|
|
1036
1036
|
{ type: "header", text: { type: "plain_text", text: `Approval needed: ${a.tool}` } },
|
|
1037
1037
|
{
|
|
@@ -1047,7 +1047,7 @@ function approvalSlackMessage(event, url) {
|
|
|
1047
1047
|
elements: [
|
|
1048
1048
|
{
|
|
1049
1049
|
type: "mrkdwn",
|
|
1050
|
-
text: `Approve: \`
|
|
1050
|
+
text: `Approve: \`okgate approve ${a.id}\` \xB7 Deny: \`okgate deny ${a.id}\` \xB7 expires ${a.expires_at}`
|
|
1051
1051
|
}
|
|
1052
1052
|
]
|
|
1053
1053
|
}
|
|
@@ -1070,7 +1070,7 @@ function approvalSlackMessage(event, url) {
|
|
|
1070
1070
|
}
|
|
1071
1071
|
function haltSlackMessage(event) {
|
|
1072
1072
|
const cause = event.error?.cause ?? "";
|
|
1073
|
-
const text = `[
|
|
1073
|
+
const text = `[okgate] ${event.type}: ${event.tool} (run ${event.runId}) \u2014 ${cause}`;
|
|
1074
1074
|
return {
|
|
1075
1075
|
text,
|
|
1076
1076
|
blocks: [
|
|
@@ -1087,7 +1087,7 @@ ${cause}`
|
|
|
1087
1087
|
elements: [
|
|
1088
1088
|
{
|
|
1089
1089
|
type: "mrkdwn",
|
|
1090
|
-
text: `\`
|
|
1090
|
+
text: `\`okgate report --run ${event.runId}\` for the full picture${event.type === "KILLED" ? "" : " \xB7 `okgate kill` to stop everything"}`
|
|
1091
1091
|
}
|
|
1092
1092
|
]
|
|
1093
1093
|
}
|
|
@@ -1119,7 +1119,7 @@ function createAlerter(opts) {
|
|
|
1119
1119
|
webhook,
|
|
1120
1120
|
{
|
|
1121
1121
|
event: event.type,
|
|
1122
|
-
product: "
|
|
1122
|
+
product: "okgate",
|
|
1123
1123
|
subject: message.text,
|
|
1124
1124
|
summary: event.error?.cause ?? "",
|
|
1125
1125
|
occurredAt: event.at,
|
|
@@ -1143,18 +1143,22 @@ function createAlerter(opts) {
|
|
|
1143
1143
|
|
|
1144
1144
|
// src/context.ts
|
|
1145
1145
|
import { existsSync } from "fs";
|
|
1146
|
-
import { resolve } from "path";
|
|
1147
|
-
import {
|
|
1146
|
+
import { basename, resolve } from "path";
|
|
1147
|
+
import {
|
|
1148
|
+
Guard,
|
|
1149
|
+
POLICY_FILE,
|
|
1150
|
+
envTwin
|
|
1151
|
+
} from "@agentwares/agentguard-core";
|
|
1148
1152
|
import {
|
|
1149
1153
|
FileApprovalStore,
|
|
1150
1154
|
FileAuditSink,
|
|
1151
|
-
FileKillSwitch,
|
|
1152
1155
|
FileStateStore,
|
|
1156
|
+
findPolicyFile,
|
|
1157
|
+
killSwitchFor,
|
|
1153
1158
|
loadPolicyFile
|
|
1154
1159
|
} from "@agentwares/agentguard-core/node";
|
|
1155
1160
|
import { existsSync as exists, readFileSync as readFileSync2 } from "fs";
|
|
1156
1161
|
import { homedir } from "os";
|
|
1157
|
-
var DEFAULT_POLICY_FILE = "agentguard.yaml";
|
|
1158
1162
|
function readStdin(maxBytes = 16 * 1024 * 1024, timeoutMs = 1e4) {
|
|
1159
1163
|
if (process.stdin.isTTY) return Promise.resolve("");
|
|
1160
1164
|
return new Promise((resolveText) => {
|
|
@@ -1190,10 +1194,8 @@ function defaultIo() {
|
|
|
1190
1194
|
};
|
|
1191
1195
|
}
|
|
1192
1196
|
function policyPathFrom(args, io) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
flagString(args, "config") ?? io.env.AGENTGUARD_CONFIG ?? DEFAULT_POLICY_FILE
|
|
1196
|
-
);
|
|
1197
|
+
const named = flagString(args, "config") ?? envTwin(io.env, "CONFIG");
|
|
1198
|
+
return named !== void 0 ? resolve(io.cwd, named) : findPolicyFile(io.cwd);
|
|
1197
1199
|
}
|
|
1198
1200
|
function loadPolicyFor(args, io, opts = {}) {
|
|
1199
1201
|
return loadPolicyFile(policyPathFrom(args, io), {
|
|
@@ -1202,7 +1204,7 @@ function loadPolicyFor(args, io, opts = {}) {
|
|
|
1202
1204
|
});
|
|
1203
1205
|
}
|
|
1204
1206
|
function createFileGuard(loaded, opts = {}) {
|
|
1205
|
-
const kill =
|
|
1207
|
+
const kill = killSwitchFor(loaded);
|
|
1206
1208
|
const audit = new FileAuditSink(loaded.auditPath);
|
|
1207
1209
|
const approvals = new FileApprovalStore(loaded.stateDir);
|
|
1208
1210
|
const alerter = opts.alerts === false ? void 0 : createAlerter({ policy: loaded.policy, approvalUrl: opts.approvalUrl, log: opts.log });
|
|
@@ -1213,6 +1215,7 @@ function createFileGuard(loaded, opts = {}) {
|
|
|
1213
1215
|
kill,
|
|
1214
1216
|
approvals,
|
|
1215
1217
|
env: opts.env ?? process.env,
|
|
1218
|
+
policyFile: basename(loaded.path),
|
|
1216
1219
|
approvalUrl: opts.approvalUrl,
|
|
1217
1220
|
onEvent: async (event) => {
|
|
1218
1221
|
await opts.onEvent?.(event);
|
|
@@ -1255,7 +1258,6 @@ export {
|
|
|
1255
1258
|
approvalSlackMessage,
|
|
1256
1259
|
haltSlackMessage,
|
|
1257
1260
|
createAlerter,
|
|
1258
|
-
DEFAULT_POLICY_FILE,
|
|
1259
1261
|
homeFrom,
|
|
1260
1262
|
defaultIo,
|
|
1261
1263
|
policyPathFrom,
|
|
@@ -1265,4 +1267,4 @@ export {
|
|
|
1265
1267
|
approvalUrlFor,
|
|
1266
1268
|
describeError
|
|
1267
1269
|
};
|
|
1268
|
-
//# sourceMappingURL=chunk-
|
|
1270
|
+
//# sourceMappingURL=chunk-4WTFMFRY.js.map
|