@ory/argus 0.13.7 → 0.13.9

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.
@@ -43,6 +43,14 @@ export interface SessionStartOptions {
43
43
  * user gate still runs, refreshes tokens, and records the `user.auth` span.
44
44
  */
45
45
  export declare function sessionStart(client: OryAgentClient, opts: SessionStartOptions): Promise<SessionStartResult>;
46
+ /**
47
+ * Write the `user → agent` delegation tuple, at most once per install
48
+ * (see {@link writeDelegationOnce}). Exported so harness plugins that run
49
+ * their own session-start sequence (rather than {@link sessionStart}) share
50
+ * the same write-once semantics instead of re-issuing the write every
51
+ * session. No-op until both principals are populated.
52
+ */
53
+ export declare function writeUserDelegatesAgent(client: OryAgentClient): Promise<void>;
46
54
  export interface RegisterSubagentOptions {
47
55
  harness: string;
48
56
  subAgentType: string;
@@ -50,6 +58,14 @@ export interface RegisterSubagentOptions {
50
58
  subAgentGate?: typeof ensureSubAgentIdentity;
51
59
  }
52
60
  export declare function registerSubagent(client: OryAgentClient, opts: RegisterSubagentOptions): Promise<void>;
61
+ /**
62
+ * Write the `agent → subagent` delegation tuple, at most once per install
63
+ * (see {@link writeDelegationOnce}). Exported so harness plugins that
64
+ * resolve the sub-agent identity through their own event plumbing (rather
65
+ * than {@link registerSubagent}) still share the write-once semantics.
66
+ * No-op until the agent principal is populated.
67
+ */
68
+ export declare function writeAgentDelegatesSubagent(client: OryAgentClient, subAgentSubject: string, subAgentType: string): Promise<void>;
53
69
  export interface GateResult {
54
70
  /** False only when the tool was hard-denied in enforce mode. */
55
71
  proceed: boolean;
package/dist/adapters.js CHANGED
@@ -21,7 +21,9 @@
21
21
  Object.defineProperty(exports, "__esModule", { value: true });
22
22
  exports.resolveNamespace = resolveNamespace;
23
23
  exports.sessionStart = sessionStart;
24
+ exports.writeUserDelegatesAgent = writeUserDelegatesAgent;
24
25
  exports.registerSubagent = registerSubagent;
26
+ exports.writeAgentDelegatesSubagent = writeAgentDelegatesSubagent;
25
27
  exports.gate = gate;
26
28
  exports.complete = complete;
27
29
  exports.wrapTool = wrapTool;
@@ -52,7 +54,7 @@ async function sessionStart(client, opts) {
52
54
  projectUrl,
53
55
  harness: opts.harness,
54
56
  });
55
- await recordUserDelegatesAgent(client);
57
+ await writeUserDelegatesAgent(client);
56
58
  return {
57
59
  proceed: decision.proceed,
58
60
  userMode: decision.mode,
@@ -60,25 +62,63 @@ async function sessionStart(client, opts) {
60
62
  agentKind: creds.kind,
61
63
  };
62
64
  }
63
- async function recordUserDelegatesAgent(client) {
64
- const user = client.userPrincipal.subject;
65
- const agent = client.agentPrincipal.subject;
66
- if (!user || !agent)
67
- return;
65
+ /**
66
+ * Write a delegation tuple at most once. The tuple is stable for a given
67
+ * (namespace, object, subject), and Keto's create API is not idempotent, so
68
+ * this goes through {@link OryAgentClient.ensureRelationship}: it reads the
69
+ * exact tuple first and only writes when absent — no duplicate on repeated
70
+ * session starts, and self-healing if the tuple was deleted. If the existence
71
+ * probe fails we skip the write rather than risk a duplicate (delegation
72
+ * tuples are audit-only, so a missed write is harmless and retried next
73
+ * session). Best-effort: any failure is logged and swallowed.
74
+ */
75
+ async function writeDelegationOnce(client, check, options) {
68
76
  try {
69
- await client.createRelationship({
70
- namespace: resolveNamespace(),
71
- object: `agent:${agent}`,
72
- relation: "delegate",
73
- subjectId: `user:${user}`,
74
- }, { spanAttributes: { delegation: "user-to-agent" } });
77
+ const res = await client.ensureRelationship(check, {
78
+ spanAttributes: options.spanAttributes,
79
+ });
80
+ if (res.probeError) {
81
+ client.logger.debug("delegation.probe_failed", {
82
+ ...options.failedContext,
83
+ message: res.probeError.message,
84
+ });
85
+ }
75
86
  }
76
87
  catch (err) {
77
- client.logger.warn("delegation.user_to_agent.failed", {
88
+ client.logger.warn(options.failedEvent, {
89
+ ...options.failedContext,
78
90
  message: err instanceof Error ? err.message : String(err),
79
91
  });
80
92
  }
81
93
  }
94
+ /**
95
+ * Write the `user → agent` delegation tuple, at most once per install
96
+ * (see {@link writeDelegationOnce}). Exported so harness plugins that run
97
+ * their own session-start sequence (rather than {@link sessionStart}) share
98
+ * the same write-once semantics instead of re-issuing the write every
99
+ * session. No-op until both principals are populated.
100
+ */
101
+ async function writeUserDelegatesAgent(client) {
102
+ const user = client.userPrincipal.subject;
103
+ const agent = client.agentPrincipal.subject;
104
+ if (!user || !agent) {
105
+ client.logger.debug("delegation.skip", {
106
+ reason: "missing principal",
107
+ hasUser: !!user,
108
+ hasAgent: !!agent,
109
+ });
110
+ return;
111
+ }
112
+ await writeDelegationOnce(client, {
113
+ namespace: resolveNamespace(),
114
+ object: `agent:${agent}`,
115
+ relation: "delegate",
116
+ subjectId: `user:${user}`,
117
+ }, {
118
+ spanAttributes: { delegation: "user-to-agent" },
119
+ failedEvent: "delegation.user_to_agent.failed",
120
+ });
121
+ }
82
122
  async function registerSubagent(client, opts) {
83
123
  const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
84
124
  const subAgentGate = opts.subAgentGate ?? agent_auth_js_1.ensureSubAgentIdentity;
@@ -99,23 +139,34 @@ async function registerSubagent(client, opts) {
99
139
  }
100
140
  if (identity.kind !== "dynamic" || !identity.subject)
101
141
  return;
142
+ await writeAgentDelegatesSubagent(client, identity.subject, opts.subAgentType);
143
+ }
144
+ /**
145
+ * Write the `agent → subagent` delegation tuple, at most once per install
146
+ * (see {@link writeDelegationOnce}). Exported so harness plugins that
147
+ * resolve the sub-agent identity through their own event plumbing (rather
148
+ * than {@link registerSubagent}) still share the write-once semantics.
149
+ * No-op until the agent principal is populated.
150
+ */
151
+ async function writeAgentDelegatesSubagent(client, subAgentSubject, subAgentType) {
102
152
  const agent = client.agentPrincipal.subject;
103
- if (!agent)
104
- return;
105
- try {
106
- await client.createRelationship({
107
- namespace: resolveNamespace(),
108
- object: `subagent:${identity.subject}`,
109
- relation: "delegate",
110
- subjectId: `agent:${agent}`,
111
- }, { spanAttributes: { delegation: "agent-to-subagent", subAgentType: opts.subAgentType } });
112
- }
113
- catch (err) {
114
- client.logger.warn("delegation.agent_to_subagent.failed", {
115
- subAgentType: opts.subAgentType,
116
- message: err instanceof Error ? err.message : String(err),
153
+ if (!agent) {
154
+ client.logger.debug("delegation.skip", {
155
+ reason: "agent principal not populated",
156
+ subAgentType,
117
157
  });
158
+ return;
118
159
  }
160
+ await writeDelegationOnce(client, {
161
+ namespace: resolveNamespace(),
162
+ object: `subagent:${subAgentSubject}`,
163
+ relation: "delegate",
164
+ subjectId: `agent:${agent}`,
165
+ }, {
166
+ spanAttributes: { delegation: "agent-to-subagent", subAgentType },
167
+ failedEvent: "delegation.agent_to_subagent.failed",
168
+ failedContext: { subAgentType },
169
+ });
119
170
  }
120
171
  /**
121
172
  * Authorize a tool call, record the spans, and return a normalized {@link GateResult}.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "badfe6ff1166227b0eefaeb54018482631e0ce7b",
4
- "commitShort": "badfe6f",
3
+ "commit": "f7663bf2a792378be5a94c7d6bb0d6671e1da6a9",
4
+ "commitShort": "f7663bf",
5
5
  "branch": "main",
6
- "commitDate": "2026-07-15T11:49:39-07:00",
6
+ "commitDate": "2026-07-15T15:56:35-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-07-15T18:53:45.680Z"
8
+ "builtAt": "2026-07-15T23:00:39.070Z"
9
9
  }
package/dist/client.d.ts CHANGED
@@ -159,6 +159,37 @@ export declare class OryAgentClient {
159
159
  created: boolean;
160
160
  alreadyExisted: boolean;
161
161
  }>;
162
+ /**
163
+ * True if the *exact* relation tuple already exists in Keto. Queries the
164
+ * relation-tuple listing API filtered by the tuple's own coordinates
165
+ * (namespace/object/relation + subject), so — unlike `checkPermission` —
166
+ * it does not follow subject-set expansion: it answers "is this literal
167
+ * tuple stored?", not "does this permission resolve?". Throws a classified
168
+ * {@link OryError} if the query itself fails.
169
+ */
170
+ relationshipExists(check: PermissionCheck): Promise<boolean>;
171
+ /**
172
+ * Idempotent relationship write. Keto's create API is **not** idempotent —
173
+ * `PUT /admin/relation-tuples` inserts a fresh row on every call (each with
174
+ * a new primary key) and never returns 409, so calling `createRelationship`
175
+ * on repeated bootstraps / session starts accumulates duplicate tuples.
176
+ * This reads the exact tuple first (via {@link relationshipExists}) and only
177
+ * writes when it is absent — which also self-heals if the tuple was deleted
178
+ * out of band.
179
+ *
180
+ * If the existence probe itself fails, the tuple is left untouched (we do
181
+ * not write blind, to avoid re-introducing duplicates when the read path is
182
+ * misbehaving) and the classified error is returned as `probeError`. Callers
183
+ * that must guarantee the grant exists (e.g. install-time bootstrap) can
184
+ * fall back to {@link createRelationship} when `probeError` is set.
185
+ */
186
+ ensureRelationship(check: PermissionCheck, options?: {
187
+ spanAttributes?: Record<string, unknown>;
188
+ }): Promise<{
189
+ created: boolean;
190
+ alreadyExisted: boolean;
191
+ probeError?: OryError;
192
+ }>;
162
193
  /**
163
194
  * Delete a relation tuple in Keto. Missing tuples (404) are treated as
164
195
  * success so callers can call this idempotently when unwinding state.
package/dist/client.js CHANGED
@@ -551,6 +551,64 @@ class OryAgentClient {
551
551
  throw oryErr;
552
552
  }
553
553
  }
554
+ /**
555
+ * True if the *exact* relation tuple already exists in Keto. Queries the
556
+ * relation-tuple listing API filtered by the tuple's own coordinates
557
+ * (namespace/object/relation + subject), so — unlike `checkPermission` —
558
+ * it does not follow subject-set expansion: it answers "is this literal
559
+ * tuple stored?", not "does this permission resolve?". Throws a classified
560
+ * {@link OryError} if the query itself fails.
561
+ */
562
+ async relationshipExists(check) {
563
+ try {
564
+ const response = await this.relationship.getRelationships({
565
+ namespace: check.namespace,
566
+ object: check.object,
567
+ relation: check.relation,
568
+ ...(check.subjectId ? { subjectId: check.subjectId } : {}),
569
+ ...(check.subjectSet
570
+ ? {
571
+ subjectSetNamespace: check.subjectSet.namespace,
572
+ subjectSetObject: check.subjectSet.object,
573
+ subjectSetRelation: check.subjectSet.relation,
574
+ }
575
+ : {}),
576
+ pageSize: 1,
577
+ });
578
+ const tuples = response.data?.relation_tuples ?? [];
579
+ return tuples.length > 0;
580
+ }
581
+ catch (err) {
582
+ throw this.classifyError(err);
583
+ }
584
+ }
585
+ /**
586
+ * Idempotent relationship write. Keto's create API is **not** idempotent —
587
+ * `PUT /admin/relation-tuples` inserts a fresh row on every call (each with
588
+ * a new primary key) and never returns 409, so calling `createRelationship`
589
+ * on repeated bootstraps / session starts accumulates duplicate tuples.
590
+ * This reads the exact tuple first (via {@link relationshipExists}) and only
591
+ * writes when it is absent — which also self-heals if the tuple was deleted
592
+ * out of band.
593
+ *
594
+ * If the existence probe itself fails, the tuple is left untouched (we do
595
+ * not write blind, to avoid re-introducing duplicates when the read path is
596
+ * misbehaving) and the classified error is returned as `probeError`. Callers
597
+ * that must guarantee the grant exists (e.g. install-time bootstrap) can
598
+ * fall back to {@link createRelationship} when `probeError` is set.
599
+ */
600
+ async ensureRelationship(check, options) {
601
+ let exists;
602
+ try {
603
+ exists = await this.relationshipExists(check);
604
+ }
605
+ catch (err) {
606
+ return { created: false, alreadyExisted: false, probeError: err };
607
+ }
608
+ if (exists)
609
+ return { created: false, alreadyExisted: true };
610
+ return this.createRelationship(check, options);
611
+ }
554
612
  /**
555
613
  * Delete a relation tuple in Keto. Missing tuples (404) are treated as
556
614
  * success so callers can call this idempotently when unwinding state.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { OryAgentClient, type OryAgentConfig, type PrincipalIdentity, } from "./client.js";
2
- export { DebugLogger, redactLogData, type LogEntry, type LogLevel, } from "./logger.js";
2
+ export { DebugLogger, redactLogData, withQuietStderr, type LogEntry, type LogLevel, } from "./logger.js";
3
3
  export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type TraceEvent, type SpanStatus, type TraceSpan, type SpanOptions, type TracerOptions, type TracerContext, 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";
@@ -27,4 +27,4 @@ export { resolveUserSubject, runWithUserSubject, subjectLabel, type UserSubjectR
27
27
  export { formatDenialMessage, formatDenialSummary, formatAlertMessage, formatAlertSummary, alertAttributes, OryDenialError, type DenialContext, type AlertAttributes, } from "./denial.js";
28
28
  export { summarizeToolInput, summarizeToolOutput, type ToolInputSummary, type ToolOutputSummary, } from "./tool-metadata.js";
29
29
  export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, type SpanExporter, type OtlpExporterOptions, type OtlpProtocol, } from "./otel/index.js";
30
- export { resolveNamespace, sessionStart, gate, complete, registerSubagent, wrapTool, type SessionStartResult, type SessionStartOptions, type RegisterSubagentOptions, type GateResult, type GateOptions, type WrapToolOptions, } from "./adapters.js";
30
+ export { resolveNamespace, sessionStart, gate, complete, registerSubagent, writeUserDelegatesAgent, writeAgentDelegatesSubagent, wrapTool, type SessionStartResult, type SessionStartOptions, type RegisterSubagentOptions, type GateResult, type GateOptions, type WrapToolOptions, } from "./adapters.js";
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.runConfigureCommand = exports.clearCredentialsForUninstall = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = exports.revokeAgentDynamicClient = exports.clearSubAgentDynamicCredentials = exports.saveSubAgentDynamicCredentials = exports.loadSubAgentDynamicCredentials = exports.clearAgentDynamicCredentials = exports.saveAgentDynamicCredentials = exports.loadAgentDynamicCredentials = exports.registerAgentClient = exports.fetchClientCredentialsToken = exports.ensureSubAgentIdentity = exports.ensureAgentIdentity = exports.resolveAgentCredentials = exports.ensureAuthenticated = exports.ensureUserAuthenticated = exports.TOKEN_EXPIRY_SKEW_SEC = exports.waitForPeerTokensSync = exports.waitForPeerTokens = exports.clearPkceFlightLock = exports.tryAcquirePkceFlightLock = exports.refreshAndSave = exports.isExpired = exports.clearTokens = exports.saveTokens = exports.loadTokens = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.LOOPBACK_PORTS = exports.buildAuthorizeUrl = exports.sha256Base64Url = exports.generateCodeVerifier = exports.detectHeadless = exports.refreshAccessToken = exports.pkceLogin = exports.getHarnessDataDir = exports.getDataDir = exports.getConfigPath = exports.mutateConfig = exports.resolveConfig = exports.saveConfig = exports.loadConfig = exports.watchTraceFile = exports.formatSpan = exports.deriveTraceId = exports.ActiveSpan = exports.Tracer = exports.redactLogData = exports.DebugLogger = exports.OryAgentClient = void 0;
4
- exports.runDevLauncher = exports.unregisterPlugin = exports.registerPlugin = exports.removeMcpServer = exports.mergeMcpServer = exports.mcpServerEntry = exports.resolveMcpServerCommand = exports.emitDeferredNextSteps = exports.beginDeferNextSteps = exports.nextStepsSink = exports.printNextSteps = exports.printSetupHelp = exports.removeFlatHooks = exports.mergeFlatHooks = exports.flatHookEntry = exports.removeMatcherHooks = exports.mergeMatcherHooks = exports.matcherHookEntry = exports.resolveHookCommand = exports.isOryHookCommand = exports.writeJsonFile = exports.readJsonFile = exports.parseSetupArgs = exports.resolveBuildInfo = exports.collectVersionInfo = exports.runVersionCommand = exports.printPermissionsSection = exports.printAgentIdentitySection = exports.printUserIdentitySection = exports.runStatusCommand = exports.LOOPBACK_REDIRECT_URIS = exports.projectUrlFromSlug = exports.runPostInstall = exports.runInteractiveSetup = exports.printPermissionsOnboardingHelp = exports.maybeAutoBootstrap = exports.isUserIdentityCached = exports.runPermissionsCommand = exports.oryNpx = exports.interactiveConfigPrompt = exports.promptForProjectUrl = exports.promptOnTty = exports.isTtyAvailable = exports.runWatchCommand = exports.printTraceTail = exports.printEnvHelp = exports.printLogTail = exports.printEnvironment = exports.printOryConfig = exports.runAgentCommand = void 0;
5
- exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.runWithUserSubject = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.TOOL_EXECUTION_PHASES = exports.USER_FACING_PHASES = exports.HARNESS_LIFECYCLE_MAP = exports.isToolExecutionPhase = exports.isUserFacingPhase = exports.classifyLifecycle = exports.isInteractiveTool = exports.getInteractiveToolCatalog = exports.INTERACTIVE_TOOL_CATALOG = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.gateToolCall = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = exports.runLocalCommand = exports.ORY_COMMAND_SLUGS = exports.ORY_COMMAND_SKILL_NAMES = exports.ORY_SKILL_NAMES = exports.removeSkillDirs = exports.writeSkillTree = exports.toSkillMarkdown = exports.commandToPlainMarkdown = exports.commandToFrontmatterMarkdown = exports.commandToToml = exports.commandToSkill = exports.renderOryCommands = exports.renderOrySkills = void 0;
6
- exports.wrapTool = exports.registerSubagent = exports.complete = exports.gate = exports.sessionStart = exports.resolveNamespace = exports.parseKeyValueList = void 0;
3
+ exports.clearCredentialsForUninstall = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = exports.revokeAgentDynamicClient = exports.clearSubAgentDynamicCredentials = exports.saveSubAgentDynamicCredentials = exports.loadSubAgentDynamicCredentials = exports.clearAgentDynamicCredentials = exports.saveAgentDynamicCredentials = exports.loadAgentDynamicCredentials = exports.registerAgentClient = exports.fetchClientCredentialsToken = exports.ensureSubAgentIdentity = exports.ensureAgentIdentity = exports.resolveAgentCredentials = exports.ensureAuthenticated = exports.ensureUserAuthenticated = exports.TOKEN_EXPIRY_SKEW_SEC = exports.waitForPeerTokensSync = exports.waitForPeerTokens = exports.clearPkceFlightLock = exports.tryAcquirePkceFlightLock = exports.refreshAndSave = exports.isExpired = exports.clearTokens = exports.saveTokens = exports.loadTokens = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.LOOPBACK_PORTS = exports.buildAuthorizeUrl = exports.sha256Base64Url = exports.generateCodeVerifier = exports.detectHeadless = exports.refreshAccessToken = exports.pkceLogin = exports.getHarnessDataDir = exports.getDataDir = exports.getConfigPath = exports.mutateConfig = exports.resolveConfig = exports.saveConfig = exports.loadConfig = exports.watchTraceFile = exports.formatSpan = exports.deriveTraceId = exports.ActiveSpan = exports.Tracer = exports.withQuietStderr = exports.redactLogData = exports.DebugLogger = exports.OryAgentClient = void 0;
4
+ exports.unregisterPlugin = exports.registerPlugin = exports.removeMcpServer = exports.mergeMcpServer = exports.mcpServerEntry = exports.resolveMcpServerCommand = exports.emitDeferredNextSteps = exports.beginDeferNextSteps = exports.nextStepsSink = exports.printNextSteps = exports.printSetupHelp = exports.removeFlatHooks = exports.mergeFlatHooks = exports.flatHookEntry = exports.removeMatcherHooks = exports.mergeMatcherHooks = exports.matcherHookEntry = exports.resolveHookCommand = exports.isOryHookCommand = exports.writeJsonFile = exports.readJsonFile = exports.parseSetupArgs = exports.resolveBuildInfo = exports.collectVersionInfo = exports.runVersionCommand = exports.printPermissionsSection = exports.printAgentIdentitySection = exports.printUserIdentitySection = exports.runStatusCommand = exports.LOOPBACK_REDIRECT_URIS = exports.projectUrlFromSlug = exports.runPostInstall = exports.runInteractiveSetup = exports.printPermissionsOnboardingHelp = exports.maybeAutoBootstrap = exports.isUserIdentityCached = exports.runPermissionsCommand = exports.oryNpx = exports.interactiveConfigPrompt = exports.promptForProjectUrl = exports.promptOnTty = exports.isTtyAvailable = exports.runWatchCommand = exports.printTraceTail = exports.printEnvHelp = exports.printLogTail = exports.printEnvironment = exports.printOryConfig = exports.runAgentCommand = exports.runConfigureCommand = void 0;
5
+ exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.runWithUserSubject = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.TOOL_EXECUTION_PHASES = exports.USER_FACING_PHASES = exports.HARNESS_LIFECYCLE_MAP = exports.isToolExecutionPhase = exports.isUserFacingPhase = exports.classifyLifecycle = exports.isInteractiveTool = exports.getInteractiveToolCatalog = exports.INTERACTIVE_TOOL_CATALOG = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.gateToolCall = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = exports.runLocalCommand = exports.ORY_COMMAND_SLUGS = exports.ORY_COMMAND_SKILL_NAMES = exports.ORY_SKILL_NAMES = exports.removeSkillDirs = exports.writeSkillTree = exports.toSkillMarkdown = exports.commandToPlainMarkdown = exports.commandToFrontmatterMarkdown = exports.commandToToml = exports.commandToSkill = exports.renderOryCommands = exports.renderOrySkills = exports.runDevLauncher = void 0;
6
+ exports.wrapTool = exports.writeAgentDelegatesSubagent = exports.writeUserDelegatesAgent = exports.registerSubagent = exports.complete = exports.gate = exports.sessionStart = exports.resolveNamespace = exports.parseKeyValueList = exports.otlpExporterFromEnv = void 0;
7
7
  var client_js_1 = require("./client.js");
8
8
  Object.defineProperty(exports, "OryAgentClient", { enumerable: true, get: function () { return client_js_1.OryAgentClient; } });
9
9
  var logger_js_1 = require("./logger.js");
10
10
  Object.defineProperty(exports, "DebugLogger", { enumerable: true, get: function () { return logger_js_1.DebugLogger; } });
11
11
  Object.defineProperty(exports, "redactLogData", { enumerable: true, get: function () { return logger_js_1.redactLogData; } });
12
+ Object.defineProperty(exports, "withQuietStderr", { enumerable: true, get: function () { return logger_js_1.withQuietStderr; } });
12
13
  var tracer_js_1 = require("./tracer.js");
13
14
  Object.defineProperty(exports, "Tracer", { enumerable: true, get: function () { return tracer_js_1.Tracer; } });
14
15
  Object.defineProperty(exports, "ActiveSpan", { enumerable: true, get: function () { return tracer_js_1.ActiveSpan; } });
@@ -189,4 +190,6 @@ Object.defineProperty(exports, "sessionStart", { enumerable: true, get: function
189
190
  Object.defineProperty(exports, "gate", { enumerable: true, get: function () { return adapters_js_1.gate; } });
190
191
  Object.defineProperty(exports, "complete", { enumerable: true, get: function () { return adapters_js_1.complete; } });
191
192
  Object.defineProperty(exports, "registerSubagent", { enumerable: true, get: function () { return adapters_js_1.registerSubagent; } });
193
+ Object.defineProperty(exports, "writeUserDelegatesAgent", { enumerable: true, get: function () { return adapters_js_1.writeUserDelegatesAgent; } });
194
+ Object.defineProperty(exports, "writeAgentDelegatesSubagent", { enumerable: true, get: function () { return adapters_js_1.writeAgentDelegatesSubagent; } });
192
195
  Object.defineProperty(exports, "wrapTool", { enumerable: true, get: function () { return adapters_js_1.wrapTool; } });
@@ -82,6 +82,7 @@ const cli_js_1 = require("./cli.js");
82
82
  const cli_invocation_js_1 = require("./cli-invocation.js");
83
83
  const auth_js_1 = require("./auth.js");
84
84
  const client_js_1 = require("./client.js");
85
+ const logger_js_1 = require("./logger.js");
85
86
  const user_login_js_1 = require("./user-login.js");
86
87
  const agent_auth_js_1 = require("./agent-auth.js");
87
88
  const tool_catalog_js_1 = require("./tool-catalog.js");
@@ -261,8 +262,12 @@ async function runInteractiveSetup(binName, harness, args = [], deps = {}) {
261
262
  ttyPrompter = createTtyPrompter();
262
263
  }
263
264
  };
265
+ // Quiet the structured stderr firehose for the duration of the wizard so
266
+ // `[ory-agent] {...}` debug lines don't interleave with the human-facing UI
267
+ // when a session runs with ORY_AGENT_DEBUG=true (the dev launcher always
268
+ // sets it). Debug still lands in the log file.
264
269
  try {
265
- return await runWizard(binName, harness, { runner, prompt, deps, runInteractive });
270
+ return await (0, logger_js_1.withQuietStderr)(() => runWizard(binName, harness, { runner, prompt, deps, runInteractive }));
266
271
  }
267
272
  catch (err) {
268
273
  const msg = err instanceof Error ? err.message : String(err);
package/dist/logger.d.ts CHANGED
@@ -12,6 +12,15 @@ export interface LogEntry {
12
12
  */
13
13
  export declare function redactLogData(value: unknown): unknown;
14
14
  export declare class DebugLogger {
15
+ /**
16
+ * Process-global switch to suppress the stderr firehose while still writing
17
+ * to the log file. The interactive installer sets this so structured debug
18
+ * JSON (`[ory-agent] {...}`) doesn't interleave with the wizard's
19
+ * human-facing UI when a session runs with `ORY_AGENT_DEBUG=true` (the dev
20
+ * launcher, for one, always sets it). File logging — the real observability
21
+ * channel — is unaffected.
22
+ */
23
+ static stderrSuppressed: boolean;
15
24
  enabled: boolean;
16
25
  harness: string;
17
26
  logFile: string | null;
@@ -27,3 +36,15 @@ export declare class DebugLogger {
27
36
  warn(event: string, data?: Record<string, unknown>): void;
28
37
  error(event: string, data?: Record<string, unknown>): void;
29
38
  }
39
+ /**
40
+ * Run `fn` with the structured stderr firehose suppressed, restoring the
41
+ * previous state afterward (even on throw). File logging is unaffected.
42
+ *
43
+ * Human-facing CLI commands (the interactive installer, `status`,
44
+ * `permissions status`/`bootstrap`) spin up a client and emit debug/info
45
+ * events; when the process runs with `ORY_AGENT_DEBUG=true` (the dev launcher
46
+ * always sets it) those `[ory-agent] {...}` lines would interleave with the
47
+ * command's polished output. Wrapping the command body in this keeps the
48
+ * terminal clean while the log file still captures everything.
49
+ */
50
+ export declare function withQuietStderr<T>(fn: () => Promise<T> | T): Promise<T>;
package/dist/logger.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DebugLogger = void 0;
37
37
  exports.redactLogData = redactLogData;
38
+ exports.withQuietStderr = withQuietStderr;
38
39
  const fs = __importStar(require("node:fs"));
39
40
  const path = __importStar(require("node:path"));
40
41
  /**
@@ -82,6 +83,15 @@ function redactLogData(value) {
82
83
  return value;
83
84
  }
84
85
  class DebugLogger {
86
+ /**
87
+ * Process-global switch to suppress the stderr firehose while still writing
88
+ * to the log file. The interactive installer sets this so structured debug
89
+ * JSON (`[ory-agent] {...}`) doesn't interleave with the wizard's
90
+ * human-facing UI when a session runs with `ORY_AGENT_DEBUG=true` (the dev
91
+ * launcher, for one, always sets it). File logging — the real observability
92
+ * channel — is unaffected.
93
+ */
94
+ static stderrSuppressed = false;
85
95
  enabled;
86
96
  harness;
87
97
  logFile;
@@ -113,8 +123,13 @@ class DebugLogger {
113
123
  ...(safeData ? { data: safeData } : {}),
114
124
  };
115
125
  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`);
126
+ // Write to stderr (never stdout) so we don't interfere with hook stdout,
127
+ // unless the stderr firehose is globally suppressed (e.g. during the
128
+ // interactive installer, so it doesn't clutter the wizard UI). The log
129
+ // file below still captures everything.
130
+ if (!DebugLogger.stderrSuppressed) {
131
+ process.stderr.write(`[ory-agent] ${line}\n`);
132
+ }
118
133
  if (this.logFile) {
119
134
  const dir = path.dirname(this.logFile);
120
135
  if (!fs.existsSync(dir)) {
@@ -137,3 +152,24 @@ class DebugLogger {
137
152
  }
138
153
  }
139
154
  exports.DebugLogger = DebugLogger;
155
+ /**
156
+ * Run `fn` with the structured stderr firehose suppressed, restoring the
157
+ * previous state afterward (even on throw). File logging is unaffected.
158
+ *
159
+ * Human-facing CLI commands (the interactive installer, `status`,
160
+ * `permissions status`/`bootstrap`) spin up a client and emit debug/info
161
+ * events; when the process runs with `ORY_AGENT_DEBUG=true` (the dev launcher
162
+ * always sets it) those `[ory-agent] {...}` lines would interleave with the
163
+ * command's polished output. Wrapping the command body in this keeps the
164
+ * terminal clean while the log file still captures everything.
165
+ */
166
+ async function withQuietStderr(fn) {
167
+ const prev = DebugLogger.stderrSuppressed;
168
+ DebugLogger.stderrSuppressed = true;
169
+ try {
170
+ return await fn();
171
+ }
172
+ finally {
173
+ DebugLogger.stderrSuppressed = prev;
174
+ }
175
+ }
@@ -61,6 +61,7 @@ exports.printPermissionsOnboardingHelp = printPermissionsOnboardingHelp;
61
61
  exports.maybeAutoBootstrap = maybeAutoBootstrap;
62
62
  const config_js_1 = require("./config.js");
63
63
  const client_js_1 = require("./client.js");
64
+ const logger_js_1 = require("./logger.js");
64
65
  const agent_auth_js_1 = require("./agent-auth.js");
65
66
  const auth_store_js_1 = require("./auth-store.js");
66
67
  const tool_catalog_js_1 = require("./tool-catalog.js");
@@ -85,10 +86,14 @@ async function runPermissionsCommand(binName, harness, args) {
85
86
  return sub ? 0 : 1;
86
87
  }
87
88
  switch (sub) {
89
+ // `status` and `bootstrap` spin up a client and probe/grant per tool,
90
+ // emitting debug/info events. Quiet the stderr firehose so those don't
91
+ // interleave with the command's human-facing output under
92
+ // ORY_AGENT_DEBUG=true; the log file still captures everything.
88
93
  case "status":
89
- return await runPermissionsStatus(binName, harness);
94
+ return await (0, logger_js_1.withQuietStderr)(() => runPermissionsStatus(binName, harness));
90
95
  case "bootstrap":
91
- return await runPermissionsBootstrap(binName, harness, args.slice(1));
96
+ return await (0, logger_js_1.withQuietStderr)(() => runPermissionsBootstrap(binName, harness, args.slice(1)));
92
97
  case "observe":
93
98
  return runPermissionsSetMode(binName, "observe");
94
99
  case "enforce":
@@ -283,9 +288,17 @@ async function runPermissionsBootstrap(binName, harness, args) {
283
288
  continue;
284
289
  }
285
290
  try {
286
- const res = await client.createRelationship(check, {
287
- spanAttributes: { toolName: tool, source: "permissions_bootstrap" },
288
- });
291
+ // Read-before-write: Keto's create API is not idempotent (every PUT
292
+ // inserts a new row and never 409s), so a plain createRelationship on
293
+ // re-install would duplicate the whole catalog. ensureRelationship
294
+ // probes for the exact tuple first and only writes when it is absent.
295
+ const spanAttributes = { toolName: tool, source: "permissions_bootstrap" };
296
+ let res = await client.ensureRelationship(check, { spanAttributes });
297
+ if (res.probeError) {
298
+ // Couldn't confirm existence — fall back to a direct write so a
299
+ // needed grant is still created (bootstrap's job is to guarantee it).
300
+ res = await client.createRelationship(check, { spanAttributes });
301
+ }
289
302
  if (res.alreadyExisted) {
290
303
  existed++;
291
304
  console.log(` = ${tool} (already exists)`);
@@ -17,6 +17,7 @@ const config_js_1 = require("./config.js");
17
17
  const auth_store_js_1 = require("./auth-store.js");
18
18
  const agent_auth_js_1 = require("./agent-auth.js");
19
19
  const client_js_1 = require("./client.js");
20
+ const logger_js_1 = require("./logger.js");
20
21
  const agent_auth_js_2 = require("./agent-auth.js");
21
22
  const subject_js_1 = require("./subject.js");
22
23
  const tool_catalog_js_1 = require("./tool-catalog.js");
@@ -279,28 +280,35 @@ function formatModeSuffix(source) {
279
280
  * than aborting the command.
280
281
  */
281
282
  async function runStatusCommand(binName, harness, options) {
282
- const heading = `Ory Agent Plugin Status (${options.title})`;
283
- console.log(heading);
284
- console.log("=".repeat(heading.length));
285
- console.log("");
286
- (0, cli_js_1.printOryConfig)();
287
- printUserIdentitySection();
288
- printAgentIdentitySection();
289
- await printPermissionsSection(binName, harness);
290
- if (options.printPluginSection) {
283
+ // Quiet the structured stderr firehose for the duration of the report so
284
+ // `[ory-agent] {...}` debug lines (the permissions probe runs a client and
285
+ // per-tool checks) don't interleave with this human-facing output when the
286
+ // command runs with ORY_AGENT_DEBUG=true. Debug still lands in the log file,
287
+ // and the "Recent activity" section below surfaces its tail regardless.
288
+ await (0, logger_js_1.withQuietStderr)(async () => {
289
+ const heading = `Ory Agent Plugin Status (${options.title})`;
290
+ console.log(heading);
291
+ console.log("=".repeat(heading.length));
291
292
  console.log("");
292
- options.printPluginSection();
293
- }
294
- (0, cli_js_1.printEnvironment)();
295
- console.log("");
296
- console.log("Recent activity:");
297
- (0, cli_js_1.printTraceTail)(harness);
298
- (0, cli_js_1.printLogTail)(harness);
299
- console.log("");
300
- console.log(` Watch traces live: ${(0, cli_invocation_js_1.oryNpx)(binName)} watch`);
301
- console.log("");
302
- console.log(`Drill into any section with:`);
303
- console.log(` ${(0, cli_invocation_js_1.oryNpx)(binName)} agent status`);
304
- console.log(` ${(0, cli_invocation_js_1.oryNpx)(binName)} permissions status`);
305
- console.log(` ${(0, cli_invocation_js_1.oryNpx)(binName)} configure`);
293
+ (0, cli_js_1.printOryConfig)();
294
+ printUserIdentitySection();
295
+ printAgentIdentitySection();
296
+ await printPermissionsSection(binName, harness);
297
+ if (options.printPluginSection) {
298
+ console.log("");
299
+ options.printPluginSection();
300
+ }
301
+ (0, cli_js_1.printEnvironment)();
302
+ console.log("");
303
+ console.log("Recent activity:");
304
+ (0, cli_js_1.printTraceTail)(harness);
305
+ (0, cli_js_1.printLogTail)(harness);
306
+ console.log("");
307
+ console.log(` Watch traces live: ${(0, cli_invocation_js_1.oryNpx)(binName)} watch`);
308
+ console.log("");
309
+ console.log(`Drill into any section with:`);
310
+ console.log(` ${(0, cli_invocation_js_1.oryNpx)(binName)} agent status`);
311
+ console.log(` ${(0, cli_invocation_js_1.oryNpx)(binName)} permissions status`);
312
+ console.log(` ${(0, cli_invocation_js_1.oryNpx)(binName)} configure`);
313
+ });
306
314
  }
package/dist/testing.d.ts CHANGED
@@ -14,6 +14,14 @@ export { runHarnessContractSuite, type HarnessContractAdapter, type ContractCont
14
14
  * Pass overrides to customize (e.g. a different harness name).
15
15
  */
16
16
  export declare function createMockClient(overrides?: Partial<ConstructorParameters<typeof OryAgentClient>[0]>): OryAgentClient;
17
+ /**
18
+ * Control what the exact-tuple existence probe ({@link OryAgentClient.relationshipExists},
19
+ * used by `ensureRelationship`) reports. Pass `true` to model a tuple that is
20
+ * already stored (the write is then skipped), `false` for absent (the write
21
+ * proceeds). Returns the spy so callers can e.g. `.mockRejectedValue(...)` to
22
+ * model a failed probe.
23
+ */
24
+ export declare function stubRelationshipExists(client: OryAgentClient, exists: boolean): import("vitest").Mock<(check: import("./types.js").PermissionCheck) => Promise<boolean>>;
17
25
  /**
18
26
  * Stub an internal API instance method on the client.
19
27
  * Returns a vi.fn mock so callers can assert on calls.
package/dist/testing.js CHANGED
@@ -42,6 +42,7 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.BATCH_SERVER_DENIED = exports.BATCH_SERVER_ALLOWED_TOOL_DENIED = exports.BATCH_BOTH_ALLOWED = exports.PERMISSION_DENIED = exports.PERMISSION_ALLOWED = exports.MOCK_INACTIVE_OAUTH2_RESPONSE = exports.MOCK_OAUTH2_RESPONSE = exports.MOCK_INACTIVE_SESSION_RESPONSE = exports.MOCK_SESSION_RESPONSE = exports.runHarnessContractSuite = void 0;
44
44
  exports.createMockClient = createMockClient;
45
+ exports.stubRelationshipExists = stubRelationshipExists;
45
46
  exports.stubApi = stubApi;
46
47
  exports.makeAxiosError = makeAxiosError;
47
48
  exports.makeNetworkError = makeNetworkError;
@@ -85,13 +86,31 @@ Object.defineProperty(exports, "runHarnessContractSuite", { enumerable: true, ge
85
86
  * Pass overrides to customize (e.g. a different harness name).
86
87
  */
87
88
  function createMockClient(overrides) {
88
- return new client_js_1.OryAgentClient({
89
+ const client = new client_js_1.OryAgentClient({
89
90
  projectUrl: "https://test.projects.oryapis.com",
90
91
  apiKey: "test-api-key",
91
92
  harness: "test",
92
93
  sessionCacheTtlMs: 0,
93
94
  ...overrides,
94
95
  });
96
+ // `ensureRelationship` probes for the exact tuple before writing. Default
97
+ // to "no tuple exists" so a plain createRelationship-asserting test sees the
98
+ // write proceed; tests that model an already-present tuple override this via
99
+ // `stubRelationshipExists`. We spy the public `relationshipExists` method
100
+ // (not the internal getRelationships API) because setAgentPrincipal rebuilds
101
+ // the Keto API instance and would discard an API-level stub.
102
+ vitest_1.vi.spyOn(client, "relationshipExists").mockResolvedValue(false);
103
+ return client;
104
+ }
105
+ /**
106
+ * Control what the exact-tuple existence probe ({@link OryAgentClient.relationshipExists},
107
+ * used by `ensureRelationship`) reports. Pass `true` to model a tuple that is
108
+ * already stored (the write is then skipped), `false` for absent (the write
109
+ * proceeds). Returns the spy so callers can e.g. `.mockRejectedValue(...)` to
110
+ * model a failed probe.
111
+ */
112
+ function stubRelationshipExists(client, exists) {
113
+ return vitest_1.vi.spyOn(client, "relationshipExists").mockResolvedValue(exists);
95
114
  }
96
115
  /**
97
116
  * Stub an internal API instance method on the client.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
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",