@ory/argus 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/permissions-onboarding/SKILL.md +166 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.js +13 -0
- package/dist/dev.js +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +15 -2
- package/dist/local/cli.js +1 -1
- package/dist/local/manager.js +1 -1
- package/dist/local/seed.js +8 -25
- package/dist/permissions-cli.d.ts +62 -0
- package/dist/permissions-cli.js +445 -0
- package/dist/permissions.d.ts +102 -0
- package/dist/permissions.js +89 -0
- package/dist/skills.js +5 -0
- package/dist/tool-catalog.d.ts +38 -0
- package/dist/tool-catalog.js +69 -0
- package/dist/tracer.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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/dev.js
CHANGED
|
@@ -485,7 +485,7 @@ async function bootstrapLocalOry(localDir) {
|
|
|
485
485
|
return { active: false };
|
|
486
486
|
}
|
|
487
487
|
console.log(`[ory-dev] Local Ory: agent ${seed.agent.identity.email}, user ${seed.user.identity.email}, ` +
|
|
488
|
-
`${seed.permissions.tuples}
|
|
488
|
+
`${seed.permissions.tuples} permissions in '${namespace}' for ${seed.permissions.subject}.`);
|
|
489
489
|
return { active: true, gatewayUrl, seed };
|
|
490
490
|
}
|
|
491
491
|
/**
|
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.
|
|
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; } });
|
package/dist/local/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ Commands:
|
|
|
63
63
|
up [--no-seed] Start local Ory services (Kratos, Keto, Hydra, gateway)
|
|
64
64
|
down Stop all local services (preserves data)
|
|
65
65
|
status Show service health and container states
|
|
66
|
-
seed Create test identity, session, and
|
|
66
|
+
seed Create test identity, session, and permissions
|
|
67
67
|
logs [service] [-f] View service logs (optionally follow, optionally filter)
|
|
68
68
|
reset Stop services and remove all data
|
|
69
69
|
env Print environment variables for connecting to local services
|
package/dist/local/manager.js
CHANGED
|
@@ -642,7 +642,7 @@ function printSeedResult(result) {
|
|
|
642
642
|
console.log(` User password: ${result.user.password}`);
|
|
643
643
|
console.log(` User OAuth2: ${result.user.client.clientId} (PKCE)`);
|
|
644
644
|
console.log("");
|
|
645
|
-
console.log(` Permissions: ${result.permissions.tuples}
|
|
645
|
+
console.log(` Permissions: ${result.permissions.tuples} entries in '${result.permissions.namespace}' for ${result.permissions.subject}`);
|
|
646
646
|
console.log("");
|
|
647
647
|
console.log("To use with any Ory agent plugin, set these environment variables:");
|
|
648
648
|
console.log("");
|
package/dist/local/seed.js
CHANGED
|
@@ -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
|
-
*
|
|
43
|
-
*
|
|
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,
|
|
@@ -127,7 +110,7 @@ async function seedPermissions(userId, namespace) {
|
|
|
127
110
|
catch (err) {
|
|
128
111
|
const msg = err instanceof Error ? err.message : String(err);
|
|
129
112
|
if (!msg.includes("409")) {
|
|
130
|
-
process.stderr.write(` Warning: failed to create
|
|
113
|
+
process.stderr.write(` Warning: failed to create permission for ${tool}: ${msg}\n`);
|
|
131
114
|
}
|
|
132
115
|
else {
|
|
133
116
|
created++;
|
|
@@ -202,9 +185,9 @@ async function seedLocalEnvironment(namespace = "AgentTools") {
|
|
|
202
185
|
// Subject is a SubjectSet `User:<id>` so the Console UI can rewrite
|
|
203
186
|
// the same tuple shape by hand during the runbook demo.
|
|
204
187
|
const subjectLabel = `${exports.USER_SUBJECT_NAMESPACE}:${userIdentity.id}`;
|
|
205
|
-
process.stderr.write(` Creating
|
|
188
|
+
process.stderr.write(` Creating permissions for ${COMMON_TOOLS.length} tools...\n`);
|
|
206
189
|
const tupleCount = await seedPermissions(userIdentity.id, namespace);
|
|
207
|
-
process.stderr.write(` Permissions: ${tupleCount}
|
|
190
|
+
process.stderr.write(` Permissions: ${tupleCount} entries in '${namespace}' for ${subjectLabel}\n`);
|
|
208
191
|
// 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
|
|
209
192
|
// PKCE flow has somewhere to authenticate against. The agent's
|
|
210
193
|
// OAuth2 client is intentionally NOT pre-registered; the harness
|
|
@@ -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,445 @@
|
|
|
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 Grant the current user 'use' on every built-in tool (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 permission 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}${formatModeSource(resolved.permissionModeSource)}`);
|
|
125
|
+
console.log(` (other modes: ${otherModes(resolved.permissionMode).join(", ")})`);
|
|
126
|
+
console.log(`Namespace: ${namespace}`);
|
|
127
|
+
console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"}${formatSourceSuffix(resolved.projectUrlSource)}`);
|
|
128
|
+
console.log(`Audit-only: ${resolved.auditOnly ? "yes (Ory disabled)" : "no"}`);
|
|
129
|
+
console.log("");
|
|
130
|
+
if (resolved.auditOnly) {
|
|
131
|
+
console.log("Ory is disabled (audit-only). No permission checks run.");
|
|
132
|
+
console.log(`Re-enable with: ${binName} configure --project-url <URL> --api-key <KEY>`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
if (!resolved.projectUrl) {
|
|
136
|
+
console.log("No project URL configured — cannot probe permission coverage.");
|
|
137
|
+
console.log(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
if (catalog.length === 0) {
|
|
141
|
+
console.log(`Tool catalog: (none known for harness "${harness}")`);
|
|
142
|
+
console.log(`Known harnesses with a built-in catalog: ${tool_catalog_js_1.KNOWN_HARNESSES.join(", ")}`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
const client = client_js_1.OryAgentClient.fromEnv(harness);
|
|
146
|
+
attachCachedUserPrincipal(client);
|
|
147
|
+
await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
|
|
148
|
+
/* best-effort; status keeps working in pass-through */
|
|
149
|
+
});
|
|
150
|
+
const subject = (0, subject_js_1.resolveUserSubject)(client);
|
|
151
|
+
const subjectId = (0, subject_js_1.subjectLabel)(subject);
|
|
152
|
+
if (subjectId === "agent:unknown") {
|
|
153
|
+
console.log("No user identity resolved.");
|
|
154
|
+
console.log("Run the harness once (so the auth gate caches a token) or set");
|
|
155
|
+
console.log("ORY_USER_SUBJECT_ID to probe against a known subject.");
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
console.log(`Subject: ${subjectId}`);
|
|
159
|
+
console.log(`Tool catalog: ${catalog.length} tools`);
|
|
160
|
+
console.log("");
|
|
161
|
+
const results = await probeCatalog(client, catalog, namespace, subject);
|
|
162
|
+
const allowed = results.filter((r) => r.allowed).length;
|
|
163
|
+
const denied = results.filter((r) => r.error === undefined && !r.allowed).length;
|
|
164
|
+
const errored = results.filter((r) => r.error !== undefined).length;
|
|
165
|
+
const widest = Math.max(...catalog.map((t) => t.length), 4);
|
|
166
|
+
console.log(` ${"Tool".padEnd(widest)} Status`);
|
|
167
|
+
console.log(` ${"-".repeat(widest)} ------`);
|
|
168
|
+
for (const r of results) {
|
|
169
|
+
const status = r.error
|
|
170
|
+
? `error (${r.error})`
|
|
171
|
+
: r.allowed
|
|
172
|
+
? "allowed"
|
|
173
|
+
: "denied";
|
|
174
|
+
console.log(` ${r.tool.padEnd(widest)} ${status}`);
|
|
175
|
+
}
|
|
176
|
+
console.log("");
|
|
177
|
+
console.log(`Summary: ${allowed} allowed, ${denied} denied, ${errored} errored.`);
|
|
178
|
+
if (denied > 0) {
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(`Run "${binName} permissions bootstrap" to grant the current user`);
|
|
181
|
+
console.log("'use' on every tool in this harness's catalog (idempotent).");
|
|
182
|
+
}
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
async function probeCatalog(client, catalog, namespace, subject) {
|
|
186
|
+
const rows = [];
|
|
187
|
+
for (const tool of catalog) {
|
|
188
|
+
const check = {
|
|
189
|
+
namespace,
|
|
190
|
+
object: tool,
|
|
191
|
+
relation: "use",
|
|
192
|
+
...subject,
|
|
193
|
+
};
|
|
194
|
+
try {
|
|
195
|
+
const result = await client.checkPermission(check, {
|
|
196
|
+
spanAttributes: { toolName: tool, source: "permissions_status" },
|
|
197
|
+
});
|
|
198
|
+
rows.push({ tool, allowed: result.allowed });
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
const ory = err;
|
|
202
|
+
rows.push({ tool, allowed: false, error: ory.code ?? "unknown" });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return rows;
|
|
206
|
+
}
|
|
207
|
+
async function runPermissionsBootstrap(binName, harness, args) {
|
|
208
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
209
|
+
const resolved = (0, config_js_1.resolveConfig)();
|
|
210
|
+
const namespace = resolveNamespace();
|
|
211
|
+
const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
|
|
212
|
+
if (resolved.auditOnly) {
|
|
213
|
+
console.error("Ory is in audit-only mode (kill-switch). Nothing to bootstrap.");
|
|
214
|
+
console.error(`Re-enable Ory: ${binName} configure --project-url <URL> --api-key <KEY>`);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
if (!resolved.projectUrl) {
|
|
218
|
+
console.error("No ORY_PROJECT_URL configured — cannot write permissions.");
|
|
219
|
+
console.error(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
if (catalog.length === 0) {
|
|
223
|
+
console.error(`No tool catalog known for harness "${harness}".`);
|
|
224
|
+
console.error(`Known harnesses: ${tool_catalog_js_1.KNOWN_HARNESSES.join(", ")}`);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
const client = client_js_1.OryAgentClient.fromEnv(harness);
|
|
228
|
+
attachCachedUserPrincipal(client);
|
|
229
|
+
await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
|
|
230
|
+
/* best-effort; the write may still succeed with the API key or no auth (local Keto) */
|
|
231
|
+
});
|
|
232
|
+
const subject = (0, subject_js_1.resolveUserSubject)(client);
|
|
233
|
+
const subjectId = (0, subject_js_1.subjectLabel)(subject);
|
|
234
|
+
if (subjectId === "agent:unknown") {
|
|
235
|
+
console.error("No user identity resolved — refusing to write permissions for an unknown subject.");
|
|
236
|
+
console.error("");
|
|
237
|
+
console.error("Either:");
|
|
238
|
+
console.error(" - run the harness once with ORY_AUTH_GATE=1 so a user token is cached, or");
|
|
239
|
+
console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
console.log(`Bootstrapping ${catalog.length} permissions for ${harness}`);
|
|
243
|
+
console.log("==========================================");
|
|
244
|
+
console.log("");
|
|
245
|
+
console.log(`Namespace: ${namespace}`);
|
|
246
|
+
console.log(`Subject: ${subjectId}`);
|
|
247
|
+
console.log(`Tools: ${catalog.join(", ")}`);
|
|
248
|
+
if (dryRun) {
|
|
249
|
+
console.log("");
|
|
250
|
+
console.log("(--dry-run) No permissions will be written.");
|
|
251
|
+
}
|
|
252
|
+
console.log("");
|
|
253
|
+
let created = 0;
|
|
254
|
+
let existed = 0;
|
|
255
|
+
const failures = [];
|
|
256
|
+
for (const tool of catalog) {
|
|
257
|
+
const check = {
|
|
258
|
+
namespace,
|
|
259
|
+
object: tool,
|
|
260
|
+
relation: "use",
|
|
261
|
+
...subject,
|
|
262
|
+
};
|
|
263
|
+
if (dryRun) {
|
|
264
|
+
console.log(` ~ would write: ${namespace}:${tool}#use@${subjectId}`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const res = await client.createRelationship(check, {
|
|
269
|
+
spanAttributes: { toolName: tool, source: "permissions_bootstrap" },
|
|
270
|
+
});
|
|
271
|
+
if (res.alreadyExisted) {
|
|
272
|
+
existed++;
|
|
273
|
+
console.log(` = ${tool} (already exists)`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
created++;
|
|
277
|
+
console.log(` + ${tool} (created)`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
const ory = err;
|
|
282
|
+
const reason = `${ory.code ?? "unknown"}${ory.status ? ` (HTTP ${ory.status})` : ""}`;
|
|
283
|
+
failures.push({ tool, reason });
|
|
284
|
+
console.log(` ! ${tool} (failed: ${reason})`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
console.log("");
|
|
288
|
+
if (dryRun) {
|
|
289
|
+
console.log(`Dry run: ${catalog.length} permissions would be written.`);
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
console.log(`Created: ${created} Already existed: ${existed} Failed: ${failures.length}`);
|
|
293
|
+
if (failures.length > 0) {
|
|
294
|
+
const writeAuthFailure = failures.some((f) => /(unknown|forbidden|session_inactive)/.test(f.reason) ||
|
|
295
|
+
/401|403/.test(f.reason));
|
|
296
|
+
if (writeAuthFailure) {
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log("Some writes were rejected — your current credentials may lack");
|
|
299
|
+
console.log("write scope on the permission namespace. You can apply the");
|
|
300
|
+
console.log("permissions manually using the snippet below (one per line):");
|
|
301
|
+
console.log("");
|
|
302
|
+
for (const tool of catalog) {
|
|
303
|
+
console.log(` ${namespace}:${tool}#use@${subjectId}`);
|
|
304
|
+
}
|
|
305
|
+
console.log("");
|
|
306
|
+
console.log("Or use the Ory Console: open the project, go to Permissions →");
|
|
307
|
+
console.log(`Relationships, set namespace='${namespace}', object=<tool>, relation='use',`);
|
|
308
|
+
console.log(`and subject='${subjectId}'.`);
|
|
309
|
+
}
|
|
310
|
+
return failures.length === catalog.length ? 1 : 0;
|
|
311
|
+
}
|
|
312
|
+
console.log("");
|
|
313
|
+
console.log(`Permissions are in place. Promote to enforce mode when ready:`);
|
|
314
|
+
console.log(` ${binName} permissions enforce`);
|
|
315
|
+
return 0;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether a user identity is cached locally — used by `install` to
|
|
319
|
+
* decide whether to opportunistically bootstrap tuples without
|
|
320
|
+
* prompting. Returns true when persisted tokens exist and are not
|
|
321
|
+
* expired, or when ORY_USER_SUBJECT_ID is set explicitly.
|
|
322
|
+
*/
|
|
323
|
+
function isUserIdentityCached() {
|
|
324
|
+
if (process.env.ORY_USER_SUBJECT_ID)
|
|
325
|
+
return true;
|
|
326
|
+
const tokens = (0, auth_store_js_1.loadTokens)();
|
|
327
|
+
return !!tokens && !(0, auth_store_js_1.isExpired)(tokens);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Print the "permission mode" section of the install banner.
|
|
331
|
+
*
|
|
332
|
+
* Tells the user what observe vs enforce mean, and prints the two
|
|
333
|
+
* follow-up commands they'll typically reach for next:
|
|
334
|
+
*
|
|
335
|
+
* - `<bin> permissions bootstrap` to seed tuples for the harness's
|
|
336
|
+
* built-in tools (the line is shown whether or not we just ran it
|
|
337
|
+
* ourselves, so the user can re-run after editing the catalog).
|
|
338
|
+
* - `<bin> permissions enforce` once they're satisfied with the
|
|
339
|
+
* observe-mode trace output and want denies to actually block.
|
|
340
|
+
*/
|
|
341
|
+
function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
|
|
342
|
+
const resolved = (0, config_js_1.resolveConfig)();
|
|
343
|
+
const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
|
|
344
|
+
console.log("");
|
|
345
|
+
console.log("Permissions:");
|
|
346
|
+
console.log(` Mode: ${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`);
|
|
347
|
+
console.log(` Other modes: ${otherModes(resolved.permissionMode).join(", ")}`);
|
|
348
|
+
if (resolved.auditOnly) {
|
|
349
|
+
console.log(" Ory is in audit-only mode (kill switch). Permission checks are disabled.");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (resolved.permissionMode === "observe") {
|
|
353
|
+
console.log(" Observe mode: permission denies are logged + traced, but tools still run.");
|
|
354
|
+
console.log(" This is the safe default — promote to 'enforce' once your permission set is correct.");
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
console.log(" Enforce mode: permission denies block tool execution.");
|
|
358
|
+
}
|
|
359
|
+
if (opts.bootstrappedAutomatically) {
|
|
360
|
+
console.log("");
|
|
361
|
+
console.log(` Permissions bootstrapped for ${catalog.length} built-in tools.`);
|
|
362
|
+
}
|
|
363
|
+
else if (catalog.length > 0) {
|
|
364
|
+
console.log("");
|
|
365
|
+
console.log(" Grant the current user 'use' on every built-in tool (idempotent):");
|
|
366
|
+
console.log(` npx ${binName} permissions bootstrap`);
|
|
367
|
+
}
|
|
368
|
+
console.log("");
|
|
369
|
+
console.log(" Inspect permission coverage:");
|
|
370
|
+
console.log(` npx ${binName} permissions status`);
|
|
371
|
+
console.log("");
|
|
372
|
+
console.log(" Promote to enforcing once observe-mode logs look right:");
|
|
373
|
+
console.log(` npx ${binName} permissions enforce`);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Render the trailing source annotation on a Mode line. Stays silent
|
|
377
|
+
* when the mode came from the built-in default, since saying "default"
|
|
378
|
+
* just adds noise. When the mode was overridden by env or config, name
|
|
379
|
+
* the override so the reader knows where to look.
|
|
380
|
+
*/
|
|
381
|
+
function formatModeSource(source) {
|
|
382
|
+
switch (source) {
|
|
383
|
+
case "env":
|
|
384
|
+
return ` (set via ${ENV_PERMISSION_MODE} env var)`;
|
|
385
|
+
case "config":
|
|
386
|
+
return " (saved in config; change with: permissions observe|enforce)";
|
|
387
|
+
case "default":
|
|
388
|
+
return " (built-in default)";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Generic source-suffix helper for other config fields (project URL,
|
|
393
|
+
* API key). Source 'none' means the value is unset — don't add a suffix.
|
|
394
|
+
*/
|
|
395
|
+
function formatSourceSuffix(source) {
|
|
396
|
+
switch (source) {
|
|
397
|
+
case "env":
|
|
398
|
+
return " (from env)";
|
|
399
|
+
case "config":
|
|
400
|
+
return " (from config)";
|
|
401
|
+
case "none":
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Lists the modes other than the active one, in stable order, so the
|
|
407
|
+
* banner can hint "Other modes: …" without re-listing the active one.
|
|
408
|
+
* Audit-only is a separate kill switch (config.auditOnly), but we still
|
|
409
|
+
* mention it here so first-time readers learn the full set.
|
|
410
|
+
*/
|
|
411
|
+
function otherModes(active) {
|
|
412
|
+
const all = ["observe", "enforce", "audit-only"];
|
|
413
|
+
return all.filter((m) => m !== active);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* If a user identity is already cached and the plugin is configured,
|
|
417
|
+
* opportunistically run `permissions bootstrap` so the user lands in a
|
|
418
|
+
* "tools work out of the box" state. Otherwise a no-op (the install
|
|
419
|
+
* banner still prints the manual command).
|
|
420
|
+
*
|
|
421
|
+
* Always best-effort: any failure is logged and swallowed so install
|
|
422
|
+
* never aborts because of a permissions write problem.
|
|
423
|
+
*/
|
|
424
|
+
async function maybeAutoBootstrap(binName, harness) {
|
|
425
|
+
const resolved = (0, config_js_1.resolveConfig)();
|
|
426
|
+
if (resolved.auditOnly)
|
|
427
|
+
return false;
|
|
428
|
+
if (!resolved.projectUrl)
|
|
429
|
+
return false;
|
|
430
|
+
if (!isUserIdentityCached())
|
|
431
|
+
return false;
|
|
432
|
+
if ((0, tool_catalog_js_1.getToolCatalog)(harness).length === 0)
|
|
433
|
+
return false;
|
|
434
|
+
try {
|
|
435
|
+
console.log("");
|
|
436
|
+
console.log("Bootstrapping permissions (cached user identity detected)...");
|
|
437
|
+
const code = await runPermissionsCommand(binName, harness, ["bootstrap"]);
|
|
438
|
+
return code === 0;
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
442
|
+
console.warn(`Bootstrap skipped: ${msg}`);
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -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
|
+
"version": "0.2.1",
|
|
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",
|