@ory/argus 0.1.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.
Files changed (80) hide show
  1. package/README.md +134 -0
  2. package/assets/commands/local-down.md +19 -0
  3. package/assets/commands/local-up.md +27 -0
  4. package/assets/skills/auth-setup/SKILL.md +279 -0
  5. package/assets/skills/local-dev/SKILL.md +206 -0
  6. package/assets/skills/login-flow/SKILL.md +383 -0
  7. package/assets/skills/social-login/SKILL.md +312 -0
  8. package/dist/agent-auth.d.ts +204 -0
  9. package/dist/agent-auth.js +553 -0
  10. package/dist/auth-gate.d.ts +71 -0
  11. package/dist/auth-gate.js +308 -0
  12. package/dist/auth-store.d.ts +75 -0
  13. package/dist/auth-store.js +261 -0
  14. package/dist/auth.d.ts +93 -0
  15. package/dist/auth.js +323 -0
  16. package/dist/cli.d.ts +73 -0
  17. package/dist/cli.js +484 -0
  18. package/dist/client.d.ts +158 -0
  19. package/dist/client.js +679 -0
  20. package/dist/config.d.ts +135 -0
  21. package/dist/config.js +344 -0
  22. package/dist/denial.d.ts +79 -0
  23. package/dist/denial.js +103 -0
  24. package/dist/dev.d.ts +95 -0
  25. package/dist/dev.js +514 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.js +137 -0
  28. package/dist/local/cli.d.ts +12 -0
  29. package/dist/local/cli.js +95 -0
  30. package/dist/local/configs.d.ts +89 -0
  31. package/dist/local/configs.js +634 -0
  32. package/dist/local/health.d.ts +32 -0
  33. package/dist/local/health.js +65 -0
  34. package/dist/local/index.d.ts +6 -0
  35. package/dist/local/index.js +38 -0
  36. package/dist/local/jaeger-main.d.ts +13 -0
  37. package/dist/local/jaeger-main.js +85 -0
  38. package/dist/local/jaeger.d.ts +50 -0
  39. package/dist/local/jaeger.js +162 -0
  40. package/dist/local/main.d.ts +7 -0
  41. package/dist/local/main.js +14 -0
  42. package/dist/local/manager.d.ts +45 -0
  43. package/dist/local/manager.js +676 -0
  44. package/dist/local/seed.d.ts +71 -0
  45. package/dist/local/seed.js +237 -0
  46. package/dist/logger.d.ts +29 -0
  47. package/dist/logger.js +139 -0
  48. package/dist/mcp.d.ts +76 -0
  49. package/dist/mcp.js +122 -0
  50. package/dist/otel/exporter.d.ts +17 -0
  51. package/dist/otel/exporter.js +12 -0
  52. package/dist/otel/index.d.ts +2 -0
  53. package/dist/otel/index.js +8 -0
  54. package/dist/otel/otlp-http.d.ts +116 -0
  55. package/dist/otel/otlp-http.js +322 -0
  56. package/dist/registry/cli.d.ts +12 -0
  57. package/dist/registry/cli.js +76 -0
  58. package/dist/registry/config.d.ts +23 -0
  59. package/dist/registry/config.js +80 -0
  60. package/dist/registry/index.d.ts +3 -0
  61. package/dist/registry/index.js +21 -0
  62. package/dist/registry/main.d.ts +7 -0
  63. package/dist/registry/main.js +14 -0
  64. package/dist/registry/manager.d.ts +38 -0
  65. package/dist/registry/manager.js +674 -0
  66. package/dist/setup.d.ts +118 -0
  67. package/dist/setup.js +398 -0
  68. package/dist/skills.d.ts +78 -0
  69. package/dist/skills.js +264 -0
  70. package/dist/subject.d.ts +43 -0
  71. package/dist/subject.js +55 -0
  72. package/dist/tool-metadata.d.ts +41 -0
  73. package/dist/tool-metadata.js +127 -0
  74. package/dist/tracer.d.ts +172 -0
  75. package/dist/tracer.js +452 -0
  76. package/dist/types.d.ts +57 -0
  77. package/dist/types.js +3 -0
  78. package/dist/watch-sandbox.d.ts +9 -0
  79. package/dist/watch-sandbox.js +81 -0
  80. package/package.json +79 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Persistent configuration for Ory agent plugins.
