@ory/argus 0.14.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +31 -46
  2. package/assets/commands/temporal-up.md +1 -1
  3. package/assets/skills/auth-setup/SKILL.md +1 -1
  4. package/assets/skills/local-dev/SKILL.md +17 -7
  5. package/assets/skills/ory-build-agent/SKILL.md +39 -91
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
  7. package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +88 -32
  10. package/dist/adapters.js +443 -164
  11. package/dist/agent-auth.d.ts +226 -67
  12. package/dist/agent-auth.js +951 -205
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +33 -4
  16. package/dist/auth.js +163 -20
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +77 -16
  20. package/dist/branding.js +93 -23
  21. package/dist/build-info.json +4 -4
  22. package/dist/cli-invocation.d.ts +1 -1
  23. package/dist/cli-invocation.js +2 -1
  24. package/dist/cli.d.ts +20 -29
  25. package/dist/cli.js +271 -239
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +217 -54
  29. package/dist/config.js +461 -42
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +3 -5
  33. package/dist/contract-suite.js +75 -47
  34. package/dist/denial.d.ts +36 -3
  35. package/dist/denial.js +79 -10
  36. package/dist/event-reporter.d.ts +77 -0
  37. package/dist/event-reporter.js +776 -0
  38. package/dist/external-registrations-main.d.ts +10 -0
  39. package/dist/external-registrations-main.js +38 -0
  40. package/dist/external-registrations.d.ts +79 -0
  41. package/dist/external-registrations.js +188 -0
  42. package/dist/help-cli.d.ts +39 -0
  43. package/dist/help-cli.js +55 -0
  44. package/dist/hook-timeout.d.ts +64 -0
  45. package/dist/hook-timeout.js +88 -0
  46. package/dist/index.d.ts +28 -23
  47. package/dist/index.js +173 -58
  48. package/dist/lifecycle.d.ts +3 -3
  49. package/dist/lifecycle.js +38 -6
  50. package/dist/local/cli.js +11 -6
  51. package/dist/local/configs.d.ts +74 -18
  52. package/dist/local/configs.js +291 -84
  53. package/dist/local/health.js +7 -7
  54. package/dist/local/index.d.ts +2 -2
  55. package/dist/local/index.js +24 -10
  56. package/dist/local/manager.d.ts +20 -1
  57. package/dist/local/manager.js +159 -36
  58. package/dist/local/ports.d.ts +158 -0
  59. package/dist/local/ports.js +443 -0
  60. package/dist/local/seed.d.ts +13 -5
  61. package/dist/local/seed.js +62 -38
  62. package/dist/logger.d.ts +54 -25
  63. package/dist/logger.js +329 -63
  64. package/dist/mcp.d.ts +2 -2
  65. package/dist/mcp.js +10 -5
  66. package/dist/mirror-bootstrap.d.ts +48 -0
  67. package/dist/mirror-bootstrap.js +254 -0
  68. package/dist/opl.d.ts +289 -0
  69. package/dist/opl.js +446 -0
  70. package/dist/permission-mode.d.ts +87 -0
  71. package/dist/permission-mode.js +307 -0
  72. package/dist/permissions-cli.d.ts +13 -49
  73. package/dist/permissions-cli.js +154 -348
  74. package/dist/permissions.d.ts +148 -38
  75. package/dist/permissions.js +591 -45
  76. package/dist/post-install.d.ts +33 -0
  77. package/dist/post-install.js +127 -0
  78. package/dist/read-credential.d.ts +65 -0
  79. package/dist/read-credential.js +86 -0
  80. package/dist/registry/cli.js +5 -2
  81. package/dist/registry/config.d.ts +0 -17
  82. package/dist/registry/config.js +0 -23
  83. package/dist/registry/index.d.ts +1 -1
  84. package/dist/registry/index.js +2 -2
  85. package/dist/registry/manager.d.ts +4 -21
  86. package/dist/registry/manager.js +21 -37
  87. package/dist/runtime-credential.d.ts +140 -0
  88. package/dist/runtime-credential.js +572 -0
  89. package/dist/runtime.d.ts +408 -0
  90. package/dist/runtime.js +748 -0
  91. package/dist/setup.d.ts +23 -47
  92. package/dist/setup.js +59 -116
  93. package/dist/skills.js +0 -7
  94. package/dist/status-cli.d.ts +27 -11
  95. package/dist/status-cli.js +121 -115
  96. package/dist/status-data.d.ts +116 -17
  97. package/dist/status-data.js +121 -38
  98. package/dist/subject.d.ts +126 -20
  99. package/dist/subject.js +215 -30
  100. package/dist/testing.d.ts +74 -38
  101. package/dist/testing.js +185 -68
  102. package/dist/tool-catalog.d.ts +53 -11
  103. package/dist/tool-catalog.js +164 -13
  104. package/dist/tool-metadata.d.ts +7 -6
  105. package/dist/tool-metadata.js +6 -5
  106. package/dist/types.d.ts +11 -1
  107. package/dist/uninstall.d.ts +71 -34
  108. package/dist/uninstall.js +217 -85
  109. package/dist/user-login.d.ts +9 -10
  110. package/dist/user-login.js +56 -75
  111. package/dist/watch-cli.d.ts +6 -0
  112. package/dist/watch-cli.js +217 -0
  113. package/package.json +4 -27
  114. package/assets/commands/dashboard.md +0 -34
  115. package/dist/dashboard-cli.d.ts +0 -8
  116. package/dist/dashboard-cli.js +0 -70
  117. package/dist/dev.d.ts +0 -103
  118. package/dist/dev.js +0 -583
  119. package/dist/interactive-setup.d.ts +0 -286
  120. package/dist/interactive-setup.js +0 -1734
  121. package/dist/local/jaeger-main.d.ts +0 -13
  122. package/dist/local/jaeger-main.js +0 -85
  123. package/dist/local/jaeger.d.ts +0 -50
  124. package/dist/local/jaeger.js +0 -162
  125. package/dist/otel/exporter.d.ts +0 -17
  126. package/dist/otel/exporter.js +0 -12
  127. package/dist/otel/index.d.ts +0 -2
  128. package/dist/otel/index.js +0 -8
  129. package/dist/otel/otlp.d.ts +0 -103
  130. package/dist/otel/otlp.js +0 -385
  131. package/dist/project-api-key.d.ts +0 -69
  132. package/dist/project-api-key.js +0 -147
  133. package/dist/setup-actions.d.ts +0 -232
  134. package/dist/setup-actions.js +0 -507
  135. package/dist/tracer.d.ts +0 -190
  136. package/dist/tracer.js +0 -481
  137. package/dist/watch-sandbox.d.ts +0 -9
  138. package/dist/watch-sandbox.js +0 -81
  139. package/dist/web/api.d.ts +0 -33
  140. package/dist/web/api.js +0 -294
  141. package/dist/web/launch.d.ts +0 -11
  142. package/dist/web/launch.js +0 -96
  143. package/dist/web/server.d.ts +0 -20
  144. package/dist/web/server.js +0 -233
  145. package/dist/web/types.d.ts +0 -65
  146. package/dist/web/types.js +0 -2
  147. package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
  148. package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
  149. package/dist/webapp/favicon.ico +0 -0
  150. package/dist/webapp/index.html +0 -15
