Repository navigation
feat(gateway): operator-onboarding funnel stages (ADK track item 4) - #469
Merged
Merged
Conversation
Adds a second funnel, parallel to the buyer/agent-onboarding funnel in
funnel-tracker.ts, tracking operators through kernel_created ->
device_registered -> adapter_ready -> capability_published ->
test_job_passed -> verified_run. Keyed on kernelId (not trace_id) since
operator-onboarding steps are separate CLI/daemon calls spread over
minutes-to-days.
Unlike the buyer funnel's onResponse+detectStage hook, most of these
checkpoints can't be classified from method+route+statusCode alone (per
the lane's stage-inventory research): test_job_passed returns HTTP 200
on both the honest and self-attested paths, and adapter_ready isn't a
route at all. Recording instead happens from explicit call sites, each
gated on the real success condition and wrapped in try/catch so funnel
telemetry can never break a request:
- kernel_created: POST /api/kernels, only on the created-kernel branch
- device_registered: POST /api/setup/register-device (create + update)
- adapter_ready: JobFacade.checkDeviceHealth, gated on healthy===true
and a real adapterType (excludes mock/generic-http), both resolved
from the device's DB row. Extracted isRealAdapterHealthy() so the
gate is independently unit-testable.
- capability_published: POST /api/capabilities, only on 201 create
- test_job_passed: only at the real-device-path return, gated on
finalStatus==="completed", a non-empty submitResult.deviceId, and
kernelId supplied explicitly (never the "kernel_dev_001" default) —
the deviceless self-attest branch structurally cannot reach it
Adds GET /api/admin/observability/operator-funnel behind the existing
admin guard (funnel flag + PCC_OBSERVABILITY_ADMINS).
verified_run is defined in the type but has no recorder yet: nothing on
master can prove a job's evidence was signed by the kernel's own
registered key (that lands with the unmerged D4a readiness check,
PR #428) and the setup test-job path must never claim it (self-attest
or an unverified real-device run are not the same as a verified run).
47 new/changed tests in operator-funnel.test.ts + funnel-tracker.test.ts,
all passing. Full gateway suite: 188 files, 3022 passed, 6 skipped (zero
regressions vs. the 187/2981/6 baseline). tsc --noEmit clean.
Agent: implementer-charlie
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…perator Lane review of the operator-funnel call sites. - device_registered, capability_published and test_job_passed now pass the server-side req.operatorId, as kernel_created already did, so their audit rows name the operator instead of "unknown". PostHog still carries no operator id, because it can be an email. - kernel_created uses a typed request cast instead of `any`. - getOperatorStagesForKernel documents its 10,000-row audit scan. The audit query can't filter by resource id. Tests: operator-funnel 41/41 and funnel-tracker 6/6. The lane mutation-checked five guards (placeholder kernel, completed status, mock adapter, flag, dedup), and every removal fails the suite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ess (#469 round 1) The round-1 cross-family review of 6cf7067 (a text-only run) returned SHIP-WITH-FIXES with four MEDIUM findings. Each was first reproduced at 6cf7067 with a throwaway test (6 of 6 pass). F1: test_job_passed trusted the caller's kernelId. A completed job on a mock device, on another kernel's device, or for a nonexistent kernel all counted. The recorder now resolves the device that ran and requires three things: that device belongs to the named kernel, the kernel exists, and its adapter is a real one (isExecutingAdapter: not mock or generic-http). F2: adapter_ready carried no actor, and any caller counted. Both test_job_passed and adapter_ready now require an authenticated caller and record their operator. The health route passes req.operatorId into JobFacade.checkDeviceHealth. Ownership of the kernel itself is WP-C's guard (#445); nothing is built on that unreviewed PR here. F3: the health check read the device row after the check, so a device moved mid-check was credited to its new kernel. The row is now snapshotted before the check, and the stage is recorded only if kernel and adapter are unchanged afterwards. F4: a stage was marked recorded before its audit row was written, and eviction, a restart or a second instance could write it again. - auditService.log now returns whether the row was written. - The stage counts as recorded only after a confirmed write. - A durable check first asks the audit log for an existing (kernel, stage) row, through a new optional resourceId filter on the audit query (a WHERE clause; no schema change). Tests: 8 funnel regressions plus a db filter test. The eviction test now asserts no duplicate row. Mutation-checked: 8 of 8 removals fail. gateway 3030 passed / 6 skipped (188 files), db 20/20, tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
marked this pull request as ready for review
October 3, 2026 01:02
… in capabilities.ts #469 was SHIP at a9dbf24 but master moved and the PR stopped merging. The one conflict, POST /api/capabilities' created branch: #469's capability_published recorder runs first, unchanged, then master's 201 response with ignoredFields and hints (astra pack 111 MEDIUM 4), unchanged. setup.ts merged automatically beside master's N83 register-device changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…t unguarded (#450 onto #469) Astra's SHIP of the merge-up (8e00dad) noted a MEDIUM test gap: with #450's stricter route, F1a (mock device) and F1b (kernel-la) are refused before the funnel's own conditions run, so removing `device?.kernelId === kernelId` or `isExecutingAdapter(device.adapterType)` from setup.ts left every funnel test passing. Reproduced first: each mutation kept 50/50 green. - F1d: a passed run whose job reports a device of another kernel records no test_job_passed. - F1e: a passed run on a generic-http (placeholder) device records none. Both runs pass #450's route (passed:true), so only the funnel condition keeps the stage out. Mutation check: each removal now fails exactly its own test (51/52). Test-only; 52/52, tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBTXKQ2uqtcrsUtPnQdtf8
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…d #5832) The steward's MERGE NOW simulation found #475 (N85, SHIP @8190cee9) conflicting with #469 (operator funnel stages, c964afa) in packages/gateway/src/facades/job.facade.ts, so #475 now merges after #469. The one conflict was the import block: #475 imports writeJobStatusGuarded and SETTLEMENT_OWNED_MESSAGE; #469 imports recordOperatorStage and adds two exported helpers. Both sides are kept, #475's line first. Every other hunk merged automatically: the guarded status write and the funnel tracking in the health check are in different functions. Results: the targeted suites (settlement-owned-status, carrier x3, funnel-tracker, operator-funnel, job-status-vocab) pass 148/148. Full gateway suite: 3781 passed, 13 skipped. tsc is clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
Merges PR #469 (operator-onboarding funnel stages, ADK track item 4) into #448's branch (fix/n68-coarse-kernel-location). Conflict: packages/gateway/src/routes/kernels.ts - Both #448 and #469 add a new import line immediately after the existing `import type { CreateKernelInput, ... } from "../facades/index.js";` line, so git could not pick an order. - #448 wants: `import type { LocationOptInAuthority } from "../facades/kernel.facade.js";` (board N68 — the type for the POST /api/kernels location-visibility authority check). - #469 wants: `import { recordOperatorStage } from "../services/funnel-tracker.js";` (operator-onboarding funnel telemetry). - Resolution: kept both import lines (facades import before services import, grouped by path). Nothing else in the file conflicted — git auto-merged the POST /api/kernels handler body cleanly, so #448's `locationOptInAuthority(req)` argument to facade.register() and #469's recordOperatorStage() try/catch in the `if (created)` block both survive untouched, each using its own now-present import. agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…iles Conflicts and how each keeps both sides: - db IAuditLogRepository, repositories/audit-log, services/audit-service: master's #469 added a resourceId filter; #538 stores resource ids keyed, so resourceId joins eventType, actor and resourceType as a both-forms filter (the raw value and its keyed hash). - facades/job.facade, routes/capabilities, routes/operator-relay: both sides' imports (#538's closed-schema helpers; master's settlement-owned-status and funnel-tracker). - facades/kernel.facade: #538's declared audit fields plus master's locationVisibility, declared from the closed vocabulary ["exact","approximate"]. - server.ts: #538's closed logger (gatewayLoggerOptions, requestIdHeader false, genReqId) replaces master's GATEWAY_LOGGER_OPTIONS serializer, which it subsumes (no URL, header or raw IP ever). The write-audit hook keeps master's module (services/write-audit-hook.ts, #458) with #538's declared fields ported in (route template, closed method and action, server metrics), and master's telemetryLookalikeHook stays. - observability/closed-sinks: the request log line omits even the keyed client hash for the public telemetry sink (#458's rule). agent: pcc-readmodels (c255d7dc)
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…hema The master merge (63401c2) left 8 failing tests in 4 files; each is master code that postdates #538's base meeting the closed observability schema: - the logger ratchet: 7 raw log messages in routes/operator.ts, routes/operator-work.ts and readmodels/legacy-settlement.ts now go through lit(). - the timing ratchet: services/funnel-tracker.ts (#469) built its span event with spreads; it now uses addClosedEvent with declared fields (the stage from OPERATOR_STAGES, the ids declared) and no spread. - routes/operator-channels.ts: master's N84 added dns_busy to OutboundErrorCode, but #538's hardcoded OUTBOUND_ERROR_CODES lacked it, so a saturated resolver's code was keyed out of the operator log N84 keeps it in. The vocabulary is now a Record over the union (complete by construction: a new code fails to compile until listed). n84-dns-bound reads the warning's declared arguments as they leave. - feedback-attempt-production (#458): its app now mirrors server.ts's closed logger (gatewayLoggerOptions, issued request ids, closedLoggerHooks); audit rows are found by their declared route template; the test that pinned a raw URL in other routes' audit now pins the closed rule (route template only, no query), and the log-line test pins method plus route, with the client's keyed hash for other routes and nothing at all for the sink. Targeted: the 4 files pass (ratchets and funnel 56, feedback 12, n84 19); tsc 0. agent: pcc-readmodels (c255d7dc)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ADK / hackathon track item 4: an operator-onboarding funnel beside the existing buyer funnel. It tracks each kernel's progression from new kernel to verified operator:
kernel_created → device_registered → adapter_ready → capability_published → test_job_passed → verified_runIt is inert unless
PCC_FUNNEL_ENABLED=true, the same switch as the buyer funnel.Keyed on
kernelId, nottrace_id. Operator onboarding spans separate CLI and daemon calls over days, and a kernel id exists at every stage.Recorded by explicit calls after success, not by an onResponse classifier. Two stages can't be judged from the status code:
adapter_readylives inside the device health check;Where each stage is recorded:
kernel_createdPOST /api/kernelsdevice_registeredPOST /api/setup/register-deviceadapter_readyJobFacade.checkDeviceHealth)mockorgeneric-http), with the kernel resolved from the device's own rowcapability_publishedPOST /api/capabilitiestest_job_passedPOST /api/setup/test-jobverified_runtest_job_passed.kernel_dev_001is always rejected.verified_runis defined and documented: its recorder is the readiness check in feat(gateway): operator status derives readiness from canonical rows (ADK D4(a)) #428 once that merges.Guarantees: each stage is recorded once per kernel, with bounded memory. The sinks are an audit row (
operator.funnel, actor = the server-side operator), PostHog (no operator id) and an OTel span event. Every call is wrapped so telemetry can never break a request, and no handler behaviour changes.Admin view:
GET /api/admin/observability/operator-funnel?since=returns distinct kernels per stage and conversion fromkernel_created. It uses the existing fail-closed observability gate and needs the funnel flag.Interactions
WP-C (#445) adds kernel-owner checks to register-device and to capability creation. This PR's recorder calls sit after success in those same handlers. Once #445 lands, ownership is checked first, so the funnel records only for the kernel's owner. Expect mechanical rebase conflicts.
Tests (from master
ac86a404)operator-funnel.test.ts(new)funnel-tracker.test.ts(existing)@pcc/gatewaysuitetsc --noEmit(gateway)Review
🤖 Generated with Claude Code