3
+ *
4
+ * Config is stored at ~/.config/ory-agent-plugins/config.json and shared
5
+ * across all harness plugins. Environment variables always take precedence
6
+ * over values in the config file.
7
+ */
8
+ export interface OryOAuth2Tokens {
9
+ accessToken: string;
10
+ refreshToken?: string;
11
+ /** Unix epoch seconds at which the access token expires. */
12
+ expiresAt: number;
13
+ /** Subject (sub claim) the token was issued for. */
14
+ subject?: string;
15
+ /** Client ID the token was issued under. */
16
+ clientId?: string;
17
+ /** Optional id_token (OpenID Connect). */
18
+ idToken?: string;
19
+ /** Scope string returned by the token endpoint. */
20
+ scope?: string;
21
+ }
22
+ /** Credentials for the human user principal (interactive login). */
23
+ export interface OryUserCredentials {
24
+ /** Persisted OAuth2 tokens from a browser-based PKCE login. */
25
+ oauth2?: OryOAuth2Tokens;
26
+ }
27
+ /**
28
+ * Credentials returned by an OAuth2 Dynamic Client Registration flow
29
+ * (RFC 7591). Persisted so that subsequent dev launches and process
30
+ * restarts re-use the same agent identity instead of re-registering on
31
+ * every session.
32
+ */
33
+ export interface OryAgentDynamicCredentials {
34
+ /** Issued client_id. */
35
+ clientId: string;
36
+ /** Issued client_secret. Omitted for public clients. */
37
+ clientSecret?: string;
38
+ /** Bearer used to manage this registration via RFC 7592. */
39
+ registrationAccessToken?: string;
40
+ /** RFC 7592 management URI for this registration. */
41
+ registrationClientUri?: string;
42
+ /** Unix epoch seconds when this registration was issued. */
43
+ registeredAt: number;
44
+ /** Project URL the registration was issued against. */
45
+ projectUrl: string;
46
+ /** Harness name baked into the client_name (audit aid). */
47
+ harness?: string;
48
+ }
49
+ /** Credentials for the AI agent principal (machine identity). */
50
+ export interface OryAgentCredentialsBlock {
51
+ /** Per-install credentials issued via dynamic client registration. */
52
+ dynamic?: OryAgentDynamicCredentials;
53
+ /**
54
+ * Per-sub-agent dynamic client registrations, keyed by sub-agent type
55
+ * (e.g. `"Explore"`, `"general-purpose"`). Each entry is a distinct
56
+ * OAuth2 client registered via RFC 7591 so the audit trail can
57
+ * attribute actions to a specific sub-agent identity. Reused across
58
+ * sessions for the same `(projectUrl, subAgentType)` pair.
59
+ */
60
+ subAgents?: Record<string, OryAgentDynamicCredentials>;
61
+ }
62
+ export interface OryPluginConfig {
63
+ projectUrl?: string;
64
+ apiKey?: string;
65
+ /** When true, only audit logging is enabled — no auth or permission checks. */
66
+ auditOnly?: boolean;
67
+ /** Credentials for the human user (interactive PKCE login). */
68
+ user?: OryUserCredentials;
69
+ /** Credentials for the AI agent process (machine identity). */
70
+ agent?: OryAgentCredentialsBlock;
71
+ }
72
+ /**
73
+ * Single OS-agnostic data directory for *all* Ory agent plugin state:
74
+ * the shared config file, persisted DCR credentials, and any harness
75
+ * asset directories. One place per platform — never split across
76
+ * `~/.ory/` and `~/.config/`.
77
+ *
78
+ * - Windows: `%APPDATA%/ory-agent-plugins` (e.g.
79
+ * `C:\Users\<user>\AppData\Roaming\ory-agent-plugins`).
80
+ * - Unix (macOS + Linux): `$XDG_CONFIG_HOME/ory-agent-plugins` when set,
81
+ * otherwise `~/.config/ory-agent-plugins`.
82
+ *
83
+ * `XDG_CONFIG_HOME` is also honored on Windows when set — useful for
84
+ * tests and for users who explicitly opt into XDG layout on Windows.
85
+ */
86
+ export declare function getDataDir(): string;
87
+ /**
88
+ * Per-harness sub-directory under the shared data dir. Use this for
89
+ * harness-owned asset trees (e.g. the Claude Code plugin's marketplace
90
+ * checkout) so all plugin state lives under one root.
91
+ */
92
+ export declare function getHarnessDataDir(harness: string): string;
93
+ /**
94
+ * Return the path to the config file.
95
+ */
96
+ export declare function getConfigPath(): string;
97
+ /**
98
+ * Load config from the config file. Returns an empty object if the file
99
+ * does not exist or is invalid.
100
+ */
101
+ export declare function loadConfig(): OryPluginConfig;
102
+ /**
103
+ * Save config to the config file. Merges with existing values — only
104
+ * provided fields are overwritten. Concurrent processes serialize via a
105
+ * sibling lockfile and the write is atomic (write-temp + rename).
106
+ *
107
+ * To clear a field rather than ignore it, set it to `null` in `update`
108
+ * (e.g. `saveConfig({ user: null })`); `undefined` is treated as
109
+ * "leave alone" for backwards compatibility.
110
+ */
111
+ export declare function saveConfig(update: Partial<{
112
+ [K in keyof OryPluginConfig]: OryPluginConfig[K] | null;
113
+ }>): void;
114
+ /**
115
+ * Read-modify-write the config under a sibling lockfile. The mutator
116
+ * receives the current on-disk config and must return the new config.
117
+ * Concurrent processes serialize on the lock; the write itself is
118
+ * atomic via write-temp + rename.
119
+ */
120
+ export declare function mutateConfig(mutator: (current: OryPluginConfig) => OryPluginConfig): void;
121
+ /**
122
+ * Resolve config by checking environment variables first, then the config file.
123
+ * Returns the merged result with the source of each value.
124
+ */
125
+ export declare function resolveConfig(): {
126
+ projectUrl?: string;
127
+ apiKey?: string;
128
+ auditOnly: boolean;
129
+ projectUrlSource: "env" | "config" | "none";
130
+ apiKeySource: "env" | "config" | "none";
131
+ };
132
+ /**
133
+ * Build the "not configured" prompt message for a given harness CLI bin name.
134
+ */
135
+ export declare function configPromptMessage(binName: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,344 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getDataDir = getDataDir;
37
+ exports.getHarnessDataDir = getHarnessDataDir;
38
+ exports.getConfigPath = getConfigPath;
39
+ exports.loadConfig = loadConfig;
40
+ exports.saveConfig = saveConfig;
41
+ exports.mutateConfig = mutateConfig;
42
+ exports.resolveConfig = resolveConfig;
43
+ exports.configPromptMessage = configPromptMessage;
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ /**
47
+ * Single OS-agnostic data directory for *all* Ory agent plugin state:
48
+ * the shared config file, persisted DCR credentials, and any harness
49
+ * asset directories. One place per platform — never split across
50
+ * `~/.ory/` and `~/.config/`.
51
+ *
52
+ * - Windows: `%APPDATA%/ory-agent-plugins` (e.g.
53
+ * `C:\Users\<user>\AppData\Roaming\ory-agent-plugins`).
54
+ * - Unix (macOS + Linux): `$XDG_CONFIG_HOME/ory-agent-plugins` when set,
55
+ * otherwise `~/.config/ory-agent-plugins`.
56
+ *
57
+ * `XDG_CONFIG_HOME` is also honored on Windows when set — useful for
58
+ * tests and for users who explicitly opt into XDG layout on Windows.
59
+ */
60
+ function getDataDir() {
61
+ const xdgOverride = process.env.XDG_CONFIG_HOME?.trim();
62
+ if (xdgOverride)
63
+ return path.join(xdgOverride, "ory-agent-plugins");
64
+ if (process.platform === "win32") {
65
+ const appData = process.env.APPDATA?.trim() ||
66
+ path.join(getHomeDir(), "AppData", "Roaming");
67
+ return path.join(appData, "ory-agent-plugins");
68
+ }
69
+ return path.join(getHomeDir(), ".config", "ory-agent-plugins");
70
+ }
71
+ /**
72
+ * Per-harness sub-directory under the shared data dir. Use this for
73
+ * harness-owned asset trees (e.g. the Claude Code plugin's marketplace
74
+ * checkout) so all plugin state lives under one root.
75
+ */
76
+ function getHarnessDataDir(harness) {
77
+ return path.join(getDataDir(), harness);
78
+ }
79
+ function configDir() {
80
+ return getDataDir();
81
+ }
82
+ function configFile() {
83
+ return path.join(configDir(), "config.json");
84
+ }
85
+ function configLockFile() {
86
+ return configFile() + ".lock";
87
+ }
88
+ /** A lock that is older than this is considered stale and may be broken. */
89
+ const LOCK_STALE_MS = 30_000;
90
+ /** Polling interval while waiting to acquire the lock. */
91
+ const LOCK_POLL_MS = 25;
92
+ /** Maximum total time to wait for the lock before giving up. */
93
+ const LOCK_TIMEOUT_MS = 5_000;
94
+ function getHomeDir() {
95
+ return process.env.HOME ?? process.env.USERPROFILE ?? "~";
96
+ }
97
+ /**
98
+ * Return the path to the config file.
99
+ */
100
+ function getConfigPath() {
101
+ return configFile();
102
+ }
103
+ /**
104
+ * Load config from the config file. Returns an empty object if the file
105
+ * does not exist or is invalid.
106
+ */
107
+ function loadConfig() {
108
+ try {
109
+ const file = configFile();
110
+ if (!fs.existsSync(file))
111
+ return {};
112
+ const raw = fs.readFileSync(file, "utf-8");
113
+ const parsed = JSON.parse(raw);
114
+ return {
115
+ projectUrl: typeof parsed.projectUrl === "string" ? parsed.projectUrl : undefined,
116
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
117
+ auditOnly: parsed.auditOnly === true ? true : undefined,
118
+ user: parseUserCredentials(parsed),
119
+ agent: parseAgentCredentials(parsed),
120
+ };
121
+ }
122
+ catch {
123
+ return {};
124
+ }
125
+ }
126
+ function parseAgentCredentials(parsed) {
127
+ const raw = parsed.agent;
128
+ if (!raw || typeof raw !== "object")
129
+ return undefined;
130
+ const block = raw;
131
+ const dynamic = parseAgentDynamicCredentials(block.dynamic);
132
+ const subAgents = parseSubAgentCredentials(block.subAgents);
133
+ if (!dynamic && !subAgents)
134
+ return undefined;
135
+ return {
136
+ ...(dynamic ? { dynamic } : {}),
137
+ ...(subAgents ? { subAgents } : {}),
138
+ };
139
+ }
140
+ function parseSubAgentCredentials(value) {
141
+ if (!value || typeof value !== "object")
142
+ return undefined;
143
+ const entries = value;
144
+ const result = {};
145
+ for (const [key, raw] of Object.entries(entries)) {
146
+ const creds = parseAgentDynamicCredentials(raw);
147
+ if (creds)
148
+ result[key] = creds;
149
+ }
150
+ return Object.keys(result).length > 0 ? result : undefined;
151
+ }
152
+ function parseAgentDynamicCredentials(value) {
153
+ if (!value || typeof value !== "object")
154
+ return undefined;
155
+ const v = value;
156
+ if (typeof v.clientId !== "string" || v.clientId.length === 0)
157
+ return undefined;
158
+ if (typeof v.projectUrl !== "string" || v.projectUrl.length === 0)
159
+ return undefined;
160
+ const registeredAt = typeof v.registeredAt === "number" ? v.registeredAt : Math.floor(Date.now() / 1000);
161
+ return {
162
+ clientId: v.clientId,
163
+ clientSecret: typeof v.clientSecret === "string" ? v.clientSecret : undefined,
164
+ registrationAccessToken: typeof v.registrationAccessToken === "string" ? v.registrationAccessToken : undefined,
165
+ registrationClientUri: typeof v.registrationClientUri === "string" ? v.registrationClientUri : undefined,
166
+ registeredAt,
167
+ projectUrl: v.projectUrl,
168
+ harness: typeof v.harness === "string" ? v.harness : undefined,
169
+ };
170
+ }
171
+ function parseUserCredentials(parsed) {
172
+ // Prefer the new nested shape; fall back to the legacy root-level
173
+ // `oauth2` block so configs written before the user/agent split still
174
+ // round-trip. The next saveConfig() drops the legacy field.
175
+ const nested = parsed.user && typeof parsed.user === "object"
176
+ ? parsed.user
177
+ : undefined;
178
+ const oauth2 = parseOAuth2(nested?.oauth2 ?? parsed.oauth2);
179
+ if (!oauth2)
180
+ return undefined;
181
+ return { oauth2 };
182
+ }
183
+ function parseOAuth2(value) {
184
+ if (!value || typeof value !== "object")
185
+ return undefined;
186
+ const v = value;
187
+ if (typeof v.accessToken !== "string" || v.accessToken.length === 0)
188
+ return undefined;
189
+ if (typeof v.expiresAt !== "number")
190
+ return undefined;
191
+ return {
192
+ accessToken: v.accessToken,
193
+ refreshToken: typeof v.refreshToken === "string" ? v.refreshToken : undefined,
194
+ expiresAt: v.expiresAt,
195
+ subject: typeof v.subject === "string" ? v.subject : undefined,
196
+ clientId: typeof v.clientId === "string" ? v.clientId : undefined,
197
+ idToken: typeof v.idToken === "string" ? v.idToken : undefined,
198
+ scope: typeof v.scope === "string" ? v.scope : undefined,
199
+ };
200
+ }
201
+ /**
202
+ * Save config to the config file. Merges with existing values — only
203
+ * provided fields are overwritten. Concurrent processes serialize via a
204
+ * sibling lockfile and the write is atomic (write-temp + rename).
205
+ *
206
+ * To clear a field rather than ignore it, set it to `null` in `update`
207
+ * (e.g. `saveConfig({ user: null })`); `undefined` is treated as
208
+ * "leave alone" for backwards compatibility.
209
+ */
210
+ function saveConfig(update) {
211
+ mutateConfig((existing) => mergeConfig(existing, update));
212
+ }
213
+ /**
214
+ * Read-modify-write the config under a sibling lockfile. The mutator
215
+ * receives the current on-disk config and must return the new config.
216
+ * Concurrent processes serialize on the lock; the write itself is
217
+ * atomic via write-temp + rename.
218
+ */
219
+ function mutateConfig(mutator) {
220
+ ensureConfigDir();
221
+ const lock = acquireLock();
222
+ try {
223
+ const existing = loadConfig();
224
+ const next = mutator(existing);
225
+ writeConfigAtomic(next);
226
+ }
227
+ finally {
228
+ releaseLock(lock);
229
+ }
230
+ }
231
+ function mergeConfig(existing, update) {
232
+ const result = { ...existing };
233
+ for (const key of Object.keys(update)) {
234
+ const value = update[key];
235
+ if (value === undefined)
236
+ continue;
237
+ if (value === null) {
238
+ delete result[key];
239
+ }
240
+ else {
241
+ // Type system can't track the per-key narrowing; the runtime is correct.
242
+ result[key] = value;
243
+ }
244
+ }
245
+ return result;
246
+ }
247
+ function ensureConfigDir() {
248
+ const dir = configDir();
249
+ if (!fs.existsSync(dir)) {
250
+ fs.mkdirSync(dir, { recursive: true });
251
+ }
252
+ }
253
+ function acquireLock() {
254
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
255
+ const lockPath = configLockFile();
256
+ for (;;) {
257
+ try {
258
+ const fd = fs.openSync(lockPath, "wx", 0o600);
259
+ fs.writeSync(fd, String(process.pid));
260
+ return { fd };
261
+ }
262
+ catch (err) {
263
+ if (err.code !== "EEXIST")
264
+ throw err;
265
+ // Lock exists — check if it's stale.
266
+ try {
267
+ const stat = fs.statSync(lockPath);
268
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
269
+ // Best-effort break; if another process beats us, we'll retry.
270
+ try {
271
+ fs.unlinkSync(lockPath);
272
+ }
273
+ catch {
274
+ /* ignore */
275
+ }
276
+ continue;
277
+ }
278
+ }
279
+ catch {
280
+ // Lockfile vanished between EEXIST and stat — retry immediately.
281
+ continue;
282
+ }
283
+ if (Date.now() > deadline) {
284
+ throw new Error(`Timed out waiting for config lock at ${lockPath}. ` +
285
+ "Another process may be holding it; remove the file if you are sure no other process is running.");
286
+ }
287
+ sleepSync(LOCK_POLL_MS);
288
+ }
289
+ }
290
+ }
291
+ const SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
292
+ function sleepSync(ms) {
293
+ Atomics.wait(SLEEP_BUF, 0, 0, ms);
294
+ }
295
+ function releaseLock(lock) {
296
+ try {
297
+ fs.closeSync(lock.fd);
298
+ }
299
+ catch {
300
+ /* ignore */
301
+ }
302
+ try {
303
+ fs.unlinkSync(configLockFile());
304
+ }
305
+ catch {
306
+ /* ignore */
307
+ }
308
+ }
309
+ function writeConfigAtomic(config) {
310
+ const target = configFile();
311
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
312
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
313
+ fs.renameSync(tmp, target);
314
+ }
315
+ /**
316
+ * Resolve config by checking environment variables first, then the config file.
317
+ * Returns the merged result with the source of each value.
318
+ */
319
+ function resolveConfig() {
320
+ const file = loadConfig();
321
+ const envProjectUrl = process.env.ORY_PROJECT_URL;
322
+ const envApiKey = process.env.ORY_API_KEY;
323
+ return {
324
+ projectUrl: envProjectUrl ?? file.projectUrl,
325
+ apiKey: envApiKey ?? file.apiKey,
326
+ auditOnly: file.auditOnly === true,
327
+ projectUrlSource: envProjectUrl ? "env" : file.projectUrl ? "config" : "none",
328
+ apiKeySource: envApiKey ? "env" : file.apiKey ? "config" : "none",
329
+ };
330
+ }
331
+ /**
332
+ * Build the "not configured" prompt message for a given harness CLI bin name.
333
+ */
334
+ function configPromptMessage(binName) {
335
+ return ("The Ory agent plugin is installed but not yet configured.\n" +
336
+ "\n" +
337
+ " Option 1 — Connect to Ory (enables authentication and permission checks):\n" +
338
+ ` npx ${binName} configure --project-url <URL> --api-key <KEY>\n` +
339
+ "\n" +
340
+ " Option 2 — Continue without authentication (audit logging only):\n" +
341
+ ` npx ${binName} configure --audit-only\n` +
342
+ "\n" +
343
+ `Configuration is saved to ${configFile()} and shared across all agent plugins.`);
344
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Standardized denial messaging for tool calls blocked by Ory Permissions.
3
+ *
4
+ * All harness plugins format their deny reason / system message via these
5
+ * helpers so the human-facing text is consistent: tells the user which
6
+ * subject lacks which permission, and points them at their Ory Keto admin.
7
+ *
8
+ * For harnesses whose plugin contract has no native "block" signal
9
+ * (currently OpenCode), throw `OryDenialError` from the pre-tool hook —
10
+ * that aborts execution and surfaces `error.message` to the agent.
11
+ */
12
+ import type { McpToolIdentifier } from "./mcp.js";
13
+ export interface DenialContext {
14
+ tool: string;
15
+ subjectId: string;
16
+ namespace?: string;
17
+ mcp?: McpToolIdentifier;
18
+ }
19
+ /**
20
+ * Span attributes that mark a denial as a security alert.
21
+ *
22
+ * Spread these onto the existing `tool.block` span attributes whenever an
23
+ * actual deny comes back from Ory. The shape is stable so downstream
24
+ * alerting rules can hook on `attributes.alert === true` and filter by
25
+ * `attributes.severity` or `attributes.alertKind` without string matching.
26
+ *
27
+ * The `blocked` field distinguishes harnesses whose plugin contract can
28
+ * stop the tool (claude-code/codex/gemini-cli exit-2, openclaw `{ block:
29
+ * true }`) from those that can only advise (opencode throws and relies on
30
+ * the harness to honor the exception). When `blocked === false` the agent
31
+ * may still execute the tool, so the alert is the only enforcement signal.
32
+ */
33
+ export interface AlertAttributes {
34
+ alert: true;
35
+ severity: "high";
36
+ alertKind: "permission_denied";
37
+ blocked: boolean;
38
+ }
39
+ export declare function alertAttributes(blocked: boolean): AlertAttributes;
40
+ /**
41
+ * Human-facing denial reason for an alert that the harness cannot enforce
42
+ * (advisory mode). Prepends an explicit "ORY SECURITY ALERT TRIGGERED"
43
+ * banner and notes that the trace span is the alert signal, so the agent
44
+ * — and any human reading transcripts — can't miss that a security event
45
+ * was emitted even though the tool was allowed to proceed.
46
+ */
47
+ export declare function formatAlertMessage(ctx: DenialContext): string;
48
+ /**
49
+ * Short alert summary suitable for compact UI fields (Gemini's
50
+ * systemMessage, etc.) when the denial cannot be enforced.
51
+ */
52
+ export declare function formatAlertSummary(ctx: DenialContext): string;
53
+ /**
54
+ * Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
55
+ * reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
56
+ * thrown error message).
57
+ */
58
+ export declare function formatDenialMessage(ctx: DenialContext): string;
59
+ /**
60
+ * Short denial summary suitable for compact UI fields (e.g. Gemini's
61
+ * systemMessage which appears inline in the agent's TUI).
62
+ */
63
+ export declare function formatDenialSummary(ctx: DenialContext): string;
64
+ /**
65
+ * Thrown by plugins whose hook contract has no native block signal and
66
+ * which fall back to aborting tool execution via an exception (OpenCode
67
+ * pattern). Because the harness may or may not honor the throw, the
68
+ * message uses `formatAlertMessage()` so the agent — and any human
69
+ * reading transcripts — sees explicit "ORY SECURITY ALERT TRIGGERED"
70
+ * text. The `code` field stays stable for callers/tests.
71
+ */
72
+ export declare class OryDenialError extends Error {
73
+ readonly code: "ory_permission_denied";
74
+ readonly tool: string;
75
+ readonly subjectId: string;
76
+ readonly namespace?: string;
77
+ readonly mcp?: McpToolIdentifier;
78
+ constructor(ctx: DenialContext);
79
+ }
package/dist/denial.js ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Standardized denial messaging for tool calls blocked by Ory Permissions.
4
+ *
5
+ * All harness plugins format their deny reason / system message via these
6
+ * helpers so the human-facing text is consistent: tells the user which
7
+ * subject lacks which permission, and points them at their Ory Keto admin.
8
+ *
9
+ * For harnesses whose plugin contract has no native "block" signal
10
+ * (currently OpenCode), throw `OryDenialError` from the pre-tool hook —
11
+ * that aborts execution and surfaces `error.message` to the agent.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.OryDenialError = void 0;
15
+ exports.alertAttributes = alertAttributes;
16
+ exports.formatAlertMessage = formatAlertMessage;
17
+ exports.formatAlertSummary = formatAlertSummary;
18
+ exports.formatDenialMessage = formatDenialMessage;
19
+ exports.formatDenialSummary = formatDenialSummary;
20
+ const ADMIN_HINT = "Contact your Ory Keto administrator to grant the required permission.";
21
+ function alertAttributes(blocked) {
22
+ return {
23
+ alert: true,
24
+ severity: "high",
25
+ alertKind: "permission_denied",
26
+ blocked,
27
+ };
28
+ }
29
+ const ALERT_PREFIX = "ORY SECURITY ALERT TRIGGERED";
30
+ /**
31
+ * Human-facing denial reason for an alert that the harness cannot enforce
32
+ * (advisory mode). Prepends an explicit "ORY SECURITY ALERT TRIGGERED"
33
+ * banner and notes that the trace span is the alert signal, so the agent
34
+ * — and any human reading transcripts — can't miss that a security event
35
+ * was emitted even though the tool was allowed to proceed.
36
+ */
37
+ function formatAlertMessage(ctx) {
38
+ return `[${ALERT_PREFIX}] ${formatDenialMessage(ctx)} A security alert trace span was recorded for this denied tool call.`;
39
+ }
40
+ /**
41
+ * Short alert summary suitable for compact UI fields (Gemini's
42
+ * systemMessage, etc.) when the denial cannot be enforced.
43
+ */
44
+ function formatAlertSummary(ctx) {
45
+ return `[${ALERT_PREFIX}] ${formatDenialSummary(ctx)}`;
46
+ }
47
+ /**
48
+ * Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
49
+ * reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
50
+ * thrown error message).
51
+ */
52
+ function formatDenialMessage(ctx) {
53
+ if (ctx.mcp) {
54
+ const tool = ctx.mcp.toolName
55
+ ? `${ctx.mcp.serverName}/${ctx.mcp.toolName}`
56
+ : ctx.mcp.serverName;
57
+ return (`Ory: permission denied for MCP server "${ctx.mcp.serverName}". ` +
58
+ `Subject "${ctx.subjectId}" is not authorized to use "${tool}". ` +
59
+ ADMIN_HINT);
60
+ }
61
+ const ns = ctx.namespace ?? "AgentTools";
62
+ return (`Ory: permission denied for "${ctx.tool}". ` +
63
+ `Subject "${ctx.subjectId}" is not authorized to invoke "${ctx.tool}" ` +
64
+ `(namespace "${ns}"). ` +
65
+ ADMIN_HINT);
66
+ }
67
+ /**
68
+ * Short denial summary suitable for compact UI fields (e.g. Gemini's
69
+ * systemMessage which appears inline in the agent's TUI).
70
+ */
71
+ function formatDenialSummary(ctx) {
72
+ if (ctx.mcp) {
73
+ const tool = ctx.mcp.toolName
74
+ ? `${ctx.mcp.serverName}/${ctx.mcp.toolName}`
75
+ : ctx.mcp.serverName;
76
+ return `Ory: permission denied for MCP tool "${tool}". ${ADMIN_HINT}`;
77
+ }
78
+ return `Ory: permission denied for "${ctx.tool}". ${ADMIN_HINT}`;
79
+ }
80
+ /**
81
+ * Thrown by plugins whose hook contract has no native block signal and
82
+ * which fall back to aborting tool execution via an exception (OpenCode
83
+ * pattern). Because the harness may or may not honor the throw, the
84
+ * message uses `formatAlertMessage()` so the agent — and any human
85
+ * reading transcripts — sees explicit "ORY SECURITY ALERT TRIGGERED"
86
+ * text. The `code` field stays stable for callers/tests.
87
+ */
88
+ class OryDenialError extends Error {
89
+ code = "ory_permission_denied";
90
+ tool;
91
+ subjectId;
92
+ namespace;
93
+ mcp;
94
+ constructor(ctx) {
95
+ super(formatAlertMessage(ctx));
96
+ this.name = "OryDenialError";
97
+ this.tool = ctx.tool;
98
+ this.subjectId = ctx.subjectId;
99
+ this.namespace = ctx.namespace;
100
+ this.mcp = ctx.mcp;
101
+ }
102
+ }
103
+ exports.OryDenialError = OryDenialError;