@praesidias/sdk 0.0.0-stage → 0.1.1

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/index.cjs ADDED
@@ -0,0 +1,2027 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ BUILT_IN_INPUTS: () => BUILT_IN_INPUTS,
24
+ MAX_ACTION_LENGTH: () => MAX_ACTION_LENGTH,
25
+ MappingError: () => MappingError,
26
+ Praesidias: () => Praesidias,
27
+ PraesidiasAuthError: () => PraesidiasAuthError,
28
+ PraesidiasConfigError: () => PraesidiasConfigError,
29
+ PraesidiasError: () => PraesidiasError,
30
+ PraesidiasRequestError: () => PraesidiasRequestError,
31
+ PraesidiasUnavailableError: () => PraesidiasUnavailableError,
32
+ TOOLS_PER_REPORT: () => TOOLS_PER_REPORT,
33
+ ToolGate: () => ToolGate,
34
+ UNPARSED: () => UNPARSED,
35
+ WORKLOAD_TOKEN_HEADER: () => WORKLOAD_TOKEN_HEADER,
36
+ agentToolActions: () => agentToolActions,
37
+ checkMapping: () => checkMapping,
38
+ createToolGate: () => createToolGate,
39
+ fitActionName: () => fitActionName,
40
+ inputsOfSchema: () => inputsOfSchema,
41
+ isSensitivePath: () => isSensitivePath,
42
+ loadMapping: () => loadMapping,
43
+ mapToolCall: () => mapToolCall,
44
+ matches: () => matches,
45
+ mcpAction: () => mcpAction,
46
+ otherAction: () => otherAction,
47
+ parseShellCommand: () => parseShellCommand,
48
+ patchPaths: () => patchPaths,
49
+ sentenceFor: () => sentenceFor,
50
+ shellActions: () => shellActions,
51
+ subjectIdFor: () => subjectIdFor,
52
+ toolAction: () => toolAction,
53
+ toolsForReport: () => toolsForReport
54
+ });
55
+ module.exports = __toCommonJS(index_exports);
56
+
57
+ // src/client.ts
58
+ var import_node_crypto3 = require("crypto");
59
+ var import_promises = require("fs/promises");
60
+
61
+ // src/errors.ts
62
+ var PraesidiasError = class extends Error {
63
+ /** A stable, machine-readable code. */
64
+ code;
65
+ status;
66
+ correlationId;
67
+ constructor(message, details) {
68
+ super(message);
69
+ this.name = new.target.name;
70
+ this.code = details.code;
71
+ this.status = details.status ?? null;
72
+ this.correlationId = details.correlationId ?? null;
73
+ }
74
+ };
75
+ var PraesidiasUnavailableError = class extends PraesidiasError {
76
+ };
77
+ var PraesidiasAuthError = class extends PraesidiasError {
78
+ };
79
+ var PraesidiasConfigError = class extends PraesidiasError {
80
+ };
81
+ var PraesidiasRequestError = class extends PraesidiasError {
82
+ };
83
+ function errorClassOf(thrown) {
84
+ const name = thrown instanceof Error ? thrown.name || thrown.constructor?.name || "Error" : typeof thrown;
85
+ const clean = String(name).replace(/[^A-Za-z0-9_.]/g, "").slice(0, 120);
86
+ return /^[A-Za-z]/.test(clean) ? clean : "Error";
87
+ }
88
+
89
+ // src/http.ts
90
+ var import_node_crypto = require("crypto");
91
+ var WORKLOAD_TOKEN_HEADER = "x-praesidias-workload-token";
92
+ var RETRY_STATUS = /* @__PURE__ */ new Set([429, 502, 503, 504]);
93
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
94
+ if (signal?.aborted) return reject(signal.reason);
95
+ const timer = setTimeout(resolve, ms);
96
+ signal?.addEventListener("abort", () => (clearTimeout(timer), reject(signal.reason)), {
97
+ once: true
98
+ });
99
+ });
100
+ function checkedBaseUrl(raw, allowInsecure) {
101
+ let url;
102
+ try {
103
+ url = new URL(raw);
104
+ } catch {
105
+ throw new PraesidiasConfigError("The Praesidias address is not a valid URL.", {
106
+ code: "BASE_URL_INVALID"
107
+ });
108
+ }
109
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
110
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && (local || allowInsecure))) {
111
+ throw new PraesidiasConfigError(
112
+ "The Praesidias address must use https (http is accepted for localhost only).",
113
+ { code: "BASE_URL_INSECURE" }
114
+ );
115
+ }
116
+ return url.origin + url.pathname.replace(/\/+$/, "");
117
+ }
118
+ async function readWorkloadToken(http) {
119
+ if (!http.workloadToken) return null;
120
+ let token;
121
+ try {
122
+ const value = await http.workloadToken();
123
+ if (value === null || value === void 0) return null;
124
+ token = String(value).trim();
125
+ } catch {
126
+ throw new PraesidiasConfigError("The workload identity token could not be read.", {
127
+ code: "WORKLOAD_TOKEN_UNREADABLE"
128
+ });
129
+ }
130
+ if (!token) {
131
+ throw new PraesidiasConfigError("The workload identity token is empty.", {
132
+ code: "WORKLOAD_TOKEN_UNREADABLE"
133
+ });
134
+ }
135
+ return token;
136
+ }
137
+ async function call(http, method, path, options = {}) {
138
+ const correlationId = options.correlationId ?? (0, import_node_crypto.randomUUID)();
139
+ const attempts = 1 + Math.max(0, options.retries ?? http.retries);
140
+ const payload = options.body === void 0 ? void 0 : JSON.stringify(options.body);
141
+ let last = { status: null, code: "NETWORK" };
142
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
143
+ if (attempt > 1) {
144
+ http.log({ event: "retry", method, path, attempt, correlationId, code: last.code });
145
+ await sleep(http.retryDelayMs * 2 ** (attempt - 2) * (0.5 + Math.random()), options.signal);
146
+ }
147
+ const started = Date.now();
148
+ const timeout = new AbortController();
149
+ const timer = setTimeout(() => timeout.abort(), http.timeoutMs);
150
+ const onAbort = () => timeout.abort();
151
+ options.signal?.addEventListener("abort", onAbort, { once: true });
152
+ const workload = await readWorkloadToken(http);
153
+ let res;
154
+ try {
155
+ http.log({ event: "request", method, path, attempt, correlationId });
156
+ res = await http.fetch(`${http.baseUrl}${path}`, {
157
+ method,
158
+ headers: {
159
+ "x-api-key": http.apiKey,
160
+ ...workload ? { [WORKLOAD_TOKEN_HEADER]: workload } : {},
161
+ "x-correlation-id": correlationId,
162
+ accept: "application/json",
163
+ ...payload === void 0 ? {} : { "content-type": "application/json" }
164
+ },
165
+ ...payload === void 0 ? {} : { body: payload },
166
+ signal: timeout.signal
167
+ });
168
+ } catch {
169
+ if (options.signal?.aborted) throw options.signal.reason;
170
+ last = { status: null, code: timeout.signal.aborted ? "TIMEOUT" : "NETWORK" };
171
+ http.log({ event: "failure", method, path, attempt, correlationId, code: last.code });
172
+ continue;
173
+ } finally {
174
+ clearTimeout(timer);
175
+ options.signal?.removeEventListener("abort", onAbort);
176
+ }
177
+ const durationMs = Date.now() - started;
178
+ let body = null;
179
+ let readable = true;
180
+ try {
181
+ const text = await res.text();
182
+ body = text ? JSON.parse(text) : null;
183
+ } catch {
184
+ readable = false;
185
+ }
186
+ const rawCode = body?.code;
187
+ const code = typeof rawCode === "string" && /^[A-Z][A-Z0-9_]{1,63}$/.test(rawCode) ? rawCode : "";
188
+ http.log({
189
+ event: "response",
190
+ method,
191
+ path,
192
+ attempt,
193
+ status: res.status,
194
+ correlationId,
195
+ durationMs,
196
+ ...code ? { code } : {}
197
+ });
198
+ if (res.status >= 200 && res.status < 300) {
199
+ if (!readable) {
200
+ throw new PraesidiasUnavailableError("Praesidias answered in a way that cannot be read.", {
201
+ code: "BAD_REPLY",
202
+ status: res.status,
203
+ correlationId
204
+ });
205
+ }
206
+ return { status: res.status, body, correlationId };
207
+ }
208
+ if (res.status === 401 || res.status === 403) {
209
+ throw new PraesidiasAuthError(
210
+ code.startsWith("WORKLOAD_") ? "Praesidias did not accept this agent's workload identity." : res.status === 401 ? "Praesidias did not accept this key." : "This key is not allowed to do that.",
211
+ {
212
+ code: code || (res.status === 401 ? "KEY_REFUSED" : "FORBIDDEN"),
213
+ status: res.status,
214
+ correlationId
215
+ }
216
+ );
217
+ }
218
+ if (RETRY_STATUS.has(res.status) || res.status >= 500) {
219
+ last = { status: res.status, code: code || `HTTP_${res.status}` };
220
+ if (RETRY_STATUS.has(res.status)) continue;
221
+ break;
222
+ }
223
+ throw new PraesidiasRequestError("Praesidias refused this request.", {
224
+ code: code || `HTTP_${res.status}`,
225
+ status: res.status,
226
+ correlationId
227
+ });
228
+ }
229
+ throw new PraesidiasUnavailableError(
230
+ last.code === "TIMEOUT" ? "Praesidias did not answer in time." : last.status === null ? "Praesidias could not be reached." : "Praesidias is not available right now.",
231
+ { code: last.code, status: last.status, correlationId }
232
+ );
233
+ }
234
+
235
+ // src/reasons.ts
236
+ var SENTENCES = {
237
+ ALLOW_POLICY_RULE: "A rule allows this.",
238
+ DENY_POLICY_DEFAULT: "No rule allows this action, so it is blocked.",
239
+ DENY_POLICY_RULE: "A rule blocks this action.",
240
+ DENY_POLICY_NO_CASE_MATCHED: "None of the rule\u2019s cases covers this request, so it is blocked.",
241
+ DENY_POLICY_LIMIT_EXCEEDED: "This action has reached its limit for now.",
242
+ DENY_POLICY_BUDGET_EXCEEDED: "This would go over a budget, so it is blocked.",
243
+ DENY_CONTEXT_INVALID: "The details sent with this request do not match what the rule expects (a field is missing, of the wrong type or out of range).",
244
+ DENY_CONDITION_EVALUATION_FAILED: "A rule could not be checked for this request, so it is blocked.",
245
+ DENY_BUDGETS_EVALUATION_FAILED: "A budget could not be checked for this request, so it is blocked.",
246
+ DENY_LIMITS_EVALUATION_FAILED: "A limit could not be checked for this request, so it is blocked.",
247
+ DENY_APPROVAL_REQUIRED: "This action needs approval from a person first.",
248
+ DENY_APPROVAL_ARTIFACT_IDS_REQUIRED: "This action needs approval from a person first.",
249
+ DENY_APPROVAL_QUORUM_UNMET: "This action needs more approvals before it can go ahead.",
250
+ DENY_APPROVALS_INVALID: "The approvals given for this action are not valid.",
251
+ DENY_APPROVAL_ARTIFACT_INVALID: "An approval given for this action does not cover it: it was for another request, has been used, has expired or was given by someone who may not approve it.",
252
+ DENY_APPROVAL_ARTIFACT_NOT_FOUND: "An approval given for this action does not exist.",
253
+ DENY_APPROVAL_ALREADY_USED: "The approval for this action has already been used.",
254
+ DENY_AUTHORITY_INVALID: "This agent's access is not valid: it was revoked, has expired, or does not cover this.",
255
+ DENY_AUTHORITY_REVOKED_DURING_DECISION: "This agent's access was revoked while it was being checked.",
256
+ DENY_POLICY_MISSING_REQUIRED_AUTHORITY_SCOPE: "This agent's access does not cover what the rule requires.",
257
+ DENY_POLICY_AUTHORITY_SCOPE_REQUIREMENTS_UNMET: "This agent's access does not cover what the rule requires.",
258
+ DENY_POLICY_AUTHORITY_PATH_REQUIREMENTS_UNMET: "This agent's access was not given the way the rule requires.",
259
+ DENY_POLICY_CHANGED_DURING_DECISION: "The rules changed while this was being decided. Ask again.",
260
+ DENY_GROUPS_CHANGED_DURING_DECISION: "This agent's groups changed while this was being decided. Ask again.",
261
+ ALLOW_POLICY_REPORT_ONLY: "A rule allows this.",
262
+ DENY_PRINCIPAL_NOT_BOUND: "This key may not act as that agent.",
263
+ DENY_WORKLOAD_NOT_VERIFIED: "This agent must run from its own workload, and this request did not prove that.",
264
+ DENY_DOMAIN_NO_MATCH: "This action is outside every area, so it is blocked.",
265
+ DENY_POLICY_DOMAIN_MISMATCH: "This action does not belong to the area its rule names.",
266
+ DENY_DOMAIN_MODE_NOT_ALLOWED: "This way of running the action is not allowed in its area.",
267
+ DENY_POLICY_MODE_NOT_ALLOWED: "This way of running the action is not allowed by its rule.",
268
+ DENY_STAKE_INSUFFICIENT: "There is not enough stake for this action.",
269
+ PAYLOAD_DECISION_MISMATCH: "What was to be authorized differs from what was decided.",
270
+ EXECUTION_BOUND_VALUE_MISMATCH: "What was to be run differs from what was decided."
271
+ };
272
+ var WAITING = /* @__PURE__ */ new Set([
273
+ "DENY_APPROVAL_REQUIRED",
274
+ "DENY_APPROVAL_ARTIFACT_IDS_REQUIRED",
275
+ "DENY_APPROVAL_QUORUM_UNMET"
276
+ ]);
277
+ var isWaitingCode = (code) => WAITING.has(code);
278
+ function sentenceFor(code) {
279
+ return SENTENCES[code] ?? (code.startsWith("DENY_") ? "Praesidias blocked this action." : code);
280
+ }
281
+ function messageOf(reasons, code) {
282
+ const own = reasons?.caseMessage;
283
+ return typeof own === "string" && own.trim() ? own.slice(0, 500) : sentenceFor(code);
284
+ }
285
+
286
+ // src/report.ts
287
+ var ACTION_NAME = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
288
+ var FIELD_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,63}$/;
289
+ var TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "array", "object"]);
290
+ var TOOLS_PER_REPORT = 500;
291
+ function toolsForReport(tools) {
292
+ const byAction = /* @__PURE__ */ new Map();
293
+ for (const tool of tools) {
294
+ const name = typeof tool?.name === "string" ? tool.name.trim().slice(0, 200) : "";
295
+ const action = typeof tool?.action === "string" ? tool.action : "";
296
+ if (!name || !ACTION_NAME.test(action)) continue;
297
+ const inputs = {};
298
+ for (const [field, spec] of Object.entries(tool.inputs ?? {})) {
299
+ if (Object.keys(inputs).length >= 50) break;
300
+ if (!FIELD_NAME.test(field) || !spec || !TYPES.has(spec.type)) continue;
301
+ inputs[field] = typeof spec.description === "string" && spec.description ? { type: spec.type, description: spec.description.slice(0, 200) } : { type: spec.type };
302
+ }
303
+ const description = typeof tool.description === "string" && tool.description.trim() ? tool.description.trim().slice(0, 500) : null;
304
+ byAction.set(action, { name, action, ...description ? { description } : {}, inputs });
305
+ }
306
+ return [...byAction.values()];
307
+ }
308
+ function inputsOfSchema(schema) {
309
+ const props = schema?.properties;
310
+ const out = {};
311
+ if (!props || typeof props !== "object") return out;
312
+ for (const [field, spec] of Object.entries(props)) {
313
+ const raw = spec?.type;
314
+ const type = Array.isArray(raw) ? raw.find((t) => t !== "null") : raw;
315
+ const mapped = type === "integer" ? "number" : TYPES.has(type) ? type : null;
316
+ if (!mapped) continue;
317
+ const description = spec.description;
318
+ out[field] = typeof description === "string" && description ? { type: mapped, description: description.slice(0, 200) } : { type: mapped };
319
+ }
320
+ return out;
321
+ }
322
+
323
+ // src/subject.ts
324
+ var import_node_crypto2 = require("crypto");
325
+ var NAMESPACE = "8f1c9a52-6b7e-5d3f-9c41-2e7a0b6d4f10";
326
+ var bytes = (uuid) => Buffer.from(uuid.replace(/-/g, ""), "hex");
327
+ function subjectIdFor(organizationId, resource) {
328
+ const name = `${organizationId}
329
+ ${resource.type}
330
+ ${resource.id ?? ""}`;
331
+ const hash = (0, import_node_crypto2.createHash)("sha1").update(bytes(NAMESPACE)).update(name, "utf8").digest();
332
+ hash[6] = hash[6] & 15 | 80;
333
+ hash[8] = hash[8] & 63 | 128;
334
+ const hex = hash.subarray(0, 16).toString("hex");
335
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
336
+ }
337
+
338
+ // src/client.ts
339
+ var DEFAULT_TIMING = {
340
+ retryDelayMs: 250,
341
+ meCacheMs: 3e4,
342
+ pollStartMs: 2e3,
343
+ pollMaxMs: 15e3,
344
+ authorizationTtlMs: 5 * 6e4
345
+ };
346
+ function detailsOf(body) {
347
+ const problems = Array.isArray(body.reasons?.rule?.problems) ? body.reasons.rule.problems.filter((p) => !!p && typeof p === "object").map((p) => ({ path: String(p.path), problem: String(p.problem) })) : [];
348
+ const b = body.reasons?.budget;
349
+ const budget = b && typeof b.budgetId === "string" ? {
350
+ budgetId: b.budgetId,
351
+ spent: String(b.spent),
352
+ amount: String(b.amount),
353
+ cap: String(b.cap)
354
+ } : null;
355
+ const failures = Array.isArray(body.reasons?.approvals?.failures) ? body.reasons.approvals.failures.map(
356
+ (f) => f && typeof f === "object" && "code" in f ? String(f.code) : String(f)
357
+ ) : [];
358
+ return { problems, budget, approvalFailures: failures };
359
+ }
360
+ function workloadTokenSource(options) {
361
+ const given = options.workloadToken;
362
+ if (typeof given === "string") return async () => given;
363
+ if (typeof given === "function")
364
+ return async () => {
365
+ const value = await given();
366
+ return value === null || value === void 0 ? null : String(value);
367
+ };
368
+ const file2 = (options.workloadTokenFile ?? env("PRAESIDIAS_WORKLOAD_TOKEN_FILE")).trim();
369
+ return file2 ? () => (0, import_promises.readFile)(file2, "utf8") : null;
370
+ }
371
+ var env = (name) => typeof process !== "undefined" ? (process.env[name] ?? "").trim() : "";
372
+ var Praesidias = class {
373
+ http;
374
+ timing;
375
+ system;
376
+ consoleUrl;
377
+ onApproval;
378
+ approvalTimeoutMs;
379
+ cachedMe = null;
380
+ /** The last "who am I" ever read, however old: only to know, offline, if Audit mode was on. */
381
+ lastMe = null;
382
+ /** Requests run offline under Audit mode, oldest first, to record once Praesidias answers. */
383
+ offline = [];
384
+ flushing = false;
385
+ constructor(options = {}) {
386
+ const apiKey = (options.apiKey ?? env("PRAESIDIAS_API_KEY")).trim();
387
+ if (!apiKey) {
388
+ throw new PraesidiasConfigError(
389
+ "No Praesidias key. Set the PRAESIDIAS_API_KEY environment variable.",
390
+ { code: "API_KEY_MISSING" }
391
+ );
392
+ }
393
+ const baseUrl = (options.baseUrl ?? env("PRAESIDIAS_URL")).trim();
394
+ if (!baseUrl) {
395
+ throw new PraesidiasConfigError(
396
+ "No Praesidias address. Set the PRAESIDIAS_URL environment variable.",
397
+ { code: "BASE_URL_MISSING" }
398
+ );
399
+ }
400
+ if (options.workloadToken !== void 0 && options.workloadTokenFile !== void 0) {
401
+ throw new PraesidiasConfigError(
402
+ "Give the workload token either as a file (workloadTokenFile) or directly (workloadToken), not both.",
403
+ { code: "WORKLOAD_TOKEN_CONFLICT" }
404
+ );
405
+ }
406
+ this.timing = { ...DEFAULT_TIMING, ...options.timing };
407
+ this.http = {
408
+ baseUrl: checkedBaseUrl(baseUrl, options.allowInsecure === true),
409
+ apiKey,
410
+ timeoutMs: options.timeoutMs ?? 1e4,
411
+ retries: options.retries ?? 2,
412
+ fetch: options.fetch ?? fetch,
413
+ log: options.logger ?? (() => {
414
+ }),
415
+ retryDelayMs: this.timing.retryDelayMs,
416
+ workloadToken: workloadTokenSource(options)
417
+ };
418
+ this.system = (options.system ?? env("PRAESIDIAS_SYSTEM")) || null;
419
+ const consoleUrl = (options.consoleUrl ?? env("PRAESIDIAS_CONSOLE_URL")).replace(/\/+$/, "");
420
+ this.consoleUrl = consoleUrl || null;
421
+ this.onApproval = options.onApproval ?? "wait";
422
+ this.approvalTimeoutMs = options.approvalTimeoutMs ?? 15 * 6e4;
423
+ Object.defineProperty(this, "http", { enumerable: false });
424
+ }
425
+ toJSON() {
426
+ return { praesidias: this.http.baseUrl };
427
+ }
428
+ /**
429
+ * Who this key is. Cached briefly. If it cannot be read, this throws: the SDK never guesses
430
+ * who the agent is or whether it is in Audit mode.
431
+ */
432
+ async me(options = {}) {
433
+ const now = Date.now();
434
+ if (!options.fresh && this.cachedMe && now - this.cachedMe.at < this.timing.meCacheMs) {
435
+ return this.cachedMe.value;
436
+ }
437
+ const reply = await call(this.http, "GET", "/agent/me", {
438
+ ...options.signal ? { signal: options.signal } : {}
439
+ });
440
+ const me = reply.body;
441
+ if (!me || typeof me.principalId !== "string" || !Array.isArray(me.grantIds)) {
442
+ throw new PraesidiasUnavailableError("Praesidias answered in a way that cannot be read.", {
443
+ code: "BAD_REPLY",
444
+ correlationId: reply.correlationId
445
+ });
446
+ }
447
+ this.cachedMe = { at: now, value: me };
448
+ this.lastMe = me;
449
+ if (this.offline.length > 0) void this.flushOffline();
450
+ return me;
451
+ }
452
+ /** How many requests that ran offline, under Audit mode, are still to be recorded. */
453
+ get offlinePending() {
454
+ return this.offline.length;
455
+ }
456
+ /**
457
+ * Sends the requests that ran offline under Audit mode as ordinary decisions, oldest first, so
458
+ * they are in the record. Stops at the first that cannot be sent; the rest wait for next time.
459
+ */
460
+ async flushOffline() {
461
+ if (this.flushing) return 0;
462
+ this.flushing = true;
463
+ let sent = 0;
464
+ try {
465
+ while (this.offline.length > 0) {
466
+ const item = this.offline[0];
467
+ try {
468
+ await this.decide(item.action, item.resource, {
469
+ system: item.system,
470
+ context: item.context,
471
+ correlationId: item.correlationId,
472
+ idempotencyKey: `offline:${item.correlationId}`
473
+ });
474
+ } catch (err) {
475
+ if (err instanceof PraesidiasUnavailableError || !(err instanceof PraesidiasError)) break;
476
+ }
477
+ this.offline.shift();
478
+ sent += 1;
479
+ }
480
+ } finally {
481
+ this.flushing = false;
482
+ }
483
+ return sent;
484
+ }
485
+ /**
486
+ * Tells Praesidias which tools this agent has, so the Console can list them and say which have
487
+ * a rule. Names and input types only. Never throws and never holds the agent up: false when it
488
+ * could not be sent (an older Praesidias without this, or one that cannot be reached).
489
+ */
490
+ async reportTools(source, tools) {
491
+ const clean = toolsForReport(tools);
492
+ try {
493
+ for (let i = 0; i < clean.length; i += TOOLS_PER_REPORT) {
494
+ await call(this.http, "POST", "/agent/tools", {
495
+ body: { source, tools: clean.slice(i, i + TOOLS_PER_REPORT) },
496
+ retries: 1
497
+ });
498
+ }
499
+ return true;
500
+ } catch {
501
+ return false;
502
+ }
503
+ }
504
+ linkTo(page) {
505
+ return this.consoleUrl ? `${this.consoleUrl}/${page}` : null;
506
+ }
507
+ systemFor(options) {
508
+ const system = options.system ?? this.system;
509
+ if (!system) {
510
+ throw new PraesidiasConfigError(
511
+ 'Say which system this agent acts on: the "system" option, or PRAESIDIAS_SYSTEM.',
512
+ { code: "SYSTEM_MISSING" }
513
+ );
514
+ }
515
+ return system;
516
+ }
517
+ /** Asks Praesidias whether this agent may do this action on this resource. Nothing is run. */
518
+ async decide(action, resource, options = {}) {
519
+ const me = await this.me(options.signal ? { signal: options.signal } : {});
520
+ const system = this.systemFor(options);
521
+ const correlationId = options.correlationId ?? (0, import_node_crypto3.randomUUID)();
522
+ const context = options.context ?? {};
523
+ const reply = await call(this.http, "POST", "/control/decide", {
524
+ correlationId,
525
+ ...options.signal ? { signal: options.signal } : {},
526
+ body: {
527
+ systemId: system,
528
+ executionMode: "pre_gate",
529
+ actionType: action,
530
+ resourceType: resource.type,
531
+ resourceId: resource.id ?? null,
532
+ actingPrincipalId: me.principalId,
533
+ assertedGrantIds: me.grantIds,
534
+ ...options.approvalArtifactIds?.length ? { approvalArtifactIds: options.approvalArtifactIds } : {},
535
+ idempotencyKey: options.idempotencyKey ?? (0, import_node_crypto3.randomUUID)(),
536
+ correlationId,
537
+ context
538
+ }
539
+ });
540
+ const body = reply.body;
541
+ const result = body?.result;
542
+ if (!body || typeof body.decisionId !== "string" || result !== "ALLOW" && result !== "DENY") {
543
+ throw new PraesidiasUnavailableError("Praesidias answered in a way that cannot be read.", {
544
+ code: "BAD_REPLY",
545
+ correlationId
546
+ });
547
+ }
548
+ const code = typeof body.reasons?.code === "string" ? body.reasons.code : result === "ALLOW" ? "ALLOW" : "DENY_UNSPECIFIED";
549
+ const paths = body.constraints?.bound?.paths;
550
+ const caseId = body.constraints?.rule?.caseId;
551
+ return {
552
+ decisionId: body.decisionId,
553
+ allowed: result === "ALLOW",
554
+ reason: { code, message: messageOf(body.reasons, code) },
555
+ context,
556
+ caseId: typeof caseId === "string" ? caseId : null,
557
+ boundPaths: Array.isArray(paths) ? paths.map(String) : [],
558
+ details: detailsOf(body),
559
+ needsApproval: result === "DENY" && isWaitingCode(code),
560
+ expiresAt: body.expiresAt ?? null,
561
+ action,
562
+ resource,
563
+ system,
564
+ rulesVersion: body.policy?.version ?? null,
565
+ correlationId
566
+ };
567
+ }
568
+ /**
569
+ * Gets Praesidias's signed authorization for an allowed decision. Call it straight after an
570
+ * ALLOW and before running the action; a decision can be authorized once.
571
+ */
572
+ async commit(decision, options = {}) {
573
+ if (!decision.allowed) {
574
+ throw new PraesidiasRequestError("Only an allowed decision can be authorized.", {
575
+ code: "DECISION_NOT_ALLOW",
576
+ correlationId: decision.correlationId
577
+ });
578
+ }
579
+ const me = await this.me(options.signal ? { signal: options.signal } : {});
580
+ const payload = {
581
+ v: 1,
582
+ action: decision.action,
583
+ resource: { type: decision.resource.type, id: decision.resource.id ?? null },
584
+ system: decision.system,
585
+ expiresAt: new Date(Date.now() + this.timing.authorizationTtlMs).toISOString(),
586
+ // A decision locked to some of its values is authorized only for those same values.
587
+ ...decision.boundPaths.length > 0 ? { context: decision.context } : {}
588
+ };
589
+ const reply = await call(this.http, "POST", "/control/commit", {
590
+ correlationId: decision.correlationId,
591
+ ...options.signal ? { signal: options.signal } : {},
592
+ body: {
593
+ decisionId: decision.decisionId,
594
+ subjectId: subjectIdFor(me.organizationId, decision.resource),
595
+ kind: me.authorizationKind || "ACTION_AUTHORIZED",
596
+ payload,
597
+ idempotencyKey: options.idempotencyKey ?? `authorize:${decision.decisionId}`,
598
+ correlationId: decision.correlationId
599
+ }
600
+ });
601
+ const raw = reply.body;
602
+ if (!raw || typeof raw.id !== "string" || typeof raw.signature !== "string" || raw.decisionId !== decision.decisionId) {
603
+ throw new PraesidiasUnavailableError("Praesidias answered in a way that cannot be read.", {
604
+ code: "BAD_REPLY",
605
+ correlationId: decision.correlationId
606
+ });
607
+ }
608
+ return {
609
+ id: raw.id,
610
+ decisionId: decision.decisionId,
611
+ kind: String(raw.kind),
612
+ subjectId: String(raw.subjectId),
613
+ hash: String(raw.hash),
614
+ signature: raw.signature,
615
+ signingKeyId: typeof raw.signingKeyId === "string" ? raw.signingKeyId : null,
616
+ issuedAt: String(raw.issuedAt),
617
+ payload: raw.payload ?? {},
618
+ raw
619
+ };
620
+ }
621
+ /** Whether a decision that needs approval has been approved yet. */
622
+ async approvalStatus(decisionId, options = {}) {
623
+ const reply = await call(
624
+ this.http,
625
+ "GET",
626
+ `/agent/decisions/${encodeURIComponent(decisionId)}/approval`,
627
+ options.signal ? { signal: options.signal } : {}
628
+ );
629
+ const body = reply.body;
630
+ if (!body || !["waiting", "approved", "rules_changed", "not_waiting"].includes(body.state)) {
631
+ throw new PraesidiasUnavailableError("Praesidias answered in a way that cannot be read.", {
632
+ code: "BAD_REPLY",
633
+ correlationId: reply.correlationId
634
+ });
635
+ }
636
+ return body;
637
+ }
638
+ /** Reports what happened after an authorized action ran. */
639
+ async reportOutcome(proof, outcome) {
640
+ await call(this.http, "POST", "/agent/outcomes", {
641
+ body: {
642
+ decisionId: proof.decisionId,
643
+ proofId: proof.id,
644
+ status: outcome.status,
645
+ ...outcome.status === "failed" ? { errorClass: errorClassOf(outcome.error) } : {}
646
+ }
647
+ });
648
+ }
649
+ /**
650
+ * Runs `fn` only if Praesidias allows this action on this resource.
651
+ *
652
+ * decide → (wait for approval if needed) → get the signed authorization → run fn → report
653
+ *
654
+ * Returns `executed`, `blocked` or `needs_approval`. Throws a PraesidiasError when Praesidias
655
+ * could not give a clear answer; in that case `fn` has not run. If `fn` itself throws, its
656
+ * error is reported as a failed outcome and thrown on unchanged.
657
+ */
658
+ async guard(action, resource, fn, options = {}) {
659
+ const correlationId = options.correlationId ?? (0, import_node_crypto3.randomUUID)();
660
+ const base = options.idempotencyKey ?? (0, import_node_crypto3.randomUUID)();
661
+ const onApproval = options.onApproval ?? this.onApproval;
662
+ const signal = options.signal;
663
+ let round = 0;
664
+ const offlineUnderAudit = async (err) => {
665
+ const last = this.lastMe;
666
+ const unreachable = err instanceof PraesidiasUnavailableError && err.code !== "BAD_REPLY";
667
+ const auditUntil = last?.mode === "audit" && last.audit ? Date.parse(last.audit.endsAt) : NaN;
668
+ if (!unreachable || !last?.agentKitEnabled || !(auditUntil > Date.now())) throw err;
669
+ const system = this.systemFor(options);
670
+ this.offline.push({
671
+ action,
672
+ resource,
673
+ system,
674
+ context: options.context ?? {},
675
+ correlationId
676
+ });
677
+ if (this.offline.length > 1e3) this.offline.splice(0, this.offline.length - 1e3);
678
+ const value = await fn({
679
+ decisionId: `offline:${correlationId}`,
680
+ correlationId,
681
+ proof: null,
682
+ auditMode: true
683
+ });
684
+ return {
685
+ ...common(null, "activity"),
686
+ status: "executed",
687
+ value,
688
+ proof: null,
689
+ audit: { wouldHave: "unknown", level: last.audit.level, offline: true },
690
+ outcomeRecorded: false
691
+ };
692
+ };
693
+ const common = (decisionId, page) => ({
694
+ decisionId,
695
+ correlationId,
696
+ consoleUrl: this.linkTo(page)
697
+ });
698
+ let me;
699
+ try {
700
+ me = await this.me(signal ? { signal } : {});
701
+ } catch (err) {
702
+ return offlineUnderAudit(err);
703
+ }
704
+ if (!me.agentKitEnabled) {
705
+ throw new PraesidiasConfigError(
706
+ "The Praesidias agent kit is not enabled for this organization yet. An Admin enables it in the Console, under Settings.",
707
+ { code: "AGENT_KIT_NOT_ENABLED", correlationId }
708
+ );
709
+ }
710
+ const ask = (approvalArtifactIds) => {
711
+ round += 1;
712
+ return this.decide(action, resource, {
713
+ ...options.system ? { system: options.system } : {},
714
+ ...options.context ? { context: options.context } : {},
715
+ ...signal ? { signal } : {},
716
+ ...approvalArtifactIds ? { approvalArtifactIds } : {},
717
+ correlationId,
718
+ idempotencyKey: `${base}:decide:${round}`
719
+ });
720
+ };
721
+ const run = async (decision) => {
722
+ const proof = await this.commit(decision, {
723
+ idempotencyKey: `${base}:authorize:${decision.decisionId}`,
724
+ ...signal ? { signal } : {}
725
+ });
726
+ let value;
727
+ try {
728
+ value = await fn({
729
+ decisionId: decision.decisionId,
730
+ correlationId,
731
+ proof,
732
+ auditMode: false
733
+ });
734
+ } catch (thrown) {
735
+ await this.reportOutcome(proof, { status: "failed", error: thrown }).catch(() => void 0);
736
+ throw thrown;
737
+ }
738
+ const outcomeRecorded = await this.reportOutcome(proof, { status: "succeeded" }).then(
739
+ () => true,
740
+ () => false
741
+ );
742
+ return {
743
+ ...common(decision.decisionId, "activity"),
744
+ status: "executed",
745
+ value,
746
+ proof,
747
+ audit: null,
748
+ outcomeRecorded
749
+ };
750
+ };
751
+ const underAudit = async (decision) => {
752
+ if (me.mode !== "audit") return null;
753
+ let permit;
754
+ try {
755
+ permit = (await call(this.http, "POST", "/agent/audit/permit", {
756
+ body: { decisionId: decision.decisionId },
757
+ correlationId,
758
+ retries: 0,
759
+ ...signal ? { signal } : {}
760
+ })).body;
761
+ } catch {
762
+ return null;
763
+ }
764
+ if (permit?.proceed !== true || typeof permit.permitId !== "string") return null;
765
+ const permitId = permit.permitId;
766
+ const report = (status, error) => call(this.http, "POST", "/agent/audit/outcome", {
767
+ body: {
768
+ permitId,
769
+ status,
770
+ ...status === "failed" ? { errorClass: errorClassOf(error) } : {}
771
+ },
772
+ correlationId
773
+ }).then(
774
+ () => true,
775
+ () => false
776
+ );
777
+ let value;
778
+ try {
779
+ value = await fn({
780
+ decisionId: decision.decisionId,
781
+ correlationId,
782
+ proof: null,
783
+ auditMode: true
784
+ });
785
+ } catch (thrown) {
786
+ await report("failed", thrown);
787
+ throw thrown;
788
+ }
789
+ return {
790
+ ...common(decision.decisionId, "activity"),
791
+ status: "executed",
792
+ value,
793
+ proof: null,
794
+ audit: {
795
+ wouldHave: permit.wouldHave === "waited" ? "waited" : "blocked",
796
+ level: permit.level === "everything" ? "everything" : "new_actions"
797
+ },
798
+ outcomeRecorded: await report("succeeded")
799
+ };
800
+ };
801
+ const settle = async (decision) => {
802
+ if (decision.allowed) return run(decision);
803
+ const audited = await underAudit(decision);
804
+ if (audited) return audited;
805
+ if (!decision.needsApproval) {
806
+ return {
807
+ ...common(decision.decisionId, "activity"),
808
+ status: "blocked",
809
+ reason: decision.reason
810
+ };
811
+ }
812
+ return waitForApproval(
813
+ decision,
814
+ onApproval === "wait" ? options.approvalTimeoutMs ?? this.approvalTimeoutMs : 0,
815
+ signal
816
+ );
817
+ };
818
+ const waitForApproval = async (first2, timeoutMs, waitSignal) => {
819
+ let decision = first2;
820
+ let status = {
821
+ decisionId: decision.decisionId,
822
+ state: "waiting",
823
+ approvalsHave: 0,
824
+ approvalsNeeded: null,
825
+ approvalArtifactIds: []
826
+ };
827
+ const deadline = Date.now() + timeoutMs;
828
+ let delay = this.timing.pollStartMs;
829
+ for (; ; ) {
830
+ status = await this.approvalStatus(
831
+ decision.decisionId,
832
+ waitSignal ? { signal: waitSignal } : {}
833
+ );
834
+ if (status.state === "approved" || status.state === "rules_changed") {
835
+ const next = await ask(
836
+ status.state === "approved" ? status.approvalArtifactIds : void 0
837
+ );
838
+ if (next.allowed) return run(next);
839
+ if (!next.needsApproval) {
840
+ return {
841
+ ...common(next.decisionId, "activity"),
842
+ status: "blocked",
843
+ reason: next.reason
844
+ };
845
+ }
846
+ decision = next;
847
+ } else if (status.state === "not_waiting") {
848
+ return {
849
+ ...common(decision.decisionId, "activity"),
850
+ status: "blocked",
851
+ reason: decision.reason
852
+ };
853
+ }
854
+ const left = deadline - Date.now();
855
+ if (left <= 0) break;
856
+ await new Promise((resolve, reject) => {
857
+ const timer = setTimeout(resolve, Math.min(delay, left));
858
+ waitSignal?.addEventListener(
859
+ "abort",
860
+ () => (clearTimeout(timer), reject(waitSignal.reason)),
861
+ { once: true }
862
+ );
863
+ });
864
+ delay = Math.min(this.timing.pollMaxMs, Math.round(delay * 1.5));
865
+ }
866
+ const waiting = decision;
867
+ return {
868
+ ...common(waiting.decisionId, "approvals"),
869
+ status: "needs_approval",
870
+ reason: waiting.reason,
871
+ approvalsHave: status.approvalsHave,
872
+ approvalsNeeded: status.approvalsNeeded,
873
+ resume: (again = {}) => waitForApproval(
874
+ waiting,
875
+ again.approvalTimeoutMs ?? this.approvalTimeoutMs,
876
+ again.signal ?? waitSignal
877
+ )
878
+ };
879
+ };
880
+ let first;
881
+ try {
882
+ first = await ask();
883
+ } catch (err) {
884
+ return offlineUnderAudit(err);
885
+ }
886
+ return settle(first);
887
+ }
888
+ };
889
+
890
+ // src/mapping.ts
891
+ var import_node_fs = require("fs");
892
+ var DEFAULT_ID_ARGS = ["id", "path", "uri", "url", "name", "key"];
893
+ var ACTION_NAME2 = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
894
+ var MappingError = class extends Error {
895
+ };
896
+ function fail(message) {
897
+ throw new MappingError(`Mapping: ${message}`);
898
+ }
899
+ var CONDITION_OPS = [
900
+ "gt",
901
+ "gte",
902
+ "lt",
903
+ "lte",
904
+ "eq",
905
+ "ne",
906
+ "in",
907
+ "startsWith",
908
+ "contains",
909
+ "exists"
910
+ ];
911
+ function checkCondition(condition, where) {
912
+ const c = condition;
913
+ if (!c || typeof c !== "object" || typeof c.arg !== "string" || !c.arg)
914
+ fail(`${where} needs an "arg".`);
915
+ const ops = Object.keys(c).filter((k) => k !== "arg");
916
+ if (ops.length !== 1 || !CONDITION_OPS.includes(ops[0])) {
917
+ fail(`${where} needs exactly one of: ${CONDITION_OPS.join(", ")}.`);
918
+ }
919
+ }
920
+ var FIELD_NAME2 = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
921
+ function checkFields(fields, where) {
922
+ if (fields === void 0) return;
923
+ if (!fields || typeof fields !== "object" || Array.isArray(fields))
924
+ fail(`${where} must be an object.`);
925
+ for (const [field, source] of Object.entries(fields)) {
926
+ if (!FIELD_NAME2.test(field)) fail(`${where}: "${field}" is not a field name.`);
927
+ const arg = typeof source === "string" ? source : source?.arg;
928
+ if (typeof arg !== "string" || !arg) fail(`${where}.${field} needs an argument name.`);
929
+ const as = typeof source === "object" ? source.as : void 0;
930
+ if (as !== void 0 && as !== "integer" && as !== "string" && as !== "boolean") {
931
+ fail(`${where}.${field}.as must be integer, string or boolean.`);
932
+ }
933
+ }
934
+ }
935
+ function checkTool(name, tool) {
936
+ if (!tool || typeof tool !== "object") fail(`tools.${name} must be an object.`);
937
+ checkFields(tool.fields, `tools.${name}.fields`);
938
+ for (const [index, rule] of (tool.when ?? []).entries()) {
939
+ const where = `tools.${name}.when[${index}]`;
940
+ if (typeof rule?.action !== "string" || !rule.action) fail(`${where} needs an "action".`);
941
+ for (const condition of Array.isArray(rule.if) ? rule.if : [rule.if])
942
+ checkCondition(condition, `${where}.if`);
943
+ }
944
+ }
945
+ function checkMapping(raw) {
946
+ const file2 = raw;
947
+ if (!file2 || typeof file2 !== "object") fail("must be a JSON object.");
948
+ if (typeof file2.system !== "string" || !file2.system) fail('"system" is required.');
949
+ if (typeof file2.server !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(file2.server)) {
950
+ fail('"server" is required: a short name of letters, digits, - and _.');
951
+ }
952
+ checkFields(file2.defaults?.fields, "defaults.fields");
953
+ for (const [name, tool] of Object.entries(file2.tools ?? {})) checkTool(name, tool);
954
+ return file2;
955
+ }
956
+ function loadMapping(path) {
957
+ let text;
958
+ try {
959
+ text = (0, import_node_fs.readFileSync)(path, "utf8");
960
+ } catch {
961
+ return fail(`cannot read ${path}.`);
962
+ }
963
+ try {
964
+ return checkMapping(JSON.parse(text));
965
+ } catch (err) {
966
+ if (err instanceof MappingError) throw err;
967
+ return fail(`${path} is not valid JSON.`);
968
+ }
969
+ }
970
+ function argAt(args, path) {
971
+ let value = args;
972
+ for (const part2 of path.split(".")) {
973
+ if (value === null || typeof value !== "object" || !Object.prototype.hasOwnProperty.call(value, part2)) {
974
+ return void 0;
975
+ }
976
+ value = value[part2];
977
+ }
978
+ return value;
979
+ }
980
+ function matches(condition, args) {
981
+ const value = argAt(args, condition.arg);
982
+ const number = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
983
+ if ("exists" in condition) return (value !== void 0 && value !== null) === condition.exists;
984
+ if ("gt" in condition) return number > condition.gt;
985
+ if ("gte" in condition) return number >= condition.gte;
986
+ if ("lt" in condition) return number < condition.lt;
987
+ if ("lte" in condition) return number <= condition.lte;
988
+ if ("eq" in condition) return value === condition.eq;
989
+ if ("ne" in condition) return value !== condition.ne;
990
+ if ("in" in condition) return condition.in.includes(value);
991
+ if ("startsWith" in condition)
992
+ return typeof value === "string" && value.startsWith(condition.startsWith);
993
+ if ("contains" in condition)
994
+ return typeof value === "string" && value.includes(condition.contains);
995
+ return false;
996
+ }
997
+ function toolEntry(file2, tool) {
998
+ const tools = file2.tools ?? {};
999
+ if (Object.prototype.hasOwnProperty.call(tools, tool)) return tools[tool];
1000
+ const family = Object.keys(tools).filter((name) => name.endsWith("*") && tool.startsWith(name.slice(0, -1))).sort((a, b) => b.length - a.length)[0];
1001
+ return family ? tools[family] : {};
1002
+ }
1003
+ var scalar = (value) => typeof value === "string" || typeof value === "number" ? String(value).slice(0, 256) : null;
1004
+ function readAs(value, as) {
1005
+ if (value === void 0 || value === null) return void 0;
1006
+ if (as === void 0) return value;
1007
+ if (as === "integer") {
1008
+ if (typeof value === "number") return Number.isSafeInteger(value) ? value : void 0;
1009
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
1010
+ const n = Number(value.trim());
1011
+ return Number.isSafeInteger(n) ? n : void 0;
1012
+ }
1013
+ return void 0;
1014
+ }
1015
+ if (as === "string")
1016
+ return typeof value === "string" ? value : typeof value === "number" ? String(value) : void 0;
1017
+ if (typeof value === "boolean") return value;
1018
+ if (value === "true") return true;
1019
+ if (value === "false") return false;
1020
+ return void 0;
1021
+ }
1022
+ function fieldsOf(fields, args) {
1023
+ const out = {};
1024
+ for (const [field, source] of Object.entries(fields)) {
1025
+ const arg = typeof source === "string" ? source : source.arg;
1026
+ const as = typeof source === "string" ? void 0 : source.as;
1027
+ const value = readAs(argAt(args, arg), as);
1028
+ if (value !== void 0) out[field] = value;
1029
+ }
1030
+ return out;
1031
+ }
1032
+ function toolAction(file2, tool) {
1033
+ const entry = toolEntry(file2, tool);
1034
+ if (entry.ungoverned === true) return null;
1035
+ const template = entry.action ?? file2.defaults?.action ?? "{server}.{tool}";
1036
+ const action = template.replaceAll("{server}", file2.server).replaceAll("{tool}", tool);
1037
+ return ACTION_NAME2.test(action) ? action : null;
1038
+ }
1039
+ function mapToolCall(file2, tool, args) {
1040
+ const entry = toolEntry(file2, tool);
1041
+ if (entry.ungoverned === true) return { governed: false };
1042
+ const matched = (entry.when ?? []).find(
1043
+ (rule) => (Array.isArray(rule.if) ? rule.if : [rule.if]).every((condition) => matches(condition, args))
1044
+ );
1045
+ const template = matched?.action ?? entry.action ?? file2.defaults?.action ?? "{server}.{tool}";
1046
+ const fill = (text) => text.replaceAll("{server}", file2.server).replaceAll("{tool}", tool);
1047
+ const action = fill(template);
1048
+ if (!ACTION_NAME2.test(action)) {
1049
+ throw new MappingError(`The tool "${tool}" maps to an action name Praesidias does not accept.`);
1050
+ }
1051
+ const resource = { ...file2.defaults?.resource, ...entry.resource, ...matched?.resource };
1052
+ const idFrom = resource.idFrom === void 0 ? DEFAULT_ID_ARGS : [resource.idFrom].flat();
1053
+ const id = idFrom.map((name) => scalar(argAt(args, name))).find((value) => value !== null) ?? null;
1054
+ const fields = entry.fields ?? file2.defaults?.fields;
1055
+ if (fields) {
1056
+ return {
1057
+ governed: true,
1058
+ action,
1059
+ resource: { type: fill(resource.type ?? "{tool}").slice(0, 64), id },
1060
+ system: entry.system ?? file2.system,
1061
+ context: fieldsOf(fields, args)
1062
+ };
1063
+ }
1064
+ const context = {
1065
+ tool,
1066
+ server: file2.server,
1067
+ argumentNames: args && typeof args === "object" ? Object.keys(args).slice(0, 50) : []
1068
+ };
1069
+ for (const name of entry.context ?? file2.defaults?.context ?? []) {
1070
+ const value = argAt(args, name);
1071
+ if (["string", "number", "boolean"].includes(typeof value)) {
1072
+ context[name] = typeof value === "string" ? value.slice(0, 256) : value;
1073
+ }
1074
+ }
1075
+ return {
1076
+ governed: true,
1077
+ action,
1078
+ resource: { type: fill(resource.type ?? "{tool}").slice(0, 64), id },
1079
+ system: entry.system ?? file2.system,
1080
+ context
1081
+ };
1082
+ }
1083
+
1084
+ // src/tools.ts
1085
+ var WAITING_TTL_MS = 24 * 60 * 60 * 1e3;
1086
+ var ToolGate = class {
1087
+ constructor(praesidias, mapping, options = {}) {
1088
+ this.praesidias = praesidias;
1089
+ this.mapping = checkMapping(mapping);
1090
+ this.onApproval = options.onApproval ?? "return";
1091
+ this.approvalTimeoutMs = options.approvalTimeoutMs;
1092
+ this.log = options.log ?? (() => {
1093
+ });
1094
+ }
1095
+ praesidias;
1096
+ mapping;
1097
+ onApproval;
1098
+ approvalTimeoutMs;
1099
+ log;
1100
+ waiting = /* @__PURE__ */ new Map();
1101
+ /** Every tool this gate has been given, by action, as last sent to Praesidias. */
1102
+ known = /* @__PURE__ */ new Map();
1103
+ reportTimer = null;
1104
+ lastReport = "";
1105
+ /**
1106
+ * Tells Praesidias about tools this agent has (the adapters call it with the tools they are
1107
+ * given), so the Console lists them and says which have a rule. Tools given together, or one
1108
+ * at a time in a row, go in one report, sent shortly after; a list that has not changed is not
1109
+ * sent again. Never throws and never holds anything up.
1110
+ */
1111
+ reportTools(tools) {
1112
+ for (const tool of tools) {
1113
+ if (typeof tool?.name !== "string" || !tool.name) continue;
1114
+ const action = toolAction(this.mapping, tool.name);
1115
+ if (!action) continue;
1116
+ this.known.set(action, {
1117
+ name: tool.name,
1118
+ action,
1119
+ ...typeof tool.description === "string" ? { description: tool.description } : {},
1120
+ inputs: inputsOfSchema(tool.schema)
1121
+ });
1122
+ }
1123
+ if (this.reportTimer) return;
1124
+ this.reportTimer = setTimeout(() => {
1125
+ this.reportTimer = null;
1126
+ const list = [...this.known.values()];
1127
+ const key = JSON.stringify(list);
1128
+ if (key === this.lastReport || list.length === 0) return;
1129
+ this.lastReport = key;
1130
+ void this.praesidias.reportTools("adapter", list).then((sent) => {
1131
+ if (!sent) this.lastReport = "";
1132
+ });
1133
+ }, 50);
1134
+ this.reportTimer.unref?.();
1135
+ }
1136
+ /** Runs `fn` if Praesidias allows this tool call, and says what happened either way. */
1137
+ async run(tool, args, fn) {
1138
+ let mapped;
1139
+ try {
1140
+ mapped = mapToolCall(this.mapping, tool, args ?? {});
1141
+ } catch (err) {
1142
+ this.log(`${tool}: not run (mapping)`);
1143
+ return {
1144
+ status: "unavailable",
1145
+ ran: false,
1146
+ decisionId: null,
1147
+ code: "MAPPING_INVALID",
1148
+ message: err instanceof MappingError ? `Not run. ${err.message}` : "Not run: this tool could not be mapped to an action."
1149
+ };
1150
+ }
1151
+ if (!mapped.governed) {
1152
+ this.log(`${tool}: passed through (marked ungoverned in the mapping)`);
1153
+ return {
1154
+ status: "passed_through",
1155
+ ran: true,
1156
+ value: await fn(),
1157
+ message: "Ran. This tool is not governed.",
1158
+ decisionId: null,
1159
+ audit: false
1160
+ };
1161
+ }
1162
+ const key = JSON.stringify([
1163
+ mapped.action,
1164
+ mapped.resource.type,
1165
+ mapped.resource.id,
1166
+ args ?? null
1167
+ ]);
1168
+ for (const [k, entry] of this.waiting)
1169
+ if (Date.now() - entry.at > WAITING_TTL_MS) this.waiting.delete(k);
1170
+ const remembered = this.waiting.get(key);
1171
+ const latest = remembered?.latest ?? { run: fn };
1172
+ latest.run = fn;
1173
+ let result;
1174
+ try {
1175
+ result = remembered ? await remembered.resume() : await this.praesidias.guard(
1176
+ mapped.action,
1177
+ mapped.resource,
1178
+ () => latest.run(),
1179
+ {
1180
+ system: mapped.system,
1181
+ context: mapped.context,
1182
+ onApproval: this.onApproval,
1183
+ ...this.approvalTimeoutMs !== void 0 ? { approvalTimeoutMs: this.approvalTimeoutMs } : {}
1184
+ }
1185
+ );
1186
+ } catch (err) {
1187
+ this.waiting.delete(key);
1188
+ if (err instanceof PraesidiasError) {
1189
+ this.log(`${tool} \u2192 ${mapped.action}: not run (${err.code})`);
1190
+ return {
1191
+ status: "unavailable",
1192
+ ran: false,
1193
+ decisionId: null,
1194
+ code: err.code,
1195
+ message: err.code === "AGENT_KIT_NOT_ENABLED" ? `Not run. ${err.message}` : "Not run. Praesidias could not confirm this action is allowed, so it was not carried out. Try again shortly."
1196
+ };
1197
+ }
1198
+ this.log(`${tool} \u2192 ${mapped.action}: ran, and the tool failed`);
1199
+ throw err;
1200
+ }
1201
+ if (result.status === "executed") {
1202
+ this.waiting.delete(key);
1203
+ this.log(`${tool} \u2192 ${mapped.action}: allowed${result.audit ? " (Audit mode)" : ""}`);
1204
+ return {
1205
+ status: "executed",
1206
+ ran: true,
1207
+ value: result.value,
1208
+ message: "Ran.",
1209
+ decisionId: result.decisionId,
1210
+ audit: result.audit !== null
1211
+ };
1212
+ }
1213
+ if (result.status === "blocked") {
1214
+ this.waiting.delete(key);
1215
+ this.log(`${tool} \u2192 ${mapped.action}: blocked (${result.reason.code})`);
1216
+ return {
1217
+ status: "blocked",
1218
+ ran: false,
1219
+ decisionId: result.decisionId,
1220
+ code: result.reason.code,
1221
+ consoleUrl: result.consoleUrl,
1222
+ message: `Blocked by Praesidias. ${result.reason.message}${result.consoleUrl ? ` Details: ${result.consoleUrl}` : ""}`
1223
+ };
1224
+ }
1225
+ const resume = result.resume;
1226
+ this.waiting.set(key, {
1227
+ at: Date.now(),
1228
+ latest,
1229
+ resume: () => resume(
1230
+ this.approvalTimeoutMs !== void 0 ? { approvalTimeoutMs: this.approvalTimeoutMs } : { approvalTimeoutMs: 0 }
1231
+ )
1232
+ });
1233
+ this.log(`${tool} \u2192 ${mapped.action}: needs approval`);
1234
+ const have = result.approvalsNeeded ? ` (${result.approvalsHave} of ${result.approvalsNeeded} so far)` : "";
1235
+ return {
1236
+ status: "needs_approval",
1237
+ ran: false,
1238
+ decisionId: result.decisionId,
1239
+ approvalsHave: result.approvalsHave,
1240
+ approvalsNeeded: result.approvalsNeeded,
1241
+ consoleUrl: result.consoleUrl,
1242
+ message: `Needs approval. This action was not run: a person has to approve it first${have}.${result.consoleUrl ? ` They can approve it here: ${result.consoleUrl}.` : ""} Once it is approved, make the same request again.`
1243
+ };
1244
+ }
1245
+ /**
1246
+ * A tool function that governs itself. When the action is not allowed it does not throw: it
1247
+ * returns the sentence saying why, which is what a model needs to read in order to tell the
1248
+ * user or to try again later. Pass `refuse` to turn that sentence into your framework's shape.
1249
+ */
1250
+ wrap(tool, fn, refuse) {
1251
+ return async (args) => {
1252
+ const outcome = await this.run(tool, args, () => fn(args));
1253
+ if (outcome.ran) return outcome.value;
1254
+ return refuse ? refuse(outcome) : outcome.message;
1255
+ };
1256
+ }
1257
+ };
1258
+ function createToolGate(praesidias, mapping, options) {
1259
+ return new ToolGate(praesidias, mapping, options);
1260
+ }
1261
+
1262
+ // src/agent-actions.ts
1263
+ var import_node_crypto4 = require("crypto");
1264
+ var import_node_path = require("path");
1265
+
1266
+ // src/shell.ts
1267
+ var UNPARSED = "__unparsed__";
1268
+ var MAX_PARTS = 20;
1269
+ var MAX_COMMAND = 2e3;
1270
+ var MAX_INPUT = 1e5;
1271
+ var MAX_FLAGS = 50;
1272
+ var MAX_FLAG_LENGTH = 100;
1273
+ var MAX_SUBCOMMAND = 200;
1274
+ var Unparsed = class extends Error {
1275
+ };
1276
+ var unparsed = () => {
1277
+ throw new Unparsed();
1278
+ };
1279
+ function split(command, dialect) {
1280
+ const posix2 = dialect === "posix";
1281
+ const parts = [];
1282
+ let words = [];
1283
+ let word = "";
1284
+ let inWord = false;
1285
+ let dynamic = false;
1286
+ let quoted = false;
1287
+ let nextIsRedirect = false;
1288
+ let heredocNext = null;
1289
+ const heredocs = [];
1290
+ let partStart = 0;
1291
+ let commentAt = -1;
1292
+ const n = command.length;
1293
+ const endWord = () => {
1294
+ if (!inWord) return;
1295
+ if (heredocNext) {
1296
+ heredocs.push({ delimiter: word, quoted, strip: heredocNext.strip });
1297
+ heredocNext = null;
1298
+ } else {
1299
+ words.push({ text: word, dynamic, redirect: nextIsRedirect });
1300
+ }
1301
+ nextIsRedirect = false;
1302
+ word = "";
1303
+ inWord = false;
1304
+ dynamic = false;
1305
+ quoted = false;
1306
+ };
1307
+ const endPart = (end) => {
1308
+ endWord();
1309
+ if (nextIsRedirect || heredocNext) unparsed();
1310
+ const text = command.slice(partStart, commentAt === -1 ? end : Math.min(end, commentAt)).trim();
1311
+ if (words.length > 0) parts.push({ words, text });
1312
+ words = [];
1313
+ commentAt = -1;
1314
+ };
1315
+ const readHeredocs = (from) => {
1316
+ let i2 = from;
1317
+ for (const doc of heredocs) {
1318
+ for (; ; ) {
1319
+ if (i2 >= n) break;
1320
+ let end = command.indexOf("\n", i2);
1321
+ if (end === -1) end = n;
1322
+ const line = command.slice(i2, end);
1323
+ i2 = end + 1;
1324
+ const compare = doc.strip ? line.replace(/^\t+/, "") : line;
1325
+ if (compare === doc.delimiter) break;
1326
+ if (!doc.quoted && (line.includes("$(") || line.includes("`"))) unparsed();
1327
+ }
1328
+ }
1329
+ heredocs.length = 0;
1330
+ return Math.min(i2, n);
1331
+ };
1332
+ let i = 0;
1333
+ while (i < n) {
1334
+ const c = command[i];
1335
+ const next = command[i + 1];
1336
+ if (c === "'") {
1337
+ let close = command.indexOf("'", i + 1);
1338
+ if (close === -1) unparsed();
1339
+ word += command.slice(i + 1, close);
1340
+ while (!posix2 && command[close + 1] === "'") {
1341
+ const again = command.indexOf("'", close + 2);
1342
+ if (again === -1) unparsed();
1343
+ word += `'${command.slice(close + 2, again)}`;
1344
+ close = again;
1345
+ }
1346
+ inWord = true;
1347
+ quoted = true;
1348
+ i = close + 1;
1349
+ continue;
1350
+ }
1351
+ if (c === '"') {
1352
+ let j = i + 1;
1353
+ for (; ; ) {
1354
+ if (j >= n) unparsed();
1355
+ const d = command[j];
1356
+ if (d === '"') break;
1357
+ if (posix2 && d === "\\" && j + 1 < n) {
1358
+ const e = command[j + 1];
1359
+ if (e === "\n") {
1360
+ j += 2;
1361
+ continue;
1362
+ }
1363
+ if ('"\\$`'.includes(e)) {
1364
+ word += e;
1365
+ j += 2;
1366
+ continue;
1367
+ }
1368
+ word += d;
1369
+ j += 1;
1370
+ continue;
1371
+ }
1372
+ if (d === "`" || d === "$" && command[j + 1] === "(") unparsed();
1373
+ if (d === "$") dynamic = true;
1374
+ word += d;
1375
+ j += 1;
1376
+ }
1377
+ inWord = true;
1378
+ quoted = true;
1379
+ i = j + 1;
1380
+ continue;
1381
+ }
1382
+ if (c === "\\" && posix2) {
1383
+ if (next === "\n") {
1384
+ i += 2;
1385
+ continue;
1386
+ }
1387
+ if (next !== void 0) word += next;
1388
+ inWord = true;
1389
+ quoted = true;
1390
+ i += 2;
1391
+ continue;
1392
+ }
1393
+ if (c === "`") unparsed();
1394
+ if (c === "$" && next === "(") unparsed();
1395
+ if (!posix2 && c === "@" && next === "(") unparsed();
1396
+ if ((c === "<" || c === ">") && next === "(") unparsed();
1397
+ if (c === "#" && !inWord) {
1398
+ commentAt = i;
1399
+ const end = command.indexOf("\n", i);
1400
+ i = end === -1 ? n : end;
1401
+ continue;
1402
+ }
1403
+ if (c === " " || c === " " || c === "\r") {
1404
+ endWord();
1405
+ i += 1;
1406
+ continue;
1407
+ }
1408
+ if (c === "\n" || c === ";") {
1409
+ endPart(i);
1410
+ i += 1;
1411
+ if (c === "\n" && posix2 && heredocs.length > 0) i = readHeredocs(i);
1412
+ partStart = i;
1413
+ continue;
1414
+ }
1415
+ if (c === "&") {
1416
+ if (next === ">") {
1417
+ endWord();
1418
+ i += command[i + 2] === ">" ? 3 : 2;
1419
+ nextIsRedirect = true;
1420
+ continue;
1421
+ }
1422
+ endPart(i);
1423
+ i += next === "&" ? 2 : 1;
1424
+ partStart = i;
1425
+ continue;
1426
+ }
1427
+ if (c === "|") {
1428
+ endPart(i);
1429
+ i += next === "|" || next === "&" ? 2 : 1;
1430
+ partStart = i;
1431
+ continue;
1432
+ }
1433
+ if (c === "(" || c === ")") {
1434
+ endPart(i);
1435
+ i += 1;
1436
+ partStart = i;
1437
+ continue;
1438
+ }
1439
+ if (c === "<" || c === ">") {
1440
+ if (inWord && !quoted && /^[0-9]+$/.test(word)) {
1441
+ word = "";
1442
+ inWord = false;
1443
+ }
1444
+ endWord();
1445
+ if (c === "<" && next === "<") {
1446
+ if (command[i + 2] === "<") {
1447
+ i += 3;
1448
+ nextIsRedirect = true;
1449
+ continue;
1450
+ }
1451
+ const strip = command[i + 2] === "-";
1452
+ i += strip ? 3 : 2;
1453
+ heredocNext = { strip };
1454
+ continue;
1455
+ }
1456
+ i += 1;
1457
+ if (command[i] === ">" || command[i] === "|" || command[i] === "&" || command[i] === "<") {
1458
+ if (command[i] === "&") {
1459
+ i += 1;
1460
+ const m = /^([0-9]+|-)/.exec(command.slice(i));
1461
+ if (m) {
1462
+ i += m[0].length;
1463
+ continue;
1464
+ }
1465
+ } else {
1466
+ i += 1;
1467
+ }
1468
+ }
1469
+ nextIsRedirect = true;
1470
+ continue;
1471
+ }
1472
+ if (c === "$" || c === "*" || c === "?") dynamic = true;
1473
+ word += c;
1474
+ inWord = true;
1475
+ i += 1;
1476
+ }
1477
+ endPart(n);
1478
+ return parts;
1479
+ }
1480
+ var KEYWORDS = /* @__PURE__ */ new Set([
1481
+ "if",
1482
+ "then",
1483
+ "else",
1484
+ "elif",
1485
+ "fi",
1486
+ "do",
1487
+ "done",
1488
+ "while",
1489
+ "until",
1490
+ "!",
1491
+ "{",
1492
+ "}",
1493
+ "esac"
1494
+ ]);
1495
+ var KEEP_KEYWORDS = /* @__PURE__ */ new Set(["for", "case", "select", "function", "coproc"]);
1496
+ var SHELLS = /* @__PURE__ */ new Set([
1497
+ "sh",
1498
+ "bash",
1499
+ "zsh",
1500
+ "dash",
1501
+ "ksh",
1502
+ "mksh",
1503
+ "ash",
1504
+ "fish",
1505
+ "csh",
1506
+ "tcsh",
1507
+ "busybox"
1508
+ ]);
1509
+ var POWERSHELLS = /* @__PURE__ */ new Set(["powershell", "pwsh"]);
1510
+ var EVALUATORS = /* @__PURE__ */ new Set(["eval", "iex", "invoke-expression"]);
1511
+ var VALUE_FLAGS = {
1512
+ git: ["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--config-env", "--exec-path"],
1513
+ docker: ["-H", "--host", "--context", "-c", "--config", "-l", "--log-level"],
1514
+ kubectl: [
1515
+ "-n",
1516
+ "--namespace",
1517
+ "--context",
1518
+ "--cluster",
1519
+ "--user",
1520
+ "-s",
1521
+ "--server",
1522
+ "--kubeconfig"
1523
+ ],
1524
+ helm: ["-n", "--namespace", "--kube-context", "--kubeconfig"],
1525
+ npm: ["-C", "--prefix", "-w", "--workspace"],
1526
+ pnpm: ["-C", "--dir", "--filter", "-F"],
1527
+ yarn: ["--cwd"],
1528
+ gh: ["-R", "--repo"],
1529
+ cargo: ["-C", "--manifest-path"],
1530
+ terraform: []
1531
+ };
1532
+ var lastSegment = (text) => {
1533
+ const parts = text.split(/[\\/]/);
1534
+ return parts[parts.length - 1] ?? text;
1535
+ };
1536
+ function programName(word) {
1537
+ return lastSegment(word.replace(/^\\+/, "")).toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "");
1538
+ }
1539
+ var isFlag = (text) => text.length > 1 && text.startsWith("-");
1540
+ function skipFlags(words, from, valued) {
1541
+ let i = from;
1542
+ while (i < words.length) {
1543
+ const text = words[i].text;
1544
+ if (text === "--") return i + 1;
1545
+ if (!isFlag(text)) return i;
1546
+ i += valued.has(text) ? 2 : 1;
1547
+ }
1548
+ return i;
1549
+ }
1550
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*\+?=/;
1551
+ function unwrap(words) {
1552
+ let rest = words;
1553
+ for (let depth = 0; depth < 8 && rest.length > 0; depth += 1) {
1554
+ const name = programName(rest[0].text);
1555
+ let from;
1556
+ switch (name) {
1557
+ case "sudo":
1558
+ from = skipFlags(
1559
+ rest,
1560
+ 1,
1561
+ /* @__PURE__ */ new Set([
1562
+ "-u",
1563
+ "-g",
1564
+ "-h",
1565
+ "-p",
1566
+ "-C",
1567
+ "-D",
1568
+ "-r",
1569
+ "-t",
1570
+ "-U",
1571
+ "-T",
1572
+ "--user",
1573
+ "--group",
1574
+ "--host",
1575
+ "--prompt",
1576
+ "--chdir"
1577
+ ])
1578
+ );
1579
+ break;
1580
+ case "doas":
1581
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set(["-u", "-C"]));
1582
+ break;
1583
+ case "env": {
1584
+ let i = 1;
1585
+ while (i < rest.length) {
1586
+ const text = rest[i].text;
1587
+ if (text === "-S" || text.startsWith("--split-string") || /^-[a-zA-Z]*S/.test(text))
1588
+ unparsed();
1589
+ if (text === "--") {
1590
+ i += 1;
1591
+ break;
1592
+ }
1593
+ if (text === "-u" || text === "--unset" || text === "-C" || text === "--chdir") i += 2;
1594
+ else if (isFlag(text) || ASSIGNMENT.test(text)) i += 1;
1595
+ else break;
1596
+ }
1597
+ from = i;
1598
+ break;
1599
+ }
1600
+ case "command":
1601
+ case "builtin":
1602
+ case "nohup":
1603
+ case "time":
1604
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set());
1605
+ break;
1606
+ case "exec":
1607
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set(["-a"]));
1608
+ break;
1609
+ case "nice":
1610
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set(["-n", "--adjustment"]));
1611
+ break;
1612
+ case "ionice":
1613
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set(["-c", "-n", "-p", "--class", "--classdata"]));
1614
+ break;
1615
+ case "stdbuf":
1616
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set(["-i", "-o", "-e", "--input", "--output", "--error"]));
1617
+ break;
1618
+ case "timeout":
1619
+ from = skipFlags(rest, 1, /* @__PURE__ */ new Set(["-s", "-k", "--signal", "--kill-after"]));
1620
+ from += 1;
1621
+ break;
1622
+ case "xargs":
1623
+ from = skipFlags(
1624
+ rest,
1625
+ 1,
1626
+ /* @__PURE__ */ new Set([
1627
+ "-n",
1628
+ "-I",
1629
+ "-L",
1630
+ "-P",
1631
+ "-s",
1632
+ "-d",
1633
+ "-E",
1634
+ "-a",
1635
+ "--max-args",
1636
+ "--max-lines",
1637
+ "--max-procs",
1638
+ "--max-chars",
1639
+ "--delimiter",
1640
+ "--eof",
1641
+ "--arg-file",
1642
+ "--replace"
1643
+ ])
1644
+ );
1645
+ if (from >= rest.length) return rest;
1646
+ break;
1647
+ default:
1648
+ return rest;
1649
+ }
1650
+ if (from >= rest.length) return rest.slice(0, 1);
1651
+ rest = rest.slice(from);
1652
+ while (rest.length > 1 && ASSIGNMENT.test(rest[0].text)) rest = rest.slice(1);
1653
+ }
1654
+ return rest;
1655
+ }
1656
+ function flagsOf(args, dialect) {
1657
+ const out = [];
1658
+ const add = (flag) => {
1659
+ const cut = clipText(flag, MAX_FLAG_LENGTH);
1660
+ if (!out.includes(cut) && out.length < MAX_FLAGS) out.push(cut);
1661
+ };
1662
+ for (const arg of args) {
1663
+ if (arg === "--") break;
1664
+ if (!isFlag(arg)) continue;
1665
+ add(arg);
1666
+ if (arg.startsWith("--")) {
1667
+ const eq = arg.indexOf("=");
1668
+ if (eq > 2) add(arg.slice(0, eq));
1669
+ } else if (dialect === "posix" && /^-[A-Za-z]{2,}$/.test(arg)) {
1670
+ for (const letter of arg.slice(1)) add(`-${letter}`);
1671
+ }
1672
+ }
1673
+ return out;
1674
+ }
1675
+ function subcommandOf(program, args) {
1676
+ const valued = new Set(VALUE_FLAGS[program] ?? []);
1677
+ for (let i = 0; i < args.length; i += 1) {
1678
+ const arg = args[i];
1679
+ if (arg === "--") return clipText(args[i + 1] ?? "", MAX_SUBCOMMAND);
1680
+ if (isFlag(arg)) {
1681
+ if (valued.has(arg)) i += 1;
1682
+ continue;
1683
+ }
1684
+ return clipText(arg, MAX_SUBCOMMAND);
1685
+ }
1686
+ return "";
1687
+ }
1688
+ function clipText(text, max) {
1689
+ if (text.length <= max) return text;
1690
+ let end = max;
1691
+ const last = text.charCodeAt(end - 1);
1692
+ if (last >= 55296 && last <= 56319) end -= 1;
1693
+ return text.slice(0, end);
1694
+ }
1695
+ var clip = (text) => clipText(text, MAX_COMMAND);
1696
+ function analyse(raw, out, dialect, nested = false) {
1697
+ let words = raw.words.filter((w) => !w.redirect);
1698
+ while (words.length > 0 && ASSIGNMENT.test(words[0].text)) words = words.slice(1);
1699
+ while (words.length > 0 && KEYWORDS.has(words[0].text)) words = words.slice(1);
1700
+ if (words.length === 0) return;
1701
+ if (KEEP_KEYWORDS.has(words[0].text)) {
1702
+ out.push({ program: words[0].text, subcommand: "", flags: [], command: clip(raw.text) });
1703
+ return;
1704
+ }
1705
+ words = unwrap(words);
1706
+ const first = words[0];
1707
+ if (first.dynamic) unparsed();
1708
+ const program = programName(first.text);
1709
+ if (!program) unparsed();
1710
+ const args = words.slice(1).map((w) => w.text);
1711
+ if (EVALUATORS.has(program)) unparsed();
1712
+ if (SHELLS.has(program) && args.some((a) => /^-[a-zA-Z]*c[a-zA-Z]*$/.test(a))) unparsed();
1713
+ if (POWERSHELLS.has(program) && args.some((a) => /^[-/](c|command|e|ec|enc|encodedcommand)$/i.test(a)))
1714
+ unparsed();
1715
+ if (program === "cmd" && args.some((a) => /^\/[ckr]$/i.test(a))) unparsed();
1716
+ const execAt = program === "find" ? args.findIndex((a) => ["-exec", "-execdir", "-ok", "-okdir"].includes(a)) : -1;
1717
+ out.push({
1718
+ program,
1719
+ subcommand: subcommandOf(program, args),
1720
+ flags: flagsOf(execAt === -1 ? args : args.slice(0, execAt + 1), dialect),
1721
+ command: clip(nested ? words.map((w) => w.text).join(" ") : raw.text)
1722
+ });
1723
+ if (program === "find") {
1724
+ for (let i = 0; i < args.length; i += 1) {
1725
+ if (!["-exec", "-execdir", "-ok", "-okdir"].includes(args[i])) continue;
1726
+ const inner = [];
1727
+ let j = i + 1;
1728
+ for (; j < args.length && args[j] !== ";" && args[j] !== "+"; j += 1) {
1729
+ inner.push(words[j + 1]);
1730
+ }
1731
+ if (inner.length > 0) analyse({ words: inner, text: "" }, out, dialect, true);
1732
+ i = j;
1733
+ }
1734
+ }
1735
+ }
1736
+ function parseShellCommand(command, dialect = "posix") {
1737
+ if (typeof command !== "string" || command.length > MAX_INPUT) return { parsed: false };
1738
+ try {
1739
+ const parts = [];
1740
+ for (const raw of split(command, dialect)) {
1741
+ analyse(raw, parts, dialect);
1742
+ if (parts.length > MAX_PARTS) return { parsed: false };
1743
+ }
1744
+ return { parsed: true, parts };
1745
+ } catch (err) {
1746
+ if (err instanceof Unparsed) return { parsed: false };
1747
+ throw err;
1748
+ }
1749
+ }
1750
+ function unparsedPart(command) {
1751
+ return { program: UNPARSED, subcommand: "", flags: [], command: clip(String(command ?? "")) };
1752
+ }
1753
+
1754
+ // src/agent-actions.ts
1755
+ var MAX_ACTION_LENGTH = 64;
1756
+ var MAX_TEXT = 2e3;
1757
+ var MAX_INPUT_TEXT = 1024;
1758
+ var MAX_QUERY = 200;
1759
+ var MAX_DESCRIPTION = 500;
1760
+ var MAX_RESOURCE_ID = 128;
1761
+ var SENSITIVE = [
1762
+ /^\.env$/,
1763
+ /^\.env\..+$/,
1764
+ /^.*\.pem$/,
1765
+ /^.*\.key$/,
1766
+ /^id_rsa.*$/,
1767
+ /^id_ed25519.*$/,
1768
+ /^\.npmrc$/,
1769
+ /^\.pypirc$/,
1770
+ /^\.netrc$/,
1771
+ /^credentials.*$/,
1772
+ /^.*\.p12$/,
1773
+ /^.*\.pfx$/,
1774
+ /^\.aws$/,
1775
+ /^\.ssh$/,
1776
+ /^\.gnupg$/,
1777
+ /^secrets\..+$/
1778
+ ];
1779
+ var sensitiveName = (segment) => {
1780
+ const name = segment.toLowerCase();
1781
+ return name.length > 0 && SENSITIVE.some((pattern) => pattern.test(name));
1782
+ };
1783
+ function isSensitivePath(path) {
1784
+ return String(path ?? "").split(/[\\/]+/).some((segment) => sensitiveName(segment) || sensitiveName(segment.replace(/[*?]/g, "")));
1785
+ }
1786
+ var clip2 = clipText;
1787
+ var str = (value) => typeof value === "string" ? value : "";
1788
+ function fitActionName(name) {
1789
+ if (name.length <= MAX_ACTION_LENGTH) return name;
1790
+ const hash = (0, import_node_crypto4.createHash)("sha256").update(name).digest("hex").slice(0, 8);
1791
+ return `${name.slice(0, MAX_ACTION_LENGTH - 9)}-${hash}`;
1792
+ }
1793
+ var part = (text) => text.toLowerCase().replace(/[^a-z0-9_-]/g, "_") || "_";
1794
+ function mcpAction(tool) {
1795
+ const match = /^mcp__(.+?)__(.+)$/.exec(tool);
1796
+ if (!match) return null;
1797
+ const server = part(match[1]);
1798
+ const name = part(match[2]);
1799
+ return { action: fitActionName(`mcp.${server}.${name}`), server, tool: name };
1800
+ }
1801
+ function otherAction(agent, tool) {
1802
+ return fitActionName(`${agent}.${part(tool)}`);
1803
+ }
1804
+ function pathFacts(raw, call2) {
1805
+ const platform = call2.platform ?? (process.platform === "win32" ? "win32" : "posix");
1806
+ const lib = platform === "win32" ? import_node_path.win32 : import_node_path.posix;
1807
+ const cwd = call2.cwd || lib.resolve(".");
1808
+ const project = call2.projectDir || cwd;
1809
+ const home = call2.home ? lib.resolve(call2.home) : "";
1810
+ const expanded = home && /^~([\\/]|$)/.test(raw) ? lib.join(home, raw.slice(1)) : raw;
1811
+ const absolute = lib.resolve(cwd, expanded || ".");
1812
+ const inside = lib.relative(project, absolute);
1813
+ const inProject = inside === "" || !inside.startsWith("..") && !lib.isAbsolute(inside) && inside !== "..";
1814
+ const fromHome = home ? lib.relative(home, absolute) : "..";
1815
+ let shown;
1816
+ if (inProject) shown = inside === "" ? "." : inside;
1817
+ else if (home && (fromHome === "" || !fromHome.startsWith("..") && !lib.isAbsolute(fromHome)))
1818
+ shown = fromHome === "" ? "~" : `~/${fromHome}`;
1819
+ else shown = absolute;
1820
+ shown = shown.split("\\").join("/");
1821
+ return {
1822
+ path: clip2(shown, MAX_TEXT),
1823
+ sensitive: isSensitivePath(absolute) || isSensitivePath(raw),
1824
+ in_project: inProject
1825
+ };
1826
+ }
1827
+ var file = (action, facts, extra) => ({
1828
+ action,
1829
+ resourceType: "file",
1830
+ resourceId: clip2(facts.path, MAX_RESOURCE_ID),
1831
+ context: action === "files.read" ? { path: facts.path, sensitive: facts.sensitive || (extra ? isSensitivePath(extra) : false) } : { path: facts.path, sensitive: facts.sensitive, in_project: facts.in_project }
1832
+ });
1833
+ var shellAction = (p) => ({
1834
+ action: "shell.run",
1835
+ resourceType: "command",
1836
+ resourceId: clip2(p.program, MAX_RESOURCE_ID) || null,
1837
+ context: { program: p.program, subcommand: p.subcommand, flags: p.flags, command: p.command }
1838
+ });
1839
+ function shellActions(command, dialect = "posix") {
1840
+ const parsed = parseShellCommand(command, dialect);
1841
+ if (!parsed.parsed) return [shellAction(unparsedPart(command))];
1842
+ if (parsed.parts.length === 0)
1843
+ return [shellAction({ program: "", subcommand: "", flags: [], command: "" })];
1844
+ return parsed.parts.map(shellAction);
1845
+ }
1846
+ function patchPaths(patch) {
1847
+ const paths = [];
1848
+ for (const line of String(patch ?? "").split(/\r?\n/)) {
1849
+ const match = /^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/.exec(line);
1850
+ if (match) {
1851
+ const path = match[1].trim();
1852
+ if (path && !paths.includes(path)) paths.push(path);
1853
+ }
1854
+ }
1855
+ return paths;
1856
+ }
1857
+ function mcpContext(input, declared) {
1858
+ const out = {};
1859
+ if (!input || typeof input !== "object" || Array.isArray(input)) return out;
1860
+ const allowed = declared ? new Set(declared) : null;
1861
+ for (const [name, value] of Object.entries(input)) {
1862
+ if (allowed && !allowed.has(name)) continue;
1863
+ if (name.length > 64) continue;
1864
+ if (typeof value === "string") out[name] = clip2(value, MAX_INPUT_TEXT);
1865
+ else if (typeof value === "number" || typeof value === "boolean" || value === null)
1866
+ out[name] = value;
1867
+ else if (typeof value === "object") {
1868
+ const text = JSON.stringify(value);
1869
+ if (text !== void 0 && text.length <= MAX_TEXT) out[name] = value;
1870
+ }
1871
+ }
1872
+ return out;
1873
+ }
1874
+ var shellQuote = (arg) => /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `'${arg.split("'").join("'\\''")}'`;
1875
+ var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "Grep", "Glob", "LS"]);
1876
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
1877
+ var DELEGATE_TOOLS = /* @__PURE__ */ new Set(["Task", "Agent", "spawn_agent"]);
1878
+ function agentToolActions(call2) {
1879
+ const tool = String(call2.tool ?? "");
1880
+ const input = call2.input && typeof call2.input === "object" ? call2.input : {};
1881
+ if (tool === "Bash" || tool === "shell" || tool === "exec_command" || tool === "local_shell") {
1882
+ const command = Array.isArray(input.command) ? input.command.map((arg) => shellQuote(String(arg))).join(" ") : str(input.command) || str(input.cmd);
1883
+ return shellActions(command);
1884
+ }
1885
+ if (tool === "PowerShell") return shellActions(str(input.command), "powershell");
1886
+ if (tool === "write_stdin") return [shellAction(unparsedPart(str(input.chars)))];
1887
+ if (READ_TOOLS.has(tool)) {
1888
+ const raw = str(input.file_path) || str(input.path) || ".";
1889
+ const pattern = tool === "Glob" ? str(input.pattern) : tool === "Grep" ? str(input.glob) : "";
1890
+ return [file("files.read", pathFacts(raw, call2), pattern)];
1891
+ }
1892
+ if (WRITE_TOOLS.has(tool)) {
1893
+ const raw = str(input.file_path) || str(input.notebook_path) || str(input.path);
1894
+ return [file("files.write", pathFacts(raw, call2))];
1895
+ }
1896
+ if (tool === "apply_patch") {
1897
+ const paths = patchPaths(str(input.command) || str(input.patch) || str(input.input));
1898
+ if (paths.length === 0) {
1899
+ return [
1900
+ {
1901
+ action: "files.write",
1902
+ resourceType: "file",
1903
+ resourceId: null,
1904
+ context: { path: "", sensitive: true, in_project: false }
1905
+ }
1906
+ ];
1907
+ }
1908
+ return paths.map((path) => file("files.write", pathFacts(path, call2)));
1909
+ }
1910
+ if (tool === "WebFetch") {
1911
+ const url = str(input.url);
1912
+ let host = "";
1913
+ try {
1914
+ host = new URL(url).hostname.toLowerCase();
1915
+ } catch {
1916
+ host = "";
1917
+ }
1918
+ return [
1919
+ {
1920
+ action: "web.fetch",
1921
+ resourceType: "url",
1922
+ resourceId: clip2(host, MAX_RESOURCE_ID) || null,
1923
+ context: { host, url: clip2(url, MAX_TEXT) }
1924
+ }
1925
+ ];
1926
+ }
1927
+ if (tool === "WebSearch") {
1928
+ return [
1929
+ {
1930
+ action: "web.search",
1931
+ resourceType: "web_search",
1932
+ resourceId: null,
1933
+ context: { query: clip2(str(input.query), MAX_QUERY) }
1934
+ }
1935
+ ];
1936
+ }
1937
+ if (DELEGATE_TOOLS.has(tool)) {
1938
+ const description = str(input.description) || str(input.prompt) || str(input.message) || str(input.task);
1939
+ const kind = str(input.subagent_type) || str(input.agent_type);
1940
+ return [
1941
+ {
1942
+ action: "agent.delegate",
1943
+ resourceType: "agent",
1944
+ resourceId: kind ? clip2(kind, MAX_RESOURCE_ID) : null,
1945
+ context: { description: clip2(description, MAX_DESCRIPTION) }
1946
+ }
1947
+ ];
1948
+ }
1949
+ const mcp = mcpAction(tool);
1950
+ if (mcp) {
1951
+ return [
1952
+ {
1953
+ action: mcp.action,
1954
+ resourceType: "mcp_tool",
1955
+ resourceId: clip2(`${mcp.server}.${mcp.tool}`, MAX_RESOURCE_ID),
1956
+ context: mcpContext(call2.input, call2.declaredInputs)
1957
+ }
1958
+ ];
1959
+ }
1960
+ return [
1961
+ {
1962
+ action: otherAction(call2.agent, tool),
1963
+ resourceType: "tool",
1964
+ resourceId: clip2(tool, MAX_RESOURCE_ID) || null,
1965
+ context: { tool: clip2(tool, MAX_TEXT) }
1966
+ }
1967
+ ];
1968
+ }
1969
+ var BUILT_IN_INPUTS = {
1970
+ "shell.run": {
1971
+ program: { type: "string", description: "The program run, without its folder" },
1972
+ subcommand: { type: "string", description: "Its first argument that is not a flag" },
1973
+ flags: { type: "array", description: "Its flags" },
1974
+ command: { type: "string", description: "The command as written (up to 2,000 characters)" }
1975
+ },
1976
+ "files.read": {
1977
+ path: { type: "string", description: "The file or folder" },
1978
+ sensitive: { type: "boolean", description: "A secret file: .env, keys, credentials" }
1979
+ },
1980
+ "files.write": {
1981
+ path: { type: "string", description: "The file" },
1982
+ sensitive: { type: "boolean", description: "A secret file: .env, keys, credentials" },
1983
+ in_project: { type: "boolean", description: "Inside this project" }
1984
+ },
1985
+ "web.fetch": {
1986
+ host: { type: "string", description: "The web address\u2019s host" },
1987
+ url: { type: "string", description: "The web address" }
1988
+ },
1989
+ "web.search": { query: { type: "string", description: "What is searched for" } },
1990
+ "agent.delegate": {
1991
+ description: { type: "string", description: "What the helper agent is asked to do" }
1992
+ }
1993
+ };
1994
+ // Annotate the CommonJS export names for ESM import in node:
1995
+ 0 && (module.exports = {
1996
+ BUILT_IN_INPUTS,
1997
+ MAX_ACTION_LENGTH,
1998
+ MappingError,
1999
+ Praesidias,
2000
+ PraesidiasAuthError,
2001
+ PraesidiasConfigError,
2002
+ PraesidiasError,
2003
+ PraesidiasRequestError,
2004
+ PraesidiasUnavailableError,
2005
+ TOOLS_PER_REPORT,
2006
+ ToolGate,
2007
+ UNPARSED,
2008
+ WORKLOAD_TOKEN_HEADER,
2009
+ agentToolActions,
2010
+ checkMapping,
2011
+ createToolGate,
2012
+ fitActionName,
2013
+ inputsOfSchema,
2014
+ isSensitivePath,
2015
+ loadMapping,
2016
+ mapToolCall,
2017
+ matches,
2018
+ mcpAction,
2019
+ otherAction,
2020
+ parseShellCommand,
2021
+ patchPaths,
2022
+ sentenceFor,
2023
+ shellActions,
2024
+ subjectIdFor,
2025
+ toolAction,
2026
+ toolsForReport
2027
+ });