@ory/argus 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -658,10 +658,8 @@ function attachOtlpExporterFromEnv(client, harness) {
658
658
  return;
659
659
  client.tracer.setExporter(exporter);
660
660
  client.logger.info("otel.export.enabled", {
661
- endpoint: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
662
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
663
- process.env.ORY_OTLP_ENDPOINT,
664
- serviceName: process.env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${harness}`,
661
+ endpoint: exporter.endpoint,
662
+ serviceName: exporter.resource.attributes["service.name"],
665
663
  });
666
664
  }
667
665
  /**
package/dist/dev.js CHANGED
@@ -322,16 +322,7 @@ async function runDevLauncher(config) {
322
322
  ? buildLocalOryEnv(localOry.gatewayUrl, localOry.seed)
323
323
  : { ORY_AGENT_SUBJECT_ID: sessionName }),
324
324
  ...(otelEndpoint
325
- ? {
326
- OTEL_EXPORTER_OTLP_ENDPOINT: otelEndpoint,
327
- // ORY-prefixed alias for harnesses (e.g. Claude Code) that
328
- // filter the env passed to hook subprocesses and drop
329
- // OTEL_*-prefixed vars. The OTLP exporter reads this as a
330
- // fallback so traces still reach Jaeger when OTEL_* is stripped.
331
- ORY_OTLP_ENDPOINT: otelEndpoint,
332
- OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME ??
333
- `ory-agent-plugin-${config.harnessName}`,
334
- }
325
+ ? buildOtelEnv(otelEndpoint, config.harnessName)
335
326
  : {}),
336
327
  };
337
328
  const result = (0, node_child_process_1.spawnSync)(config.command, forwardedArgs, {
@@ -488,10 +479,53 @@ async function bootstrapLocalOry(localDir) {
488
479
  `${seed.permissions.tuples} permissions in '${namespace}' for ${seed.permissions.subject}.`);
489
480
  return { active: true, gatewayUrl, seed };
490
481
  }
482
+ /**
483
+ * OTel env vars the dev launcher propagates into the harness child. Each
484
+ * entry pairs the standard `OTEL_*` name with its `ORY_OTLP_*` alias; the
485
+ * alias is set in lockstep so harnesses (notably Claude Code) that sanitize
486
+ * `OTEL_*` out of the hook-subprocess env still receive the configuration.
487
+ *
488
+ * Keep this list in sync with `ORY_ENV_ALIAS` in
489
+ * `packages/core/src/otel/otlp.ts` — the exporter consults the same aliases
490
+ * on the read side.
491
+ */
492
+ const OTEL_FORWARDED_VARS = [
493
+ ["OTEL_EXPORTER_OTLP_ENDPOINT", "ORY_OTLP_ENDPOINT"],
494
+ ["OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "ORY_OTLP_TRACES_ENDPOINT"],
495
+ ["OTEL_EXPORTER_OTLP_HEADERS", "ORY_OTLP_HEADERS"],
496
+ ["OTEL_EXPORTER_OTLP_TRACES_HEADERS", "ORY_OTLP_TRACES_HEADERS"],
497
+ ["OTEL_EXPORTER_OTLP_PROTOCOL", "ORY_OTLP_PROTOCOL"],
498
+ ["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL", "ORY_OTLP_TRACES_PROTOCOL"],
499
+ ["OTEL_SERVICE_NAME", "ORY_OTLP_SERVICE_NAME"],
500
+ ["OTEL_RESOURCE_ATTRIBUTES", "ORY_OTLP_RESOURCE_ATTRIBUTES"],
501
+ ];
502
+ /**
503
+ * Compose the OTLP env block for the harness child process. The resolved
504
+ * endpoint is always written under both `OTEL_EXPORTER_OTLP_ENDPOINT` and
505
+ * `ORY_OTLP_ENDPOINT`; every other OTel knob inherited from the parent
506
+ * shell (and `OTEL_SERVICE_NAME`, which we default when unset) is mirrored
507
+ * to its `ORY_OTLP_*` alias on the way through.
508
+ */
509
+ function buildOtelEnv(endpoint, harnessName) {
510
+ const out = {};
511
+ const defaults = {
512
+ OTEL_EXPORTER_OTLP_ENDPOINT: endpoint,
513
+ OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${harnessName}`,
514
+ };
515
+ for (const [standardName, aliasName] of OTEL_FORWARDED_VARS) {
516
+ const value = defaults[standardName] ?? process.env[standardName];
517
+ if (!value)
518
+ continue;
519
+ out[standardName] = value;
520
+ out[aliasName] = value;
521
+ }
522
+ return out;
523
+ }
491
524
  /**
492
525
  * Determine the OTLP endpoint for the dev launch:
493
526
  * 1. Honor OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
494
- * from the parent shell (Honeycomb, custom collector, etc.).
527
+ * (or their ORY_OTLP_* aliases) from the parent shell (Honeycomb,
528
+ * custom collector, etc.).
495
529
  * 2. Otherwise auto-launch a local Jaeger container and use it. If Jaeger
496
530
  * is already reachable (from `local up` or a prior dev launch), reuse
497
531
  * it without spawning another container.
@@ -500,7 +534,9 @@ async function bootstrapLocalOry(localDir) {
500
534
  */
501
535
  async function resolveOtelEndpoint() {
502
536
  const fromEnv = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ??
503
- process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
537
+ process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
538
+ process.env.ORY_OTLP_TRACES_ENDPOINT ??
539
+ process.env.ORY_OTLP_ENDPOINT;
504
540
  if (fromEnv)
505
541
  return fromEnv;
506
542
  console.log("[ory-dev] Ensuring Jaeger is running for trace viewing...");
package/dist/index.d.ts CHANGED
@@ -15,8 +15,9 @@ export { runDevLauncher, type DevLauncherConfig, type InstallContext, } from "./
15
15
  export { renderOrySkills, renderOryCommands, commandToSkill, commandToToml, commandToFrontmatterMarkdown, commandToPlainMarkdown, toSkillMarkdown, writeSkillTree, removeSkillDirs, ORY_SKILL_NAMES, ORY_COMMAND_SKILL_NAMES, ORY_COMMAND_SLUGS, type RenderedSkill, type RenderedCommand, type RenderProfileOptions, } from "./skills.js";
16
16
  export { runLocalCommand, ensureDevJaeger, stopDevJaeger, DEV_JAEGER_CONTAINER, type EnsureDevJaegerResult, type StopDevJaegerResult, } from "./local/index.js";
17
17
  export { runRegistryCommand } from "./registry/index.js";
18
- export { checkAndDecide, applyPermissionMode, type PermissionDecision, type ModeDecision, type DecisionSpanAttributes, type CheckAndDecideOptions, type ApplyPermissionModeContext, } from "./permissions.js";
19
- export { HARNESS_TOOL_CATALOG, KNOWN_HARNESSES, ALL_TOOLS, getToolCatalog, type KnownHarness, } from "./tool-catalog.js";
18
+ export { checkAndDecide, applyPermissionMode, gateToolCall, type PermissionDecision, type ModeDecision, type DecisionSpanAttributes, type CheckAndDecideOptions, type ApplyPermissionModeContext, type GateToolCallArgs, type ToolGateOutcome, } from "./permissions.js";
19
+ export { HARNESS_TOOL_CATALOG, KNOWN_HARNESSES, ALL_TOOLS, getToolCatalog, INTERACTIVE_TOOL_CATALOG, getInteractiveToolCatalog, isInteractiveTool, type KnownHarness, } from "./tool-catalog.js";
20
+ export { classifyLifecycle, isUserFacingPhase, isToolExecutionPhase, HARNESS_LIFECYCLE_MAP, USER_FACING_PHASES, TOOL_EXECUTION_PHASES, type LifecyclePhase, } from "./lifecycle.js";
20
21
  export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkMcpPermission, type McpToolIdentifier, type McpPermissionCheckOptions, type McpPermissionResult, } from "./mcp.js";
21
22
  export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
22
23
  export { formatDenialMessage, formatDenialSummary, formatAlertMessage, formatAlertSummary, alertAttributes, OryDenialError, type DenialContext, type AlertAttributes, } from "./denial.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.printOryConfig = exports.runAgentCommand = exports.runConfigureCommand = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = exports.clearSubAgentDynamicCredentials = exports.saveSubAgentDynamicCredentials = exports.loadSubAgentDynamicCredentials = exports.clearAgentDynamicCredentials = exports.saveAgentDynamicCredentials = exports.loadAgentDynamicCredentials = exports.registerAgentClient = exports.fetchClientCredentialsToken = exports.ensureSubAgentIdentity = exports.ensureAgentIdentity = exports.resolveAgentCredentials = exports.ensureAuthenticated = exports.ensureUserAuthenticated = exports.TOKEN_EXPIRY_SKEW_SEC = exports.waitForPeerTokensSync = exports.waitForPeerTokens = exports.clearPkceFlightLock = exports.tryAcquirePkceFlightLock = exports.refreshAndSave = exports.isExpired = exports.clearTokens = exports.saveTokens = exports.loadTokens = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.LOOPBACK_PORTS = exports.buildAuthorizeUrl = exports.sha256Base64Url = exports.generateCodeVerifier = exports.detectHeadless = exports.refreshAccessToken = exports.pkceLogin = exports.getHarnessDataDir = exports.getDataDir = exports.getConfigPath = exports.mutateConfig = exports.resolveConfig = exports.saveConfig = exports.loadConfig = exports.watchTraceFile = exports.formatSpan = exports.deriveTraceId = exports.ActiveSpan = exports.Tracer = exports.redactLogData = exports.DebugLogger = exports.OryAgentClient = void 0;
4
4
  exports.runLocalCommand = exports.ORY_COMMAND_SLUGS = exports.ORY_COMMAND_SKILL_NAMES = exports.ORY_SKILL_NAMES = exports.removeSkillDirs = exports.writeSkillTree = exports.toSkillMarkdown = exports.commandToPlainMarkdown = exports.commandToFrontmatterMarkdown = exports.commandToToml = exports.commandToSkill = exports.renderOryCommands = exports.renderOrySkills = exports.runDevLauncher = exports.unregisterPlugin = exports.registerPlugin = exports.removeMcpServer = exports.mergeMcpServer = exports.mcpServerEntry = exports.resolveMcpServerCommand = exports.printNextSteps = exports.printSetupHelp = exports.removeFlatHooks = exports.mergeFlatHooks = exports.flatHookEntry = exports.removeMatcherHooks = exports.mergeMatcherHooks = exports.matcherHookEntry = exports.resolveHookCommand = exports.isOryHookCommand = exports.writeJsonFile = exports.readJsonFile = exports.parseSetupArgs = exports.printPermissionsSection = exports.printAgentIdentitySection = exports.printUserIdentitySection = exports.runStatusCommand = exports.printPermissionsOnboardingHelp = exports.maybeAutoBootstrap = exports.isUserIdentityCached = exports.runPermissionsCommand = exports.interactiveConfigPrompt = exports.promptForProjectUrl = exports.promptOnTty = exports.isTtyAvailable = exports.runWatchCommand = exports.printTraceTail = exports.printEnvHelp = exports.printLogTail = exports.printEnvironment = void 0;
5
- exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
5
+ exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.TOOL_EXECUTION_PHASES = exports.USER_FACING_PHASES = exports.HARNESS_LIFECYCLE_MAP = exports.isToolExecutionPhase = exports.isUserFacingPhase = exports.classifyLifecycle = exports.isInteractiveTool = exports.getInteractiveToolCatalog = exports.INTERACTIVE_TOOL_CATALOG = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.gateToolCall = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
6
6
  var client_js_1 = require("./client.js");
7
7
  Object.defineProperty(exports, "OryAgentClient", { enumerable: true, get: function () { return client_js_1.OryAgentClient; } });
8
8
  var logger_js_1 = require("./logger.js");
@@ -126,11 +126,22 @@ Object.defineProperty(exports, "runRegistryCommand", { enumerable: true, get: fu
126
126
  var permissions_js_1 = require("./permissions.js");
127
127
  Object.defineProperty(exports, "checkAndDecide", { enumerable: true, get: function () { return permissions_js_1.checkAndDecide; } });
128
128
  Object.defineProperty(exports, "applyPermissionMode", { enumerable: true, get: function () { return permissions_js_1.applyPermissionMode; } });
129
+ Object.defineProperty(exports, "gateToolCall", { enumerable: true, get: function () { return permissions_js_1.gateToolCall; } });
129
130
  var tool_catalog_js_1 = require("./tool-catalog.js");
130
131
  Object.defineProperty(exports, "HARNESS_TOOL_CATALOG", { enumerable: true, get: function () { return tool_catalog_js_1.HARNESS_TOOL_CATALOG; } });
131
132
  Object.defineProperty(exports, "KNOWN_HARNESSES", { enumerable: true, get: function () { return tool_catalog_js_1.KNOWN_HARNESSES; } });
132
133
  Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return tool_catalog_js_1.ALL_TOOLS; } });
