TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed
RCA: TransferManager::uploadNewPointclouds HttpError 403
Overview#
What Happened#
2026-06-26 09:53 KST, cupixworks-capture-3dreconstruction-instance 에이전트가 3D 재구성 결과의 새 pointcloud 그룹을 생성하기 위해 cupixworks-api POST /api/v1/pointclouds를 호출했으나 403 Permission Denied 응답을 받고 작업이 실패했다. 에이전트는 UploadNewPointcloudsContainer::createGroupPointcloud 단계에서 group pointcloud 생성에 실패해 업로드 task 생성 전에 reject 되었고, 동일 시각에 ThreeDReconstruction::run | end가 error 레벨로 함께 기록되며 해당 job은 updateErrorActionJob 으로 종료되었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | HttpError |
| exception.message | HTTP request failed |
| top_frame | pointcloudApi.js:874:40 (@tesla/typescript-node-sdk createPointcloud) |
| upstream cause | Cupix::Errors::PermissionDenied / PERM10000 at PointcloudFactory#create! |
| policy | RecordPolicy#create? returned false |
| runtime | Node.js agent on EC2 instance (/tmp/agent/dist/...) |
| env | production, region ap-southeast-2, tenant nswgov (cluster frontmatter) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| sinsw / nswgov (ap-southeast-2) | 1 | 단일 capture 의 3D 재구성 job 종료 실패 — pointcloud 미업로드 |
Timeline#
- 2026-06-26 09:53:50 KST — 에이전트가
TransferManager::uploadNewPointclouds호출 →UploadNewPointcloudsContainer::createGroupPointcloud가POST /api/v1/pointclouds요청. - 2026-06-26 09:53:51 KST — cupixworks-api 가
[403] POST /api/v1/pointclouds (Api::V1::PointcloudsController#create)응답.Cupix::Errors::PermissionDenied(PERM10000). - 2026-06-26 09:53:52 KST —
RecordPolicy#create?가Permission denied on creating a model Recordinfo 로그를 남김. - 2026-06-26 09:53:50 KST — 에이전트 측에서
HttpError가TransferManager::uploadNewPointcloudscatch 로 전파되어 error 로그 기록. - 2026-06-26 09:53:50 KST —
ThreeDReconstruction::run | end가 error 레벨로 기록되며jobManager.updateErrorActionJob호출.
Error Log#
TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed
at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.202605301249_1e9855b03300e7077469e8a23e5d7e36/node_modules/@tesla/typescript-node-sdk/api/pointcloudApi.js:874:40)
at self.callback (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:185:22)
at Request.emit (node:events:524:28)
at Request.emit (node:domain:489:12)
at Request.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1154:10)
at Request.emit (node:events:524:28)
at Request.emit (node:domain:489:12)
at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1076:12)
at Object.onceWrapper (node:events:638:28)
at IncomingMessage.emit (node:events:536:35)","message":"HTTP request failed","response":{"body":{},"statusCode":403},"body":{},"statusCode":403,"name":"HttpError"}
Impact#
- Service:
cupixworks-capture-3dreconstruction-instance - Team: sinsw
- 발생 횟수: 1
- 최초 발생: 2026-06-26 09:53 KST
- 최근 발생: 2026-06-26 09:53 KST
영향 범위는 단일 인스턴스의 단일 3D 재구성 job. cupixworks-api 측 7일 검색 결과 POST /api/v1/pointclouds 의 403 은 본 시점 1건만 관측되어 광범위한 정책 회귀가 아닌 단일 record 의 권한 상태 문제로 보인다.
Root Cause Summary#
에이전트가 createGroupPointcloud 단계에서 cupixworks-api 의 POST /api/v1/pointclouds 를 호출하자 PointcloudFactory#create! 가 부모 Record 에 대해 Pundit.policy(current_user, parent).create? 를 평가했고, 해당 Record 의 applied_permission 배열에서 write/all bit ([3], [4]) 가 모두 0 이라 RecordPolicy 가 상속한 ApplicationPolicy#create? 가 false 를 반환했다. 그 결과 Cupix::Errors::PermissionDenied (PERM10000) 가 발생하고 API 가 403 을 응답했다. 에이전트의 CupixAuth::retryable 는 statusCode > 500 만 재시도하므로 403 은 즉시 reject 되고 ThreeDReconstruction::run 의 catch 블록이 실행되어 job 이 error 상태로 종료되었다. 즉 코드 결함이 아니라, 에이전트가 사용하는 자격(token)의 user 가 해당 record 에 대해 write/all 권한이 없거나, record 의 applied_permission 이 예상과 다르게 셋업된 상태가 근본 원인이다.
Technical Analysis#
Code Path#
Entry point (agent 측): three-d-reconstruction-service.ts:378 — uploadNewPointclouds 호출.
private uploadPointcloudFiles = async (cpCapture: CPCapture): Promise<void> => {
for (let index = 0; index < cpCapture.cpClusters.length; index++) {
const cpCluster = cpCapture.cpClusters[index];
if (cpCluster.cpPointclouds.length > 0) {
logger.debug('ThreeDReconstruction::uploadPointcloudFiles | upload new pointclouds - cluster id: %d, pointcloud size: %d', cpCluster.id, cpCluster.cpPointclouds.length);
await this.transferManager.uploadNewPointclouds(cpCluster, cpCluster.cpPointclouds);
// ... 후속 업로드는 본 단계 실패 시 도달하지 않음
}
}
};
TransferManager::uploadNewPointclouds 는 컨테이너를 만들고 init() 단계의 reject 를 그대로 외부로 전파한다.
uploadNewPointclouds = (cpCluster: CPCluster, items: CPPointcloud[]): Promise<void> => new Promise((resolve, reject) => {
const container = new UploadNewPointcloudsContainer(this, cpCluster, items, resolve, reject);
container.init()
.then(() => {
container.tasks.forEach(task => {
this.addTask(task);
});
})
.catch(ec => {
logger.error('TransferManager::uploadNewPointclouds | %s', JSON.stringify(ec, Object.getOwnPropertyNames(ec)));
reject();
});
});
컨테이너 초기화는 group pointcloud 생성 API 호출을 포함한다 — 이 호출이 실패의 직접 원인이다.
private createGroupPointcloud = async (): Promise<void> => {
const cpCapture = this.cpCluster.cpCapture;
const createPointcloudRequest = {
kind: 'group',
name: cpCapture?.name + '_' + this.cpCluster.name,
pointcloud_type: TESLA.PointcloudType._3dReconstructed,
level_id: cpCapture?.srvLevel?.id as number,
record_id: cpCapture?.srvRecord?.id as number,
capture_id: cpCapture?.id as number,
cluster_id: this.cpCluster?.id as number
};
logger.debug('UploadNewPointcloudsContainer::createGroupPointcloud | createPointcloudRequest - %s', JSON.stringify(createPointcloudRequest));
this._groupPointcloud = await this.cupixApi.pointcloud.create(createPointcloudRequest);
pointcloud.create 는 SDK 의 createPointcloud 를 호출하고, 응답을 unwrap 한다.
async create(createPointcloudRequest: TESLA.CreatePointcloudRequest): Promise<TESLA.Pointcloud> {
const api = await this.api();
const res = await api.createPointcloud(Fields.PointcloudFields, createPointcloudRequest);
return unwrapAttributes(res);
}
서버 측(tesla) Failure point — PointcloudFactory#create! 에서 record 에 대한 정책 검사:
def create!(params = {})
self.model = ::Pointcloud.new kind: params[:kind]
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'level is required') if params[:level_id].blank?
if params[:record_id].present?
self.parent = RecordRepository.new(current_user: self.current_user).show(params[:record_id])
else
raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'record_id is required')
end
self.model.record = self.parent
self.model.facility = self.parent.facility
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') unless Pundit.policy(self.current_user, self.parent).create?
RecordPolicy#create? 는 별도 override 없이 ApplicationPolicy#create? 를 상속해 적용한다.
def create?
return true if admin_administrator?
return false if archived_entity?
if record.has_attribute?(:applied_permission)
if record.applied_permission[3] == 1 || record.applied_permission[4] == 1
true
else
Cupix::Logger.info(
"Permission denied on creating a model #{record.class.name}",
class: self.class.name,
function: __method__,
user: { id: user.id, email: user.email }
)
false
end
else
true
end
end
기대 동작: 에이전트가 인증한 user 가 Record 에 대해 write(applied_permission[3]==1) 또는 all(applied_permission[4]==1) 권한을 보유해 create? 가 true 를 반환해야 한다.
실제 동작: 두 bit 모두 0(또는 user 가 admin_administrator? 아님 및 record 가 archived) 으로 create? 가 false 를 반환 → 403 PermissionDenied.
또한 에이전트의 재시도 정책은 5xx 에만 동작하므로 403 에서는 즉시 실패한다.
retryable = <T>(f: () => Promise<T>, retries?: number): Promise<T> => new Promise((resolve, reject) => {
const _retries = retries != undefined ? retries : 0;
f()
.then(response => resolve(response))
.catch(e => {
if (e && e.statusCode > 500 && _retries < Constants.MaxRetries) {
// ... retry only on >500
} else {
reject(e);
}
});
});
상위 ThreeDReconstruction::run 의 catch 가 error 로그를 남기고 job 을 error 상태로 마무리한다.
logger.info('ThreeDReconstruction::run | end');
await this.jobManager.updateCompleteActionJob(this.ActionName, TESLA.UpdateJobRequest.StateEnum.Stopped);
} catch (error: any) {
logger.error('ThreeDReconstruction::run | end', error);
await this.jobManager.updateErrorActionJob(this.ActionName);
} finally {
await this.removeWorkspaceAndPointCloudDir(cpCapture!);
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager" "uploadNewPointclouds"
service:cupixworks-api "/pointclouds" @http.status_code:403
service:cupixworks-api "Permission denied on creating a model Record"
에이전트 측 에러 원문 (status:error, 2026-06-26 09:53:50 KST):
{
"message": "HTTP request failed",
"response": { "body": {}, "statusCode": 403 },
"body": {},
"statusCode": 403,
"name": "HttpError"
}
같은 초의 cupixworks-api 응답 로그 (2026-06-26 09:53:51 KST):
{
"message": "[403] POST /api/v1/pointclouds (Api::V1::PointcloudsController#create)",
"error": {
"reason": "Permission denied",
"code": "PERM10000",
"message": "Permission denied",
"class": "Cupix::Errors::PermissionDenied"
}
}
Pundit 정책 거부 정보 로그 (2026-06-26 09:53:52 KST):
{
"message": "Permission denied on creating a model Record",
"class": "RecordPolicy",
"function": "create?"
}
같은 인스턴스에서 직후 동시 발생한 ThreeDReconstruction::run | end 가 error 레벨로 기록 — 동일 시각의 형제 클러스터 3d3f45ac-fde9-4c64-a226-f1b9d96030a7 와 일치.
7일 범위에서 POST /api/v1/pointclouds 403 은 본 1건만 관측 (service:cupixworks-api "[403] POST /api/v1/pointclouds" → 1 hit). 정책 회귀(regression)가 아닌 단일 record 의 권한 상태 문제임을 시사.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | cupixworks-api 가 Pundit RecordPolicy#create? 를 통해 403 PermissionDenied 를 반환 — 에이전트 user 가 해당 Record 의 applied_permission write/all bit 미보유 |
API 로그 [403] POST /api/v1/pointclouds … PERM10000, 직후 Permission denied on creating a model Record (RecordPolicy#create?) 로그, PointcloudFactory#create! line 20 의 명시적 raise |
없음 | Confirmed |
| H2 | 토큰 만료/인증 실패로 인한 403 | — | 401 이 아닌 403, 같은 인스턴스에서 직전 CupixAuth::setSession session_id: … 가 정상 기록, handleError 가 별도 인증 실패 경로를 타지 않음 |
Rejected |
| H3 | 광범위한 권한 정책 회귀(코드 배포) | — | 7일간 POST /api/v1/pointclouds 403 이 1건만 발생, 같은 시각 동일 controller #index 는 200 응답 다수 |
Rejected |
| H4 | record/level/capture/cluster id 불일치로 인한 ARG10000 파라미터 검증 실패가 403 으로 잘못 분류 |
— | 응답 error class 가 Cupix::Errors::PermissionDenied 이며 code 가 PERM10000 으로 명시 — Parameter 검증 실패라면 ARG10000 이 와야 함 |
Rejected |
| H5 | 외부 의존성(S3, ELB) 일시 장애 | — | status-board dep:* 매칭 없음, 동일 에이전트의 같은 분 다른 API 호출(setSession, JobManager) 이 정상 동작 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 영향 받은 record (capture/cluster 의 부모 record) 의 권한 상태를 운영팀이 직접 확인. 후보:
Record.applied_permission의 write/all bit 값- 해당 user 가 속한 team 의 role/permission 매핑
- record 의
applied_cycle_state가archived/archiving인지 (archived 이면ApplicationPolicy#create?가 즉시 false 반환)
- 코드 변경은 불필요. 데이터/권한 설정 수정으로 해결.
- 운영팀이 cluster frontmatter 의
regions: ap-southeast-2,tenant: nswgov, 그리고 직후 동시 시각의 형제 cluster 와 함께 capture/record id 를 확인. 동일 인스턴스의 같은 분 (09:53:50–09:53:52 KST) info 로그에JobManager::loadJob가 없으므로 어떤 job id 인지는 더 좁힌 검색 필요 (service:cupixworks-capture-3dreconstruction-instance "loadJob" -f "2026-06-26T00:50:00,2026-06-26T00:55:00").
단기 개선 (1주 이내)#
UploadNewPointcloudsContainer::createGroupPointcloud가pointcloud.create실패 시 단순 에러 외에 요청 컨텍스트(record_id, capture_id, cluster_id, level_id)를 함께 남기도록 로그 보강. 현재JSON.stringify(ec, Object.getOwnPropertyNames(ec))만 남겨 어느 record 가 거부됐는지 사후 추적이 어렵다.- cupixworks-api
Cupix::Logger.info("Permission denied on creating a model …")가 user.id/email 외에record.id도 함께 남기도록 보강 — 현재 user 만 기록되어 record 식별이 불가하다 (참고:application_policy.rb:21-29의read?는 model.id 를 남기지만create?는 남기지 않음). - 운영 가시성: 403 발생 시 에이전트는 즉시 job 을 error 종료하므로, 동일 record 에 대한 재시도 차단 정책 또는 명시적 사용자 알림 경로가 필요한지 검토.
장기 개선 (재발 방지)#
- 3D 재구성 에이전트가 사용하는 service account 가 production 환경의 record write 권한을 보장하도록 권한 모델 점검 — 특정 tenant/team 권한 변경 시 에이전트 권한이 함께 회수되지 않도록 분리.
- 정책 거부(403/PERM10000) 메트릭을 controller 별 dashboard 로 노출해 단발성/광범위성 회귀를 빠르게 구분.
- (Revision 1 추가) 에이전트가 user 권한과 분리된 system identity 로 동작하도록 인증 모델 변경 검토 — 아래 "Agent Permission Model" 섹션의 옵션과 비용 평가 참조.
Agent Permission Model — 변경 비용 평가#
피드백 "agent 가 뜨는 순간에는 user 권한없이 동작해야할거 같은데 이렇게 동작을 수정하려면 얼마나 어려운지 계산좀" 에 대한 분석. 현재 구조와 세 가지 변경 옵션의 영향 범위를 코드 기반으로 추산했다.
현재 구조 (As-Is)#
에이전트가 cupixworks-api 를 호출할 때 사용하는 자격은 실제 human user 의 세션이다. 에이전트 전용 user 도, 정책 우회 경로도 존재하지 않는다.
app/jobs/create_capture_3d_reconstruction_job.rb:57—session = self.user.agent_team_session(self.jobable.team)로 job 을 발행한 user 의 세션을 발급. Job 의self.user는 capture 를 생성/소유한 일반 user 이다.app/models/user.rb:116-126—agent_team_session은grant_type: :auth_agent세션을 생성하지만, 소유자는 호출한 user (self.sessions.active.where(grant_type: :auth_agent)).app/factories/session_factory.rb:38-43—auth_agent와api_token의 유일한 차이는expires_at만료 기간. 권한/role 차이 없음.app/controllers/concerns/legacy_verification_controller.rb:83—@current_user = @session.user로 controller 는 일반 user 로 인식.app/factories/pointcloud_factory.rb:20—Pundit.policy(self.current_user, self.parent).create?가 그 일반 user 의applied_permission만 평가.
즉 본 incident 는 "에이전트가 user 권한으로 동작" 한다는 설계 자체로 인해 user 의 권한 변동이 그대로 에이전트 실패로 이어지는 구조적 위험을 드러낸다.
변경 옵션과 비용#
| 옵션 | 접근 | 코드 변경 범위 | 복잡도 | 회귀 위험 | 보안/감사 영향 |
|---|---|---|---|---|---|
| A. 운영 데이터 수정 | 영향 받은 record 의 applied_permission 또는 user role 보정 |
0 file | trivial | 없음 | 변동 없음 (현재 권장 즉시 조치와 동일) |
| B. Policy 레벨 우회 | ApplicationPolicy 에서 @session.grant_type == :auth_agent 이면 create/update/delete 우회 |
약 5-15 file (Pundit context 확장 + 6개 메서드) | small-medium | 중간 | 높음 — 탈취된 agent token = tenant-wide god-mode |
| C. 전용 system user (per-team) | team 마다 agent_system user 를 두고 agent_team_session 이 그것을 사용 |
약 3-7 file (user.rb, job 들의 self.user) + data migration |
medium | 높음 (감사 추적 단절) | 중간 — 감사 로그에서 human attribution 사라짐 |
| D. 정책 모델에 "agent identity" 도입 | Pundit.policy(user, record) → Pundit.policy(actor, record) 시그니처 변경, actor 가 user 또는 agent role 보유 |
92 file / 167 call site 전수 수정 + 모든 policy class 수정 | large | 매우 높음 | 중간 — 새 권한 모델 필요 |
옵션별 세부 추산#
Option A — Operational only
- 변경 파일: 0
- 작업: 운영팀이 capture/cluster 의 parent record 권한 점검 후 user role 또는 record
applied_permission보정 - 장점: 즉시 가능, regression risk 없음
- 단점: 근본 원인 해결 아님 — 동일 패턴 재발 가능
Option B — Policy-level agent bypass
대상 정책 메서드: ApplicationPolicy#create?, #update?, #delete?, #share?, #archive?, #read? (6개).
def create?
return true if admin_administrator?
return true if auth_agent_session? # NEW
return false if archived_entity?
# ... existing logic
end
Plumbing: 현재 policy 는 (user, record) 만 받음 — session/grant_type 접근 불가. 두 가지 방법:
- Pundit policy_scope 에 context 주입 — Pundit
pundit_useroverride 로 user 대신{ user:, session: }객체를 전달.app/controllers/api_controller.rb의 모든 controller 에서pundit_user메서드 override 필요. 영향:api_controller.rb1 file +application_policy.rb1 file + 시그니처 변경 영향받는Scope.resolve들 (~92 file 중 일부). - User 모델에
from_agent_session?flag 추가 —legacy_verification_controller.rb:83에서@current_user.instance_variable_set(:@from_agent_session, @session.grant_type == 'auth_agent')셋업. 영향:legacy_verification_controller.rb1 file +user.rb1 file +application_policy.rb1 file. 가장 작은 변경 경로 (3-5 file).
테스트: spec/policies/application_policy_spec.rb + 영향받는 자식 policy spec 다수.
리스크: agent 토큰이 탈취되면 해당 team 전체 record 에 대해 write 가능 — 현재는 user 권한 범위로 제한되지만, bypass 후엔 그 제한이 사라짐. 따라서 User-Agent 검증, IP allowlist, 또는 별도 mTLS 등 보강 필요. 보안 영향이 크므로 보안 리뷰 필수.
Option C — Per-team system user
def agent_team_session(current_team)
system_user = current_team.agent_system_user # NEW
system_user.sessions.active
.where(grant_type: :auth_agent, team: current_team)
# ...
end
대상 변경:
app/models/team.rb—agent_system_userassociation + lazy createapp/models/user.rb:116-126—agent_team_session이 self 가 아닌 system user 사용- 모든 Job 클래스의
self.user.agent_team_session(...)호출 약 30+ 위치 (Grep 결과 38건 확인됨) — 호출 자체는 그대로지만 의미가 달라짐 - Data migration: 기존 team 마다 system user 생성, 적절한
applied_permission또는administratorgroup 매핑 - 모든 record/facility/team 의 audit log 가 system user 로 표시 — UX 변경
변경 범위: 코어는 3-5 file 이나, migration + 감사 로그 의미 변경 으로 인한 전사 검토 필요.
리스크: 감사 추적이 "어떤 user 가 일으킨 작업인가" 정보를 잃음. job 메타데이터로 보강 필요. 또한 system user 가 administrator 그룹이면 admin_administrator? 가 true 가 되어 모든 record 우회 — Option B 보다 더 강력한 권한이 발생.
Option D — actor 시그니처 변경
가장 깨끗하나 가장 비싸다. 162개 Pundit.policy(...) 호출 + 모든 policy class 의 user 사용처를 actor 로 전수 수정. 모든 policy spec 재작성. 1-2 주 단위 작업이며 회귀 위험 크다.
권장#
- 이번 incident 해결: Option A (운영팀 데이터 수정).
- 재발 방지로 1순위: Option B 의 두 번째 방법(
from_agent_session?flag) — 3-5 file 변경으로 작은 폭. 단 보안 리뷰 필수. - Option C 는 감사 로그 영향이 크므로, 보안/제품팀 합의가 선행되어야 함.
- Option D 는 본 incident 의 비용 대비 효과가 낮음 — 보류.
미확인 항목#
- 다른 에이전트(forge-agent, complete-agent, mask-enhancement-agent 등)도 동일하게 user 세션으로 동작하는지 — 본 분석은
create_capture_3d_reconstruction_job만 확인. 변경 시 일관성 검증 필요. - production 의
auth_agent세션이 통과하는 controller 가Api::V1외에Api::V2/Api::Admin도 있는지 — Option B 의 통합 정도 결정에 영향.
Monitoring#
403 PERM10000 발생률 모니터링 (timeseries widget 용):
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api/v1/pointclouds_controller#create,http.status_code:403}.as_count()
에이전트 측 uploadNewPointclouds 실패 추세:
logs("service:cupixworks-capture-3dreconstruction-instance status:error \"TransferManager::uploadNewPointclouds\"").index("*").rollup("count").by("@environment")
permission denied 정책 로그 추세 (record/workspace/pointcloud 등 모델별):
logs("service:cupixworks-api \"Permission denied on creating a model\"").index("*").rollup("count").by("@class")
Risk Assessment#
- Risk level: low — 1건/7일, 단일 tenant, 단일 record. 광범위한 회귀 없음.
- 예상 복잡도: trivial (운영 데이터/권한 수정). 코드 변경 시 standard (로그 보강). Agent permission 모델 분리는 Option B 기준 small-medium (3-5 file + 보안 리뷰).
Revision History#
Revision 1#
Feedback: "agent 가 뜨는 순간에는 user 권한없이 동작해야할거 같은데 이렇게 동작을 수정하려면 얼마나 어려운지 계산좀" — 에이전트가 호출 user 의 권한과 무관하게 동작하도록 바꾸는 비용 추산 요청.
판정:
| 피드백 항목 | 판정 | 근거 |
|---|---|---|
| 에이전트가 user 권한 없이 동작해야 한다는 문제 제기 | 수용 | 현재 에이전트는 job 발행 user 의 세션으로 동작 — app/jobs/create_capture_3d_reconstruction_job.rb:57 의 self.user.agent_team_session(self.jobable.team), app/models/user.rb:116-126 의 agent_team_session 이 self.sessions 로 동일 user 세션 발급. app/factories/session_factory.rb:38-43 에서 auth_agent grant_type 은 만료 기간만 다를 뿐 권한 분리 없음. 본 incident 의 직접 원인이 user 의 record 권한 부재이므로 구조적 위험이 맞음. |
| 변경 난이도 추산 | 수용 | Pundit.policy 호출 167건/92파일 (/home/ec2-user/repos/tesla/app Grep count). 가장 작은 변경 경로(Option B 의 user flag 방식)는 legacy_verification_controller.rb, user.rb, application_policy.rb 3 file. 가장 큰 변경 경로(Option D actor 시그니처)는 92 file 전수. Option A/B/C/D 비용 추산을 본문 "Agent Permission Model — 변경 비용 평가" 섹션에 반영. |
변경 사항:
- 새 섹션 추가:
## Agent Permission Model — 변경 비용 평가— 현재 구조, 4가지 옵션(A: 운영 데이터, B: policy 우회, C: per-team system user, D: actor 시그니처 변경) 별 코드 변경 범위/복잡도/리스크 표 + 권장. Fix Recommendation의장기 개선에 Revision 1 항목 1개 추가 (새 섹션 참조).Risk Assessment에 agent permission 모델 분리의 복잡도(Option B 기준 small-medium) 추가.
추가 조사 내용:
/home/ec2-user/repos/tesla/app/models/user.rb:116-126—agent_team_session정의 확인./home/ec2-user/repos/tesla/app/jobs/create_capture_3d_reconstruction_job.rb:57,86— 3D 재구성 job 의 세션 발급 경로./home/ec2-user/repos/tesla/app/factories/session_factory.rb:38-43—auth_agentgrant_type 의 의미 (만료 기간만 다름)./home/ec2-user/repos/tesla/app/controllers/concerns/legacy_verification_controller.rb:44,83— controller 가@current_user = @session.user로 세팅하는 위치,auth_agenttoken_type 인식 위치./home/ec2-user/repos/tesla/app/policies/application_policy.rb:1-36— Pundit 정책의user/record시그니처 (session 비전달)./home/ec2-user/repos/tesla/app/policies/concerns/sales_team_policy/deprecated.rb:5-7— 유일한 명시적 우회admin_administrator?의 정의./home/ec2-user/repos/tesla/app/factories/user_factory.rb:18— 시스템 내부 호출 전용skip_permission_check파라미터 패턴 (API 노출 안 됨)./home/ec2-user/repos/cupixworks/applications/agents/packages/api/src/authentication/cupix-auth.ts:200-212,236— 에이전트 측 세션 사용 흐름과User-Agent: <CPX_AGENT_NAME>헤더 송신 (서버 측 정책에 활용 가능한 신호).- Grep counts:
Pundit\.policy호출 167건/92파일,agent_team_session호출 38건 (teslaapp/기준).