Gateway prefix retirement
The legacy Rails gateway (rulebase-web/rulebase-api2/src/lib/legacy-rails-gateway/)
forwards a request to Rails only when API2 has no method-specific handler for
it and its first path segment is in rails-public-route-prefixes.json.
Prefixes in retired-rails-route-prefixes.json are answered locally with
Rails’ JSON 404 and outcome=retired. Retiring a prefix is how “Rails is
idle” becomes “Rails receives no public traffic” for that route family; the
separate audit of API2’s direct RAILS_INTERNAL_BASE_URL callers is what
turns that into “Rails is unused”.
The two JSON files are kept consistent with the Rails route table by
rulebase-api/spec/lint/api2_gateway_compatibility_prefixes_spec.rb:
allowlist = Rails route prefixes − blocked (/internal, /good_job, …) −
retired, and every retired prefix must still be a Rails route.
Retirement procedure
Run this once per prefix (or per batch). Every step is mechanical; the only judgement is step 2.
This is step 4 of the
Workstream 1 execution sequence;
/knowledge_base_documents
is the worked precedent. Before starting a batch, read the plan, refresh the
boundary measurements, reconcile merged revisions, and add a ledger row for
the batch when the PR is up.
-
Measure a clean window. Find the last gateway-behaviour deploy:
git log -3 --format='%h %ci %s' -- rulebase-web/rulebase-api2/src/lib/legacy-rails-gateway flightcontrol.cue, then take the time that deploy finished in Flightcontrol as the window start. Querylegacy_rails_gatewayevents withoutcome=forwardedafter that point in both regions (/ecs/fc-rulebase-api2-prod-5whj12suTaskDefinition, us-east-1;/ecs/fc-rulebase-api2-prod-eu-9rfp06kTaskDefinition, eu-central-1), grouped by first path segment,method,path,status. Never use a window that straddles a deploy that changed forwarding behaviour: counts from before the cutover are not evidence that a prefix is live.What the gateway logs do and do not prove. Zero
forwardedevents for a prefix means no inbound public traffic reached Rails through the gateway for it. It does not mean Rails no longer serves the prefix: about twenty API2 modules call the Rails origin directly with a plainfetchagainstenv.RAILS_INTERNAL_BASE_URL(for examplelib/qa-sampling-recovery/rails.ts,lib/sales-call-compliance/rails.ts,lib/run-scheduled-export.ts,lib/scorecard-chat-context.tshitting/scorecards/:id,routes/slack-interactions.ts), bypassingcreateLegacyRailsGatewayand emitting nolegacy_rails_gatewayevent. Retiring a prefix only changes the gateway’s handling of inbound public requests, so those internal calls keep working — but a “Rails is unused” claim needs all three legs:- gateway
legacy_rails_gatewayevents (inbound public traffic); - Rails’ own request logs
(
/ecs/fc-rulebase-api-prod-xv109mmTaskDefinition, us-east-1;/ecs/fc-rulebase-api-prod-eu-u56f166nTaskDefinition, eu-central-1), filteredispresent(path) and path != "/up", which see every request from any source; rg RAILS_INTERNAL_BASE_URL rulebase-web/rulebase-api2/srcfor direct API2 callers of the prefix.
The third leg is the one that is easy to forget.
- gateway
-
Classify the prefix. For each Rails route under it (
bin/rails routes -g '^/<prefix>'):- API2 owns it natively if
nativelyOwnedMethods(app, path, [method])is non-empty for every Rails method/path pair, and the handler does not callproxyLegacyRailsRequest(public media, Zendesk sidebar, Outreach webhooks and conversation uploads still proxy deliberately). AnALLregistration is not ownership. - Rails genuinely serves it if any first-party client (rulebase-ui,
rulebase-admin, rulebase-beam-web, rulebase-zendesk-app, api2, ML,
lambdas), a provider callback, or a documented public path builds the
URL. Search with
rg -e "['\"]/<prefix>([/'\"?.]|$)" --glob '!**/__generated__/**' --glob '!**/*.test.*'(also check template literals). Leave the prefix and note the caller. - Dead if neither of the above holds and the window shows no forwarded
request that Rails answered with a non-
404status.
Both “owned” and “dead” are allowlist decisions: they make the prefix unreachable from the public gateway. Neither certifies that Rails serves nothing under it (see the three legs above).
- API2 owns it natively if
-
Add ownership contracts. For a natively owned family, add an exact
method/pathentry per Rails route toPUBLIC_ROUTE_OWNERSHIP_CONTRACTSinpublic-route-ownership.tswith the status an unauthenticated request gets and the native structured event (ornull). Add at least oneexpected: "retired"request under the prefix toUNOWNED_PUBLIC_REQUESTS(a method Rails never routed, or any path for a dead prefix).src/app.route-ownership.test.tsexecutes these against the composed app with the fallback enabled and fails if ownership regresses toALL, if the handler proxies Rails, or if the gateway would forward. -
Move the prefix. Remove it from
rails-public-route-prefixes.json, add it toretired-rails-route-prefixes.json(both sorted and unique), then runcd rulebase-web/rulebase-api2 && pnpm typecheck && pnpm testandcd rulebase-api && bin/rspec spec/lint/api2_gateway_compatibility_prefixes_spec.rb. -
Verify after deploy.
outcome=retiredfor the prefix should be scanner noise or stale-tab residue only. Anyretiredevent from a current client build or a non-browser caller is a rollback trigger.
Rollback
- Immediate hold: set
LEGACY_RAILS_FALLBACK_UNKNOWN_PATHS=forwardon the API2 web service. Underforwardthe retired and allowlist checks are skipped and every unowned path forwards to Rails again, without a deploy of code. - Permanent: revert the retiring PR. This restores the prefix to the allowlist and removes its contracts; the drift spec keeps both files consistent.
2026-09-21 batch
Measurement window. The last gateway-behaviour changes on main before
this batch were ef94fa43b (2026-09-20, delete the Action Cable bridge,
#12624), bf823fcb0 (2026-09-19, cut off /cable, #12564) and f043a0ca5
(2026-09-19, retire /knowledge_base_documents, #12574); the last
flightcontrol.cue change was 0ffdf3d6f (2026-09-19, #12554). The clean
window therefore starts at the completion of the 2026-09-20 deploy.
Production CloudWatch was not reachable from the authoring session, so the
batch below is based on the operator-supplied 24 h census ending 2026-09-21
(1,131 forwarded US requests: /conversations 1,076 uploads, /admin 25,
/pages 12, /agent 9, /clients 1; everything except uploads scanner
paths) plus static analysis, and is deliberately conservative: only prefixes
that are fully owned by API2 or have no caller anywhere in the monorepo were
retired. A later operator query of Rails’ own request logs over a clean 44 h
window (the second evidence leg, so it includes direct API2 fetches) found
Rails serving only conversations (2,002 US), conversation_images
(758 US / 275 EU), internal (232 US / 108 EU; gateway-blocked) and
integrations (10 US) — none of the 85 retired prefixes appeared. “No caller” covers URL construction in first-party clients and the
gateway path only; API2’s direct RAILS_INTERNAL_BASE_URL fetches are being
audited in a sibling session and are unaffected by allowlist changes.
Out of scope (sibling sessions): /conversations (live uploads),
/conversation_images and /rails (ActiveStorage), /integrations (Slack
block actions) stay on the allowlist untouched.
Retired: API2 owns every Rails route natively (25 prefixes)
Every Rails method/path under these prefixes has a method-specific API2
handler that does not proxy Rails; anonymous requests get API2’s 401. Each
route is listed in PUBLIC_ROUTE_OWNERSHIP_CONTRACTS.
| Prefix | Rails routes | API2-owned | Callers (ui/admin/beam/zendesk/api2) |
|---|---|---|---|
/agent_knowledge_gap_reports | 2 | 2 | 1/0/0/0/1 |
/assignment_queue_memberships | 1 | 1 | 1/0/0/0/1 |
/check_results | 1 | 1 | 1/0/0/0/2 |
/compliance_alerts | 2 | 2 | 3/0/0/0/1 |
/compliance_cases | 2 | 2 | 3/0/0/0/1 |
/compliance_rules | 2 | 2 | 2/0/0/0/1 |
/conversation_assignments | 2 | 2 | 2/0/0/0/1 |
/dashboard_widget_types | 3 | 3 | 7/0/0/0/1 |
/digest_emails | 4 | 4 | 1/0/0/0/2 |
/disputes | 2 | 2 | 4/0/0/0/1 |
/evaluation_contest_feedback_attachments | 1 | 1 | 1/0/0/0/1 |
/evaluation_scorecard_criterion_results | 2 | 2 | 2/0/0/0/1 |
/features | 1 | 1 | 2/0/0/0/2 |
/jira_request_types | 2 | 2 | 0/0/1/0/1 |
/macros | 2 | 2 | 1/0/0/0/1 |
/notion_pages | 1 | 1 | 1/0/0/0/2 |
/organization_data_sources | 7 | 7 | 7/0/0/0/3 |
/permissions | 1 | 1 | 2/0/0/0/1 |
/program_controls | 3 | 3 | 2/0/0/0/2 |
/qa_evaluation_attachments | 1 | 1 | 0/0/0/0/1 |
/reports | 2 | 2 | 1/0/0/0/1 |
/scorecard_evaluation_results | 1 | 1 | 2/0/0/0/1 |
/search | 1 | 1 | 1/0/0/0/3 |
/session | 1 | 1 | 8/1/4/0/4 |
/zendesk_brands | 2 | 2 | 2/0/0/0/1 |
Retired: no caller in either service (60 prefixes)
Rails still routes these, but no file in rulebase-ui, rulebase-admin,
rulebase-beam-web, rulebase-zendesk-app, api2, rulebase-ml, lambdas or
scripts builds the URL (generated OpenAPI types and tests excluded), and API2
has no handler. /pages is the legacy Pages feature the app navigation guide
already marks as removed; its forwarded traffic in the census was
/pages/index.astro.mjs.map scanner probes.
| Prefix | Rails routes | API2-owned | Callers (ui/admin/beam/zendesk/api2) |
|---|---|---|---|
/agent_knowledge_gaps | 2 | 0 | 0/0/0/0/0 |
/anomaly_detection_rules | 6 | 0 | 0/0/0/0/0 |
/call_scenario_assessment_criteria | 4 | 0 | 0/0/0/0/0 |
/call_scenario_persona_languages | 1 | 0 | 0/0/0/0/0 |
/call_scenario_persona_tones | 1 | 0 | 0/0/0/0/0 |
/call_scenario_personas | 6 | 0 | 0/0/0/0/0 |
/call_scenarios | 12 | 0 | 0/0/0/0/0 |
/call_scripts | 6 | 0 | 0/0/0/0/0 |
/company_fields | 6 | 0 | 0/0/0/0/0 |
/compliance_reviews | 7 | 0 | 0/0/0/0/0 |
/compliance_rule_documents | 6 | 0 | 0/0/0/0/0 |
/compliance_rule_sections | 6 | 0 | 0/0/0/0/0 |
/content_types | 6 | 0 | 0/0/0/0/0 |
/conversation_cx_risk_snapshot_scorecard_results | 1 | 0 | 0/0/0/0/0 |
/conversation_filters | 6 | 0 | 0/0/0/0/0 |
/conversation_part_call_detail_transcript_messages | 2 | 0 | 0/0/0/0/0 |
/conversation_part_call_details | 1 | 0 | 0/0/0/0/0 |
/conversation_parts | 2 | 0 | 0/0/0/0/0 |
/customer_accounts | 8 | 0 | 0/0/0/0/0 |
/customer_complaint_categories | 6 | 0 | 0/0/0/0/0 |
/customer_complaints | 11 | 0 | 0/0/0/0/0 |
/demo_audit_events | 6 | 0 | 0/0/0/0/0 |
/evaluation_assignments | 4 | 0 | 0/0/0/0/0 |
/evaluation_batch_assignees | 1 | 0 | 0/0/0/0/0 |
/evaluation_batch_conversations | 9 | 0 | 0/0/0/0/0 |
/evaluation_batches | 16 | 0 | 0/0/0/0/0 |
/evaluation_compliance_criterion_group_results | 2 | 0 | 0/0/0/0/0 |
/google_sheets_spreadsheets | 1 | 0 | 0/0/0/0/0 |
/group_employees | 1 | 0 | 0/0/0/0/0 |
/group_memberships | 1 | 0 | 0/0/0/0/0 |
/guide_sections | 6 | 0 | 0/0/0/0/0 |
/import_historical_conversations_tasks | 3 | 0 | 0/0/0/0/0 |
/import_knowledge_base_documents_tasks | 3 | 0 | 0/0/0/0/0 |
/knowledge_base_crawl_tasks | 3 | 0 | 0/0/0/0/0 |
/knowledge_base_responses | 1 | 0 | 0/0/0/0/0 |
/knowledge_base_sections | 8 | 0 | 0/0/0/0/0 |
/knowledge_base_sources | 7 | 0 | 0/0/0/0/0 |
/lines_of_business | 4 | 0 | 0/0/0/0/0 |
/manual_evaluations | 4 | 0 | 0/0/0/0/0 |
/marketing_reviews | 6 | 0 | 0/0/0/0/0 |
/pages | 8 | 0 | 0/0/0/0/0 |
/phishing_case_runs | 4 | 0 | 0/0/0/0/0 |
/phishing_case_training_run_answers | 6 | 0 | 0/0/0/0/0 |
/phishing_case_training_runs | 3 | 0 | 0/0/0/0/0 |
/phishing_cases | 4 | 0 | 0/0/0/0/0 |
/pinned_conversations | 3 | 0 | 0/0/0/0/0 |
/procedures | 7 | 0 | 0/0/0/0/0 |
/qa_evaluations | 3 | 0 | 0/0/0/0/0 |
/report_types | 7 | 0 | 0/0/0/0/0 |
/rule_types | 2 | 0 | 0/0/0/0/0 |
/run_conversation_cx_risk_snapshot_tasks | 3 | 0 | 0/0/0/0/0 |
/scorecard_criterion_score_options | 7 | 0 | 0/0/0/0/0 |
/sheet_rows | 4 | 0 | 0/0/0/0/0 |
/simulated_calls | 6 | 0 | 0/0/0/0/0 |
/simulation_assignments | 6 | 0 | 0/0/0/0/0 |
/simulations | 26 | 0 | 0/0/0/0/0 |
/test_columns | 4 | 0 | 0/0/0/0/0 |
/test_rows | 4 | 0 | 0/0/0/0/0 |
/test_values | 3 | 0 | 0/0/0/0/0 |
/voice_profiles | 7 | 0 | 0/0/0/0/0 |
Kept on the allowlist (73 prefixes as of this batch; 70 after the 2026-09-23 batch below)
Rails still serves at least one route under each of these that a first-party
client calls, or the prefix belongs to a sibling migration. /admin
(rulebase-admin, Admin::BaseController) and /agent (agent UI,
Agent::BaseController) are kept even though their census traffic was
scanner noise: both have authenticated Rails routes with live callers, so they
are not retired until they are migrated. Since
ENG-3385 the scanner round trip
is no longer paid either: isScannerShapedPath() answers dotfile segments and
.php/.asp/.jsp/.cgi files under any allowlisted prefix (except /rails,
whose Active Storage *filename glob is user-controlled) with Rails’ 404
and outcome=rejected: dotfile segments because Rails’ default segment
pattern [^\/.?]+ cannot route them, the file suffixes because no Rails
route, registered MIME extension or first-party client uses them. Real routes
under the same prefixes, and real (.:format) suffixes such as .json/.csv,
still forward.
/zendesk-app is API2-owned but its sidebar handler still proxies Rails.
| Prefix | Rails routes | API2-owned | Callers (ui/admin/beam/zendesk/api2) |
|---|---|---|---|
/accounts | 30 | 23 | 6/0/0/0/8 |
/admin | 68 | 7 | 14/38/0/0/17 |
/admin_portal_links | 1 | 0 | 1/1/0/0/0 |
/agent | 4 | 0 | 5/0/0/0/6 |
/agent_performance_reports | 2 | 0 | 0/1/0/0/0 |
/agent_test_columns | 3 | 2 | 1/0/0/0/1 |
/alerts | 4 | 0 | 1/0/0/0/0 |
/api_keys | 6 | 3 | 3/0/0/0/2 |
/assignment_queues | 8 | 7 | 8/0/0/0/2 |
/audit_events | 2 | 1 | 1/0/0/0/1 |
/call_script_guidelines | 6 | 1 | 0/0/0/0/1 |
/calls | 6 | 2 | 3/0/1/0/3 |
/clients | 6 | 2 | 2/0/0/0/1 |
/compliance_agents | 23 | 22 | 23/0/0/0/7 |
/compliance_criterion_groups | 6 | 5 | 3/0/0/0/2 |
/compliance_policies | 6 | 1 | 1/0/0/0/1 |
/compliance_tests | 12 | 6 | 7/0/0/0/2 |
/controls | 4 | 3 | 5/0/0/0/1 |
/conversation_cx_risk_snapshots | 5 | 1 | 1/0/0/0/1 |
/conversation_fields | 6 | 0 | 0/0/0/0/1 (doc comments only; retired 2026-09-23, see below) |
/conversation_images | 1 | 1 | 0/0/0/0/6 |
/conversation_resolution_assignment_rules | 7 | 5 | 4/0/0/0/2 |
/conversation_threads | 2 | 1 | 1/0/0/0/1 |
/conversations | 66 | 43 | 51/1/5/0/60 |
/customer_fields | 6 | 0 | 0/0/0/0/1 (doc comments only; retired 2026-09-23, see below) |
/customer_issues | 8 | 1 | 1/0/0/0/2 |
/dashboard_widget_type_groups | 4 | 3 | 2/0/0/0/1 |
/data_sources | 5 | 2 | 3/0/0/0/2 |
/employee_fields | 6 | 0 | 0/0/0/0/1 (doc comments only; retired 2026-09-23, see below) |
/employees | 14 | 11 | 16/0/0/0/6 |
/evaluation_contests | 3 | 1 | 1/0/0/0/1 |
/evaluation_sampling_rules | 7 | 5 | 6/0/0/0/2 |
/evaluation_schedules | 6 | 4 | 3/0/0/0/1 |
/evaluation_scorecard_criterion_result_comments | 5 | 3 | 2/0/0/0/1 |
/frameworks | 2 | 1 | 1/0/0/0/2 |
/groups | 13 | 4 | 2/0/0/0/2 |
/integrations | 3 | 0 | 0/0/0/0/2 |
/invitations | 10 | 6 | 4/0/0/0/2 |
/jira_issue_drafts | 8 | 5 | 0/0/1/0/1 |
/locations | 6 | 0 | 1/0/0/0/1 |
/memberships | 8 | 7 | 5/0/0/0/2 |
/notification_alert_rules | 9 | 6 | 6/0/0/0/2 |
/organization | 7 | 6 | 11/0/1/0/5 |
/organizations | 8 | 3 | 6/0/0/0/5 |
/partners | 7 | 5 | 4/0/0/0/2 |
/program_risk_factors | 4 | 3 | 1/0/0/0/1 |
/program_sampling_rules | 3 | 2 | 1/0/0/0/1 |
/program_test_questions | 3 | 2 | 2/0/0/0/1 |
/qa_agent_evaluations | 13 | 7 | 8/0/0/0/4 |
/qa_evaluation_requests | 4 | 0 | 0/0/0/0/1 |
/rails | 23 | 2 | 1/0/0/0/4 |
/redaction_rules | 6 | 4 | 1/0/0/0/2 |
/roles | 6 | 5 | 6/0/0/0/2 |
/scorecard_criteria | 13 | 6 | 7/0/0/0/4 |
/scorecard_criterion_checks | 6 | 1 | 1/0/0/0/1 |
/scorecard_criterion_score_option_justification_options | 5 | 2 | 3/0/0/0/2 |
/scorecard_sections | 6 | 1 | 1/0/0/0/1 |
/scorecards | 18 | 14 | 13/0/0/0/13 |
/scoring_guides | 6 | 3 | 2/0/0/0/2 |
/sheet_cells | 4 | 2 | 2/0/0/0/1 |
/sheet_columns | 4 | 2 | 3/0/0/0/1 |
/sheet_versions | 3 | 2 | 1/0/0/0/1 |
/sheets | 14 | 12 | 16/0/0/0/7 |
/sla_policies | 7 | 5 | 4/0/0/0/2 |
/slack_channels | 2 | 1 | 2/0/0/0/1 |
/tags | 4 | 3 | 3/0/0/0/3 |
/teams | 7 | 6 | 9/0/0/0/9 |
/test_reports | 2 | 1 | 2/0/0/0/2 |
/test_risk_scenarios | 4 | 3 | 1/0/0/0/1 |
/trmnl | 1 | 0 | 0/0/0/0/0 |
/webhooks | 7 | 4 | 0/0/0/0/14 |
/work_schedules | 6 | 5 | 4/0/0/0/2 |
/workspaces | 8 | 4 | 3/0/0/0/3 |
2026-09-23 batch: legacy object-field prefixes (ENG-3383 )
Measurement window. No fresh production query was possible from the
authoring session (coordinator AWS SSO expired at 05:21Z); the evidence is
the already-recorded operator censuses in the
retirement plan:
across the 24 h windows ending 01:20Z, 03:20Z and 04:20Z the only
forwarded request under any of these prefixes was one EU
GET /conversation_fields 200, timestamped before the 00:56Z #12820
cutover, and the fully post-canary slice 02:35:13Z → 04:20Z had zero
outcome=forwarded events in both regions. /customer_fields and
/employee_fields did not appear in any census.
Why they were kept on 2026-09-21. The static classifier counted one
API2 “caller” for each (0/0/0/0/1). Re-checked by hand with the step 2
rg pattern: those hits are the doc comments at the top of
routes/object-field-reads.ts, routes/object-field-writes.ts and
lib/employee-payload.ts naming the Rails paths they replaced — no file in
rulebase-ui, rulebase-admin, rulebase-beam-web, rulebase-zendesk-app or api2
builds a /conversation_fields, /customer_fields or /employee_fields
URL. Every first-party read and write already goes to the API2-native
hyphenated routes (features/objects/api/object-fields.ts,
get-conversation-fields.ts, get-customer-fields.ts,
features/employees/api/get-employee-fields.ts,
features/employee-fields/api/get-employee-fields.ts). /company_fields,
the fourth member of the family, was already retired in the 2026-09-21 batch.
Classification. API2 owns the boundary natively under a different path,
the same shape as /knowledge_base_documents: index, create, update
and destroy map to GET/POST /<type>-fields and
PATCH/DELETE /<type>-fields/:id; Rails’ show has no successor and no
first-party caller.
| Prefix | Rails routes | API2 successor routes | Callers (ui/admin/beam/zendesk/api2) |
|---|---|---|---|
/conversation_fields | 6 | 4 (/conversation-fields) | 0/0/0/0/0 |
/customer_fields | 6 | 4 (/customer-fields) | 0/0/0/0/0 |
/employee_fields | 6 | 4 (/employee-fields) | 0/0/0/0/0 |
Contracts. PUBLIC_ROUTE_OWNERSHIP_CONTRACTS gains the object-fields
family (the twelve native successor routes, anonymous 401) and
UNOWNED_PUBLIC_REQUESTS gains expected: "retired" entries for the legacy
paths including the empty-segment /conversation_fields/ shape that the
ENG-3371 package previously listed as forwarded. The retired check runs
before path normalization, so that shape is now answered locally as well.
Rollback and post-deploy gate. As above. Expected post-deploy signal:
outcome=retired under these three prefixes at zero or stale-tab residue
only; any retired event from a current UI build is a rollback trigger and
means a caller the static search missed.
2026-09-25 batch: resources families with a live wildcard-middleware shadow (ENG-3463 )
Why this batch exists. While checking method parity for the remaining
allowlisted resources families, the exact-match variant of step 2 (an
ownership check that ignores * registrations) showed that
routes/org-directory-mutations.ts registered requireNonAgentMembership
with a method-scoped wildcard:
router.on(["POST","PATCH","DELETE"], ["/teams/*", "/partners/*", "/memberships/*", "/invitations/*", ...]).
createLegacyRailsGateway treats any method-specific registration in
matchedRoutes as native ownership, so every unported Rails route under
those shapes was answered 404 locally instead of being forwarded — a live
dependency, not scanner noise: POST /invitations
(InvitationsController#create, no api2 port) and PUT /partners/:id (the
Rails resources alias of update). Shadowed requests emit no
legacy_rails_gateway event, so no gateway census could have shown this.
The static rg search in step 2 does not catch it either: it looks for
callers, not for registrations. Step 2 addendum: run the ownership check
with wildcard registrations excluded (!route.path.includes("*")) and treat
any method-scoped router.on([...], "/<prefix>/*", middleware) as a bug —
attach middleware per route (createRoute({ middleware: [...] })) or use an
ALL router.use.
Measurement window. No fresh production query was possible from the
authoring session (no AWS credentials on the box). The recorded censuses in
the retirement plan — 24 h to 07:40Z,
15:40Z and 17:45Z on 2026-09-24 — show zero successful Rails forwards in
both regions and every forwarded 404 under /admin or /agent; none of
the seven prefixes below appears in any of them. That is consistent with,
not proof of, “no live Rails caller”; the classification rests on the
exact-method parity below and the static caller search.
Classification. Rails routes from bin/rails routes versus exact
(non-wildcard) api2 handlers on the composed app:
| Prefix | Rails method/path pairs | api2 exact handlers | Gap |
|---|---|---|---|
/assignment_queues | 8 | 8 | PUT /:id added in this batch |
/compliance_agents | 23 | 23 | PUT /:id added |
/controls | 4 | 4 | PUT /:id added |
/memberships | 8 | 6 | PUT /:id added; POST /memberships and POST /memberships/:id/deactivate are routed by Rails but MembershipsController has no create/deactivate action (ActionNotFound → 404), so the local 404 is the same answer |
/roles | 6 | 6 | PATCH /:id added (handler was PUT-only) |
/teams | 7 | 7 | PUT /:id added; GET /teams/download_import_members_template already native |
/work_schedules | 6 | 6 | PUT /:id added |
Callers (ui/admin/beam/core/api2 RAILS_INTERNAL_BASE_URL/customer docs) for
methods api2 did not already own: 0/0/0/0/0/0 for all seven. /partners
and /invitations stay on the allowlist: PUT /partners/:id gains a native
alias but POST /invitations (and Rails’ PATCH/PUT/DELETE /invitations/:id,
action-less) keep forwarding.
Contracts. PUBLIC_ROUTE_OWNERSHIP_CONTRACTS gains seven session-owned
families (61 exact method/path pairs, anonymous 401).
UNOWNED_PUBLIC_REQUESTS gains retired entries for the two action-less
membership routes and never-routed methods, a normalized entry for
/work_schedules/, and forwarded pins for POST /invitations and
POST /partners/123/nope so a future wildcard registration fails the
ownership test instead of silently shadowing Rails again. Allowlist 71 → 64,
retired 89 → 96.
Rollback and post-deploy gate. As above. Expected post-deploy signal:
outcome=retired under the seven prefixes at zero or stale-tab residue only.
outcome=forwarded for POST /invitations is expected to appear once the
shadow is gone — it is the live Rails dependency this batch un-hid, not a
regression; it becomes the next port candidate.
2026-09-26 batch: compliance-program resources families with PUT alias parity (ENG-3469 )
Why this batch exists. Re-running step 2 with wildcard registrations
excluded on main 53c53de5d showed six compliance-program families whose
only Rails-only pair is PUT /:id (the resources alias of update); api2
had registered PATCH /:id only. Adding the alias completes exact method
parity, so the prefixes can leave the allowlist.
Measurement window. No fresh production query was possible from the
authoring session (no AWS credentials on the box). The recorded censuses in
the retirement plan — the 2026-09-24
07:40Z/15:40Z/17:45Z windows and the last recorded window with zero
successful Rails forwards — list none of the six prefixes as forwarded. That
is consistent with, not proof of, “no live Rails caller”; the classification
rests on exact-method parity and the static caller search below. No new zero
window is claimed.
Classification. Rails routes from bin/rails routes versus exact
(non-wildcard) api2 handlers on the composed app:
| Prefix | Rails method/path pairs | api2 exact handlers | Gap |
|---|---|---|---|
/agent_test_columns | 3 | 3 | PUT /:id added in this batch |
/compliance_criterion_groups | 6 | 6 | PUT /:id added |
/program_risk_factors | 4 | 4 | PUT /:id added |
/program_sampling_rules | 3 | 3 | PUT /:id added |
/program_test_questions | 3 | 3 | PUT /:id added |
/test_risk_scenarios | 4 | 4 | PUT /:id added |
Callers (ui/admin/beam/core/api2 RAILS_INTERNAL_BASE_URL/customer docs) for
PUT or for the Rails origin: 0/0/0/0/0/0. Every ui caller uses
rulebaseApi2.PATCH (features/compliance/api/*,
compliance-review-dialog-content.tsx).
Contracts. PUBLIC_ROUTE_OWNERSHIP_CONTRACTS gains six session-owned
families (23 exact method/path pairs, anonymous 401).
UNOWNED_PUBLIC_REQUESTS gains retired entries for never-routed paths
under each prefix (GET /agent_test_columns,
POST /compliance_criterion_groups/123/nope, GET /program_risk_factors,
GET /program_sampling_rules, GET /program_test_questions,
GET /test_risk_scenarios). Allowlist 64 → 58, retired 96 → 102.
Rollback and post-deploy gate. As above. Expected post-deploy signal:
outcome=retired under the six prefixes at zero. Still live after this
batch: /invitations (POST /invitations, the two template downloads),
/partners (GET /partners/:partner_id/employees), /conversations,
/accounts, /admin, /agent.