Skip to content

[OPIK-8183] [BE] feat: accept CIPX device tokens and record device_id on cipx identities - #8094

Merged
LifeXplorer merged 24 commits into
mainfrom
avinahradau/OPIK-8183-cipx-device-token-auth
Sep 3, 2026
Merged

[OPIK-8183] [BE] feat: accept CIPX device tokens and record device_id on cipx identities#8094
LifeXplorer merged 24 commits into
mainfrom
avinahradau/OPIK-8183-cipx-device-token-auth

Conversation

@LifeXplorer

@LifeXplorer LifeXplorer commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Details

Adds a CIPX device-token authentication branch to opik-backend, beside the existing API-key and MCP-OAuth branches, and records the validated machine identity on the Cost Intelligence identity table. An Authorization header starting with opik_cipx_at_ is posted to cost-api's POST /v1/private/ai-spend/devices/validate; cost-api owns the signing key and the device registry, so opik-backend verifies nothing locally (no crypto, no key distribution in this repo) and revocation takes effect within the credentials-cache TTL.

  • AuthCipxTokenUtils.isCipxToken -> CipxTokenValidationService.authenticate, which validates against cost-api and fills the request context straight from the validation response. The react service is not consulted on this path: /validate already returns user_name, workspace_id, workspace_name and device_id, and a device token names a machine rather than a Comet user, so there is nobody for EM to authenticate. (An earlier revision of this branch routed it through AuthService.authorizeOAuth; on a stack where auth-by-username is disabled that is a 404, which verifyResponse turns into a 500 on every ingest.)

  • The ingest path is one cached lookup. The resolved caller — user, workspace, device id — is cached under the token in the existing AuthCredentialsCacheService at its existing apiKeyResolutionCacheTTLInSec (5s), mirroring authenticateUsingApiKey. A warm request is a single cache read with no outbound call at all; a cold request is one validate call. No permissions are passed to the cache, because nothing verified any and nothing may be cached as granted. The cache instance is now provided once in AuthModule and shared by both callers. The MCP-OAuth path is deliberately untouched.

  • No Comet-Workspace header on this path — validated and cached by token alone. A token maps 1:1 to a device and a device to the workspace it enrolled against, so the workspace is derived, never supplied: AuthFilter does not pass the header into the branch (so the branch cannot read it), workspace_name is not in the validate request body, and RequestContext.workspaceName comes from the validate response with no fallback. The header was only ever a null-fallback the response does not need, and a blank one was accepted anyway, so it blocked no attacker holding a token — only an honest client. Token-only keying is safe and slightly better: nothing invalidates this cache by workspace (entries expire purely by TTL, and CacheService exposes only cache/resolve), API-key entries always carry a non-blank workspace because RemoteAuthService.authenticate 403s on a blank one — so authV2-<token>- is a namespace only CIPX occupies — and there is now one entry per device instead of one per header variant, which removes a cache-busting vector. The API-key and MCP-OAuth branches still require the header and must keep doing so; they reach many workspaces.

  • Ingest-only allowlist — this is the whole authorization the credential gets. Because the caller is resolved locally, no path or permission check happens anywhere else, so CipxTokenValidationService.INGEST_ENDPOINTS restricts a device token to exactly what the cipx shipper calls; everything else is 403, checked before the token is validated so a non-ingest path cannot learn whether it is valid. Shape follows the in-repo RemoteAuthService.PUBLIC_ENDPOINTS precedent.

    Method Path
    POST /v1/private/spans/batch
    PATCH /v1/private/spans/batch
    POST /v1/private/traces
    PATCH /v1/private/traces/{uuid}
  • Quotas and permissions are empty for a device token, by decision. There is no Comet user to resolve a role or a quota for, so @UsageLimited (TracesResource, SpansResource) cannot trip and @RequiredPermissions(TRACE_SPAN_THREAD_LOG) goes unverified for this credential. That is accepted for an enterprise AI-Spend workspace, and the allowlist above is what bounds it instead. The exemption is stated on the method that fills the request context so it reads as a decision rather than an oversight — reinstating the EM call is explicitly warned against there, because it does not work.

  • The validator is reached in-cluster, off any publicly routed prefix — and that routing is the control. The call is POST /v1/internal/cipx-device-tokens/validate on http://ai-cost-backend, the k8s service name, the same pattern authentication.reactService.url already follows with http://react-svc:8080. This is a security property, not a routing preference: cost-api's public contract is the /v1/private/ai-spend/ prefix, which nginx forwards to it from the internet, so a validator there would have been an internet-reachable oracle turning a presented token into a user name, workspace id and device id. Under /v1/internal/ nothing routes it publicly. Nothing is exposed today either way — cost-api's device router is not yet on main — so this lands before exposure exists. Do not move it back under the ai-spend prefix; the reason is recorded on the class.

  • The call itself is unauthenticated, as opik's own /v1/internal/usage already is. An earlier revision of this branch carried a shared serviceToken bearer credential; it was justified by public exposure, which the routing above removes, and it cost a long-lived secret with no rotation story plus a two-sided config coupling that fails closed on a mismatch. Config is therefore just { enabled, url }.

  • device_id propagation — carried on RequestContext, put into the reactive context by AsyncUtils, and attached to TracesCreated and TraceCostIntelligenceChanged at publish time in TraceService, following the existing TracesCreated.workspaceName precedent (the cipx subscriber runs off the request path and cannot read the request context).

  • Write pathCostIntelligenceIngestionListener and CipxTraceIdentityDAO write device_id from the event, never from trace.metadata(), so a client cannot claim another machine's identity by writing one into its own metadata. Empty string when absent, matching how harness handles legacy rows.

  • Migration000119_add_device_id_to_cipx_trace_identities.sql on db-app-analytics: ADD COLUMN IF NOT EXISTS device_id String DEFAULT '' (plain String, not LowCardinality: one value per enrolled machine), with a --rollback line.

  • Config / chartcipxTokenValidation: { enabled, url } in config.yml plus CipxTokenValidationConfig, wired like McpOAuthConfig; CIPX_TOKEN_VALIDATION_ENABLED and CIPX_TOKEN_VALIDATION_URL in the Helm chart. url defaults to http://ai-cost-backend, inert while enabled is false. There is no startup validation of url: a relative or blank override boots and fails on the first ingest instead. That is the accepted trade for a smaller config surface — the branch ships disabled with a working in-cluster default, so reaching that state takes a deployment deliberately overriding the value.

