@ory/argus 0.14.0 → 1.0.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.
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 +413 -0
  90. package/dist/runtime.js +825 -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
package/dist/logger.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export type ActivityStatus = "ok" | "error" | "denied" | "skipped";
2
3
  export interface LogEntry {
3
4
  timestamp: string;
4
5
  level: LogLevel;
@@ -6,45 +7,73 @@ export interface LogEntry {
6
7
  harness: string;
7
8
  data?: Record<string, unknown>;
8
9
  }
9
- /**
10
- * Recursively redact sensitive values from a structured log payload.
11
- * The original object is not modified.
12
- */
10
+ export interface ActivityEntry {
11
+ timestamp: string;
12
+ level: "activity";
13
+ event: string;
14
+ harness: string;
15
+ status: ActivityStatus;
16
+ durationMs: number;
17
+ sessionId?: string;
18
+ attributes?: Record<string, unknown>;
19
+ }
20
+ export type UnifiedLogEntry = LogEntry | ActivityEntry;
21
+ export type ActivityEnricher = () => Record<string, unknown> | undefined;
22
+ export type ActivitySink = (entry: ActivityEntry) => void;
13
23
  export declare function redactLogData(value: unknown): unknown;
24
+ export declare function activityValueHash(value: string): string;
25
+ /** Convert arbitrary strings to correlation-safe summaries for always-on activity. */
26
+ export declare function sanitizeActivityAttributes(attributes: Record<string, unknown>): Record<string, unknown>;
27
+ export declare class ActiveActivity {
28
+ private readonly logger;
29
+ private readonly event;
30
+ private readonly baseAttributes?;
31
+ private readonly sessionId?;
32
+ private readonly startedAt;
33
+ private readonly timestamp;
34
+ private endedEntry;
35
+ constructor(logger: DebugLogger, event: string, baseAttributes?: Record<string, unknown> | undefined, sessionId?: string | undefined);
36
+ get ended(): boolean;
37
+ end(status: ActivityStatus, attributes?: Record<string, unknown>): ActivityEntry;
38
+ }
14
39
  export declare class DebugLogger {
15
- /**
16
- * Process-global switch to suppress the stderr firehose while still writing
17
- * to the log file. The interactive installer sets this so structured debug
18
- * JSON (`[ory-agent] {...}`) doesn't interleave with the wizard's
19
- * human-facing UI when a session runs with `ORY_AGENT_DEBUG=true` (the dev
20
- * launcher, for one, always sets it). File logging — the real observability
21
- * channel — is unaffected.
22
- */
23
40
  static stderrSuppressed: boolean;
24
41
  enabled: boolean;
25
- harness: string;
26
- logFile: string | null;
42
+ readonly harness: string;
43
+ readonly logFile: string | null;
44
+ private readonly activityEntries;
45
+ private activityEnricher;
46
+ private activitySink;
27
47
  constructor(harness: string, opts?: {
28
48
  logFile?: string;
29
49
  });
30
50
  get isEnabled(): boolean;
31
51
  enable(): void;
32
52
  disable(): void;
53
+ setActivityEnricher(enricher: ActivityEnricher | null): void;
54
+ setActivitySink(sink: ActivitySink | null): void;
55
+ activities(): ReadonlyArray<ActivityEntry>;
56
+ startActivity(event: string, attributes?: Record<string, unknown>, sessionId?: string): ActiveActivity;
57
+ activity(event: string, status: ActivityStatus, options?: {
58
+ attributes?: Record<string, unknown>;
59
+ durationMs?: number;
60
+ sessionId?: string;
61
+ timestamp?: string;
62
+ }): ActivityEntry;
33
63
  log(level: LogLevel, event: string, data?: Record<string, unknown>): void;
34
64
  debug(event: string, data?: Record<string, unknown>): void;
35
65
  info(event: string, data?: Record<string, unknown>): void;
36
66
  warn(event: string, data?: Record<string, unknown>): void;
37
67
  error(event: string, data?: Record<string, unknown>): void;
68
+ private writeToStderr;
69
+ private writeToFile;
70
+ }
71
+ export interface FormattedLogEntryParts {
72
+ time: string;
73
+ event: string;
74
+ marker: string;
75
+ details: string[];
38
76
  }
39
- /**
40
- * Run `fn` with the structured stderr firehose suppressed, restoring the
41
- * previous state afterward (even on throw). File logging is unaffected.
42
- *
43
- * Human-facing CLI commands (the interactive installer, `status`,
44
- * `permissions status`/`bootstrap`) spin up a client and emit debug/info
45
- * events; when the process runs with `ORY_AGENT_DEBUG=true` (the dev launcher
46
- * always sets it) those `[ory-agent] {...}` lines would interleave with the
47
- * command's polished output. Wrapping the command body in this keeps the
48
- * terminal clean while the log file still captures everything.
49
- */
77
+ export declare function formatLogEntryParts(entry: UnifiedLogEntry): FormattedLogEntryParts;
78
+ export declare function formatLogEntry(entry: UnifiedLogEntry): string;
50
79
  export declare function withQuietStderr<T>(fn: () => Promise<T> | T): Promise<T>;
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;