@ory/argus 0.7.3 → 0.8.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 CHANGED
@@ -6,7 +6,7 @@ const client_1 = require("@ory/client");
6
6
  const logger_js_1 = require("./logger.js");
7
7
  const tracer_js_1 = require("./tracer.js");
8
8
  const config_js_1 = require("./config.js");
9
- const otlp_http_js_1 = require("./otel/otlp-http.js");
9
+ const otlp_js_1 = require("./otel/otlp.js");
10
10
  class OryAgentClient {
11
11
  frontend;
12
12
  oauth2;
@@ -642,7 +642,7 @@ class OryAgentClient {
642
642
  exports.OryAgentClient = OryAgentClient;
643
643
  function attachOtlpExporterFromEnv(client, harness) {
644
644
  const meta = client.tracer.processMetadata;
645
- const exporter = (0, otlp_http_js_1.otlpExporterFromEnv)({
645
+ const exporter = (0, otlp_js_1.otlpExporterFromEnv)({
646
646
  harness,
647
647
  hostname: meta.hostname,
648
648
  user: meta.user,
package/dist/index.d.ts CHANGED
@@ -21,4 +21,4 @@ export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkM
21
21
  export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
22
22
  export { formatDenialMessage, formatDenialSummary, formatAlertMessage, formatAlertSummary, alertAttributes, OryDenialError, type DenialContext, type AlertAttributes, } from "./denial.js";
23
23
  export { summarizeToolInput, summarizeToolOutput, type ToolInputSummary, type ToolOutputSummary, } from "./tool-metadata.js";
24
- export { OtlpHttpExporter, otlpExporterFromEnv, parseKeyValueList, type SpanExporter, type OtlpHttpExporterOptions, } from "./otel/index.js";
24
+ export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, type SpanExporter, type OtlpExporterOptions, type OtlpProtocol, } from "./otel/index.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.printOryConfig = exports.runAgentCommand = exports.runConfigureCommand = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = 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
4
  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 = exports.unregisterPlugin = exports.registerPlugin = exports.removeMcpServer = exports.mergeMcpServer = exports.mcpServerEntry = exports.resolveMcpServerCommand = 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.printPermissionsSection = exports.printAgentIdentitySection = exports.printUserIdentitySection = exports.runStatusCommand = exports.printPermissionsOnboardingHelp = exports.maybeAutoBootstrap = exports.isUserIdentityCached = exports.runPermissionsCommand = exports.interactiveConfigPrompt = exports.promptForProjectUrl = exports.promptOnTty = exports.isTtyAvailable = exports.runWatchCommand = exports.printTraceTail = exports.printEnvHelp = exports.printLogTail = exports.printEnvironment = void 0;
5
- exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpHttpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
5
+ exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
6
6
  var client_js_1 = require("./client.js");
7
7
  Object.defineProperty(exports, "OryAgentClient", { enumerable: true, get: function () { return client_js_1.OryAgentClient; } });
8
8
  var logger_js_1 = require("./logger.js");
@@ -150,6 +150,6 @@ var tool_metadata_js_1 = require("./tool-metadata.js");
150
150
  Object.defineProperty(exports, "summarizeToolInput", { enumerable: true, get: function () { return tool_metadata_js_1.summarizeToolInput; } });
151
151
  Object.defineProperty(exports, "summarizeToolOutput", { enumerable: true, get: function () { return tool_metadata_js_1.summarizeToolOutput; } });
152
152
  var index_js_3 = require("./otel/index.js");
153
- Object.defineProperty(exports, "OtlpHttpExporter", { enumerable: true, get: function () { return index_js_3.OtlpHttpExporter; } });
153
+ Object.defineProperty(exports, "OtlpExporter", { enumerable: true, get: function () { return index_js_3.OtlpExporter; } });
154
154
  Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return index_js_3.otlpExporterFromEnv; } });
155
155
  Object.defineProperty(exports, "parseKeyValueList", { enumerable: true, get: function () { return index_js_3.parseKeyValueList; } });
@@ -1,2 +1,2 @@
1
1
  export type { SpanExporter } from "./exporter.js";
2
- export { OtlpHttpExporter, otlpExporterFromEnv, buildExportRequest, parseKeyValueList, type OtlpHttpExporterOptions, } from "./otlp-http.js";
2
+ export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, toReadableSpan, type OtlpExporterOptions, type OtlpProtocol, } from "./otlp.js";
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseKeyValueList = exports.buildExportRequest = exports.otlpExporterFromEnv = exports.OtlpHttpExporter = void 0;
4
- var otlp_http_js_1 = require("./otlp-http.js");
5
- Object.defineProperty(exports, "OtlpHttpExporter", { enumerable: true, get: function () { return otlp_http_js_1.OtlpHttpExporter; } });
6
- Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return otlp_http_js_1.otlpExporterFromEnv; } });
7
- Object.defineProperty(exports, "buildExportRequest", { enumerable: true, get: function () { return otlp_http_js_1.buildExportRequest; } });
8
- Object.defineProperty(exports, "parseKeyValueList", { enumerable: true, get: function () { return otlp_http_js_1.parseKeyValueList; } });
3
+ exports.toReadableSpan = exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = void 0;
4
+ var otlp_js_1 = require("./otlp.js");
5
+ Object.defineProperty(exports, "OtlpExporter", { enumerable: true, get: function () { return otlp_js_1.OtlpExporter; } });
6
+ Object.defineProperty(exports, "otlpExporterFromEnv", { enumerable: true, get: function () { return otlp_js_1.otlpExporterFromEnv; } });
7
+ Object.defineProperty(exports, "parseKeyValueList", { enumerable: true, get: function () { return otlp_js_1.parseKeyValueList; } });
8
+ Object.defineProperty(exports, "toReadableSpan", { enumerable: true, get: function () { return otlp_js_1.toReadableSpan; } });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * OTLP exporter — SDK-backed.
3
+ *
4
+ * Bridges our internal `TraceSpan` records into the official OpenTelemetry
5
+ * SDK exporters from `@opentelemetry/exporter-trace-otlp-http` and
6
+ * `@opentelemetry/exporter-trace-otlp-proto`. Each completed `TraceSpan`
7
+ * is converted to a `ReadableSpan` shape and handed to the SDK exporter,
8
+ * which serializes (JSON or protobuf) and POSTs to an OTLP HTTP endpoint.
9
+ *
10
+ * Standard OTel env vars consumed by `otlpExporterFromEnv()`:
11
+ * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
12
+ * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
13
+ * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
14
+ * - OTEL_EXPORTER_OTLP_TRACES_HEADERS traces-specific headers
15
+ * - OTEL_EXPORTER_OTLP_PROTOCOL `http/protobuf` (default) or `http/json`
16
+ * - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL traces-specific protocol
17
+ * - OTEL_SERVICE_NAME service.name resource attribute
18
+ * - OTEL_RESOURCE_ATTRIBUTES additional resource attributes
19
+ * - ORY_OTLP_ENDPOINT Ory-prefixed alias for the dev launcher
20
+ *
21
+ * Failures are swallowed (callers receive a resolved Promise) so trace
22
+ * export can never break the agent session. Transport errors surface
23
+ * through the `onError` callback.
24
+ */
25
+ import { type AttributeValue } from "@opentelemetry/api";
26
+ import { type Resource } from "@opentelemetry/resources";
27
+ import type { ReadableSpan } from "@opentelemetry/sdk-trace-base";
28
+ import type { SpanExporter as SdkSpanExporter } from "@opentelemetry/sdk-trace-base";
29
+ import type { SpanExporter } from "./exporter.js";
30
+ import type { TraceSpan } from "../tracer.js";
31
+ export type OtlpProtocol = "http/json" | "http/protobuf";
32
+ export interface OtlpExporterOptions {
33
+ /** Full traces endpoint (e.g. `http://localhost:4318/v1/traces`). */
34
+ endpoint: string;
35
+ /** Wire protocol. Default: `http/protobuf` (the OTel spec default). */
36
+ protocol?: OtlpProtocol;
37
+ /** Additional HTTP headers to send with each request. */
38
+ headers?: Record<string, string>;
39
+ /** Resource attributes merged into every export. */
40
+ resource?: Record<string, AttributeValue>;
41
+ /** Per-request timeout in milliseconds. Default: 5000. */
42
+ timeoutMs?: number;
43
+ /** Optional sink for transport errors. Defaults to a no-op. */
44
+ onError?: (err: unknown) => void;
45
+ /**
46
+ * Inject a custom SDK exporter (used by tests). When provided, supersedes
47
+ * the protocol selector — the wrapper passes converted ReadableSpans to
48
+ * this exporter as-is.
49
+ */
50
+ sdkExporter?: SdkSpanExporter;
51
+ }
52
+ /**
53
+ * OTLP exporter that delegates wire-format serialization and HTTP transport
54
+ * to the official `@opentelemetry/exporter-trace-otlp-*` SDK exporters.
55
+ */
56
+ export declare class OtlpExporter implements SpanExporter {
57
+ readonly endpoint: string;
58
+ readonly protocol: OtlpProtocol;
59
+ readonly headers: Record<string, string>;
60
+ readonly resource: Resource;
61
+ private readonly delegate;
62
+ private readonly onError;
63
+ private readonly inFlight;
64
+ constructor(opts: OtlpExporterOptions);
65
+ export(spans: ReadonlyArray<TraceSpan>): Promise<void>;
66
+ shutdown(): Promise<void>;
67
+ }
68
+ /**
69
+ * Build an OtlpExporter from process env. Returns undefined when no
70
+ * endpoint is configured or the requested protocol is unsupported.
71
+ */
72
+ export declare function otlpExporterFromEnv(opts: {
73
+ harness: string;
74
+ hostname?: string;
75
+ user?: string;
76
+ pluginVersion?: string;
77
+ cwd?: string;
78
+ gitBranch?: string;
79
+ gitCommit?: string;
80
+ onError?: (err: unknown) => void;
81
+ env?: NodeJS.ProcessEnv;
82
+ }): OtlpExporter | undefined;
83
+ /**
84
+ * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
85
+ */
86
+ export declare function parseKeyValueList(raw: string | undefined): Record<string, string>;
87
+ /**
88
+ * Convert our internal `TraceSpan` record to an OTel SDK `ReadableSpan`.
89
+ * Exported for tests; the OTLP exporters consume this shape directly.
90
+ */
91
+ export declare function toReadableSpan(span: TraceSpan, resource: Resource): ReadableSpan;
@@ -0,0 +1,348 @@
1
+ "use strict";
2
+ /**
3
+ * OTLP exporter — SDK-backed.
4
+ *
5
+ * Bridges our internal `TraceSpan` records into the official OpenTelemetry
6
+ * SDK exporters from `@opentelemetry/exporter-trace-otlp-http` and
7
+ * `@opentelemetry/exporter-trace-otlp-proto`. Each completed `TraceSpan`
8
+ * is converted to a `ReadableSpan` shape and handed to the SDK exporter,
9
+ * which serializes (JSON or protobuf) and POSTs to an OTLP HTTP endpoint.
10
+ *
11
+ * Standard OTel env vars consumed by `otlpExporterFromEnv()`:
12
+ * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
13
+ * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
14
+ * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
15
+ * - OTEL_EXPORTER_OTLP_TRACES_HEADERS traces-specific headers
16
+ * - OTEL_EXPORTER_OTLP_PROTOCOL `http/protobuf` (default) or `http/json`
17
+ * - OTEL_EXPORTER_OTLP_TRACES_PROTOCOL traces-specific protocol
18
+ * - OTEL_SERVICE_NAME service.name resource attribute
19
+ * - OTEL_RESOURCE_ATTRIBUTES additional resource attributes
20
+ * - ORY_OTLP_ENDPOINT Ory-prefixed alias for the dev launcher
21
+ *
22
+ * Failures are swallowed (callers receive a resolved Promise) so trace
23
+ * export can never break the agent session. Transport errors surface
24
+ * through the `onError` callback.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.OtlpExporter = void 0;
28
+ exports.otlpExporterFromEnv = otlpExporterFromEnv;
29
+ exports.parseKeyValueList = parseKeyValueList;
30
+ exports.toReadableSpan = toReadableSpan;
31
+ const api_1 = require("@opentelemetry/api");
32
+ const core_1 = require("@opentelemetry/core");
33
+ const resources_1 = require("@opentelemetry/resources");
34
+ const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
35
+ const exporter_trace_otlp_proto_1 = require("@opentelemetry/exporter-trace-otlp-proto");
36
+ const TRACES_PATH = "/v1/traces";
37
+ const SCOPE = {
38
+ name: "ory-agent-plugins",
39
+ };
40
+ /**
41
+ * OTLP exporter that delegates wire-format serialization and HTTP transport
42
+ * to the official `@opentelemetry/exporter-trace-otlp-*` SDK exporters.
43
+ */
44
+ class OtlpExporter {
45
+ endpoint;
46
+ protocol;
47
+ headers;
48
+ resource;
49
+ delegate;
50
+ onError;
51
+ inFlight = new Set();
52
+ constructor(opts) {
53
+ this.endpoint = opts.endpoint;
54
+ this.protocol = opts.protocol ?? "http/protobuf";
55
+ this.headers = { ...(opts.headers ?? {}) };
56
+ this.resource = (0, resources_1.resourceFromAttributes)(stripUndefined(opts.resource ?? {}));
57
+ this.onError = opts.onError ?? (() => undefined);
58
+ this.delegate =
59
+ opts.sdkExporter ??
60
+ buildSdkExporter(this.endpoint, this.protocol, this.headers, opts.timeoutMs);
61
+ }
62
+ export(spans) {
63
+ if (spans.length === 0)
64
+ return Promise.resolve();
65
+ const readables = spans.map((s) => toReadableSpan(s, this.resource));
66
+ const promise = new Promise((resolve) => {
67
+ try {
68
+ this.delegate.export(readables, (result) => {
69
+ if (result.code !== core_1.ExportResultCode.SUCCESS) {
70
+ this.onError(result.error ?? new Error("OTLP export failed"));
71
+ }
72
+ resolve();
73
+ });
74
+ }
75
+ catch (err) {
76
+ // Defensive — SDK exporter contract says export() must not throw,
77
+ // but we honor our own contract regardless.
78
+ this.onError(err);
79
+ resolve();
80
+ }
81
+ });
82
+ this.inFlight.add(promise);
83
+ promise.finally(() => this.inFlight.delete(promise));
84
+ return promise;
85
+ }
86
+ async shutdown() {
87
+ // Drain in-flight exports first. Subprocess hooks (claude-code, codex,
88
+ // gemini-cli) call process.exit() the instant they hand back stdout —
89
+ // without this drain the underlying HTTP request is killed mid-flight
90
+ // and spans never reach the collector even though the NDJSON file
91
+ // already contains them.
92
+ if (this.inFlight.size > 0) {
93
+ await Promise.allSettled([...this.inFlight]);
94
+ }
95
+ try {
96
+ await this.delegate.shutdown();
97
+ }
98
+ catch (err) {
99
+ this.onError(err);
100
+ }
101
+ }
102
+ }
103
+ exports.OtlpExporter = OtlpExporter;
104
+ function buildSdkExporter(endpoint, protocol, headers, timeoutMs) {
105
+ const config = {
106
+ url: endpoint,
107
+ headers,
108
+ timeoutMillis: timeoutMs ?? 5_000,
109
+ };
110
+ return protocol === "http/json"
111
+ ? new exporter_trace_otlp_http_1.OTLPTraceExporter(config)
112
+ : new exporter_trace_otlp_proto_1.OTLPTraceExporter(config);
113
+ }
114
+ // ─── Factory ───────────────────────────────────────────────────────
115
+ /**
116
+ * Build an OtlpExporter from process env. Returns undefined when no
117
+ * endpoint is configured or the requested protocol is unsupported.
118
+ */
119
+ function otlpExporterFromEnv(opts) {
120
+ const env = opts.env ?? process.env;
121
+ const endpoint = resolveEndpoint(env);
122
+ if (!endpoint)
123
+ return undefined;
124
+ const protocol = resolveProtocol(env, opts.onError);
125
+ if (!protocol)
126
+ return undefined;
127
+ const headers = mergeHeaders(parseKeyValueList(env.OTEL_EXPORTER_OTLP_HEADERS), parseKeyValueList(env.OTEL_EXPORTER_OTLP_TRACES_HEADERS));
128
+ const resource = {
129
+ "service.name": env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${opts.harness}`,
130
+ "service.namespace": "ory.agent_plugins",
131
+ "ory.harness": opts.harness,
132
+ "process.pid": process.pid,
133
+ "process.runtime.name": "nodejs",
134
+ "process.runtime.version": process.version,
135
+ };
136
+ if (opts.hostname)
137
+ resource["host.name"] = opts.hostname;
138
+ if (opts.user)
139
+ resource["host.user"] = opts.user;
140
+ if (opts.pluginVersion)
141
+ resource["service.version"] = opts.pluginVersion;
142
+ if (opts.cwd)
143
+ resource["process.cwd"] = opts.cwd;
144
+ if (opts.gitBranch)
145
+ resource["ory.git.branch"] = opts.gitBranch;
146
+ if (opts.gitCommit)
147
+ resource["ory.git.commit"] = opts.gitCommit;
148
+ for (const [k, v] of Object.entries(parseKeyValueList(env.OTEL_RESOURCE_ATTRIBUTES))) {
149
+ resource[k] = v;
150
+ }
151
+ return new OtlpExporter({
152
+ endpoint,
153
+ protocol,
154
+ headers,
155
+ resource,
156
+ onError: opts.onError,
157
+ });
158
+ }
159
+ function resolveEndpoint(env) {
160
+ const tracesUrl = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
161
+ if (tracesUrl)
162
+ return tracesUrl;
163
+ // ORY_OTLP_ENDPOINT is an Ory-prefixed alias used by the dev launcher.
164
+ // Some harnesses (e.g. Claude Code) filter the env they pass to hook
165
+ // subprocesses and drop OTEL_*-prefixed vars, but allow ORY_*-prefixed
166
+ // ones through. The alias lets the launcher hand the OTLP endpoint to
167
+ // the hook process without depending on OTEL_* propagation.
168
+ const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || env.ORY_OTLP_ENDPOINT?.trim();
169
+ if (!base)
170
+ return undefined;
171
+ return base.replace(/\/+$/, "") + TRACES_PATH;
172
+ }
173
+ function resolveProtocol(env, onError) {
174
+ const raw = (env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ??
175
+ env.OTEL_EXPORTER_OTLP_PROTOCOL ??
176
+ "http/protobuf")
177
+ .trim()
178
+ .toLowerCase();
179
+ if (raw === "http/protobuf" || raw === "http/json")
180
+ return raw;
181
+ onError?.(new Error(`OTEL_EXPORTER_OTLP_PROTOCOL=${raw} not supported (only http/protobuf and http/json). Skipping OTel export.`));
182
+ return undefined;
183
+ }
184
+ // ─── Env parsing helpers ───────────────────────────────────────────
185
+ /**
186
+ * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
187
+ */
188
+ function parseKeyValueList(raw) {
189
+ const out = {};
190
+ if (!raw)
191
+ return out;
192
+ for (const pair of raw.split(",")) {
193
+ const trimmed = pair.trim();
194
+ if (!trimmed)
195
+ continue;
196
+ const eq = trimmed.indexOf("=");
197
+ if (eq <= 0)
198
+ continue;
199
+ const key = trimmed.slice(0, eq).trim();
200
+ const value = trimmed.slice(eq + 1).trim();
201
+ if (!key)
202
+ continue;
203
+ out[key] = safeDecodeURIComponent(value);
204
+ }
205
+ return out;
206
+ }
207
+ function safeDecodeURIComponent(value) {
208
+ try {
209
+ return decodeURIComponent(value);
210
+ }
211
+ catch {
212
+ return value;
213
+ }
214
+ }
215
+ function mergeHeaders(...sources) {
216
+ const out = {};
217
+ for (const src of sources) {
218
+ for (const [k, v] of Object.entries(src)) {
219
+ out[k.toLowerCase()] = v;
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+ // ─── TraceSpan → ReadableSpan conversion ───────────────────────────
225
+ /**
226
+ * Convert our internal `TraceSpan` record to an OTel SDK `ReadableSpan`.
227
+ * Exported for tests; the OTLP exporters consume this shape directly.
228
+ */
229
+ function toReadableSpan(span, resource) {
230
+ // OTLP wire format expects 32-hex traceId / 16-hex spanId. Our internal
231
+ // IDs are shorter (hashed session prefix + random suffix), so pad with
232
+ // leading zeros to canonical width.
233
+ const traceId = padHex(span.traceId, 32);
234
+ const spanId = padHex(span.spanId, 16);
235
+ const ctx = {
236
+ traceId,
237
+ spanId,
238
+ traceFlags: 1, // sampled
239
+ isRemote: false,
240
+ };
241
+ const parentSpanContext = span.parentSpanId
242
+ ? {
243
+ traceId,
244
+ spanId: padHex(span.parentSpanId, 16),
245
+ traceFlags: 1,
246
+ isRemote: false,
247
+ }
248
+ : undefined;
249
+ const startTime = isoToHrTime(span.startedAt);
250
+ const endTime = isoToHrTime(span.endedAt);
251
+ const attributes = {
252
+ "ory.event": span.event,
253
+ "ory.span.status": span.status,
254
+ "ory.duration_ms": span.durationMs,
255
+ };
256
+ if (span.sessionId)
257
+ attributes["ory.session_id"] = span.sessionId;
258
+ for (const [k, v] of Object.entries(flattenAttributes(span.attributes ?? {}))) {
259
+ attributes[k] = v;
260
+ }
261
+ return {
262
+ name: span.event,
263
+ kind: api_1.SpanKind.INTERNAL,
264
+ spanContext: () => ctx,
265
+ parentSpanContext,
266
+ startTime,
267
+ endTime,
268
+ status: toOtelStatus(span.status),
269
+ attributes,
270
+ links: [],
271
+ events: [],
272
+ duration: hrTimeSubtract(endTime, startTime),
273
+ ended: true,
274
+ resource,
275
+ instrumentationScope: SCOPE,
276
+ droppedAttributesCount: 0,
277
+ droppedEventsCount: 0,
278
+ droppedLinksCount: 0,
279
+ };
280
+ }
281
+ function toOtelStatus(status) {
282
+ switch (status) {
283
+ case "ok":
284
+ return { code: api_1.SpanStatusCode.OK };
285
+ case "error":
286
+ return { code: api_1.SpanStatusCode.ERROR, message: "error" };
287
+ case "denied":
288
+ return { code: api_1.SpanStatusCode.ERROR, message: "denied" };
289
+ case "skipped":
290
+ default:
291
+ return { code: api_1.SpanStatusCode.UNSET };
292
+ }
293
+ }
294
+ function isoToHrTime(iso) {
295
+ const ms = Date.parse(iso);
296
+ if (!Number.isFinite(ms))
297
+ return [0, 0];
298
+ const seconds = Math.trunc(ms / 1000);
299
+ const nanos = (ms - seconds * 1000) * 1_000_000;
300
+ return [seconds, nanos];
301
+ }
302
+ function hrTimeSubtract(a, b) {
303
+ let sec = a[0] - b[0];
304
+ let nanos = a[1] - b[1];
305
+ if (nanos < 0) {
306
+ sec -= 1;
307
+ nanos += 1_000_000_000;
308
+ }
309
+ if (sec < 0) {
310
+ return [0, 0];
311
+ }
312
+ return [sec, nanos];
313
+ }
314
+ function padHex(hex, length) {
315
+ const lower = hex.toLowerCase().replace(/[^0-9a-f]/g, "");
316
+ if (lower.length >= length)
317
+ return lower.slice(-length);
318
+ return lower.padStart(length, "0");
319
+ }
320
+ function flattenAttributes(attrs) {
321
+ const out = {};
322
+ for (const [key, value] of Object.entries(attrs)) {
323
+ if (value === null || value === undefined)
324
+ continue;
325
+ if (typeof value === "string" ||
326
+ typeof value === "number" ||
327
+ typeof value === "boolean") {
328
+ out[key] = value;
329
+ continue;
330
+ }
331
+ try {
332
+ out[key] = JSON.stringify(value);
333
+ }
334
+ catch {
335
+ /* skip un-serializable values */
336
+ }
337
+ }
338
+ return out;
339
+ }
340
+ function stripUndefined(source) {
341
+ const out = {};
342
+ for (const [k, v] of Object.entries(source)) {
343
+ if (v === undefined)
344
+ continue;
345
+ out[k] = v;
346
+ }
347
+ return out;
348
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.7.3",
3
+ "version": "0.8.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",
@@ -65,6 +65,12 @@
65
65
  "!dist/**/*.tsbuildinfo"
66
66
  ],
67
67
  "dependencies": {
68
+ "@opentelemetry/api": "^1.9.0",
69
+ "@opentelemetry/core": "^2.8.0",
70
+ "@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
71
+ "@opentelemetry/exporter-trace-otlp-proto": "^0.219.0",
72
+ "@opentelemetry/resources": "^2.8.0",
73
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
68
74
  "@ory/client": "^1.22.37",
69
75
  "open": "^11.0.0"
70
76
  },
@@ -1,116 +0,0 @@
1
- /**
2
- * OTLP/HTTP JSON exporter.
3
- *
4
- * Serializes TraceSpan records into the OTLP ExportTraceServiceRequest
5
- * shape and POSTs them to an OTLP HTTP endpoint as JSON. Works with
6
- * Jaeger's OTLP receiver, Grafana Tempo, OpenTelemetry Collector,
7
- * Honeycomb, and any other vendor that accepts OTLP/HTTP.
8
- *
9
- * No protobuf dependency — relies on the global `fetch`. The exporter
10
- * is intentionally side-effect-free: failures are swallowed (callers
11
- * receive a resolved Promise) so trace export can never break the
12
- * agent session.
13
- *
14
- * Standard env vars consumed by `otlpExporterFromEnv()`:
15
- * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
16
- * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
17
- * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
18
- * - OTEL_EXPORTER_OTLP_TRACES_HEADERS headers specific to traces
19
- * - OTEL_SERVICE_NAME service.name resource attribute
20
- * - OTEL_RESOURCE_ATTRIBUTES additional resource attrs
21
- */
22
- import type { SpanExporter } from "./exporter.js";
23
- import type { TraceSpan } from "../tracer.js";
24
- export interface OtlpHttpExporterOptions {
25
- /** Full traces endpoint (e.g. `http://localhost:4318/v1/traces`). */
26
- endpoint: string;
27
- /** Additional HTTP headers to send with each request. */
28
- headers?: Record<string, string>;
29
- /** Resource attributes (service.name, host.name, …) merged into every export. */
30
- resource?: Record<string, AttributeValue>;
31
- /** Per-request timeout in milliseconds. Default: 5000. */
32
- timeoutMs?: number;
33
- /** Optional sink for transport errors. Defaults to a no-op. */
34
- onError?: (err: unknown) => void;
35
- /**
36
- * Override the fetch function (used by tests). Defaults to global fetch.
37
- */
38
- fetch?: typeof fetch;
39
- }
40
- export declare class OtlpHttpExporter implements SpanExporter {
41
- private readonly endpoint;
42
- private readonly headers;
43
- private readonly resource;
44
- private readonly timeoutMs;
45
- private readonly onError;
46
- private readonly fetchImpl;
47
- private readonly inFlight;
48
- constructor(opts: OtlpHttpExporterOptions);
49
- export(spans: ReadonlyArray<TraceSpan>): Promise<void>;
50
- private send;
51
- shutdown(): Promise<void>;
52
- }
53
- /**
54
- * Build an OtlpHttpExporter from process env. Returns undefined when no
55
- * endpoint is configured.
56
- */
57
- export declare function otlpExporterFromEnv(opts: {
58
- harness: string;
59
- hostname?: string;
60
- user?: string;
61
- pluginVersion?: string;
62
- cwd?: string;
63
- gitBranch?: string;
64
- gitCommit?: string;
65
- onError?: (err: unknown) => void;
66
- env?: NodeJS.ProcessEnv;
67
- }): OtlpHttpExporter | undefined;
68
- /**
69
- * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
70
- */
71
- export declare function parseKeyValueList(raw: string | undefined): Record<string, string>;
72
- type AttributeValue = string | number | boolean | null | undefined;
73
- interface OtlpKeyValue {
74
- key: string;
75
- value: OtlpAnyValue;
76
- }
77
- interface OtlpAnyValue {
78
- stringValue?: string;
79
- intValue?: string;
80
- doubleValue?: number;
81
- boolValue?: boolean;
82
- arrayValue?: {
83
- values: OtlpAnyValue[];
84
- };
85
- }
86
- interface OtlpStatus {
87
- code: 0 | 1 | 2;
88
- message?: string;
89
- }
90
- interface OtlpSpan {
91
- traceId: string;
92
- spanId: string;
93
- parentSpanId?: string;
94
- name: string;
95
- kind: number;
96
- startTimeUnixNano: string;
97
- endTimeUnixNano: string;
98
- attributes?: OtlpKeyValue[];
99
- status?: OtlpStatus;
100
- }
101
- interface OtlpResourceSpans {
102
- resource: {
103
- attributes: OtlpKeyValue[];
104
- };
105
- scopeSpans: Array<{
106
- scope: {
107
- name: string;
108
- };
109
- spans: OtlpSpan[];
110
- }>;
111
- }
112
- interface OtlpExportRequest {
113
- resourceSpans: OtlpResourceSpans[];
114
- }
115
- export declare function buildExportRequest(spans: ReadonlyArray<TraceSpan>, resource: Record<string, AttributeValue>): OtlpExportRequest;
116
- export {};
@@ -1,322 +0,0 @@
1
- "use strict";
2
- /**
3
- * OTLP/HTTP JSON exporter.
4
- *
5
- * Serializes TraceSpan records into the OTLP ExportTraceServiceRequest
6
- * shape and POSTs them to an OTLP HTTP endpoint as JSON. Works with
7
- * Jaeger's OTLP receiver, Grafana Tempo, OpenTelemetry Collector,
8
- * Honeycomb, and any other vendor that accepts OTLP/HTTP.
9
- *
10
- * No protobuf dependency — relies on the global `fetch`. The exporter
11
- * is intentionally side-effect-free: failures are swallowed (callers
12
- * receive a resolved Promise) so trace export can never break the
13
- * agent session.
14
- *
15
- * Standard env vars consumed by `otlpExporterFromEnv()`:
16
- * - OTEL_EXPORTER_OTLP_ENDPOINT base URL (we append /v1/traces)
17
- * - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT full traces URL (overrides base)
18
- * - OTEL_EXPORTER_OTLP_HEADERS `k=v,k=v` pairs
19
- * - OTEL_EXPORTER_OTLP_TRACES_HEADERS headers specific to traces
20
- * - OTEL_SERVICE_NAME service.name resource attribute
21
- * - OTEL_RESOURCE_ATTRIBUTES additional resource attrs
22
- */
23
- Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.OtlpHttpExporter = void 0;
25
- exports.otlpExporterFromEnv = otlpExporterFromEnv;
26
- exports.parseKeyValueList = parseKeyValueList;
27
- exports.buildExportRequest = buildExportRequest;
28
- const TRACES_PATH = "/v1/traces";
29
- const SCOPE_NAME = "ory-agent-plugins";
30
- class OtlpHttpExporter {
31
- endpoint;
32
- headers;
33
- resource;
34
- timeoutMs;
35
- onError;
36
- fetchImpl;
37
- inFlight = new Set();
38
- constructor(opts) {
39
- this.endpoint = opts.endpoint;
40
- this.headers = { "content-type": "application/json", ...opts.headers };
41
- this.resource = opts.resource ?? {};
42
- this.timeoutMs = opts.timeoutMs ?? 5_000;
43
- this.onError = opts.onError ?? (() => undefined);
44
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
45
- }
46
- export(spans) {
47
- if (spans.length === 0)
48
- return Promise.resolve();
49
- if (typeof this.fetchImpl !== "function") {
50
- this.onError(new Error("global fetch is not available"));
51
- return Promise.resolve();
52
- }
53
- const promise = this.send(spans);
54
- this.inFlight.add(promise);
55
- promise.finally(() => this.inFlight.delete(promise));
56
- return promise;
57
- }
58
- async send(spans) {
59
- const payload = buildExportRequest(spans, this.resource);
60
- const controller = new AbortController();
61
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
62
- try {
63
- const response = await this.fetchImpl(this.endpoint, {
64
- method: "POST",
65
- headers: this.headers,
66
- body: JSON.stringify(payload),
67
- signal: controller.signal,
68
- });
69
- if (!response.ok) {
70
- // Drain body so the connection can be reused but ignore the value.
71
- try {
72
- await response.text();
73
- }
74
- catch {
75
- /* ignore */
76
- }
77
- this.onError(new Error(`OTLP export failed with status ${response.status}`));
78
- }
79
- }
80
- catch (err) {
81
- this.onError(err);
82
- }
83
- finally {
84
- clearTimeout(timer);
85
- }
86
- }
87
- async shutdown() {
88
- // Drain in-flight exports. Subprocess hooks (claude-code, codex,
89
- // gemini-cli) call process.exit() the instant they hand back stdout —
90
- // without this drain the OTLP fetch is killed mid-flight and spans
91
- // never reach the collector even though the NDJSON file already
92
- // contains them.
93
- if (this.inFlight.size === 0)
94
- return;
95
- await Promise.allSettled([...this.inFlight]);
96
- }
97
- }
98
- exports.OtlpHttpExporter = OtlpHttpExporter;
99
- /**
100
- * Build an OtlpHttpExporter from process env. Returns undefined when no
101
- * endpoint is configured.
102
- */
103
- function otlpExporterFromEnv(opts) {
104
- const env = opts.env ?? process.env;
105
- const endpoint = resolveEndpoint(env);
106
- if (!endpoint)
107
- return undefined;
108
- const protocol = env.OTEL_EXPORTER_OTLP_PROTOCOL ?? env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL;
109
- if (protocol && protocol !== "http/json") {
110
- opts.onError?.(new Error(`OTEL_EXPORTER_OTLP_PROTOCOL=${protocol} not supported (only http/json). Skipping OTel export.`));
111
- return undefined;
112
- }
113
- const headers = mergeHeaders(parseKeyValueList(env.OTEL_EXPORTER_OTLP_HEADERS), parseKeyValueList(env.OTEL_EXPORTER_OTLP_TRACES_HEADERS));
114
- const resource = {
115
- "service.name": env.OTEL_SERVICE_NAME ?? `ory-agent-plugin-${opts.harness}`,
116
- "service.namespace": "ory.agent_plugins",
117
- "ory.harness": opts.harness,
118
- "process.pid": process.pid,
119
- "process.runtime.name": "nodejs",
120
- "process.runtime.version": process.version,
121
- };
122
- if (opts.hostname)
123
- resource["host.name"] = opts.hostname;
124
- if (opts.user)
125
- resource["host.user"] = opts.user;
126
- if (opts.pluginVersion) {
127
- resource["service.version"] = opts.pluginVersion;
128
- }
129
- if (opts.cwd)
130
- resource["process.cwd"] = opts.cwd;
131
- if (opts.gitBranch)
132
- resource["ory.git.branch"] = opts.gitBranch;
133
- if (opts.gitCommit)
134
- resource["ory.git.commit"] = opts.gitCommit;
135
- for (const [k, v] of Object.entries(parseKeyValueList(env.OTEL_RESOURCE_ATTRIBUTES))) {
136
- resource[k] = v;
137
- }
138
- return new OtlpHttpExporter({
139
- endpoint,
140
- headers,
141
- resource,
142
- onError: opts.onError,
143
- });
144
- }
145
- // ─── Endpoint / header parsing ─────────────────────────────────────
146
- function resolveEndpoint(env) {
147
- const tracesUrl = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
148
- if (tracesUrl) {
149
- // User specified the full traces URL — use as-is.
150
- return tracesUrl;
151
- }
152
- // ORY_OTLP_ENDPOINT is an ORY-prefixed alias used by the dev launcher.
153
- // Some harnesses (e.g. Claude Code) filter the env they pass to hook
154
- // subprocesses and drop OTEL_*-prefixed vars, but allow ORY_*-prefixed
155
- // ones through. The alias lets the launcher hand the OTLP endpoint to
156
- // the hook process without depending on OTEL_* propagation.
157
- const base = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || env.ORY_OTLP_ENDPOINT?.trim();
158
- if (!base)
159
- return undefined;
160
- return base.replace(/\/+$/, "") + TRACES_PATH;
161
- }
162
- /**
163
- * Parse the OTel `k=v,k2=v2` env-var format. Values may be URL-encoded.
164
- */
165
- function parseKeyValueList(raw) {
166
- const out = {};
167
- if (!raw)
168
- return out;
169
- for (const pair of raw.split(",")) {
170
- const trimmed = pair.trim();
171
- if (!trimmed)
172
- continue;
173
- const eq = trimmed.indexOf("=");
174
- if (eq <= 0)
175
- continue;
176
- const key = trimmed.slice(0, eq).trim();
177
- const value = trimmed.slice(eq + 1).trim();
178
- if (!key)
179
- continue;
180
- out[key] = safeDecodeURIComponent(value);
181
- }
182
- return out;
183
- }
184
- function safeDecodeURIComponent(value) {
185
- try {
186
- return decodeURIComponent(value);
187
- }
188
- catch {
189
- return value;
190
- }
191
- }
192
- function mergeHeaders(...sources) {
193
- const out = {};
194
- for (const src of sources) {
195
- for (const [k, v] of Object.entries(src)) {
196
- out[k.toLowerCase()] = v;
197
- }
198
- }
199
- return out;
200
- }
201
- function buildExportRequest(spans, resource) {
202
- const otlpSpans = spans.map(toOtlpSpan);
203
- return {
204
- resourceSpans: [
205
- {
206
- resource: { attributes: toAttributes(resource) },
207
- scopeSpans: [
208
- {
209
- scope: { name: SCOPE_NAME },
210
- spans: otlpSpans,
211
- },
212
- ],
213
- },
214
- ],
215
- };
216
- }
217
- function toOtlpSpan(span) {
218
- // OTLP/HTTP JSON expects 32-hex traceId and 16-hex spanId per the
219
- // proto3-JSON canonical mapping with hex string encoding (the OTel
220
- // spec also allows base64; modern collectors and Honeycomb accept
221
- // hex). Pad short ids with leading zeros.
222
- const status = toOtlpStatus(span.status);
223
- const baseAttributes = {
224
- "ory.event": span.event,
225
- "ory.span.status": span.status,
226
- "ory.duration_ms": span.durationMs,
227
- };
228
- if (span.sessionId)
229
- baseAttributes["ory.session_id"] = span.sessionId;
230
- const flat = flattenAttributes(span.attributes ?? {});
231
- const attrs = { ...baseAttributes, ...flat };
232
- return {
233
- traceId: padHex(span.traceId, 32),
234
- spanId: padHex(span.spanId, 16),
235
- ...(span.parentSpanId
236
- ? { parentSpanId: padHex(span.parentSpanId, 16) }
237
- : {}),
238
- name: span.event,
239
- kind: 1, // SPAN_KIND_INTERNAL
240
- startTimeUnixNano: isoToUnixNano(span.startedAt),
241
- endTimeUnixNano: isoToUnixNano(span.endedAt),
242
- attributes: toAttributes(attrs),
243
- ...(status ? { status } : {}),
244
- };
245
- }
246
- function toOtlpStatus(status) {
247
- switch (status) {
248
- case "ok":
249
- return { code: 1 };
250
- case "error":
251
- return { code: 2, message: "error" };
252
- case "denied":
253
- return { code: 2, message: "denied" };
254
- case "skipped":
255
- return undefined; // STATUS_CODE_UNSET
256
- default:
257
- return undefined;
258
- }
259
- }
260
- function isoToUnixNano(iso) {
261
- const ms = Date.parse(iso);
262
- if (!Number.isFinite(ms))
263
- return "0";
264
- // BigInt avoids 2^53 rounding for ns timestamps.
265
- return (BigInt(ms) * 1000000n).toString();
266
- }
267
- function padHex(hex, length) {
268
- const lower = hex.toLowerCase().replace(/[^0-9a-f]/g, "");
269
- if (lower.length >= length)
270
- return lower.slice(-length);
271
- return lower.padStart(length, "0");
272
- }
273
- function toAttributes(source) {
274
- const out = [];
275
- for (const [key, value] of Object.entries(source)) {
276
- const v = toAnyValue(value);
277
- if (v)
278
- out.push({ key, value: v });
279
- }
280
- return out;
281
- }
282
- function toAnyValue(value) {
283
- if (value === null || value === undefined)
284
- return undefined;
285
- if (typeof value === "string")
286
- return { stringValue: value };
287
- if (typeof value === "boolean")
288
- return { boolValue: value };
289
- if (typeof value === "number") {
290
- if (Number.isInteger(value)) {
291
- return { intValue: value.toString() };
292
- }
293
- return { doubleValue: value };
294
- }
295
- return undefined;
296
- }
297
- /**
298
- * Flatten nested attribute objects into a single-level Record where
299
- * complex values are JSON-stringified. Keeps OTLP attributes compact
300
- * and queryable as scalars in vendor backends.
301
- */
302
- function flattenAttributes(attrs) {
303
- const out = {};
304
- for (const [key, value] of Object.entries(attrs)) {
305
- if (value === null || value === undefined)
306
- continue;
307
- if (typeof value === "string" ||
308
- typeof value === "number" ||
309
- typeof value === "boolean") {
310
- out[key] = value;
311
- }
312
- else {
313
- try {
314
- out[key] = JSON.stringify(value);
315
- }
316
- catch {
317
- /* skip un-serializable values */
318
- }
319
- }
320
- }
321
- return out;
322
- }