@ory/argus 0.13.9 → 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 (133) hide show
  1. package/README.md +31 -45
  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 +43 -97
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -18
  7. package/assets/skills/ory-temporal-worker/SKILL.md +38 -42
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +93 -30
  10. package/dist/adapters.js +464 -136
  11. package/dist/agent-auth.d.ts +258 -68
  12. package/dist/agent-auth.js +998 -202
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +40 -4
  16. package/dist/auth.js +247 -19
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +128 -0
  20. package/dist/branding.js +151 -0
  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 -278
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +249 -57
  29. package/dist/config.js +486 -62
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +8 -8
  33. package/dist/contract-suite.js +88 -69
  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 +31 -19
  47. package/dist/index.js +182 -31
  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.d.ts +14 -0
  54. package/dist/local/health.js +50 -4
  55. package/dist/local/index.d.ts +2 -2
  56. package/dist/local/index.js +24 -10
  57. package/dist/local/manager.d.ts +20 -1
  58. package/dist/local/manager.js +160 -39
  59. package/dist/local/ports.d.ts +158 -0
  60. package/dist/local/ports.js +443 -0
  61. package/dist/local/seed.d.ts +22 -25
  62. package/dist/local/seed.js +88 -56
  63. package/dist/logger.d.ts +54 -25
  64. package/dist/logger.js +329 -63
  65. package/dist/mcp.d.ts +2 -2
  66. package/dist/mcp.js +10 -5
  67. package/dist/mirror-bootstrap.d.ts +48 -0
  68. package/dist/mirror-bootstrap.js +254 -0
  69. package/dist/opl.d.ts +289 -0
  70. package/dist/opl.js +446 -0
  71. package/dist/permission-mode.d.ts +87 -0
  72. package/dist/permission-mode.js +307 -0
  73. package/dist/permissions-cli.d.ts +13 -49
  74. package/dist/permissions-cli.js +154 -348
  75. package/dist/permissions.d.ts +148 -38
  76. package/dist/permissions.js +591 -45
  77. package/dist/post-install.d.ts +33 -0
  78. package/dist/post-install.js +127 -0
  79. package/dist/read-credential.d.ts +65 -0
  80. package/dist/read-credential.js +86 -0
  81. package/dist/registry/cli.js +5 -2
  82. package/dist/registry/config.d.ts +0 -17
  83. package/dist/registry/config.js +0 -23
  84. package/dist/registry/index.d.ts +1 -1
  85. package/dist/registry/index.js +2 -2
  86. package/dist/registry/manager.d.ts +4 -21
  87. package/dist/registry/manager.js +83 -55
  88. package/dist/runtime-credential.d.ts +140 -0
  89. package/dist/runtime-credential.js +572 -0
  90. package/dist/runtime.d.ts +408 -0
  91. package/dist/runtime.js +748 -0
  92. package/dist/setup.d.ts +23 -28
  93. package/dist/setup.js +57 -84
  94. package/dist/status-cli.d.ts +29 -13
  95. package/dist/status-cli.js +124 -144
  96. package/dist/status-data.d.ts +195 -0
  97. package/dist/status-data.js +333 -0
  98. package/dist/status-system.d.ts +24 -0
  99. package/dist/status-system.js +56 -0
  100. package/dist/subject.d.ts +126 -20
  101. package/dist/subject.js +215 -30
  102. package/dist/testing.d.ts +74 -38
  103. package/dist/testing.js +185 -68
  104. package/dist/tool-catalog.d.ts +53 -11
  105. package/dist/tool-catalog.js +164 -13
  106. package/dist/tool-metadata.d.ts +7 -6
  107. package/dist/tool-metadata.js +6 -5
  108. package/dist/types.d.ts +11 -1
  109. package/dist/uninstall.d.ts +74 -19
  110. package/dist/uninstall.js +224 -49
  111. package/dist/user-login.d.ts +22 -16
  112. package/dist/user-login.js +67 -96
  113. package/dist/watch-cli.d.ts +6 -0
  114. package/dist/watch-cli.js +217 -0
  115. package/package.json +3 -11
  116. package/dist/dev.d.ts +0 -103
  117. package/dist/dev.js +0 -584
  118. package/dist/interactive-setup.d.ts +0 -165
  119. package/dist/interactive-setup.js +0 -1546
  120. package/dist/local/jaeger-main.d.ts +0 -13
  121. package/dist/local/jaeger-main.js +0 -85
  122. package/dist/local/jaeger.d.ts +0 -50
  123. package/dist/local/jaeger.js +0 -162
  124. package/dist/otel/exporter.d.ts +0 -17
  125. package/dist/otel/exporter.js +0 -12
  126. package/dist/otel/index.d.ts +0 -2
  127. package/dist/otel/index.js +0 -8
  128. package/dist/otel/otlp.d.ts +0 -103
  129. package/dist/otel/otlp.js +0 -385
  130. package/dist/tracer.d.ts +0 -190
  131. package/dist/tracer.js +0 -481
  132. package/dist/watch-sandbox.d.ts +0 -9
  133. package/dist/watch-sandbox.js +0 -81
