@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.
- package/README.md +134 -0
- package/assets/commands/local-down.md +19 -0
- package/assets/commands/local-up.md +27 -0
- package/assets/skills/auth-setup/SKILL.md +279 -0
- package/assets/skills/local-dev/SKILL.md +206 -0
- package/assets/skills/login-flow/SKILL.md +383 -0
- package/assets/skills/social-login/SKILL.md +312 -0
- package/dist/agent-auth.d.ts +204 -0
- package/dist/agent-auth.js +553 -0
- package/dist/auth-gate.d.ts +71 -0
- package/dist/auth-gate.js +308 -0
- package/dist/auth-store.d.ts +75 -0
- package/dist/auth-store.js +261 -0
- package/dist/auth.d.ts +93 -0
- package/dist/auth.js +323 -0
- package/dist/cli.d.ts +73 -0
- package/dist/cli.js +484 -0
- package/dist/client.d.ts +158 -0
- package/dist/client.js +679 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.js +344 -0
- package/dist/denial.d.ts +79 -0
- package/dist/denial.js +103 -0
- package/dist/dev.d.ts +95 -0
- package/dist/dev.js +514 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +137 -0
- package/dist/local/cli.d.ts +12 -0
- package/dist/local/cli.js +95 -0
- package/dist/local/configs.d.ts +89 -0
- package/dist/local/configs.js +634 -0
- package/dist/local/health.d.ts +32 -0
- package/dist/local/health.js +65 -0
- package/dist/local/index.d.ts +6 -0
- package/dist/local/index.js +38 -0
- package/dist/local/jaeger-main.d.ts +13 -0
- package/dist/local/jaeger-main.js +85 -0
- package/dist/local/jaeger.d.ts +50 -0
- package/dist/local/jaeger.js +162 -0
- package/dist/local/main.d.ts +7 -0
- package/dist/local/main.js +14 -0
- package/dist/local/manager.d.ts +45 -0
- package/dist/local/manager.js +676 -0
- package/dist/local/seed.d.ts +71 -0
- package/dist/local/seed.js +237 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.js +139 -0
- package/dist/mcp.d.ts +76 -0
- package/dist/mcp.js +122 -0
- package/dist/otel/exporter.d.ts +17 -0
- package/dist/otel/exporter.js +12 -0
- package/dist/otel/index.d.ts +2 -0
- package/dist/otel/index.js +8 -0
- package/dist/otel/otlp-http.d.ts +116 -0
- package/dist/otel/otlp-http.js +322 -0
- package/dist/registry/cli.d.ts +12 -0
- package/dist/registry/cli.js +76 -0
- package/dist/registry/config.d.ts +23 -0
- package/dist/registry/config.js +80 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +21 -0
- package/dist/registry/main.d.ts +7 -0
- package/dist/registry/main.js +14 -0
- package/dist/registry/manager.d.ts +38 -0
- package/dist/registry/manager.js +674 -0
- package/dist/setup.d.ts +118 -0
- package/dist/setup.js +398 -0
- package/dist/skills.d.ts +78 -0
- package/dist/skills.js +264 -0
- package/dist/subject.d.ts +43 -0
- package/dist/subject.js +55 -0
- package/dist/tool-metadata.d.ts +41 -0
- package/dist/tool-metadata.js +127 -0
- package/dist/tracer.d.ts +172 -0
- package/dist/tracer.js +452 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +3 -0
- package/dist/watch-sandbox.d.ts +9 -0
- package/dist/watch-sandbox.js +81 -0
- 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
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -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
|
+
}
|