133
134
  Object.defineProperty(exports, "getToolCatalog", { enumerable: true, get: function () { return tool_catalog_js_1.getToolCatalog; } });
135
+ Object.defineProperty(exports, "INTERACTIVE_TOOL_CATALOG", { enumerable: true, get: function () { return tool_catalog_js_1.INTERACTIVE_TOOL_CATALOG; } });
136
+ Object.defineProperty(exports, "getInteractiveToolCatalog", { enumerable: true, get: function () { return tool_catalog_js_1.getInteractiveToolCatalog; } });
137
+ Object.defineProperty(exports, "isInteractiveTool", { enumerable: true, get: function () { return tool_catalog_js_1.isInteractiveTool; } });
138
+ var lifecycle_js_1 = require("./lifecycle.js");
139
+ Object.defineProperty(exports, "classifyLifecycle", { enumerable: true, get: function () { return lifecycle_js_1.classifyLifecycle; } });
140
+ Object.defineProperty(exports, "isUserFacingPhase", { enumerable: true, get: function () { return lifecycle_js_1.isUserFacingPhase; } });
141
+ Object.defineProperty(exports, "isToolExecutionPhase", { enumerable: true, get: function () { return lifecycle_js_1.isToolExecutionPhase; } });
142
+ Object.defineProperty(exports, "HARNESS_LIFECYCLE_MAP", { enumerable: true, get: function () { return lifecycle_js_1.HARNESS_LIFECYCLE_MAP; } });
143
+ Object.defineProperty(exports, "USER_FACING_PHASES", { enumerable: true, get: function () { return lifecycle_js_1.USER_FACING_PHASES; } });
144
+ Object.defineProperty(exports, "TOOL_EXECUTION_PHASES", { enumerable: true, get: function () { return lifecycle_js_1.TOOL_EXECUTION_PHASES; } });
134
145
  var mcp_js_1 = require("./mcp.js");
