@ory/argus 0.8.1 → 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/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
+ }
@@ -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.1",
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",