@@ -0,0 +1,776 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AgentSecurityEventReporter = exports.SERVER_ACTIVITY_EVENTS = void 0;
37
+ exports.projectActivityEntry = projectActivityEntry;
38
+ exports.eventOutboxProjectScope = eventOutboxProjectScope;
39
+ exports.eventPrincipalKey = eventPrincipalKey;
40
+ const node_crypto_1 = require("node:crypto");
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ exports.SERVER_ACTIVITY_EVENTS = new Set([
44
+ "session.start", "session.end", "session.verify",
45
+ "tool.invoke", "tool.complete", "tool.block", "tool.fail",
46
+ "subagent.start", "subagent.stop",
47
+ "user.auth", "agent.auth", "agent.session.retire", "agent.retired.drain",
48
+ "oauth2.login", "oauth2.refresh", "oauth2.introspect",
49
+ "permission.mode_resolve", "permission.observe_deny", "permission.block_observed",
50
+ "bash.parser_unavailable", "user.interaction",
51
+ ]);
52
+ const STRING_ATTRIBUTES = new Set([
53
+ "blockReason", "composerMode", "harness", "kind", "mcpServer", "mcpTool",
54
+ "mode", "model", "permissionMode", "source", "stage", "status",
55
+ "subAgentType", "toolName", "trigger", "alertKind", "severity",
56
+ "blockedPrincipalLevel", "failureType", "namespace", "relation", "reasonCode",
57
+ "scope", "postureSource", "object", "revoke", "delegation", "delegationKind",
58
+ ]);
59
+ const HASH_ATTRIBUTES = new Set([
60
+ "bashCommandHash", "inputHash", "outputHash", "filePathHash", "urlHostHash",
61
+ "errorHash", "reasonHash", "blockedPrincipalHash", "subjectHash",
62
+ ]);
63
+ const NUMBER_ATTRIBUTES = new Set([
64
+ "bashCommandBytes", "bashExitCode", "bashStderrBytes", "bashStdoutBytes", "count",
65
+ "durationMs", "inputBytes", "outputBytes", "responseLen", "shellCommandCount", "summaryLen",
66
+ "filePathBytes", "errorBytes", "reasonBytes", "executionTimeMs",
67
+ "blockedPrincipalBytes", "subjectBytes", "retired", "keep", "queued", "revoked",
68
+ "expiresAt", "warningsCount",
69
+ ]);
70
+ const BOOL_ATTRIBUTES = new Set([
71
+ "active", "allowed", "hasOutput", "observed", "securityConnected", "shellTooComplex",
72
+ "alert", "blocked", "checkRejected", "hasError", "success",
73
+ "enforceGranted", "proceed", "interactive",
74
+ ]);
75
+ const ARRAY_ATTRIBUTES = new Set(["shellWords", "shellDeniedWords", "warningsHashes"]);
76
+ const SAFE_STRING = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,254}$/;
77
+ const HASH = /^[a-f0-9]{16}$/;
78
+ const MAX_EVENT_AGE_MS = 24 * 60 * 60 * 1_000;
79
+ const MAX_FUTURE_SKEW_MS = 5 * 60 * 1_000;
80
+ const MAX_NUMBER = 1e12;
81
+ const DEFAULT_QUEUE_SIZE = 1_000;
82
+ const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
83
+ const DEFAULT_RETRY_BASE_MS = 250;
84
+ const DEFAULT_RETRY_MAX_MS = 30_000;
85
+ const LOCK_HEARTBEAT_MS = 15_000;
86
+ const LOCK_STALE_MS = 60_000;
87
+ const EVENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
88
+ const ACTIVITY_STATUSES = new Set(["ok", "error", "denied", "skipped"]);
89
+ /** Best-effort delivery of privacy-safe client activity to Agent Security. */
90
+ class AgentSecurityEventReporter {
91
+ options;
92
+ queue = [];
93
+ fetcher;
94
+ maxQueueSize;
95
+ requestTimeoutMs;
96
+ now;
97
+ eventId;
98
+ delivery;
99
+ scheduled = false;
100
+ activeRequest;
101
+ flushDeadline;
102
+ suppressRetry = false;
103
+ retryTimer;
104
+ retryAttempt = 0;
105
+ retryBaseMs;
106
+ retryMaxMs;
107
+ random;
108
+ constructor(options) {
109
+ this.options = options;
110
+ this.fetcher = options.fetch ?? globalThis.fetch;
111
+ this.maxQueueSize = Math.max(1, Math.floor(options.maxQueueSize ?? DEFAULT_QUEUE_SIZE));
112
+ this.requestTimeoutMs = Math.max(1, options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
113
+ this.now = options.now ?? Date.now;
114
+ this.eventId = options.eventId ?? node_crypto_1.randomUUID;
115
+ this.retryBaseMs = Math.max(1, options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS);
116
+ this.retryMaxMs = Math.max(this.retryBaseMs, options.retryMaxMs ?? DEFAULT_RETRY_MAX_MS);
117
+ this.random = options.random ?? Math.random;
118
+ this.syncOutbox();
119
+ }
120
+ get pending() {
121
+ return this.queue.length;
122
+ }
123
+ get pendingScopes() {
124
+ const scopes = new Map();
125
+ for (const pending of this.queue) {
126
+ const scope = pendingScope(pending);
127
+ scopes.set(scopeKey(scope), scope);
128
+ }
129
+ return [...scopes.values()];
130
+ }
131
+ enqueue(entry, principalKey) {
132
+ try {
133
+ const event = projectActivityEntry(entry, this.eventId);
134
+ if (!event)
135
+ return;
136
+ const occurredAt = Date.parse(event.occurred_at);
137
+ if (!Number.isFinite(occurredAt) || occurredAt > this.now() + MAX_FUTURE_SKEW_MS)
138
+ return;
139
+ this.prune();
140
+ const pending = {
141
+ event,
142
+ ...(principalKey ? { principalKey } : {}),
143
+ };
144
+ const file = this.persist(pending);
145
+ this.queue.push({ ...pending, file });
146
+ this.enforceBounds();
147
+ if (this.retryTimer) {
148
+ clearTimeout(this.retryTimer);
149
+ this.retryTimer = undefined;
150
+ }
151
+ this.schedule();
152
+ }
153
+ catch (error) {
154
+ this.diagnostic("events.enqueue_failed", error);
155
+ }
156
+ }
157
+ /** Attempt all currently deliverable batches, bounded by the caller's deadline. */
158
+ async flush(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
159
+ if (timeoutMs <= 0)
160
+ return;
161
+ const deadline = Date.now() + timeoutMs;
162
+ this.flushDeadline = this.flushDeadline === undefined
163
+ ? deadline
164
+ : Math.min(this.flushDeadline, deadline);
165
+ if (this.retryTimer) {
166
+ clearTimeout(this.retryTimer);
167
+ this.retryTimer = undefined;
168
+ }
169
+ const work = this.delivery ?? this.startDelivery();
170
+ let timer;
171
+ await Promise.race([
172
+ work,
173
+ new Promise((resolve) => {
174
+ timer = setTimeout(() => {
175
+ this.suppressRetry = true;
176
+ this.activeRequest?.abort();
177
+ resolve();
178
+ }, timeoutMs);
179
+ timer.unref?.();
180
+ }),
181
+ ]);
182
+ if (timer)
183
+ clearTimeout(timer);
184
+ if (this.flushDeadline === deadline)
185
+ this.flushDeadline = undefined;
186
+ }
187
+ schedule() {
188
+ if (this.scheduled || this.delivery)
189
+ return;
190
+ this.scheduled = true;
191
+ queueMicrotask(() => {
192
+ this.scheduled = false;
193
+ if (!this.delivery)
194
+ this.startDelivery();
195
+ });
196
+ }
197
+ startDelivery() {
198
+ let shouldRetry = false;
199
+ const work = this.drain().then((retry) => {
200
+ shouldRetry = retry;
201
+ }).catch((error) => {
202
+ shouldRetry = true;
203
+ this.diagnostic("events.delivery_failed", error);
204
+ });
205
+ this.delivery = work;
206
+ void work.finally(() => {
207
+ if (this.delivery === work)
208
+ this.delivery = undefined;
209
+ this.suppressRetry = false;
210
+ if (this.queue.length === 0)
211
+ this.retryAttempt = 0;
212
+ else if (shouldRetry)
213
+ this.scheduleRetry();
214
+ }).catch(() => undefined);
215
+ return work;
216
+ }
217
+ scheduleRetry() {
218
+ if (this.retryTimer || this.delivery || this.queue.length === 0)
219
+ return;
220
+ const ceiling = Math.min(this.retryMaxMs, this.retryBaseMs * 2 ** this.retryAttempt++);
221
+ const delay = Math.max(1, Math.floor(ceiling * (0.5 + this.random() * 0.5)));
222
+ this.retryTimer = setTimeout(() => {
223
+ this.retryTimer = undefined;
224
+ if (!this.delivery)
225
+ this.startDelivery();
226
+ }, delay);
227
+ this.retryTimer.unref?.();
228
+ }
229
+ async drain() {
230
+ const releaseDelivery = this.acquireOutboxLock("delivery");
231
+ if (this.options.outboxDir && !releaseDelivery)
232
+ return true;
233
+ let shouldRetry = false;
234
+ try {
235
+ this.prune();
236
+ const retainedScopes = new Set();
237
+ while (this.queue.length > 0) {
238
+ const first = this.queue.find((pending) => !retainedScopes.has(scopeKey(pendingScope(pending))));
239
+ if (!first)
240
+ return shouldRetry;
241
+ const scope = pendingScope(first);
242
+ const key = scopeKey(scope);
243
+ const batch = this.queue
244
+ .filter((pending) => scopeKey(pendingScope(pending)) === key)
245
+ .slice(0, 100);
246
+ const result = await this.deliver(batch.map(({ event }) => event), scope);
247
+ shouldRetry ||= result.retry;
248
+ if (result.remove.size === 0) {
249
+ retainedScopes.add(key);
250
+ continue;
251
+ }
252
+ for (const pending of batch) {
253
+ if (result.remove.has(pending.event.event_id))
254
+ this.removeFile(pending.file);
255
+ }
256
+ for (let index = this.queue.length - 1; index >= 0; index--) {
257
+ if (result.remove.has(this.queue[index].event.event_id))
258
+ this.queue.splice(index, 1);
259
+ }
260
+ if (result.remove.size < batch.length)
261
+ retainedScopes.add(key);
262
+ }
263
+ return shouldRetry;
264
+ }
265
+ finally {
266
+ releaseDelivery?.();
267
+ }
268
+ }
269
+ async deliver(batch, scope) {
270
+ try {
271
+ const result = await this.request(batch, scope);
272
+ if (result === "accepted") {
273
+ return { remove: new Set(batch.map((event) => event.event_id)), retry: false };
274
+ }
275
+ if (result === "reject" && batch.length === 1) {
276
+ return { remove: new Set([batch[0].event_id]), retry: false };
277
+ }
278
+ if (result === "reject") {
279
+ const remove = new Set();
280
+ let retry = false;
281
+ for (const event of batch) {
282
+ const isolated = await this.deliver([event], scope);
283
+ for (const eventId of isolated.remove)
284
+ remove.add(eventId);
285
+ retry ||= isolated.retry;
286
+ }
287
+ return { remove, retry };
288
+ }
289
+ return { remove: new Set(), retry: result === "retain" || result === "retry" };
290
+ }
291
+ catch (error) {
292
+ this.diagnostic("events.delivery_failed", error);
293
+ return { remove: new Set(), retry: true };
294
+ }
295
+ }
296
+ async request(batch, scope) {
297
+ const token = this.options.token?.(scope.sessionId, scope.principalKey);
298
+ const authenticatedRequest = this.options.request;
299
+ if (!token && !authenticatedRequest)
300
+ return "retain";
301
+ const controller = new AbortController();
302
+ this.activeRequest = controller;
303
+ const remaining = this.flushDeadline === undefined
304
+ ? this.requestTimeoutMs
305
+ : Math.max(1, Math.min(this.requestTimeoutMs, this.flushDeadline - Date.now()));
306
+ const timeout = setTimeout(() => controller.abort(), remaining);
307
+ timeout.unref?.();
308
+ try {
309
+ const url = `${this.options.agentSecurityUrl.replace(/\/+$/, "")}/agent-security/v1/events:record`;
310
+ const init = {
311
+ method: "POST",
312
+ headers: {
313
+ "content-type": "application/json",
314
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
315
+ },
316
+ body: JSON.stringify({ project_url: this.options.projectUrl, events: batch }),
317
+ signal: controller.signal,
318
+ };
319
+ const authenticatedResponse = authenticatedRequest?.(url, init, scope.sessionId, scope.principalKey);
320
+ const response = authenticatedResponse
321
+ ? await authenticatedResponse
322
+ : token
323
+ ? await this.fetcher(url, init)
324
+ : undefined;
325
+ if (!response)
326
+ return "retain";
327
+ if (response.status === 400 || response.status === 409 || response.status === 415) {
328
+ this.diagnostic("events.batch_rejected", new Error(`HTTP ${response.status}`));
329
+ return "reject";
330
+ }
331
+ if (response.status === 401) {
332
+ try {
333
+ if (token)
334
+ this.options.onUnauthorized?.(scope.sessionId, scope.principalKey, token);
335
+ }
336
+ catch (error) {
337
+ this.diagnostic("events.credential_invalidation_failed", error);
338
+ }
339
+ return "retain";
340
+ }
341
+ if (response.status === 429 || (response.status >= 500 && response.status <= 599)) {
342
+ return "retry";
343
+ }
344
+ if (response.status !== 202) {
345
+ this.diagnostic("events.delivery_failed", new Error(`HTTP ${response.status}`));
346
+ return "retain";
347
+ }
348
+ let body;
349
+ try {
350
+ body = await abortable(response.json(), controller.signal);
351
+ }
352
+ catch {
353
+ return "retain";
354
+ }
355
+ const ids = body?.event_ids;
356
+ if (!Array.isArray(ids)
357
+ || ids.length !== batch.length
358
+ || ids.some((id, index) => id !== batch[index].event_id)) {
359
+ this.diagnostic("events.invalid_response", new Error("response event IDs did not match request order"));
360
+ return "retain";
361
+ }
362
+ return "accepted";
363
+ }
364
+ finally {
365
+ clearTimeout(timeout);
366
+ if (this.activeRequest === controller)
367
+ this.activeRequest = undefined;
368
+ }
369
+ }
370
+ persist(pending) {
371
+ const dir = this.options.outboxDir;
372
+ if (!dir || !EVENT_ID.test(pending.event.event_id))
373
+ return undefined;
374
+ const file = path.join(dir, `${pending.event.event_id}.json`);
375
+ const temporary = path.join(dir, `.${pending.event.event_id}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`);
376
+ let descriptor;
377
+ try {
378
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
379
+ descriptor = fs.openSync(temporary, "wx", 0o600);
380
+ fs.writeFileSync(descriptor, JSON.stringify({
381
+ event: pending.event,
382
+ ...(pending.principalKey ? { principal_key: pending.principalKey } : {}),
383
+ }), "utf8");
384
+ fs.fsyncSync(descriptor);
385
+ fs.closeSync(descriptor);
386
+ descriptor = undefined;
387
+ fs.renameSync(temporary, file);
388
+ fsyncDirectory(dir);
389
+ return file;
390
+ }
391
+ catch (error) {
392
+ if (descriptor !== undefined) {
393
+ try {
394
+ fs.closeSync(descriptor);
395
+ }
396
+ catch { /* best effort */ }
397
+ }
398
+ this.removeFile(temporary);
399
+ this.diagnostic("events.outbox_write_failed", error);
400
+ return undefined;
401
+ }
402
+ }
403
+ syncOutbox() {
404
+ const dir = this.options.outboxDir;
405
+ if (!dir)
406
+ return;
407
+ const releaseMaintenance = this.acquireOutboxLock("maintenance");
408
+ if (!releaseMaintenance)
409
+ return;
410
+ try {
411
+ let names;
412
+ try {
413
+ names = fs.readdirSync(dir);
414
+ }
415
+ catch (error) {
416
+ if (error.code !== "ENOENT") {
417
+ this.diagnostic("events.outbox_read_failed", error);
418
+ }
419
+ return;
420
+ }
421
+ const cutoff = this.now() - MAX_EVENT_AGE_MS;
422
+ const loaded = [];
423
+ for (const name of names) {
424
+ const file = path.join(dir, name);
425
+ if (name.endsWith(".tmp")) {
426
+ try {
427
+ if (fs.statSync(file).mtimeMs < cutoff)
428
+ this.removeFile(file);
429
+ }
430
+ catch {
431
+ // A concurrent writer may have renamed its temporary file already.
432
+ }
433
+ continue;
434
+ }
435
+ if (!name.endsWith(".json"))
436
+ continue;
437
+ try {
438
+ const stored = JSON.parse(fs.readFileSync(file, "utf8"));
439
+ const pending = parsePendingEvent(stored);
440
+ if (!pending || name !== `${pending.event.event_id}.json`) {
441
+ this.removeFile(file);
442
+ continue;
443
+ }
444
+ const timestamp = Date.parse(pending.event.occurred_at);
445
+ if (!Number.isFinite(timestamp)
446
+ || timestamp < cutoff
447
+ || timestamp > this.now() + MAX_FUTURE_SKEW_MS) {
448
+ this.removeFile(file);
449
+ continue;
450
+ }
451
+ loaded.push({ ...pending, file });
452
+ }
453
+ catch {
454
+ this.removeFile(file);
455
+ }
456
+ }
457
+ loaded.sort(comparePending);
458
+ for (const pending of loaded.splice(0, Math.max(0, loaded.length - this.maxQueueSize))) {
459
+ this.removeFile(pending.file);
460
+ }
461
+ const files = new Set(loaded.map(({ file }) => file));
462
+ for (let index = this.queue.length - 1; index >= 0; index--) {
463
+ const file = this.queue[index].file;
464
+ if (file && !files.has(file))
465
+ this.queue.splice(index, 1);
466
+ }
467
+ const ids = new Set(this.queue.map(({ event }) => event.event_id));
468
+ for (const pending of loaded) {
469
+ if (!ids.has(pending.event.event_id))
470
+ this.queue.push(pending);
471
+ }
472
+ this.queue.sort(comparePending);
473
+ }
474
+ finally {
475
+ releaseMaintenance();
476
+ }
477
+ }
478
+ acquireOutboxLock(name) {
479
+ const dir = this.options.outboxDir;
480
+ if (!dir)
481
+ return () => undefined;
482
+ const lock = path.join(dir, `.${name}.lock`);
483
+ try {
484
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
485
+ this.cleanRenamedLocks(lock);
486
+ for (let attempt = 0; attempt < 2; attempt++) {
487
+ const token = (0, node_crypto_1.randomUUID)();
488
+ try {
489
+ fs.mkdirSync(lock, { mode: 0o700 });
490
+ const owner = path.join(lock, "owner");
491
+ let descriptor;
492
+ try {
493
+ descriptor = fs.openSync(owner, "wx+", 0o600);
494
+ fs.writeFileSync(descriptor, token, "utf8");
495
+ fs.fsyncSync(descriptor);
496
+ }
497
+ catch (error) {
498
+ if (descriptor !== undefined)
499
+ fs.closeSync(descriptor);
500
+ fs.rmSync(lock, { recursive: true, force: true });
501
+ throw error;
502
+ }
503
+ const ownerDescriptor = descriptor;
504
+ const heartbeat = setInterval(() => {
505
+ try {
506
+ if (ownsLockPath(ownerDescriptor, owner, token)) {
507
+ const now = new Date();
508
+ fs.futimesSync(ownerDescriptor, now, now);
509
+ }
510
+ }
511
+ catch { /* best effort */ }
512
+ }, LOCK_HEARTBEAT_MS);
513
+ heartbeat.unref?.();
514
+ return () => {
515
+ clearInterval(heartbeat);
516
+ try {
517
+ if (ownsLockPath(ownerDescriptor, owner, token)) {
518
+ fs.unlinkSync(owner);
519
+ fs.rmdirSync(lock);
520
+ }
521
+ }
522
+ catch { /* best effort */ }
523
+ try {
524
+ fs.closeSync(ownerDescriptor);
525
+ }
526
+ catch { /* best effort */ }
527
+ this.cleanRenamedLocks(lock);
528
+ };
529
+ }
530
+ catch (error) {
531
+ if (error.code !== "EEXIST")
532
+ throw error;
533
+ try {
534
+ const owner = path.join(lock, "owner");
535
+ let modified;
536
+ try {
537
+ modified = fs.statSync(owner).mtimeMs;
538
+ }
539
+ catch {
540
+ modified = fs.statSync(lock).mtimeMs;
541
+ }
542
+ if (this.now() - modified <= LOCK_STALE_MS)
543
+ return undefined;
544
+ const stale = `${lock}.stale.${(0, node_crypto_1.randomUUID)()}`;
545
+ fs.renameSync(lock, stale);
546
+ fs.rmSync(stale, { recursive: true, force: true });
547
+ }
548
+ catch (takeoverError) {
549
+ if (takeoverError.code !== "ENOENT")
550
+ return undefined;
551
+ }
552
+ }
553
+ }
554
+ }
555
+ catch (error) {
556
+ this.diagnostic("events.outbox_lock_failed", error);
557
+ }
558
+ return undefined;
559
+ }
560
+ cleanRenamedLocks(lock) {
561
+ const prefix = `${path.basename(lock)}.stale.`;
562
+ try {
563
+ for (const name of fs.readdirSync(path.dirname(lock))) {
564
+ if (name.startsWith(prefix)) {
565
+ fs.rmSync(path.join(path.dirname(lock), name), { recursive: true, force: true });
566
+ }
567
+ }
568
+ }
569
+ catch { /* best effort */ }
570
+ }
571
+ enforceBounds() {
572
+ this.syncOutbox();
573
+ this.queue.sort(comparePending);
574
+ while (this.queue.length > this.maxQueueSize) {
575
+ const removed = this.queue.shift();
576
+ this.removeFile(removed?.file);
577
+ }
578
+ }
579
+ removeFile(file) {
580
+ if (!file)
581
+ return;
582
+ try {
583
+ fs.unlinkSync(file);
584
+ }
585
+ catch (error) {
586
+ if (error.code !== "ENOENT") {
587
+ this.diagnostic("events.outbox_delete_failed", error);
588
+ }
589
+ }
590
+ }
591
+ prune() {
592
+ this.syncOutbox();
593
+ const cutoff = this.now() - MAX_EVENT_AGE_MS;
594
+ for (let index = this.queue.length - 1; index >= 0; index--) {
595
+ const timestamp = Date.parse(this.queue[index].event.occurred_at);
596
+ if (!Number.isFinite(timestamp) || timestamp < cutoff) {
597
+ this.removeFile(this.queue[index].file);
598
+ this.queue.splice(index, 1);
599
+ }
600
+ }
601
+ }
602
+ diagnostic(event, error) {
603
+ this.options.logger?.debug(event, {
604
+ message: error instanceof Error ? error.message : String(error),
605
+ pending: this.queue.length,
606
+ });
607
+ }
608
+ }
609
+ exports.AgentSecurityEventReporter = AgentSecurityEventReporter;
610
+ function ownsLockPath(descriptor, owner, token) {
611
+ const opened = fs.fstatSync(descriptor);
612
+ const current = fs.statSync(owner);
613
+ if (opened.dev !== current.dev || opened.ino !== current.ino)
614
+ return false;
615
+ const buffer = Buffer.alloc(Buffer.byteLength(token));
616
+ return fs.readSync(descriptor, buffer, 0, buffer.length, 0) === buffer.length
617
+ && buffer.toString("utf8") === token;
618
+ }
619
+ function fsyncDirectory(directory) {
620
+ let descriptor;
621
+ try {
622
+ descriptor = fs.openSync(directory, "r");
623
+ fs.fsyncSync(descriptor);
624
+ }
625
+ catch {
626
+ // Some platforms do not permit opening or syncing a directory.
627
+ }
628
+ finally {
629
+ if (descriptor !== undefined)
630
+ fs.closeSync(descriptor);
631
+ }
632
+ }
633
+ function comparePending(left, right) {
634
+ return Date.parse(left.event.occurred_at) - Date.parse(right.event.occurred_at)
635
+ || left.event.event_id.localeCompare(right.event.event_id);
636
+ }
637
+ function pendingScope(pending) {
638
+ return {
639
+ ...(pending.event.session_id ? { sessionId: pending.event.session_id } : {}),
640
+ ...(pending.principalKey ? { principalKey: pending.principalKey } : {}),
641
+ };
642
+ }
643
+ function scopeKey(scope) {
644
+ return `${scope.sessionId ?? ""}\u0000${scope.principalKey ?? ""}`;
645
+ }
646
+ function parsePendingEvent(value) {
647
+ // Accept the original event-only format so an upgrade can drain an existing outbox.
648
+ if (isServerActivityEvent(value))
649
+ return { event: value };
650
+ if (!value || typeof value !== "object" || Array.isArray(value))
651
+ return undefined;
652
+ const stored = value;
653
+ if (Object.keys(stored).some((key) => !["event", "principal_key"].includes(key))) {
654
+ return undefined;
655
+ }
656
+ if (!isServerActivityEvent(stored.event))
657
+ return undefined;
658
+ if (stored.principal_key !== undefined
659
+ && (typeof stored.principal_key !== "string" || !/^[a-f0-9]{32}$/.test(stored.principal_key))) {
660
+ return undefined;
661
+ }
662
+ return {
663
+ event: stored.event,
664
+ ...(stored.principal_key ? { principalKey: stored.principal_key } : {}),
665
+ };
666
+ }
667
+ function isServerActivityEvent(value) {
668
+ if (!value || typeof value !== "object" || Array.isArray(value))
669
+ return false;
670
+ const event = value;
671
+ if (Object.keys(event).some((key) => ![
672
+ "event_id", "name", "status", "occurred_at", "session_id", "attributes",
673
+ ].includes(key)))
674
+ return false;
675
+ if (typeof event.event_id !== "string" || !EVENT_ID.test(event.event_id)
676
+ || typeof event.name !== "string" || !exports.SERVER_ACTIVITY_EVENTS.has(event.name)
677
+ || typeof event.status !== "string" || !ACTIVITY_STATUSES.has(event.status)
678
+ || typeof event.occurred_at !== "string"
679
+ || (event.session_id !== undefined
680
+ && (typeof event.session_id !== "string" || event.session_id.length > 512)))
681
+ return false;
682
+ if (event.attributes === undefined)
683
+ return true;
684
+ if (!event.attributes || typeof event.attributes !== "object" || Array.isArray(event.attributes)) {
685
+ return false;
686
+ }
687
+ const attributes = event.attributes;
688
+ const projected = projectAttributes(attributes);
689
+ return Object.keys(projected).length === Object.keys(attributes).length
690
+ && Object.entries(projected).every(([key, item]) => attributes[key] === item);
691
+ }
692
+ function abortable(promise, signal) {
693
+ if (signal.aborted)
694
+ return Promise.reject(signal.reason);
695
+ return new Promise((resolve, reject) => {
696
+ const abort = () => reject(signal.reason);
697
+ signal.addEventListener("abort", abort, { once: true });
698
+ promise.then((value) => {
699
+ signal.removeEventListener("abort", abort);
700
+ resolve(value);
701
+ }, (error) => {
702
+ signal.removeEventListener("abort", abort);
703
+ reject(error);
704
+ });
705
+ });
706
+ }
707
+ function projectActivityEntry(entry, createEventId = node_crypto_1.randomUUID) {
708
+ if (!exports.SERVER_ACTIVITY_EVENTS.has(entry.event))
709
+ return undefined;
710
+ const suppliedDuration = entry.attributes?.durationMs;
711
+ const durationMs = entry.durationMs > 0
712
+ ? entry.durationMs
713
+ : typeof suppliedDuration === "number"
714
+ ? suppliedDuration
715
+ : entry.durationMs;
716
+ const attributes = projectAttributes({
717
+ ...entry.attributes,
718
+ harness: entry.harness,
719
+ durationMs,
720
+ });
721
+ const timestamp = Date.parse(entry.timestamp);
722
+ const occurredAt = entry.durationMs === 0
723
+ && durationMs > 0
724
+ && Number.isFinite(timestamp)
725
+ ? new Date(timestamp - durationMs).toISOString()
726
+ : entry.timestamp;
727
+ return {
728
+ event_id: createEventId(),
729
+ name: entry.event,
730
+ status: entry.status,
731
+ occurred_at: occurredAt,
732
+ ...(entry.sessionId !== undefined && entry.sessionId.length <= 512
733
+ ? { session_id: entry.sessionId }
734
+ : {}),
735
+ ...(Object.keys(attributes).length > 0 ? { attributes } : {}),
736
+ };
737
+ }
738
+ /** Stable, non-identifying directory name that prevents cross-project replay. */
739
+ function eventOutboxProjectScope(projectUrl) {
740
+ return (0, node_crypto_1.createHash)("sha256").update(projectUrl).digest("hex").slice(0, 16);
741
+ }
742
+ /** Stable local selector for a bearer without persisting its identifying subject. */
743
+ function eventPrincipalKey(subject) {
744
+ return (0, node_crypto_1.createHash)("sha256").update(subject).digest("hex").slice(0, 32);
745
+ }
746
+ function projectAttributes(input) {
747
+ const output = {};
748
+ for (const [name, value] of Object.entries(input)) {
749
+ if (name === "checkRejected" && typeof value === "string" && SAFE_STRING.test(value)) {
750
+ output.checkRejected = true;
751
+ if (!("reasonCode" in input))
752
+ output.reasonCode = value;
753
+ }
754
+ else if (STRING_ATTRIBUTES.has(name) && typeof value === "string" && SAFE_STRING.test(value)) {
755
+ output[name] = value;
756
+ }
757
+ else if (HASH_ATTRIBUTES.has(name) && typeof value === "string" && HASH.test(value)) {
758
+ output[name] = value;
759
+ }
760
+ else if (NUMBER_ATTRIBUTES.has(name) && typeof value === "number" && Number.isFinite(value)) {
761
+ const maximum = name === "durationMs" ? MAX_EVENT_AGE_MS : MAX_NUMBER;
762
+ if (value >= 0 && value <= maximum)
763
+ output[name] = value;
764
+ }
765
+ else if (BOOL_ATTRIBUTES.has(name) && typeof value === "boolean") {
766
+ output[name] = value;
767
+ }
768
+ else if (ARRAY_ATTRIBUTES.has(name)
769
+ && Array.isArray(value)
770
+ && value.length <= 100
771
+ && value.every((item) => typeof item === "string" && SAFE_STRING.test(item))) {
772
+ output[name] = value;
773
+ }
774
+ }
775
+ return output;
776
+ }