Repository navigation
Conversation
genki has one trace exporter for the whole platform, configured from the environment, and the portal's Tracing screen was a mock-up. ADR 0040 gives each agency its own connections (part of #97). - A tenant saves a connection with PUT /tenants/{tenant}/tracing/connections/{id} (manage-services, change reason, audited): a host, a public key, the secret key as a secret://tenant/... reference set for that host, the services it covers, a residency and whether it receives content. - The host must be HTTPS and never a private or link-local address, and every call to it goes through the egress guard. In development mode a Langfuse on loopback may use HTTP. - A service is covered by at most one connection. - On Digdir's Langfuse (LANGFUSE_BASE_URL) a new connection starts as global and only an operator sets its residency. Content is refused there until LANGFUSE_INSTANCE_RECEIVES_CONTENT=true, and on an agency's own Langfuse until the agency declares erasure. - POST .../{id}/check opens the connection's project with its keys, tested against a local Langfuse 4.4.0. - TRACES_CONTENT is honoured only in development mode. - The portal's Tracing screen lists, adds, edits, checks and removes connections in live mode.
There was a problem hiding this comment.
🟡 Changes recommended
Critical issues leave secret keys in task logs, permit authorization bypass and loss of audit attribution, allow worker trace-content export outside development, and let concurrent saves create duplicate service coverage.
4 open issues
What changed in this PR
Adds tenant-specific Langfuse connection management, replacing the portal’s unavailable Tracing tab. This establishes connection configuration for the later run-export and deletion work in #196 and #197.
Changes:
- Adds audited connection APIs with secret-reference validation, guarded outbound checks, service coverage rules, and operator-controlled residency.
- Adds portal controls to list, create, edit, check, and remove connections.
- Restricts API trace content to development mode and adds documentation, translations, and tests.
| File | Description |
|---|---|
docs/api/access.md |
Documents connection endpoints and restrictions. |
apps/portal/src/screens/integrations/LiveTracing.tsx |
Implements connection management UI. |
apps/portal/src/screens/Integrations.tsx |
Enables the live Tracing tab. |
apps/portal/src/screens/integrations-tracing.test.tsx |
Tests connection display and creation. |
apps/portal/src/screens/changes/changes.ts |
Maps tracing audit actions to labels. |
apps/portal/src/i18n/nb/operate-integrations.ts |
Adds Bokmål connection text. |
apps/portal/src/i18n/nb/core-screens.ts |
Adds Bokmål change-log labels. |
apps/portal/src/i18n/en/operate-integrations.ts |
Adds English connection text. |
apps/portal/src/i18n/en/core-screens.ts |
Adds English change-log labels. |
apps/portal/src/api/tracing.ts |
Adds the typed tracing API client. |
apps/portal/src/api.ts |
Registers the tracing client. |
apps/api/src/trace-connections.ts |
Implements storage, validation, checks, and routes. |
apps/api/src/trace-connections.test.ts |
Tests connection rules and access controls. |
apps/api/src/rights.ts |
Defines tracing administration permissions. |
apps/api/src/platform-modules.ts |
Adds tracing dependencies to modules. |
apps/api/src/openapi/tags.ts |
Categorizes tenant tracing endpoints. |
apps/api/src/openapi/access.ts |
Documents tracing schemas and routes. |
apps/api/src/main.ts |
Wires tracing settings and API content gating. |
apps/api/src/i18n/nb/errors.ts |
Adds Bokmål tracing errors. |
apps/api/src/i18n/en/errors.ts |
Adds English tracing errors. |
apps/api/src/composition.ts |
Registers and supplies the tracing module. |
apps/api/src/change-reason.ts |
Exempts connection checks from change reasons. |
apps/api/src/boot-guard.ts |
Adds the development-only trace-content predicate. |
apps/api/src/admin-tasks.test.ts |
Includes tracing writes in audit-route coverage. |
.env.example |
Explains content gating and instance erasure confirmation. |
Review effort: Balanced
| for (const service of next.services) { | ||
| const covering = await coveringService(tenant, service); | ||
| if (covering && covering.id !== next.id) |
There was a problem hiding this comment.
Kept: the coverage check is not atomic across API instances, since config entries have a version check per id and no lock across ids; it would take a store-level lock. Saving a connection is a rare, audited admin action, and #196 makes the outcome safe: a service that two connections cover is sent to neither, so a race cannot send a run to the wrong Langfuse.
…ghts
- PUT .../tracing/connections/{id} checks the connection before the
admin task starts, so a key typed in clear is refused without ever
entering a task's input.
- The connection routes act with the principal enterTenant returns, so
an operator who is a member gets only their own rights there, and a
support session keeps its actor.
- The worker's platform-wide exporter sends text only in development
mode, as the API's does, through the shared tracesContent.
…langfuse-connections
…langfuse-connections
- The secret is checked against its host before the admin task starts, so a key typed in place of a secret's name never reaches the audit log; the portal field is now "Navn på hemmeligheten". - A host is an address only, without a path, credentials or a trailing dot, so Digdir's Langfuse cannot be taken for an agency's own. - Outside development mode no connection receives content while LANGFUSE_BASE_URL is unset, and .env.example names it. - A connection to Digdir's Langfuse saved without a residency keeps the operator's, and deleting an unknown connection answers 404. - The portal asks for another name instead of replacing a connection.
|
Ready for review. Copilot and a claim-checking review are done (no blind review) and CI passes. Open: the thread on two saves at once claiming a service (not atomic across instances; #196 sends such a service nowhere); the edit form for a connection to Digdir's Langfuse still offers residency and the declaration, removing has no confirm step, and egress errors read in English. |
|
Found while testing the stack in the portal:
|
…ress alone The form asked for the name of a stored secret, but no portal screen stores one for a host, so an agency could not finish the setup. The form now takes the secret key, stores it bound to the connection's address and saves the connection with a reference to it; an empty field keeps the stored key, and a new address needs the key again. The id field says what it accepts and refuses anything else in the portal's language, and the check says that Langfuse does not check the public key.
|
Fixed in df7dafd:
Checked by "stores the secret key for the connection's address alone, then saves the connection with only a reference to it", "refuses an id with anything but small letters, digits and hyphens, in the language of the portal", "keeps the stored secret key when it is left empty, and asks for it again when the address changes" and "says the check shows the secret key opens the project, and that Langfuse does not check the public key"; |

Stacked on #194.
What and why
genki has one trace exporter for the whole platform, set from the environment, and the portal's Tracing tab said it was not available. ADR 0040 gives each agency its own connections to a Langfuse. Part of #97.
PUT /tenants/{tenant}/tracing/connections/{id}(manage-services, change reason, audited): a host, a public key, the secret key as asecret://tenant/...reference set for that host, the services it covers, a residency and whether it receives content.GETlists them without the secret,DELETEremoves one and answers 404 for one that does not exist.enterTenantreturns.LANGFUSE_BASE_URL) a new connection starts asglobal, only an operator sets its residency (PUT /operator/tracing/connections/{tenant}/{id}/residency), and saving it again without a residency keeps the operator's. Content is refused there untilLANGFUSE_INSTANCE_RECEIVES_CONTENT=true, on an agency's own Langfuse until the agency declares erasure, and outside development mode whileLANGFUSE_BASE_URLis unset.POST .../{id}/checkopens the connection's project with its keys.TRACES_CONTENTis honoured only in development mode, in the API and the worker (tracesContentin@genai/composition).langfuse-<id>) set for the connection's address alone, then saves the connection with a reference to it; an empty key keeps the stored one, and a new address needs the key again. The id field says it takes small letters, digits and hyphens, and refuses anything else in the portal's language. The check says that the secret key opens the project, since Langfuse accepts any public key with the right secret key.docs/api/access.md,.env.example, OpenAPI. i18n:apps/api/src/i18n/{en,nb}/errors.ts, portaloperate-integrationsand the change-log labels incore-screens.Not in this PR: sending a run to a connection (#196) and deleting what was sent (#197). Two saves at the same moment can both claim a service; #196 sends such a service nowhere.
Screenshots
Before: the Tracing tab says it is not available

After: a connection to a local Langfuse, checked against its project

The form takes the secret key and says what an id may hold

The check names the project the secret key opens, and says the public key is not checked

How it was tested
CI on this head passes (static, size and the four test shards). The top of the stack (Delete what genki sent to an agency's Langfuse when it purges the case #197) passes
npm run check: 470 files, 5103 tests.New tests: "saves a connection with a secret reference and lists it without the secret", "refuses a host that is not HTTPS, or resolves to a private or link-local address", "takes a host only as an address, without a path, credentials or a trailing dot", "refuses another tenant's secret, a secret not set for the host, and a key typed in clear", "covers a service with at most one connection", "starts a connection to Digdir's Langfuse as global, and only an operator sets its residency", "receives content only where erasure holds: Digdir's Langfuse once operators confirm it, an agency's own once it declares it", "refuses content outside development mode while Digdir's Langfuse is not configured", "removes a connection, and answers 404 for one that does not exist", "lets only a person with manage-services change connections, also an operator who is a member", "honours TRACES_CONTENT only in development mode", "shows each connection with its services, who set its residency, and whether it gets text", "stores the secret key for the connection's address alone, then saves the connection with only a reference to it", "asks for another name rather than replacing a connection that has it", "refuses an id with anything but small letters, digits and hyphens, in the language of the portal", "keeps the stored secret key when it is left empty, and asks for it again when the address changes", "says the check shows the secret key opens the project, and that Langfuse does not check the public key".
"checks a connection against its project through the egress path" runs against a local Langfuse 4.4.0 with made-up data, and is skipped without one.
In the portal after
npm start:lokal, the host, the public key, the secret key, services and a reason, and save. Click "Sjekk koblingen": it says the secret key opens the project and that Langfuse does not check the public key.npm run checkpassesChecklist
packages/providers-*docs/architecture/modulesare updated if a contract, boundary or module changed