attestari 2.9.3 → 2.9.5
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/dist/hook.js +1211 -1084
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -2,72 +2,134 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
// src/main.ts
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
// src/adapters/claude-code.ts
|
|
8
|
-
var import_node_path6 = require("node:path");
|
|
5
|
+
var import_node_fs12 = require("node:fs");
|
|
9
6
|
|
|
10
|
-
// src/
|
|
11
|
-
var
|
|
7
|
+
// src/adapters/antigravity.ts
|
|
8
|
+
var import_node_fs3 = require("node:fs");
|
|
9
|
+
var import_node_path3 = require("node:path");
|
|
12
10
|
|
|
13
|
-
// src/
|
|
14
|
-
var import_node_crypto = require("node:crypto");
|
|
11
|
+
// src/denies.ts
|
|
15
12
|
var import_node_fs = require("node:fs");
|
|
16
|
-
var import_node_path2 = require("node:path");
|
|
17
|
-
|
|
18
|
-
// src/paths.ts
|
|
19
|
-
var import_node_os = require("node:os");
|
|
20
13
|
var import_node_path = require("node:path");
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
|
|
15
|
+
// src/decide.ts
|
|
16
|
+
var STALE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
17
|
+
var NOT_CHECKED = "not_checked";
|
|
18
|
+
var PACKAGES = /* @__PURE__ */ new Set(["npm", "pypi", "remote"]);
|
|
19
|
+
var BY_KEY = /* @__PURE__ */ new Set(["local", "skill"]);
|
|
20
|
+
var REPORTS = /* @__PURE__ */ new Set(["npm", "pypi"]);
|
|
21
|
+
var STRICTEST = { block: 0, warn: 1, allow: 2 };
|
|
22
|
+
function reducedKey(key) {
|
|
23
|
+
return Array.from(key, (c) => /^[A-Za-z0-9@/._-]$/.test(c) ? c : "_").join("").slice(0, 214);
|
|
23
24
|
}
|
|
24
|
-
function
|
|
25
|
-
return (0,
|
|
25
|
+
function suffixedKey(reduced, folder) {
|
|
26
|
+
return `${reduced.slice(0, 214 - folder.length - 1)}/${folder}`;
|
|
26
27
|
}
|
|
27
|
-
function
|
|
28
|
-
return
|
|
28
|
+
function oneLine(text) {
|
|
29
|
+
return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
30
|
+
}
|
|
31
|
+
function named(name, version) {
|
|
32
|
+
return version === null ? name : `${name} ${version}`;
|
|
33
|
+
}
|
|
34
|
+
function report(source, name, version) {
|
|
35
|
+
if (!REPORTS.has(source)) return "app.attestari.ai/tools";
|
|
36
|
+
const path = name.split("/").map(encodeURIComponent).join("/");
|
|
37
|
+
return `app.attestari.ai/tools/${source}/${path}${version === null ? "" : `/${encodeURIComponent(version)}`}`;
|
|
38
|
+
}
|
|
39
|
+
var BLOCKED = "Blocked by your organization's policy:";
|
|
40
|
+
var REASONS = /* @__PURE__ */ new Map([
|
|
41
|
+
["below_minimum_grade", (e, p) => `${BLOCKED} ${named(e.name, e.version)} is graded ${e.grade === null ? "" : `${e.grade}, `}below the minimum ${p.policy.minimum_grade}.`],
|
|
42
|
+
["not_examined", (e) => `${BLOCKED} ${named(e.name, e.version)} has not been examined by Attestari yet.`],
|
|
43
|
+
["under_re_examination", (e) => `${BLOCKED} ${named(e.name, e.version)} is being re-examined by Attestari.`],
|
|
44
|
+
["not_gradeable", (e) => `${BLOCKED} ${named(e.name, e.version)} was examined and could not be graded.`]
|
|
45
|
+
]);
|
|
46
|
+
function blocked(entry, policy) {
|
|
47
|
+
const words = REASONS.get(entry.rule);
|
|
48
|
+
const said = words ? words(entry, policy) : `${BLOCKED} ${named(entry.name, entry.version)} (${entry.rule}).`;
|
|
49
|
+
return `${said} ${report(entry.source, entry.name, entry.version)}`;
|
|
50
|
+
}
|
|
51
|
+
function matches(entry, resolved) {
|
|
52
|
+
if (entry.source !== resolved.source) return false;
|
|
53
|
+
if (BY_KEY.has(resolved.source)) {
|
|
54
|
+
const reduced = reducedKey(resolved.key);
|
|
55
|
+
return entry.key === reduced || resolved.folder !== void 0 && entry.key === suffixedKey(reduced, resolved.folder);
|
|
56
|
+
}
|
|
57
|
+
if (PACKAGES.has(resolved.source)) return entry.name === resolved.name && (resolved.version === null || entry.version === resolved.version);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function entryFor(policy, resolved) {
|
|
61
|
+
return policy.servers.filter((e) => matches(e, resolved)).sort((a, b) => STRICTEST[a.decision] - STRICTEST[b.decision])[0];
|
|
62
|
+
}
|
|
63
|
+
function deny(rule, reason) {
|
|
64
|
+
return { allow: false, reason: oneLine(reason), rule };
|
|
65
|
+
}
|
|
66
|
+
function decide(policy, ageMs, resolved, key = resolved?.key) {
|
|
67
|
+
const tool = resolved !== null ? named(resolved.name, resolved.version) : key !== void 0 ? `the server "${reducedKey(key)}"` : "this server";
|
|
68
|
+
if (policy === null) {
|
|
69
|
+
return deny(NOT_CHECKED, `Not checked by Attestari: this agent holds no Attestari policy, so ${tool} is blocked. Pair it at app.attestari.ai/agents`);
|
|
70
|
+
}
|
|
71
|
+
if (ageMs > STALE_MS) {
|
|
72
|
+
return deny(NOT_CHECKED, `Attestari blocked ${tool}: not checked in for 7 days: open app.attestari.ai`);
|
|
73
|
+
}
|
|
74
|
+
const entry = resolved === null ? void 0 : entryFor(policy, resolved);
|
|
75
|
+
if (entry === void 0) {
|
|
76
|
+
return deny(policy.unlisted.rule, `Not yet checked by Attestari: ${tool} is not in your organization's policy yet. app.attestari.ai/agents`);
|
|
77
|
+
}
|
|
78
|
+
if (entry.decision === "allow") return { allow: true };
|
|
79
|
+
if (entry.decision === "warn") return { allow: true, warn: { rule: entry.rule } };
|
|
80
|
+
return deny(entry.rule, blocked(entry, policy));
|
|
29
81
|
}
|
|
30
82
|
|
|
31
|
-
// src/
|
|
83
|
+
// src/denies.ts
|
|
32
84
|
function isObject(value) {
|
|
33
85
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
86
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
87
|
+
function denyOf(r) {
|
|
88
|
+
const launch = r.launch;
|
|
89
|
+
if (!isObject(launch)) return null;
|
|
90
|
+
const command = launch.command;
|
|
91
|
+
if (Array.isArray(command) && command.length > 0 && command.every((c) => typeof c === "string")) return { serverCommand: command };
|
|
92
|
+
if (typeof launch.url === "string" && launch.url !== "") return { serverUrl: launch.url };
|
|
93
|
+
return null;
|
|
37
94
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
95
|
+
function blockedForms(resolved, policy) {
|
|
96
|
+
const forms = resolved.filter((r) => entryFor(policy, r)?.decision === "block").map(denyOf).filter((e) => e !== null);
|
|
97
|
+
return [...new Set(forms.map((e) => JSON.stringify(e)))];
|
|
40
98
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
const unlisted = value.unlisted;
|
|
48
|
-
if (!isObject(unlisted) || unlisted.decision !== "block" || unlisted.rule !== "not_checked") return false;
|
|
49
|
-
return isString(value.etag) && value.etag !== "" && isString(value.compiled_at) && Array.isArray(value.not_enforced) && value.not_enforced.every(isString) && Array.isArray(value.servers) && value.servers.every(isEntry);
|
|
99
|
+
function indentOf(text) {
|
|
100
|
+
const first = /\n([ \t]+)\S/.exec(text);
|
|
101
|
+
if (first === null) return 2;
|
|
102
|
+
return first[1].startsWith(" ") ? " " : Math.min(first[1].length, 8);
|
|
50
103
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
104
|
+
function bytesOf(file) {
|
|
105
|
+
try {
|
|
106
|
+
return (0, import_node_fs.readFileSync)(file);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.code === "ENOENT") return null;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
56
111
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
60
|
-
return dir;
|
|
112
|
+
function same(a, b) {
|
|
113
|
+
return a === null || b === null ? a === b : a.equals(b);
|
|
61
114
|
}
|
|
62
|
-
|
|
63
|
-
|
|
115
|
+
var beforeRename = null;
|
|
116
|
+
function writeBeside(file, data, mode, read) {
|
|
117
|
+
const temporary = `${file}.${process.pid}.attestari.tmp`;
|
|
64
118
|
try {
|
|
65
|
-
(0, import_node_fs.writeFileSync)(temporary, data, { mode });
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
119
|
+
(0, import_node_fs.writeFileSync)(temporary, data, mode === void 0 ? {} : { mode });
|
|
120
|
+
if (mode !== void 0) {
|
|
121
|
+
try {
|
|
122
|
+
(0, import_node_fs.chmodSync)(temporary, mode);
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
beforeRename?.();
|
|
127
|
+
if (!same(bytesOf(file), read)) {
|
|
128
|
+
(0, import_node_fs.unlinkSync)(temporary);
|
|
129
|
+
return false;
|
|
69
130
|
}
|
|
70
131
|
(0, import_node_fs.renameSync)(temporary, file);
|
|
132
|
+
return true;
|
|
71
133
|
} catch (error) {
|
|
72
134
|
try {
|
|
73
135
|
(0, import_node_fs.unlinkSync)(temporary);
|
|
@@ -76,1025 +138,1082 @@ function writeAtomic(file, data, mode) {
|
|
|
76
138
|
throw error;
|
|
77
139
|
}
|
|
78
140
|
}
|
|
79
|
-
function
|
|
141
|
+
function formatLike(text, value) {
|
|
142
|
+
const bom = text !== null && text.startsWith("\uFEFF");
|
|
143
|
+
const crlf = text !== null && text.includes("\r\n");
|
|
144
|
+
const newline = text === null || /\n$/.test(text);
|
|
145
|
+
let out = JSON.stringify(value, null, text === null ? 2 : indentOf(text)) + (newline ? "\n" : "");
|
|
146
|
+
if (crlf) out = out.replace(/\n/g, "\r\n");
|
|
147
|
+
if (bom) out = `\uFEFF${out}`;
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function modeOf(file, text) {
|
|
80
151
|
try {
|
|
81
|
-
return (0, import_node_fs.
|
|
152
|
+
return text === null ? void 0 : (0, import_node_fs.statSync)(file).mode & 511;
|
|
82
153
|
} catch {
|
|
83
|
-
return
|
|
154
|
+
return void 0;
|
|
84
155
|
}
|
|
85
156
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
157
|
+
function say(line) {
|
|
158
|
+
process.stderr.write(`attestari-hook: ${line}
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
function writeDenies(agent, settingsPath, resolved, policy, written, takeBack = true, elsewhere = /* @__PURE__ */ new Set()) {
|
|
162
|
+
void agent;
|
|
163
|
+
const wanted = blockedForms(resolved, policy);
|
|
164
|
+
let read;
|
|
91
165
|
try {
|
|
92
|
-
|
|
166
|
+
read = bytesOf(settingsPath);
|
|
93
167
|
} catch {
|
|
94
|
-
|
|
168
|
+
say(`${settingsPath} could not be read; its deny entries are left as they were`);
|
|
169
|
+
return written;
|
|
95
170
|
}
|
|
96
|
-
|
|
97
|
-
|
|
171
|
+
const text = read === null ? null : read.toString("utf8");
|
|
172
|
+
if (text === null) {
|
|
173
|
+
if (wanted.length === 0) return [];
|
|
174
|
+
if (!(0, import_node_fs.existsSync)((0, import_node_path.dirname)(settingsPath))) {
|
|
175
|
+
say(`${(0, import_node_path.dirname)(settingsPath)} does not exist; no deny entry is written`);
|
|
176
|
+
return written;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const bom = text !== null && text.startsWith("\uFEFF");
|
|
180
|
+
let settings;
|
|
98
181
|
try {
|
|
99
|
-
|
|
182
|
+
settings = text === null ? {} : JSON.parse(bom ? text.slice(1) : text);
|
|
100
183
|
} catch {
|
|
101
|
-
|
|
184
|
+
settings = void 0;
|
|
102
185
|
}
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
writeAtomic(file, `${lines.slice(-MAX_EVENTS).join("\n")}
|
|
135
|
-
`, 384);
|
|
186
|
+
if (!isObject(settings) || settings.deniedMcpServers !== void 0 && !Array.isArray(settings.deniedMcpServers)) {
|
|
187
|
+
say(`${settingsPath} is not a settings file the hook can read; its deny entries are left as they were`);
|
|
188
|
+
return written;
|
|
189
|
+
}
|
|
190
|
+
const existing = settings.deniedMcpServers ?? [];
|
|
191
|
+
const mine = new Set(written);
|
|
192
|
+
const want = new Set(wanted);
|
|
193
|
+
const applies = (form) => want.has(form) || elsewhere.has(form);
|
|
194
|
+
const kept = existing.filter((e) => {
|
|
195
|
+
const form = JSON.stringify(e);
|
|
196
|
+
return !(takeBack && mine.has(form) && !applies(form));
|
|
197
|
+
});
|
|
198
|
+
const present = new Set(kept.map((e) => JSON.stringify(e)));
|
|
199
|
+
const added = wanted.filter((form) => !present.has(form));
|
|
200
|
+
const now = takeBack ? [...wanted.filter((form) => mine.has(form) || added.includes(form)), ...written.filter((form) => !want.has(form) && elsewhere.has(form))] : [...written, ...added.filter((form) => !mine.has(form))];
|
|
201
|
+
if (added.length === 0 && kept.length === existing.length) return now;
|
|
202
|
+
const next = { ...settings, deniedMcpServers: [...kept, ...added.map((form) => JSON.parse(form))] };
|
|
203
|
+
const crlf = text !== null && text.includes("\r\n");
|
|
204
|
+
const newline = text === null || /\n$/.test(text);
|
|
205
|
+
let out = JSON.stringify(next, null, text === null ? 2 : indentOf(text)) + (newline ? "\n" : "");
|
|
206
|
+
if (crlf) out = out.replace(/\n/g, "\r\n");
|
|
207
|
+
if (bom) out = `\uFEFF${out}`;
|
|
208
|
+
let mode;
|
|
209
|
+
try {
|
|
210
|
+
mode = text === null ? void 0 : (0, import_node_fs.statSync)(settingsPath).mode & 511;
|
|
211
|
+
} catch {
|
|
212
|
+
mode = void 0;
|
|
213
|
+
}
|
|
214
|
+
if (!writeBeside(settingsPath, out, mode, read)) {
|
|
215
|
+
say(`${settingsPath} changed while the hook wrote its deny entries; they are left as they were until the next check-in`);
|
|
216
|
+
return written;
|
|
136
217
|
}
|
|
218
|
+
return now;
|
|
137
219
|
}
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
220
|
+
function writeDenyList(file, get, put, wanted, written, takeBack = true, elsewhere = /* @__PURE__ */ new Set()) {
|
|
221
|
+
let read;
|
|
222
|
+
try {
|
|
223
|
+
read = bytesOf(file);
|
|
224
|
+
} catch {
|
|
225
|
+
say(`${file} could not be read; its deny entries are left as they were`);
|
|
226
|
+
return written;
|
|
227
|
+
}
|
|
228
|
+
const text = read === null ? null : read.toString("utf8");
|
|
229
|
+
if (text === null) {
|
|
230
|
+
if (wanted.length === 0) return [];
|
|
231
|
+
if (!(0, import_node_fs.existsSync)((0, import_node_path.dirname)(file))) {
|
|
232
|
+
say(`${(0, import_node_path.dirname)(file)} does not exist; no deny entry is written`);
|
|
233
|
+
return written;
|
|
145
234
|
}
|
|
146
235
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
function removeEvents(agent, ids) {
|
|
150
|
-
const file = (0, import_node_path2.join)(cacheDir(agent), "events.jsonl");
|
|
151
|
-
const text = readText(file);
|
|
152
|
-
if (text === null) return;
|
|
153
|
-
const sent2 = new Set(ids);
|
|
154
|
-
const keep = readEvents(agent).filter((event) => !sent2.has(event.id));
|
|
155
|
-
if (keep.length === 0) {
|
|
156
|
-
try {
|
|
157
|
-
(0, import_node_fs.unlinkSync)(file);
|
|
158
|
-
} catch {
|
|
159
|
-
}
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
writeAtomic(file, `${keep.map((event) => JSON.stringify(event)).join("\n")}
|
|
163
|
-
`, 384);
|
|
164
|
-
}
|
|
165
|
-
function folderId(cwd) {
|
|
166
|
-
return (0, import_node_crypto.createHash)("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
|
|
167
|
-
}
|
|
168
|
-
var FOLDER_FILE = /^([0-9a-f]{16})\.json$/;
|
|
169
|
-
function parseResolution(id, text, modifiedMs) {
|
|
170
|
-
if (text === null) return null;
|
|
171
|
-
let document;
|
|
236
|
+
const bom = text !== null && text.startsWith("\uFEFF");
|
|
237
|
+
let settings;
|
|
172
238
|
try {
|
|
173
|
-
|
|
239
|
+
settings = text === null ? {} : JSON.parse(bom ? text.slice(1) : text);
|
|
174
240
|
} catch {
|
|
175
|
-
|
|
241
|
+
settings = void 0;
|
|
176
242
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
243
|
+
const existing = isObject(settings) ? get(settings) : null;
|
|
244
|
+
if (!isObject(settings) || existing === null) {
|
|
245
|
+
say(`${file} is not a settings file the hook can read; its deny entries are left as they were`);
|
|
246
|
+
return written;
|
|
247
|
+
}
|
|
248
|
+
const current = existing ?? [];
|
|
249
|
+
const mine = new Set(written);
|
|
250
|
+
const want = new Set(wanted);
|
|
251
|
+
const applies = (form) => want.has(form) || elsewhere.has(form);
|
|
252
|
+
const kept = current.filter((e) => {
|
|
253
|
+
const form = JSON.stringify(e);
|
|
254
|
+
return !(takeBack && mine.has(form) && !applies(form));
|
|
255
|
+
});
|
|
256
|
+
const present = new Set(kept.map((e) => JSON.stringify(e)));
|
|
257
|
+
const added = wanted.filter((form) => !present.has(form));
|
|
258
|
+
const now = takeBack ? [...wanted.filter((form) => mine.has(form) || added.includes(form)), ...written.filter((form) => !want.has(form) && elsewhere.has(form))] : [...written, ...added.filter((form) => !mine.has(form))];
|
|
259
|
+
if (added.length === 0 && kept.length === current.length) return now;
|
|
260
|
+
const next = put(settings, [...kept, ...added.map((form) => JSON.parse(form))]);
|
|
261
|
+
const out = formatLike(text, next);
|
|
262
|
+
if (!writeBeside(file, bom ? `\uFEFF${out}` : out, modeOf(file, text), read)) {
|
|
263
|
+
say(`${file} changed while the hook wrote its deny entries; they are left as they were until the next check-in`);
|
|
264
|
+
return written;
|
|
265
|
+
}
|
|
266
|
+
return now;
|
|
190
267
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
268
|
+
|
|
269
|
+
// src/resolve.ts
|
|
270
|
+
var import_node_crypto = require("node:crypto");
|
|
271
|
+
var import_node_fs2 = require("node:fs");
|
|
272
|
+
var import_node_path2 = require("node:path");
|
|
273
|
+
var SERVERS_AT_MOST = 200;
|
|
274
|
+
var NPM_NAME = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/;
|
|
275
|
+
var PYPI_NAME = /^(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$/;
|
|
276
|
+
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
277
|
+
var SOURCES = /* @__PURE__ */ new Set(["npm", "pypi", "local", "remote"]);
|
|
278
|
+
var NPM_EXACT = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/;
|
|
279
|
+
var PYPI_EXACT = /^[0-9][0-9A-Za-z.+!-]*$/;
|
|
280
|
+
function isStrings(value) {
|
|
281
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
194
282
|
}
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
`, 384);
|
|
283
|
+
function runner(command) {
|
|
284
|
+
const file = command.split(/[\\/]/).pop() ?? command;
|
|
285
|
+
return file.toLowerCase().replace(/\.(cmd|exe|bat|ps1)$/, "");
|
|
199
286
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} catch {
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
287
|
+
var NPM_WANTED = /^[0-9A-Za-z.^~<>=|*+ -]*$/;
|
|
288
|
+
function npmSpec(spec) {
|
|
289
|
+
const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
|
|
290
|
+
const name = at > 0 ? spec.slice(0, at) : spec;
|
|
291
|
+
const wanted = at > 0 ? spec.slice(at + 1) : "";
|
|
292
|
+
if (!NPM_NAME.test(name) || name.length > 214 || !NPM_WANTED.test(wanted)) return null;
|
|
293
|
+
const exact = NPM_EXACT.exec(wanted);
|
|
294
|
+
return { source: "npm", name, version: exact ? exact[1] : null };
|
|
211
295
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for (const name of names) {
|
|
222
|
-
try {
|
|
223
|
-
const file = (0, import_node_path2.join)(dir, name);
|
|
224
|
-
if (now - (0, import_node_fs.statSync)(file).mtimeMs > maxAgeMs) {
|
|
225
|
-
(0, import_node_fs.unlinkSync)(file);
|
|
226
|
-
removed += 1;
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
296
|
+
var RUNNER_NO_VALUE = /* @__PURE__ */ new Set(["-y", "--yes", "-q", "--quiet"]);
|
|
297
|
+
function npmRunner(args) {
|
|
298
|
+
const packages = [];
|
|
299
|
+
let i = 0;
|
|
300
|
+
for (; i < args.length; i++) {
|
|
301
|
+
const a = args[i];
|
|
302
|
+
if (a === "--") {
|
|
303
|
+
i++;
|
|
304
|
+
break;
|
|
229
305
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let names;
|
|
235
|
-
try {
|
|
236
|
-
names = (0, import_node_fs.readdirSync)((0, import_node_path2.join)(cacheDir(agent), "resolved")).sort();
|
|
237
|
-
} catch {
|
|
238
|
-
return [];
|
|
239
|
-
}
|
|
240
|
-
const out = [];
|
|
241
|
-
for (const name of names) {
|
|
242
|
-
const id = FOLDER_FILE.exec(name)?.[1];
|
|
243
|
-
if (id === void 0) continue;
|
|
244
|
-
const file = (0, import_node_path2.join)(cacheDir(agent), "resolved", name);
|
|
245
|
-
let modifiedMs;
|
|
246
|
-
try {
|
|
247
|
-
modifiedMs = (0, import_node_fs.statSync)(file).mtimeMs;
|
|
248
|
-
} catch {
|
|
249
|
-
modifiedMs = void 0;
|
|
306
|
+
if (a === "-p" || a === "--package") {
|
|
307
|
+
if (i + 1 >= args.length) return null;
|
|
308
|
+
packages.push(args[++i]);
|
|
309
|
+
continue;
|
|
250
310
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return out;
|
|
255
|
-
}
|
|
256
|
-
function writeResolution(agent, cwd, servers, complete) {
|
|
257
|
-
const dir = (0, import_node_path2.join)(ensureDir(agent), "resolved");
|
|
258
|
-
const file = (0, import_node_path2.join)(dir, `${folderId(cwd)}.json`);
|
|
259
|
-
try {
|
|
260
|
-
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
261
|
-
const kept = servers.map(({ folder: _folder, ...r }) => r);
|
|
262
|
-
writeAtomic(file, `${JSON.stringify({ complete, cwd, servers: kept }, null, 2)}
|
|
263
|
-
`, 384);
|
|
264
|
-
} catch (error) {
|
|
265
|
-
try {
|
|
266
|
-
(0, import_node_fs.rmSync)(file, { force: true });
|
|
267
|
-
} catch {
|
|
311
|
+
if (a.startsWith("--package=")) {
|
|
312
|
+
packages.push(a.slice("--package=".length));
|
|
313
|
+
continue;
|
|
268
314
|
}
|
|
269
|
-
|
|
315
|
+
if (RUNNER_NO_VALUE.has(a)) continue;
|
|
316
|
+
if (a.startsWith("-")) return null;
|
|
317
|
+
break;
|
|
270
318
|
}
|
|
319
|
+
if (packages.length > 1) return null;
|
|
320
|
+
const spec = packages[0] ?? args[i];
|
|
321
|
+
return spec === void 0 ? null : npmSpec(spec);
|
|
271
322
|
}
|
|
272
|
-
function
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
323
|
+
function pypiSpec(spec) {
|
|
324
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?(.*)$/.exec(spec.trim());
|
|
325
|
+
if (match === null || !PYPI_NAME.test(match[1]) || match[1].length > 214) return null;
|
|
326
|
+
const rest = match[2].trim();
|
|
327
|
+
if (/[:/]/.test(rest)) return null;
|
|
328
|
+
const exact = /^(?:==|@)\s*([^\s,;<>=!~*@]+)$/.exec(rest);
|
|
329
|
+
if (rest !== "" && exact === null && !/^[<>=!~]/.test(rest)) return null;
|
|
330
|
+
return { source: "pypi", name: match[1], version: exact && PYPI_EXACT.test(exact[1]) ? exact[1] : null };
|
|
331
|
+
}
|
|
332
|
+
function pypiRunner(args, from) {
|
|
333
|
+
let chosen = null;
|
|
334
|
+
let i = 0;
|
|
335
|
+
for (; i < args.length; i++) {
|
|
336
|
+
const a = args[i];
|
|
337
|
+
if (a === from || a.startsWith(`${from}=`)) {
|
|
338
|
+
const value = a === from ? args[++i] : a.slice(from.length + 1);
|
|
339
|
+
if (value === void 0 || chosen !== null) return null;
|
|
340
|
+
chosen = value;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (RUNNER_NO_VALUE.has(a)) continue;
|
|
344
|
+
if (from === "--from" && (a === "--python" || a === "-p")) {
|
|
345
|
+
if (i + 1 >= args.length) return null;
|
|
346
|
+
i++;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (from === "--from" && a.startsWith("--python=")) continue;
|
|
350
|
+
if (a.startsWith("-")) return null;
|
|
351
|
+
break;
|
|
280
352
|
}
|
|
353
|
+
const spec = chosen ?? args[i];
|
|
354
|
+
return spec === void 0 ? null : pypiSpec(spec);
|
|
281
355
|
}
|
|
282
|
-
function
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
(0, import_node_fs.rmSync)((0, import_node_path2.join)(cacheDir(agent), "written", `${id}.json`), { force: true });
|
|
356
|
+
function unwrapped(command, args) {
|
|
357
|
+
if (runner(command) === "cmd" && args.length >= 2 && /^\/[ck]$/i.test(args[0])) return unwrapped(args[1], args.slice(2));
|
|
358
|
+
return [command, args];
|
|
286
359
|
}
|
|
287
|
-
function
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
360
|
+
function registryRunner(configuredCommand, configuredArgs) {
|
|
361
|
+
const [command, args] = unwrapped(configuredCommand, configuredArgs);
|
|
362
|
+
const run3 = runner(command);
|
|
363
|
+
if (run3 === "npx" || run3 === "bunx" || run3 === "pnpx" || run3 === "uvx") return true;
|
|
364
|
+
if ((run3 === "pnpm" || run3 === "yarn") && args[0] === "dlx") return true;
|
|
365
|
+
return run3 === "pipx" && args[0] === "run";
|
|
292
366
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
367
|
+
function packageOf(configuredCommand, configuredArgs) {
|
|
368
|
+
const [command, args] = unwrapped(configuredCommand, configuredArgs);
|
|
369
|
+
const run3 = runner(command);
|
|
370
|
+
if (run3 === "npx" || run3 === "bunx" || run3 === "pnpx") return npmRunner(args);
|
|
371
|
+
if ((run3 === "pnpm" || run3 === "yarn") && args[0] === "dlx") return npmRunner(args.slice(1));
|
|
372
|
+
if (run3 === "uvx") return pypiRunner(args, "--from");
|
|
373
|
+
if (run3 === "pipx" && args[0] === "run") return pypiRunner(args.slice(1), "--spec");
|
|
374
|
+
return null;
|
|
298
375
|
}
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
try {
|
|
302
|
-
(0, import_node_fs.unlinkSync)((0, import_node_path2.join)(cacheDir(agent), name));
|
|
303
|
-
} catch {
|
|
304
|
-
}
|
|
305
|
-
}
|
|
376
|
+
function secretLike(segment) {
|
|
377
|
+
return /[%@=+]/.test(segment) || segment.length >= 16 && /[0-9]/.test(segment) && /[A-Za-z]/.test(segment);
|
|
306
378
|
}
|
|
307
|
-
function
|
|
379
|
+
function remoteName(address) {
|
|
380
|
+
let url2;
|
|
308
381
|
try {
|
|
309
|
-
|
|
382
|
+
url2 = new URL(address.trim());
|
|
310
383
|
} catch {
|
|
384
|
+
return null;
|
|
311
385
|
}
|
|
386
|
+
if (!["http:", "https:", "ws:", "wss:"].includes(url2.protocol) || url2.hostname === "") return null;
|
|
387
|
+
const port = url2.port === "" ? "" : `:${url2.port}`;
|
|
388
|
+
const path = url2.pathname.replace(/\/+$/, "").split("/").map((segment) => secretLike(segment) ? "*" : segment).join("/");
|
|
389
|
+
return `${url2.hostname.toLowerCase()}${port}${path}`;
|
|
312
390
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
function readStamp(agent, name) {
|
|
318
|
-
const parts = (readText((0, import_node_path2.join)(cacheDir(agent), name)) ?? "").trim().split(/\s+/);
|
|
319
|
-
const at = Date.parse(parts[0]);
|
|
320
|
-
if (parts[0] === "" || Number.isNaN(at)) return null;
|
|
321
|
-
return { at, word: parts[1] ?? null };
|
|
391
|
+
function pathLike(arg) {
|
|
392
|
+
if (arg.startsWith("-") || /^\/[A-Za-z?]$/.test(arg)) return false;
|
|
393
|
+
if (/[=?#@]/.test(arg) || arg.replace(/^[A-Za-z]:(?=[\\/])/, "").includes(":")) return false;
|
|
394
|
+
return /[\\/]/.test(arg) || arg.startsWith("~") || /\.(?:[cm]?[jt]s|py)$/i.test(arg);
|
|
322
395
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
var SECRET_BYTES = 32;
|
|
329
|
-
function readSecret(agent) {
|
|
330
|
-
const file = (0, import_node_path2.join)(ensureDir(agent), "secret");
|
|
331
|
-
const held = () => {
|
|
332
|
-
try {
|
|
333
|
-
const bytes = (0, import_node_fs.readFileSync)(file);
|
|
334
|
-
return bytes.length === SECRET_BYTES ? bytes : null;
|
|
335
|
-
} catch {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
const existing = held();
|
|
340
|
-
if (existing !== null) return existing;
|
|
341
|
-
const fresh = (0, import_node_crypto.randomBytes)(SECRET_BYTES);
|
|
342
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
343
|
-
(0, import_node_fs.writeFileSync)(temporary, fresh, { mode: 384 });
|
|
344
|
-
try {
|
|
345
|
-
try {
|
|
346
|
-
(0, import_node_fs.chmodSync)(temporary, 384);
|
|
347
|
-
} catch {
|
|
348
|
-
}
|
|
349
|
-
try {
|
|
350
|
-
(0, import_node_fs.linkSync)(temporary, file);
|
|
351
|
-
return fresh;
|
|
352
|
-
} catch {
|
|
353
|
-
const other = held();
|
|
354
|
-
if (other !== null) return other;
|
|
355
|
-
(0, import_node_fs.renameSync)(temporary, file);
|
|
356
|
-
return fresh;
|
|
357
|
-
}
|
|
358
|
-
} finally {
|
|
359
|
-
try {
|
|
360
|
-
(0, import_node_fs.unlinkSync)(temporary);
|
|
361
|
-
} catch {
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// src/decide.ts
|
|
367
|
-
var STALE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
368
|
-
var NOT_CHECKED = "not_checked";
|
|
369
|
-
var PACKAGES = /* @__PURE__ */ new Set(["npm", "pypi", "remote"]);
|
|
370
|
-
var BY_KEY = /* @__PURE__ */ new Set(["local", "skill"]);
|
|
371
|
-
var REPORTS = /* @__PURE__ */ new Set(["npm", "pypi"]);
|
|
372
|
-
var STRICTEST = { block: 0, warn: 1, allow: 2 };
|
|
373
|
-
function reducedKey(key) {
|
|
374
|
-
return Array.from(key, (c) => /^[A-Za-z0-9@/._-]$/.test(c) ? c : "_").join("").slice(0, 214);
|
|
396
|
+
var FILE_NAME = /^[A-Za-z0-9._-]{1,200}$/;
|
|
397
|
+
function fileName(file) {
|
|
398
|
+
if (!FILE_NAME.test(file) || secretLike(file)) return false;
|
|
399
|
+
return !(file.length >= 16 && !file.includes(".") && /[a-z]/.test(file) && /[A-Z]/.test(file));
|
|
375
400
|
}
|
|
376
|
-
function
|
|
377
|
-
|
|
401
|
+
function fileAndFolder(path, home2) {
|
|
402
|
+
const expanded = path === "~" || path.startsWith("~/") || path.startsWith("~\\") ? `${home2}${path.slice(1)}` : path;
|
|
403
|
+
const parts = expanded.split(/[\\/]/);
|
|
404
|
+
const file = parts.pop() ?? "";
|
|
405
|
+
return { file, folder: parts.join("/") };
|
|
378
406
|
}
|
|
379
|
-
function
|
|
380
|
-
|
|
407
|
+
function labelOf(folder, home2) {
|
|
408
|
+
const slashed = folder.replace(/\\/g, "/");
|
|
409
|
+
const root = home2.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
410
|
+
const fold = (s) => process.platform === "win32" ? s.toLowerCase() : s;
|
|
411
|
+
if (root !== "" && (fold(slashed) === fold(root) || fold(slashed).startsWith(`${fold(root)}/`))) return `~${slashed.slice(root.length)}`;
|
|
412
|
+
return slashed;
|
|
381
413
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
414
|
+
function localName(configuredCommand, configuredArgs, home2, secret) {
|
|
415
|
+
const [command, args] = unwrapped(configuredCommand, configuredArgs);
|
|
416
|
+
const path = args.find(pathLike);
|
|
417
|
+
const fromArg = path === void 0 ? null : fileAndFolder(path, home2);
|
|
418
|
+
const chosen = fromArg !== null && fileName(fromArg.file) ? fromArg : fileAndFolder(command, home2);
|
|
419
|
+
const file = fileName(chosen.file) ? chosen.file : "local";
|
|
420
|
+
const hash = (0, import_node_crypto.createHmac)("sha256", secret).update(labelOf(chosen.folder, home2), "utf8").digest("hex").slice(0, 12);
|
|
421
|
+
return `${file}#${hash}`;
|
|
384
422
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
423
|
+
function resolve(key, entry, home2, secret) {
|
|
424
|
+
const type = entry.type;
|
|
425
|
+
const command = entry.command;
|
|
426
|
+
const remote = type === "http" || type === "sse" || type === "ws" || type === void 0 && typeof command !== "string";
|
|
427
|
+
if (remote) {
|
|
428
|
+
const url2 = entry.url;
|
|
429
|
+
if (typeof url2 !== "string") return null;
|
|
430
|
+
const name = remoteName(url2);
|
|
431
|
+
if (name === null) return null;
|
|
432
|
+
const launch2 = { url: url2 };
|
|
433
|
+
return { key, source: "remote", name, version: null, local: url2, launch: launch2 };
|
|
434
|
+
}
|
|
435
|
+
if (type !== void 0 && type !== "stdio") return null;
|
|
436
|
+
const args = entry.args === void 0 ? [] : entry.args;
|
|
437
|
+
if (typeof command !== "string" || command === "" || !isStrings(args)) return null;
|
|
438
|
+
const launch = { command: [command, ...args] };
|
|
439
|
+
const local = [command, ...args].join(" ");
|
|
440
|
+
const found = packageOf(command, args);
|
|
441
|
+
if (found !== null) return { key, ...found, local, launch };
|
|
442
|
+
if (registryRunner(command, args)) return null;
|
|
443
|
+
return { key, source: "local", name: localName(command, args, home2, secret), version: null, local, launch };
|
|
389
444
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
["below_minimum_grade", (e, p) => `${BLOCKED} ${named(e.name, e.version)} is graded ${e.grade === null ? "" : `${e.grade}, `}below the minimum ${p.policy.minimum_grade}.`],
|
|
393
|
-
["not_examined", (e) => `${BLOCKED} ${named(e.name, e.version)} has not been examined by Attestari yet.`],
|
|
394
|
-
["under_re_examination", (e) => `${BLOCKED} ${named(e.name, e.version)} is being re-examined by Attestari.`],
|
|
395
|
-
["not_gradeable", (e) => `${BLOCKED} ${named(e.name, e.version)} was examined and could not be graded.`]
|
|
396
|
-
]);
|
|
397
|
-
function blocked(entry, policy) {
|
|
398
|
-
const words = REASONS.get(entry.rule);
|
|
399
|
-
const said = words ? words(entry, policy) : `${BLOCKED} ${named(entry.name, entry.version)} (${entry.rule}).`;
|
|
400
|
-
return `${said} ${report(entry.source, entry.name, entry.version)}`;
|
|
445
|
+
function oneLineName(value) {
|
|
446
|
+
return value.length >= 1 && value.length <= 214 && !/\s/.test(value) && !CONTROL.test(value);
|
|
401
447
|
}
|
|
402
|
-
function
|
|
403
|
-
if (
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
return
|
|
448
|
+
function reportable(r) {
|
|
449
|
+
if (!SOURCES.has(r.source) || !oneLineName(r.name)) return false;
|
|
450
|
+
if (r.version !== null && (r.version.length < 1 || r.version.length > 64 || /\s/.test(r.version) || CONTROL.test(r.version))) return false;
|
|
451
|
+
if (r.source === "remote" && (/[?#@]/.test(r.name) || r.name.includes("://"))) return false;
|
|
452
|
+
if (r.source === "npm" && !NPM_NAME.test(r.name)) return false;
|
|
453
|
+
if (r.source === "pypi" && !PYPI_NAME.test(r.name)) return false;
|
|
454
|
+
if (r.source === "local" && !LOCAL_NAME.test(r.name)) return false;
|
|
455
|
+
return reducedKey(r.key) !== "";
|
|
410
456
|
}
|
|
411
|
-
|
|
412
|
-
|
|
457
|
+
var LOCAL_NAME = /^[A-Za-z0-9._-]{1,200}#[0-9a-f]{12}$/;
|
|
458
|
+
function byKey(a, b) {
|
|
459
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
413
460
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
461
|
+
function finished(servers) {
|
|
462
|
+
const counts = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const s of servers) counts.set(s.key, (counts.get(s.key) ?? 0) + 1);
|
|
464
|
+
return servers.filter((s) => counts.get(s.key) === 1).sort((a, b) => byKey(a.key, b.key)).slice(0, SERVERS_AT_MOST);
|
|
416
465
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
466
|
+
var sent = (r, key) => ({ key, source: r.source, name: r.name, version: r.version });
|
|
467
|
+
function reportedAll(folders) {
|
|
468
|
+
const byReduced = /* @__PURE__ */ new Map();
|
|
469
|
+
for (const { folder, servers } of folders) {
|
|
470
|
+
for (const r of servers.filter(reportable)) {
|
|
471
|
+
const key = reducedKey(r.key);
|
|
472
|
+
byReduced.set(key, [...byReduced.get(key) ?? [], { folder, r }]);
|
|
473
|
+
}
|
|
424
474
|
}
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
475
|
+
const out = [];
|
|
476
|
+
for (const [key, held] of byReduced) {
|
|
477
|
+
const [first] = held;
|
|
478
|
+
const same2 = held.every(({ r }) => r.source === first.r.source && r.name === first.r.name && r.version === first.r.version);
|
|
479
|
+
if (same2) out.push(sent(first.r, key));
|
|
480
|
+
else for (const { folder, r } of held) out.push(sent(r, suffixedKey(key, folder)));
|
|
428
481
|
}
|
|
429
|
-
|
|
430
|
-
if (entry.decision === "warn") return { allow: true, warn: { rule: entry.rule } };
|
|
431
|
-
return deny(entry.rule, blocked(entry, policy));
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// src/refresh.ts
|
|
435
|
-
var import_node_child_process = require("node:child_process");
|
|
436
|
-
var import_node_fs2 = require("node:fs");
|
|
437
|
-
var HOUR_MS = 60 * 60 * 1e3;
|
|
438
|
-
var DAY_MS = 24 * HOUR_MS;
|
|
439
|
-
var CHECKED_IN = "checked_in";
|
|
440
|
-
var LOOKUP_UNAVAILABLE = "lookup_unavailable";
|
|
441
|
-
var REFUSED = "refused";
|
|
442
|
-
function say(line) {
|
|
443
|
-
process.stderr.write(`attestari-hook: ${line.replace(/\s+/g, " ").trim()}
|
|
444
|
-
`);
|
|
482
|
+
return finished(out);
|
|
445
483
|
}
|
|
446
|
-
function
|
|
447
|
-
return
|
|
484
|
+
function isObject2(value) {
|
|
485
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
448
486
|
}
|
|
449
|
-
function
|
|
487
|
+
function readJson(file) {
|
|
488
|
+
let text;
|
|
450
489
|
try {
|
|
451
|
-
|
|
490
|
+
text = (0, import_node_fs2.readFileSync)(file, "utf8");
|
|
452
491
|
} catch (error) {
|
|
453
|
-
|
|
492
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
454
493
|
}
|
|
455
|
-
}
|
|
456
|
-
function usableFolder(folder) {
|
|
457
494
|
try {
|
|
458
|
-
|
|
495
|
+
const value = JSON.parse(text.replace(/^\ufeff/, ""));
|
|
496
|
+
return isObject2(value) ? value : null;
|
|
459
497
|
} catch {
|
|
460
|
-
return
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
function spawnRefresh(agent, cwd) {
|
|
464
|
-
const at = /* @__PURE__ */ new Date();
|
|
465
|
-
attempt("refresh_started_at was not written", () => writeStamp(agent, "refresh_started_at", at));
|
|
466
|
-
const folder = usableFolder(cwd);
|
|
467
|
-
let pid;
|
|
468
|
-
try {
|
|
469
|
-
const child = (0, import_node_child_process.spawn)(process.execPath, [__filename, agent, "refresh", ...cwd !== void 0 && cwd !== "" ? ["--cwd", cwd] : []], {
|
|
470
|
-
cwd: folder,
|
|
471
|
-
detached: true,
|
|
472
|
-
stdio: "ignore",
|
|
473
|
-
windowsHide: true
|
|
474
|
-
});
|
|
475
|
-
child.on("error", () => {
|
|
476
|
-
});
|
|
477
|
-
child.unref();
|
|
478
|
-
pid = child.pid;
|
|
479
|
-
} catch (error) {
|
|
480
|
-
say(`the refresh could not be started: ${message(error)}`);
|
|
481
|
-
}
|
|
482
|
-
if (pid === void 0) {
|
|
483
|
-
attempt("refresh_started_at was not removed", () => deleteStamp(agent, "refresh_started_at"));
|
|
484
|
-
return;
|
|
498
|
+
return null;
|
|
485
499
|
}
|
|
486
|
-
const started = pid;
|
|
487
|
-
attempt("refresh_started_at was not written", () => writeStamp(agent, "refresh_started_at", at, started));
|
|
488
500
|
}
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
if (
|
|
492
|
-
return
|
|
493
|
-
}
|
|
494
|
-
function refreshDue(agent, now = Date.now()) {
|
|
495
|
-
if (readToken(agent) === null) return false;
|
|
496
|
-
const checked = stampAge(agent, "checked_in_at", now);
|
|
497
|
-
if (checked !== null && checked < HOUR_MS) return false;
|
|
498
|
-
const started = stampAge(agent, "refresh_started_at", now);
|
|
499
|
-
if (started !== null && started < HOUR_MS) return false;
|
|
500
|
-
return !heldBack(agent, now, false);
|
|
501
|
+
function serversOf(holder) {
|
|
502
|
+
if (holder === null) return null;
|
|
503
|
+
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
504
|
+
return isObject2(holder.mcpServers) ? holder.mcpServers : null;
|
|
501
505
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
506
|
+
function claudeCodeScopes(home2, cwd) {
|
|
507
|
+
const claude = readJson((0, import_node_path2.join)(home2, ".claude.json"));
|
|
508
|
+
let project;
|
|
509
|
+
if (claude === null) project = null;
|
|
510
|
+
else if (claude !== void 0 && claude.projects !== void 0) {
|
|
511
|
+
const projects = claude.projects;
|
|
512
|
+
if (!isObject2(projects)) project = null;
|
|
513
|
+
else {
|
|
514
|
+
const trimmed = cwd.replace(/[\\/]+$/, "");
|
|
515
|
+
const forms = [cwd, cwd.replace(/\\/g, "/"), ...trimmed === "" ? [] : [trimmed, trimmed.replace(/\\/g, "/")]];
|
|
516
|
+
const held = forms.map((form) => projects[form]).find((value) => value !== void 0);
|
|
517
|
+
project = held === void 0 ? void 0 : isObject2(held) ? held : null;
|
|
518
|
+
}
|
|
510
519
|
}
|
|
520
|
+
return [
|
|
521
|
+
{ label: "~/.claude.json (this project)", servers: serversOf(project) },
|
|
522
|
+
{ label: ".mcp.json", servers: serversOf(readJson((0, import_node_path2.join)(cwd, ".mcp.json"))) },
|
|
523
|
+
{ label: "~/.claude.json", servers: serversOf(claude) }
|
|
524
|
+
];
|
|
511
525
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
526
|
+
function unnameable(entry) {
|
|
527
|
+
if (!isObject2(entry) || entry.type !== void 0 && entry.type !== "stdio") return false;
|
|
528
|
+
const args = entry.args === void 0 ? [] : entry.args;
|
|
529
|
+
return typeof entry.command === "string" && entry.command !== "" && isStrings(args) && registryRunner(entry.command, args);
|
|
517
530
|
}
|
|
518
|
-
function
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const resolved = held.length === 1 ? held[0] : null;
|
|
530
|
-
const cached = readPolicy(agent);
|
|
531
|
-
const verdict = decide(cached?.policy ?? null, cached?.ageMs ?? 0, resolved, parsed.shown ?? parsed.key);
|
|
532
|
-
const tool = resolved && { source: resolved.source, name: resolved.name, version: resolved.version };
|
|
533
|
-
if (!verdict.allow) {
|
|
534
|
-
if (tool) record(adapter, "blocked", verdict.rule, tool);
|
|
535
|
-
return adapter.deny(event, verdict);
|
|
531
|
+
function resolveConfiguration(home2, cwd, secret, scopes5 = claudeCodeScopes(home2, cwd)) {
|
|
532
|
+
const notes = [];
|
|
533
|
+
const entries = /* @__PURE__ */ new Map();
|
|
534
|
+
let complete = true;
|
|
535
|
+
for (const scope of scopes5) {
|
|
536
|
+
if (scope.servers === null) {
|
|
537
|
+
notes.push(`${scope.label} could not be read: its servers, and those of every scope below it, are not checked`);
|
|
538
|
+
complete = false;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
for (const [key, entry] of Object.entries(scope.servers ?? {})) if (!entries.has(key)) entries.set(key, entry);
|
|
536
542
|
}
|
|
537
|
-
|
|
538
|
-
|
|
543
|
+
const resolved = [];
|
|
544
|
+
for (const [key, entry] of entries) {
|
|
545
|
+
const shown = reducedKey(key) || "(empty)";
|
|
546
|
+
if (key.includes("__") || reducedKey(key) === "") {
|
|
547
|
+
notes.push(`the server "${shown}" is not checked: its key cannot name one server`);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const r = isObject2(entry) ? resolve(key, entry, home2, secret) : null;
|
|
551
|
+
if (r === null && unnameable(entry)) notes.push(`the server is not checked: its package could not be named: ${shown}`);
|
|
552
|
+
else if (r === null) notes.push(`the server "${shown}" is not checked: neither a command nor an address`);
|
|
553
|
+
else if (!reportable(r)) notes.push(`the server "${shown}" is not checked: its name is not one the device API takes`);
|
|
554
|
+
else resolved.push(r);
|
|
555
|
+
}
|
|
556
|
+
const counts = /* @__PURE__ */ new Map();
|
|
557
|
+
for (const r of resolved) counts.set(reducedKey(r.key), (counts.get(reducedKey(r.key)) ?? 0) + 1);
|
|
558
|
+
const unique = resolved.filter((r) => {
|
|
559
|
+
if (counts.get(reducedKey(r.key)) === 1) return true;
|
|
560
|
+
notes.push(`the server "${reducedKey(r.key)}" is not checked: two keys reduce to the same string`);
|
|
561
|
+
return false;
|
|
562
|
+
});
|
|
563
|
+
unique.sort((a, b) => byKey(reducedKey(a.key), reducedKey(b.key)));
|
|
564
|
+
if (unique.length > SERVERS_AT_MOST) {
|
|
565
|
+
notes.push(`${unique.length} servers: the first ${SERVERS_AT_MOST} by key are checked, the rest are not`);
|
|
566
|
+
}
|
|
567
|
+
return { resolved: unique.slice(0, SERVERS_AT_MOST), notes, complete };
|
|
539
568
|
}
|
|
540
569
|
|
|
541
|
-
// src/
|
|
542
|
-
var
|
|
543
|
-
var
|
|
544
|
-
|
|
570
|
+
// src/adapters/antigravity.ts
|
|
571
|
+
var MCP_TOOL = "call_mcp_tool";
|
|
572
|
+
var TIMEOUT_S = 10;
|
|
573
|
+
var COPY = (0, import_node_path3.join)(".attestari", "hook.js");
|
|
574
|
+
var GROUP = "attestari";
|
|
575
|
+
function isObject3(value) {
|
|
545
576
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
546
577
|
}
|
|
547
|
-
function
|
|
548
|
-
|
|
549
|
-
if (!isObject2(launch)) return null;
|
|
550
|
-
const command = launch.command;
|
|
551
|
-
if (Array.isArray(command) && command.length > 0 && command.every((c) => typeof c === "string")) return { serverCommand: command };
|
|
552
|
-
if (typeof launch.url === "string" && launch.url !== "") return { serverUrl: launch.url };
|
|
553
|
-
return null;
|
|
578
|
+
function isStrings2(value) {
|
|
579
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
554
580
|
}
|
|
555
|
-
function
|
|
556
|
-
|
|
557
|
-
|
|
581
|
+
function parse(_event, stdinText) {
|
|
582
|
+
let input;
|
|
583
|
+
try {
|
|
584
|
+
input = JSON.parse(stdinText);
|
|
585
|
+
} catch {
|
|
586
|
+
return { kind: "unreadable" };
|
|
587
|
+
}
|
|
588
|
+
if (!isObject3(input)) return { kind: "unreadable" };
|
|
589
|
+
const call = input.toolCall;
|
|
590
|
+
if (!isObject3(call) || typeof call.name !== "string") return { kind: "unreadable" };
|
|
591
|
+
if (call.name !== MCP_TOOL) return { kind: "other" };
|
|
592
|
+
const args = isObject3(call.args) ? call.args : null;
|
|
593
|
+
const key = args?.ServerName;
|
|
594
|
+
if (typeof key !== "string" || key === "") return { kind: "unreadable" };
|
|
595
|
+
const roots = isStrings2(input.workspacePaths) ? input.workspacePaths : [];
|
|
596
|
+
return {
|
|
597
|
+
kind: "mcp",
|
|
598
|
+
key,
|
|
599
|
+
plugin: false,
|
|
600
|
+
sessionId: typeof input.conversationId === "string" ? input.conversationId : void 0,
|
|
601
|
+
cwd: roots.length > 0 && roots[0] !== "" ? roots[0] : void 0
|
|
602
|
+
};
|
|
558
603
|
}
|
|
559
|
-
function
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return first[1].startsWith(" ") ? " " : Math.min(first[1].length, 8);
|
|
604
|
+
function deny2(_event, verdict) {
|
|
605
|
+
return { stdout: `${JSON.stringify({ decision: "deny", reason: verdict.reason })}
|
|
606
|
+
`, exitCode: 0 };
|
|
563
607
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
608
|
+
var allow = () => ({ stdout: `${JSON.stringify({ decision: "allow" })}
|
|
609
|
+
`, exitCode: 0 });
|
|
610
|
+
function antigravityEntry(entry) {
|
|
611
|
+
if (!isObject3(entry)) return entry;
|
|
612
|
+
if (entry.disabled === true) return void 0;
|
|
613
|
+
if (typeof entry.serverUrl !== "string") return entry;
|
|
614
|
+
const { command: _c, args: _a, serverUrl, type: _t, ...rest } = entry;
|
|
615
|
+
return { ...rest, type: "http", url: serverUrl };
|
|
616
|
+
}
|
|
617
|
+
function serversOf2(holder) {
|
|
618
|
+
if (holder === null) return null;
|
|
619
|
+
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
620
|
+
if (!isObject3(holder.mcpServers)) return null;
|
|
621
|
+
const kept = Object.entries(holder.mcpServers).map(([key, entry]) => [key, antigravityEntry(entry)]).filter(([, entry]) => entry !== void 0);
|
|
622
|
+
return Object.fromEntries(kept);
|
|
623
|
+
}
|
|
624
|
+
function scopes(home2, cwd) {
|
|
625
|
+
return [
|
|
626
|
+
{ label: ".agents/mcp_config.json", servers: serversOf2(readJson((0, import_node_path3.join)(cwd, ".agents", "mcp_config.json"))) },
|
|
627
|
+
{ label: "~/.gemini/config/mcp_config.json", servers: serversOf2(readJson((0, import_node_path3.join)(home2, ".gemini", "config", "mcp_config.json"))) }
|
|
628
|
+
];
|
|
629
|
+
}
|
|
630
|
+
function quoted(path) {
|
|
631
|
+
return /[\s"]/.test(path) ? `"${path.replace(/"/g, '\\"')}"` : path;
|
|
632
|
+
}
|
|
633
|
+
function hookCommands(hook2) {
|
|
634
|
+
return {
|
|
635
|
+
preToolUse: `node ${quoted(hook2)} antigravity pre-tool-use`,
|
|
636
|
+
sessionStart: `node ${quoted(hook2)} antigravity session-start`
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function install(home2, hookFile = __filename) {
|
|
640
|
+
const copy = (0, import_node_path3.join)(home2, COPY);
|
|
641
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(home2, ".attestari"), { recursive: true });
|
|
642
|
+
(0, import_node_fs3.copyFileSync)(hookFile, copy);
|
|
643
|
+
const commands = hookCommands(copy);
|
|
644
|
+
const file = (0, import_node_path3.join)(home2, ".gemini", "config", "hooks.json");
|
|
645
|
+
const read = bytesOf(file);
|
|
646
|
+
const text = read === null ? null : read.toString("utf8");
|
|
647
|
+
let groups = {};
|
|
648
|
+
if (text !== null) {
|
|
649
|
+
try {
|
|
650
|
+
groups = JSON.parse(text.replace(/^\ufeff/, ""));
|
|
651
|
+
} catch {
|
|
652
|
+
throw new Error(`${file} could not be read as Antigravity's hooks file; the hook was not installed`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (!isObject3(groups)) throw new Error(`${file} is not Antigravity's hooks file; the hook was not installed`);
|
|
656
|
+
const next = {
|
|
657
|
+
...groups,
|
|
658
|
+
[GROUP]: {
|
|
659
|
+
PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: commands.preToolUse, timeout: TIMEOUT_S }] }],
|
|
660
|
+
SessionStart: [{ type: "command", command: commands.sessionStart, timeout: TIMEOUT_S }]
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
const after = formatLike(text, next);
|
|
664
|
+
if (text !== after) {
|
|
665
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(home2, ".gemini", "config"), { recursive: true });
|
|
666
|
+
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
570
667
|
}
|
|
668
|
+
return { path: file, before: text, after };
|
|
571
669
|
}
|
|
572
|
-
|
|
573
|
-
|
|
670
|
+
var antigravity = {
|
|
671
|
+
name: "antigravity",
|
|
672
|
+
client: "Antigravity CLI",
|
|
673
|
+
events: ["PreToolUse", "SessionStart"],
|
|
674
|
+
parse,
|
|
675
|
+
deny: deny2,
|
|
676
|
+
allow,
|
|
677
|
+
scopes,
|
|
678
|
+
// Antigravity's own deny form is `permissions.deny` (`mcp(<server>/*)`) in ~/.gemini/antigravity-cli/settings.json:
|
|
679
|
+
// not written; the hook decides, and that file holds the CLI's other state.
|
|
680
|
+
install
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/adapters/claude-code.ts
|
|
684
|
+
var import_node_path7 = require("node:path");
|
|
685
|
+
|
|
686
|
+
// src/call.ts
|
|
687
|
+
var import_node_crypto3 = require("node:crypto");
|
|
688
|
+
|
|
689
|
+
// src/cache.ts
|
|
690
|
+
var import_node_crypto2 = require("node:crypto");
|
|
691
|
+
var import_node_fs4 = require("node:fs");
|
|
692
|
+
var import_node_path5 = require("node:path");
|
|
693
|
+
|
|
694
|
+
// src/paths.ts
|
|
695
|
+
var import_node_os = require("node:os");
|
|
696
|
+
var import_node_path4 = require("node:path");
|
|
697
|
+
function home() {
|
|
698
|
+
return process.env.HOME || process.env.USERPROFILE || (0, import_node_os.homedir)();
|
|
574
699
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
700
|
+
function attestariDir() {
|
|
701
|
+
return (0, import_node_path4.join)(home(), ".attestari");
|
|
702
|
+
}
|
|
703
|
+
function agentDir(agent) {
|
|
704
|
+
return (0, import_node_path4.join)(attestariDir(), agent);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/validate.ts
|
|
708
|
+
function isObject4(value) {
|
|
709
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
710
|
+
}
|
|
711
|
+
function isString(value) {
|
|
712
|
+
return typeof value === "string";
|
|
713
|
+
}
|
|
714
|
+
function isEntry(value) {
|
|
715
|
+
return isObject4(value) && isString(value.key) && isString(value.source) && isString(value.name) && (value.version === null || isString(value.version)) && (value.decision === "allow" || value.decision === "block" || value.decision === "warn") && isString(value.rule) && (value.grade === null || isString(value.grade));
|
|
716
|
+
}
|
|
717
|
+
function isCompiled(value) {
|
|
718
|
+
if (!isObject4(value) || value.format !== 1) return false;
|
|
719
|
+
const policy = value.policy;
|
|
720
|
+
if (!isObject4(policy) || typeof policy.version !== "number" || !isString(policy.minimum_grade) || typeof policy.block_not_examined !== "boolean" || typeof policy.block_not_gradeable !== "boolean") {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
const unlisted = value.unlisted;
|
|
724
|
+
if (!isObject4(unlisted) || unlisted.decision !== "block" || unlisted.rule !== "not_checked") return false;
|
|
725
|
+
return isString(value.etag) && value.etag !== "" && isString(value.compiled_at) && Array.isArray(value.not_enforced) && value.not_enforced.every(isString) && Array.isArray(value.servers) && value.servers.every(isEntry);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/cache.ts
|
|
729
|
+
var MAX_EVENTS = 1e3;
|
|
730
|
+
function cacheDir(agent) {
|
|
731
|
+
return agentDir(agent);
|
|
732
|
+
}
|
|
733
|
+
function ensureDir(agent) {
|
|
734
|
+
const dir = cacheDir(agent);
|
|
735
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
736
|
+
return dir;
|
|
737
|
+
}
|
|
738
|
+
function writeAtomic(file, data, mode) {
|
|
739
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
578
740
|
try {
|
|
579
|
-
(0,
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
} catch {
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
beforeRename?.();
|
|
587
|
-
if (!same(bytesOf(file), read)) {
|
|
588
|
-
(0, import_node_fs3.unlinkSync)(temporary);
|
|
589
|
-
return false;
|
|
741
|
+
(0, import_node_fs4.writeFileSync)(temporary, data, { mode });
|
|
742
|
+
try {
|
|
743
|
+
(0, import_node_fs4.chmodSync)(temporary, mode);
|
|
744
|
+
} catch {
|
|
590
745
|
}
|
|
591
|
-
(0,
|
|
592
|
-
return true;
|
|
746
|
+
(0, import_node_fs4.renameSync)(temporary, file);
|
|
593
747
|
} catch (error) {
|
|
594
748
|
try {
|
|
595
|
-
(0,
|
|
749
|
+
(0, import_node_fs4.unlinkSync)(temporary);
|
|
596
750
|
} catch {
|
|
597
751
|
}
|
|
598
752
|
throw error;
|
|
599
753
|
}
|
|
600
754
|
}
|
|
601
|
-
function
|
|
602
|
-
const bom = text !== null && text.startsWith("\uFEFF");
|
|
603
|
-
const crlf = text !== null && text.includes("\r\n");
|
|
604
|
-
const newline = text === null || /\n$/.test(text);
|
|
605
|
-
let out = JSON.stringify(value, null, text === null ? 2 : indentOf(text)) + (newline ? "\n" : "");
|
|
606
|
-
if (crlf) out = out.replace(/\n/g, "\r\n");
|
|
607
|
-
if (bom) out = `\uFEFF${out}`;
|
|
608
|
-
return out;
|
|
609
|
-
}
|
|
610
|
-
function modeOf(file, text) {
|
|
755
|
+
function readText(file) {
|
|
611
756
|
try {
|
|
612
|
-
return
|
|
757
|
+
return (0, import_node_fs4.readFileSync)(file, "utf8");
|
|
613
758
|
} catch {
|
|
614
|
-
return
|
|
759
|
+
return null;
|
|
615
760
|
}
|
|
616
761
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
void agent;
|
|
623
|
-
const wanted = blockedForms(resolved, policy);
|
|
624
|
-
let read;
|
|
762
|
+
function readPolicy(agent) {
|
|
763
|
+
const file = (0, import_node_path5.join)(cacheDir(agent), "policy.json");
|
|
764
|
+
const text = readText(file);
|
|
765
|
+
if (text === null) return null;
|
|
766
|
+
let document;
|
|
625
767
|
try {
|
|
626
|
-
|
|
768
|
+
document = JSON.parse(text);
|
|
627
769
|
} catch {
|
|
628
|
-
|
|
629
|
-
return written;
|
|
630
|
-
}
|
|
631
|
-
const text = read === null ? null : read.toString("utf8");
|
|
632
|
-
if (text === null) {
|
|
633
|
-
if (wanted.length === 0) return [];
|
|
634
|
-
if (!(0, import_node_fs3.existsSync)((0, import_node_path3.dirname)(settingsPath))) {
|
|
635
|
-
say2(`${(0, import_node_path3.dirname)(settingsPath)} does not exist; no deny entry is written`);
|
|
636
|
-
return written;
|
|
637
|
-
}
|
|
770
|
+
return null;
|
|
638
771
|
}
|
|
639
|
-
|
|
640
|
-
let
|
|
772
|
+
if (!isCompiled(document)) return null;
|
|
773
|
+
let modified;
|
|
641
774
|
try {
|
|
642
|
-
|
|
775
|
+
modified = (0, import_node_fs4.statSync)(file).mtimeMs;
|
|
643
776
|
} catch {
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
if (!isObject2(settings) || settings.deniedMcpServers !== void 0 && !Array.isArray(settings.deniedMcpServers)) {
|
|
647
|
-
say2(`${settingsPath} is not a settings file the hook can read; its deny entries are left as they were`);
|
|
648
|
-
return written;
|
|
777
|
+
return null;
|
|
649
778
|
}
|
|
650
|
-
|
|
651
|
-
const mine = new Set(written);
|
|
652
|
-
const want = new Set(wanted);
|
|
653
|
-
const applies = (form) => want.has(form) || elsewhere.has(form);
|
|
654
|
-
const kept = existing.filter((e) => {
|
|
655
|
-
const form = JSON.stringify(e);
|
|
656
|
-
return !(takeBack && mine.has(form) && !applies(form));
|
|
657
|
-
});
|
|
658
|
-
const present = new Set(kept.map((e) => JSON.stringify(e)));
|
|
659
|
-
const added = wanted.filter((form) => !present.has(form));
|
|
660
|
-
const now = takeBack ? [...wanted.filter((form) => mine.has(form) || added.includes(form)), ...written.filter((form) => !want.has(form) && elsewhere.has(form))] : [...written, ...added.filter((form) => !mine.has(form))];
|
|
661
|
-
if (added.length === 0 && kept.length === existing.length) return now;
|
|
662
|
-
const next = { ...settings, deniedMcpServers: [...kept, ...added.map((form) => JSON.parse(form))] };
|
|
663
|
-
const crlf = text !== null && text.includes("\r\n");
|
|
664
|
-
const newline = text === null || /\n$/.test(text);
|
|
665
|
-
let out = JSON.stringify(next, null, text === null ? 2 : indentOf(text)) + (newline ? "\n" : "");
|
|
666
|
-
if (crlf) out = out.replace(/\n/g, "\r\n");
|
|
667
|
-
if (bom) out = `\uFEFF${out}`;
|
|
668
|
-
let mode;
|
|
669
|
-
try {
|
|
670
|
-
mode = text === null ? void 0 : (0, import_node_fs3.statSync)(settingsPath).mode & 511;
|
|
671
|
-
} catch {
|
|
672
|
-
mode = void 0;
|
|
673
|
-
}
|
|
674
|
-
if (!writeBeside(settingsPath, out, mode, read)) {
|
|
675
|
-
say2(`${settingsPath} changed while the hook wrote its deny entries; they are left as they were until the next check-in`);
|
|
676
|
-
return written;
|
|
677
|
-
}
|
|
678
|
-
return now;
|
|
779
|
+
return { policy: document, ageMs: Math.max(0, Date.now() - modified) };
|
|
679
780
|
}
|
|
680
|
-
function
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
} catch {
|
|
685
|
-
say2(`${file} could not be read; its deny entries are left as they were`);
|
|
686
|
-
return written;
|
|
687
|
-
}
|
|
688
|
-
const text = read === null ? null : read.toString("utf8");
|
|
689
|
-
if (text === null) {
|
|
690
|
-
if (wanted.length === 0) return [];
|
|
691
|
-
if (!(0, import_node_fs3.existsSync)((0, import_node_path3.dirname)(file))) {
|
|
692
|
-
say2(`${(0, import_node_path3.dirname)(file)} does not exist; no deny entry is written`);
|
|
693
|
-
return written;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
const bom = text !== null && text.startsWith("\uFEFF");
|
|
697
|
-
let settings;
|
|
698
|
-
try {
|
|
699
|
-
settings = text === null ? {} : JSON.parse(bom ? text.slice(1) : text);
|
|
700
|
-
} catch {
|
|
701
|
-
settings = void 0;
|
|
702
|
-
}
|
|
703
|
-
const existing = isObject2(settings) ? get(settings) : null;
|
|
704
|
-
if (!isObject2(settings) || existing === null) {
|
|
705
|
-
say2(`${file} is not a settings file the hook can read; its deny entries are left as they were`);
|
|
706
|
-
return written;
|
|
707
|
-
}
|
|
708
|
-
const current = existing ?? [];
|
|
709
|
-
const mine = new Set(written);
|
|
710
|
-
const want = new Set(wanted);
|
|
711
|
-
const applies = (form) => want.has(form) || elsewhere.has(form);
|
|
712
|
-
const kept = current.filter((e) => {
|
|
713
|
-
const form = JSON.stringify(e);
|
|
714
|
-
return !(takeBack && mine.has(form) && !applies(form));
|
|
715
|
-
});
|
|
716
|
-
const present = new Set(kept.map((e) => JSON.stringify(e)));
|
|
717
|
-
const added = wanted.filter((form) => !present.has(form));
|
|
718
|
-
const now = takeBack ? [...wanted.filter((form) => mine.has(form) || added.includes(form)), ...written.filter((form) => !want.has(form) && elsewhere.has(form))] : [...written, ...added.filter((form) => !mine.has(form))];
|
|
719
|
-
if (added.length === 0 && kept.length === current.length) return now;
|
|
720
|
-
const next = put(settings, [...kept, ...added.map((form) => JSON.parse(form))]);
|
|
721
|
-
const out = formatLike(text, next);
|
|
722
|
-
if (!writeBeside(file, bom ? `\uFEFF${out}` : out, modeOf(file, text), read)) {
|
|
723
|
-
say2(`${file} changed while the hook wrote its deny entries; they are left as they were until the next check-in`);
|
|
724
|
-
return written;
|
|
725
|
-
}
|
|
726
|
-
return now;
|
|
781
|
+
function isResolved(value) {
|
|
782
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
783
|
+
const r = value;
|
|
784
|
+
return typeof r.key === "string" && typeof r.source === "string" && typeof r.name === "string" && (r.version === null || typeof r.version === "string") && typeof r.local === "string";
|
|
727
785
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
var DEFAULT_MARKETPLACE = "https://attestari.ai/plugins/marketplace.json";
|
|
734
|
-
var PLUGIN = "attestari@attestari";
|
|
735
|
-
var TIMEOUT_MS = 6e4;
|
|
736
|
-
var WINDOWS_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
737
|
-
var CMD_UNSAFE = /["%^&|<>!\r\n]/;
|
|
738
|
-
function marketplace() {
|
|
739
|
-
return process.env.ATTESTARI_MARKETPLACE || DEFAULT_MARKETPLACE;
|
|
786
|
+
function writePolicy(agent, policy, etag) {
|
|
787
|
+
const dir = ensureDir(agent);
|
|
788
|
+
writeAtomic((0, import_node_path5.join)(dir, "policy.json"), `${JSON.stringify(policy, null, 2)}
|
|
789
|
+
`, 384);
|
|
790
|
+
writeAtomic((0, import_node_path5.join)(dir, "policy.etag"), etag, 384);
|
|
740
791
|
}
|
|
741
|
-
function
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
["plugin", "install", PLUGIN, "--scope", "user"]
|
|
745
|
-
];
|
|
792
|
+
function readEtag(agent) {
|
|
793
|
+
const etag = readText((0, import_node_path5.join)(cacheDir(agent), "policy.etag"))?.trim();
|
|
794
|
+
return etag ? etag : null;
|
|
746
795
|
}
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
796
|
+
function readToken(agent) {
|
|
797
|
+
const text = readText((0, import_node_path5.join)(cacheDir(agent), "token"));
|
|
798
|
+
const token = text?.trim();
|
|
799
|
+
return token ? token : null;
|
|
800
|
+
}
|
|
801
|
+
function writeToken(agent, token) {
|
|
802
|
+
writeAtomic((0, import_node_path5.join)(ensureDir(agent), "token"), token, 384);
|
|
803
|
+
}
|
|
804
|
+
function appendEvent(agent, event) {
|
|
805
|
+
const file = (0, import_node_path5.join)(ensureDir(agent), "events.jsonl");
|
|
806
|
+
(0, import_node_fs4.appendFileSync)(file, `${JSON.stringify(event)}
|
|
807
|
+
`, { mode: 384 });
|
|
808
|
+
const lines = (readText(file) ?? "").split("\n").filter((line) => line !== "");
|
|
809
|
+
if (lines.length > MAX_EVENTS) {
|
|
810
|
+
writeAtomic(file, `${lines.slice(-MAX_EVENTS).join("\n")}
|
|
811
|
+
`, 384);
|
|
754
812
|
}
|
|
755
813
|
}
|
|
756
|
-
function
|
|
757
|
-
const
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
814
|
+
function readEvents(agent) {
|
|
815
|
+
const events = [];
|
|
816
|
+
for (const line of (readText((0, import_node_path5.join)(cacheDir(agent), "events.jsonl")) ?? "").split("\n")) {
|
|
817
|
+
if (line === "") continue;
|
|
818
|
+
try {
|
|
819
|
+
events.push(JSON.parse(line));
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
763
822
|
}
|
|
764
|
-
return
|
|
823
|
+
return events;
|
|
765
824
|
}
|
|
766
|
-
function
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
if (
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
825
|
+
function removeEvents(agent, ids) {
|
|
826
|
+
const file = (0, import_node_path5.join)(cacheDir(agent), "events.jsonl");
|
|
827
|
+
const text = readText(file);
|
|
828
|
+
if (text === null) return;
|
|
829
|
+
const sent2 = new Set(ids);
|
|
830
|
+
const keep = readEvents(agent).filter((event) => !sent2.has(event.id));
|
|
831
|
+
if (keep.length === 0) {
|
|
832
|
+
try {
|
|
833
|
+
(0, import_node_fs4.unlinkSync)(file);
|
|
834
|
+
} catch {
|
|
835
|
+
}
|
|
836
|
+
return;
|
|
776
837
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
if (result.error) return code ?? "did not run";
|
|
780
|
-
if (result.status === 0) return null;
|
|
781
|
-
return result.status !== null ? `exit ${result.status}` : result.signal ?? "did not run";
|
|
838
|
+
writeAtomic(file, `${keep.map((event) => JSON.stringify(event)).join("\n")}
|
|
839
|
+
`, 384);
|
|
782
840
|
}
|
|
783
|
-
function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
841
|
+
function folderId(cwd) {
|
|
842
|
+
return (0, import_node_crypto2.createHash)("sha256").update(cwd, "utf8").digest("hex").slice(0, 16);
|
|
843
|
+
}
|
|
844
|
+
var FOLDER_FILE = /^([0-9a-f]{16})\.json$/;
|
|
845
|
+
function parseResolution(id, text, modifiedMs) {
|
|
846
|
+
if (text === null) return null;
|
|
847
|
+
let document;
|
|
848
|
+
try {
|
|
849
|
+
document = JSON.parse(text);
|
|
850
|
+
} catch {
|
|
851
|
+
return null;
|
|
789
852
|
}
|
|
790
|
-
|
|
853
|
+
if (typeof document !== "object" || document === null || Array.isArray(document)) return null;
|
|
854
|
+
const { complete, servers, cwd } = document;
|
|
855
|
+
if (typeof complete !== "boolean" || !Array.isArray(servers) || !servers.every(isResolved)) return null;
|
|
856
|
+
const path = typeof cwd === "string" && cwd !== "" && folderId(cwd) === id ? { cwd } : {};
|
|
857
|
+
const when = modifiedMs === void 0 ? {} : { modifiedMs };
|
|
858
|
+
return { id, complete, servers: servers.map((r) => ({ ...r, folder: id.slice(0, 8) })), ...path, ...when };
|
|
791
859
|
}
|
|
792
|
-
function
|
|
793
|
-
|
|
794
|
-
const shown = /\s/.test(market) ? `"${market}"` : market;
|
|
795
|
-
return steps(shown).map((args) => ` claude ${args.join(" ")}
|
|
796
|
-
`).join("");
|
|
860
|
+
function readResolution(agent, cwd) {
|
|
861
|
+
return readResolutionOf(agent, folderId(cwd));
|
|
797
862
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
var import_node_fs5 = require("node:fs");
|
|
802
|
-
var import_node_path5 = require("node:path");
|
|
803
|
-
var SERVERS_AT_MOST = 200;
|
|
804
|
-
var NPM_NAME = /^(?:@[A-Za-z0-9~-][A-Za-z0-9._~-]*\/)?[A-Za-z0-9~-][A-Za-z0-9._~-]*$/;
|
|
805
|
-
var PYPI_NAME = /^(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9._-]*[A-Za-z0-9])$/;
|
|
806
|
-
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/;
|
|
807
|
-
var SOURCES = /* @__PURE__ */ new Set(["npm", "pypi", "local", "remote"]);
|
|
808
|
-
var NPM_EXACT = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/;
|
|
809
|
-
var PYPI_EXACT = /^[0-9][0-9A-Za-z.+!-]*$/;
|
|
810
|
-
function isStrings(value) {
|
|
811
|
-
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
863
|
+
function readResolutionOf(agent, id) {
|
|
864
|
+
if (!/^[0-9a-f]{16}$/.test(id)) return null;
|
|
865
|
+
return parseResolution(id, readText((0, import_node_path5.join)(cacheDir(agent), "resolved", `${id}.json`)));
|
|
812
866
|
}
|
|
813
|
-
function
|
|
814
|
-
const
|
|
815
|
-
return
|
|
867
|
+
function sessionFile(agent, sessionId) {
|
|
868
|
+
const name = /^[A-Za-z0-9_-]{1,128}$/.test(sessionId) ? sessionId : (0, import_node_crypto2.createHash)("sha256").update(sessionId, "utf8").digest("hex");
|
|
869
|
+
return (0, import_node_path5.join)(cacheDir(agent), "sessions", name);
|
|
816
870
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const wanted = at > 0 ? spec.slice(at + 1) : "";
|
|
822
|
-
if (!NPM_NAME.test(name) || name.length > 214 || !NPM_WANTED.test(wanted)) return null;
|
|
823
|
-
const exact = NPM_EXACT.exec(wanted);
|
|
824
|
-
return { source: "npm", name, version: exact ? exact[1] : null };
|
|
871
|
+
function writeSession(agent, sessionId, cwd) {
|
|
872
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path5.join)(ensureDir(agent), "sessions"), { recursive: true, mode: 448 });
|
|
873
|
+
writeAtomic(sessionFile(agent, sessionId), `${JSON.stringify({ folder: folderId(cwd), cwd })}
|
|
874
|
+
`, 384);
|
|
825
875
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (a === "-p" || a === "--package") {
|
|
837
|
-
if (i + 1 >= args.length) return null;
|
|
838
|
-
packages.push(args[++i]);
|
|
839
|
-
continue;
|
|
840
|
-
}
|
|
841
|
-
if (a.startsWith("--package=")) {
|
|
842
|
-
packages.push(a.slice("--package=".length));
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
|
-
if (RUNNER_NO_VALUE.has(a)) continue;
|
|
846
|
-
if (a.startsWith("-")) return null;
|
|
847
|
-
break;
|
|
876
|
+
function readSession(agent, sessionId) {
|
|
877
|
+
const text = readText(sessionFile(agent, sessionId));
|
|
878
|
+
if (text === null) return null;
|
|
879
|
+
try {
|
|
880
|
+
const value = JSON.parse(text);
|
|
881
|
+
if (typeof value !== "object" || value === null) return null;
|
|
882
|
+
const { folder, cwd } = value;
|
|
883
|
+
return typeof folder === "string" && typeof cwd === "string" && cwd !== "" && folderId(cwd) === folder ? { folder, cwd } : null;
|
|
884
|
+
} catch {
|
|
885
|
+
return null;
|
|
848
886
|
}
|
|
849
|
-
if (packages.length > 1) return null;
|
|
850
|
-
const spec = packages[0] ?? args[i];
|
|
851
|
-
return spec === void 0 ? null : npmSpec(spec);
|
|
852
|
-
}
|
|
853
|
-
function pypiSpec(spec) {
|
|
854
|
-
const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?(.*)$/.exec(spec.trim());
|
|
855
|
-
if (match === null || !PYPI_NAME.test(match[1]) || match[1].length > 214) return null;
|
|
856
|
-
const rest = match[2].trim();
|
|
857
|
-
if (/[:/]/.test(rest)) return null;
|
|
858
|
-
const exact = /^(?:==|@)\s*([^\s,;<>=!~*@]+)$/.exec(rest);
|
|
859
|
-
if (rest !== "" && exact === null && !/^[<>=!~]/.test(rest)) return null;
|
|
860
|
-
return { source: "pypi", name: match[1], version: exact && PYPI_EXACT.test(exact[1]) ? exact[1] : null };
|
|
861
887
|
}
|
|
862
|
-
function
|
|
863
|
-
|
|
864
|
-
let
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
888
|
+
function pruneSessions(agent, maxAgeMs, now = Date.now()) {
|
|
889
|
+
const dir = (0, import_node_path5.join)(cacheDir(agent), "sessions");
|
|
890
|
+
let names;
|
|
891
|
+
try {
|
|
892
|
+
names = (0, import_node_fs4.readdirSync)(dir);
|
|
893
|
+
} catch {
|
|
894
|
+
return 0;
|
|
895
|
+
}
|
|
896
|
+
let removed = 0;
|
|
897
|
+
for (const name of names) {
|
|
898
|
+
try {
|
|
899
|
+
const file = (0, import_node_path5.join)(dir, name);
|
|
900
|
+
if (now - (0, import_node_fs4.statSync)(file).mtimeMs > maxAgeMs) {
|
|
901
|
+
(0, import_node_fs4.unlinkSync)(file);
|
|
902
|
+
removed += 1;
|
|
903
|
+
}
|
|
904
|
+
} catch {
|
|
872
905
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
906
|
+
}
|
|
907
|
+
return removed;
|
|
908
|
+
}
|
|
909
|
+
function readResolutions(agent) {
|
|
910
|
+
let names;
|
|
911
|
+
try {
|
|
912
|
+
names = (0, import_node_fs4.readdirSync)((0, import_node_path5.join)(cacheDir(agent), "resolved")).sort();
|
|
913
|
+
} catch {
|
|
914
|
+
return [];
|
|
915
|
+
}
|
|
916
|
+
const out = [];
|
|
917
|
+
for (const name of names) {
|
|
918
|
+
const id = FOLDER_FILE.exec(name)?.[1];
|
|
919
|
+
if (id === void 0) continue;
|
|
920
|
+
const file = (0, import_node_path5.join)(cacheDir(agent), "resolved", name);
|
|
921
|
+
let modifiedMs;
|
|
922
|
+
try {
|
|
923
|
+
modifiedMs = (0, import_node_fs4.statSync)(file).mtimeMs;
|
|
924
|
+
} catch {
|
|
925
|
+
modifiedMs = void 0;
|
|
878
926
|
}
|
|
879
|
-
|
|
880
|
-
if (
|
|
881
|
-
break;
|
|
927
|
+
const resolution = parseResolution(id, readText(file), modifiedMs);
|
|
928
|
+
if (resolution !== null) out.push(resolution);
|
|
882
929
|
}
|
|
883
|
-
|
|
884
|
-
return spec === void 0 ? null : pypiSpec(spec);
|
|
930
|
+
return out;
|
|
885
931
|
}
|
|
886
|
-
function
|
|
887
|
-
|
|
888
|
-
|
|
932
|
+
function writeResolution(agent, cwd, servers, complete) {
|
|
933
|
+
const dir = (0, import_node_path5.join)(ensureDir(agent), "resolved");
|
|
934
|
+
const file = (0, import_node_path5.join)(dir, `${folderId(cwd)}.json`);
|
|
935
|
+
try {
|
|
936
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
937
|
+
const kept = servers.map(({ folder: _folder, ...r }) => r);
|
|
938
|
+
writeAtomic(file, `${JSON.stringify({ complete, cwd, servers: kept }, null, 2)}
|
|
939
|
+
`, 384);
|
|
940
|
+
} catch (error) {
|
|
941
|
+
try {
|
|
942
|
+
(0, import_node_fs4.rmSync)(file, { force: true });
|
|
943
|
+
} catch {
|
|
944
|
+
}
|
|
945
|
+
throw error;
|
|
946
|
+
}
|
|
889
947
|
}
|
|
890
|
-
function
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
948
|
+
function readWritten(agent, id) {
|
|
949
|
+
const text = readText((0, import_node_path5.join)(cacheDir(agent), "written", `${id}.json`));
|
|
950
|
+
if (text === null) return [];
|
|
951
|
+
try {
|
|
952
|
+
const value = JSON.parse(text);
|
|
953
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : [];
|
|
954
|
+
} catch {
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
896
957
|
}
|
|
897
|
-
function
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if ((run3 === "pnpm" || run3 === "yarn") && args[0] === "dlx") return npmRunner(args.slice(1));
|
|
902
|
-
if (run3 === "uvx") return pypiRunner(args, "--from");
|
|
903
|
-
if (run3 === "pipx" && args[0] === "run") return pypiRunner(args.slice(1), "--spec");
|
|
904
|
-
return null;
|
|
958
|
+
function dropResolution(agent, id) {
|
|
959
|
+
if (!/^[0-9a-f]{16}$/.test(id)) return;
|
|
960
|
+
(0, import_node_fs4.rmSync)((0, import_node_path5.join)(cacheDir(agent), "resolved", `${id}.json`), { force: true });
|
|
961
|
+
(0, import_node_fs4.rmSync)((0, import_node_path5.join)(cacheDir(agent), "written", `${id}.json`), { force: true });
|
|
905
962
|
}
|
|
906
|
-
function
|
|
907
|
-
|
|
963
|
+
function writeWritten(agent, id, written) {
|
|
964
|
+
const dir = (0, import_node_path5.join)(ensureDir(agent), "written");
|
|
965
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
966
|
+
writeAtomic((0, import_node_path5.join)(dir, `${id}.json`), `${JSON.stringify(written, null, 2)}
|
|
967
|
+
`, 384);
|
|
908
968
|
}
|
|
909
|
-
function
|
|
910
|
-
let url2;
|
|
969
|
+
function deleteToken(agent) {
|
|
911
970
|
try {
|
|
912
|
-
|
|
971
|
+
(0, import_node_fs4.unlinkSync)((0, import_node_path5.join)(cacheDir(agent), "token"));
|
|
913
972
|
} catch {
|
|
914
|
-
return null;
|
|
915
973
|
}
|
|
916
|
-
if (!["http:", "https:", "ws:", "wss:"].includes(url2.protocol) || url2.hostname === "") return null;
|
|
917
|
-
const port = url2.port === "" ? "" : `:${url2.port}`;
|
|
918
|
-
const path = url2.pathname.replace(/\/+$/, "").split("/").map((segment) => secretLike(segment) ? "*" : segment).join("/");
|
|
919
|
-
return `${url2.hostname.toLowerCase()}${port}${path}`;
|
|
920
974
|
}
|
|
921
|
-
function
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
975
|
+
function deletePolicy(agent) {
|
|
976
|
+
for (const name of ["policy.json", "policy.etag"]) {
|
|
977
|
+
try {
|
|
978
|
+
(0, import_node_fs4.unlinkSync)((0, import_node_path5.join)(cacheDir(agent), name));
|
|
979
|
+
} catch {
|
|
980
|
+
}
|
|
981
|
+
}
|
|
925
982
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
983
|
+
function deleteStamp(agent, name) {
|
|
984
|
+
try {
|
|
985
|
+
(0, import_node_fs4.unlinkSync)((0, import_node_path5.join)(cacheDir(agent), name));
|
|
986
|
+
} catch {
|
|
987
|
+
}
|
|
930
988
|
}
|
|
931
|
-
function
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const file = parts.pop() ?? "";
|
|
935
|
-
return { file, folder: parts.join("/") };
|
|
989
|
+
function writeStamp(agent, name, at = /* @__PURE__ */ new Date(), word) {
|
|
990
|
+
writeAtomic((0, import_node_path5.join)(ensureDir(agent), name), `${at.toISOString()}${word === void 0 ? "" : ` ${word}`}
|
|
991
|
+
`, 384);
|
|
936
992
|
}
|
|
937
|
-
function
|
|
938
|
-
const
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
return slashed;
|
|
993
|
+
function readStamp(agent, name) {
|
|
994
|
+
const parts = (readText((0, import_node_path5.join)(cacheDir(agent), name)) ?? "").trim().split(/\s+/);
|
|
995
|
+
const at = Date.parse(parts[0]);
|
|
996
|
+
if (parts[0] === "" || Number.isNaN(at)) return null;
|
|
997
|
+
return { at, word: parts[1] ?? null };
|
|
943
998
|
}
|
|
944
|
-
function
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const chosen = fromArg !== null && fileName(fromArg.file) ? fromArg : fileAndFolder(command, home2);
|
|
949
|
-
const file = fileName(chosen.file) ? chosen.file : "local";
|
|
950
|
-
const hash = (0, import_node_crypto3.createHmac)("sha256", secret).update(labelOf(chosen.folder, home2), "utf8").digest("hex").slice(0, 12);
|
|
951
|
-
return `${file}#${hash}`;
|
|
999
|
+
function stampAge(agent, name, now = Date.now()) {
|
|
1000
|
+
const stamp = readStamp(agent, name);
|
|
1001
|
+
if (stamp === null || stamp.at > now) return null;
|
|
1002
|
+
return now - stamp.at;
|
|
952
1003
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1004
|
+
var SECRET_BYTES = 32;
|
|
1005
|
+
function readSecret(agent) {
|
|
1006
|
+
const file = (0, import_node_path5.join)(ensureDir(agent), "secret");
|
|
1007
|
+
const held = () => {
|
|
1008
|
+
try {
|
|
1009
|
+
const bytes = (0, import_node_fs4.readFileSync)(file);
|
|
1010
|
+
return bytes.length === SECRET_BYTES ? bytes : null;
|
|
1011
|
+
} catch {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const existing = held();
|
|
1016
|
+
if (existing !== null) return existing;
|
|
1017
|
+
const fresh = (0, import_node_crypto2.randomBytes)(SECRET_BYTES);
|
|
1018
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
1019
|
+
(0, import_node_fs4.writeFileSync)(temporary, fresh, { mode: 384 });
|
|
1020
|
+
try {
|
|
1021
|
+
try {
|
|
1022
|
+
(0, import_node_fs4.chmodSync)(temporary, 384);
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
(0, import_node_fs4.linkSync)(temporary, file);
|
|
1027
|
+
return fresh;
|
|
1028
|
+
} catch {
|
|
1029
|
+
const other = held();
|
|
1030
|
+
if (other !== null) return other;
|
|
1031
|
+
(0, import_node_fs4.renameSync)(temporary, file);
|
|
1032
|
+
return fresh;
|
|
1033
|
+
}
|
|
1034
|
+
} finally {
|
|
1035
|
+
try {
|
|
1036
|
+
(0, import_node_fs4.unlinkSync)(temporary);
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
964
1039
|
}
|
|
965
|
-
if (type !== void 0 && type !== "stdio") return null;
|
|
966
|
-
const args = entry.args === void 0 ? [] : entry.args;
|
|
967
|
-
if (typeof command !== "string" || command === "" || !isStrings(args)) return null;
|
|
968
|
-
const launch = { command: [command, ...args] };
|
|
969
|
-
const local = [command, ...args].join(" ");
|
|
970
|
-
const found = packageOf(command, args);
|
|
971
|
-
if (found !== null) return { key, ...found, local, launch };
|
|
972
|
-
if (registryRunner(command, args)) return null;
|
|
973
|
-
return { key, source: "local", name: localName(command, args, home2, secret), version: null, local, launch };
|
|
974
1040
|
}
|
|
975
|
-
|
|
976
|
-
|
|
1041
|
+
|
|
1042
|
+
// src/refresh.ts
|
|
1043
|
+
var import_node_child_process = require("node:child_process");
|
|
1044
|
+
var import_node_fs5 = require("node:fs");
|
|
1045
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
1046
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
1047
|
+
var CHECKED_IN = "checked_in";
|
|
1048
|
+
var LOOKUP_UNAVAILABLE = "lookup_unavailable";
|
|
1049
|
+
var REFUSED = "refused";
|
|
1050
|
+
function say2(line) {
|
|
1051
|
+
process.stderr.write(`attestari-hook: ${line.replace(/\s+/g, " ").trim()}
|
|
1052
|
+
`);
|
|
977
1053
|
}
|
|
978
|
-
function
|
|
979
|
-
|
|
980
|
-
if (r.version !== null && (r.version.length < 1 || r.version.length > 64 || /\s/.test(r.version) || CONTROL.test(r.version))) return false;
|
|
981
|
-
if (r.source === "remote" && (/[?#@]/.test(r.name) || r.name.includes("://"))) return false;
|
|
982
|
-
if (r.source === "npm" && !NPM_NAME.test(r.name)) return false;
|
|
983
|
-
if (r.source === "pypi" && !PYPI_NAME.test(r.name)) return false;
|
|
984
|
-
if (r.source === "local" && !LOCAL_NAME.test(r.name)) return false;
|
|
985
|
-
return reducedKey(r.key) !== "";
|
|
1054
|
+
function message(error) {
|
|
1055
|
+
return error instanceof Error ? error.message : String(error);
|
|
986
1056
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1057
|
+
function attempt(what, fn) {
|
|
1058
|
+
try {
|
|
1059
|
+
fn();
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
say2(`${what}: ${message(error)}`);
|
|
1062
|
+
}
|
|
990
1063
|
}
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1064
|
+
function usableFolder(folder) {
|
|
1065
|
+
try {
|
|
1066
|
+
return typeof folder === "string" && folder !== "" && (0, import_node_fs5.statSync)(folder).isDirectory() ? folder : void 0;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return void 0;
|
|
1069
|
+
}
|
|
995
1070
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1071
|
+
function spawnRefresh(agent, cwd) {
|
|
1072
|
+
const at = /* @__PURE__ */ new Date();
|
|
1073
|
+
attempt("refresh_started_at was not written", () => writeStamp(agent, "refresh_started_at", at));
|
|
1074
|
+
const folder = usableFolder(cwd);
|
|
1075
|
+
let pid;
|
|
1076
|
+
try {
|
|
1077
|
+
const child = (0, import_node_child_process.spawn)(process.execPath, [__filename, agent, "refresh", ...cwd !== void 0 && cwd !== "" ? ["--cwd", cwd] : []], {
|
|
1078
|
+
cwd: folder,
|
|
1079
|
+
detached: true,
|
|
1080
|
+
stdio: "ignore",
|
|
1081
|
+
windowsHide: true
|
|
1082
|
+
});
|
|
1083
|
+
child.on("error", () => {
|
|
1084
|
+
});
|
|
1085
|
+
child.unref();
|
|
1086
|
+
pid = child.pid;
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
say2(`the refresh could not be started: ${message(error)}`);
|
|
1004
1089
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
const same2 = held.every(({ r }) => r.source === first.r.source && r.name === first.r.name && r.version === first.r.version);
|
|
1009
|
-
if (same2) out.push(sent(first.r, key));
|
|
1010
|
-
else for (const { folder, r } of held) out.push(sent(r, suffixedKey(key, folder)));
|
|
1090
|
+
if (pid === void 0) {
|
|
1091
|
+
attempt("refresh_started_at was not removed", () => deleteStamp(agent, "refresh_started_at"));
|
|
1092
|
+
return;
|
|
1011
1093
|
}
|
|
1012
|
-
|
|
1094
|
+
const started = pid;
|
|
1095
|
+
attempt("refresh_started_at was not written", () => writeStamp(agent, "refresh_started_at", at, started));
|
|
1013
1096
|
}
|
|
1014
|
-
function
|
|
1015
|
-
|
|
1097
|
+
function heldBack(agent, now, onlyAfterRefusal) {
|
|
1098
|
+
const next = readStamp(agent, "next_refresh_at");
|
|
1099
|
+
if (next === null || next.at <= now) return false;
|
|
1100
|
+
return !onlyAfterRefusal || next.word !== CHECKED_IN;
|
|
1016
1101
|
}
|
|
1017
|
-
function
|
|
1018
|
-
|
|
1102
|
+
function refreshDue(agent, now = Date.now()) {
|
|
1103
|
+
if (readToken(agent) === null) return false;
|
|
1104
|
+
const checked = stampAge(agent, "checked_in_at", now);
|
|
1105
|
+
if (checked !== null && checked < HOUR_MS) return false;
|
|
1106
|
+
const started = stampAge(agent, "refresh_started_at", now);
|
|
1107
|
+
if (started !== null && started < HOUR_MS) return false;
|
|
1108
|
+
return !heldBack(agent, now, false);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// src/call.ts
|
|
1112
|
+
var UNREADABLE = "Attestari could not read the tool call";
|
|
1113
|
+
var UNREADABLE_VERDICT = { allow: false, reason: UNREADABLE, rule: "unreadable" };
|
|
1114
|
+
function record(adapter, decision, rule, tool) {
|
|
1019
1115
|
try {
|
|
1020
|
-
|
|
1021
|
-
} catch
|
|
1022
|
-
return error.code === "ENOENT" ? void 0 : null;
|
|
1116
|
+
appendEvent(adapter.name, { id: (0, import_node_crypto3.randomUUID)(), at: (/* @__PURE__ */ new Date()).toISOString(), decision, rule, tool, agent_client: adapter.client });
|
|
1117
|
+
} catch {
|
|
1023
1118
|
}
|
|
1119
|
+
}
|
|
1120
|
+
function refreshWhenDue(agent, cwd) {
|
|
1024
1121
|
try {
|
|
1025
|
-
|
|
1026
|
-
return isObject3(value) ? value : null;
|
|
1122
|
+
if (refreshDue(agent)) spawnRefresh(agent, cwd);
|
|
1027
1123
|
} catch {
|
|
1028
|
-
return null;
|
|
1029
1124
|
}
|
|
1030
1125
|
}
|
|
1031
|
-
function
|
|
1032
|
-
|
|
1033
|
-
if (
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1126
|
+
function decideCall(adapter, event, stdinText) {
|
|
1127
|
+
const parsed = adapter.parse(event, stdinText);
|
|
1128
|
+
if (parsed.kind === "unreadable") return adapter.deny(event, UNREADABLE_VERDICT);
|
|
1129
|
+
if (parsed.kind === "other") return adapter.allow(event);
|
|
1130
|
+
const agent = adapter.name;
|
|
1131
|
+
const session = parsed.sessionId !== void 0 && parsed.sessionId !== "" ? readSession(agent, parsed.sessionId) : null;
|
|
1132
|
+
const cwd = session?.cwd ?? (parsed.cwd !== void 0 && parsed.cwd !== "" ? parsed.cwd : void 0);
|
|
1133
|
+
refreshWhenDue(agent, cwd);
|
|
1134
|
+
const resolution = session !== null ? readResolutionOf(agent, session.folder) : cwd !== void 0 ? readResolution(agent, cwd) : null;
|
|
1135
|
+
const same2 = adapter.matches ?? ((a, b) => a === b);
|
|
1136
|
+
const held = parsed.plugin ? [] : (resolution?.servers ?? []).filter((r) => same2(parsed.key, r.key));
|
|
1137
|
+
const resolved = held.length === 1 ? held[0] : null;
|
|
1138
|
+
const cached = readPolicy(agent);
|
|
1139
|
+
const verdict = decide(cached?.policy ?? null, cached?.ageMs ?? 0, resolved, parsed.shown ?? parsed.key);
|
|
1140
|
+
const tool = resolved && { source: resolved.source, name: resolved.name, version: resolved.version };
|
|
1141
|
+
if (!verdict.allow) {
|
|
1142
|
+
if (tool) record(adapter, "blocked", verdict.rule, tool);
|
|
1143
|
+
return adapter.deny(event, verdict);
|
|
1049
1144
|
}
|
|
1145
|
+
if (verdict.warn && tool) record(adapter, "warned", verdict.warn.rule, tool);
|
|
1146
|
+
return adapter.allow(event);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/plugin.ts
|
|
1150
|
+
var import_node_child_process2 = require("node:child_process");
|
|
1151
|
+
var import_node_fs6 = require("node:fs");
|
|
1152
|
+
var import_node_path6 = require("node:path");
|
|
1153
|
+
var DEFAULT_MARKETPLACE = "https://attestari.ai/plugins/marketplace.json";
|
|
1154
|
+
var PLUGIN = "attestari@attestari";
|
|
1155
|
+
var TIMEOUT_MS = 6e4;
|
|
1156
|
+
var WINDOWS_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
1157
|
+
var CMD_UNSAFE = /["%^&|<>!\r\n]/;
|
|
1158
|
+
function marketplace() {
|
|
1159
|
+
return process.env.ATTESTARI_MARKETPLACE || DEFAULT_MARKETPLACE;
|
|
1160
|
+
}
|
|
1161
|
+
function steps(market) {
|
|
1050
1162
|
return [
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
{ label: "~/.claude.json", servers: serversOf(claude) }
|
|
1163
|
+
["plugin", "marketplace", "add", market],
|
|
1164
|
+
["plugin", "install", PLUGIN, "--scope", "user"]
|
|
1054
1165
|
];
|
|
1055
1166
|
}
|
|
1056
|
-
function
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1167
|
+
function runnable(file, windows) {
|
|
1168
|
+
try {
|
|
1169
|
+
if (!(0, import_node_fs6.statSync)(file).isFile()) return false;
|
|
1170
|
+
if (!windows) (0, import_node_fs6.accessSync)(file, import_node_fs6.constants.X_OK);
|
|
1171
|
+
return true;
|
|
1172
|
+
} catch {
|
|
1173
|
+
return false;
|
|
1174
|
+
}
|
|
1060
1175
|
}
|
|
1061
|
-
function
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
complete = false;
|
|
1069
|
-
break;
|
|
1070
|
-
}
|
|
1071
|
-
for (const [key, entry] of Object.entries(scope.servers ?? {})) if (!entries.has(key)) entries.set(key, entry);
|
|
1176
|
+
function findOnPath(name, path = process.env.PATH ?? "", windows = process.platform === "win32") {
|
|
1177
|
+
const named2 = (process.env.PATHEXT ?? "").split(";").map((e) => e.trim().toLowerCase()).filter((e) => WINDOWS_EXTENSIONS.includes(e));
|
|
1178
|
+
const extensions = named2.length > 0 ? named2 : WINDOWS_EXTENSIONS;
|
|
1179
|
+
for (const folder of path.split(import_node_path6.delimiter)) {
|
|
1180
|
+
if (folder.trim() === "") continue;
|
|
1181
|
+
const candidates = windows ? extensions.map((e) => (0, import_node_path6.join)(folder, `${name}${e}`)) : [(0, import_node_path6.join)(folder, name)];
|
|
1182
|
+
for (const candidate of candidates) if (runnable(candidate, windows)) return candidate;
|
|
1072
1183
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
function run(file, args) {
|
|
1187
|
+
const options = { timeout: TIMEOUT_MS, stdio: "pipe", windowsHide: true };
|
|
1188
|
+
let result;
|
|
1189
|
+
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(file)) {
|
|
1190
|
+
const all = [file, ...args];
|
|
1191
|
+
if (all.some((a) => CMD_UNSAFE.test(a))) return "a character cmd.exe would read";
|
|
1192
|
+
const line = all.map((a) => `"${a}"`).join(" ");
|
|
1193
|
+
result = (0, import_node_child_process2.spawnSync)(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${line}"`], { ...options, windowsVerbatimArguments: true });
|
|
1194
|
+
} else {
|
|
1195
|
+
result = (0, import_node_child_process2.spawnSync)(file, args, options);
|
|
1085
1196
|
}
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1197
|
+
const code = result.error?.code;
|
|
1198
|
+
if (code === "ETIMEDOUT") return "timed out";
|
|
1199
|
+
if (result.error) return code ?? "did not run";
|
|
1200
|
+
if (result.status === 0) return null;
|
|
1201
|
+
return result.status !== null ? `exit ${result.status}` : result.signal ?? "did not run";
|
|
1202
|
+
}
|
|
1203
|
+
function installPlugin() {
|
|
1204
|
+
const claude = findOnPath("claude");
|
|
1205
|
+
if (claude === null) return { installed: false, why: "Claude Code (claude) is not on the PATH" };
|
|
1206
|
+
for (const args of steps(marketplace())) {
|
|
1207
|
+
const failed2 = run(claude, args);
|
|
1208
|
+
if (failed2 !== null) return { installed: false, why: `\`claude ${args.slice(0, args[1] === "marketplace" ? 3 : 2).join(" ")}\` failed (${failed2})` };
|
|
1096
1209
|
}
|
|
1097
|
-
return {
|
|
1210
|
+
return { installed: true };
|
|
1211
|
+
}
|
|
1212
|
+
function installCommands() {
|
|
1213
|
+
const market = marketplace().replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
1214
|
+
const shown = /\s/.test(market) ? `"${market}"` : market;
|
|
1215
|
+
return steps(shown).map((args) => ` claude ${args.join(" ")}
|
|
1216
|
+
`).join("");
|
|
1098
1217
|
}
|
|
1099
1218
|
|
|
1100
1219
|
// src/adapters/claude-code.ts
|
|
@@ -1106,19 +1225,19 @@ function serverKeyOf(toolName) {
|
|
|
1106
1225
|
if (end <= 0 || end + 2 >= rest.length) return null;
|
|
1107
1226
|
return rest.slice(0, end);
|
|
1108
1227
|
}
|
|
1109
|
-
function
|
|
1228
|
+
function isObject5(value) {
|
|
1110
1229
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1111
1230
|
}
|
|
1112
|
-
function
|
|
1231
|
+
function parse2(_event, stdinText) {
|
|
1113
1232
|
let input;
|
|
1114
1233
|
try {
|
|
1115
1234
|
input = JSON.parse(stdinText);
|
|
1116
1235
|
} catch {
|
|
1117
1236
|
return { kind: "unreadable" };
|
|
1118
1237
|
}
|
|
1119
|
-
if (!
|
|
1238
|
+
if (!isObject5(input) || typeof input.tool_name !== "string") return { kind: "unreadable" };
|
|
1120
1239
|
if (typeof input.cursor_version === "string") return { kind: "other" };
|
|
1121
|
-
const server =
|
|
1240
|
+
const server = isObject5(input.mcp_server) ? input.mcp_server : null;
|
|
1122
1241
|
const parsed = serverKeyOf(input.tool_name);
|
|
1123
1242
|
if (parsed === null) return server === null ? { kind: "other" } : { kind: "unreadable" };
|
|
1124
1243
|
const key = typeof server?.name === "string" && server.name !== "" ? server.name : parsed;
|
|
@@ -1131,32 +1250,32 @@ function parse(_event, stdinText) {
|
|
|
1131
1250
|
cwd: typeof input.cwd === "string" ? input.cwd : void 0
|
|
1132
1251
|
};
|
|
1133
1252
|
}
|
|
1134
|
-
function
|
|
1253
|
+
function deny3(_event, verdict) {
|
|
1135
1254
|
const output = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: verdict.reason } };
|
|
1136
1255
|
return { stdout: `${JSON.stringify(output)}
|
|
1137
1256
|
`, exitCode: 0 };
|
|
1138
1257
|
}
|
|
1139
|
-
var
|
|
1258
|
+
var allow2 = () => ({ stdout: "", exitCode: 0 });
|
|
1140
1259
|
var claudeCode = {
|
|
1141
1260
|
name: "claude-code",
|
|
1142
1261
|
client: "Claude Code",
|
|
1143
1262
|
events: ["PreToolUse", "SessionStart"],
|
|
1144
|
-
parse,
|
|
1145
|
-
deny:
|
|
1146
|
-
allow,
|
|
1263
|
+
parse: parse2,
|
|
1264
|
+
deny: deny3,
|
|
1265
|
+
allow: allow2,
|
|
1147
1266
|
scopes: claudeCodeScopes,
|
|
1148
1267
|
// Claude Code's own deny form: deniedMcpServers in ~/.claude/settings.json (denies.ts).
|
|
1149
|
-
writeDenies: (resolved, policy, written, takeBack, elsewhere) => writeDenies("claude-code", (0,
|
|
1268
|
+
writeDenies: (resolved, policy, written, takeBack, elsewhere) => writeDenies("claude-code", (0, import_node_path7.join)(home(), ".claude", "settings.json"), resolved, policy, written, takeBack, elsewhere),
|
|
1150
1269
|
// Installed by plugin (D8), not by a file the hook writes.
|
|
1151
1270
|
install: () => ({ commands: installCommands().split("\n").map((line) => line.trim()).filter((line) => line !== "") })
|
|
1152
1271
|
};
|
|
1153
1272
|
|
|
1154
1273
|
// src/adapters/codex.ts
|
|
1155
|
-
var
|
|
1156
|
-
var
|
|
1274
|
+
var import_node_fs7 = require("node:fs");
|
|
1275
|
+
var import_node_path8 = require("node:path");
|
|
1157
1276
|
|
|
1158
1277
|
// src/toml.ts
|
|
1159
|
-
function
|
|
1278
|
+
function isObject6(value) {
|
|
1160
1279
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1161
1280
|
}
|
|
1162
1281
|
var Fail = class extends Error {
|
|
@@ -1300,7 +1419,7 @@ function assign(table, parts, value) {
|
|
|
1300
1419
|
const made = {};
|
|
1301
1420
|
here[part] = made;
|
|
1302
1421
|
here = made;
|
|
1303
|
-
} else if (
|
|
1422
|
+
} else if (isObject6(next)) here = next;
|
|
1304
1423
|
else throw new Fail("key reused");
|
|
1305
1424
|
}
|
|
1306
1425
|
const last = parts[parts.length - 1];
|
|
@@ -1315,7 +1434,7 @@ function tableAt(root, parts) {
|
|
|
1315
1434
|
const made = {};
|
|
1316
1435
|
here[part] = made;
|
|
1317
1436
|
here = made;
|
|
1318
|
-
} else if (
|
|
1437
|
+
} else if (isObject6(next)) here = next;
|
|
1319
1438
|
else throw new Fail("table reused");
|
|
1320
1439
|
}
|
|
1321
1440
|
return here;
|
|
@@ -1368,23 +1487,23 @@ function parseToml(input) {
|
|
|
1368
1487
|
|
|
1369
1488
|
// src/adapters/codex.ts
|
|
1370
1489
|
var PREFIX2 = "mcp__";
|
|
1371
|
-
var
|
|
1372
|
-
var
|
|
1490
|
+
var TIMEOUT_S2 = 10;
|
|
1491
|
+
var COPY2 = (0, import_node_path8.join)(".attestari", "hook.js");
|
|
1373
1492
|
var STATUS = "Attestari is checking this MCP call";
|
|
1374
|
-
function
|
|
1493
|
+
function isObject7(value) {
|
|
1375
1494
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1376
1495
|
}
|
|
1377
1496
|
function sanitized(key) {
|
|
1378
1497
|
return key.replace(/[^A-Za-z0-9_]/g, "_");
|
|
1379
1498
|
}
|
|
1380
|
-
function
|
|
1499
|
+
function parse3(_event, stdinText) {
|
|
1381
1500
|
let input;
|
|
1382
1501
|
try {
|
|
1383
1502
|
input = JSON.parse(stdinText);
|
|
1384
1503
|
} catch {
|
|
1385
1504
|
return { kind: "unreadable" };
|
|
1386
1505
|
}
|
|
1387
|
-
if (!
|
|
1506
|
+
if (!isObject7(input) || typeof input.tool_name !== "string") return { kind: "unreadable" };
|
|
1388
1507
|
const name = input.tool_name;
|
|
1389
1508
|
if (!name.startsWith(PREFIX2)) return { kind: "other" };
|
|
1390
1509
|
const rest = name.slice(PREFIX2.length);
|
|
@@ -1402,16 +1521,16 @@ function matches2(parsedKey, configuredKey) {
|
|
|
1402
1521
|
const head = `${sanitized(configuredKey)}__`;
|
|
1403
1522
|
return parsedKey.startsWith(head) && parsedKey.length > head.length;
|
|
1404
1523
|
}
|
|
1405
|
-
function
|
|
1524
|
+
function deny4(_event, verdict) {
|
|
1406
1525
|
const output = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: verdict.reason } };
|
|
1407
1526
|
return { stdout: `${JSON.stringify(output)}
|
|
1408
1527
|
`, exitCode: 0 };
|
|
1409
1528
|
}
|
|
1410
|
-
var
|
|
1529
|
+
var allow3 = () => ({ stdout: "", exitCode: 0 });
|
|
1411
1530
|
function serversOfToml(file) {
|
|
1412
1531
|
let text;
|
|
1413
1532
|
try {
|
|
1414
|
-
text = (0,
|
|
1533
|
+
text = (0, import_node_fs7.readFileSync)(file, "utf8");
|
|
1415
1534
|
} catch (error) {
|
|
1416
1535
|
return error.code === "ENOENT" ? void 0 : null;
|
|
1417
1536
|
}
|
|
@@ -1419,26 +1538,26 @@ function serversOfToml(file) {
|
|
|
1419
1538
|
if (table === null) return null;
|
|
1420
1539
|
const servers = table.mcp_servers;
|
|
1421
1540
|
if (servers === void 0) return void 0;
|
|
1422
|
-
if (!
|
|
1541
|
+
if (!isObject7(servers)) return null;
|
|
1423
1542
|
const live = {};
|
|
1424
|
-
for (const [key, entry] of Object.entries(servers)) if (!(
|
|
1543
|
+
for (const [key, entry] of Object.entries(servers)) if (!(isObject7(entry) && entry.enabled === false)) live[key] = entry;
|
|
1425
1544
|
return live;
|
|
1426
1545
|
}
|
|
1427
|
-
function
|
|
1546
|
+
function scopes2(home2, cwd) {
|
|
1428
1547
|
return [
|
|
1429
|
-
{ label: "~/.codex/managed_config.toml", servers: serversOfToml((0,
|
|
1430
|
-
{ label: ".codex/config.toml", servers: serversOfToml((0,
|
|
1431
|
-
{ label: "~/.codex/config.toml", servers: serversOfToml((0,
|
|
1548
|
+
{ label: "~/.codex/managed_config.toml", servers: serversOfToml((0, import_node_path8.join)(home2, ".codex", "managed_config.toml")) },
|
|
1549
|
+
{ label: ".codex/config.toml", servers: serversOfToml((0, import_node_path8.join)(cwd, ".codex", "config.toml")) },
|
|
1550
|
+
{ label: "~/.codex/config.toml", servers: serversOfToml((0, import_node_path8.join)(home2, ".codex", "config.toml")) }
|
|
1432
1551
|
];
|
|
1433
1552
|
}
|
|
1434
|
-
function
|
|
1553
|
+
function quoted2(path) {
|
|
1435
1554
|
return /[\s"]/.test(path) ? `"${path.replace(/"/g, '\\"')}"` : path;
|
|
1436
1555
|
}
|
|
1437
1556
|
function powershellQuoted(path) {
|
|
1438
1557
|
return `'${path.replace(/'/g, "''")}'`;
|
|
1439
1558
|
}
|
|
1440
|
-
function
|
|
1441
|
-
const program = platform2 === "win32" ? `& ${powershellQuoted(node)} ${powershellQuoted(hook2)}` : `${
|
|
1559
|
+
function hookCommands2(node, hook2, platform2 = process.platform) {
|
|
1560
|
+
const program = platform2 === "win32" ? `& ${powershellQuoted(node)} ${powershellQuoted(hook2)}` : `${quoted2(node)} ${quoted2(hook2)}`;
|
|
1442
1561
|
return {
|
|
1443
1562
|
preToolUse: `${program} codex pre-tool-use`,
|
|
1444
1563
|
sessionStart: `${program} codex session-start`
|
|
@@ -1446,23 +1565,23 @@ function hookCommands(node, hook2, platform2 = process.platform) {
|
|
|
1446
1565
|
}
|
|
1447
1566
|
var MINE = /attestari.*\bcodex (pre-tool-use|session-start)\b/;
|
|
1448
1567
|
function isMine(handler) {
|
|
1449
|
-
return
|
|
1568
|
+
return isObject7(handler) && typeof handler.command === "string" && MINE.test(handler.command);
|
|
1450
1569
|
}
|
|
1451
1570
|
function withHandler(groups, matcher, handler) {
|
|
1452
1571
|
const current = Array.isArray(groups) ? groups : [];
|
|
1453
|
-
const cleaned = current.map((g) =>
|
|
1454
|
-
const at = cleaned.findIndex((g) =>
|
|
1572
|
+
const cleaned = current.map((g) => isObject7(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((h) => !isMine(h)) } : g).filter((g) => !(isObject7(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
1573
|
+
const at = cleaned.findIndex((g) => isObject7(g) && g.matcher === matcher && Array.isArray(g.hooks));
|
|
1455
1574
|
if (at === -1) return [...cleaned, matcher === void 0 ? { hooks: [handler] } : { matcher, hooks: [handler] }];
|
|
1456
1575
|
const group = cleaned[at];
|
|
1457
1576
|
cleaned[at] = { ...group, hooks: [...group.hooks, handler] };
|
|
1458
1577
|
return cleaned;
|
|
1459
1578
|
}
|
|
1460
|
-
function
|
|
1461
|
-
const copy = (0,
|
|
1462
|
-
(0,
|
|
1463
|
-
(0,
|
|
1464
|
-
const commands =
|
|
1465
|
-
const file = (0,
|
|
1579
|
+
function install2(home2, hookFile = __filename, node = process.execPath, platform2 = process.platform) {
|
|
1580
|
+
const copy = (0, import_node_path8.join)(home2, COPY2);
|
|
1581
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(home2, ".attestari"), { recursive: true });
|
|
1582
|
+
(0, import_node_fs7.copyFileSync)(hookFile, copy);
|
|
1583
|
+
const commands = hookCommands2(node, copy, platform2);
|
|
1584
|
+
const file = (0, import_node_path8.join)(home2, ".codex", "hooks.json");
|
|
1466
1585
|
const read = bytesOf(file);
|
|
1467
1586
|
const text = read === null ? null : read.toString("utf8");
|
|
1468
1587
|
let settings = {};
|
|
@@ -1473,21 +1592,21 @@ function install(home2, hookFile = __filename, node = process.execPath, platform
|
|
|
1473
1592
|
throw new Error(`${file} could not be read as Codex's hooks file; the hook was not installed`);
|
|
1474
1593
|
}
|
|
1475
1594
|
}
|
|
1476
|
-
if (!
|
|
1595
|
+
if (!isObject7(settings) || settings.hooks !== void 0 && !isObject7(settings.hooks)) {
|
|
1477
1596
|
throw new Error(`${file} is not Codex's hooks file; the hook was not installed`);
|
|
1478
1597
|
}
|
|
1479
|
-
const hooks =
|
|
1598
|
+
const hooks = isObject7(settings.hooks) ? settings.hooks : {};
|
|
1480
1599
|
const next = {
|
|
1481
1600
|
...settings,
|
|
1482
1601
|
hooks: {
|
|
1483
1602
|
...hooks,
|
|
1484
|
-
PreToolUse: withHandler(hooks.PreToolUse, "^mcp__", { type: "command", command: commands.preToolUse, timeout:
|
|
1485
|
-
SessionStart: withHandler(hooks.SessionStart, void 0, { type: "command", command: commands.sessionStart, timeout:
|
|
1603
|
+
PreToolUse: withHandler(hooks.PreToolUse, "^mcp__", { type: "command", command: commands.preToolUse, timeout: TIMEOUT_S2, statusMessage: STATUS }),
|
|
1604
|
+
SessionStart: withHandler(hooks.SessionStart, void 0, { type: "command", command: commands.sessionStart, timeout: TIMEOUT_S2 })
|
|
1486
1605
|
}
|
|
1487
1606
|
};
|
|
1488
1607
|
const after = formatLike(text, next);
|
|
1489
1608
|
if (text !== after) {
|
|
1490
|
-
(0,
|
|
1609
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.join)(home2, ".codex"), { recursive: true });
|
|
1491
1610
|
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
1492
1611
|
}
|
|
1493
1612
|
return { path: file, before: text, after };
|
|
@@ -1496,18 +1615,18 @@ var codex = {
|
|
|
1496
1615
|
name: "codex",
|
|
1497
1616
|
client: "Codex",
|
|
1498
1617
|
events: ["PreToolUse", "SessionStart"],
|
|
1499
|
-
parse:
|
|
1618
|
+
parse: parse3,
|
|
1500
1619
|
matches: matches2,
|
|
1501
|
-
deny:
|
|
1502
|
-
allow:
|
|
1503
|
-
scopes,
|
|
1620
|
+
deny: deny4,
|
|
1621
|
+
allow: allow3,
|
|
1622
|
+
scopes: scopes2,
|
|
1504
1623
|
// Codex's own deny form would be `enabled = false` in the person's server table: not written. The hook decides.
|
|
1505
|
-
install
|
|
1624
|
+
install: install2
|
|
1506
1625
|
};
|
|
1507
1626
|
|
|
1508
1627
|
// src/adapters/cursor.ts
|
|
1509
|
-
var
|
|
1510
|
-
var
|
|
1628
|
+
var import_node_fs8 = require("node:fs");
|
|
1629
|
+
var import_node_path9 = require("node:path");
|
|
1511
1630
|
|
|
1512
1631
|
// src/jsonc.ts
|
|
1513
1632
|
function stripJsonc(text) {
|
|
@@ -1550,62 +1669,67 @@ function parseJsonc(text) {
|
|
|
1550
1669
|
}
|
|
1551
1670
|
|
|
1552
1671
|
// src/adapters/cursor.ts
|
|
1553
|
-
var
|
|
1554
|
-
var
|
|
1555
|
-
function
|
|
1672
|
+
var TIMEOUT_S3 = 10;
|
|
1673
|
+
var COPY3 = (0, import_node_path9.join)(".attestari", "hook.js");
|
|
1674
|
+
function isObject8(value) {
|
|
1556
1675
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1557
1676
|
}
|
|
1558
|
-
function
|
|
1677
|
+
function isStrings3(value) {
|
|
1559
1678
|
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
1560
1679
|
}
|
|
1561
|
-
function
|
|
1680
|
+
function parse4(_event, stdinText) {
|
|
1562
1681
|
let input;
|
|
1563
1682
|
try {
|
|
1564
1683
|
input = JSON.parse(stdinText);
|
|
1565
1684
|
} catch {
|
|
1566
1685
|
return { kind: "unreadable" };
|
|
1567
1686
|
}
|
|
1568
|
-
if (!
|
|
1687
|
+
if (!isObject8(input)) return { kind: "unreadable" };
|
|
1569
1688
|
const key = input.mcp_server_name;
|
|
1570
1689
|
if (typeof key !== "string" || key === "") return { kind: "unreadable" };
|
|
1571
|
-
const roots =
|
|
1690
|
+
const roots = isStrings3(input.workspace_roots) ? input.workspace_roots : [];
|
|
1572
1691
|
return {
|
|
1573
1692
|
kind: "mcp",
|
|
1574
1693
|
key,
|
|
1575
1694
|
plugin: false,
|
|
1576
1695
|
sessionId: typeof input.conversation_id === "string" ? input.conversation_id : void 0,
|
|
1577
|
-
cwd: roots.length > 0 && roots[0] !== "" ? roots[0] : void 0
|
|
1696
|
+
cwd: roots.length > 0 && roots[0] !== "" ? rootPath(roots[0]) : void 0
|
|
1578
1697
|
};
|
|
1579
1698
|
}
|
|
1580
|
-
function
|
|
1699
|
+
function rootPath(root) {
|
|
1700
|
+
const drive = /^\/([A-Za-z]:)(\/.*)?$/.exec(root);
|
|
1701
|
+
if (drive === null) return root;
|
|
1702
|
+
return `${drive[1]}${(drive[2] ?? "\\").replace(/\//g, "\\")}`;
|
|
1703
|
+
}
|
|
1704
|
+
function deny5(_event, verdict) {
|
|
1581
1705
|
return { stdout: `${JSON.stringify({ permission: "deny", user_message: verdict.reason, agent_message: verdict.reason })}
|
|
1582
1706
|
`, exitCode: 0 };
|
|
1583
1707
|
}
|
|
1584
|
-
var
|
|
1708
|
+
var allow4 = () => ({ stdout: `${JSON.stringify({ permission: "allow" })}
|
|
1585
1709
|
`, exitCode: 0 });
|
|
1586
1710
|
function readCursorJson(file) {
|
|
1587
1711
|
let text;
|
|
1588
1712
|
try {
|
|
1589
|
-
text = (0,
|
|
1713
|
+
text = (0, import_node_fs8.readFileSync)(file, "utf8");
|
|
1590
1714
|
} catch (error) {
|
|
1591
1715
|
return error.code === "ENOENT" ? void 0 : null;
|
|
1592
1716
|
}
|
|
1593
1717
|
try {
|
|
1594
1718
|
const value = parseJsonc(text);
|
|
1595
|
-
return
|
|
1719
|
+
return isObject8(value) ? value : null;
|
|
1596
1720
|
} catch {
|
|
1597
1721
|
return null;
|
|
1598
1722
|
}
|
|
1599
1723
|
}
|
|
1600
|
-
function
|
|
1724
|
+
function serversOf3(holder) {
|
|
1601
1725
|
if (holder === null) return null;
|
|
1602
1726
|
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
1603
|
-
return
|
|
1727
|
+
return isObject8(holder.mcpServers) ? holder.mcpServers : null;
|
|
1604
1728
|
}
|
|
1605
|
-
function
|
|
1729
|
+
function scopes3(home2, cwd) {
|
|
1606
1730
|
return [
|
|
1607
|
-
{ label: ".cursor/mcp.json", servers:
|
|
1608
|
-
{ label: "~/.cursor/mcp.json", servers:
|
|
1731
|
+
{ label: ".cursor/mcp.json", servers: serversOf3(readCursorJson((0, import_node_path9.join)(cwd, ".cursor", "mcp.json"))) },
|
|
1732
|
+
{ label: "~/.cursor/mcp.json", servers: serversOf3(readCursorJson((0, import_node_path9.join)(home2, ".cursor", "mcp.json"))) }
|
|
1609
1733
|
];
|
|
1610
1734
|
}
|
|
1611
1735
|
var denyToken = (key) => `Mcp(${key}:*)`;
|
|
@@ -1615,42 +1739,42 @@ function cursorDenyForms(resolved, policy) {
|
|
|
1615
1739
|
}
|
|
1616
1740
|
function writeDenies2(resolved, policy, written, takeBack, elsewhere) {
|
|
1617
1741
|
return writeDenyList(
|
|
1618
|
-
(0,
|
|
1742
|
+
(0, import_node_path9.join)(home(), ".cursor", "cli-config.json"),
|
|
1619
1743
|
(settings) => {
|
|
1620
1744
|
const permissions = settings.permissions;
|
|
1621
1745
|
if (permissions === void 0) return void 0;
|
|
1622
|
-
if (!
|
|
1746
|
+
if (!isObject8(permissions)) return null;
|
|
1623
1747
|
if (permissions.deny === void 0) return void 0;
|
|
1624
1748
|
return Array.isArray(permissions.deny) ? permissions.deny : null;
|
|
1625
1749
|
},
|
|
1626
|
-
(settings, list) => ({ ...settings, permissions: { ...
|
|
1750
|
+
(settings, list) => ({ ...settings, permissions: { ...isObject8(settings.permissions) ? settings.permissions : {}, deny: list } }),
|
|
1627
1751
|
cursorDenyForms(resolved, policy),
|
|
1628
1752
|
written,
|
|
1629
1753
|
takeBack,
|
|
1630
1754
|
elsewhere
|
|
1631
1755
|
);
|
|
1632
1756
|
}
|
|
1633
|
-
function
|
|
1757
|
+
function quoted3(path) {
|
|
1634
1758
|
return /[\s"]/.test(path) ? `"${path.replace(/"/g, '\\"')}"` : path;
|
|
1635
1759
|
}
|
|
1636
|
-
function
|
|
1760
|
+
function hookCommands3(node, hook2) {
|
|
1637
1761
|
return {
|
|
1638
|
-
preToolUse: `${
|
|
1639
|
-
sessionStart: `${
|
|
1762
|
+
preToolUse: `${quoted3(node)} ${quoted3(hook2)} cursor pre-tool-use`,
|
|
1763
|
+
sessionStart: `${quoted3(node)} ${quoted3(hook2)} cursor session-start`
|
|
1640
1764
|
};
|
|
1641
1765
|
}
|
|
1642
1766
|
var MINE2 = /attestari.*\bcursor (pre-tool-use|session-start)\b/;
|
|
1643
1767
|
function withEntry(list, entry) {
|
|
1644
1768
|
const current = Array.isArray(list) ? list : [];
|
|
1645
|
-
const others = current.filter((e) => !(
|
|
1769
|
+
const others = current.filter((e) => !(isObject8(e) && typeof e.command === "string" && MINE2.test(e.command)));
|
|
1646
1770
|
return [...others, entry];
|
|
1647
1771
|
}
|
|
1648
|
-
function
|
|
1649
|
-
const copy = (0,
|
|
1650
|
-
(0,
|
|
1651
|
-
(0,
|
|
1652
|
-
const commands =
|
|
1653
|
-
const file = (0,
|
|
1772
|
+
function install3(home2, hookFile = __filename, node = process.execPath) {
|
|
1773
|
+
const copy = (0, import_node_path9.join)(home2, COPY3);
|
|
1774
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(home2, ".attestari"), { recursive: true });
|
|
1775
|
+
(0, import_node_fs8.copyFileSync)(hookFile, copy);
|
|
1776
|
+
const commands = hookCommands3(node, copy);
|
|
1777
|
+
const file = (0, import_node_path9.join)(home2, ".cursor", "hooks.json");
|
|
1654
1778
|
const read = bytesOf(file);
|
|
1655
1779
|
const text = read === null ? null : read.toString("utf8");
|
|
1656
1780
|
let settings = {};
|
|
@@ -1661,20 +1785,20 @@ function install2(home2, hookFile = __filename, node = process.execPath) {
|
|
|
1661
1785
|
throw new Error(`${file} could not be read as Cursor's hooks file; the hook was not installed`);
|
|
1662
1786
|
}
|
|
1663
1787
|
}
|
|
1664
|
-
if (!
|
|
1665
|
-
const hooks =
|
|
1788
|
+
if (!isObject8(settings)) throw new Error(`${file} is not Cursor's hooks file; the hook was not installed`);
|
|
1789
|
+
const hooks = isObject8(settings.hooks) ? settings.hooks : {};
|
|
1666
1790
|
const next = {
|
|
1667
1791
|
...settings,
|
|
1668
1792
|
version: typeof settings.version === "number" ? settings.version : 1,
|
|
1669
1793
|
hooks: {
|
|
1670
1794
|
...hooks,
|
|
1671
|
-
beforeMCPExecution: withEntry(hooks.beforeMCPExecution, { command: commands.preToolUse, timeout:
|
|
1672
|
-
sessionStart: withEntry(hooks.sessionStart, { command: commands.sessionStart, timeout:
|
|
1795
|
+
beforeMCPExecution: withEntry(hooks.beforeMCPExecution, { command: commands.preToolUse, timeout: TIMEOUT_S3, failClosed: true }),
|
|
1796
|
+
sessionStart: withEntry(hooks.sessionStart, { command: commands.sessionStart, timeout: TIMEOUT_S3 })
|
|
1673
1797
|
}
|
|
1674
1798
|
};
|
|
1675
1799
|
const after = formatLike(text, next);
|
|
1676
1800
|
if (text !== after) {
|
|
1677
|
-
(0,
|
|
1801
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path9.join)(home2, ".cursor"), { recursive: true });
|
|
1678
1802
|
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
1679
1803
|
}
|
|
1680
1804
|
return { path: file, before: text, after };
|
|
@@ -1683,34 +1807,34 @@ var cursor = {
|
|
|
1683
1807
|
name: "cursor",
|
|
1684
1808
|
client: "Cursor",
|
|
1685
1809
|
events: ["beforeMCPExecution", "sessionStart"],
|
|
1686
|
-
parse:
|
|
1687
|
-
deny:
|
|
1688
|
-
allow:
|
|
1689
|
-
scopes:
|
|
1810
|
+
parse: parse4,
|
|
1811
|
+
deny: deny5,
|
|
1812
|
+
allow: allow4,
|
|
1813
|
+
scopes: scopes3,
|
|
1690
1814
|
writeDenies: writeDenies2,
|
|
1691
|
-
install:
|
|
1815
|
+
install: install3
|
|
1692
1816
|
};
|
|
1693
1817
|
|
|
1694
1818
|
// src/adapters/gemini.ts
|
|
1695
|
-
var
|
|
1696
|
-
var
|
|
1819
|
+
var import_node_fs9 = require("node:fs");
|
|
1820
|
+
var import_node_path10 = require("node:path");
|
|
1697
1821
|
var PREFIX3 = "mcp_";
|
|
1698
1822
|
var TIMEOUT_MS2 = 1e4;
|
|
1699
|
-
var
|
|
1823
|
+
var COPY4 = (0, import_node_path10.join)(".attestari", "hook.js");
|
|
1700
1824
|
var HANDLER = "attestari";
|
|
1701
1825
|
var SESSION_HANDLER = "attestari-session";
|
|
1702
|
-
function
|
|
1826
|
+
function isObject9(value) {
|
|
1703
1827
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1704
1828
|
}
|
|
1705
|
-
function
|
|
1829
|
+
function parse5(_event, stdinText) {
|
|
1706
1830
|
let input;
|
|
1707
1831
|
try {
|
|
1708
1832
|
input = JSON.parse(stdinText);
|
|
1709
1833
|
} catch {
|
|
1710
1834
|
return { kind: "unreadable" };
|
|
1711
1835
|
}
|
|
1712
|
-
if (!
|
|
1713
|
-
const context =
|
|
1836
|
+
if (!isObject9(input) || typeof input.tool_name !== "string") return { kind: "unreadable" };
|
|
1837
|
+
const context = isObject9(input.mcp_context) ? input.mcp_context : null;
|
|
1714
1838
|
if (context === null) return input.tool_name.startsWith(PREFIX3) ? { kind: "unreadable" } : { kind: "other" };
|
|
1715
1839
|
const key = context.server_name;
|
|
1716
1840
|
if (typeof key !== "string" || key === "") return { kind: "unreadable" };
|
|
@@ -1722,35 +1846,35 @@ function parse4(_event, stdinText) {
|
|
|
1722
1846
|
cwd: typeof input.cwd === "string" ? input.cwd : void 0
|
|
1723
1847
|
};
|
|
1724
1848
|
}
|
|
1725
|
-
function
|
|
1849
|
+
function deny6(_event, verdict) {
|
|
1726
1850
|
return { stdout: `${JSON.stringify({ decision: "deny", reason: verdict.reason })}
|
|
1727
1851
|
`, exitCode: 0 };
|
|
1728
1852
|
}
|
|
1729
|
-
var
|
|
1853
|
+
var allow5 = () => ({ stdout: "", exitCode: 0 });
|
|
1730
1854
|
function geminiEntry(entry) {
|
|
1731
|
-
if (!
|
|
1855
|
+
if (!isObject9(entry)) return entry;
|
|
1732
1856
|
const address = typeof entry.httpUrl === "string" ? entry.httpUrl : typeof entry.url === "string" ? entry.url : null;
|
|
1733
1857
|
if (address === null) return entry;
|
|
1734
1858
|
const { command: _c, args: _a, httpUrl: _h, url: _u, type: _t, ...rest } = entry;
|
|
1735
1859
|
return { ...rest, type: "http", url: address };
|
|
1736
1860
|
}
|
|
1737
|
-
function
|
|
1861
|
+
function serversOf4(holder) {
|
|
1738
1862
|
if (holder === null) return null;
|
|
1739
1863
|
if (holder === void 0 || holder.mcpServers === void 0) return void 0;
|
|
1740
|
-
if (!
|
|
1864
|
+
if (!isObject9(holder.mcpServers)) return null;
|
|
1741
1865
|
return Object.fromEntries(Object.entries(holder.mcpServers).map(([key, entry]) => [key, geminiEntry(entry)]));
|
|
1742
1866
|
}
|
|
1743
1867
|
function systemSettingsPath(platform2 = process.platform, env = process.env) {
|
|
1744
1868
|
if (env.GEMINI_CLI_SYSTEM_SETTINGS_PATH) return env.GEMINI_CLI_SYSTEM_SETTINGS_PATH;
|
|
1745
|
-
if (platform2 === "win32") return
|
|
1869
|
+
if (platform2 === "win32") return import_node_path10.win32.join(env.ProgramData ?? "C:\\ProgramData", "gemini-cli", "settings.json");
|
|
1746
1870
|
if (platform2 === "darwin") return "/Library/Application Support/GeminiCli/settings.json";
|
|
1747
1871
|
return "/etc/gemini-cli/settings.json";
|
|
1748
1872
|
}
|
|
1749
|
-
function
|
|
1873
|
+
function scopes4(home2, cwd) {
|
|
1750
1874
|
return [
|
|
1751
|
-
{ label: "the system settings.json", servers:
|
|
1752
|
-
{ label: ".gemini/settings.json", servers:
|
|
1753
|
-
{ label: "~/.gemini/settings.json", servers:
|
|
1875
|
+
{ label: "the system settings.json", servers: serversOf4(readJson(systemSettingsPath())) },
|
|
1876
|
+
{ label: ".gemini/settings.json", servers: serversOf4(readJson((0, import_node_path10.join)(cwd, ".gemini", "settings.json"))) },
|
|
1877
|
+
{ label: "~/.gemini/settings.json", servers: serversOf4(readJson((0, import_node_path10.join)(home2, ".gemini", "settings.json"))) }
|
|
1754
1878
|
];
|
|
1755
1879
|
}
|
|
1756
1880
|
function geminiDenyForms(resolved, policy) {
|
|
@@ -1759,33 +1883,33 @@ function geminiDenyForms(resolved, policy) {
|
|
|
1759
1883
|
}
|
|
1760
1884
|
function writeDenies3(resolved, policy, written, takeBack, elsewhere) {
|
|
1761
1885
|
return writeDenyList(
|
|
1762
|
-
(0,
|
|
1886
|
+
(0, import_node_path10.join)(home(), ".gemini", "settings.json"),
|
|
1763
1887
|
(settings) => {
|
|
1764
1888
|
const mcp = settings.mcp;
|
|
1765
1889
|
if (mcp === void 0) return void 0;
|
|
1766
|
-
if (!
|
|
1890
|
+
if (!isObject9(mcp)) return null;
|
|
1767
1891
|
if (mcp.excluded === void 0) return void 0;
|
|
1768
1892
|
return Array.isArray(mcp.excluded) ? mcp.excluded : null;
|
|
1769
1893
|
},
|
|
1770
|
-
(settings, list) => ({ ...settings, mcp: { ...
|
|
1894
|
+
(settings, list) => ({ ...settings, mcp: { ...isObject9(settings.mcp) ? settings.mcp : {}, excluded: list } }),
|
|
1771
1895
|
geminiDenyForms(resolved, policy),
|
|
1772
1896
|
written,
|
|
1773
1897
|
takeBack,
|
|
1774
1898
|
elsewhere
|
|
1775
1899
|
);
|
|
1776
1900
|
}
|
|
1777
|
-
function
|
|
1901
|
+
function quoted4(path) {
|
|
1778
1902
|
return /[\s"]/.test(path) ? `"${path}"` : path;
|
|
1779
1903
|
}
|
|
1780
|
-
var LAUNCHER = (0,
|
|
1904
|
+
var LAUNCHER = (0, import_node_path10.join)(".attestari", "gemini-hook.cmd");
|
|
1781
1905
|
function geminiShell(platform2 = process.platform, env = process.env) {
|
|
1782
1906
|
if (platform2 !== "win32") return "sh";
|
|
1783
1907
|
const comSpec = (env.ComSpec ?? "").toLowerCase();
|
|
1784
1908
|
if (comSpec.endsWith("powershell.exe") || comSpec.endsWith("pwsh.exe")) return "powershell";
|
|
1785
1909
|
return findOnPath("pwsh", env.PATH ?? "", true) !== null ? "powershell" : "cmd";
|
|
1786
1910
|
}
|
|
1787
|
-
function
|
|
1788
|
-
const program = shell === "sh" || launcher === void 0 ? `${
|
|
1911
|
+
function hookCommands4(node, hook2, shell, launcher) {
|
|
1912
|
+
const program = shell === "sh" || launcher === void 0 ? `${quoted4(node)} ${quoted4(hook2)}` : quoted4(launcher);
|
|
1789
1913
|
const call = shell === "powershell" && program.startsWith('"') ? `& ${program}` : program;
|
|
1790
1914
|
return { preToolUse: `${call} gemini pre-tool-use`, sessionStart: `${call} gemini session-start` };
|
|
1791
1915
|
}
|
|
@@ -1794,29 +1918,29 @@ function launcherText(node, hook2) {
|
|
|
1794
1918
|
`;
|
|
1795
1919
|
}
|
|
1796
1920
|
function isMine2(handler) {
|
|
1797
|
-
return
|
|
1921
|
+
return isObject9(handler) && (handler.name === HANDLER || handler.name === SESSION_HANDLER || typeof handler.command === "string" && /attestari.*\bgemini (pre-tool-use|session-start)\b/.test(handler.command));
|
|
1798
1922
|
}
|
|
1799
1923
|
function withHandler2(groups, matcher, handler) {
|
|
1800
1924
|
const current = Array.isArray(groups) ? groups : [];
|
|
1801
|
-
const cleaned = current.map((g) =>
|
|
1802
|
-
const at = cleaned.findIndex((g) =>
|
|
1925
|
+
const cleaned = current.map((g) => isObject9(g) && Array.isArray(g.hooks) ? { ...g, hooks: g.hooks.filter((h) => !isMine2(h)) } : g).filter((g) => !(isObject9(g) && Array.isArray(g.hooks) && g.hooks.length === 0));
|
|
1926
|
+
const at = cleaned.findIndex((g) => isObject9(g) && g.matcher === matcher && Array.isArray(g.hooks));
|
|
1803
1927
|
if (at === -1) return [...cleaned, matcher === void 0 ? { hooks: [handler] } : { matcher, hooks: [handler] }];
|
|
1804
1928
|
const group = cleaned[at];
|
|
1805
1929
|
cleaned[at] = { ...group, hooks: [...group.hooks, handler] };
|
|
1806
1930
|
return cleaned;
|
|
1807
1931
|
}
|
|
1808
|
-
function
|
|
1809
|
-
const copy = (0,
|
|
1810
|
-
(0,
|
|
1811
|
-
(0,
|
|
1932
|
+
function install4(home2, hookFile = __filename, node = process.execPath) {
|
|
1933
|
+
const copy = (0, import_node_path10.join)(home2, COPY4);
|
|
1934
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path10.join)(home2, ".attestari"), { recursive: true });
|
|
1935
|
+
(0, import_node_fs9.copyFileSync)(hookFile, copy);
|
|
1812
1936
|
const shell = geminiShell();
|
|
1813
1937
|
let launcher;
|
|
1814
1938
|
if (shell !== "sh") {
|
|
1815
|
-
launcher = (0,
|
|
1816
|
-
(0,
|
|
1939
|
+
launcher = (0, import_node_path10.join)(home2, LAUNCHER);
|
|
1940
|
+
(0, import_node_fs9.writeFileSync)(launcher, launcherText(node, copy));
|
|
1817
1941
|
}
|
|
1818
|
-
const commands =
|
|
1819
|
-
const file = (0,
|
|
1942
|
+
const commands = hookCommands4(node, copy, shell, launcher);
|
|
1943
|
+
const file = (0, import_node_path10.join)(home2, ".gemini", "settings.json");
|
|
1820
1944
|
const read = bytesOf(file);
|
|
1821
1945
|
const text = read === null ? null : read.toString("utf8");
|
|
1822
1946
|
let settings = {};
|
|
@@ -1827,10 +1951,10 @@ function install3(home2, hookFile = __filename, node = process.execPath) {
|
|
|
1827
1951
|
throw new Error(`${file} could not be read as Gemini CLI's settings file; the hook was not installed`);
|
|
1828
1952
|
}
|
|
1829
1953
|
}
|
|
1830
|
-
if (!
|
|
1954
|
+
if (!isObject9(settings) || settings.hooks !== void 0 && !isObject9(settings.hooks)) {
|
|
1831
1955
|
throw new Error(`${file} is not Gemini CLI's settings file; the hook was not installed`);
|
|
1832
1956
|
}
|
|
1833
|
-
const hooks =
|
|
1957
|
+
const hooks = isObject9(settings.hooks) ? settings.hooks : {};
|
|
1834
1958
|
const next = {
|
|
1835
1959
|
...settings,
|
|
1836
1960
|
hooks: {
|
|
@@ -1841,7 +1965,7 @@ function install3(home2, hookFile = __filename, node = process.execPath) {
|
|
|
1841
1965
|
};
|
|
1842
1966
|
const after = formatLike(text, next);
|
|
1843
1967
|
if (text !== after) {
|
|
1844
|
-
(0,
|
|
1968
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path10.join)(home2, ".gemini"), { recursive: true });
|
|
1845
1969
|
if (!writeBeside(file, after, modeOf(file, text), read)) throw new Error(`${file} changed while the hook was installed; run the install again`);
|
|
1846
1970
|
}
|
|
1847
1971
|
return { path: file, before: text, after };
|
|
@@ -1850,12 +1974,12 @@ var gemini = {
|
|
|
1850
1974
|
name: "gemini",
|
|
1851
1975
|
client: "Gemini CLI",
|
|
1852
1976
|
events: ["BeforeTool", "SessionStart"],
|
|
1853
|
-
parse:
|
|
1854
|
-
deny:
|
|
1855
|
-
allow:
|
|
1856
|
-
scopes:
|
|
1977
|
+
parse: parse5,
|
|
1978
|
+
deny: deny6,
|
|
1979
|
+
allow: allow5,
|
|
1980
|
+
scopes: scopes4,
|
|
1857
1981
|
writeDenies: writeDenies3,
|
|
1858
|
-
install:
|
|
1982
|
+
install: install4
|
|
1859
1983
|
};
|
|
1860
1984
|
|
|
1861
1985
|
// src/adapters/registry.ts
|
|
@@ -1863,14 +1987,15 @@ var ADAPTERS = /* @__PURE__ */ new Map([
|
|
|
1863
1987
|
[claudeCode.name, claudeCode],
|
|
1864
1988
|
[cursor.name, cursor],
|
|
1865
1989
|
[codex.name, codex],
|
|
1866
|
-
[gemini.name, gemini]
|
|
1990
|
+
[gemini.name, gemini],
|
|
1991
|
+
[antigravity.name, antigravity]
|
|
1867
1992
|
]);
|
|
1868
1993
|
function adapterFor(name) {
|
|
1869
1994
|
return ADAPTERS.get(name) ?? null;
|
|
1870
1995
|
}
|
|
1871
1996
|
|
|
1872
1997
|
// src/session.ts
|
|
1873
|
-
var
|
|
1998
|
+
var import_node_fs10 = require("node:fs");
|
|
1874
1999
|
var import_node_os2 = require("node:os");
|
|
1875
2000
|
|
|
1876
2001
|
// src/api.ts
|
|
@@ -1878,7 +2003,7 @@ var import_node_http = require("node:http");
|
|
|
1878
2003
|
var import_node_https = require("node:https");
|
|
1879
2004
|
|
|
1880
2005
|
// src/version.ts
|
|
1881
|
-
var HOOK_VERSION = "2.9.
|
|
2006
|
+
var HOOK_VERSION = "2.9.5";
|
|
1882
2007
|
|
|
1883
2008
|
// src/api.ts
|
|
1884
2009
|
var timeoutMs = 1e4;
|
|
@@ -2011,8 +2136,8 @@ var FOLDER_MS = 30 * DAY_MS;
|
|
|
2011
2136
|
var PAIRING_LINE = "Attestari: this agent is not protected. Run `npx -y attestari@2.9 pair --code <code from app.attestari.ai/agents>`";
|
|
2012
2137
|
var INVALID_CODE = "That code is not valid or has expired. Make a new one at app.attestari.ai/agents.";
|
|
2013
2138
|
var TOKEN_REFUSED = "Pairing did not complete: Attestari refused the new token. Make a new code at app.attestari.ai/agents.";
|
|
2014
|
-
var PAIR_USAGE = "usage: attestari-hook pair --code <code> [--agent claude-code|cursor|codex|gemini]";
|
|
2015
|
-
var INSTALL_USAGE = "usage: attestari-hook install --agent claude-code|cursor|codex|gemini";
|
|
2139
|
+
var PAIR_USAGE = "usage: attestari-hook pair --code <code> [--agent claude-code|cursor|codex|gemini|antigravity]";
|
|
2140
|
+
var INSTALL_USAGE = "usage: attestari-hook install --agent claude-code|cursor|codex|gemini|antigravity";
|
|
2016
2141
|
var ACCOUNTS = "https://accounts.attestari.ai";
|
|
2017
2142
|
var LOOPBACK = /* @__PURE__ */ new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
2018
2143
|
function accountsBase() {
|
|
@@ -2025,16 +2150,16 @@ function accountsBase() {
|
|
|
2025
2150
|
host = null;
|
|
2026
2151
|
}
|
|
2027
2152
|
if (host !== null && LOOPBACK.has(host)) return wanted;
|
|
2028
|
-
|
|
2153
|
+
say2("ATTESTARI_ACCOUNTS_URL is ignored: only a loopback address (127.0.0.1, ::1, localhost) may replace accounts.attestari.ai");
|
|
2029
2154
|
return ACCOUNTS;
|
|
2030
2155
|
}
|
|
2031
|
-
function
|
|
2156
|
+
function isObject10(value) {
|
|
2032
2157
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2033
2158
|
}
|
|
2034
2159
|
function resolveAndKeep(adapter, cwd) {
|
|
2035
2160
|
const agent = adapter.name;
|
|
2036
2161
|
const { resolved, notes, complete } = resolveConfiguration(home(), cwd, readSecret(agent), adapter.scopes(home(), cwd));
|
|
2037
|
-
for (const note of notes)
|
|
2162
|
+
for (const note of notes) say2(note);
|
|
2038
2163
|
attempt("the resolution was not written", () => writeResolution(agent, cwd, resolved, complete));
|
|
2039
2164
|
}
|
|
2040
2165
|
function sessionStart(stdinText, adapter) {
|
|
@@ -2046,9 +2171,9 @@ function sessionStart(stdinText, adapter) {
|
|
|
2046
2171
|
input = JSON.parse(stdinText);
|
|
2047
2172
|
} catch {
|
|
2048
2173
|
}
|
|
2049
|
-
const cwd =
|
|
2174
|
+
const cwd = isObject10(input) && typeof input.cwd === "string" && input.cwd !== "" ? input.cwd : process.cwd();
|
|
2050
2175
|
attempt("the configuration was not resolved", () => resolveAndKeep(adapter, cwd));
|
|
2051
|
-
if (
|
|
2176
|
+
if (isObject10(input) && typeof input.session_id === "string" && input.session_id !== "") {
|
|
2052
2177
|
const sessionId = input.session_id;
|
|
2053
2178
|
attempt("the session was not bound to its folder", () => writeSession(agent, sessionId, cwd));
|
|
2054
2179
|
}
|
|
@@ -2068,7 +2193,7 @@ async function refresh(adapter, cwd) {
|
|
|
2068
2193
|
try {
|
|
2069
2194
|
resolveAndKeep(adapter, cwd);
|
|
2070
2195
|
} catch (error) {
|
|
2071
|
-
|
|
2196
|
+
say2(`the configuration was not resolved: ${message(error)}`);
|
|
2072
2197
|
}
|
|
2073
2198
|
}
|
|
2074
2199
|
try {
|
|
@@ -2096,7 +2221,7 @@ async function refresh(adapter, cwd) {
|
|
|
2096
2221
|
held !== null && etag === held.policy.etag ? etag : null
|
|
2097
2222
|
);
|
|
2098
2223
|
} catch (error) {
|
|
2099
|
-
if (!(error instanceof ApiError))
|
|
2224
|
+
if (!(error instanceof ApiError)) say2(`the check-in failed: ${message(error)}`);
|
|
2100
2225
|
else if (error.status === 401) {
|
|
2101
2226
|
if (error.said === "invalid_token") {
|
|
2102
2227
|
deleteToken(agent);
|
|
@@ -2104,7 +2229,7 @@ async function refresh(adapter, cwd) {
|
|
|
2104
2229
|
}
|
|
2105
2230
|
} else if (error.status === 503) nextAt(agent, Math.min(Math.max((error.retryAfterSeconds ?? 0) * 1e3, HOUR_MS), DAY_MS), LOOKUP_UNAVAILABLE);
|
|
2106
2231
|
else if (error.status === 403) nextAt(agent, HOUR_MS, REFUSED);
|
|
2107
|
-
else if (error.status !== 0)
|
|
2232
|
+
else if (error.status !== 0) say2(`the check-in was refused: ${error.status} ${error.error}`);
|
|
2108
2233
|
return null;
|
|
2109
2234
|
}
|
|
2110
2235
|
let policy = held?.policy ?? null;
|
|
@@ -2130,7 +2255,7 @@ async function refresh(adapter, cwd) {
|
|
|
2130
2255
|
}
|
|
2131
2256
|
function folderGone(folder) {
|
|
2132
2257
|
try {
|
|
2133
|
-
return !(0,
|
|
2258
|
+
return !(0, import_node_fs10.statSync)(folder).isDirectory();
|
|
2134
2259
|
} catch (error) {
|
|
2135
2260
|
const code = error.code;
|
|
2136
2261
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
@@ -2227,7 +2352,7 @@ ${PAIR_USAGE}
|
|
|
2227
2352
|
resolveAndKeep(adapter, cwd);
|
|
2228
2353
|
refreshed = await refresh(adapter, cwd);
|
|
2229
2354
|
} catch (error) {
|
|
2230
|
-
|
|
2355
|
+
say2(`the first check-in failed: ${message(error)}`);
|
|
2231
2356
|
}
|
|
2232
2357
|
if (readToken(agent) === null) {
|
|
2233
2358
|
process.stderr.write(`${TOKEN_REFUSED}
|
|
@@ -2253,6 +2378,8 @@ function afterInstall(agent) {
|
|
|
2253
2378
|
return ["Codex runs no user hook until you trust it: in Codex, run /hooks and trust the Attestari handlers."];
|
|
2254
2379
|
case "gemini":
|
|
2255
2380
|
return ["Gemini CLI runs hooks in trusted folders only; the hook decides there."];
|
|
2381
|
+
case "antigravity":
|
|
2382
|
+
return ["Antigravity reads ~/.gemini/config/hooks.json itself; /hooks in agy lists the attestari group. Nothing to restart."];
|
|
2256
2383
|
default:
|
|
2257
2384
|
return [];
|
|
2258
2385
|
}
|
|
@@ -2260,7 +2387,7 @@ function afterInstall(agent) {
|
|
|
2260
2387
|
function installHook(adapter) {
|
|
2261
2388
|
if (adapter.name === "claude-code") {
|
|
2262
2389
|
if (process.env.CLAUDE_PLUGIN_ROOT) {
|
|
2263
|
-
|
|
2390
|
+
say2("the Attestari plugin is installed in Claude Code already; the hook decides from the next session");
|
|
2264
2391
|
return true;
|
|
2265
2392
|
}
|
|
2266
2393
|
let plugin;
|
|
@@ -2269,15 +2396,15 @@ function installHook(adapter) {
|
|
|
2269
2396
|
} catch (error) {
|
|
2270
2397
|
plugin = { installed: false, why: `The install failed (${oneLine2(message(error))})` };
|
|
2271
2398
|
}
|
|
2272
|
-
if (plugin.installed)
|
|
2399
|
+
if (plugin.installed) say2("the Attestari plugin is installed in Claude Code; it runs from the next session");
|
|
2273
2400
|
else process.stderr.write(`${plugin.why}, so the Attestari plugin was not installed. Run:
|
|
2274
2401
|
${installCommands()}`);
|
|
2275
2402
|
return plugin.installed;
|
|
2276
2403
|
}
|
|
2277
2404
|
try {
|
|
2278
2405
|
const done = adapter.install(home());
|
|
2279
|
-
if ("path" in done)
|
|
2280
|
-
for (const line of afterInstall(adapter.name))
|
|
2406
|
+
if ("path" in done) say2(`the hook is installed in ${done.path}${done.before === done.after ? " (it was already)" : ""}`);
|
|
2407
|
+
for (const line of afterInstall(adapter.name)) say2(line);
|
|
2281
2408
|
return true;
|
|
2282
2409
|
} catch (error) {
|
|
2283
2410
|
process.stderr.write(`The hook was not installed: ${oneLine2(message(error))}. Fix that and run: npx -y attestari@2.9 install --agent ${adapter.name}
|
|
@@ -2285,7 +2412,7 @@ ${installCommands()}`);
|
|
|
2285
2412
|
return false;
|
|
2286
2413
|
}
|
|
2287
2414
|
}
|
|
2288
|
-
function
|
|
2415
|
+
function install5(args) {
|
|
2289
2416
|
let agent;
|
|
2290
2417
|
for (let i = 0; i < args.length; i++) {
|
|
2291
2418
|
const a = args[i];
|
|
@@ -2307,8 +2434,8 @@ function install4(args) {
|
|
|
2307
2434
|
}
|
|
2308
2435
|
|
|
2309
2436
|
// src/status.ts
|
|
2310
|
-
var
|
|
2311
|
-
var
|
|
2437
|
+
var import_node_fs11 = require("node:fs");
|
|
2438
|
+
var import_node_path11 = require("node:path");
|
|
2312
2439
|
function age(ms) {
|
|
2313
2440
|
const minutes = Math.floor(ms / 6e4);
|
|
2314
2441
|
if (minutes < 60) return `${Math.max(minutes, 0)} min`;
|
|
@@ -2317,27 +2444,27 @@ function age(ms) {
|
|
|
2317
2444
|
return `${Math.floor(hours / 24)} days`;
|
|
2318
2445
|
}
|
|
2319
2446
|
function policyLine(dir) {
|
|
2320
|
-
const file = (0,
|
|
2321
|
-
if (!(0,
|
|
2447
|
+
const file = (0, import_node_path11.join)(dir, "policy.json");
|
|
2448
|
+
if (!(0, import_node_fs11.existsSync)(file)) return null;
|
|
2322
2449
|
let version = "unreadable";
|
|
2323
2450
|
try {
|
|
2324
|
-
const document = JSON.parse((0,
|
|
2451
|
+
const document = JSON.parse((0, import_node_fs11.readFileSync)(file, "utf8"));
|
|
2325
2452
|
const v = document?.policy?.version;
|
|
2326
2453
|
if (typeof v === "number") version = String(v);
|
|
2327
2454
|
} catch {
|
|
2328
2455
|
}
|
|
2329
|
-
return `policy ${version}, ${age(Date.now() - (0,
|
|
2456
|
+
return `policy ${version}, ${age(Date.now() - (0, import_node_fs11.statSync)(file).mtimeMs)} old`;
|
|
2330
2457
|
}
|
|
2331
2458
|
function status() {
|
|
2332
2459
|
const root = attestariDir();
|
|
2333
|
-
const agents = (0,
|
|
2460
|
+
const agents = (0, import_node_fs11.existsSync)(root) ? (0, import_node_fs11.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort() : [];
|
|
2334
2461
|
if (agents.length === 0) {
|
|
2335
2462
|
console.log("not paired");
|
|
2336
2463
|
return 0;
|
|
2337
2464
|
}
|
|
2338
2465
|
for (const agent of agents) {
|
|
2339
2466
|
const dir = agentDir(agent);
|
|
2340
|
-
const token = (0,
|
|
2467
|
+
const token = (0, import_node_fs11.existsSync)((0, import_node_path11.join)(dir, "token")) ? "token present" : "no token";
|
|
2341
2468
|
const policy = policyLine(dir) ?? "no policy";
|
|
2342
2469
|
console.log(`${agent}: ${token}; ${policy}`);
|
|
2343
2470
|
}
|
|
@@ -2345,7 +2472,7 @@ function status() {
|
|
|
2345
2472
|
}
|
|
2346
2473
|
|
|
2347
2474
|
// src/main.ts
|
|
2348
|
-
var AGENTS = ["claude-code", "cursor", "codex", "gemini"];
|
|
2475
|
+
var AGENTS = ["claude-code", "cursor", "codex", "gemini", "antigravity"];
|
|
2349
2476
|
var EVENTS = ["session-start", "pre-tool-use", "refresh"];
|
|
2350
2477
|
var USAGE = [
|
|
2351
2478
|
"usage: attestari-hook <agent> <event>",
|
|
@@ -2359,12 +2486,12 @@ function notBuilt(name) {
|
|
|
2359
2486
|
return 2;
|
|
2360
2487
|
}
|
|
2361
2488
|
function stdin() {
|
|
2362
|
-
return (0,
|
|
2489
|
+
return (0, import_node_fs12.readFileSync)(0, "utf8").replace(/^/, "");
|
|
2363
2490
|
}
|
|
2364
2491
|
var COMMANDS = {
|
|
2365
2492
|
status: () => status(),
|
|
2366
2493
|
pair: (args) => pair(args),
|
|
2367
|
-
install: (args) =>
|
|
2494
|
+
install: (args) => install5(args)
|
|
2368
2495
|
};
|
|
2369
2496
|
function hook(agent, event, args) {
|
|
2370
2497
|
const adapter = adapterFor(agent);
|