package/dist/logger.js CHANGED
@@ -33,15 +33,67 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.DebugLogger = void 0;
36
+ exports.DebugLogger = exports.ActiveActivity = void 0;
37
37
  exports.redactLogData = redactLogData;
38
+ exports.activityValueHash = activityValueHash;
39
+ exports.sanitizeActivityAttributes = sanitizeActivityAttributes;
40
+ exports.formatLogEntryParts = formatLogEntryParts;
41
+ exports.formatLogEntry = formatLogEntry;
38
42
  exports.withQuietStderr = withQuietStderr;
39
43
  const fs = __importStar(require("node:fs"));
40
44
  const path = __importStar(require("node:path"));
41
- /**
42
- * Keys whose values are always redacted before being written to stderr or
43
- * to the log file. Match is case-insensitive on the final path segment.
44
- */
45
+ const node_crypto_1 = require("node:crypto");
46
+ const context_js_1 = require("./context.js");
47
+ const MAX_BUFFERED_ACTIVITIES = 1_000;
48
+ const ACTIVITY_HASH_LENGTH = 16;
49
+ // These values are bounded vocabularies or names needed to interpret an event.
50
+ // Every other string is summarized before it reaches the always-on activity sink.
51
+ const SAFE_ACTIVITY_STRING_KEYS = new Set([
52
+ "alertKind",
53
+ "blockReason",
54
+ "blockedPrincipalLevel",
55
+ "checkRejected",
56
+ "composerMode",
57
+ "delegation",
58
+ "delegationKind",
59
+ "decision",
60
+ "event",
61
+ "failureType",
62
+ "harness",
63
+ "kind",
64
+ "mcpServer",
65
+ "mcpTool",
66
+ "mode",
67
+ "model",
68
+ "namespace",
69
+ "notificationType",
70
+ "object",
71
+ "permissionMode",
72
+ "postureSource",
73
+ "relation",
74
+ "reasonCode",
75
+ "revoke",
76
+ "scope",
77
+ "severity",
78
+ "source",
79
+ "stage",
80
+ "status",
81
+ "subAgentType",
82
+ "toolName",
83
+ "trigger",
84
+ ]);
85
+ const SAFE_ACTIVITY_STRING_ARRAY_KEYS = new Set(["missing", "shellDeniedWords", "shellWords"]);
86
+ const SAFE_ACTIVITY_ID_KEYS = new Set(["oryRequestId"]);
87
+ const SAFE_ACTIVITY_ERROR_CODES = new Set([
88
+ "forbidden",
89
+ "network_error",
90
+ "not_found",
91
+ "rate_limited",
92
+ "session_aal2_required",
93
+ "session_inactive",
94
+ "tls_error",
95
+ "unknown",
96
+ ]);
45
97
  const REDACTED_KEYS = new Set([
46
98
  "accesstoken",
47
99
  "access_token",
@@ -51,50 +103,119 @@ const REDACTED_KEYS = new Set([
51
103
  "id_token",
52
104
  "sessiontoken",
53
105
  "session_token",
106
+ "token",
54
107
  "code_verifier",
55
108
  "codeverifier",
56
109
  "client_secret",
57
110
  "clientsecret",
58
111
  "authorization",
112
+ "cookie",
59
113
  "password",
60
114
  "apikey",
61
115
  "api_key",
62
116
  ]);
63
- /**
64
- * Recursively redact sensitive values from a structured log payload.
65
- * The original object is not modified.
66
- */
67
117
  function redactLogData(value) {
68
- if (Array.isArray(value)) {
118
+ if (Array.isArray(value))
69
119
  return value.map(redactLogData);
70
- }
71
120
  if (value && typeof value === "object") {
72
121
  const out = {};
73
- for (const [k, v] of Object.entries(value)) {
74
- if (REDACTED_KEYS.has(k.toLowerCase()) && v !== undefined && v !== null) {
75
- out[k] = "[redacted]";
122
+ for (const [key, item] of Object.entries(value)) {
123
+ out[key] = REDACTED_KEYS.has(key.toLowerCase()) && item !== undefined && item !== null
124
+ ? "[redacted]"
125
+ : redactLogData(item);
126
+ }
127
+ return out;
128
+ }
129
+ return value;
130
+ }
131
+ function activityValueHash(value) {
132
+ return (0, node_crypto_1.createHash)("sha256").update(value).digest("hex").slice(0, ACTIVITY_HASH_LENGTH);
133
+ }
134
+ /** Convert arbitrary strings to correlation-safe summaries for always-on activity. */
135
+ function sanitizeActivityAttributes(attributes) {
136
+ const out = {};
137
+ for (const [key, value] of Object.entries(attributes)) {
138
+ if (typeof value === "string") {
139
+ if (value === "[redacted]"
140
+ || SAFE_ACTIVITY_STRING_KEYS.has(key)
141
+ || SAFE_ACTIVITY_ID_KEYS.has(key)
142
+ || (key === "error" && SAFE_ACTIVITY_ERROR_CODES.has(value))
143
+ || (key.endsWith("Hash") && /^[a-f0-9]{16,64}$/i.test(value))) {
144
+ out[key] = value;
76
145
  }
77
146
  else {
78
- out[k] = redactLogData(v);
147
+ out[`${key}Bytes`] = Buffer.byteLength(value, "utf8");
148
+ out[`${key}Hash`] = activityValueHash(value);
79
149
  }
150
+ continue;
80
151
  }
81
- return out;
152
+ if (Array.isArray(value)) {
153
+ if (value.every((item) => typeof item === "string")) {
154
+ if (SAFE_ACTIVITY_STRING_ARRAY_KEYS.has(key)) {
155
+ out[key] = value;
156
+ }
157
+ else {
158
+ out[`${key}Count`] = value.length;
159
+ out[`${key}Hashes`] = value.map(activityValueHash);
160
+ }
161
+ continue;
162
+ }
163
+ out[key] = value.map((item) => {
164
+ if (typeof item === "string")
165
+ return activityValueHash(item);
166
+ if (item && typeof item === "object") {
167
+ return sanitizeActivityAttributes(item);
168
+ }
169
+ return item;
170
+ });
171
+ continue;
172
+ }
173
+ if (value && typeof value === "object" && !Array.isArray(value)) {
174
+ out[key] = sanitizeActivityAttributes(value);
175
+ continue;
176
+ }
177
+ out[key] = value;
178
+ }
179
+ return out;
180
+ }
181
+ class ActiveActivity {
182
+ logger;
183
+ event;
184
+ baseAttributes;
185
+ sessionId;
186
+ startedAt = performance.now();
187
+ timestamp = new Date().toISOString();
188
+ endedEntry;
189
+ constructor(logger, event, baseAttributes, sessionId) {
190
+ this.logger = logger;
191
+ this.event = event;
192
+ this.baseAttributes = baseAttributes;
193
+ this.sessionId = sessionId;
194
+ }
195
+ get ended() {
196
+ return this.endedEntry !== undefined;
197
+ }
198
+ end(status, attributes) {
199
+ if (this.endedEntry)
200
+ return this.endedEntry;
201
+ this.endedEntry = this.logger.activity(this.event, status, {
202
+ durationMs: Math.round(performance.now() - this.startedAt),
203
+ timestamp: this.timestamp,
204
+ sessionId: this.sessionId,
205
+ attributes: { ...this.baseAttributes, ...attributes },
206
+ });
207
+ return this.endedEntry;
82
208
  }
83
- return value;
84
209
  }
210
+ exports.ActiveActivity = ActiveActivity;
85
211
  class DebugLogger {
86
- /**
87
- * Process-global switch to suppress the stderr firehose while still writing
88
- * to the log file. The interactive installer sets this so structured debug
89
- * JSON (`[ory-agent] {...}`) doesn't interleave with the wizard's
90
- * human-facing UI when a session runs with `ORY_AGENT_DEBUG=true` (the dev
91
- * launcher, for one, always sets it). File logging — the real observability
92
- * channel — is unaffected.
93
- */
94
212
  static stderrSuppressed = false;
95
213
  enabled;
96
214
  harness;
97
215
  logFile;
216
+ activityEntries = [];
217
+ activityEnricher = null;
218
+ activitySink = null;
98
219
  constructor(harness, opts) {
99
220
  this.enabled = process.env.ORY_AGENT_DEBUG === "true";
100
221
  this.harness = harness;
@@ -109,12 +230,60 @@ class DebugLogger {
109
230
  disable() {
110
231
  this.enabled = false;
111
232
  }
233
+ setActivityEnricher(enricher) {
234
+ this.activityEnricher = enricher;
235
+ }
236
+ setActivitySink(sink) {
237
+ this.activitySink = sink;
238
+ }
239
+ activities() {
240
+ return this.activityEntries;
241
+ }
242
+ startActivity(event, attributes, sessionId) {
243
+ return new ActiveActivity(this, event, attributes, sessionId);
244
+ }
245
+ activity(event, status, options = {}) {
246
+ let enriched;
247
+ try {
248
+ enriched = this.activityEnricher?.();
249
+ }
250
+ catch {
251
+ enriched = undefined;
252
+ }
253
+ const merged = { ...enriched, ...options.attributes };
254
+ const safeAttributes = Object.keys(merged).length > 0
255
+ ? sanitizeActivityAttributes(redactLogData(merged))
256
+ : undefined;
257
+ const sessionId = options.sessionId ?? (0, context_js_1.currentSessionContext)().sessionId;
258
+ const entry = {
259
+ timestamp: options.timestamp ?? new Date().toISOString(),
260
+ level: "activity",
261
+ event,
262
+ harness: this.harness,
263
+ status,
264
+ durationMs: options.durationMs ?? 0,
265
+ ...(sessionId ? { sessionId } : {}),
266
+ ...(safeAttributes ? { attributes: safeAttributes } : {}),
267
+ };
268
+ this.activityEntries.push(entry);
269
+ if (this.activityEntries.length > MAX_BUFFERED_ACTIVITIES) {
270
+ this.activityEntries.splice(0, this.activityEntries.length - MAX_BUFFERED_ACTIVITIES);
271
+ }
272
+ if (this.enabled)
273
+ this.writeToStderr(entry);
274
+ this.writeToFile(entry);
275
+ try {
276
+ this.activitySink?.(entry);
277
+ }
278
+ catch {
279
+ // Remote reporting must never affect local activity or a hook decision.
280
+ }
281
+ return entry;
282
+ }
112
283
  log(level, event, data) {
113
284
  if (!this.enabled)
114
285
  return;
115
- const safeData = data
116
- ? redactLogData(data)
117
- : undefined;
286
+ const safeData = data ? redactLogData(data) : undefined;
118
287
  const entry = {
119
288
  timestamp: new Date().toISOString(),
120
289
  level,
@@ -122,54 +291,151 @@ class DebugLogger {
122
291
  harness: this.harness,
123
292
  ...(safeData ? { data: safeData } : {}),
124
293
  };
125
- const line = JSON.stringify(entry);
126
- // Write to stderr (never stdout) so we don't interfere with hook stdout,
127
- // unless the stderr firehose is globally suppressed (e.g. during the
128
- // interactive installer, so it doesn't clutter the wizard UI). The log
129
- // file below still captures everything.
130
- if (!DebugLogger.stderrSuppressed) {
131
- process.stderr.write(`[ory-agent] ${line}\n`);
294
+ this.writeToStderr(entry);
295
+ this.writeToFile(entry);
296
+ }
297
+ debug(event, data) { this.log("debug", event, data); }
298
+ info(event, data) { this.log("info", event, data); }
299
+ warn(event, data) { this.log("warn", event, data); }
300
+ error(event, data) { this.log("error", event, data); }
301
+ writeToStderr(entry) {
302
+ if (DebugLogger.stderrSuppressed)
303
+ return;
304
+ try {
305
+ process.stderr.write(`[ory-agent] ${JSON.stringify(entry)}\n`);
132
306
  }
133
- if (this.logFile) {
134
- const dir = path.dirname(this.logFile);
135
- if (!fs.existsSync(dir)) {
136
- fs.mkdirSync(dir, { recursive: true });
137
- }
138
- fs.appendFileSync(this.logFile, line + "\n");
307
+ catch {
308
+ // Logging must never affect a hook decision.
139
309
  }
140
310
  }
141
- debug(event, data) {
142
- this.log("debug", event, data);
311
+ writeToFile(entry) {
312
+ if (!this.logFile)
313
+ return;
314
+ try {
315
+ fs.mkdirSync(path.dirname(this.logFile), { recursive: true });
316
+ fs.appendFileSync(this.logFile, JSON.stringify(entry) + "\n");
317
+ }
318
+ catch {
319
+ // Activity/audit persistence is best-effort and must never block a hook.
320
+ }
321
+ }
322
+ }
323
+ exports.DebugLogger = DebugLogger;
324
+ const STATUS_MARKERS = {
325
+ ok: "+",
326
+ error: "!",
327
+ denied: "x",
328
+ skipped: "-",
329
+ };
330
+ function displayValue(value) {
331
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
332
+ const text = String(value).replace(/\s+/g, " ");
333
+ return text.length > 240 ? `${text.slice(0, 237)}...` : text;
143
334
  }
144
- info(event, data) {
145
- this.log("info", event, data);
335
+ if (Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean")) {
336
+ return displayValue(value.join(","));
146
337
  }
147
- warn(event, data) {
148
- this.log("warn", event, data);
338
+ return undefined;
339
+ }
340
+ function decisionFor(entry, values) {
341
+ const explicit = displayValue(values.decision);
342
+ if (explicit)
343
+ return explicit;
344
+ if (values.observed === true && values.allowed === false)
345
+ return "observe";
346
+ if (values.allowed === true)
347
+ return "allow";
348
+ if (values.allowed === false)
349
+ return "deny";
350
+ if (values.securityConnected === false)
351
+ return "not_connected";
352
+ if (entry.level === "activity" && entry.status === "error")
353
+ return "error";
354
+ if (entry.event.includes("fail_open"))
355
+ return "fail_open";
356
+ if (entry.event.endsWith(".failed") || entry.event.endsWith(".failure"))
357
+ return "error";
358
+ return undefined;
359
+ }
360
+ function formatEntryDetails(entry) {
361
+ const values = entry.level === "activity" ? entry.attributes ?? {} : entry.data ?? {};
362
+ const details = [];
363
+ const decision = decisionFor(entry, values);
364
+ const tool = displayValue(values.toolName ?? values.tool);
365
+ const mode = displayValue(values.permissionMode ?? values.mode);
366
+ const error = displayValue(values.error ?? values.code);
367
+ const message = displayValue(values.message);
368
+ if (entry.level === "activity")
369
+ details.push(`status=${entry.status}`);
370
+ if (decision)
371
+ details.push(`decision=${decision}`);
372
+ if (tool)
373
+ details.push(`tool=${tool}`);
374
+ for (const key of [
375
+ "namespace",
376
+ "object",
377
+ "relation",
378
+ "blockReason",
379
+ "reasonCode",
380
+ "reason",
381
+ "stage",
382
+ "kind",
383
+ "trigger",
384
+ ]) {
385
+ const value = displayValue(values[key]);
386
+ if (value)
387
+ details.push(`${key}=${value}`);
149
388
  }
150
- error(event, data) {
151
- this.log("error", event, data);
389
+ if (mode)
390
+ details.push(`mode=${mode}`);
391
+ if (error)
392
+ details.push(`error=${error}`);
393
+ const status = entry.level !== "activity" ? displayValue(values.status) : undefined;
394
+ if (status)
395
+ details.push(`http=${status}`);
396
+ if (message)
397
+ details.push(`message=${message}`);
398
+ for (const key of ["count", "allowed", "denied", "tooComplex"]) {
399
+ if ((key === "allowed" || key === "denied") && typeof values[key] === "boolean")
400
+ continue;
401
+ const value = displayValue(values[key]);
402
+ if (value !== undefined)
403
+ details.push(`${key}=${value}`);
152
404
  }
405
+ const shellWords = displayValue(values.shellWords ?? values.words);
406
+ if (shellWords)
407
+ details.push(`commands=${shellWords}`);
408
+ const deniedWords = displayValue(values.shellDeniedWords);
409
+ if (deniedWords)
410
+ details.push(`deniedCommands=${deniedWords}`);
411
+ const command = displayValue(values.command);
412
+ if (command)
413
+ details.push(`command=${command}`);
414
+ if (entry.level === "activity" && entry.durationMs > 0)
415
+ details.push(`duration=${entry.durationMs}ms`);
416
+ if (entry.level === "activity" && entry.sessionId)
417
+ details.push(`session=${entry.sessionId}`);
418
+ return details;
419
+ }
420
+ function formatLogEntryParts(entry) {
421
+ return {
422
+ time: entry.timestamp.slice(11, 23),
423
+ event: entry.event,
424
+ marker: entry.level === "activity" ? STATUS_MARKERS[entry.status] : `[${entry.level}]`,
425
+ details: formatEntryDetails(entry),
426
+ };
427
+ }
428
+ function formatLogEntry(entry) {
429
+ const { time, event, marker, details } = formatLogEntryParts(entry);
430
+ return `${time} ${event.padEnd(28)} ${marker}${details.length ? ` ${details.join(" ")}` : ""}`;
153
431
  }
154
- exports.DebugLogger = DebugLogger;
155
- /**
156
- * Run `fn` with the structured stderr firehose suppressed, restoring the
157
- * previous state afterward (even on throw). File logging is unaffected.
158
- *
159
- * Human-facing CLI commands (the interactive installer, `status`,
160
- * `permissions status`/`bootstrap`) spin up a client and emit debug/info
161
- * events; when the process runs with `ORY_AGENT_DEBUG=true` (the dev launcher
162
- * always sets it) those `[ory-agent] {...}` lines would interleave with the
163
- * command's polished output. Wrapping the command body in this keeps the
164
- * terminal clean while the log file still captures everything.
165
- */
166
432
  async function withQuietStderr(fn) {
167
- const prev = DebugLogger.stderrSuppressed;
433
+ const previous = DebugLogger.stderrSuppressed;
168
434
  DebugLogger.stderrSuppressed = true;
169
435
  try {
170
436
  return await fn();
171
437
  }
172
438
  finally {
173
- DebugLogger.stderrSuppressed = prev;
439
+ DebugLogger.stderrSuppressed = previous;
174
440
  }
175
441
  }
package/dist/mcp.d.ts CHANGED
@@ -30,10 +30,10 @@ export interface McpPermissionCheckOptions {
30
30
  checkToolLevel?: boolean;
31
31
  /**
32
32
  * Attributes merged into the permission.check / permission.batch_check
33
- * trace spans. Use this to carry the harness-side tool name and any
33
+ * activity events. Use this to carry the harness-side tool name and any
34
34
  * other context the Zanzibar tuple shape doesn't capture.
35
35
  */
36
- spanAttributes?: Record<string, unknown>;
36
+ activityAttributes?: Record<string, unknown>;
37
37
  }
38
38
  export interface McpPermissionResult {
39
39
  /** Whether the server-level check passed. */
package/dist/mcp.js CHANGED
@@ -13,6 +13,7 @@ exports.parseClaudeCodeMcpTool = parseClaudeCodeMcpTool;
13
13
  exports.parseGeminiMcpTool = parseGeminiMcpTool;
14
14
  exports.parseMcpToolGeneric = parseMcpToolGeneric;
15
15
  exports.checkMcpPermission = checkMcpPermission;
16
+ const read_credential_js_1 = require("./read-credential.js");
16
17
  // ─── Parsers ───────────────────────────────────────────────────────
17
18
  /**
18
19
  * Parse a Claude Code MCP tool name of the form `mcp__<server>__<tool>`.
@@ -80,20 +81,24 @@ async function checkMcpPermission(client, mcpTool, options) {
80
81
  (process.env.ORY_MCP_TOOL_NAMESPACE ?? "mcp_tools");
81
82
  const checkToolLevel = options.checkToolLevel ??
82
83
  process.env.ORY_MCP_TOOL_LEVEL_CHECKS === "true";
84
+ // MCP tool calls arrive on the same subprocess lifecycle events as any other
85
+ // tool, so this read needs a credential resolved for exactly the reasons
86
+ // `gateToolCall` does (#242).
87
+ await (0, read_credential_js_1.ensureReadCredential)(client);
83
88
  const serverCheck = {
84
89
  namespace: serverNamespace,
85
90
  object: mcpTool.serverName,
86
- relation: "use",
91
+ relation: "users",
87
92
  ...options.subject,
88
93
  };
89
- const spanAttributes = {
94
+ const activityAttributes = {
90
95
  mcpServer: mcpTool.serverName,
91
96
  ...(mcpTool.toolName ? { mcpTool: mcpTool.toolName } : {}),
92
- ...options.spanAttributes,
97
+ ...options.activityAttributes,
93
98
  };
94
99
  if (!checkToolLevel || !mcpTool.toolName) {
95
100
  // Server-only check
96
- const result = await client.checkPermission(serverCheck, { spanAttributes });
101
+ const result = await client.checkPermission(serverCheck, { activityAttributes });
97
102
  return {
98
103
  serverAllowed: result.allowed,
99
104
  toolAllowed: true,
@@ -109,7 +114,7 @@ async function checkMcpPermission(client, mcpTool, options) {
109
114
  relation: "invoke",
110
115
  ...options.subject,
111
116
  };
112
- const batchResult = await client.batchCheckPermissions([serverCheck, toolCheck], { spanAttributes });
117
+ const batchResult = await client.batchCheckPermissions([serverCheck, toolCheck], { activityAttributes });
113
118
  const serverAllowed = batchResult.results[0]?.allowed ?? false;
114
119
  const toolAllowed = batchResult.results[1]?.allowed ?? false;
115
120
  return {
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The bootstrap committed to the public install-surface mirrors.
3
+ *
4
+ * Three harnesses install from a git repo rather than from npm — Claude Code
5
+ * (plugin marketplace), Gemini CLI (`gemini extensions install <url>`) and
6
+ * Codex (`codex plugin marketplace add <url>`) — so those repos carry a
7
+ * checked-in `hooks.json` / manifest. A machine-specific shim path cannot go
8
+ * into a file a teammate will clone, and an `npx` command must not go back onto
9
+ * the tool-call path, so those surfaces instead reference a **bootstrap** that
10
+ * ships inside the plugin tree, addressed through the harness's own path
11
+ * variable (verified per harness — see `docs/distribution.md`).
12
+ *
13
+ * The bootstrap is deliberately **dependency-free**: the mirror repos have no
14
+ * `node_modules`, so it uses `node:` builtins only. It does three things:
15
+ *
16
+ * 1. Read the runtime manifest this machine's `install` wrote and `require`
17
+ * the wired entry. That is the steady state, and it costs one file read.
18
+ * 2. On a **session-start** event with nothing wired, complete the install
19
+ * once by shelling out to `npx <package> install`. This is what keeps
20
+ * `claude plugin install …` a working one-liner: the marketplace install
21
+ * is the install, and the runtime it needs is materialized on first use —
22
+ * never per hook invocation.
23
+ * 3. Fail **open** in every other unresolved case: print what to run, emit
24
+ * the harness's pass-through response, exit 0. A tool-call event never
25
+ * triggers a materialize, because a `PreToolUse` hook has seconds to
26
+ * answer and a harness that treats an unanswered gate as a block would
27
+ * otherwise block the tool while npm ran.
28
+ */
29
+ /** Which entry point the generated bootstrap loads. */
30
+ export type MirrorBootstrapKind = "hook" | "mcp";
31
+ export interface MirrorBootstrapOptions {
32
+ /** Harness slug, e.g. `claude-code` — the manifest key to look up. */
33
+ harness: string;
34
+ /** npm package the install command names, e.g. `@ory/claude-code`. */
35
+ packageName: string;
36
+ /** CLI bin the install command runs, e.g. `ory-claude`. */
37
+ binName: string;
38
+ /** Harness pass-through payload for the runtime-missing case. */
39
+ fallbackStdout?: string;
40
+ /**
41
+ * Event names (as they appear in the hook payload) that may trigger the
42
+ * one-time materialize. Defaults to the session-start spellings the
43
+ * harnesses use.
44
+ */
45
+ sessionStartEvents?: readonly string[];
46
+ }
47
+ /** Render the bootstrap script for one entry kind. */
48
+ export declare function renderMirrorBootstrap(kind: MirrorBootstrapKind, opts: MirrorBootstrapOptions): string;