diff --git a/apps/server/src/relay/AgentAwarenessRelay.test.ts b/apps/server/src/relay/AgentAwarenessRelay.test.ts index 40ed694723d..79aa1bebe99 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.test.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.test.ts @@ -20,6 +20,7 @@ import { CommandId, ProviderInstanceId } from "@t3tools/contracts"; import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import { RELAY_ACTIVITY_PUBLISH_TYP, verifyRelayJwt } from "@t3tools/shared/relayJwt"; import { describe, expect, it } from "@effect/vitest"; +import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -98,6 +99,50 @@ function makeMemorySecretStore() { } describe.sequential("signRelayAgentActivityPublishProof", () => { + it("redacts relay URL secrets from log attributes", () => { + const relayUrl = + "https://relay-user:relay-password@relay.example.test/private/path?token=relay-secret#fragment"; + const attributes = AgentAwarenessRelay.relayUrlLogAttributes(relayUrl); + + expect(attributes).toEqual({ + relayUrlConfigured: true, + relayUrlInputLength: relayUrl.length, + relayUrlProtocol: "https:", + relayUrlHostname: "relay.example.test", + }); + const renderedAttributes = Object.values(attributes).join(" "); + expect(renderedAttributes).not.toContain("relay-user"); + expect(renderedAttributes).not.toContain("relay-password"); + expect(renderedAttributes).not.toContain("private/path"); + expect(renderedAttributes).not.toContain("relay-secret"); + expect(renderedAttributes).not.toContain("fragment"); + }); + + it("summarizes publish causes without serializing nested failures or defects", () => { + const privateFailureDetail = "private relay response body"; + const privateDefectDetail = "private relay defect detail"; + const cause = Cause.combine( + Cause.fail({ + _tag: "RelayPublishError", + cause: new Error(privateFailureDetail), + detail: privateFailureDetail, + }), + Cause.die(new Error(privateDefectDetail)), + ); + const attributes = AgentAwarenessRelay.relayPublishCauseLogAttributes(cause); + + expect(attributes).toEqual({ + causeReasonCount: 2, + causeFailureCount: 1, + causeDefectCount: 1, + causeInterruptionCount: 0, + causeFailureTags: ["RelayPublishError"], + }); + const renderedAttributes = Object.values(attributes).join(" "); + expect(renderedAttributes).not.toContain(privateFailureDetail); + expect(renderedAttributes).not.toContain(privateDefectDetail); + }); + it("distinguishes pending link credentials from disabled publication", () => { expect( AgentAwarenessRelay.resolveAgentActivityPublishingStartupState({ diff --git a/apps/server/src/relay/AgentAwarenessRelay.ts b/apps/server/src/relay/AgentAwarenessRelay.ts index 4e036e3ea0e..0b955bcd5c6 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.ts @@ -18,6 +18,7 @@ import { RELAY_ACTIVITY_PUBLISH_TYP, signRelayJwt, } from "@t3tools/shared/relayJwt"; +import { getUrlDiagnostics } from "@t3tools/shared/urlDiagnostics"; import * as Cause from "effect/Cause"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; @@ -98,6 +99,44 @@ export function isAgentActivityPublishingEnabled(value: string | null): boolean return value === "true"; } +export function relayUrlLogAttributes(relayUrl: string | undefined) { + if (relayUrl === undefined) { + return { relayUrlConfigured: false }; + } + const diagnostics = getUrlDiagnostics(relayUrl); + return { + relayUrlConfigured: true, + relayUrlInputLength: diagnostics.inputLength, + ...(diagnostics.protocol === undefined ? {} : { relayUrlProtocol: diagnostics.protocol }), + ...(diagnostics.hostname === undefined ? {} : { relayUrlHostname: diagnostics.hostname }), + }; +} + +export function relayPublishCauseLogAttributes(cause: Cause.Cause) { + const failureTags = cause.reasons.flatMap((reason) => { + if (!Cause.isFailReason(reason)) { + return []; + } + const error = reason.error; + if ( + typeof error !== "object" || + error === null || + !("_tag" in error) || + typeof error._tag !== "string" + ) { + return []; + } + return [error._tag]; + }); + return { + causeReasonCount: cause.reasons.length, + causeFailureCount: cause.reasons.filter(Cause.isFailReason).length, + causeDefectCount: cause.reasons.filter(Cause.isDieReason).length, + causeInterruptionCount: cause.reasons.filter(Cause.isInterruptReason).length, + causeFailureTags: [...new Set(failureTags)], + }; +} + export function resolveAgentActivityPublishingStartupState(input: { readonly relayConfigured: boolean; readonly publishEnabled: boolean; @@ -417,12 +456,12 @@ export const make = Effect.gen(function* () { const publishThread: AgentAwarenessRelay["Service"]["publishThread"] = (threadId) => publishThreadUnsafe(threadId).pipe( - Effect.catchCause((cause) => { - return Effect.logWarning("agent activity publish failed", { + Effect.catchCause((cause) => + Effect.logWarning("agent activity publish failed", { threadId, - cause: Cause.pretty(cause), - }); - }), + ...relayPublishCauseLogAttributes(cause), + }), + ), Effect.withSpan("AgentAwarenessRelay.publishThread"), withRelayClientTracing, ); @@ -467,7 +506,7 @@ export const make = Effect.gen(function* () { if (logEnabledWhenReady) { const relayConfig = yield* readRelayConfig.pipe(Effect.orElseSucceed(() => null)); yield* Effect.logInfo("agent activity publishing enabled after link reconciliation", { - relayUrl: relayConfig?.url, + ...relayUrlLogAttributes(relayConfig?.url), }); } return; @@ -499,7 +538,7 @@ export const make = Effect.gen(function* () { break; case "enabled": yield* Effect.logInfo("agent activity publishing enabled", { - relayUrl: relayConfig?.url, + ...relayUrlLogAttributes(relayConfig?.url), }); break; }