135
146
  Object.defineProperty(exports, "parseClaudeCodeMcpTool", { enumerable: true, get: function () { return mcp_js_1.parseClaudeCodeMcpTool; } });
136
147
  Object.defineProperty(exports, "parseGeminiMcpTool", { enumerable: true, get: function () { return mcp_js_1.parseGeminiMcpTool; } });
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Canonical lifecycle vocabulary for harness plugins.
3
+ *
4
+ * Each harness exposes a different set of hook event names (Claude Code's
5
+ * `PreToolUse`, Gemini's `BeforeTool`, OpenCode's `tool.execute.before`,
6
+ * …). Mapping them onto a shared phase set lets the plugin code reason
7
+ * about *what is happening* without first remembering *which harness
8
+ * we're in*. Spans, logs, docs, and tests all reference the same set of
9
+ * phases.
10
+ *
11
+ * Phases split into three buckets:
12
+ *
13
+ * - **Session / agent lifecycle.** `session.start`, `session.stop`,
14
+ * `compaction`. The session begins or ends, or the harness compacts
15
+ * context. Plugins authenticate here (start) and may flush state
16
+ * (stop).
17
+ *
18
+ * - **User-facing interactions.** `user.prompt`, `user.interaction`,
19
+ * `permission.ask`. The harness is communicating with the human:
20
+ * they typed a prompt, the harness is surfacing a confirmation,
21
+ * the harness is asking whether to allow a tool. These are *traced*
22
+ * but never gated by a tool permission check — the user is the
23
+ * final decision-maker. (`permission.ask` does consult Ory to
24
+ * inform the harness's UI, but the call is advisory.)
25
+ *
26
+ * - **Tool execution.** `tool.before`, `tool.after`, `tool.failure`,
27
+ * `subagent.start`, `subagent.stop`. The agent is reaching into an
28
+ * external system (or spawning a child agent that will). These are
29
+ * the only phases that run a true `use` permission gate.
30
+ *
31
+ * `passthrough` is the explicit "nothing to do here" bucket — the
32
+ * harness fired an event we don't model, and we record an audit span
33
+ * and return.
34
+ */
35
+ export type LifecyclePhase = "session.start" | "session.stop" | "user.prompt" | "user.interaction" | "permission.ask" | "tool.before" | "tool.after" | "tool.failure" | "subagent.start" | "subagent.stop" | "compaction" | "passthrough";
36
+ /**
37
+ * Per-harness map from the harness's native event name to a canonical
38
+ * {@link LifecyclePhase}. Event names that don't appear here resolve to
39
+ * `passthrough` via {@link classifyLifecycle}.
40
+ */
41
+ export declare const HARNESS_LIFECYCLE_MAP: Record<string, Record<string, LifecyclePhase>>;
42
+ /**
43
+ * Resolve a harness event name to its canonical {@link LifecyclePhase}.
44
+ * Unknown harnesses or unknown event names both yield `passthrough`.
45
+ */
46
+ export declare function classifyLifecycle(harness: string, event: string): LifecyclePhase;
47
+ /**
48
+ * Phases that surface to the human (prompts, confirmations, notifications,
49
+ * advisory permission asks). Tool-call permission gates must never fire
50
+ * on these — the user is the decision-maker.
51
+ */
52
+ export declare const USER_FACING_PHASES: ReadonlySet<LifecyclePhase>;
53
+ /**
54
+ * Phases that represent actual tool execution against external systems.
55
+ * Only these run a real `use` permission check.
56
+ */
57
+ export declare const TOOL_EXECUTION_PHASES: ReadonlySet<LifecyclePhase>;
58
+ export declare function isUserFacingPhase(phase: LifecyclePhase): boolean;
59
+ export declare function isToolExecutionPhase(phase: LifecyclePhase): boolean;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical lifecycle vocabulary for harness plugins.
4
+ *
5
+ * Each harness exposes a different set of hook event names (Claude Code's
6
+ * `PreToolUse`, Gemini's `BeforeTool`, OpenCode's `tool.execute.before`,
7
+ * …). Mapping them onto a shared phase set lets the plugin code reason
8
+ * about *what is happening* without first remembering *which harness
9
+ * we're in*. Spans, logs, docs, and tests all reference the same set of
10
+ * phases.
11
+ *
12
+ * Phases split into three buckets:
13
+ *
14
+ * - **Session / agent lifecycle.** `session.start`, `session.stop`,
15
+ * `compaction`. The session begins or ends, or the harness compacts
16
+ * context. Plugins authenticate here (start) and may flush state
17
+ * (stop).
18
+ *
19
+ * - **User-facing interactions.** `user.prompt`, `user.interaction`,
20
+ * `permission.ask`. The harness is communicating with the human:
21
+ * they typed a prompt, the harness is surfacing a confirmation,
22
+ * the harness is asking whether to allow a tool. These are *traced*
23
+ * but never gated by a tool permission check — the user is the
24
+ * final decision-maker. (`permission.ask` does consult Ory to
25
+ * inform the harness's UI, but the call is advisory.)
26
+ *
27
+ * - **Tool execution.** `tool.before`, `tool.after`, `tool.failure`,
28
+ * `subagent.start`, `subagent.stop`. The agent is reaching into an
29
+ * external system (or spawning a child agent that will). These are
30
+ * the only phases that run a true `use` permission gate.
31
+ *
32
+ * `passthrough` is the explicit "nothing to do here" bucket — the
33
+ * harness fired an event we don't model, and we record an audit span
34
+ * and return.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.TOOL_EXECUTION_PHASES = exports.USER_FACING_PHASES = exports.HARNESS_LIFECYCLE_MAP = void 0;
38
+ exports.classifyLifecycle = classifyLifecycle;
39
+ exports.isUserFacingPhase = isUserFacingPhase;
40
+ exports.isToolExecutionPhase = isToolExecutionPhase;
41
+ /**
42
+ * Per-harness map from the harness's native event name to a canonical
43
+ * {@link LifecyclePhase}. Event names that don't appear here resolve to
44
+ * `passthrough` via {@link classifyLifecycle}.
45
+ */
46
+ exports.HARNESS_LIFECYCLE_MAP = {
47
+ "claude-code": {
48
+ SessionStart: "session.start",
49
+ SessionEnd: "session.stop",
50
+ PreToolUse: "tool.before",
51
+ PostToolUse: "tool.after",
52
+ PostToolUseFailure: "tool.failure",
53
+ PermissionRequest: "permission.ask",
54
+ UserPromptSubmit: "user.prompt",
55
+ SubagentStart: "subagent.start",
56
+ SubagentStop: "subagent.stop",
57
+ },
58
+ codex: {
59
+ SessionStart: "session.start",
60
+ Stop: "session.stop",
61
+ PreToolUse: "tool.before",
62
+ PostToolUse: "tool.after",
63
+ PermissionRequest: "permission.ask",
64
+ UserPromptSubmit: "user.prompt",
65
+ },
66
+ "gemini-cli": {
67
+ SessionStart: "session.start",
68
+ SessionEnd: "session.stop",
69
+ BeforeTool: "tool.before",
70
+ AfterTool: "tool.after",
71
+ BeforeToolSelection: "tool.before",
72
+ Notification: "user.interaction",
73
+ PreCompress: "compaction",
74
+ },
75
+ openclaw: {
76
+ session_start: "session.start",
77
+ before_agent_run: "user.prompt",
78
+ before_tool_call: "tool.before",
79
+ after_tool_call: "tool.after",
80
+ subagent_spawning: "subagent.start",
81
+ subagent_spawned: "subagent.start",
82
+ subagent_ended: "subagent.stop",
83
+ },
84
+ opencode: {
85
+ config: "session.start",
86
+ "chat.message": "user.prompt",
87
+ "permission.ask": "permission.ask",
88
+ "tool.execute.before": "tool.before",
89
+ "tool.execute.after": "tool.after",
90
+ },
91
+ };
92
+ /**
93
+ * Resolve a harness event name to its canonical {@link LifecyclePhase}.
94
+ * Unknown harnesses or unknown event names both yield `passthrough`.
95
+ */
96
+ function classifyLifecycle(harness, event) {
97
+ return exports.HARNESS_LIFECYCLE_MAP[harness]?.[event] ?? "passthrough";
98
+ }
99
+ /**
100
+ * Phases that surface to the human (prompts, confirmations, notifications,
101
+ * advisory permission asks). Tool-call permission gates must never fire
102
+ * on these — the user is the decision-maker.
103
+ */
104
+ exports.USER_FACING_PHASES = new Set([
105
+ "user.prompt",
106
+ "user.interaction",
107
+ "permission.ask",
108
+ ]);
109
+ /**
110
+ * Phases that represent actual tool execution against external systems.
111
+ * Only these run a real `use` permission check.
112
+ */
113
+ exports.TOOL_EXECUTION_PHASES = new Set([
114
+ "tool.before",
115
+ "tool.after",
116
+ "tool.failure",
117
+ ]);
118
+ function isUserFacingPhase(phase) {
119
+ return exports.USER_FACING_PHASES.has(phase);
120
+ }
121
+ function isToolExecutionPhase(phase) {
122
+ return exports.TOOL_EXECUTION_PHASES.has(phase);
123
+ }
@@ -7,16 +7,22 @@
7
7
  * is converted to a `ReadableSpan` shape and handed to the SDK exporter,
8
8
  * which serializes (JSON or protobuf) and POSTs to an OTLP HTTP endpoint.
9
9
  *
10
- * Standard OTel env vars consumed by `otlpExporterFromEnv()`:
11
- * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
12
- * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
13
- * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
14
- * - OTEL_EXPORTER_OTLP_TRACES_HEADERS traces-specific headers
15
- * - OTEL_EXPORTER_OTLP_PROTOCOL `http/protobuf` (default) or `http/json`
16
- * - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL traces-specific protocol
17
- * - OTEL_SERVICE_NAME service.name resource attribute
18
- * - OTEL_RESOURCE_ATTRIBUTES additional resource attributes
19
- * - ORY_OTLP_ENDPOINT Ory-prefixed alias for the dev launcher
10
+ * Standard OTel env vars consumed by `otlpExporterFromEnv()`. Each one has
11
+ * an `ORY_OTLP_*`-prefixed alias that is consulted as a fallback when the
12
+ * standard name is unset. Some harnesses (notably Claude Code) sanitize the
13
+ * env passed to hook subprocesses and drop every `OTEL_*` var, so the dev
14
+ * launcher mirrors each setting under the Ory prefix to survive the strip.
15
+ *
16
+ * Standard name Ory-prefixed fallback
17
+ * ───────────────────────────────────── ─────────────────────────────────
18
+ * OTEL_EXPORTER_OTLP_ENDPOINT ORY_OTLP_ENDPOINT
19
+ * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ORY_OTLP_TRACES_ENDPOINT
20
+ * OTEL_EXPORTER_OTLP_HEADERS ORY_OTLP_HEADERS
21
+ * OTEL_EXPORTER_OTLP_TRACES_HEADERS ORY_OTLP_TRACES_HEADERS
22
+ * OTEL_EXPORTER_OTLP_PROTOCOL ORY_OTLP_PROTOCOL
23
+ * OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ORY_OTLP_TRACES_PROTOCOL
24
+ * OTEL_SERVICE_NAME ORY_OTLP_SERVICE_NAME
25
+ * OTEL_RESOURCE_ATTRIBUTES ORY_OTLP_RESOURCE_ATTRIBUTES
20
26
  *
21
27
  * Failures are swallowed (callers receive a resolved Promise) so trace
22
28
  * export can never break the agent session. Transport errors surface
@@ -80,6 +86,12 @@ export declare function otlpExporterFromEnv(opts: {
80
86
  onError?: (err: unknown) => void;
81
87
  env?: NodeJS.ProcessEnv;
82
88
  }): OtlpExporter | undefined;
89
+ /**
90
+ * Read an OTel env var, falling back to its `ORY_OTLP_*`-prefixed alias when
91
+ * the standard name is unset. Trims whitespace and treats empty strings as
92
+ * absent so a propagated-but-blank var doesn't shadow the alias.
93
+ */
94
+ export declare function readEnvWithAlias(env: NodeJS.ProcessEnv, standardName: string): string | undefined;
83
95
  /**
84
96
  * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
85
97
  */
package/dist/otel/otlp.js CHANGED
@@ -8,16 +8,22 @@
8
8
  * is converted to a `ReadableSpan` shape and handed to the SDK exporter,
9
9
  * which serializes (JSON or protobuf) and POSTs to an OTLP HTTP endpoint.
10
10
  *
11
- * Standard OTel env vars consumed by `otlpExporterFromEnv()`:
12
- * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
13
- * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
14
- * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
15
- * - OTEL_EXPORTER_OTLP_TRACES_HEADERS traces-specific headers
16
- * - OTEL_EXPORTER_OTLP_PROTOCOL `http/protobuf` (default) or `http/json`
17
- * - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL traces-specific protocol
18
- * - OTEL_SERVICE_NAME service.name resource attribute
19
- * - OTEL_RESOURCE_ATTRIBUTES additional resource attributes
20
- * - ORY_OTLP_ENDPOINT Ory-prefixed alias for the dev launcher
11
+ * Standard OTel env vars consumed by `otlpExporterFromEnv()`. Each one has
12
+ * an `ORY_OTLP_*`-prefixed alias that is consulted as a fallback when the
13
+ * standard name is unset. Some harnesses (notably Claude Code) sanitize the
14
+ * env passed to hook subprocesses and drop every `OTEL_*` var, so the dev
15
+ * launcher mirrors each setting under the Ory prefix to survive the strip.
16
+ *
17
+ * Standard name Ory-prefixed fallback
18
+ * ───────────────────────────────────── ─────────────────────────────────
19
+ * OTEL_EXPORTER_OTLP_ENDPOINT ORY_OTLP_ENDPOINT
20
+ * OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ORY_OTLP_TRACES_ENDPOINT
21
+ * OTEL_EXPORTER_OTLP_HEADERS ORY_OTLP_HEADERS
22
+ * OTEL_EXPORTER_OTLP_TRACES_HEADERS ORY_OTLP_TRACES_HEADERS
23
+ * OTEL_EXPORTER_OTLP_PROTOCOL ORY_OTLP_PROTOCOL
24
+ * OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ORY_OTLP_TRACES_PROTOCOL
25
+ * OTEL_SERVICE_NAME ORY_OTLP_SERVICE_NAME
26
+ * OTEL_RESOURCE_ATTRIBUTES ORY_OTLP_RESOURCE_ATTRIBUTES
21
27
  *
22
28
  * Failures are swallowed (callers receive a resolved Promise) so trace
23
29
  * export can never break the agent session. Transport errors surface
@@ -26,6 +32,7 @@
26
32
  Object.defineProperty(exports, "__esModule", { value: true });
27
33
  exports.OtlpExporter = void 0;
28
34
  exports.otlpExporterFromEnv = otlpExporterFromEnv;
35
+ exports.readEnvWithAlias = readEnvWithAlias;
29
36
  exports.parseKeyValueList = parseKeyValueList;
30
37
  exports.toReadableSpan = toReadableSpan;
31
38
  const api_1 = require("@opentelemetry/api");
@@ -124,9 +131,10 @@ function otlpExporterFromEnv(opts) {
124
131
  const protocol = resolveProtocol(env, opts.onError);
125
132
  if (!protocol)
126
133
  return undefined;
127
- const headers = mergeHeaders(parseKeyValueList(env.OTEL_EXPORTER_OTLP_HEADERS), parseKeyValueList(env.OTEL_EXPORTER_OTLP_TRACES_HEADERS));
134
+ const headers = mergeHeaders(parseKeyValueList(readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_HEADERS")), parseKeyValueList(readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_TRACES_HEADERS")));
128
135
  const resource = {
129
- "service.name": env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${opts.harness}`,
136
+ "service.name": readEnvWithAlias(env, "OTEL_SERVICE_NAME") ??
137
+ `ory-agent-plugin-${opts.harness}`,
130
138
  "service.namespace": "ory.agent_plugins",
131
139
  "ory.harness": opts.harness,
132
140
  "process.pid": process.pid,
@@ -145,7 +153,7 @@ function otlpExporterFromEnv(opts) {
145
153
  resource["ory.git.branch"] = opts.gitBranch;
146
154
  if (opts.gitCommit)
147
155
  resource["ory.git.commit"] = opts.gitCommit;
148
- for (const [k, v] of Object.entries(parseKeyValueList(env.OTEL_RESOURCE_ATTRIBUTES))) {
156
+ for (const [k, v] of Object.entries(parseKeyValueList(readEnvWithAlias(env, "OTEL_RESOURCE_ATTRIBUTES")))) {
149
157
  resource[k] = v;
150
158
  }
151
159
  return new OtlpExporter({
@@ -157,22 +165,20 @@ function otlpExporterFromEnv(opts) {
157
165
  });
158
166
  }
159
167
  function resolveEndpoint(env) {
160
- const tracesUrl = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
168
+ // ORY_OTLP_*-prefixed aliases survive harnesses (e.g. Claude Code) that
169
+ // sanitize the env passed to hook subprocesses and drop OTEL_*-prefixed
170
+ // vars. The standard OTel name still wins when present.
171
+ const tracesUrl = readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT");
161
172
  if (tracesUrl)
162
173
  return tracesUrl;
163
- // ORY_OTLP_ENDPOINT is an Ory-prefixed alias used by the dev launcher.
164
- // Some harnesses (e.g. Claude Code) filter the env they pass to hook
165
- // subprocesses and drop OTEL_*-prefixed vars, but allow ORY_*-prefixed
166
- // ones through. The alias lets the launcher hand the OTLP endpoint to
167
- // the hook process without depending on OTEL_* propagation.
168
- const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || env.ORY_OTLP_ENDPOINT?.trim();
174
+ const base = readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_ENDPOINT");
169
175
  if (!base)
170
176
  return undefined;
171
177
  return base.replace(/\/+$/, "") + TRACES_PATH;
172
178
  }
173
179
  function resolveProtocol(env, onError) {
174
- const raw = (env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ??
175
- env.OTEL_EXPORTER_OTLP_PROTOCOL ??
180
+ const raw = (readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL") ??
181
+ readEnvWithAlias(env, "OTEL_EXPORTER_OTLP_PROTOCOL") ??
176
182
  "http/protobuf")
177
183
  .trim()
178
184
  .toLowerCase();
@@ -181,6 +187,37 @@ function resolveProtocol(env, onError) {
181
187
  onError?.(new Error(`OTEL_EXPORTER_OTLP_PROTOCOL=${raw} not supported (only http/protobuf and http/json). Skipping OTel export.`));
182
188
  return undefined;
183
189
  }
190
+ /**
191
+ * Standard OTel env var name → Ory-prefixed fallback. Standard wins when set;
192
+ * the alias is consulted only when the standard name is empty/missing. The
193
+ * historical `ORY_OTLP_ENDPOINT` alias for `OTEL_EXPORTER_OTLP_ENDPOINT` is
194
+ * preserved verbatim for compatibility with existing dev-launcher envs.
195
+ */
196
+ const ORY_ENV_ALIAS = {
197
+ OTEL_EXPORTER_OTLP_ENDPOINT: "ORY_OTLP_ENDPOINT",
198
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: "ORY_OTLP_TRACES_ENDPOINT",
199
+ OTEL_EXPORTER_OTLP_HEADERS: "ORY_OTLP_HEADERS",
200
+ OTEL_EXPORTER_OTLP_TRACES_HEADERS: "ORY_OTLP_TRACES_HEADERS",
201
+ OTEL_EXPORTER_OTLP_PROTOCOL: "ORY_OTLP_PROTOCOL",
202
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "ORY_OTLP_TRACES_PROTOCOL",
203
+ OTEL_SERVICE_NAME: "ORY_OTLP_SERVICE_NAME",
204
+ OTEL_RESOURCE_ATTRIBUTES: "ORY_OTLP_RESOURCE_ATTRIBUTES",
205
+ };
206
+ /**
207
+ * Read an OTel env var, falling back to its `ORY_OTLP_*`-prefixed alias when
208
+ * the standard name is unset. Trims whitespace and treats empty strings as
209
+ * absent so a propagated-but-blank var doesn't shadow the alias.
210
+ */
211
+ function readEnvWithAlias(env, standardName) {
212
+ const primary = env[standardName]?.trim();
213
+ if (primary)
214
+ return primary;
215
+ const aliasName = ORY_ENV_ALIAS[standardName];
216
+ if (!aliasName)
217
+ return undefined;
218
+ const alias = env[aliasName]?.trim();
219
+ return alias || undefined;
220
+ }
184
221
  // ─── Env parsing helpers ───────────────────────────────────────────
185
222
  /**
186
223
  * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
@@ -117,6 +117,57 @@ export interface ApplyPermissionModeContext {
117
117
  * proceed despite the deny.
118
118
  */
119
119
  export declare function applyPermissionMode(client: OryAgentClient, allowed: boolean, context?: ApplyPermissionModeContext): ModeDecision;
120
+ /**
121
+ * Outcome of {@link gateToolCall}. Either the tool is a user-interaction
122
+ * primitive (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, …) and we
123
+ * pass through with a single audit span, or it's a real tool execution
124
+ * and the caller gets the standard {@link PermissionDecision}.
125
+ */
126
+ export type ToolGateOutcome = {
127
+ kind: "interactive";
128
+ /** Attributes to attach to the caller's pass-through trace, if any. */
129
+ spanAttributes: {
130
+ interactive: true;
131
+ toolName: string;
132
+ };
133
+ } | PermissionDecision;
134
+ export interface GateToolCallArgs {
135
+ /** Harness name (`claude-code`, `codex`, …). Used to look up the interactive-tool catalog. */
136
+ harness: string;
137
+ /** The tool the agent is invoking. */
138
+ toolName: string;
139
+ /** Permission check to run when the tool is a real execution. */
140
+ check: PermissionCheck;
141
+ /**
142
+ * Attributes merged into the permission span (real path) and the
143
+ * `user.interaction` span (interactive path).
144
+ */
145
+ spanAttributes?: Record<string, unknown>;
146
+ /** Override the resolved {@link PermissionMode}. Tests use this. */
147
+ modeOverride?: PermissionMode;
148
+ }
149
+ /**
150
+ * Single entry point for the pre-tool-use gate. Splits the harness's
151
+ * incoming "tool" into two semantic categories:
152
+ *
153
+ * - **Interactive** — the tool surfaces UI to the user
154
+ * (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, plus anything
155
+ * listed in `ORY_INTERACTIVE_TOOLS`). The plugin must not gate these
156
+ * through Ory: the user is the decision-maker, and blocking them in
157
+ * enforce mode (or logging a misleading observe-deny) hides the very
158
+ * prompt the user needs to see. We record one `user.interaction`
159
+ * audit span and return — no permission check, no `tool.invoke`,
160
+ * no `tool.block`.
161
+ *
162
+ * - **Execution** — every other tool. Delegates to
163
+ * {@link checkAndDecide} so observe/enforce/fail-open behavior is
164
+ * identical to the legacy code path.
165
+ *
166
+ * The caller branches on `outcome.kind`. The four execution kinds
167
+ * (`allow`, `deny`, `observe`, `fail_open`) keep their existing
168
+ * semantics; the new `interactive` kind means "do nothing else."
169
+ */
170
+ export declare function gateToolCall(client: OryAgentClient, args: GateToolCallArgs): Promise<ToolGateOutcome>;
120
171
  /**
121
172
  * Run a permission check and resolve the configured mode against the
122
173
  * result. Never throws — fail-open scenarios are surfaced as a typed
@@ -23,8 +23,10 @@
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.applyPermissionMode = applyPermissionMode;
26
+ exports.gateToolCall = gateToolCall;
26
27
  exports.checkAndDecide = checkAndDecide;
27
28
  const config_js_1 = require("./config.js");
29
+ const tool_catalog_js_1 = require("./tool-catalog.js");
28
30
  function formatSubjectSet(set) {
29
31
  if (!set)
30
32
  return undefined;
@@ -75,6 +77,52 @@ function applyPermissionMode(client, allowed, context = {}) {
75
77
  }
76
78
  return { kind: "deny", mode: "enforce", spanAttributes };
77
79
  }
80
+ /**
81
+ * Single entry point for the pre-tool-use gate. Splits the harness's
82
+ * incoming "tool" into two semantic categories:
83
+ *
84
+ * - **Interactive** — the tool surfaces UI to the user
85
+ * (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, plus anything
86
+ * listed in `ORY_INTERACTIVE_TOOLS`). The plugin must not gate these
87
+ * through Ory: the user is the decision-maker, and blocking them in
88
+ * enforce mode (or logging a misleading observe-deny) hides the very
89
+ * prompt the user needs to see. We record one `user.interaction`
90
+ * audit span and return — no permission check, no `tool.invoke`,
91
+ * no `tool.block`.
92
+ *
93
+ * - **Execution** — every other tool. Delegates to
94
+ * {@link checkAndDecide} so observe/enforce/fail-open behavior is
95
+ * identical to the legacy code path.
96
+ *
97
+ * The caller branches on `outcome.kind`. The four execution kinds
98
+ * (`allow`, `deny`, `observe`, `fail_open`) keep their existing
99
+ * semantics; the new `interactive` kind means "do nothing else."
100
+ */
101
+ async function gateToolCall(client, args) {
102
+ if ((0, tool_catalog_js_1.isInteractiveTool)(args.harness, args.toolName)) {
103
+ client.logger.debug("tool.interactive", {
104
+ harness: args.harness,
105
+ toolName: args.toolName,
106
+ note: "user-interaction primitive — skipping permission check",
107
+ });
108
+ client.tracer.record("user.interaction", "ok", {
109
+ attributes: {
110
+ harness: args.harness,
111
+ toolName: args.toolName,
112
+ kind: "tool",
113
+ ...args.spanAttributes,
114
+ },
115
+ });
116
+ return {
117
+ kind: "interactive",
118
+ spanAttributes: { interactive: true, toolName: args.toolName },
119
+ };
120
+ }
121
+ return checkAndDecide(client, args.check, {
122
+ spanAttributes: args.spanAttributes,
123
+ modeOverride: args.modeOverride,
124
+ });
125
+ }
78
126
  /**
79
127
  * Run a permission check and resolve the configured mode against the
80
128
  * result. Never throws — fail-open scenarios are surfaced as a typed
@@ -36,3 +36,34 @@ export declare function getToolCatalog(harness: string): readonly string[];
36
36
  * broad access so the same launcher can run every harness end-to-end.
37
37
  */
38
38
  export declare const ALL_TOOLS: readonly string[];
39
+ /**
40
+ * Per-harness tool names whose semantics are "ask / inform the user"
41
+ * rather than "act on an external system". These reach the pre-tool-use
42
+ * hook the same way real tools do (e.g. Claude Code's `AskUserQuestion`
43
+ * arrives via `PreToolUse`), but they aren't tool *executions* — gating
44
+ * them through Ory would either block the user from being asked or, in
45
+ * observe mode, log a misleading `permission.observe_deny` for an event
46
+ * the user is about to handle directly.
47
+ *
48
+ * Plugins consult this list in their pre-tool gate and short-circuit to
49
+ * a single `user.interaction` audit span when a match is found. See
50
+ * `gateToolCall` in `permissions.ts`.
51
+ *
52
+ * The list is best-effort and additive: operators can extend it at
53
+ * runtime via the `ORY_INTERACTIVE_TOOLS` env var (comma-separated names
54
+ * applied to every harness).
55
+ */
56
+ export declare const INTERACTIVE_TOOL_CATALOG: Record<string, readonly string[]>;
57
+ /**
58
+ * Names from {@link INTERACTIVE_TOOL_CATALOG} for the given harness,
59
+ * merged with any operator-supplied names from `ORY_INTERACTIVE_TOOLS`.
60
+ * Unknown harnesses still respect the env-var extension.
61
+ */
62
+ export declare function getInteractiveToolCatalog(harness: string): readonly string[];
63
+ /**
64
+ * Does the named tool, for the given harness, represent a user-facing
65
+ * interaction (ask / inform / confirm) rather than an external-system
66
+ * tool execution? Treats unknown harnesses the same as known ones — the
67
+ * env-var extension still applies.
68
+ */
69
+ export declare function isInteractiveTool(harness: string, toolName: string): boolean;
@@ -13,8 +13,10 @@
13
13
  * catalog update is safe.
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = void 0;
16
+ exports.INTERACTIVE_TOOL_CATALOG = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = void 0;
17
17
  exports.getToolCatalog = getToolCatalog;
18
+ exports.getInteractiveToolCatalog = getInteractiveToolCatalog;
19
+ exports.isInteractiveTool = isInteractiveTool;
18
20
  /**
19
21
  * Built-in tool names known to ship with each supported harness.
20
22
  */
@@ -67,3 +69,58 @@ function getToolCatalog(harness) {
67
69
  * broad access so the same launcher can run every harness end-to-end.
68
70
  */
69
71
  exports.ALL_TOOLS = Array.from(new Set(Object.values(exports.HARNESS_TOOL_CATALOG).flat()));
72
+ /**
73
+ * Per-harness tool names whose semantics are "ask / inform the user"
74
+ * rather than "act on an external system". These reach the pre-tool-use
75
+ * hook the same way real tools do (e.g. Claude Code's `AskUserQuestion`
76
+ * arrives via `PreToolUse`), but they aren't tool *executions* — gating
77
+ * them through Ory would either block the user from being asked or, in
78
+ * observe mode, log a misleading `permission.observe_deny` for an event
79
+ * the user is about to handle directly.
80
+ *
81
+ * Plugins consult this list in their pre-tool gate and short-circuit to
82
+ * a single `user.interaction` audit span when a match is found. See
83
+ * `gateToolCall` in `permissions.ts`.
84
+ *
85
+ * The list is best-effort and additive: operators can extend it at
86
+ * runtime via the `ORY_INTERACTIVE_TOOLS` env var (comma-separated names
87
+ * applied to every harness).
88
+ */
89
+ exports.INTERACTIVE_TOOL_CATALOG = {
90
+ "claude-code": ["AskUserQuestion", "ExitPlanMode", "TodoWrite"],
91
+ codex: [],
92
+ "gemini-cli": [],
93
+ openclaw: [],
94
+ opencode: [],
95
+ };
96
+ function parseEnvInteractiveTools() {
97
+ const raw = process.env.ORY_INTERACTIVE_TOOLS;
98
+ if (!raw)
99
+ return [];
100
+ return raw
101
+ .split(",")
102
+ .map((name) => name.trim())
103
+ .filter((name) => name.length > 0);
104
+ }
105
+ /**
106
+ * Names from {@link INTERACTIVE_TOOL_CATALOG} for the given harness,
107
+ * merged with any operator-supplied names from `ORY_INTERACTIVE_TOOLS`.
108
+ * Unknown harnesses still respect the env-var extension.
109
+ */
110
+ function getInteractiveToolCatalog(harness) {
111
+ const builtIn = exports.INTERACTIVE_TOOL_CATALOG[harness] ?? [];
112
+ const envExtra = parseEnvInteractiveTools();
113
+ if (envExtra.length === 0)
114
+ return builtIn;
115
+ // Stable order: built-ins first, then env additions, deduplicated.
116
+ return Array.from(new Set([...builtIn, ...envExtra]));
117
+ }
118
+ /**
119
+ * Does the named tool, for the given harness, represent a user-facing
120
+ * interaction (ask / inform / confirm) rather than an external-system
121
+ * tool execution? Treats unknown harnesses the same as known ones — the
122
+ * env-var extension still applies.
123
+ */
124
+ function isInteractiveTool(harness, toolName) {
125
+ return getInteractiveToolCatalog(harness).includes(toolName);
126
+ }
package/dist/tracer.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { EventEmitter } from "node:events";
20
20
  import type { SpanExporter } from "./otel/exporter.js";
21
- export type TraceEvent = "session.start" | "session.end" | "session.verify" | "user.auth" | "user.prompt" | "agent.auth" | "oauth2.introspect" | "oauth2.login" | "oauth2.refresh" | "permission.check" | "permission.batch_check" | "permission.observe_deny" | "tool.invoke" | "tool.complete" | "tool.block" | "tool.fail" | "turn.stop" | "subagent.start" | "subagent.stop" | "notification" | "compaction" | "relationship.create" | "relationship.delete" | "relationship.patch" | "hook.receive" | "hook.passthrough" | "config.resolve";
21
+ export type TraceEvent = "session.start" | "session.end" | "session.verify" | "user.auth" | "user.prompt" | "user.interaction" | "agent.auth" | "oauth2.introspect" | "oauth2.login" | "oauth2.refresh" | "permission.check" | "permission.batch_check" | "permission.observe_deny" | "tool.invoke" | "tool.complete" | "tool.block" | "tool.fail" | "turn.stop" | "subagent.start" | "subagent.stop" | "notification" | "compaction" | "relationship.create" | "relationship.delete" | "relationship.patch" | "hook.receive" | "hook.passthrough" | "config.resolve";
22
22
  export type SpanStatus = "ok" | "error" | "denied" | "skipped";
23
23
  export interface TraceSpan {
24
24
  traceId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",