@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,71 @@
1
+ /**
2
+ * Seed the local Ory environment with test data for development.
3
+ *
4
+ * Creates two principals:
5
+ *
6
+ * 1. **Agent** (`agent@ory-local.dev`) — a Kratos identity that the
7
+ * harness will register a DCR OAuth2 client *for* on first session
8
+ * start. No OAuth2 client is pre-registered here.
9
+ *
10
+ * 2. **User** (`user@ory-local.dev`) — a human identity with a known
11
+ * password (printed by the launcher) and an OAuth2
12
+ * `authorization_code` + PKCE client registered against the
13
+ * loopback redirect URIs. The launcher always runs the real PKCE
14
+ * browser flow; the user types these credentials into the login
15
+ * UI to complete the flow.
16
+ *
17
+ * Permission tuples are written as SubjectSet `User:<identity.id>` so
18
+ * the Console's *Add relationship* dialog (which always emits SubjectSet
19
+ * subjects) can recreate them by hand for the demo walkthrough. Plugins
20
+ * build the matching SubjectSet check when `ORY_USER_SUBJECT_NAMESPACE`
21
+ * is set (the launcher sets it to `User`).
22
+ *
23
+ * Idempotent — re-running the seed reuses existing identities and
24
+ * recreates OAuth2 clients (so the secret is always known).
25
+ */
26
+ export interface SeededIdentity {
27
+ id: string;
28
+ email: string;
29
+ }
30
+ export interface SeededOAuth2Client {
31
+ clientId: string;
32
+ clientSecret?: string;
33
+ }
34
+ export interface SeedResult {
35
+ /**
36
+ * Agent (machine) identity — Kratos identity only. The agent's OAuth2
37
+ * client is *not* pre-registered: the harness self-registers via DCR
38
+ * (RFC 7591) on first session start using the user's bearer as the
39
+ * initial access token.
40
+ */
41
+ agent: {
42
+ identity: SeededIdentity;
43
+ };
44
+ /** User (human) identity — subject of permission checks. */
45
+ user: {
46
+ identity: SeededIdentity;
47
+ /**
48
+ * Password for the user identity. Printed by the launcher so the
49
+ * developer can type it into the login UI during the PKCE browser
50
+ * flow. Local-dev only — never used in production.
51
+ */
52
+ password: string;
53
+ /** OAuth2 authorization_code+PKCE client for interactive logins. */
54
+ client: SeededOAuth2Client;
55
+ };
56
+ /** Permission tuples written against the user's subject. */
57
+ permissions: {
58
+ namespace: string;
59
+ subject: string;
60
+ tuples: number;
61
+ };
62
+ }
63
+ /** Subject namespace for user identities in seeded permission tuples. */
64
+ export declare const USER_SUBJECT_NAMESPACE = "User";
65
+ /**
66
+ * Run the full seed process. Idempotent — safe to run multiple times.
67
+ *
68
+ * Returns a `SeedResult` describing both identities, the user's
69
+ * pre-minted session, and the OAuth2 clients registered for each.
70
+ */
71
+ export declare function seedLocalEnvironment(namespace?: string): Promise<SeedResult>;
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ /**
3
+ * Seed the local Ory environment with test data for development.
4
+ *
5
+ * Creates two principals:
6
+ *
7
+ * 1. **Agent** (`agent@ory-local.dev`) — a Kratos identity that the
8
+ * harness will register a DCR OAuth2 client *for* on first session
9
+ * start. No OAuth2 client is pre-registered here.
10
+ *
11
+ * 2. **User** (`user@ory-local.dev`) — a human identity with a known
12
+ * password (printed by the launcher) and an OAuth2
13
+ * `authorization_code` + PKCE client registered against the
14
+ * loopback redirect URIs. The launcher always runs the real PKCE
15
+ * browser flow; the user types these credentials into the login
16
+ * UI to complete the flow.
17
+ *
18
+ * Permission tuples are written as SubjectSet `User:<identity.id>` so
19
+ * the Console's *Add relationship* dialog (which always emits SubjectSet
20
+ * subjects) can recreate them by hand for the demo walkthrough. Plugins
21
+ * build the matching SubjectSet check when `ORY_USER_SUBJECT_NAMESPACE`
22
+ * is set (the launcher sets it to `User`).
23
+ *
24
+ * Idempotent — re-running the seed reuses existing identities and
25
+ * recreates OAuth2 clients (so the secret is always known).
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.USER_SUBJECT_NAMESPACE = void 0;
29
+ exports.seedLocalEnvironment = seedLocalEnvironment;
30
+ const auth_js_1 = require("../auth.js");
31
+ const configs_js_1 = require("./configs.js");
32
+ const KRATOS_ADMIN = `http://localhost:${configs_js_1.KRATOS_ADMIN_PORT}`;
33
+ const KETO_WRITE = `http://localhost:${configs_js_1.KETO_WRITE_PORT}`;
34
+ const HYDRA_ADMIN = `http://localhost:${configs_js_1.HYDRA_ADMIN_PORT}`;
35
+ const AGENT_EMAIL = "agent@ory-local.dev";
36
+ const AGENT_PASSWORD = "ory-agent-local-dev-password!";
37
+ const USER_EMAIL = "user@ory-local.dev";
38
+ const USER_PASSWORD = "ory-user-local-dev-password!";
39
+ const USER_CLIENT_NAME = "ory-agent-plugins-local-user";
40
+ const USER_CLIENT_ID = "ory-user-local";
41
+ /**
42
+ * Common tools that agents typically use. Permission tuples are seeded
43
+ * so the user identity is allowed to use all of them.
44
+ */
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
+ ];
65
+ async function jsonFetch(url, opts = {}) {
66
+ const res = await fetch(url, {
67
+ ...opts,
68
+ headers: {
69
+ "Content-Type": "application/json",
70
+ ...opts.headers,
71
+ },
72
+ signal: AbortSignal.timeout(10_000),
73
+ });
74
+ const body = await res.text();
75
+ if (!res.ok) {
76
+ throw new Error(`${opts.method ?? "GET"} ${url} → ${res.status}: ${body}`);
77
+ }
78
+ return body ? JSON.parse(body) : {};
79
+ }
80
+ /** Seed (or look up) a Kratos identity by email + password. */
81
+ async function seedIdentity(email, password, name) {
82
+ const existing = (await jsonFetch(`${KRATOS_ADMIN}/admin/identities?credentials_identifier=${encodeURIComponent(email)}`));
83
+ if (existing.length > 0) {
84
+ return { id: existing[0].id, email };
85
+ }
86
+ const identity = (await jsonFetch(`${KRATOS_ADMIN}/admin/identities`, {
87
+ method: "POST",
88
+ body: JSON.stringify({
89
+ schema_id: "agent",
90
+ traits: { email, name },
91
+ credentials: {
92
+ password: { config: { password } },
93
+ },
94
+ state: "active",
95
+ }),
96
+ }));
97
+ return { id: identity.id, email };
98
+ }
99
+ /** Subject namespace for user identities in seeded permission tuples. */
100
+ exports.USER_SUBJECT_NAMESPACE = "User";
101
+ /**
102
+ * Write permission tuples granting the user identity access to all
103
+ * common tools. The subject is written as a SubjectSet
104
+ * `User:<userId>` (no relation) so the Console UI's *Add relationship*
105
+ * dialog — which only emits SubjectSet subjects — can recreate the same
106
+ * tuple by hand during the runbook walkthrough.
107
+ */
108
+ async function seedPermissions(userId, namespace) {
109
+ let created = 0;
110
+ for (const tool of COMMON_TOOLS) {
111
+ try {
112
+ await jsonFetch(`${KETO_WRITE}/admin/relation-tuples`, {
113
+ method: "PUT",
114
+ body: JSON.stringify({
115
+ namespace,
116
+ object: tool,
117
+ relation: "use",
118
+ subject_set: {
119
+ namespace: exports.USER_SUBJECT_NAMESPACE,
120
+ object: userId,
121
+ relation: "",
122
+ },
123
+ }),
124
+ });
125
+ created++;
126
+ }
127
+ catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ if (!msg.includes("409")) {
130
+ process.stderr.write(` Warning: failed to create tuple for ${tool}: ${msg}\n`);
131
+ }
132
+ else {
133
+ created++;
134
+ }
135
+ }
136
+ }
137
+ return created;
138
+ }
139
+ /**
140
+ * Delete a Hydra OAuth2 client if it exists, then create it with a
141
+ * known client_id/secret. We always recreate so the launcher knows the
142
+ * exact credentials to inject into the harness env.
143
+ */
144
+ async function ensureOAuth2Client(spec) {
145
+ // Look up by client_name; delete if present so we can recreate with a
146
+ // deterministic secret.
147
+ const existing = (await jsonFetch(`${HYDRA_ADMIN}/admin/clients?client_name=${encodeURIComponent(spec.clientName)}`));
148
+ for (const c of existing) {
149
+ if (c.client_name !== spec.clientName)
150
+ continue;
151
+ try {
152
+ await jsonFetch(`${HYDRA_ADMIN}/admin/clients/${encodeURIComponent(c.client_id)}`, {
153
+ method: "DELETE",
154
+ });
155
+ }
156
+ catch {
157
+ // best-effort cleanup
158
+ }
159
+ }
160
+ const body = {
161
+ client_id: spec.clientId,
162
+ client_name: spec.clientName,
163
+ grant_types: spec.grantTypes,
164
+ response_types: spec.responseTypes,
165
+ scope: spec.scope,
166
+ token_endpoint_auth_method: spec.tokenEndpointAuthMethod,
167
+ };
168
+ if (spec.clientSecret)
169
+ body.client_secret = spec.clientSecret;
170
+ if (spec.redirectUris)
171
+ body.redirect_uris = spec.redirectUris;
172
+ const client = (await jsonFetch(`${HYDRA_ADMIN}/admin/clients`, {
173
+ method: "POST",
174
+ body: JSON.stringify(body),
175
+ }));
176
+ return {
177
+ clientId: client.client_id,
178
+ clientSecret: client.client_secret ?? spec.clientSecret,
179
+ };
180
+ }
181
+ /** PKCE redirect URIs — must match the loopback ports the harness binds to. */
182
+ function loopbackRedirectUris() {
183
+ return auth_js_1.LOOPBACK_PORTS.map((p) => `http://127.0.0.1:${p}/callback`);
184
+ }
185
+ /**
186
+ * Run the full seed process. Idempotent — safe to run multiple times.
187
+ *
188
+ * Returns a `SeedResult` describing both identities, the user's
189
+ * pre-minted session, and the OAuth2 clients registered for each.
190
+ */
191
+ async function seedLocalEnvironment(namespace = "AgentTools") {
192
+ process.stderr.write("Seeding local Ory environment...\n\n");
193
+ // 1. Identities
194
+ process.stderr.write(" Creating agent identity...\n");
195
+ const agentIdentity = await seedIdentity(AGENT_EMAIL, AGENT_PASSWORD, "Local Agent");
196
+ process.stderr.write(` Agent: ${agentIdentity.id} (${agentIdentity.email})\n`);
197
+ process.stderr.write(" Creating user identity...\n");
198
+ const userIdentity = await seedIdentity(USER_EMAIL, USER_PASSWORD, "Local User");
199
+ process.stderr.write(` User: ${userIdentity.id} (${userIdentity.email})\n`);
200
+ // 2. Permission tuples — written against the user's identity since the
201
+ // user is the subject of permission checks in the plugin handlers.
202
+ // Subject is a SubjectSet `User:<id>` so the Console UI can rewrite
203
+ // the same tuple shape by hand during the runbook demo.
204
+ const subjectLabel = `${exports.USER_SUBJECT_NAMESPACE}:${userIdentity.id}`;
205
+ process.stderr.write(` Creating permission tuples for ${COMMON_TOOLS.length} tools...\n`);
206
+ const tupleCount = await seedPermissions(userIdentity.id, namespace);
207
+ process.stderr.write(` Permissions: ${tupleCount} tuples in '${namespace}' for ${subjectLabel}\n`);
208
+ // 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
209
+ // PKCE flow has somewhere to authenticate against. The agent's
210
+ // OAuth2 client is intentionally NOT pre-registered; the harness
211
+ // will self-register via DCR on first session start.
212
+ process.stderr.write(" Registering user OAuth2 client (PKCE)...\n");
213
+ let userClient = { clientId: USER_CLIENT_ID };
214
+ try {
215
+ userClient = await ensureOAuth2Client({
216
+ clientName: USER_CLIENT_NAME,
217
+ clientId: USER_CLIENT_ID,
218
+ // Public PKCE client — no secret. Hydra accepts an empty string
219
+ // for `token_endpoint_auth_method=none`.
220
+ grantTypes: ["authorization_code", "refresh_token"],
221
+ responseTypes: ["code"],
222
+ scope: "openid offline_access",
223
+ tokenEndpointAuthMethod: "none",
224
+ redirectUris: loopbackRedirectUris(),
225
+ });
226
+ process.stderr.write(` User client: ${userClient.clientId}\n`);
227
+ }
228
+ catch (err) {
229
+ const msg = err instanceof Error ? err.message : String(err);
230
+ process.stderr.write(` Warning: user OAuth2 client registration failed: ${msg}\n`);
231
+ }
232
+ return {
233
+ agent: { identity: agentIdentity },
234
+ user: { identity: userIdentity, password: USER_PASSWORD, client: userClient },
235
+ permissions: { namespace, subject: subjectLabel, tuples: tupleCount },
236
+ };
237
+ }
@@ -0,0 +1,29 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export interface LogEntry {
3
+ timestamp: string;
4
+ level: LogLevel;
5
+ event: string;
6
+ harness: string;
7
+ data?: Record<string, unknown>;
8
+ }
9
+ /**
10
+ * Recursively redact sensitive values from a structured log payload.
11
+ * The original object is not modified.
12
+ */
13
+ export declare function redactLogData(value: unknown): unknown;
14
+ export declare class DebugLogger {
15
+ enabled: boolean;
16
+ harness: string;
17
+ logFile: string | null;
18
+ constructor(harness: string, opts?: {
19
+ logFile?: string;
20
+ });
21
+ get isEnabled(): boolean;
22
+ enable(): void;
23
+ disable(): void;
24
+ log(level: LogLevel, event: string, data?: Record<string, unknown>): void;
25
+ debug(event: string, data?: Record<string, unknown>): void;
26
+ info(event: string, data?: Record<string, unknown>): void;
27
+ warn(event: string, data?: Record<string, unknown>): void;
28
+ error(event: string, data?: Record<string, unknown>): void;
29
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,139 @@
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.DebugLogger = void 0;
37
+ exports.redactLogData = redactLogData;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ /**
41
+ * Keys whose values are always redacted before being written to stderr or
42
+ * to the log file. Match is case-insensitive on the final path segment.
43
+ */
44
+ const REDACTED_KEYS = new Set([
45
+ "accesstoken",
46
+ "access_token",
47
+ "refreshtoken",
48
+ "refresh_token",
49
+ "idtoken",
50
+ "id_token",
51
+ "sessiontoken",
52
+ "session_token",
53
+ "code_verifier",
54
+ "codeverifier",
55
+ "client_secret",
56
+ "clientsecret",
57
+ "authorization",
58
+ "password",
59
+ "apikey",
60
+ "api_key",
61
+ ]);
62
+ /**
63
+ * Recursively redact sensitive values from a structured log payload.
64
+ * The original object is not modified.
65
+ */
66
+ function redactLogData(value) {
67
+ if (Array.isArray(value)) {
68
+ return value.map(redactLogData);
69
+ }
70
+ if (value && typeof value === "object") {
71
+ const out = {};
72
+ for (const [k, v] of Object.entries(value)) {
73
+ if (REDACTED_KEYS.has(k.toLowerCase()) && v !== undefined && v !== null) {
74
+ out[k] = "[redacted]";
75
+ }
76
+ else {
77
+ out[k] = redactLogData(v);
78
+ }
79
+ }
80
+ return out;
81
+ }
82
+ return value;
83
+ }
84
+ class DebugLogger {
85
+ enabled;
86
+ harness;
87
+ logFile;
88
+ constructor(harness, opts) {
89
+ this.enabled = process.env.ORY_AGENT_DEBUG === "true";
90
+ this.harness = harness;
91
+ this.logFile = opts?.logFile ?? null;
92
+ }
93
+ get isEnabled() {
94
+ return this.enabled;
95
+ }
96
+ enable() {
97
+ this.enabled = true;
98
+ }
99
+ disable() {
100
+ this.enabled = false;
101
+ }
102
+ log(level, event, data) {
103
+ if (!this.enabled)
104
+ return;
105
+ const safeData = data
106
+ ? redactLogData(data)
107
+ : undefined;
108
+ const entry = {
109
+ timestamp: new Date().toISOString(),
110
+ level,
111
+ event,
112
+ harness: this.harness,
113
+ ...(safeData ? { data: safeData } : {}),
114
+ };
115
+ const line = JSON.stringify(entry);
116
+ // Always write to stderr so we don't interfere with hook stdout
117
+ process.stderr.write(`[ory-agent] ${line}\n`);
118
+ if (this.logFile) {
119
+ const dir = path.dirname(this.logFile);
120
+ if (!fs.existsSync(dir)) {
121
+ fs.mkdirSync(dir, { recursive: true });
122
+ }
123
+ fs.appendFileSync(this.logFile, line + "\n");
124
+ }
125
+ }
126
+ debug(event, data) {
127
+ this.log("debug", event, data);
128
+ }
129
+ info(event, data) {
130
+ this.log("info", event, data);
131
+ }
132
+ warn(event, data) {
133
+ this.log("warn", event, data);
134
+ }
135
+ error(event, data) {
136
+ this.log("error", event, data);
137
+ }
138
+ }
139
+ exports.DebugLogger = DebugLogger;
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * MCP (Model Context Protocol) server authorization.
3
+ *
4
+ * Provides types, parsers, and permission check helpers for securing
5
+ * MCP tool calls across all harness plugins. Each harness represents
6
+ * MCP tools differently — parsers normalize them into McpToolIdentifier,
7
+ * and checkMcpPermission runs server-level (and optionally tool-level)
8
+ * Ory permission checks.
9
+ */
10
+ import type { OryAgentClient } from "./client.js";
11
+ import type { UserSubjectRef } from "./subject.js";
12
+ export interface McpToolIdentifier {
13
+ /** The MCP server name (e.g., "github", "slack", "postgres"). */
14
+ serverName: string;
15
+ /** The tool name within the server (e.g., "create_issue", "query"). */
16
+ toolName: string;
17
+ }
18
+ export interface McpPermissionCheckOptions {
19
+ /**
20
+ * Subject for the permission check. Either a direct SubjectID
21
+ * (e.g. `session:abc123`) or a SubjectSet (e.g. `User:<UUID>`).
22
+ * Build with `resolveUserSubject(client, fallback)`.
23
+ */
24
+ subject: UserSubjectRef;
25
+ /** Namespace for server-level checks. Default: "mcp_servers". */
26
+ serverNamespace?: string;
27
+ /** Namespace for tool-level checks. Default: "mcp_tools". */
28
+ toolNamespace?: string;
29
+ /** Whether to also check tool-level permission. Default: false. */
30
+ checkToolLevel?: boolean;
31
+ /**
32
+ * Attributes merged into the permission.check / permission.batch_check
33
+ * trace spans. Use this to carry the harness-side tool name and any
34
+ * other context the Zanzibar tuple shape doesn't capture.
35
+ */
36
+ spanAttributes?: Record<string, unknown>;
37
+ }
38
+ export interface McpPermissionResult {
39
+ /** Whether the server-level check passed. */
40
+ serverAllowed: boolean;
41
+ /** Whether the tool-level check passed (true if not checked). */
42
+ toolAllowed: boolean;
43
+ /** Combined result: both server and tool (if checked) must be allowed. */
44
+ allowed: boolean;
45
+ checkedAt: string;
46
+ mcpTool: McpToolIdentifier;
47
+ }
48
+ /**
49
+ * Parse a Claude Code MCP tool name of the form `mcp__<server>__<tool>`.
50
+ * Returns null if the name doesn't match the MCP pattern.
51
+ *
52
+ * Claude Code uses double underscores as delimiters. The first `__` after
53
+ * the `mcp__` prefix separates the server name from the tool name. Server
54
+ * names may contain single underscores.
55
+ */
56
+ export declare function parseClaudeCodeMcpTool(toolName: string): McpToolIdentifier | null;
57
+ /**
58
+ * Parse MCP context from Gemini CLI hook input.
59
+ * Gemini provides server info via the `mcp_context` field.
60
+ */
61
+ export declare function parseGeminiMcpTool(toolName: string, mcpContext?: Record<string, unknown>): McpToolIdentifier | null;
62
+ /**
63
+ * Generic MCP tool parser for harnesses that use a `mcp:<server>:<tool>`
64
+ * convention. Returns null if the tool ID doesn't start with the prefix.
65
+ */
66
+ export declare function parseMcpToolGeneric(toolId: string, mcpPrefix?: string): McpToolIdentifier | null;
67
+ /**
68
+ * Check MCP server-level (and optionally tool-level) permissions.
69
+ *
70
+ * Server check: namespace=mcp_servers, object=<serverName>, relation=use
71
+ * Tool check: namespace=mcp_tools, object=<server>:<tool>, relation=invoke
72
+ *
73
+ * The subject is whatever the caller resolved (SubjectSet or direct ID).
74
+ * Throws on API errors — callers handle fail-open behavior.
75
+ */
76
+ export declare function checkMcpPermission(client: OryAgentClient, mcpTool: McpToolIdentifier, options: McpPermissionCheckOptions): Promise<McpPermissionResult>;
package/dist/mcp.js ADDED
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ /**
3
+ * MCP (Model Context Protocol) server authorization.
4
+ *
5
+ * Provides types, parsers, and permission check helpers for securing
6
+ * MCP tool calls across all harness plugins. Each harness represents
7
+ * MCP tools differently — parsers normalize them into McpToolIdentifier,
8
+ * and checkMcpPermission runs server-level (and optionally tool-level)
9
+ * Ory permission checks.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.parseClaudeCodeMcpTool = parseClaudeCodeMcpTool;
13
+ exports.parseGeminiMcpTool = parseGeminiMcpTool;
14
+ exports.parseMcpToolGeneric = parseMcpToolGeneric;
15
+ exports.checkMcpPermission = checkMcpPermission;
16
+ // ─── Parsers ───────────────────────────────────────────────────────
17
+ /**
18
+ * Parse a Claude Code MCP tool name of the form `mcp__<server>__<tool>`.
19
+ * Returns null if the name doesn't match the MCP pattern.
20
+ *
21
+ * Claude Code uses double underscores as delimiters. The first `__` after
22
+ * the `mcp__` prefix separates the server name from the tool name. Server
23
+ * names may contain single underscores.
24
+ */
25
+ function parseClaudeCodeMcpTool(toolName) {
26
+ if (!toolName.startsWith("mcp__"))
27
+ return null;
28
+ const rest = toolName.slice(5); // remove "mcp__"
29
+ const idx = rest.indexOf("__");
30
+ if (idx <= 0 || idx >= rest.length - 2)
31
+ return null;
32
+ return {
33
+ serverName: rest.slice(0, idx),
34
+ toolName: rest.slice(idx + 2),
35
+ };
36
+ }
37
+ /**
38
+ * Parse MCP context from Gemini CLI hook input.
39
+ * Gemini provides server info via the `mcp_context` field.
40
+ */
41
+ function parseGeminiMcpTool(toolName, mcpContext) {
42
+ if (!mcpContext || typeof mcpContext.server_name !== "string")
43
+ return null;
44
+ return {
45
+ serverName: mcpContext.server_name,
46
+ toolName,
47
+ };
48
+ }
49
+ /**
50
+ * Generic MCP tool parser for harnesses that use a `mcp:<server>:<tool>`
51
+ * convention. Returns null if the tool ID doesn't start with the prefix.
52
+ */
53
+ function parseMcpToolGeneric(toolId, mcpPrefix = "mcp:") {
54
+ if (!toolId.startsWith(mcpPrefix))
55
+ return null;
56
+ const rest = toolId.slice(mcpPrefix.length);
57
+ const sepIdx = rest.indexOf(":");
58
+ if (sepIdx === -1) {
59
+ return { serverName: rest, toolName: "" };
60
+ }
61
+ return {
62
+ serverName: rest.slice(0, sepIdx),
63
+ toolName: rest.slice(sepIdx + 1),
64
+ };
65
+ }
66
+ // ─── Permission Check ──────────────────────────────────────────────
67
+ /**
68
+ * Check MCP server-level (and optionally tool-level) permissions.
69
+ *
70
+ * Server check: namespace=mcp_servers, object=<serverName>, relation=use
71
+ * Tool check: namespace=mcp_tools, object=<server>:<tool>, relation=invoke
72
+ *
73
+ * The subject is whatever the caller resolved (SubjectSet or direct ID).
74
+ * Throws on API errors — callers handle fail-open behavior.
75
+ */
76
+ async function checkMcpPermission(client, mcpTool, options) {
77
+ const serverNamespace = options.serverNamespace ??
78
+ (process.env.ORY_MCP_SERVER_NAMESPACE ?? "mcp_servers");
79
+ const toolNamespace = options.toolNamespace ??
80
+ (process.env.ORY_MCP_TOOL_NAMESPACE ?? "mcp_tools");
81
+ const checkToolLevel = options.checkToolLevel ??
82
+ process.env.ORY_MCP_TOOL_LEVEL_CHECKS === "true";
83
+ const serverCheck = {
84
+ namespace: serverNamespace,
85
+ object: mcpTool.serverName,
86
+ relation: "use",
87
+ ...options.subject,
88
+ };
89
+ const spanAttributes = {
90
+ mcpServer: mcpTool.serverName,
91
+ ...(mcpTool.toolName ? { mcpTool: mcpTool.toolName } : {}),
92
+ ...options.spanAttributes,
93
+ };
94
+ if (!checkToolLevel || !mcpTool.toolName) {
95
+ // Server-only check
96
+ const result = await client.checkPermission(serverCheck, { spanAttributes });
97
+ return {
98
+ serverAllowed: result.allowed,
99
+ toolAllowed: true,
100
+ allowed: result.allowed,
101
+ checkedAt: result.checkedAt,
102
+ mcpTool,
103
+ };
104
+ }
105
+ // Batch check: server + tool
106
+ const toolCheck = {
107
+ namespace: toolNamespace,
108
+ object: `${mcpTool.serverName}:${mcpTool.toolName}`,
109
+ relation: "invoke",
110
+ ...options.subject,
111
+ };
112
+ const batchResult = await client.batchCheckPermissions([serverCheck, toolCheck], { spanAttributes });
113
+ const serverAllowed = batchResult.results[0]?.allowed ?? false;
114
+ const toolAllowed = batchResult.results[1]?.allowed ?? false;
115
+ return {
116
+ serverAllowed,
117
+ toolAllowed,
118
+ allowed: serverAllowed && toolAllowed,
119
+ checkedAt: batchResult.checkedAt,
120
+ mcpTool,
121
+ };
122
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Span exporter contract.
3
+ *
4
+ * Implementations ship completed TraceSpan records to an external trace
5
+ * sink (OTLP collector, vendor backend, in-process pipeline, etc.).
6
+ *
7
+ * Exporters must never throw. Transport failures, malformed responses,
8
+ * and shutdown races are all swallowed and logged through whatever
9
+ * channel the implementation chooses.
10
+ */
11
+ import type { TraceSpan } from "../tracer.js";
12
+ export interface SpanExporter {
13
+ /** Best-effort export of one or more spans. Must never throw. */
14
+ export(spans: ReadonlyArray<TraceSpan>): Promise<void>;
15
+ /** Flush any pending state and release resources. Must never throw. */
16
+ shutdown(): Promise<void>;
17
+ }