ES /docs

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#

  1. 2026-07-01 09:00 KST — Workspace 5413 "Boston Scientific" 라이선스 만료 (billing_expires_at: 2026-07-01T00:00:00Z, applied_billing_state: expired)
  2. 2026-07-01 02:53 KST — Capture 725158 record 촬영 (record.captured_at: 2026-06-30T17:53:29Z) — 라이선스 만료 이전 생성됨
  3. 2026-07-01 09:20:10 KST — Refinement 에이전트가 updateRefinedCapture 단계에서 PUT /captures/725158 → 403 PERM33000 반환, RefinementService::run | end - ... 에러 로그 기록
  4. 2026-07-01 09:25:05–09:25:08 KST — 후속 파이프라인(3D reconstruction / analysis / publish)이 실행 시도 중 AGT2403 (capture 에 refinement 에러 존재) 로 각 단계 skip. 각 요청 직전 [Capture] applied_license: expired on 725158 info 로그 기록

Error Log#

Datadog Logs

text
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_stateexpired 로 전환됐다. 이미 만료 이전에 업로드가 완료되어 프로세싱이 진행 중이던 Capture 725158 의 refinement 마무리 단계(RefinementService::updateRefinedCapture) 가 PUT /api/v1/captures/725158refinement_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 를 업데이트한다.

applications/agents/packages/cupix-capture-refinement-agent/src/refinement-service.ts:467-475typescript
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') 를 호출한다.

applications/agents/packages/cupix-capture-refinement-agent/src/refinement-service.ts:79-111typescript
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 에 대해 라이선스 검사가 걸린다.

app/repositories/base_repository.rb:125-138 (origin/master)ruby
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
lib/cupix/abstract/base.rb (origin/master)ruby
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 전환

app/models/concerns/facility_entity/billing.rb:52-59ruby
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 에이전트 원본 에러

text
service:cupixworks-capture-refinement-arm-instance "725158"

Datadog 쿼리 — tesla API 측 403 + 후속 파이프라인

text
service:cupixworks-api "725158"

tesla API 측 403 응답 (2026-07-01 09:25:05 KST)

text
[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 다수)

text
[Capture] applied_license: expired on 725158

후속 파이프라인 skip 로그 (2026-07-01 09:25:06 KST)

text
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 인덱스)

json
{
  "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 에 없음

text
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#updatecheck_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:473capture.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 아래 refined alignments_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 을 수동 트리거.
  • 커뮤니케이션: AGT2403 로 skip 된 3D reconstruction / analysis / publish 는 refinement 를 정상 상태로 되돌리기 전까지 재실행되지 않음. Devcon Construction 고객에게 처리 상태 공유.

단기 개선 (1주 이내)#

  • TSLA-12641 을 production 에 반영. feature/TSLA-12641 (74f00a3fb) 는 develop 에 이미 머지되어 있고, BaseRepository#updateMeasurementRepository#createcheck_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 재처리 + 만료성 오류에 대한 에이전트 재시도 정책 정비).