@ory/argus 0.2.0 → 0.3.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/dist/client.js +5 -0
- package/dist/dev.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/local/cli.js +1 -1
- package/dist/local/manager.js +1 -1
- package/dist/local/seed.js +3 -3
- package/dist/permissions-cli.js +59 -17
- package/dist/permissions.d.ts +23 -0
- package/dist/permissions.js +28 -8
- package/dist/tracer.d.ts +18 -0
- package/dist/tracer.js +31 -2
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -38,6 +38,11 @@ class OryAgentClient {
|
|
|
38
38
|
traceFile: config.traceFile,
|
|
39
39
|
...(config.exporter ? { exporter: config.exporter } : {}),
|
|
40
40
|
});
|
|
41
|
+
// Every span — including direct `tracer.record(...)` calls from
|
|
42
|
+
// plugin handlers — gets userSubject / agentSubject merged in, so
|
|
43
|
+
// tool.invoke / tool.block / permission.observe_deny spans carry
|
|
44
|
+
// the same principal attribution as the API-call spans.
|
|
45
|
+
this.tracer.setAttributeEnricher(() => this.principalSpanAttributes());
|
|
41
46
|
// Seed agent principal from the constructor `apiKey` so existing
|
|
42
47
|
// call sites keep working until they migrate to ensureAgentIdentity.
|
|
43
48
|
if (config.apiKey)
|
package/dist/dev.js
CHANGED
|
@@ -485,7 +485,7 @@ async function bootstrapLocalOry(localDir) {
|
|
|
485
485
|
return { active: false };
|
|
486
486
|
}
|
|
487
487
|
console.log(`[ory-dev] Local Ory: agent ${seed.agent.identity.email}, user ${seed.user.identity.email}, ` +
|
|
488
|
-
`${seed.permissions.tuples}
|
|
488
|
+
`${seed.permissions.tuples} permissions in '${namespace}' for ${seed.permissions.subject}.`);
|
|
489
489
|
return { active: true, gatewayUrl, seed };
|
|
490
490
|
}
|
|
491
491
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { OryAgentClient, type OryAgentConfig, type PrincipalIdentity, } from "./client.js";
|
|
2
2
|
export { DebugLogger, redactLogData, type LogEntry, type LogLevel, } from "./logger.js";
|
|
3
|
-
export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type TraceEvent, type SpanStatus, type TraceSpan, type SpanOptions, type TracerOptions, type TracerContext, } from "./tracer.js";
|
|
3
|
+
export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type TraceEvent, type SpanStatus, type TraceSpan, type SpanOptions, type TracerOptions, type TracerContext, type SpanAttributeEnricher, } from "./tracer.js";
|
|
4
4
|
export { loadConfig, saveConfig, resolveConfig, mutateConfig, getConfigPath, getDataDir, getHarnessDataDir, type OryPluginConfig, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, type PermissionMode, } from "./config.js";
|
|
5
5
|
export { pkceLogin, refreshAccessToken, detectHeadless, generateCodeVerifier, sha256Base64Url, buildAuthorizeUrl, LOOPBACK_PORTS, DEFAULT_LOGIN_TIMEOUT_MS, type PkceLoginOptions, type PkceLoginOutcome, type PkceDeclineReason, } from "./auth.js";
|
|
6
6
|
export { loadTokens, saveTokens, clearTokens, isExpired, refreshAndSave, tryAcquirePkceFlightLock, clearPkceFlightLock, waitForPeerTokens, waitForPeerTokensSync, TOKEN_EXPIRY_SKEW_SEC, type PkceFlightLock, } from "./auth-store.js";
|
|
@@ -14,7 +14,7 @@ export { runDevLauncher, type DevLauncherConfig, type InstallContext, } from "./
|
|
|
14
14
|
export { renderOrySkills, renderOryCommands, commandToSkill, commandToToml, commandToFrontmatterMarkdown, commandToPlainMarkdown, toSkillMarkdown, writeSkillTree, removeSkillDirs, ORY_SKILL_NAMES, ORY_COMMAND_SKILL_NAMES, ORY_COMMAND_SLUGS, type RenderedSkill, type RenderedCommand, type RenderProfileOptions, } from "./skills.js";
|
|
15
15
|
export { runLocalCommand, ensureDevJaeger, stopDevJaeger, DEV_JAEGER_CONTAINER, type EnsureDevJaegerResult, type StopDevJaegerResult, } from "./local/index.js";
|
|
16
16
|
export { runRegistryCommand } from "./registry/index.js";
|
|
17
|
-
export { checkAndDecide, applyPermissionMode, type PermissionDecision, type ModeDecision, type CheckAndDecideOptions, type ApplyPermissionModeContext, } from "./permissions.js";
|
|
17
|
+
export { checkAndDecide, applyPermissionMode, type PermissionDecision, type ModeDecision, type DecisionSpanAttributes, type CheckAndDecideOptions, type ApplyPermissionModeContext, } from "./permissions.js";
|
|
18
18
|
export { HARNESS_TOOL_CATALOG, KNOWN_HARNESSES, ALL_TOOLS, getToolCatalog, type KnownHarness, } from "./tool-catalog.js";
|
|
19
19
|
export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkMcpPermission, type McpToolIdentifier, type McpPermissionCheckOptions, type McpPermissionResult, } from "./mcp.js";
|
|
20
20
|
export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
|
package/dist/local/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ Commands:
|
|
|
63
63
|
up [--no-seed] Start local Ory services (Kratos, Keto, Hydra, gateway)
|
|
64
64
|
down Stop all local services (preserves data)
|
|
65
65
|
status Show service health and container states
|
|
66
|
-
seed Create test identity, session, and
|
|
66
|
+
seed Create test identity, session, and permissions
|
|
67
67
|
logs [service] [-f] View service logs (optionally follow, optionally filter)
|
|
68
68
|
reset Stop services and remove all data
|
|
69
69
|
env Print environment variables for connecting to local services
|
package/dist/local/manager.js
CHANGED
|
@@ -642,7 +642,7 @@ function printSeedResult(result) {
|
|
|
642
642
|
console.log(` User password: ${result.user.password}`);
|
|
643
643
|
console.log(` User OAuth2: ${result.user.client.clientId} (PKCE)`);
|
|
644
644
|
console.log("");
|
|
645
|
-
console.log(` Permissions: ${result.permissions.tuples}
|
|
645
|
+
console.log(` Permissions: ${result.permissions.tuples} entries in '${result.permissions.namespace}' for ${result.permissions.subject}`);
|
|
646
646
|
console.log("");
|
|
647
647
|
console.log("To use with any Ory agent plugin, set these environment variables:");
|
|
648
648
|
console.log("");
|
package/dist/local/seed.js
CHANGED
|
@@ -110,7 +110,7 @@ async function seedPermissions(userId, namespace) {
|
|
|
110
110
|
catch (err) {
|
|
111
111
|
const msg = err instanceof Error ? err.message : String(err);
|
|
112
112
|
if (!msg.includes("409")) {
|
|
113
|
-
process.stderr.write(` Warning: failed to create
|
|
113
|
+
process.stderr.write(` Warning: failed to create permission for ${tool}: ${msg}\n`);
|
|
114
114
|
}
|
|
115
115
|
else {
|
|
116
116
|
created++;
|
|
@@ -185,9 +185,9 @@ async function seedLocalEnvironment(namespace = "AgentTools") {
|
|
|
185
185
|
// Subject is a SubjectSet `User:<id>` so the Console UI can rewrite
|
|
186
186
|
// the same tuple shape by hand during the runbook demo.
|
|
187
187
|
const subjectLabel = `${exports.USER_SUBJECT_NAMESPACE}:${userIdentity.id}`;
|
|
188
|
-
process.stderr.write(` Creating
|
|
188
|
+
process.stderr.write(` Creating permissions for ${COMMON_TOOLS.length} tools...\n`);
|
|
189
189
|
const tupleCount = await seedPermissions(userIdentity.id, namespace);
|
|
190
|
-
process.stderr.write(` Permissions: ${tupleCount}
|
|
190
|
+
process.stderr.write(` Permissions: ${tupleCount} entries in '${namespace}' for ${subjectLabel}\n`);
|
|
191
191
|
// 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
|
|
192
192
|
// PKCE flow has somewhere to authenticate against. The agent's
|
|
193
193
|
// OAuth2 client is intentionally NOT pre-registered; the harness
|
package/dist/permissions-cli.js
CHANGED
|
@@ -84,7 +84,7 @@ function printPermissionsHelp(binName) {
|
|
|
84
84
|
console.log("");
|
|
85
85
|
console.log("Subcommands:");
|
|
86
86
|
console.log(" status Show permission mode and tool coverage for the current user");
|
|
87
|
-
console.log(" bootstrap
|
|
87
|
+
console.log(" bootstrap Grant the current user 'use' on every built-in tool (idempotent)");
|
|
88
88
|
console.log(" observe Switch to observe mode (denies log but don't block)");
|
|
89
89
|
console.log(" enforce Switch to enforce mode (denies block — production posture)");
|
|
90
90
|
console.log("");
|
|
@@ -104,7 +104,7 @@ function runPermissionsSetMode(binName, mode) {
|
|
|
104
104
|
console.log("Denies will be logged and recorded as `permission.observe_deny` spans,");
|
|
105
105
|
console.log("but tools will be allowed through. Run");
|
|
106
106
|
console.log(` ${binName} permissions enforce`);
|
|
107
|
-
console.log("once your
|
|
107
|
+
console.log("once your permission set is correct.");
|
|
108
108
|
}
|
|
109
109
|
else {
|
|
110
110
|
console.log("Denies will block tool execution. The plugin is now enforcing.");
|
|
@@ -121,9 +121,10 @@ async function runPermissionsStatus(binName, harness) {
|
|
|
121
121
|
console.log(`Permission status (${harness})`);
|
|
122
122
|
console.log("==========================================");
|
|
123
123
|
console.log("");
|
|
124
|
-
console.log(`Mode: ${resolved.permissionMode}
|
|
124
|
+
console.log(`Mode: ${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`);
|
|
125
|
+
console.log(` (other modes: ${otherModes(resolved.permissionMode).join(", ")})`);
|
|
125
126
|
console.log(`Namespace: ${namespace}`);
|
|
126
|
-
console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"}
|
|
127
|
+
console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"}${formatSourceSuffix(resolved.projectUrlSource)}`);
|
|
127
128
|
console.log(`Audit-only: ${resolved.auditOnly ? "yes (Ory disabled)" : "no"}`);
|
|
128
129
|
console.log("");
|
|
129
130
|
if (resolved.auditOnly) {
|
|
@@ -132,7 +133,7 @@ async function runPermissionsStatus(binName, harness) {
|
|
|
132
133
|
return 0;
|
|
133
134
|
}
|
|
134
135
|
if (!resolved.projectUrl) {
|
|
135
|
-
console.log("No project URL configured — cannot probe
|
|
136
|
+
console.log("No project URL configured — cannot probe permission coverage.");
|
|
136
137
|
console.log(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
|
|
137
138
|
return 0;
|
|
138
139
|
}
|
|
@@ -214,7 +215,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
214
215
|
return 1;
|
|
215
216
|
}
|
|
216
217
|
if (!resolved.projectUrl) {
|
|
217
|
-
console.error("No ORY_PROJECT_URL configured — cannot write
|
|
218
|
+
console.error("No ORY_PROJECT_URL configured — cannot write permissions.");
|
|
218
219
|
console.error(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
|
|
219
220
|
return 1;
|
|
220
221
|
}
|
|
@@ -231,14 +232,14 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
231
232
|
const subject = (0, subject_js_1.resolveUserSubject)(client);
|
|
232
233
|
const subjectId = (0, subject_js_1.subjectLabel)(subject);
|
|
233
234
|
if (subjectId === "agent:unknown") {
|
|
234
|
-
console.error("No user identity resolved — refusing to write
|
|
235
|
+
console.error("No user identity resolved — refusing to write permissions for an unknown subject.");
|
|
235
236
|
console.error("");
|
|
236
237
|
console.error("Either:");
|
|
237
238
|
console.error(" - run the harness once with ORY_AUTH_GATE=1 so a user token is cached, or");
|
|
238
239
|
console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
|
|
239
240
|
return 1;
|
|
240
241
|
}
|
|
241
|
-
console.log(`Bootstrapping ${catalog.length}
|
|
242
|
+
console.log(`Bootstrapping ${catalog.length} permissions for ${harness}`);
|
|
242
243
|
console.log("==========================================");
|
|
243
244
|
console.log("");
|
|
244
245
|
console.log(`Namespace: ${namespace}`);
|
|
@@ -246,7 +247,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
246
247
|
console.log(`Tools: ${catalog.join(", ")}`);
|
|
247
248
|
if (dryRun) {
|
|
248
249
|
console.log("");
|
|
249
|
-
console.log("(--dry-run) No
|
|
250
|
+
console.log("(--dry-run) No permissions will be written.");
|
|
250
251
|
}
|
|
251
252
|
console.log("");
|
|
252
253
|
let created = 0;
|
|
@@ -285,7 +286,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
285
286
|
}
|
|
286
287
|
console.log("");
|
|
287
288
|
if (dryRun) {
|
|
288
|
-
console.log(`Dry run: ${catalog.length}
|
|
289
|
+
console.log(`Dry run: ${catalog.length} permissions would be written.`);
|
|
289
290
|
return 0;
|
|
290
291
|
}
|
|
291
292
|
console.log(`Created: ${created} Already existed: ${existed} Failed: ${failures.length}`);
|
|
@@ -296,7 +297,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
296
297
|
console.log("");
|
|
297
298
|
console.log("Some writes were rejected — your current credentials may lack");
|
|
298
299
|
console.log("write scope on the permission namespace. You can apply the");
|
|
299
|
-
console.log("
|
|
300
|
+
console.log("permissions manually using the snippet below (one per line):");
|
|
300
301
|
console.log("");
|
|
301
302
|
for (const tool of catalog) {
|
|
302
303
|
console.log(` ${namespace}:${tool}#use@${subjectId}`);
|
|
@@ -309,7 +310,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
309
310
|
return failures.length === catalog.length ? 1 : 0;
|
|
310
311
|
}
|
|
311
312
|
console.log("");
|
|
312
|
-
console.log(`
|
|
313
|
+
console.log(`Permissions are in place. Promote to enforce mode when ready:`);
|
|
313
314
|
console.log(` ${binName} permissions enforce`);
|
|
314
315
|
return 0;
|
|
315
316
|
}
|
|
@@ -342,21 +343,22 @@ function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
|
|
|
342
343
|
const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
|
|
343
344
|
console.log("");
|
|
344
345
|
console.log("Permissions:");
|
|
345
|
-
console.log(` Mode: ${resolved.permissionMode}
|
|
346
|
+
console.log(` Mode: ${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`);
|
|
347
|
+
console.log(` Other modes: ${otherModes(resolved.permissionMode).join(", ")}`);
|
|
346
348
|
if (resolved.auditOnly) {
|
|
347
349
|
console.log(" Ory is in audit-only mode (kill switch). Permission checks are disabled.");
|
|
348
350
|
return;
|
|
349
351
|
}
|
|
350
352
|
if (resolved.permissionMode === "observe") {
|
|
351
353
|
console.log(" Observe mode: permission denies are logged + traced, but tools still run.");
|
|
352
|
-
console.log(" This is the safe default — promote once your
|
|
354
|
+
console.log(" This is the safe default — promote to 'enforce' once your permission set is correct.");
|
|
353
355
|
}
|
|
354
356
|
else {
|
|
355
357
|
console.log(" Enforce mode: permission denies block tool execution.");
|
|
356
358
|
}
|
|
357
359
|
if (opts.bootstrappedAutomatically) {
|
|
358
360
|
console.log("");
|
|
359
|
-
console.log(`
|
|
361
|
+
console.log(` Permissions bootstrapped for ${catalog.length} built-in tools.`);
|
|
360
362
|
}
|
|
361
363
|
else if (catalog.length > 0) {
|
|
362
364
|
console.log("");
|
|
@@ -364,12 +366,52 @@ function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
|
|
|
364
366
|
console.log(` npx ${binName} permissions bootstrap`);
|
|
365
367
|
}
|
|
366
368
|
console.log("");
|
|
367
|
-
console.log(" Inspect
|
|
369
|
+
console.log(" Inspect permission coverage:");
|
|
368
370
|
console.log(` npx ${binName} permissions status`);
|
|
369
371
|
console.log("");
|
|
370
372
|
console.log(" Promote to enforcing once observe-mode logs look right:");
|
|
371
373
|
console.log(` npx ${binName} permissions enforce`);
|
|
372
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Render the trailing source annotation on a Mode line. Stays silent
|
|
377
|
+
* when the mode came from the built-in default, since saying "default"
|
|
378
|
+
* just adds noise. When the mode was overridden by env or config, name
|
|
379
|
+
* the override so the reader knows where to look.
|
|
380
|
+
*/
|
|
381
|
+
function formatModeSource(source) {
|
|
382
|
+
switch (source) {
|
|
383
|
+
case "env":
|
|
384
|
+
return ` (set via ${ENV_PERMISSION_MODE} env var)`;
|
|
385
|
+
case "config":
|
|
386
|
+
return " (saved in config; change with: permissions observe|enforce)";
|
|
387
|
+
case "default":
|
|
388
|
+
return " (built-in default)";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Generic source-suffix helper for other config fields (project URL,
|
|
393
|
+
* API key). Source 'none' means the value is unset — don't add a suffix.
|
|
394
|
+
*/
|
|
395
|
+
function formatSourceSuffix(source) {
|
|
396
|
+
switch (source) {
|
|
397
|
+
case "env":
|
|
398
|
+
return " (from env)";
|
|
399
|
+
case "config":
|
|
400
|
+
return " (from config)";
|
|
401
|
+
case "none":
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Lists the modes other than the active one, in stable order, so the
|
|
407
|
+
* banner can hint "Other modes: …" without re-listing the active one.
|
|
408
|
+
* Audit-only is a separate kill switch (config.auditOnly), but we still
|
|
409
|
+
* mention it here so first-time readers learn the full set.
|
|
410
|
+
*/
|
|
411
|
+
function otherModes(active) {
|
|
412
|
+
const all = ["observe", "enforce", "audit-only"];
|
|
413
|
+
return all.filter((m) => m !== active);
|
|
414
|
+
}
|
|
373
415
|
/**
|
|
374
416
|
* If a user identity is already cached and the plugin is configured,
|
|
375
417
|
* opportunistically run `permissions bootstrap` so the user lands in a
|
|
@@ -391,7 +433,7 @@ async function maybeAutoBootstrap(binName, harness) {
|
|
|
391
433
|
return false;
|
|
392
434
|
try {
|
|
393
435
|
console.log("");
|
|
394
|
-
console.log("Bootstrapping
|
|
436
|
+
console.log("Bootstrapping permissions (cached user identity detected)...");
|
|
395
437
|
const code = await runPermissionsCommand(binName, harness, ["bootstrap"]);
|
|
396
438
|
return code === 0;
|
|
397
439
|
}
|
package/dist/permissions.d.ts
CHANGED
|
@@ -23,22 +23,40 @@
|
|
|
23
23
|
import type { OryAgentClient } from "./client.js";
|
|
24
24
|
import { type PermissionMode } from "./config.js";
|
|
25
25
|
import type { OryError, PermissionCheck, PermissionResult } from "./types.js";
|
|
26
|
+
/**
|
|
27
|
+
* Attributes describing *what was checked* and *under which posture*.
|
|
28
|
+
* Plugins spread this onto their `tool.invoke` / `tool.block` spans so
|
|
29
|
+
* the audit trail makes the observe-vs-enforce posture and the checked
|
|
30
|
+
* subject visible at a glance — without that, a span with `allowed=false`
|
|
31
|
+
* status=ok is ambiguous (observe pass-through? fail-open? bug?).
|
|
32
|
+
*/
|
|
33
|
+
export interface DecisionSpanAttributes {
|
|
34
|
+
permissionMode: PermissionMode;
|
|
35
|
+
/** Direct subject ID when the check used `subjectId`. */
|
|
36
|
+
subjectId?: string;
|
|
37
|
+
/** SubjectSet rendered as `<namespace>:<object>#<relation>` when used. */
|
|
38
|
+
subjectSet?: string;
|
|
39
|
+
}
|
|
26
40
|
export type PermissionDecision = {
|
|
27
41
|
kind: "allow";
|
|
28
42
|
result: PermissionResult;
|
|
29
43
|
mode: PermissionMode;
|
|
44
|
+
spanAttributes: DecisionSpanAttributes;
|
|
30
45
|
} | {
|
|
31
46
|
kind: "deny";
|
|
32
47
|
result: PermissionResult;
|
|
33
48
|
mode: "enforce";
|
|
49
|
+
spanAttributes: DecisionSpanAttributes;
|
|
34
50
|
} | {
|
|
35
51
|
kind: "observe";
|
|
36
52
|
result: PermissionResult;
|
|
37
53
|
mode: "observe";
|
|
54
|
+
spanAttributes: DecisionSpanAttributes;
|
|
38
55
|
} | {
|
|
39
56
|
kind: "fail_open";
|
|
40
57
|
error: OryError;
|
|
41
58
|
mode: PermissionMode;
|
|
59
|
+
spanAttributes: DecisionSpanAttributes;
|
|
42
60
|
};
|
|
43
61
|
/**
|
|
44
62
|
* The "what should the caller do?" half of a decision, independent of
|
|
@@ -49,12 +67,15 @@ export type PermissionDecision = {
|
|
|
49
67
|
export type ModeDecision = {
|
|
50
68
|
kind: "allow";
|
|
51
69
|
mode: PermissionMode;
|
|
70
|
+
spanAttributes: DecisionSpanAttributes;
|
|
52
71
|
} | {
|
|
53
72
|
kind: "deny";
|
|
54
73
|
mode: "enforce";
|
|
74
|
+
spanAttributes: DecisionSpanAttributes;
|
|
55
75
|
} | {
|
|
56
76
|
kind: "observe";
|
|
57
77
|
mode: "observe";
|
|
78
|
+
spanAttributes: DecisionSpanAttributes;
|
|
58
79
|
};
|
|
59
80
|
export interface CheckAndDecideOptions {
|
|
60
81
|
/**
|
|
@@ -81,6 +102,8 @@ export interface ApplyPermissionModeContext {
|
|
|
81
102
|
relation?: string;
|
|
82
103
|
/** Subject ID for the observe-deny log line, if available. */
|
|
83
104
|
subjectId?: string;
|
|
105
|
+
/** SubjectSet for the observe-deny log line, if available. */
|
|
106
|
+
subjectSet?: PermissionCheck["subjectSet"];
|
|
84
107
|
/** Attributes merged into the `permission.observe_deny` span. */
|
|
85
108
|
spanAttributes?: Record<string, unknown>;
|
|
86
109
|
/** Override the resolved mode (tests). */
|
package/dist/permissions.js
CHANGED
|
@@ -25,6 +25,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
25
25
|
exports.applyPermissionMode = applyPermissionMode;
|
|
26
26
|
exports.checkAndDecide = checkAndDecide;
|
|
27
27
|
const config_js_1 = require("./config.js");
|
|
28
|
+
function formatSubjectSet(set) {
|
|
29
|
+
if (!set)
|
|
30
|
+
return undefined;
|
|
31
|
+
return `${set.namespace}:${set.object}#${set.relation}`;
|
|
32
|
+
}
|
|
33
|
+
function buildDecisionAttributes(mode, subjectId, subjectSet) {
|
|
34
|
+
const set = formatSubjectSet(subjectSet);
|
|
35
|
+
// SubjectSet supersedes subjectId in the span (the printable label is
|
|
36
|
+
// derivable from it); avoid emitting both so consumers don't need to
|
|
37
|
+
// dedupe.
|
|
38
|
+
if (set)
|
|
39
|
+
return { permissionMode: mode, subjectSet: set };
|
|
40
|
+
if (subjectId)
|
|
41
|
+
return { permissionMode: mode, subjectId };
|
|
42
|
+
return { permissionMode: mode };
|
|
43
|
+
}
|
|
28
44
|
/**
|
|
29
45
|
* Map an `allowed` boolean from any permission check (plain or MCP)
|
|
30
46
|
* onto a {@link ModeDecision}. When `allowed === false` and the mode
|
|
@@ -34,14 +50,16 @@ const config_js_1 = require("./config.js");
|
|
|
34
50
|
*/
|
|
35
51
|
function applyPermissionMode(client, allowed, context = {}) {
|
|
36
52
|
const mode = context.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
|
|
53
|
+
const spanAttributes = buildDecisionAttributes(mode, context.subjectId, context.subjectSet);
|
|
37
54
|
if (allowed)
|
|
38
|
-
return { kind: "allow", mode };
|
|
55
|
+
return { kind: "allow", mode, spanAttributes };
|
|
39
56
|
if (mode === "observe") {
|
|
40
57
|
client.logger.warn("permission.observe_deny", {
|
|
41
58
|
namespace: context.namespace,
|
|
42
59
|
object: context.object,
|
|
43
60
|
relation: context.relation,
|
|
44
61
|
subjectId: context.subjectId,
|
|
62
|
+
subjectSet: spanAttributes.subjectSet,
|
|
45
63
|
note: "denied by Ory; allowed by plugin in observe mode",
|
|
46
64
|
});
|
|
47
65
|
client.tracer.record("permission.observe_deny", "denied", {
|
|
@@ -49,13 +67,13 @@ function applyPermissionMode(client, allowed, context = {}) {
|
|
|
49
67
|
namespace: context.namespace,
|
|
50
68
|
object: context.object,
|
|
51
69
|
relation: context.relation,
|
|
52
|
-
|
|
70
|
+
...spanAttributes,
|
|
53
71
|
...context.spanAttributes,
|
|
54
72
|
},
|
|
55
73
|
});
|
|
56
|
-
return { kind: "observe", mode: "observe" };
|
|
74
|
+
return { kind: "observe", mode: "observe", spanAttributes };
|
|
57
75
|
}
|
|
58
|
-
return { kind: "deny", mode: "enforce" };
|
|
76
|
+
return { kind: "deny", mode: "enforce", spanAttributes };
|
|
59
77
|
}
|
|
60
78
|
/**
|
|
61
79
|
* Run a permission check and resolve the configured mode against the
|
|
@@ -64,6 +82,7 @@ function applyPermissionMode(client, allowed, context = {}) {
|
|
|
64
82
|
*/
|
|
65
83
|
async function checkAndDecide(client, check, opts = {}) {
|
|
66
84
|
const mode = opts.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
|
|
85
|
+
const spanAttributes = buildDecisionAttributes(mode, check.subjectId, check.subjectSet);
|
|
67
86
|
let result;
|
|
68
87
|
try {
|
|
69
88
|
result = await client.checkPermission(check, {
|
|
@@ -71,19 +90,20 @@ async function checkAndDecide(client, check, opts = {}) {
|
|
|
71
90
|
});
|
|
72
91
|
}
|
|
73
92
|
catch (err) {
|
|
74
|
-
return { kind: "fail_open", error: err, mode };
|
|
93
|
+
return { kind: "fail_open", error: err, mode, spanAttributes };
|
|
75
94
|
}
|
|
76
95
|
const inner = applyPermissionMode(client, result.allowed, {
|
|
77
96
|
namespace: check.namespace,
|
|
78
97
|
object: check.object,
|
|
79
98
|
relation: check.relation,
|
|
80
99
|
subjectId: check.subjectId,
|
|
100
|
+
subjectSet: check.subjectSet,
|
|
81
101
|
spanAttributes: opts.spanAttributes,
|
|
82
102
|
modeOverride: opts.modeOverride,
|
|
83
103
|
});
|
|
84
104
|
if (inner.kind === "allow")
|
|
85
|
-
return { kind: "allow", result, mode: inner.mode };
|
|
105
|
+
return { kind: "allow", result, mode: inner.mode, spanAttributes: inner.spanAttributes };
|
|
86
106
|
if (inner.kind === "observe")
|
|
87
|
-
return { kind: "observe", result, mode: "observe" };
|
|
88
|
-
return { kind: "deny", result, mode: "enforce" };
|
|
107
|
+
return { kind: "observe", result, mode: "observe", spanAttributes: inner.spanAttributes };
|
|
108
|
+
return { kind: "deny", result, mode: "enforce", spanAttributes: inner.spanAttributes };
|
|
89
109
|
}
|
package/dist/tracer.d.ts
CHANGED
|
@@ -99,6 +99,14 @@ export interface ProcessMetadata {
|
|
|
99
99
|
gitBranch?: string;
|
|
100
100
|
gitCommit?: string;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Returns a record of attributes to merge into every span's attribute
|
|
104
|
+
* bag at span-end. Used by `OryAgentClient` to inject the current
|
|
105
|
+
* `userSubject` / `agentSubject` onto every span without each handler
|
|
106
|
+
* having to remember to attach them. Must not throw and should be cheap;
|
|
107
|
+
* it is invoked on every span.
|
|
108
|
+
*/
|
|
109
|
+
export type SpanAttributeEnricher = () => Record<string, unknown> | undefined;
|
|
102
110
|
export declare class Tracer extends EventEmitter {
|
|
103
111
|
readonly harness: string;
|
|
104
112
|
readonly processMetadata: ProcessMetadata;
|
|
@@ -106,6 +114,7 @@ export declare class Tracer extends EventEmitter {
|
|
|
106
114
|
private readonly _spans;
|
|
107
115
|
private _context;
|
|
108
116
|
private _exporter;
|
|
117
|
+
private _enricher;
|
|
109
118
|
constructor(opts: TracerOptions);
|
|
110
119
|
/** Backwards-compatible accessors for the most common metadata fields. */
|
|
111
120
|
get hostname(): string;
|
|
@@ -135,6 +144,15 @@ export declare class Tracer extends EventEmitter {
|
|
|
135
144
|
*/
|
|
136
145
|
setExporter(exporter: SpanExporter | null): void;
|
|
137
146
|
get exporter(): SpanExporter | null;
|
|
147
|
+
/**
|
|
148
|
+
* Install (or replace) an attribute enricher. The enricher is invoked
|
|
149
|
+
* on every span at end-time and its return value is merged into the
|
|
150
|
+
* span's attributes (with caller-supplied attributes taking precedence).
|
|
151
|
+
* Pass `null` to clear.
|
|
152
|
+
*/
|
|
153
|
+
setAttributeEnricher(enricher: SpanAttributeEnricher | null): void;
|
|
154
|
+
/** @internal — invoked by `ActiveSpan.end()`. Never throws. */
|
|
155
|
+
_callEnricher(): Record<string, unknown> | undefined;
|
|
138
156
|
/**
|
|
139
157
|
* Flush any pending exports. Call before the host process exits so
|
|
140
158
|
* the OTLP HTTP request has a chance to complete.
|
package/dist/tracer.js
CHANGED
|
@@ -98,8 +98,12 @@ class ActiveSpan {
|
|
|
98
98
|
}
|
|
99
99
|
this._ended = true;
|
|
100
100
|
const durationMs = Math.round(performance.now() - this.startMs);
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
// Pull enricher attributes (e.g. userSubject / agentSubject from the
|
|
102
|
+
// client's principals) at span-end so every span gets them, including
|
|
103
|
+
// direct `tracer.record(...)` calls from plugin handlers.
|
|
104
|
+
const enriched = this.tracer._callEnricher();
|
|
105
|
+
const merged = enriched || this.baseAttributes || attributes
|
|
106
|
+
? { ...enriched, ...this.baseAttributes, ...attributes }
|
|
103
107
|
: undefined;
|
|
104
108
|
const meta = this.tracer.processMetadata;
|
|
105
109
|
const span = {
|
|
@@ -137,6 +141,7 @@ class Tracer extends node_events_1.EventEmitter {
|
|
|
137
141
|
_spans = [];
|
|
138
142
|
_context = {};
|
|
139
143
|
_exporter;
|
|
144
|
+
_enricher = null;
|
|
140
145
|
constructor(opts) {
|
|
141
146
|
super();
|
|
142
147
|
this.harness = opts.harness;
|
|
@@ -212,6 +217,30 @@ class Tracer extends node_events_1.EventEmitter {
|
|
|
212
217
|
get exporter() {
|
|
213
218
|
return this._exporter;
|
|
214
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Install (or replace) an attribute enricher. The enricher is invoked
|
|
222
|
+
* on every span at end-time and its return value is merged into the
|
|
223
|
+
* span's attributes (with caller-supplied attributes taking precedence).
|
|
224
|
+
* Pass `null` to clear.
|
|
225
|
+
*/
|
|
226
|
+
setAttributeEnricher(enricher) {
|
|
227
|
+
this._enricher = enricher;
|
|
228
|
+
}
|
|
229
|
+
/** @internal — invoked by `ActiveSpan.end()`. Never throws. */
|
|
230
|
+
_callEnricher() {
|
|
231
|
+
if (!this._enricher)
|
|
232
|
+
return undefined;
|
|
233
|
+
try {
|
|
234
|
+
const out = this._enricher();
|
|
235
|
+
if (!out || Object.keys(out).length === 0)
|
|
236
|
+
return undefined;
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Enricher contract: never throw. Swallow defensively.
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
215
244
|
/**
|
|
216
245
|
* Flush any pending exports. Call before the host process exits so
|
|
217
246
|
* the OTLP HTTP request has a chance to complete.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/argus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|