RefinementService::run | end - {"response":{"statusCode":403,"body":{"result":{"code":"PERM33000","t
RCA: RefinementService::run — 403 PERM33000 on PUT /captures/725158
Overview#
What Happened#
2026-07-01 09:20 KST에 cupixworks-capture-refinement-arm-instance 에이전트가 Capture 725158 (Devcon Construction / Boston Scientific workspace) 의 refinement 후반 단계에서 PUT /api/v1/captures/725158 를 호출했고, tesla API 가 403 PERM33000 — Updating entity on billing expired team/workspace is not allowed 로 응답했다. Workspace 5413 의 라이선스가 정확히 20분 10초 전인 2026-07-01T00:00:00Z 에 만료되면서, 라이선스 만료 검사(check_updatable_by_billing_state!) 가 이미 진행 중이던 프로세싱 파이프라인의 PUT /captures 를 차단하여 refinement 결과가 저장되지 못했다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::PermissionDenied |
| exception.message | Updating entity on billing expired team/workspace is not allowed |
| error_code | PERM33000 |
| top_frame | lib/cupix/abstract/base.rb:44 (on origin/master) → app/repositories/base_repository.rb:132 |
| upstream call | PUT http://api-tesla.cupix.internal/api/v1/captures/725158 from RefinementService::updateRefinedCapture |
| runtime | x-runtime: 0.042188s, x-request-id: 197fca8b-668e-48b0-a1cf-9f2234b507c2 |
| env | production, us-west-2, tenant cupix |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
Devcon Construction (team.id=52, domain devcon) — Boston Scientific workspace (id=5413) |
1 (Capture 725158) | Refinement 결과 미저장, 후속 3D reconstruction / analysis / publish 도 AGT2403 로 skip |
Timeline#
- 2026-07-01 09:00 KST — Workspace 5413 "Boston Scientific" 라이선스 만료 (
billing_expires_at: 2026-07-01T00:00:00Z,applied_billing_state: expired) - 2026-07-01 02:53 KST — Capture 725158 record 촬영 (
record.captured_at: 2026-06-30T17:53:29Z) — 라이선스 만료 이전 생성됨 - 2026-07-01 09:20:10 KST — Refinement 에이전트가
updateRefinedCapture단계에서PUT /captures/725158→ 403 PERM33000 반환,RefinementService::run | end - ...에러 로그 기록 - 2026-07-01 09:25:05–09:25:08 KST — 후속 파이프라인(3D reconstruction / analysis / publish)이 실행 시도 중
AGT2403(capture 에 refinement 에러 존재) 로 각 단계 skip. 각 요청 직전[Capture] applied_license: expired on 725158info 로그 기록
Error Log#
RefinementService::run | end - {"response":{"statusCode":403,"body":{"result":{"code":"PERM33000","type":"Cupix::Errors::PermissionDenied","reason":"Updating entity on billing expired team/workspace is not allowed","message":"Updating entity on billing expired team/workspace is not allowed"}},"headers":{"date":"Wed, 01 Jul 2026 00:20:10 GMT", ... "x-request-id":"197fca8b-668e-48b0-a1cf-9f2234b507c2","x-runtime":"0.042188"},"request":{"uri":{"host":"api-tesla.cupix.internal","pathname":"/api/v1/captures/725158", ...},"method":"PUT","headers":{"User-Agent":"cupix-agent","X-CUPIX-AUTH":"session_token:r93vjem4qr8r,session_id:10787729", ...}}},"body":{"result":{}},"statusCode":403,"name":"HttpError"}
Impact#
- Service:
cupixworks-capture-refinement-arm-instance(downstream:cupixworks-api/ tesla) - Team: devcon (Devcon Construction, workspace "Boston Scientific")
- 발생 횟수: 1
- 최초 발생: 2026-07-01 09:20 KST
- 최근 발생: 2026-07-01 09:20 KST
Root Cause Summary#
Workspace 5413 의 라이선스가 2026-07-01T00:00:00Z 에 만료되면서 applied_billing_state 가 expired 로 전환됐다. 이미 만료 이전에 업로드가 완료되어 프로세싱이 진행 중이던 Capture 725158 의 refinement 마무리 단계(RefinementService::updateRefinedCapture) 가 PUT /api/v1/captures/725158 로 refinement_state: refined 를 저장하려 했지만, tesla BaseRepository#update 가 호출하는 check_updatable_by_billing_state! 이 workspace 만료를 감지하고 PERM33000 을 raise 하여 update 를 전면 차단했다. Refinement 에이전트는 이 응답을 catch 하여 updateErrorActionJob 로 job 을 error 처리했고, 이후 파이프라인 단계(3D reconstruction, analysis, publish) 는 capture 의 error_code(AGT2403) 로 인해 모두 skip 됐다. 참고로 develop 브랜치의 TSLA-12641 패치(74f00a3fb, 2026-05-08) 는 update 시 라이선스 검사를 제거하여 정확히 이 시나리오를 해결하도록 설계되어 있으나, 이 커밋은 아직 origin/master (production) 에 포함되어 있지 않다.
Technical Analysis#
Code Path#
Upstream — Refinement 에이전트에서 PUT /captures/:id 호출 지점
Refinement 파이프라인은 SkatMaster 실행이 끝난 뒤 updateRefinedCapture 에서 tesla API 로 refinement_state 를 업데이트한다.
private updateRefinedCapture = async (cpCapture: CPCapture): Promise<void> => {
logger.debug('RefinementService::updateRefinedCapture | begin');
const params = {
refinement_state: TESLA.RefinementState.Refined,
refinement_floorplan_type: (cpCapture.cpFloorplan?.floorplanType as unknown as string) || 'none'
};
const capture = await this.cupixApi.capture.update(cpCapture.id, params);
logger.debug('RefinementService::updateRefinedCapture | end - refinement_state: %s', capture.refinement_state);
};
이 호출은 RefinementService::run 의 try 블록 안에서 실행되며, 실패 시 catch 절이 updateErrorActionJob('refinement') 를 호출한다.
private run = async (): Promise<void> => {
logger.info('RefinementService::run | begin');
try {
// ... loadJob / loadCluster / runRefinement / copyAlignmentToEfs 등 ...
await this.updateRefinedCapture(cpCapture);
this.copyProcessOutput();
} catch (error) {
logger.error('RefinementService::run | end - %s', JSON.stringify(error)); // ← 이 지점에서 클러스터가 잡혔음
await this.jobManager.updateErrorActionJob('refinement');
}
};
Downstream — tesla BaseRepository#update (production/origin/master)
Failure point 는 tesla 의 BaseRepository#update 안에 있는 check_updatable_by_billing_state! 호출이다. origin/master 는 아직 TSLA-12641 이전 코드이므로 모든 엔티티 update 에 대해 라이선스 검사가 걸린다.
def update(params = {}, current_user = nil)
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') unless @model.updatable_by?(@current_user)
if self.current_user.present? && self.model.present? && self.model.respond_to?(:applied_cycle_state)
raise Cupix::Errors::PermissionDenied.new(code: 'PERM32000', reason: 'Archived entity') if %w[archived archiving].include?(self.model.applied_cycle_state) && !Pundit.policy(self.current_user, self.model).update?
end
check_updatable_by_billing_state! # ← 여기서 raise
set_params(params)
@model.last_updated_user = self.current_user if @model.respond_to?(:last_updated_user)
end
def check_updatable_by_billing_state!
Cupix::Logger.debug("[check_updatable_by_billing_state!] begins on #{self.model.class.name}", ...)
if self.current_user.present? && self.model.present? &&
(self.model.respond_to?(:workspace_id) && self.model.respond_to?(:team_id)) &&
self.current_user.team_id != TeamRepository.admin_team_id &&
self.model.respond_to?(:applied_billing_state)
if self.model.applied_billing_state == 'expired'
raise Cupix::Errors::PermissionDenied.new(code: 'PERM33000', reason: 'Updating entity on billing expired team/workspace is not allowed')
end
end
end
Applied billing state 계산 — workspace 만료 하나로 expired 전환
def applied_billing_state
billing_states = [team_billing_state, workspace_billing_state, facility_billing_state]
return 'active' if billing_states.include?('active')
return 'expired' if billing_states.include?('expired')
'none'
end
Capture 725158 의 경우: team.applied_billing_state = "none", workspace.applied_billing_state = "expired" → applied_billing_state 는 "expired".
기대 동작 vs 실제 동작
기대: 이미 만료 이전에 업로드된 capture 는 프로세싱을 완료하여 refinement/reconstruction 결과가 저장되어야 함 (TSLA-12641 / IG-2487 에서 정의된 정책 — "라이선스는 신규 자원 생성 제한이며, 기존 자원 활용은 허용"). 실제: production 코드가 모든 엔티티 update 를 차단하여 refinement 결과 저장이 실패, 후속 파이프라인 전체가 error_code AGT2403 로 skip 됨.
Log Evidence#
Datadog 쿼리 — refinement 에이전트 원본 에러
service:cupixworks-capture-refinement-arm-instance "725158"
Datadog 쿼리 — tesla API 측 403 + 후속 파이프라인
service:cupixworks-api "725158"
tesla API 측 403 응답 (2026-07-01 09:25:05 KST)
[403] PUT /api/v1/captures/725158 (Api::V1::CapturesController#update)
error.reason: "Updating entity on billing expired team/workspace is not allowed"
License 만료 직전 로그 (반복적으로 기록됨, 2026-07-01 09:25 KST 다수)
[Capture] applied_license: expired on 725158
후속 파이프라인 skip 로그 (2026-07-01 09:25:06 KST)
CaptureIntelligence skipped: error_code=AGT2403. capture_id: 725158
3D reconstruction was skipped due to the existence of an capture error code. capture_id: 725158, error_code: AGT2403
Analysis skipped: error_code=AGT2403. capture_id: 725158
capture 725158 publish skipped: error_code(AGT2403) exist
Kibana — Capture 725158 상태 (production captures 인덱스)
{
"id": 725158,
"state": "done",
"refinement_state": "error",
"team": { "id": 52, "domain": "devcon", "applied_billing_state": "none" },
"workspace": {
"id": 5413,
"name": "Boston Scientific",
"billing_state": "expired",
"billing_expires_at": "2026-07-01T00:00:00.000Z",
"applied_billing_state": "expired",
"plan": "core_builder"
},
"facility": { "id": 19280, "name": "Boston Scientific Santa Clara Hub 2" },
"record": { "captured_at": "2026-06-30T17:53:29.000Z" }
}
Git 증거 — TSLA-12641 은 아직 production 에 없음
tesla$ git log --oneline lib/cupix/abstract/base.rb
74f00a3fb TSLA-12641 feat: restrict license expiry check to Workspace/Facility/Capture ← develop
d86fde486 a
50e373ba0 Fix bug that creation preventing does not work on expired workspace ← origin/master tip
...
tesla$ git branch --contains 74f00a3fb -a | grep master
(no origin/master match)
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Workspace 5413 라이선스가 프로세싱 도중 만료되어 tesla BaseRepository#update 의 check_updatable_by_billing_state! 가 PUT /captures/:id 를 차단 (production 코드에서 update 라이선스 검사가 아직 활성 상태) |
Kibana: workspace.billing_expires_at=2026-07-01T00:00Z, applied_billing_state=expired; 에러 timestamp = 2026-07-01T00:20:10Z (만료 후 20분); Datadog: [Capture] applied_license: expired on 725158 반복 기록; [403] PUT /api/v1/captures/725158; git: 74f00a3fb (TSLA-12641) 는 origin/master 미포함, base_repository.rb:132 는 아직 check_updatable_by_billing_state! 호출 |
— | Confirmed |
| H2 | Admin team bypass 경로가 잘못 적용되어 정상 요청도 차단 | check 코드 self.current_user.team_id != TeamRepository.admin_team_id — 정상 조건 |
에이전트 세션(user 45873, team 52 devcon) 은 admin_team 이 아님 → bypass 조건에 해당 안 됨. 코드 자체는 정상 동작 | Rejected |
| H3 | 에이전트가 잘못된 endpoint / method (e.g. POST for create) 호출 | — | 로그의 "method":"PUT", "pathname":"/api/v1/captures/725158" 는 정상 update 경로. refinement-service.ts:473 의 capture.update(cpCapture.id, params) 와 일치 |
Rejected |
| H4 | 인증/세션 만료로 인한 403 (권한 오류) | 응답 payload code: "PERM33000" 존재 |
403 응답에 code: PERM33000 + type: Cupix::Errors::PermissionDenied + reason billing expired team/workspace — 세션 만료 시에는 AUTH* 계열 코드가 반환됨. X-CUPIX-AUTH 헤더도 유효 세션 형태 |
Rejected |
| H5 | Refinement 에이전트가 PUT 대신 create 흐름을 타서 BaseFactory#create! 의 라이선스 검사에 걸림 |
BaseFactory#create!:84 는 매 create 마다 라이선스 검사 실행 |
로그의 method 는 PUT 이고 controller 는 CapturesController#update. Create 경로가 아님 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 운영 조치: Capture 725158 의 refinement 결과 파일(EFS
skatMasterEfsPath아래 refinedalignments_all.json,align_preview_meta_refinement_with_prior_map.json, PNG 등) 은copyAlignmentToEfs/copyPreviewImageToEfs로 이미 EFS 에 저장되었을 수 있음(에러가 발생한 지점이updateRefinedCapture이므로 그 이전 단계는 완료). 다음 중 하나로 파이프라인을 복구한다.- Devcon workspace 5413 의 라이선스를 연장/갱신하고, 해당 capture 의 job 을 재실행하여
refinement_state를 update. 재실행 시에는 이미 완료된 SkatMaster 를 다시 돌지 않도록 idempotent 재개 경로를 확인. - 또는 admin-team 세션으로
capture.update(refinement_state: refined, refinement_floorplan_type: <값>)를 수동 실행하여 만료 검사를 bypass (check_updatable_by_billing_state!는 admin_team 을 skip). 이후 재구성/분석 job 을 수동 트리거.
- Devcon workspace 5413 의 라이선스를 연장/갱신하고, 해당 capture 의 job 을 재실행하여
- 커뮤니케이션:
AGT2403로 skip 된 3D reconstruction / analysis / publish 는 refinement 를 정상 상태로 되돌리기 전까지 재실행되지 않음. Devcon Construction 고객에게 처리 상태 공유.
단기 개선 (1주 이내)#
- TSLA-12641 을 production 에 반영.
feature/TSLA-12641(74f00a3fb) 는 develop 에 이미 머지되어 있고,BaseRepository#update및MeasurementRepository#create의check_updatable_by_billing_state!호출을 제거한다. Production 반영 후에는 이번 케이스처럼 만료 시점에 진행 중이던 프로세싱의PUT /captures가 정상적으로 200 을 반환하게 된다. 배포는 정기 develop→master merge 흐름을 따르되, 이번 IG-2487 계열 회귀 방지를 위해 우선순위를 높인다. - 에이전트 재시도/보상 흐름 확인:
RefinementService::run의 catch 절이 403 을 잡으면 즉시updateErrorActionJob로 종료하고 있으며, 만료성 403(PERM33000) 과 일시 오류(5xx, 네트워크) 를 구분하지 않는다.code === 'PERM33000'인 경우에는 job 을stopped로 두지 않고 별도 상태(e.g.waiting_for_license_renewal) 로 표시하여 라이선스 갱신 후 자동 재개할 수 있는 훅을 준비한다. (이 부분은 TSLA-12641 반영 후에도 만료가 완전히 끝난 상태에서는 여전히 위험이 있으므로 별개로 필요.)
장기 개선 (재발 방지)#
- 파이프라인 무결성 원칙 명문화: TSLA-12641 문서에 이미 정의된 정책("라이선스 만료 이후에도 이미 업로드된 데이터의 프로세싱은 완료되어야 한다") 을 각 pipeline 진입/출구 코드 근처에 comment 로 링크. 신규 팩토리/리포지토리 추가 시 회귀 여부를 리뷰 체크리스트에 포함.
- 만료 임박(예: T-24h) 알림: workspace
billing_expires_at이 임박한 시점에 in-flight capture(진행 중인 job) 개수를 대시보드에 노출. 만료 시점에 기존 파이프라인이 얼마나 영향을 받는지 사전 가시화. - Backfill 스크립트: TSLA-12641 배포 후,
billing_expires_at전후 window 에서refinement_state = error이면서 error_code 가PERM33000/AGT2403인 capture 를 찾아 재실행하는 운영 스크립트. IG-2487 (Takenaka Nagoya) 도 동일 카테고리로 이번 케이스와 함께 대상.
Monitoring#
REQUIRED SUB-SKILL 준수: 아래 쿼리는 timeseries widget 용 표준 log/metric aggregation 문법.
- Refinement 에이전트 PERM33000 응답 카운트 (widget)
text
logs("service:cupixworks-capture-refinement-arm-instance @environment:production PERM33000").index("*").rollup("count").by("team_domain") - tesla API 측 만료로 인한 update 차단 카운트
text
logs("service:cupixworks-api status:error @error.code:PERM33000 @environment:production").index("*").rollup("count").by("http.url_details.path") - 만료 임박 workspace 진행 중 job 수 (에이전트 시작 로그 기반)
text
logs("service:cupixworks-capture-refinement-arm-instance \"RefinementService::run | begin\" @environment:production").index("*").rollup("count").by("team_domain") - 알림 임계값 제안:
PERM33000응답이 refinement/postprocessor/complete 등 프로세싱 에이전트 서비스에서 15분에 3건 이상 관측되면 warning, 1시간에 10건 이상이면 critical (라이선스 만료 window 겹침 지표).
Risk Assessment#
- Risk level: medium (프로세싱 파이프라인 결과물이 저장되지 못하여 고객 데이터 활용성이 저하되지만, 원본 업로드는 안전하며 라이선스 갱신 또는 TSLA-12641 배포로 복구 가능).
- 예상 복잡도: standard (production 반영 대기 중인 develop 패치를 promote 하는 작업 + 이번 capture 재처리 + 만료성 오류에 대한 에이전트 재시도 정책 정비).