Ships disabled by default. The filter checks cipxTokenValidation.enabled before it looks at the token prefix, so while disabled it never inspects the header and existing behaviour is unchanged for self-hosted, OSS and local dev.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-8183

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: whole change (auth branch, ingest allowlist, credential caching, event propagation, DAO/migration, config + chart, tests)
  • Human verification: pending review

Testing

  • mvn compile, mvn test-compile, mvn spotless:check — all clean (scripts/dev-runner.sh --lint-be also runs clean via the pre-commit hook).
  • mvn test -Dtest=CipxTokenValidationServiceTest,AuthFilterCipxTokenTest13/13 pass. Four accepted cases (one per allowlisted method+path pair) and seven rejections: reading traces, reading spans, reading one trace by id, POST /v1/private/traces/search, GET on the allowlisted span-batch path, DELETE on a project, and POST /v1/private/traces/delete. The search and wrong-method cases are there because POST is allowed on other paths — the pair has to bite, not the method alone. Rejections also assert that no cache or validator interaction happened.
  • AuthFilterCipxTokenTest drives the real service through the filter to pin what is invisible from inside it: a device token authenticates with no Comet-Workspace header (verify(context, never()).getHeaderString(WORKSPACE_HEADER)), and two requests carrying different workspace headers produce the identical cache lookup — the property token-only keying buys.
  • mvn test -Dtest=AuthCredentialsCacheServiceTest16/16 pass, including the added cacheAndRetrieveDeviceId (device id survives the round-trip; a non-device credential reads back as no device rather than a blank one).
  • CostIntelligenceIngestionTest gains a device_id assertion (an API-key caller's identity row carries '', and the column is not read from metadata). Not executed here — it is testcontainers-backed with ClickHouse and MySQL.
  • No test asserts a startup failure on configuration: with serviceToken and the @AssertTrue gone there is no such rule left to assert.
  • Full test suite not run. No end-to-end run against a live cost-api yet: the validator endpoint does not exist server-side until the cost-api side of this ticket lands.

Documentation

Chart README rows regenerated for the three new env vars. No user-facing docs: the feature is off by default and SaaS-only.

Deliberately out of scope

  • No read-plane change. The Mac app reads spend aggregates from cost-api only, so no device-scoped read filtering is added here — and the allowlist enforces it.
  • No crypto or signature verification. Validation is delegated on purpose; token format stays cost-api's business.
  • The API-key path is untouched. Dual-accept is required and, while the Claude Code plugin lane exists, permanent.
  • SpansCreated is deliberately untouched. Only cipx_trace_identities stores device_id; cipx_spends and cipx_spend_blocks reach it by semijoin on trace_id, their documented join key since migration 000100. An earlier revision carried the field on span creation; it has been reverted and the file is byte-identical to main.
  • The MCP-OAuth path still resolves its credential per request. Same uncached-authorization shape, but changing it is a separate ticket with its own risk profile.

🤖 Generated with Claude Code

… on cipx identities

Adds a CIPX device-token branch to the authentication filter, beside the
API-key and MCP-OAuth branches. An `Authorization` header starting with
`opik_cipx_at_` is posted to cost-api's validator, which owns the signing
key and the device registry; opik-backend verifies nothing locally, so no
crypto and no key distribution land in this repo, and revocation takes
effect within the credentials cache TTL.

The validated device id reaches the cipx ingestion subscriber the way the
workspace name already does: through RequestContext, onto TracesCreated /
TraceCostIntelligenceChanged / SpansCreated at publish time, and into the
new cipx_trace_identities.device_id column. It is never read from the
trace's metadata, which a client controls.

Ships disabled: with cipxTokenValidation.enabled=false the filter never
inspects the prefix, so self-hosted and local behaviour is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation java Pull requests that update Java code Backend Infrastructure tests Including test files, or tests related like configuration. labels Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
⚓ helm-docs Regenerate Helm chart README 6.53s
☕ spotless — java backend Format Java code 5.49s
🛡️ semgrep — java backend sql Block SQL injection-prone string formatting 3.32s
Total (3 ran) 15.34s
⏭️ 41 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

LifeXplorer and others added 3 commits September 1, 2026 13:09
…n path

Caching only the cost-api validation response left the react-service
auth-by-username call on every request, so each ingest batch from each
enrolled machine still hit EM -- the cost delegated validation exists to
avoid.

The CIPX path now mirrors authenticateUsingApiKey: it resolves the full
credential once (user, workspace, quotas, permissions, device id) and
caches that under the token, so a warm request is one cache read with no
outbound call. Cold requests still validate against cost-api and then go
through AuthService.authorizeOAuth, which is what resolves quotas and
permissions.

Scoped to the CIPX path; the MCP OAuth branch is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…vice credential

An unauthenticated validate endpoint is a token oracle, so the call now
carries a shared service credential as `Authorization: Bearer <token>`.

Config gains `cipxTokenValidation.serviceToken`
(CIPX_TOKEN_VALIDATION_SERVICE_TOKEN), excluded from toString like the
other secret-bearing config fields, and the startup check now refuses a
deployment that enables the branch without it as well as without an
absolute http(s) URL. The chart carries the variable empty; real
deployments should supply it from a secret through envFrom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only cipx_trace_identities stores device_id; cipx_spends and
cipx_spend_blocks reach it by semijoin on trace_id, which migration 000100
already documents as their join key. Carrying the field on span creation
therefore added a value nothing reads.

SpansCreated and SpanService are back to their pre-change state.
TracesCreated and TraceCostIntelligenceChanged keep it -- they are what
feeds the identity write.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LifeXplorer and others added 2 commits September 1, 2026 15:47
…nse, not the react service

Routing a device token through AuthService.authorizeOAuth made it depend on
the react service's auth-by-username endpoint, which is disabled on the
local stack and answers 404 -- and verifyResponse routes anything but
200/400/401/403 to unexpectedRemoteError, so every device-token ingest was
a 500. Even where the endpoint is enabled it would 401: cost-api sends
`cipx-device-<uuid>` as the user name, which is not a Comet user.

The validate response already carries user name, workspace id, workspace
name and device id, so the request context is now filled from it directly.
The caching stays, and gets cheaper: the warm path was already one cache
read, and the cold path is now one call instead of two.

Quotas and permissions are consequently empty, which is why the request
context is filled in one place with the exemption stated there: a device
token names a machine in an enterprise AI-Spend workspace, not a user, so
@UsageLimited cannot trip and @RequiredPermissions goes unverified for it.
An ingest-only allowlist is what bounds the credential instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolving the caller from the validation response means no path or
permission check happens anywhere else, so the allowlist is the whole
authorization a device token gets. These are the tests for it: accepted on
the four endpoints the cipx shipper calls, rejected with 403 on reads, on a
delete, and on the right path with the wrong method -- the last one proving
the method and path have to match as a pair, since POST is allowed
elsewhere. Rejections assert no lookup happened either, so a non-ingest
path cannot learn whether the token is valid.

The accepted cases are served from cache so the allowlist is what is under
test rather than the validator call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… prefix

The validator was under /v1/private/ai-spend/, which is cost-api's public
routing contract -- nginx forwards exactly that prefix to it, so the
endpoint would have been reachable from the internet. An endpoint that
answers a presented token with a user name, workspace id and device id is a
validation oracle, and the service credential would have been the only
thing in front of it.

It now sits at /v1/internal/cipx-device-tokens/validate, which nothing
routes publicly, and the configured URL defaults to the in-cluster service
name http://ai-cost-backend rather than the public front door -- the same
pattern authentication.reactService.url already follows. The endpoint's
only caller is this one, in-cluster.

Not a hole being closed: cost-api's device router is not on main, so
nothing is exposed today. This lands before exposure exists. The service
credential is unchanged -- routing stops the secret being the only control,
it does not replace it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The credential was justified by public exposure, and moving the validator
under /v1/internal/ removed that: nothing forwards the endpoint from
outside, so the secret was only ever guarding a cluster-internal path.
Against that it was a long-lived credential with no rotation story and a
two-sided config coupling that fails closed on a mismatch. Routing is the
control now, as it already is for /v1/internal/usage.

Config is back to `cipxTokenValidation: { enabled, url }`. The bearer
header, the field, and the startup check go with it.

One consequence, accepted for the smaller config surface: without the
startup check a relative or blank `url` boots and fails on the first ingest
instead. The branch ships disabled with a working in-cluster default, so
that needs a deployment to have overridden the value deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Integration Group 14

 44 files   44 suites   3m 28s ⏱️
373 tests 370 ✅ 2 💤 0 ❌ 1 🔥
368 runs  365 ✅ 2 💤 0 ❌ 1 🔥

For more details on these errors, see this check.

Results for commit 240a215.

♻️ This comment has been updated with latest results.

ValidatedCipxToken claimed every field but deviceId mirrors what any other
credential resolves to. userName does not: for an API key or a session it
is a Comet username, and here it is the device's MDM-provisioned email
address, which lands in traces.created_by. That made the one field that
differs the one described as identical, and the validator's own code is not
in this repo to check against.

Corrected there, plus a line where it enters the shared request context --
every other writer of that field puts a username in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LifeXplorer and others added 3 commits September 2, 2026 11:17
Workspace ids, device ids, trace ids and token secrets were literals,
which invites coupling to their text and reads as if the values mattered.
They are now generated per class -- UUIDs where the real value is
uuid-shaped, inline RandomStringUtils elsewhere, following the getRandomId
pattern already in AuthCredentialsCacheServiceTest.

The cache test's device id is generated once into a local and used as both
the argument and the expectation, so the pair cannot drift.

Left literal on purpose, because randomising them would stop the tests
testing anything: the opik_cipx_at_ prefix, which is what selects the auth
branch; every allowlist path; and the __ai_spend_ workspace fence, which is
the contract shape of a device's bound workspace -- only its org portion is
data. The trace id has to stay uuid-shaped for the same reason, since the
allowlist's trace-update pattern matches only a UUID segment.

Also gives the project-delete rejection its own id rather than reusing the
workspace id in a project path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or really returns

Both fixtures built the user name as cipx-device-<uuid>, which is the
fallback the validator uses only when the registry address is blank. The
normal case is the device's MDM-provisioned email, so the fixtures now hold
a generated one in the style the cipx ingestion test already uses. Nothing
asserts on the value today; the point is that the next reader, and any
future assertion, sees the right shape.

No test for the fallback: cost-api owns it and covers it, and asserting it
through a mock here would only test the mock.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Python SDK E2E Tests Results (Python 3.14)

296 tests  ±0   288 ✅ ±0   4m 4s ⏱️ -22s
  1 suites ±0     8 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit dcc66e3. ± Comparison against base commit 8254d56.

♻️ This comment has been updated with latest results.

@LifeXplorer
LifeXplorer marked this pull request as ready for review September 2, 2026 12:24
@LifeXplorer
LifeXplorer requested review from a team as code owners September 2, 2026 12:24
@CometActions

CometActions commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Worth a test, but not testable yet.

The device-token path is behind cipxTokenValidation.enabled (CIPX_TOKEN_VALIDATION_ENABLED, default false), and even with it on the filter needs a reachable cost-api validator at CIPX_TOKEN_VALIDATION_URL to resolve a token — an OSS env has neither, so opik_cipx_at_... is byte-identical to today's behaviour there and there is nothing for an e2e test to observe yet. Not a gap in this PR: AuthFilterCipxTokenTest / CipxTokenValidationServiceTest already pin the accept/reject/403-outside-ingest paths at the unit level, and the CostIntelligenceIngestionTest addition pins device_id empty for an API-key caller. Recording it as a deferral so the e2e side gets re-triaged when the flag is turned on rather than shipping unexercised. Separately: no capability in the QA taxonomy models which credential types the ingest endpoints accept, so capabilities is left empty rather than mapped to a UI capability that would not catch this.

Testable once (flag): CIPX_TOKEN_VALIDATION_ENABLED=true on an env that also runs a cost-api validator at CIPX_TOKEN_VALIDATION_URL able to mint and validate a device token — the e2e estate has no such service and no way to stub one today

What a test would assert: The credential type is the axis, not the endpoint. With the flag on, Authorization: opik_cipx_at_... is accepted on POST /v1/private/traces, PATCH /v1/private/traces/{id} and POST|PATCH /v1/private/spans/batch and rejected with 403 ("CIPX device tokens are accepted on trace and span ingest only") everywhere else under /v1/private/*; a bad token is 401 and an unreachable validator is 500. A trace ingested this way lands in the workspace the device is enrolled to with created_by set to the device's MDM email rather than a Comet username, which is what a user sees in the Logs table's Created by column. The general rate-limit bucket also keys on the device id instead of the API key for this credential (unchanged for API-key callers, where getRateLimitPrincipal falls back).

Recorded rather than dropped, so this resurfaces when the gate opens — a flag is usually flipped by a PR that touches no product code and so is never triaged on its own.

Not testable yet. device_id on cipx_trace_identities cannot be asserted from the e2e estate even once the flag is on: nothing in Opik reads the table back — no resource, no endpoint, no frontend reference to cipx at all — so the column is write-only here and only cost-api consumes it. Worth flagging that the per-row positional binds went 25 -> 26 and the listener swallows its own failures, so a slipped index would write every identity row shifted and log rather than surface; CostIntelligenceIngestionTest is the only thing standing under that, which looks like the right place for it.

also touches Backend (Java API / internal), Deployment / Helm

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

Re-checked after a push on 03 Sep 08:28 UTC — nothing the verdict depends on changed.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Backend Tests - Unit Tests

1 948 tests  +20   1 947 ✅ +20   53s ⏱️ +2s
  169 suites + 3       1 💤 ± 0 
  169 files   + 3       0 ❌ ± 0 

Results for commit 5b11640. ± Comparison against base commit 8254d56.

♻️ This comment has been updated with latest results.

@BorisTkachenko BorisTkachenko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, one question about for scope of this change.

Comment on lines +51 to +58
/**
* The only endpoints a device token may reach. Rejection is by this list, never by the absence of a binding.
* Shape follows {@code RemoteAuthService.PUBLIC_ENDPOINTS}: path regex to allowed methods.
*/
private static final List<IngestEndpoint> INGEST_ENDPOINTS = List.of(
new IngestEndpoint(Pattern.compile("^/v1/private/spans/batch/?$"), Set.of("POST", "PATCH")),
new IngestEndpoint(Pattern.compile("^/v1/private/traces/?$"), Set.of("POST")),
new IngestEndpoint(Pattern.compile("^/v1/private/traces/" + UUID_REGEX + "/?$"), Set.of("PATCH")));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be not fully in the context. But as far as I recall, we were planning to use this token not only for ingestion but also by the macOS application for retrieval. So the user will only see the data which was ingested from his device.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is fine. At present retrieval goes via ai-cost-backend application, not via opik. It will be on ai-cost-backend side to validate device token and control data fetch

@LifeXplorer
LifeXplorer merged commit 9a8196b into main Sep 3, 2026
76 of 78 checks passed
@LifeXplorer
LifeXplorer deleted the avinahradau/OPIK-8183-cipx-device-token-auth branch September 3, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend documentation Improvements or additions to documentation Infrastructure java Pull requests that update Java code 🔴 size/XL tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants