@ory/argus 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,166 @@
1
+ ---
2
+ name: ory-permissions-onboarding
3
+ description: Onboard a fresh install onto Ory Permissions for AI agent tool calls. Use when the user has just installed the Ory plugin and wants to enforce per-tool authorization without first getting blocked by missing tuples. Walks through observe-mode observation, idempotent tuple bootstrap, and promotion to enforce mode.
4
+ ---
5
+
6
+ # Onboard onto Ory Permissions for Agent Tool Calls
7
+
8
+ You are helping a user move a freshly-installed Ory agent plugin from
9
+ "permissions are running but never block" (the default after install)
10
+ to "permissions are enforcing" (the production posture), without the
11
+ common first-run failure mode of getting every tool call blocked
12
+ because no tuples exist yet.
13
+
14
+ The plugin ships with two orthogonal switches:
15
+
16
+ - **`auditOnly`** — a kill switch. When set, Ory is *off entirely* (no
17
+ auth, no permission checks). This is for users who want only audit
18
+ logging of tool invocations. Not what onboarding is about.
19
+ - **`permissionMode`** — the dial this skill is about. `observe` (the
20
+ default) runs every permission check, logs denials, but allows the
21
+ tool to proceed. `enforce` blocks on deny. Mode is read from
22
+ `ORY_PERMISSION_MODE` first, then the shared config file, then
23
+ defaults to `observe`.
24
+
25
+ The journey: **install → observe → bootstrap → enforce.**
26
+
27
+ ## Step 1: Confirm the plugin is installed and configured
28
+
29
+ Verify the plugin is wired up and pointing at the right Ory project:
30
+
31
+ ```sh
32
+ {{NPX}} status
33
+ ```
34
+
35
+ What you want to see:
36
+
37
+ - **Project URL**: a real Ory URL or your local dev gateway (not "NOT SET").
38
+ - **API Key**: set (Ory Network) or unset (local Keto OSS — that's fine).
39
+ - **Mode**: not "audit-only" — that would mean Ory is disabled.
40
+
41
+ If any of those are wrong, fix them before continuing:
42
+
43
+ ```sh
44
+ {{NPX}} configure --project-url <URL> --api-key <KEY>
45
+ ```
46
+
47
+ ## Step 2: Look at the current permission posture
48
+
49
+ ```sh
50
+ {{NPX}} permissions status
51
+ ```
52
+
53
+ This prints:
54
+
55
+ - The current **permission mode** (`observe` or `enforce`) and where it
56
+ was resolved from (env / config / default).
57
+ - The **subject** (the user identity tuples are checked against).
58
+ - For each tool in this harness's built-in catalog, **allowed / denied /
59
+ errored** based on a real `checkPermission` call against your Ory
60
+ project right now.
61
+
62
+ On a fresh install you will almost always see every tool reported as
63
+ **denied** — because no tuples exist yet for the current user. That's
64
+ expected, and in observe mode it is *not blocking anyone*. The tool
65
+ calls are running through; you are simply seeing what *would* be
66
+ blocked once enforce mode is turned on.
67
+
68
+ If the status command prints `Mode: enforce` but tuples are missing,
69
+ stop here: tool calls *are* being blocked right now. Either bootstrap
70
+ (next step) or run `{{NPX}} permissions observe` first to flip back to
71
+ non-blocking.
72
+
73
+ ## Step 3: Bootstrap tuples for the harness's built-in tools
74
+
75
+ Grant the current user `use` on every tool the harness ships with:
76
+
77
+ ```sh
78
+ {{NPX}} permissions bootstrap
79
+ ```
80
+
81
+ This is **idempotent** — Keto returns 409 on a tuple that already
82
+ exists, and the command treats that as success. Safe to re-run any time
83
+ (e.g. after the harness adds a new tool to its catalog).
84
+
85
+ The command writes `<namespace>:<tool>#use@<userSubject>` for each tool
86
+ in the harness's known catalog. MCP server tools are *not* covered —
87
+ they are discovered dynamically per session, so they need tuples added
88
+ out-of-band as they come into scope.
89
+
90
+ ### When bootstrap can't write tuples
91
+
92
+ Two common failure modes:
93
+
94
+ 1. **No user identity cached.** Bootstrap needs to know which subject
95
+ to grant tuples to. If the auth gate has never run (no PKCE login,
96
+ no `ORY_USER_SUBJECT_ID`), the command refuses. Run the harness once
97
+ with `ORY_AUTH_GATE=1` to cache a user token, or set
98
+ `ORY_USER_SUBJECT_ID=<id>` to target a known subject.
99
+ 2. **Credentials lack write scope on the permission namespace.** The
100
+ command prints the full tuple list so you can apply them manually
101
+ via `keto relation-tuples create`, the Ory Console's *Add
102
+ relationship* dialog, or `curl` against Keto's write API. Hand the
103
+ list to whoever holds write credentials.
104
+
105
+ ### Add tuples for non-default tools
106
+
107
+ If the harness has tools beyond the built-in catalog (MCP servers,
108
+ custom commands), grant them by writing tuples directly. Use the same
109
+ shape:
110
+
111
+ ```
112
+ namespace: AgentTools (or whatever ORY_PERMISSION_NAMESPACE is set to)
113
+ object: <tool name>
114
+ relation: use
115
+ subject: <user subject id>
116
+ ```
117
+
118
+ Re-run `{{NPX}} permissions status` afterwards to confirm coverage.
119
+
120
+ ## Step 4: Promote to enforce mode
121
+
122
+ Once `permissions status` shows the tools you actually use are
123
+ `allowed`, flip the dial:
124
+
125
+ ```sh
126
+ {{NPX}} permissions enforce
127
+ ```
128
+
129
+ This persists `permissionMode = "enforce"` in the shared config file.
130
+ Subsequent sessions will block any tool call that returns deny. Mode
131
+ can always be flipped back:
132
+
133
+ ```sh
134
+ {{NPX}} permissions observe
135
+ ```
136
+
137
+ `ORY_PERMISSION_MODE`, when set, overrides the config — useful for CI
138
+ runs that want enforce regardless of what's persisted, or for one-off
139
+ debugging sessions where you want observe without rewriting config.
140
+
141
+ ## Step 5: Verify a real session
142
+
143
+ Start a normal agent session and confirm:
144
+
145
+ - **Allowed tools** invoke without complaint.
146
+ - **Denied tools** (if any) are blocked with a clear "Ory: permission
147
+ denied" message.
148
+ - The trace file shows `permission.check` spans for each tool call and
149
+ `tool.block` spans for any denial.
150
+
151
+ If a tool is unexpectedly blocked, run `{{NPX}} permissions status` to
152
+ confirm whether the tuple exists, then add or fix tuples and re-test.
153
+
154
+ ## Reference: what each command does
155
+
156
+ | Command | Effect |
157
+ |---|---|
158
+ | `{{NPX}} permissions status` | Print mode + per-tool allow/deny breakdown for the current user. Read-only. |
159
+ | `{{NPX}} permissions bootstrap` | Write `use` tuples for the harness's built-in tools. Idempotent. |
160
+ | `{{NPX}} permissions bootstrap --dry-run` | Print what would be written without making any changes. |
161
+ | `{{NPX}} permissions observe` | Persist `permissionMode = "observe"` (denies log, allow through). |
162
+ | `{{NPX}} permissions enforce` | Persist `permissionMode = "enforce"` (denies block). |
163
+
164
+ For deeper background on the authentication side of the flow (which
165
+ identity is the subject, how the user gate resolves it), see
166
+ {{REF_AUTH_SETUP}} and {{REF_LOGIN_FLOW}}.
package/dist/config.d.ts CHANGED
@@ -59,11 +59,30 @@ export interface OryAgentCredentialsBlock {
59
59
  */
60
60
  subAgents?: Record<string, OryAgentDynamicCredentials>;
61
61
  }
62
+ /**
63
+ * What the plugin does when a permission check returns deny.
64
+ *
65
+ * - `observe` — log the denial, emit a `permission.observe_deny` span,
66
+ * allow the tool through. The onboarding default: first-time users
67
+ * see what Ory would block, without being blocked.
68
+ * - `enforce` — block the tool. The production posture.
69
+ *
70
+ * Orthogonal to {@link OryPluginConfig.auditOnly}, which is a kill-switch
71
+ * that disables Ory entirely (no auth, no permission checks). `auditOnly`
72
+ * dominates: when set, `permissionMode` is irrelevant.
73
+ */
74
+ export type PermissionMode = "observe" | "enforce";
62
75
  export interface OryPluginConfig {
63
76
  projectUrl?: string;
64
77
  apiKey?: string;
65
78
  /** When true, only audit logging is enabled — no auth or permission checks. */
66
79
  auditOnly?: boolean;
80
+ /**
81
+ * How to handle permission denies when checks *are* running. Defaults
82
+ * to `observe` for frictionless first-run; flip to `enforce` once the
83
+ * tuple set is correct. See {@link PermissionMode}.
84
+ */
85
+ permissionMode?: PermissionMode;
67
86
  /** Credentials for the human user (interactive PKCE login). */
68
87
  user?: OryUserCredentials;
69
88
  /** Credentials for the AI agent process (machine identity). */
@@ -126,6 +145,8 @@ export declare function resolveConfig(): {
126
145
  projectUrl?: string;
127
146
  apiKey?: string;
128
147
  auditOnly: boolean;
148
+ permissionMode: PermissionMode;
149
+ permissionModeSource: "env" | "config" | "default";
129
150
  projectUrlSource: "env" | "config" | "none";
130
151
  apiKeySource: "env" | "config" | "none";
131
152
  };
package/dist/config.js CHANGED
@@ -115,6 +115,7 @@ function loadConfig() {
115
115
  projectUrl: typeof parsed.projectUrl === "string" ? parsed.projectUrl : undefined,
116
116
  apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
117
117
  auditOnly: parsed.auditOnly === true ? true : undefined,
118
+ permissionMode: parsePermissionMode(parsed.permissionMode),
118
119
  user: parseUserCredentials(parsed),
119
120
  agent: parseAgentCredentials(parsed),
120
121
  };
@@ -123,6 +124,9 @@ function loadConfig() {
123
124
  return {};
124
125
  }
125
126
  }
127
+ function parsePermissionMode(value) {
128
+ return value === "observe" || value === "enforce" ? value : undefined;
129
+ }
126
130
  function parseAgentCredentials(parsed) {
127
131
  const raw = parsed.agent;
128
132
  if (!raw || typeof raw !== "object")
@@ -320,10 +324,19 @@ function resolveConfig() {
320
324
  const file = loadConfig();
321
325
  const envProjectUrl = process.env.ORY_PROJECT_URL;
322
326
  const envApiKey = process.env.ORY_API_KEY;
327
+ const envMode = parsePermissionMode(process.env.ORY_PERMISSION_MODE);
328
+ const permissionMode = envMode ?? file.permissionMode ?? "observe";
329
+ const permissionModeSource = envMode
330
+ ? "env"
331
+ : file.permissionMode
332
+ ? "config"
333
+ : "default";
323
334
  return {
324
335
  projectUrl: envProjectUrl ?? file.projectUrl,
325
336
  apiKey: envApiKey ?? file.apiKey,
326
337
  auditOnly: file.auditOnly === true,
338
+ permissionMode,
339
+ permissionModeSource,
327
340
  projectUrlSource: envProjectUrl ? "env" : file.projectUrl ? "config" : "none",
328
341
  apiKeySource: envApiKey ? "env" : file.apiKey ? "config" : "none",
329
342
  };
package/dist/index.d.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  export { OryAgentClient, type OryAgentConfig, type PrincipalIdentity, } from "./client.js";
2
2
  export { DebugLogger, redactLogData, type LogEntry, type LogLevel, } from "./logger.js";
3
3
  export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type TraceEvent, type SpanStatus, type TraceSpan, type SpanOptions, type TracerOptions, type TracerContext, } from "./tracer.js";
4
- export { loadConfig, saveConfig, resolveConfig, mutateConfig, getConfigPath, getDataDir, getHarnessDataDir, type OryPluginConfig, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, } from "./config.js";
4
+ export { loadConfig, saveConfig, resolveConfig, mutateConfig, getConfigPath, getDataDir, getHarnessDataDir, type OryPluginConfig, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, type PermissionMode, } from "./config.js";
5
5
  export { pkceLogin, refreshAccessToken, detectHeadless, generateCodeVerifier, sha256Base64Url, buildAuthorizeUrl, LOOPBACK_PORTS, DEFAULT_LOGIN_TIMEOUT_MS, type PkceLoginOptions, type PkceLoginOutcome, type PkceDeclineReason, } from "./auth.js";
6
6
  export { loadTokens, saveTokens, clearTokens, isExpired, refreshAndSave, tryAcquirePkceFlightLock, clearPkceFlightLock, waitForPeerTokens, waitForPeerTokensSync, TOKEN_EXPIRY_SKEW_SEC, type PkceFlightLock, } from "./auth-store.js";
7
7
  export { ensureUserAuthenticated, ensureAuthenticated, type AuthGateDecision, type AuthGateMode, type AuthGateOptions, } from "./auth-gate.js";
8
8
  export { resolveAgentCredentials, ensureAgentIdentity, ensureSubAgentIdentity, fetchClientCredentialsToken, registerAgentClient, loadAgentDynamicCredentials, saveAgentDynamicCredentials, clearAgentDynamicCredentials, loadSubAgentDynamicCredentials, saveSubAgentDynamicCredentials, clearSubAgentDynamicCredentials, AGENT_TOKEN_EXPIRY_SKEW_SEC, type AgentCredentials, type AgentCredentialKind, type ResolveAgentCredentialsOptions, type EnsureAgentIdentityOptions, type RegisterAgentClientArgs, type SubAgentIdentity, type EnsureSubAgentIdentityOptions, } from "./agent-auth.js";
9
9
  export { type SessionInfo, type OAuth2TokenInfo, type PermissionCheck, type PermissionResult, type BatchPermissionResult, type OryError, type OryErrorCode, } from "./types.js";
10
10
  export { runConfigureCommand, runAgentCommand, printOryConfig, printEnvironment, printLogTail, printEnvHelp, printTraceTail, runWatchCommand, isTtyAvailable, promptOnTty, promptForProjectUrl, interactiveConfigPrompt, } from "./cli.js";
11
+ export { runPermissionsCommand, isUserIdentityCached, maybeAutoBootstrap, printPermissionsOnboardingHelp, } from "./permissions-cli.js";
11
12
  export { parseSetupArgs, readJsonFile, writeJsonFile, isOryHookCommand, resolveHookCommand, matcherHookEntry, mergeMatcherHooks, removeMatcherHooks, flatHookEntry, mergeFlatHooks, removeFlatHooks, printSetupHelp, printNextSteps, resolveMcpServerCommand, mcpServerEntry, mergeMcpServer, removeMcpServer, registerPlugin, unregisterPlugin, type SetupArgs, type HookCommand, type MatcherEntry, } from "./setup.js";
12
13
  export { runDevLauncher, type DevLauncherConfig, type InstallContext, } from "./dev.js";
13
14
  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";
14
15
  export { runLocalCommand, ensureDevJaeger, stopDevJaeger, DEV_JAEGER_CONTAINER, type EnsureDevJaegerResult, type StopDevJaegerResult, } from "./local/index.js";
15
16
  export { runRegistryCommand } from "./registry/index.js";
17
+ export { checkAndDecide, applyPermissionMode, type PermissionDecision, type ModeDecision, type CheckAndDecideOptions, type ApplyPermissionModeContext, } from "./permissions.js";
18
+ export { HARNESS_TOOL_CATALOG, KNOWN_HARNESSES, ALL_TOOLS, getToolCatalog, type KnownHarness, } from "./tool-catalog.js";
16
19
  export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkMcpPermission, type McpToolIdentifier, type McpPermissionCheckOptions, type McpPermissionResult, } from "./mcp.js";
17
20
  export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
18
21
  export { formatDenialMessage, formatDenialSummary, formatAlertMessage, formatAlertSummary, alertAttributes, OryDenialError, type DenialContext, type AlertAttributes, } from "./denial.js";
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use strict";
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
- exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = 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.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.OtlpHttpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = void 0;
4
+ exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = 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.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.OtlpHttpExporter = 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 = 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");
@@ -71,6 +71,11 @@ Object.defineProperty(exports, "isTtyAvailable", { enumerable: true, get: functi
71
71
  Object.defineProperty(exports, "promptOnTty", { enumerable: true, get: function () { return cli_js_1.promptOnTty; } });
72
72
  Object.defineProperty(exports, "promptForProjectUrl", { enumerable: true, get: function () { return cli_js_1.promptForProjectUrl; } });
73
73
  Object.defineProperty(exports, "interactiveConfigPrompt", { enumerable: true, get: function () { return cli_js_1.interactiveConfigPrompt; } });
74
+ var permissions_cli_js_1 = require("./permissions-cli.js");
75
+ Object.defineProperty(exports, "runPermissionsCommand", { enumerable: true, get: function () { return permissions_cli_js_1.runPermissionsCommand; } });
76
+ Object.defineProperty(exports, "isUserIdentityCached", { enumerable: true, get: function () { return permissions_cli_js_1.isUserIdentityCached; } });
77
+ Object.defineProperty(exports, "maybeAutoBootstrap", { enumerable: true, get: function () { return permissions_cli_js_1.maybeAutoBootstrap; } });
78
+ Object.defineProperty(exports, "printPermissionsOnboardingHelp", { enumerable: true, get: function () { return permissions_cli_js_1.printPermissionsOnboardingHelp; } });
74
79
  var setup_js_1 = require("./setup.js");
75
80
  Object.defineProperty(exports, "parseSetupArgs", { enumerable: true, get: function () { return setup_js_1.parseSetupArgs; } });
76
81
  Object.defineProperty(exports, "readJsonFile", { enumerable: true, get: function () { return setup_js_1.readJsonFile; } });
@@ -113,6 +118,14 @@ Object.defineProperty(exports, "stopDevJaeger", { enumerable: true, get: functio
113
118
  Object.defineProperty(exports, "DEV_JAEGER_CONTAINER", { enumerable: true, get: function () { return index_js_1.DEV_JAEGER_CONTAINER; } });
114
119
  var index_js_2 = require("./registry/index.js");
115
120
  Object.defineProperty(exports, "runRegistryCommand", { enumerable: true, get: function () { return index_js_2.runRegistryCommand; } });
121
+ var permissions_js_1 = require("./permissions.js");
122
+ Object.defineProperty(exports, "checkAndDecide", { enumerable: true, get: function () { return permissions_js_1.checkAndDecide; } });
123
+ Object.defineProperty(exports, "applyPermissionMode", { enumerable: true, get: function () { return permissions_js_1.applyPermissionMode; } });
124
+ var tool_catalog_js_1 = require("./tool-catalog.js");
125
+ Object.defineProperty(exports, "HARNESS_TOOL_CATALOG", { enumerable: true, get: function () { return tool_catalog_js_1.HARNESS_TOOL_CATALOG; } });
126
+ Object.defineProperty(exports, "KNOWN_HARNESSES", { enumerable: true, get: function () { return tool_catalog_js_1.KNOWN_HARNESSES; } });
127
+ Object.defineProperty(exports, "ALL_TOOLS", { enumerable: true, get: function () { return tool_catalog_js_1.ALL_TOOLS; } });
128
+ Object.defineProperty(exports, "getToolCatalog", { enumerable: true, get: function () { return tool_catalog_js_1.getToolCatalog; } });
116
129
  var mcp_js_1 = require("./mcp.js");
117
130
  Object.defineProperty(exports, "parseClaudeCodeMcpTool", { enumerable: true, get: function () { return mcp_js_1.parseClaudeCodeMcpTool; } });
118
131
  Object.defineProperty(exports, "parseGeminiMcpTool", { enumerable: true, get: function () { return mcp_js_1.parseGeminiMcpTool; } });
@@ -28,6 +28,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
28
28
  exports.USER_SUBJECT_NAMESPACE = void 0;
29
29
  exports.seedLocalEnvironment = seedLocalEnvironment;
30
30
  const auth_js_1 = require("../auth.js");
31
+ const tool_catalog_js_1 = require("../tool-catalog.js");
31
32
  const configs_js_1 = require("./configs.js");
32
33
  const KRATOS_ADMIN = `http://localhost:${configs_js_1.KRATOS_ADMIN_PORT}`;
33
34
  const KETO_WRITE = `http://localhost:${configs_js_1.KETO_WRITE_PORT}`;
@@ -39,29 +40,11 @@ const USER_PASSWORD = "ory-user-local-dev-password!";
39
40
  const USER_CLIENT_NAME = "ory-agent-plugins-local-user";
40
41
  const USER_CLIENT_ID = "ory-user-local";
41
42
  /**
42
- * Common tools that agents typically use. Permission tuples are seeded
43
- * so the user identity is allowed to use all of them.
43
+ * Tools the dev launcher seeds tuples for. Sourced from the shared
44
+ * per-harness catalog so a single seed grants the local user identity
45
+ * `use` on every harness's built-in tool set.
44
46
  */
45
- const COMMON_TOOLS = [
46
- "Read",
47
- "Write",
48
- "Edit",
49
- "Bash",
50
- "Glob",
51
- "Grep",
52
- "WebFetch",
53
- "WebSearch",
54
- "Agent",
55
- "NotebookEdit",
56
- "TodoRead",
57
- "TodoWrite",
58
- "execute_command",
59
- "read_file",
60
- "write_file",
61
- "list_directory",
62
- "search_files",
63
- "browser",
64
- ];
47
+ const COMMON_TOOLS = tool_catalog_js_1.ALL_TOOLS;
65
48
  async function jsonFetch(url, opts = {}) {
66
49
  const res = await fetch(url, {
67
50
  ...opts,
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared `permissions` CLI implementation. Each harness's `cli/main.ts`
3
+ * delegates to {@link runPermissionsCommand}, passing the harness's name
4
+ * so subcommands can scope to that harness's tool catalog.
5
+ *
6
+ * Subcommands:
7
+ *
8
+ * - `status` — Print the configured {@link PermissionMode} and, when
9
+ * the client can reach Ory, the allow/deny breakdown
10
+ * across the harness's known tool catalog.
11
+ * - `bootstrap` — Idempotently write `<namespace>:<tool>#use@<userSubject>`
12
+ * tuples for every tool in the harness's catalog. Run
13
+ * automatically by `install` when a user identity is
14
+ * cached; can be re-run by hand any time.
15
+ * - `observe` — Persist `permissionMode = "observe"` in the shared
16
+ * config (denies log but don't block).
17
+ * - `enforce` — Persist `permissionMode = "enforce"` in the shared
18
+ * config (denies block — the production posture).
19
+ *
20
+ * All four are non-destructive on existing config and idempotent on
21
+ * Keto: re-running them is always safe.
22
+ */
23
+ /**
24
+ * Entry point invoked by each harness CLI's `permissions` case.
25
+ *
26
+ * Returns the intended process exit code (0 on success, 1 on usage or
27
+ * config error). Never throws — surfaces failures via stderr + non-zero
28
+ * exit so the parent CLI doesn't need a try/catch.
29
+ */
30
+ export declare function runPermissionsCommand(binName: string, harness: string, args: string[]): Promise<number>;
31
+ /**
32
+ * Whether a user identity is cached locally — used by `install` to
33
+ * decide whether to opportunistically bootstrap tuples without
34
+ * prompting. Returns true when persisted tokens exist and are not
35
+ * expired, or when ORY_USER_SUBJECT_ID is set explicitly.
36
+ */
37
+ export declare function isUserIdentityCached(): boolean;
38
+ /**
39
+ * Print the "permission mode" section of the install banner.
40
+ *
41
+ * Tells the user what observe vs enforce mean, and prints the two
42
+ * follow-up commands they'll typically reach for next:
43
+ *
44
+ * - `<bin> permissions bootstrap` to seed tuples for the harness's
45
+ * built-in tools (the line is shown whether or not we just ran it
46
+ * ourselves, so the user can re-run after editing the catalog).
47
+ * - `<bin> permissions enforce` once they're satisfied with the
48
+ * observe-mode trace output and want denies to actually block.
49
+ */
50
+ export declare function printPermissionsOnboardingHelp(binName: string, harness: string, opts?: {
51
+ bootstrappedAutomatically?: boolean;
52
+ }): void;
53
+ /**
54
+ * If a user identity is already cached and the plugin is configured,
55
+ * opportunistically run `permissions bootstrap` so the user lands in a
56
+ * "tools work out of the box" state. Otherwise a no-op (the install
57
+ * banner still prints the manual command).
58
+ *
59
+ * Always best-effort: any failure is logged and swallowed so install
60
+ * never aborts because of a permissions write problem.
61
+ */
62
+ export declare function maybeAutoBootstrap(binName: string, harness: string): Promise<boolean>;
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ /**
3
+ * Shared `permissions` CLI implementation. Each harness's `cli/main.ts`
4
+ * delegates to {@link runPermissionsCommand}, passing the harness's name
5
+ * so subcommands can scope to that harness's tool catalog.
6
+ *
7
+ * Subcommands:
8
+ *
9
+ * - `status` — Print the configured {@link PermissionMode} and, when
10
+ * the client can reach Ory, the allow/deny breakdown
11
+ * across the harness's known tool catalog.
12
+ * - `bootstrap` — Idempotently write `<namespace>:<tool>#use@<userSubject>`
13
+ * tuples for every tool in the harness's catalog. Run
14
+ * automatically by `install` when a user identity is
15
+ * cached; can be re-run by hand any time.
16
+ * - `observe` — Persist `permissionMode = "observe"` in the shared
17
+ * config (denies log but don't block).
18
+ * - `enforce` — Persist `permissionMode = "enforce"` in the shared
19
+ * config (denies block — the production posture).
20
+ *
21
+ * All four are non-destructive on existing config and idempotent on
22
+ * Keto: re-running them is always safe.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.runPermissionsCommand = runPermissionsCommand;
26
+ exports.isUserIdentityCached = isUserIdentityCached;
27
+ exports.printPermissionsOnboardingHelp = printPermissionsOnboardingHelp;
28
+ exports.maybeAutoBootstrap = maybeAutoBootstrap;
29
+ const config_js_1 = require("./config.js");
30
+ const client_js_1 = require("./client.js");
31
+ const agent_auth_js_1 = require("./agent-auth.js");
32
+ const auth_store_js_1 = require("./auth-store.js");
33
+ const tool_catalog_js_1 = require("./tool-catalog.js");
34
+ const subject_js_1 = require("./subject.js");
35
+ const ENV_PERMISSION_MODE = "ORY_PERMISSION_MODE";
36
+ function resolveNamespace() {
37
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
38
+ }
39
+ /**
40
+ * Apply persisted user OAuth2 tokens (if any) to the client's user
41
+ * principal so downstream subject resolution and tuple writes see a
42
+ * concrete identity. Mirrors what `ensureUserAuthenticated` does on a
43
+ * cache hit — but works whether or not `ORY_AUTH_GATE` is enabled.
44
+ */
45
+ function attachCachedUserPrincipal(client) {
46
+ const tokens = (0, auth_store_js_1.loadTokens)();
47
+ if (!tokens || (0, auth_store_js_1.isExpired)(tokens))
48
+ return;
49
+ client.setUserPrincipal({
50
+ subject: tokens.subject,
51
+ token: tokens.accessToken,
52
+ });
53
+ }
54
+ /**
55
+ * Entry point invoked by each harness CLI's `permissions` case.
56
+ *
57
+ * Returns the intended process exit code (0 on success, 1 on usage or
58
+ * config error). Never throws — surfaces failures via stderr + non-zero
59
+ * exit so the parent CLI doesn't need a try/catch.
60
+ */
61
+ async function runPermissionsCommand(binName, harness, args) {
62
+ const sub = args[0];
63
+ if (!sub || sub === "--help" || sub === "-h") {
64
+ printPermissionsHelp(binName);
65
+ return sub ? 0 : 1;
66
+ }
67
+ switch (sub) {
68
+ case "status":
69
+ return await runPermissionsStatus(binName, harness);
70
+ case "bootstrap":
71
+ return await runPermissionsBootstrap(binName, harness, args.slice(1));
72
+ case "observe":
73
+ return runPermissionsSetMode(binName, "observe");
74
+ case "enforce":
75
+ return runPermissionsSetMode(binName, "enforce");
76
+ default:
77
+ console.error(`Unknown permissions subcommand: ${sub}`);
78
+ console.error(`Run "${binName} permissions --help" for usage.`);
79
+ return 1;
80
+ }
81
+ }
82
+ function printPermissionsHelp(binName) {
83
+ console.log(`Usage: ${binName} permissions <subcommand>`);
84
+ console.log("");
85
+ console.log("Subcommands:");
86
+ console.log(" status Show permission mode and tool coverage for the current user");
87
+ console.log(" bootstrap Write 'use' tuples for the harness's built-in tools (idempotent)");
88
+ console.log(" observe Switch to observe mode (denies log but don't block)");
89
+ console.log(" enforce Switch to enforce mode (denies block — production posture)");
90
+ console.log("");
91
+ console.log("Permission mode controls what happens when an Ory check returns deny:");
92
+ console.log(" observe — Log + audit-span, allow the tool through. The onboarding default.");
93
+ console.log(" enforce — Block the tool. The production posture.");
94
+ console.log("");
95
+ console.log(`Mode is read from ${ENV_PERMISSION_MODE} env, then the shared config file.`);
96
+ }
97
+ function runPermissionsSetMode(binName, mode) {
98
+ (0, config_js_1.saveConfig)({ permissionMode: mode });
99
+ const configPath = (0, config_js_1.getConfigPath)();
100
+ console.log(`Permission mode set to '${mode}'.`);
101
+ console.log(` Saved to: ${configPath}`);
102
+ console.log("");
103
+ if (mode === "observe") {
104
+ console.log("Denies will be logged and recorded as `permission.observe_deny` spans,");
105
+ console.log("but tools will be allowed through. Run");
106
+ console.log(` ${binName} permissions enforce`);
107
+ console.log("once your tuple set is correct.");
108
+ }
109
+ else {
110
+ console.log("Denies will block tool execution. The plugin is now enforcing.");
111
+ console.log(`Switch back with: ${binName} permissions observe`);
112
+ }
113
+ console.log("");
114
+ console.log(`Note: the ${ENV_PERMISSION_MODE} environment variable, when set, overrides this.`);
115
+ return 0;
116
+ }
117
+ async function runPermissionsStatus(binName, harness) {
118
+ const resolved = (0, config_js_1.resolveConfig)();
119
+ const namespace = resolveNamespace();
120
+ const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
121
+ console.log(`Permission status (${harness})`);
122
+ console.log("==========================================");
123
+ console.log("");
124
+ console.log(`Mode: ${resolved.permissionMode} [source: ${resolved.permissionModeSource}]`);
125
+ console.log(`Namespace: ${namespace}`);
126
+ console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"} [source: ${resolved.projectUrlSource}]`);
127
+ console.log(`Audit-only: ${resolved.auditOnly ? "yes (Ory disabled)" : "no"}`);
128
+ console.log("");
129
+ if (resolved.auditOnly) {
130
+ console.log("Ory is disabled (audit-only). No permission checks run.");
131
+ console.log(`Re-enable with: ${binName} configure --project-url <URL> --api-key <KEY>`);
132
+ return 0;
133
+ }
134
+ if (!resolved.projectUrl) {
135
+ console.log("No project URL configured — cannot probe tuple coverage.");
136
+ console.log(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
137
+ return 0;
138
+ }
139
+ if (catalog.length === 0) {
140
+ console.log(`Tool catalog: (none known for harness "${harness}")`);
141
+ console.log(`Known harnesses with a built-in catalog: ${tool_catalog_js_1.KNOWN_HARNESSES.join(", ")}`);
142
+ return 0;
143
+ }
144
+ const client = client_js_1.OryAgentClient.fromEnv(harness);
145
+ attachCachedUserPrincipal(client);
146
+ await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
147
+ /* best-effort; status keeps working in pass-through */
148
+ });
149
+ const subject = (0, subject_js_1.resolveUserSubject)(client);
150
+ const subjectId = (0, subject_js_1.subjectLabel)(subject);
151
+ if (subjectId === "agent:unknown") {
152
+ console.log("No user identity resolved.");
153
+ console.log("Run the harness once (so the auth gate caches a token) or set");
154
+ console.log("ORY_USER_SUBJECT_ID to probe against a known subject.");
155
+ return 0;
156
+ }
157
+ console.log(`Subject: ${subjectId}`);
158
+ console.log(`Tool catalog: ${catalog.length} tools`);
159
+ console.log("");
160
+ const results = await probeCatalog(client, catalog, namespace, subject);
161
+ const allowed = results.filter((r) => r.allowed).length;
162
+ const denied = results.filter((r) => r.error === undefined && !r.allowed).length;
163
+ const errored = results.filter((r) => r.error !== undefined).length;
164
+ const widest = Math.max(...catalog.map((t) => t.length), 4);
165
+ console.log(` ${"Tool".padEnd(widest)} Status`);
166
+ console.log(` ${"-".repeat(widest)} ------`);
167
+ for (const r of results) {
168
+ const status = r.error
169
+ ? `error (${r.error})`
170
+ : r.allowed
171
+ ? "allowed"
172
+ : "denied";
173
+ console.log(` ${r.tool.padEnd(widest)} ${status}`);
174
+ }
175
+ console.log("");
176
+ console.log(`Summary: ${allowed} allowed, ${denied} denied, ${errored} errored.`);
177
+ if (denied > 0) {
178
+ console.log("");
179
+ console.log(`Run "${binName} permissions bootstrap" to grant the current user`);
180
+ console.log("'use' on every tool in this harness's catalog (idempotent).");
181
+ }
182
+ return 0;
183
+ }
184
+ async function probeCatalog(client, catalog, namespace, subject) {
185
+ const rows = [];
186
+ for (const tool of catalog) {
187
+ const check = {
188
+ namespace,
189
+ object: tool,
190
+ relation: "use",
191
+ ...subject,
192
+ };
193
+ try {
194
+ const result = await client.checkPermission(check, {
195
+ spanAttributes: { toolName: tool, source: "permissions_status" },
196
+ });
197
+ rows.push({ tool, allowed: result.allowed });
198
+ }
199
+ catch (err) {
200
+ const ory = err;
201
+ rows.push({ tool, allowed: false, error: ory.code ?? "unknown" });
202
+ }
203
+ }
204
+ return rows;
205
+ }
206
+ async function runPermissionsBootstrap(binName, harness, args) {
207
+ const dryRun = args.includes("--dry-run") || args.includes("-n");
208
+ const resolved = (0, config_js_1.resolveConfig)();
209
+ const namespace = resolveNamespace();
210
+ const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
211
+ if (resolved.auditOnly) {
212
+ console.error("Ory is in audit-only mode (kill-switch). Nothing to bootstrap.");
213
+ console.error(`Re-enable Ory: ${binName} configure --project-url <URL> --api-key <KEY>`);
214
+ return 1;
215
+ }
216
+ if (!resolved.projectUrl) {
217
+ console.error("No ORY_PROJECT_URL configured — cannot write tuples.");
218
+ console.error(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
219
+ return 1;
220
+ }
221
+ if (catalog.length === 0) {
222
+ console.error(`No tool catalog known for harness "${harness}".`);
223
+ console.error(`Known harnesses: ${tool_catalog_js_1.KNOWN_HARNESSES.join(", ")}`);
224
+ return 1;
225
+ }
226
+ const client = client_js_1.OryAgentClient.fromEnv(harness);
227
+ attachCachedUserPrincipal(client);
228
+ await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
229
+ /* best-effort; the write may still succeed with the API key or no auth (local Keto) */
230
+ });
231
+ const subject = (0, subject_js_1.resolveUserSubject)(client);
232
+ const subjectId = (0, subject_js_1.subjectLabel)(subject);
233
+ if (subjectId === "agent:unknown") {
234
+ console.error("No user identity resolved — refusing to write tuples to an unknown subject.");
235
+ console.error("");
236
+ console.error("Either:");
237
+ console.error(" - run the harness once with ORY_AUTH_GATE=1 so a user token is cached, or");
238
+ console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
239
+ return 1;
240
+ }
241
+ console.log(`Bootstrapping ${catalog.length} permission tuples for ${harness}`);
242
+ console.log("==========================================");
243
+ console.log("");
244
+ console.log(`Namespace: ${namespace}`);
245
+ console.log(`Subject: ${subjectId}`);
246
+ console.log(`Tools: ${catalog.join(", ")}`);
247
+ if (dryRun) {
248
+ console.log("");
249
+ console.log("(--dry-run) No tuples will be written.");
250
+ }
251
+ console.log("");
252
+ let created = 0;
253
+ let existed = 0;
254
+ const failures = [];
255
+ for (const tool of catalog) {
256
+ const check = {
257
+ namespace,
258
+ object: tool,
259
+ relation: "use",
260
+ ...subject,
261
+ };
262
+ if (dryRun) {
263
+ console.log(` ~ would write: ${namespace}:${tool}#use@${subjectId}`);
264
+ continue;
265
+ }
266
+ try {
267
+ const res = await client.createRelationship(check, {
268
+ spanAttributes: { toolName: tool, source: "permissions_bootstrap" },
269
+ });
270
+ if (res.alreadyExisted) {
271
+ existed++;
272
+ console.log(` = ${tool} (already exists)`);
273
+ }
274
+ else {
275
+ created++;
276
+ console.log(` + ${tool} (created)`);
277
+ }
278
+ }
279
+ catch (err) {
280
+ const ory = err;
281
+ const reason = `${ory.code ?? "unknown"}${ory.status ? ` (HTTP ${ory.status})` : ""}`;
282
+ failures.push({ tool, reason });
283
+ console.log(` ! ${tool} (failed: ${reason})`);
284
+ }
285
+ }
286
+ console.log("");
287
+ if (dryRun) {
288
+ console.log(`Dry run: ${catalog.length} tuples would be written.`);
289
+ return 0;
290
+ }
291
+ console.log(`Created: ${created} Already existed: ${existed} Failed: ${failures.length}`);
292
+ if (failures.length > 0) {
293
+ const writeAuthFailure = failures.some((f) => /(unknown|forbidden|session_inactive)/.test(f.reason) ||
294
+ /401|403/.test(f.reason));
295
+ if (writeAuthFailure) {
296
+ console.log("");
297
+ console.log("Some writes were rejected — your current credentials may lack");
298
+ console.log("write scope on the permission namespace. You can apply the");
299
+ console.log("tuples manually using the snippet below (one tuple per line):");
300
+ console.log("");
301
+ for (const tool of catalog) {
302
+ console.log(` ${namespace}:${tool}#use@${subjectId}`);
303
+ }
304
+ console.log("");
305
+ console.log("Or use the Ory Console: open the project, go to Permissions →");
306
+ console.log(`Relationships, set namespace='${namespace}', object=<tool>, relation='use',`);
307
+ console.log(`and subject='${subjectId}'.`);
308
+ }
309
+ return failures.length === catalog.length ? 1 : 0;
310
+ }
311
+ console.log("");
312
+ console.log(`Permission tuples are in place. Promote to enforce mode when ready:`);
313
+ console.log(` ${binName} permissions enforce`);
314
+ return 0;
315
+ }
316
+ /**
317
+ * Whether a user identity is cached locally — used by `install` to
318
+ * decide whether to opportunistically bootstrap tuples without
319
+ * prompting. Returns true when persisted tokens exist and are not
320
+ * expired, or when ORY_USER_SUBJECT_ID is set explicitly.
321
+ */
322
+ function isUserIdentityCached() {
323
+ if (process.env.ORY_USER_SUBJECT_ID)
324
+ return true;
325
+ const tokens = (0, auth_store_js_1.loadTokens)();
326
+ return !!tokens && !(0, auth_store_js_1.isExpired)(tokens);
327
+ }
328
+ /**
329
+ * Print the "permission mode" section of the install banner.
330
+ *
331
+ * Tells the user what observe vs enforce mean, and prints the two
332
+ * follow-up commands they'll typically reach for next:
333
+ *
334
+ * - `<bin> permissions bootstrap` to seed tuples for the harness's
335
+ * built-in tools (the line is shown whether or not we just ran it
336
+ * ourselves, so the user can re-run after editing the catalog).
337
+ * - `<bin> permissions enforce` once they're satisfied with the
338
+ * observe-mode trace output and want denies to actually block.
339
+ */
340
+ function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
341
+ const resolved = (0, config_js_1.resolveConfig)();
342
+ const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
343
+ console.log("");
344
+ console.log("Permissions:");
345
+ console.log(` Mode: ${resolved.permissionMode} [source: ${resolved.permissionModeSource}]`);
346
+ if (resolved.auditOnly) {
347
+ console.log(" Ory is in audit-only mode (kill switch). Permission checks are disabled.");
348
+ return;
349
+ }
350
+ if (resolved.permissionMode === "observe") {
351
+ console.log(" Observe mode: permission denies are logged + traced, but tools still run.");
352
+ console.log(" This is the safe default — promote once your tuple set is correct.");
353
+ }
354
+ else {
355
+ console.log(" Enforce mode: permission denies block tool execution.");
356
+ }
357
+ if (opts.bootstrappedAutomatically) {
358
+ console.log("");
359
+ console.log(` Tuples bootstrapped for ${catalog.length} built-in tools.`);
360
+ }
361
+ else if (catalog.length > 0) {
362
+ console.log("");
363
+ console.log(" Grant the current user 'use' on every built-in tool (idempotent):");
364
+ console.log(` npx ${binName} permissions bootstrap`);
365
+ }
366
+ console.log("");
367
+ console.log(" Inspect tuple coverage:");
368
+ console.log(` npx ${binName} permissions status`);
369
+ console.log("");
370
+ console.log(" Promote to enforcing once observe-mode logs look right:");
371
+ console.log(` npx ${binName} permissions enforce`);
372
+ }
373
+ /**
374
+ * If a user identity is already cached and the plugin is configured,
375
+ * opportunistically run `permissions bootstrap` so the user lands in a
376
+ * "tools work out of the box" state. Otherwise a no-op (the install
377
+ * banner still prints the manual command).
378
+ *
379
+ * Always best-effort: any failure is logged and swallowed so install
380
+ * never aborts because of a permissions write problem.
381
+ */
382
+ async function maybeAutoBootstrap(binName, harness) {
383
+ const resolved = (0, config_js_1.resolveConfig)();
384
+ if (resolved.auditOnly)
385
+ return false;
386
+ if (!resolved.projectUrl)
387
+ return false;
388
+ if (!isUserIdentityCached())
389
+ return false;
390
+ if ((0, tool_catalog_js_1.getToolCatalog)(harness).length === 0)
391
+ return false;
392
+ try {
393
+ console.log("");
394
+ console.log("Bootstrapping permission tuples (cached user identity detected)...");
395
+ const code = await runPermissionsCommand(binName, harness, ["bootstrap"]);
396
+ return code === 0;
397
+ }
398
+ catch (err) {
399
+ const msg = err instanceof Error ? err.message : String(err);
400
+ console.warn(`Bootstrap skipped: ${msg}`);
401
+ return false;
402
+ }
403
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Shared permission-check helper that applies the configured
3
+ * {@link PermissionMode} so plugins don't each re-implement the
4
+ * observe-vs-enforce branch.
5
+ *
6
+ * Plugins should call this in their pre-tool-use handler instead of
7
+ * `client.checkPermission` directly. The helper:
8
+ *
9
+ * 1. Runs the check.
10
+ * 2. On a Keto deny (`result.allowed === false`), applies the mode:
11
+ * - `observe` — record a `permission.observe_deny` audit span,
12
+ * return `{ kind: "observe", … }` so the caller allows the
13
+ * tool through.
14
+ * - `enforce` — return `{ kind: "deny", … }` so the caller blocks.
15
+ * 3. On a thrown {@link OryError} (network_error, rate_limited, etc.),
16
+ * returns `{ kind: "fail_open", … }`. Callers should log and allow.
17
+ *
18
+ * The discriminated `kind` lets plugins map cleanly to their native
19
+ * decision shape (claude-code exit codes, openclaw `{ block: true }`,
20
+ * opencode `throw OryDenialError`, etc.) without re-implementing the
21
+ * mode logic.
22
+ */
23
+ import type { OryAgentClient } from "./client.js";
24
+ import { type PermissionMode } from "./config.js";
25
+ import type { OryError, PermissionCheck, PermissionResult } from "./types.js";
26
+ export type PermissionDecision = {
27
+ kind: "allow";
28
+ result: PermissionResult;
29
+ mode: PermissionMode;
30
+ } | {
31
+ kind: "deny";
32
+ result: PermissionResult;
33
+ mode: "enforce";
34
+ } | {
35
+ kind: "observe";
36
+ result: PermissionResult;
37
+ mode: "observe";
38
+ } | {
39
+ kind: "fail_open";
40
+ error: OryError;
41
+ mode: PermissionMode;
42
+ };
43
+ /**
44
+ * The "what should the caller do?" half of a decision, independent of
45
+ * which kind of check produced the underlying `allowed` boolean. Used
46
+ * by both the regular check path (via {@link checkAndDecide}) and the
47
+ * MCP path (via {@link applyPermissionMode} on the MCP result).
48
+ */
49
+ export type ModeDecision = {
50
+ kind: "allow";
51
+ mode: PermissionMode;
52
+ } | {
53
+ kind: "deny";
54
+ mode: "enforce";
55
+ } | {
56
+ kind: "observe";
57
+ mode: "observe";
58
+ };
59
+ export interface CheckAndDecideOptions {
60
+ /**
61
+ * Span attributes merged into the underlying `permission.check` span
62
+ * and into the synthetic `permission.observe_deny` span when emitted.
63
+ * Same shape and semantics as `client.checkPermission`'s option.
64
+ */
65
+ spanAttributes?: Record<string, unknown>;
66
+ /**
67
+ * Override the resolved {@link PermissionMode}. Tests use this to
68
+ * exercise both branches without mutating env or config state.
69
+ */
70
+ modeOverride?: PermissionMode;
71
+ }
72
+ export interface ApplyPermissionModeContext {
73
+ /**
74
+ * Namespace / object / relation that produced the `allowed` boolean.
75
+ * Logged on observe-deny and attached to the audit span. All optional
76
+ * — the helper still works without them, but populated values make
77
+ * the audit trail searchable.
78
+ */
79
+ namespace?: string;
80
+ object?: string;
81
+ relation?: string;
82
+ /** Subject ID for the observe-deny log line, if available. */
83
+ subjectId?: string;
84
+ /** Attributes merged into the `permission.observe_deny` span. */
85
+ spanAttributes?: Record<string, unknown>;
86
+ /** Override the resolved mode (tests). */
87
+ modeOverride?: PermissionMode;
88
+ }
89
+ /**
90
+ * Map an `allowed` boolean from any permission check (plain or MCP)
91
+ * onto a {@link ModeDecision}. When `allowed === false` and the mode
92
+ * is `observe`, emits the `permission.observe_deny` audit span and
93
+ * returns `{ kind: "observe" }` so the caller knows to let the action
94
+ * proceed despite the deny.
95
+ */
96
+ export declare function applyPermissionMode(client: OryAgentClient, allowed: boolean, context?: ApplyPermissionModeContext): ModeDecision;
97
+ /**
98
+ * Run a permission check and resolve the configured mode against the
99
+ * result. Never throws — fail-open scenarios are surfaced as a typed
100
+ * `{ kind: "fail_open" }` decision.
101
+ */
102
+ export declare function checkAndDecide(client: OryAgentClient, check: PermissionCheck, opts?: CheckAndDecideOptions): Promise<PermissionDecision>;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * Shared permission-check helper that applies the configured
4
+ * {@link PermissionMode} so plugins don't each re-implement the
5
+ * observe-vs-enforce branch.
6
+ *
7
+ * Plugins should call this in their pre-tool-use handler instead of
8
+ * `client.checkPermission` directly. The helper:
9
+ *
10
+ * 1. Runs the check.
11
+ * 2. On a Keto deny (`result.allowed === false`), applies the mode:
12
+ * - `observe` — record a `permission.observe_deny` audit span,
13
+ * return `{ kind: "observe", … }` so the caller allows the
14
+ * tool through.
15
+ * - `enforce` — return `{ kind: "deny", … }` so the caller blocks.
16
+ * 3. On a thrown {@link OryError} (network_error, rate_limited, etc.),
17
+ * returns `{ kind: "fail_open", … }`. Callers should log and allow.
18
+ *
19
+ * The discriminated `kind` lets plugins map cleanly to their native
20
+ * decision shape (claude-code exit codes, openclaw `{ block: true }`,
21
+ * opencode `throw OryDenialError`, etc.) without re-implementing the
22
+ * mode logic.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.applyPermissionMode = applyPermissionMode;
26
+ exports.checkAndDecide = checkAndDecide;
27
+ const config_js_1 = require("./config.js");
28
+ /**
29
+ * Map an `allowed` boolean from any permission check (plain or MCP)
30
+ * onto a {@link ModeDecision}. When `allowed === false` and the mode
31
+ * is `observe`, emits the `permission.observe_deny` audit span and
32
+ * returns `{ kind: "observe" }` so the caller knows to let the action
33
+ * proceed despite the deny.
34
+ */
35
+ function applyPermissionMode(client, allowed, context = {}) {
36
+ const mode = context.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
37
+ if (allowed)
38
+ return { kind: "allow", mode };
39
+ if (mode === "observe") {
40
+ client.logger.warn("permission.observe_deny", {
41
+ namespace: context.namespace,
42
+ object: context.object,
43
+ relation: context.relation,
44
+ subjectId: context.subjectId,
45
+ note: "denied by Ory; allowed by plugin in observe mode",
46
+ });
47
+ client.tracer.record("permission.observe_deny", "denied", {
48
+ attributes: {
49
+ namespace: context.namespace,
50
+ object: context.object,
51
+ relation: context.relation,
52
+ mode,
53
+ ...context.spanAttributes,
54
+ },
55
+ });
56
+ return { kind: "observe", mode: "observe" };
57
+ }
58
+ return { kind: "deny", mode: "enforce" };
59
+ }
60
+ /**
61
+ * Run a permission check and resolve the configured mode against the
62
+ * result. Never throws — fail-open scenarios are surfaced as a typed
63
+ * `{ kind: "fail_open" }` decision.
64
+ */
65
+ async function checkAndDecide(client, check, opts = {}) {
66
+ const mode = opts.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
67
+ let result;
68
+ try {
69
+ result = await client.checkPermission(check, {
70
+ spanAttributes: opts.spanAttributes,
71
+ });
72
+ }
73
+ catch (err) {
74
+ return { kind: "fail_open", error: err, mode };
75
+ }
76
+ const inner = applyPermissionMode(client, result.allowed, {
77
+ namespace: check.namespace,
78
+ object: check.object,
79
+ relation: check.relation,
80
+ subjectId: check.subjectId,
81
+ spanAttributes: opts.spanAttributes,
82
+ modeOverride: opts.modeOverride,
83
+ });
84
+ if (inner.kind === "allow")
85
+ return { kind: "allow", result, mode: inner.mode };
86
+ if (inner.kind === "observe")
87
+ return { kind: "observe", result, mode: "observe" };
88
+ return { kind: "deny", result, mode: "enforce" };
89
+ }
package/dist/skills.js CHANGED
@@ -65,6 +65,11 @@ const SKILL_SOURCES = [
65
65
  { id: "login-flow", name: "ory-login-flow", file: "skills/login-flow/SKILL.md" },
66
66
  { id: "social-login", name: "ory-social-login", file: "skills/social-login/SKILL.md" },
67
67
  { id: "local-dev", name: "ory-local-dev", file: "skills/local-dev/SKILL.md" },
68
+ {
69
+ id: "permissions-onboarding",
70
+ name: "ory-permissions-onboarding",
71
+ file: "skills/permissions-onboarding/SKILL.md",
72
+ },
68
73
  ];
69
74
  const COMMAND_SOURCES = [
70
75
  {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Canonical per-harness tool catalog used by `permissions bootstrap` to
3
+ * seed Keto tuples granting the current user `use` on each tool.
4
+ *
5
+ * These lists cover each harness's built-in tools — the names the harness
6
+ * passes to the pre-tool-use hook. MCP server tools are intentionally
7
+ * omitted because they're discovered dynamically per session and can't be
8
+ * enumerated at bootstrap time.
9
+ *
10
+ * The lists are best-effort: harness vendors add and rename tools over
11
+ * time. `bootstrap` is idempotent and additive, so re-running it after a
12
+ * catalog update is safe.
13
+ */
14
+ /**
15
+ * Built-in tool names known to ship with each supported harness.
16
+ */
17
+ export declare const HARNESS_TOOL_CATALOG: {
18
+ readonly "claude-code": readonly ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "TodoWrite", "Task"];
19
+ readonly codex: readonly ["shell", "apply_patch", "read_file"];
20
+ readonly "gemini-cli": readonly ["read_file", "write_file", "edit_file", "list_files", "search_files", "shell", "web_search"];
21
+ readonly openclaw: readonly ["execute_command", "read_file", "write_file", "list_directory", "search_files", "browser"];
22
+ readonly opencode: readonly ["read", "write", "edit", "bash", "glob", "grep", "webfetch"];
23
+ };
24
+ /** Names of the harnesses with a known tool catalog. */
25
+ export type KnownHarness = keyof typeof HARNESS_TOOL_CATALOG;
26
+ export declare const KNOWN_HARNESSES: readonly KnownHarness[];
27
+ /**
28
+ * Tools for a specific harness. Returns the canonical list when known;
29
+ * empty array for unknown harnesses (callers can fall back to {@link
30
+ * ALL_TOOLS} or skip bootstrap with a warning).
31
+ */
32
+ export declare function getToolCatalog(harness: string): readonly string[];
33
+ /**
34
+ * Union of every known tool across all harnesses, deduplicated. Used by
35
+ * the dev launcher's seed routine, which grants the local user identity
36
+ * broad access so the same launcher can run every harness end-to-end.
37
+ */
38
+ export declare const ALL_TOOLS: readonly string[];
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * Canonical per-harness tool catalog used by `permissions bootstrap` to
4
+ * seed Keto tuples granting the current user `use` on each tool.
5
+ *
6
+ * These lists cover each harness's built-in tools — the names the harness
7
+ * passes to the pre-tool-use hook. MCP server tools are intentionally
8
+ * omitted because they're discovered dynamically per session and can't be
9
+ * enumerated at bootstrap time.
10
+ *
11
+ * The lists are best-effort: harness vendors add and rename tools over
12
+ * time. `bootstrap` is idempotent and additive, so re-running it after a
13
+ * catalog update is safe.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = void 0;
17
+ exports.getToolCatalog = getToolCatalog;
18
+ /**
19
+ * Built-in tool names known to ship with each supported harness.
20
+ */
21
+ exports.HARNESS_TOOL_CATALOG = {
22
+ "claude-code": [
23
+ "Read",
24
+ "Write",
25
+ "Edit",
26
+ "Bash",
27
+ "Glob",
28
+ "Grep",
29
+ "WebFetch",
30
+ "WebSearch",
31
+ "NotebookEdit",
32
+ "TodoWrite",
33
+ "Task",
34
+ ],
35
+ codex: ["shell", "apply_patch", "read_file"],
36
+ "gemini-cli": [
37
+ "read_file",
38
+ "write_file",
39
+ "edit_file",
40
+ "list_files",
41
+ "search_files",
42
+ "shell",
43
+ "web_search",
44
+ ],
45
+ openclaw: [
46
+ "execute_command",
47
+ "read_file",
48
+ "write_file",
49
+ "list_directory",
50
+ "search_files",
51
+ "browser",
52
+ ],
53
+ opencode: ["read", "write", "edit", "bash", "glob", "grep", "webfetch"],
54
+ };
55
+ exports.KNOWN_HARNESSES = Object.keys(exports.HARNESS_TOOL_CATALOG);
56
+ /**
57
+ * Tools for a specific harness. Returns the canonical list when known;
58
+ * empty array for unknown harnesses (callers can fall back to {@link
59
+ * ALL_TOOLS} or skip bootstrap with a warning).
60
+ */
61
+ function getToolCatalog(harness) {
62
+ return (exports.HARNESS_TOOL_CATALOG[harness] ?? []);
63
+ }
64
+ /**
65
+ * Union of every known tool across all harnesses, deduplicated. Used by
66
+ * the dev launcher's seed routine, which grants the local user identity
67
+ * broad access so the same launcher can run every harness end-to-end.
68
+ */
69
+ exports.ALL_TOOLS = Array.from(new Set(Object.values(exports.HARNESS_TOOL_CATALOG).flat()));
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" | "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" | "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.1.3",
3
+ "version": "0.2.0",
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",