변경 이력
RAWP 1.0.3
1.0.2 대비 변경 사항을 정리한 문서입니다.
수정됨
Document Overview
추가된 줄 2 삭제된 줄 2
1 1
# RAWP 1.0: Remote Agent Wire Protocol 2 2
3 3
| 항목 | 값 | 4 4
| ---------------------- | ------------------ | 5 5
| 상태 | Stable | 6
| 버전 | 1.0.2 | 6
| 버전 | 1.0.3 | 7 7
| 데이터 평면 규격 | **RAWP-DPS 1.0.1** | 8
| 클라이언트 렌더링 규격 | **RAWP-CRS 1.0.1** | 8
| 클라이언트 렌더링 규격 | **RAWP-CRS 1.0.2** | 9 9
10 10
--- 수정됨
4. Phase 2: 제어 및 모니터링 인터페이스 (Master → Client HTTP)
추가된 줄 132 삭제된 줄 9
1 1
## 4. Phase 2: 제어 및 모니터링 인터페이스 (Master → Client HTTP) 2 2
3 3
마스터 서버가 클라이언트를 호출할 때는 반드시 발급받은 `access_token`을 사용하며, 클라이언트는 서명을 검증해야 한다 (MUST). 4 4
5 5
### 4.1. 클라이언트 설정 동기화 (Client Configuration) 6 6
7 7
클라이언트는 시스템 한계치와 지원 능력을 서버에 동기화한다. 설정은 의미적 성격에 따라 독립된 Config Scope로 분리되며, 각 스코프는 전용 엔드포인트를 통해 전체 교체(Full Replacement) 방식으로 동기화된다. 마스터 서버는 수신한 설정 값으로 요청을 스로틀링하며, 보고되지 않은 기능을 요구해서는 안 된다 (MUST). 8 8
9 9
#### 4.1.1. Config Scope 분리 원칙 10 10
11 11
클라이언트 설정은 다음 두 개의 Config Scope로 분리된다: 12 12
13 13
| Config Scope | 엔드포인트 | 성격 | 변경 트리거 | 14 14
| ------------------- | ----------------------------------- | -------------------------------------------------- | --------------------------------------------------------- | 15 15
| **Resource Limits** | `PUT /v1/nodes/config/limits` | 수량적 운영 제약 — 처리량, 전송 크기, 메모리, 정책 | 시스템 부하 변동, 메모리 압박, 운영 정책 조정 | 16 16
| **Capabilities** | `PUT /v1/nodes/config/capabilities` | 질적 기능 선언 — 클라이언트가 지원하는 기능 목록 | 플러그인/도구 로드·언로드, 모델 교체, 소프트웨어 업데이트 | 17 17
18 18
각 스코프는 독립된 `config_version`을 가지며, 한 스코프의 갱신이 다른 스코프의 버전에 영향을 주지 않는다 (MUST). 클라이언트는 변경이 발생한 스코프의 엔드포인트만 호출하면 되고, 변경되지 않은 스코프를 함께 재전송할 필요가 없다. 19 19
20 20
#### 4.1.2. 초기 동기화 (Initial Synchronization) 21 21
22 22
클라이언트는 페어링 완료(§3.3) 직후 그리고 재기동 직후, **양쪽 Config Scope를 모두** 동기화해야 한다 (MUST). 초기 동기화 시 `config_version`은 `1`로 시작한다. 서버는 양쪽 스코프의 초기 동기화가 완료되기 전까지 해당 클라이언트에 대한 세션 초기화(§5.1) 요청을 보류해야 한다 (MUST). 초기 동기화 미완료 상태에서 세션 초기화가 시도되면 서버는 `503 Service Unavailable`을 반환해야 한다 (MUST). 23 23
24 24
마스터 서버가 특정 스코프에 대한 설정을 보유하지 않은 상태에서 해당 클라이언트의 헬스 체크(§4.2) 또는 에이전트 탐색(§4.3)을 수행하는 것은 허용된다. 설정 동기화 보류 규칙은 세션 초기화에만 적용된다. 25 25
26 26
#### 4.1.3. 런타임 설정 갱신 (Runtime Configuration Update) 27 27
28 28
클라이언트는 런타임 중 설정 변경이 발생할 때마다 해당 스코프의 엔드포인트를 호출하여 서버에 변경 사항을 동기화해야 한다 (MUST). 각 호출은 해당 스코프의 **전체 필드를 포함하는 완전한 스냅샷**이어야 하며, 부분 필드만 포함하는 것은 허용되지 않는다 (MUST NOT). 서버는 수신한 스냅샷으로 해당 스코프의 기존 상태를 전체 교체한다. 29 29
30 30
#### 4.1.4. 설정 버전 관리 및 순서 보장 (Config Versioning) 31 31
32 32
모든 Config Scope 요청에는 `config_version` 필드가 포함되어야 한다 (MUST). 이 값은 각 스코프별로 독립적이며, 클라이언트가 해당 스코프의 설정을 변경할 때마다 반드시 1씩 단조 증가시켜야 한다 (MUST). 33 33
34 34
**순서 보장 규칙**: 마스터 서버는 수신한 `config_version`이 현재 보유한 해당 스코프의 버전보다 큰 경우에만 설정을 갱신해야 한다 (MUST). 현재 보유 버전 이하의 값이 수신되면 네트워크 지연에 의한 역전(stale update)으로 간주하고, `409 Conflict`를 반환하여 거부해야 한다 (MUST). 이때 응답 바디에는 서버가 현재 보유 중인 `config_version`을 포함하여 클라이언트가 재동기화할 수 있도록 해야 한다 (MUST). 35 35
36 36
**클라이언트 재기동 시 버전 처리**: 클라이언트가 재기동될 때 이전 `config_version`을 영속적으로 보관하지 못하는 경우를 위해, 다음의 특수 규칙을 적용한다: `config_version`이 `1`인 요청은 서버가 보유한 버전과 무관하게 항상 수락해야 한다 (MUST). 이는 재기동 후 초기 동기화를 보장하기 위한 것이며, 서버는 `config_version: 1` 수신 시 내부 버전 카운터를 `1`로 리셋해야 한다. 단, 활성 세션이 존재하는 상태에서 `config_version: 1`이 수신되면, 서버는 이를 클라이언트 재기동 시그널로 해석하여 시스템 로그에 기록해야 한다 (MUST). 37 37
38 38
#### 4.1.5. Resource Limits 동기화 39 39
40 40
클라이언트의 수량적 운영 제약을 서버에 동기화한다. 41 41
42 42
**Endpoint**: `PUT /v1/nodes/config/limits` (Master Server 제공) 43 43
44 44
**Request** (Client → Master): 45 45
46 46
```json 47 47
{ 48 48
"config_version": "Number (필수, 이 스코프의 단조 증가 정수, 초기값 1)", 49 49
"effective_at": "String (필수, ISO 8601, 클라이언트가 이 설정을 적용한 시각)", 50 50
"reason": "String (선택, 변경 사유. 예: 'memory_pressure', 'scheduled_maintenance')", 51 51
"limits": { 52 52
"rate_limit": "Number (필수, 초당 처리 가능한 최대 HTTP 요청 수. 양의 정수)", 53 53
"max_frame_size": "Number (필수, WSS 단일 프레임 최대 바이트. 최소 1024)", 54 54
"reattach_window": "Number (필수, WSS 단절 시 DETACHED 상태 유지 시간 초. 0 이상 정수. 0이면 DETACHED 미지원으로, 단절 시 즉시 TERMINATED 전이)", 55 55
"max_buffer_size": "Number (필수, 최대 메모리 버퍼 바이트. 양의 정수. max_frame_size 이상이어야 함)", 56 56
"buffer_overflow_policy": "String (필수, 'RING' 또는 'DROP')" 57 57
} 58 58
} 59 59
``` 60 60
61 61
**서버 검증 규칙 (MUST)**: 62 62
63 63
| 필드 | 검증 조건 | 위반 시 에러 | 64 64
| ------------------------------- | ----------------------------------------------------------------------------------- | --------------------------------- | 65 65
| `config_version` | 양의 정수. 현재 보유 버전보다 큰 값이거나, 정확히 `1` (재기동) | `409 Conflict` (버전 역전) | 66 66
| `effective_at` | 유효한 ISO 8601 형식. 미래 시각 불허 (서버 현재 시각 + 30초 허용 오차 초과 시 거부) | `422` `EFFECTIVE_AT_IN_FUTURE` | 67 67
| `limits.rate_limit` | 양의 정수 (> 0). 소수점 불허 | `422` `INVALID_RATE_LIMIT` | 68 68
| `limits.max_frame_size` | 양의 정수. 최소 `1024` | `422` `FRAME_SIZE_TOO_SMALL` | 69 69
| `limits.reattach_window` | 0 이상 정수 | `422` `INVALID_REATTACH_WINDOW` | 70 70
| `limits.max_buffer_size` | 양의 정수. `max_frame_size` 이상이어야 함 | `422` `BUFFER_SMALLER_THAN_FRAME` | 71 71
| `limits.buffer_overflow_policy` | `"RING"` 또는 `"DROP"` 중 하나 | `422` `INVALID_OVERFLOW_POLICY` | 72 72
| 모든 필드 존재 여부 | `limits` 객체 내 모든 필드가 존재해야 함 (전체 교체) | `400` `CONFIG_FIELD_MISSING` | 73 73
74 74
**Response (200 OK):** 75 75
76 76
```json 77 77
{ 78 78
"config_version": "Number (수신한 버전 에코백)", 79 79
"accepted_at": "String (ISO 8601, 서버가 설정을 수락하고 저장한 시각)", 80 80
"active_sessions_affected": "Number (현재 RUNNING 또는 DETACHED 상태의 세션 수. 이 세션들은 이전 설정으로 계속 동작함. §4.1.8 참조)" 81 81
} 82 82
``` 83 83
84 84
#### 4.1.6. Capabilities 동기화 85 85
86 86
클라이언트가 지원하는 기능 목록을 서버에 동기화한다. 87 87
88 88
**Endpoint**: `PUT /v1/nodes/config/capabilities` (Master Server 제공) 89 89
90 90
**Request** (Client → Master): 91 91
92 92
```json 93 93
{ 94 94
"config_version": "Number (필수, 이 스코프의 단조 증가 정수, 초기값 1)", 95 95
"effective_at": "String (필수, ISO 8601, 클라이언트가 이 설정을 적용한 시각)", 96 96
"reason": "String (선택, 변경 사유. 예: 'plugin_loaded: vision_v2', 'model_replaced: gpt4o')", 97 97
"capabilities": [ 98 98
"String (지원 기능 식별자 배열. 예: 'vision_v1', 'tool_call_v2'. 빈 배열 허용)" 99 99
] 100 100
} 101 101
``` 102 102
103 103
**서버 검증 규칙 (MUST)**: 104 104
105 105
| 필드 | 검증 조건 | 위반 시 에러 | 106 106
| ----------------- | ------------------------------------------------------------------------------------------ | ------------------------------------- | 107 107
| `config_version` | §4.1.5의 `config_version` 검증과 동일 | `409 Conflict` (버전 역전) | 108 108
| `effective_at` | §4.1.5의 `effective_at` 검증과 동일 | `422` `EFFECTIVE_AT_IN_FUTURE` | 109 109
| `capabilities` | JSON 배열이어야 함. 빈 배열 허용 | `422` `INVALID_CAPABILITIES_FORMAT` | 110 110
| `capabilities[*]` | 각 항목은 비어 있지 않은 문자열. 최대 128자. 허용 문자: `a-z`, `A-Z`, `0-9`, `_`, `.`, `-` | `422` `INVALID_CAPABILITY_IDENTIFIER` | 111 111
| 중복 검사 | 배열 내 동일한 값이 2회 이상 출현하면 거부 | `422` `DUPLICATE_CAPABILITY` | 112 112
113 113
**서버의 미인식 Capability 처리**: 서버는 자신이 인식하지 못하는 capability 식별자를 수신하더라도, 형식 검증을 통과하면 거부 없이 저장해야 한다 (MUST). 이는 §1.3의 필드 무시 원칙(Postel's Law)과 일관되며, 클라이언트가 서버보다 먼저 새로운 기능을 탑재하는 시나리오를 허용하기 위함이다. 114 114
115 115
**Response (200 OK):** 116 116
117 117
```json 118 118
{ 119 119
"config_version": "Number (수신한 버전 에코백)", 120 120
"accepted_at": "String (ISO 8601, 서버가 설정을 수락하고 저장한 시각)", 121 121
"active_sessions_affected": "Number (현재 RUNNING 또는 DETACHED 상태의 세션 수. 이 세션들은 이전 capabilities로 계속 동작함. §4.1.8 참조)" 122 122
} 123 123
``` 124 124
125 125
#### 4.1.7. 설정 갱신 에러 응답 (Config Update Errors) 126 126
127 127
설정 갱신 요청이 검증에 실패한 경우, 서버는 §7.1의 에러 응답 표준 규격을 확장한 다음 포맷으로 응답해야 한다 (MUST). 128 128
129 129
**검증 실패 (422 Unprocessable Entity):** 130 130
131 131
```json 132 132
{ 133 133
"error_code": "String (에러 코드. 예: 'CONFIG_VALIDATION_FAILED')", 134 134
"message": "String (휴먼 리더블 디버깅 메시지)", 135 135
"field_errors": [ 136 136
{ 137 137
"field": "String (문제가 된 필드의 JSON Path. 예: 'limits.rate_limit', 'capabilities[2]')", 138 138
"constraint": "String (위반된 제약 조건의 식별자. 예: 'POSITIVE_INTEGER', 'MIN_VALUE:1024', 'ENUM:RING|DROP')", 139 139
"reason": "String (휴먼 리더블 설명. 예: 'MUST be a positive integer')" 140 140
} 141 141
] 142 142
} 143 143
``` 144 144
145 145
`field_errors` 배열에는 검증에 실패한 **모든** 필드가 포함되어야 한다 (MUST). 첫 번째 위반에서 중단하고 나머지를 생략해서는 안 된다. 이는 클라이언트가 한 번의 응답으로 모든 위반 사항을 파악하고 수정할 수 있도록 하기 위함이다. 146 146
147 147
**버전 충돌 (409 Conflict):** 148 148
149 149
```json 150 150
{ 151 151
"error_code": "CONFIG_VERSION_CONFLICT", 152 152
"message": "String (휴먼 리더블 디버깅 메시지)", 153 153
"current_config_version": "Number (서버가 현재 보유 중인 해당 스코프의 config_version)" 154 154
} 155 155
``` 156 156
157 157
`current_config_version`을 통해 클라이언트는 자신의 `config_version`을 서버 값 이상으로 재조정한 후 재시도할 수 있다. 158 158
159 159
#### 4.1.8. 설정 적용 범위: Session-Pinned Config 160 160
161 161
런타임 설정 갱신은 **새로 초기화되는 세션에만 적용**되며, 이미 `RUNNING` 또는 `DETACHED` 상태인 세션에는 소급 적용되지 않는다 (MUST NOT). 이를 Session-Pinned Config라 한다. 162 162
163 163
**바인딩 규칙:** 164 164
165 165
- **신규 세션** (`reattach: false`): 마스터 서버가 `POST /v1/session/init` (§5.1)을 호출하는 시점에 보유한 각 스코프의 최신 설정 스냅샷을 해당 세션에 바인딩한다. 바인딩된 설정은 세션이 `TERMINATED` 상태로 전이될 때까지 불변이다 (MUST). 166 166
- **세션 재연결** (`reattach: true`): 원래 세션에 바인딩된 설정을 그대로 유지한다. 재연결 시점의 최신 설정으로 교체하지 않는다 (MUST NOT). 167 167
168 168
**서버 추적 의무**: 마스터 서버는 각 세션에 바인딩된 `config_version` 쌍(limits 스코프 버전 + capabilities 스코프 버전)을 세션 메타데이터로 보관해야 한다 (MUST). 이 정보는 세션의 행동을 예측하고 디버깅하는 데 필요하다. 169 169
170 170
**활성 세션에 대한 서버의 행동 원칙**: 설정 갱신 후에도 활성 세션은 바인딩된 이전 설정에 따라 동작하므로, 서버는 다음을 준수해야 한다 (MUST): 171 171
172 172
- 활성 세션에 대한 스로틀링은 해당 세션에 바인딩된 `rate_limit` 값을 기준으로 적용한다. 갱신된 값이 아니다. 173 173
- 활성 세션에 대한 기능 요구는 해당 세션에 바인딩된 `capabilities` 목록 내에서만 허용한다. 갱신 후 추가된 capability를 기존 세션에 요구해서는 안 된다. 174 174
- 갱신 후 제거된 capability가 활성 세션에 바인딩되어 있는 경우, 해당 세션은 정상적으로 계속 동작한다. 서버가 해당 세션을 강제 종료하거나 기능을 제한해서는 안 된다 (MUST NOT). 175 175
176 176
#### 4.1.9. 레거시 호환: POST /v1/nodes/config (지원 종료) 177 177
178 178
`POST /v1/nodes/config` 엔드포인트는 지원이 종료되었다. 모든 클라이언트는 §4.1.5 및 §4.1.6의 분리된 PUT 엔드포인트를 사용해야 한다 (MUST). 179 179
180 180
### 4.2. 헬스 체크 (Health Check) 181 181
182 182
**Endpoint**: `GET /v1/health` (Local Client 제공) 183 183
184 184
**Response**: 185 185
186 186
```json 187 187
{ 188 188
"status": "String (필수, 'online', 'busy', 'error')", 189 189
"uptime": "Number (필수, 클라이언트 가동 초)", 190 190
"active_sessions": "Number (선택, 현재 유지 중인 세션 수)" 191 191
} 192 192
``` 193 193
194 194
**상태 전이 규칙**: 활성 세션이 0개면 즉시 `online`. 1개 이상 존재 시 `busy`. 세션이 0이 되면 즉시 `online`으로 복귀해야 한다 (MUST). 195 195
196 196
### 4.3. 에이전트 탐색 및 관리 (Agent Discovery & Management) 197 197
198 198
클라이언트 내 실행 가능한 에이전트 목록 조회 및 관리를 제공한다. 199 199
200 200
**Endpoint**: `GET /v1/agents` (Local Client 제공) 201 201
202 202
**Response**: 203 203
204 204
```json 205 205
{ 206 206
"agents": [ 207 207
{ 208 208
"id": "String (필수, 고유 식별자)", 209 209
"name": "String (필수, 세션 식별자로 사용될 이름)", 210 210
"is_active": "Boolean (필수, 에이전트 활성화 상태)", 211 211
"type": "String (필수, 'builtin' | 'custom')", 212 212
"adapter_type": "String (필수, 'process' | 'sdk'. RAWP-DPS 1.0.1 §17.1 참조)", 213 213
"supported_dps_versions": [ 214 214
"String (필수, 지원하는 DPS 프로토콜 버전. 예: 'rawp-dps-1.0')" 215 215
], 216 216
"models": [ 217 217
{ 218 218
"id": "String (필수, 모델 식별자)", 219 219
"name": "String (필수, 표시명)", 220 220
"is_default": "Boolean (필수)" 221 221
} 222 222
], 223 223
"options": [ 224 224
{ 225 225
"name": "String (필수, 옵션 이름)", 226 226
"type": "String (필수, 'string' | 'boolean' | 'number' | 'enum')", 227 227
"required": "Boolean (필수)", 228 228
"default": "Any (선택, 기본값)", 229 229
"enum_values": ["String (선택, type이 'enum'일 때 허용 값 목록)"], 230 230
"description": "String (선택, 옵션 설명)" 231 231
} 232 232
], 233 233
"single_turn_process": "Boolean (선택, 기본값 false. RAWP-DPS 1.0.1 §18 참조)", 234
"description": "String (선택, 에이전트 설명)" 234
"description": "String (선택, 에이전트 설명)", 235
"allowed_directories": [ 236
"String (선택, 에이전트가 실행 가능한 디렉토리 경로 목록)" 237
], 238
"extensions": "Object (선택, 구현체별 확장 메타데이터를 위한 열린 객체)" 235 239
} 236 240
] 237 241
} 238 242
``` 239 243
240 244
`type: "builtin"`은 클라이언트에 사전 탑재된 에이전트, `type: "custom"`은 사용자가 등록한 커스텀 에이전트를 의미한다. `models` 배열이 비어 있으면 에이전트가 모델 선택을 지원하지 않음을 의미한다. 241 245
242
#### 4.3.1. 에이전트 상세 조회 246
`allowed_directories`는 에이전트가 작업 가능한 디렉토리 경로의 목록이다. 미지정 시 디렉토리 제한이 없음을 의미한다. `extensions`는 프로토콜에 정의되지 않은 구현체별 메타데이터를 전달하기 위한 열린 객체이며, §1.3의 필드 무시 원칙을 따른다. 243 247
248
#### 4.3.1. 에이전트 동기화 프로토콜 (Agent Synchronization) 249
250
클라이언트가 보유한 에이전트 목록을 마스터 서버에 체계적으로 동기화하기 위한 프로토콜이다. 251
252
##### A) 클라이언트 → 마스터 에이전트 보고 (Push) 253
254
클라이언트가 자신의 에이전트 목록을 마스터 서버에 전체 교체(Full Replacement) 방식으로 푸시한다. 255
256
**Endpoint**: `PUT /v1/nodes/agents` (Master Server 제공) 257
258
**Request** (Client → Master): 259
260
```json 261
{ 262
"agents": ["Object (§4.3의 에이전트 스키마 배열)"] 263
} 264
``` 265
266
**Response**: `204 No Content` 267
268
**트리거 조건**: 클라이언트는 다음 시점에 에이전트 목록을 보고해야 한다 (MUST): 269
270
- 페어링 완료(§3.3) 직후 초기 보고 271
- 에이전트 추가, 수정, 삭제 시 272
- 구현체가 정의한 주기적 갱신 시점 273
274
**버전 관리**: 에이전트 동기화는 §4.1의 Config Scope와 달리 `config_version` 기반 버전 관리를 사용하지 않는다. 매 요청이 전체 교체이며, 마스터는 수신한 목록으로 기존 캐시를 덮어쓴다. 275
276
##### B) 마스터 → 클라이언트 에이전트 요청 (Pull) 277
278
마스터 서버가 최신 에이전트 목록이 필요할 때, 기존 `GET /v1/agents`(§4.3) 엔드포인트를 온디맨드로 호출하여 클라이언트로부터 직접 조회한다. 279
280
**트리거 조건**: 세션 생성(§5.1) 시 에이전트 캐시 미스, 엣지의 에이전트 목록 요청(§9.2 하위) 수신 시 등 마스터가 최신 정보를 확보해야 하는 시점에 호출한다. 281
282
##### C) 에이전트 삭제 시 세션 전파 (MUST) 283
284
클라이언트에서 에이전트가 삭제(§4.3.5)되면, 클라이언트는 해당 에이전트로 생성된 모든 활성 세션(`INIT`, `RUNNING`, `DETACHED` 상태)을 종료하고, 연결된 모든 마스터 서버에 해당 세션들이 삭제되었음을 전파해야 한다 (MUST). 세션 종료는 §5.2의 절차를 따르며, `session.deleted` WSS 알림 의무도 동일하게 적용된다. 클라이언트는 모든 관련 세션의 삭제 전파가 완료된 후에만 에이전트 삭제를 확정해야 한다 (MUST). 즉, 에이전트 레코드 제거는 세션 전파 완료 이후에 수행되어야 하며, 세션 전파가 실패한 경우 에이전트 삭제를 중단해야 한다. 285
286
#### 4.3.2. 에이전트 상세 조회 287
244 288
**Endpoint**: `GET /v1/agents/{agent_id}` (Local Client 제공) 245 289
246 290
**Response (200 OK)**: §4.3의 `agents` 배열 내 단일 에이전트 객체와 동일한 스키마. 247 291
248 292
**에러**: 존재하지 않는 `agent_id` 시 `404 Not Found`. 249 293
250
#### 4.3.2. 커스텀 에이전트 등록 294
#### 4.3.3. 커스텀 에이전트 등록 251 295
252 296
**Endpoint**: `POST /v1/agents` (Local Client 제공) 253 297
254 298
**Request**: 255 299
256 300
```json 257 301
{ 258 302
"name": "String (필수, 최대 100자)", 259 303
"adapter_type": "String (필수, 'process' | 'sdk')", 260 304
"command": "String (adapter_type이 'process'일 때 필수, 실행 명령어)", 261 305
"supported_dps_versions": ["String (필수)"], 262 306
"models": ["Object (선택, §4.3의 모델 스키마)"], 263 307
"options": ["Object (선택, §4.3의 옵션 스키마)"], 264 308
"single_turn_process": "Boolean (선택)", 265 309
"resume_flag": "String (선택, single_turn_process: true일 때. RAWP-DPS 1.0.1 §18.1 참조)", 266 310
"description": "String (선택)" 267 311
} 268 312
``` 269 313
270 314
**Response (201 Created)**: 생성된 에이전트 객체 (`type: "custom"`, `is_active: true`). 271 315
272 316
**제약 조건**: `name`이 기존 에이전트와 중복되면 `409 Conflict`를 반환한다 (MUST). 273 317
274
#### 4.3.3. 커스텀 에이전트 수정 318
#### 4.3.4. 커스텀 에이전트 수정 275 319
276 320
**Endpoint**: `PATCH /v1/agents/{agent_id}` (Local Client 제공) 277 321
278
**Request**: §4.3.2의 필드 중 변경할 필드만 포함. `type: "builtin"` 에이전트는 수정할 수 없다 (MUST NOT). 시도 시 `403 Forbidden`. 322
**Request**: §4.3.3의 필드 중 변경할 필드만 포함. `type: "builtin"` 에이전트는 수정할 수 없다 (MUST NOT). 시도 시 `403 Forbidden`. 279 323
280 324
**Response (200 OK)**: 수정된 에이전트 객체. 281 325
282
#### 4.3.4. 커스텀 에이전트 삭제 326
#### 4.3.5. 커스텀 에이전트 삭제 283 327
284 328
**Endpoint**: `DELETE /v1/agents/{agent_id}` (Local Client 제공) 285 329
286 330
**제약 조건**: `type: "builtin"` 에이전트는 삭제할 수 없다 (MUST NOT). 시도 시 `403 Forbidden`. 해당 에이전트에 활성 세션이 존재하면 `409 Conflict`를 반환한다 (MUST). 287 331
288 332
**Response**: `204 No Content`. 289 333
290
#### 4.3.5. 에이전트 활성화/비활성화 334
#### 4.3.6. 에이전트 활성화/비활성화 291 335
292 336
**Endpoint**: `PATCH /v1/agents/{agent_id}/status` (Local Client 제공) 293 337
294 338
**Request**: `{"is_active": "Boolean (필수)"}` 295 339
296 340
**Response (200 OK)**: `{"id": "String", "is_active": "Boolean"}` 297 341
298
비활성화된 에이전트(`is_active: false`)는 `GET /v1/agents` 목록에 포함되지만, 세션 초기화(§5.1)에서 해당 `agent_name` 사용이 거부된다 (MUST). 거부 시 `400 Bad Request` (error_code: `"AGENT_INACTIVE"`). 342
비활성화된 에이전트(`is_active: false`)는 `GET /v1/agents` 목록에 포함되지만, 세션 초기화(§5.1)에서 해당 `agent_id` 사용이 거부된다 (MUST). 거부 시 `400 Bad Request` (error_code: `"AGENT_INACTIVE"`). 299 343
300 344
### 4.4. 파일 탐색 (Directory Browsing) 301 345
302
인가된 작업 경로 내의 디렉토리를 조회한다. 346
인가된 작업 경로 내의 디렉토리를 조회한다. 세션 내 작업 경로 탐색은 본 절을, 세션 생성 전 파일시스템 브라우징은 §4.5를 참조한다. 303 347
304 348
**Endpoint**: `POST /v1/fs/list` (Local Client 제공) 305 349
306 350
**Request**: `{"target_path": "String (필수, 조회할 절대 경로)"}` 307 351
352
### 4.5. 파일시스템 브라우징 (Filesystem Browsing) 353
354
세션 생성 전에 마스터가 클라이언트의 파일시스템을 탐색하여 작업 디렉토리(`workspace_path`)를 선택하기 위한 엔드포인트다. §4.4의 `POST /v1/fs/list`는 세션 내 인가된 작업 경로 탐색 용도이며, 본 엔드포인트는 세션 생성 이전 단계에서 사용한다. 355
356
**Endpoint**: `POST /v1/fs/browse` (Local Client 제공) 357
358
**Request**: 359
360
```json 361
{ 362
"agent_id": "String (필수, §4.3 에이전트 식별자)", 363
"path": "String (선택, 탐색할 절대 경로. 생략 시 루트 탐색)", 364
"include_files": "Boolean (선택, 기본값 true)", 365
"include_hidden": "Boolean (선택, 기본값 false)" 366
} 367
``` 368
369
**깊이 규칙**: 370
371
| 조건 | 깊이 | 동작 | 372
| -------------------------- | ---- | ------------------------------------------------------------------- | 373
| `path` 생략 또는 빈 문자열 | 2 | `allowed_directories` 루트 (미설정 시 OS 루트) 기준 2레벨 자식 반환 | 374
| `path` 지정 | 3 | 해당 경로 기준 3레벨 자식 반환 | 375
376
**Response (200 OK)**: 377
378
```json 379
{ 380
"agent_id": "String (필수)", 381
"base_path": "String (필수, 루트 모드 시 빈 문자열)", 382
"os_type": "String (필수, 'unix' | 'windows')", 383
"entries": [ 384
{ 385
"name": "String (필수, 항목 이름)", 386
"path": "String (필수, 절대 경로)", 387
"type": "String (필수, 'directory' | 'file' | 'symlink_directory' | 'symlink_file')", 388
"size": "Number (선택, 파일만. 바이트 단위)", 389
"modified_at": "String (선택, ISO 8601)", 390
"readable": "Boolean (필수, OS 읽기 권한 보유 여부)", 391
"children": ["Entry (선택, 디렉토리만. 재귀 구조)"], 392
"truncated": "Boolean (선택, 자식 목록 잘림 여부)", 393
"total_children_count": "Number (선택, 잘림 시 실제 자식 수)", 394
"error": "String (선택, OS 에러 메시지)" 395
} 396
], 397
"truncated": "Boolean (필수, 최상위 항목 목록 잘림 여부)", 398
"total_entries_count": "Number (선택, 잘림 시 실제 항목 수)" 399
} 400
``` 401
402
**권한 모델**: 403
404
- `allowed_directories`(§4.3) 범위 밖 경로 요청 시 `403 Forbidden` (`PATH_NOT_ALLOWED`)을 반환해야 한다 (MUST). 405
- `path`가 `allowed_directories`의 조상 경로인 경우, `allowed_directories`로 이어지는 디렉토리만 필터링하여 노출해야 한다 (MUST). 범위 밖 항목은 응답에 포함하지 않는다. 406
- 요청한 `path` 자체에 OS 읽기 권한이 없으면 `403 Forbidden` (`PATH_NOT_READABLE`)을 반환해야 한다 (MUST). 407
- 미등록 `agent_id` → `404 Not Found` (`AGENT_NOT_FOUND`). 408
- 비활성 에이전트(`is_active: false`) → `400 Bad Request` (`AGENT_INACTIVE`). 409
410
**엣지 케이스**: 411
412
- **심링크**: `symlink_directory`/`symlink_file` 타입으로 구분한다. 심링크가 `allowed_directories` 범위 밖을 가리키면 `readable: false`로 표시해야 한다 (MUST). 순환 심링크 감지 시 해당 항목의 `error`에 원인을 명시하고 자식 탐색을 중단해야 한다 (MUST). 413
- **숨김 항목**: `include_hidden: false`(기본값) 시 `.`으로 시작하는 항목을 제외한다. 414
- **정렬**: 디렉토리 우선, 이름 사전순 (대소문자 무시). 415
- **Truncation**: 디렉토리당 최대 500개 항목. 초과 시 `truncated: true`와 `total_children_count`(또는 최상위의 `total_entries_count`)를 설정한다. 416
- **타임아웃**: 10초 내 응답해야 하며 (MUST), 초과 시 수집된 부분 결과를 `truncated: true`로 반환한다. 417
- **Windows**: `os_type: "windows"`, 드라이브 문자 루트(`C:\` 등), `\` 경로 구분자를 사용한다. 418
419
**에러 응답**: 420
421
| HTTP 상태 | error_code | 조건 | 422
| --------- | ------------------- | ------------------------------------ | 423
| 400 | `INVALID_PATH` | 유효하지 않은 경로 형식 | 424
| 400 | `AGENT_INACTIVE` | 비활성 에이전트 | 425
| 403 | `PATH_NOT_ALLOWED` | `allowed_directories` 범위 밖 | 426
| 403 | `PATH_NOT_READABLE` | 요청 `path` 자체에 OS 읽기 권한 없음 | 427
| 404 | `AGENT_NOT_FOUND` | 에이전트 미존재 | 428
| 404 | `PATH_NOT_FOUND` | 경로 미존재 | 429
| 500 | `FS_ERROR` | 파일시스템 에러 | 430
308 431
--- 수정됨
5. Phase 3: 세션 라이프사이클 및 연결 (Session & I/O)
추가된 줄 57 삭제된 줄 1
1 1
## 5. Phase 3: 세션 라이프사이클 및 연결 (Session & I/O) 2 2
3 3
**세션 생명주기**: `INIT` → `RUNNING` → `DETACHED`(소켓 단절 시) → `TERMINATED` 4 4
5 5
### 5.1. 세션 초기화 및 재연결 (Session Initialization) 6 6
7 7
신규 에이전트 실행 또는 DETACHED 상태 프로세스에 WSS 바인딩을 위한 티켓을 발급한다. 8 8
9 9
**Endpoint**: `POST /v1/session/init` (Local Client 제공) 10 10
11 11
**Request**: 12 12
13 13
```json 14 14
{ 15 15
"session_id": "String (필수, UUID v4)", 16
"agent_name": "String (reattach: false 시 필수)", 16
"agent_id": "String (reattach: false 시 필수)", 17 17
"workspace_path": "String (reattach: false 시 필수)", 18 18
"ticket": "String (필수, WSS 연결 인증용 난수 티켓)", 19 19
"reattach": "Boolean (선택, 기본값 false. true 시 기존 구동 중 프로세스 바인딩)", 20 20
"last_sync_timestamp": "String (선택, ISO 8601)", 21 21
"last_message_id": "String (선택, UUID)" 22 22
} 23 23
``` 24 24
25 25
**제약 조건**: `reattach: true`이나 해당 `session_id`가 존재하지 않거나 종료된 경우 `404 Not Found` 반환 (MUST). 26 26
27 27
**Config 바인딩 제약 조건**: 마스터 서버는 `reattach: false`로 세션을 초기화할 때, §4.1.8에 따라 양쪽 Config Scope(limits, capabilities)의 최신 스냅샷을 해당 세션에 바인딩해야 한다 (MUST). 양쪽 스코프의 초기 동기화(§4.1.2)가 완료되지 않은 클라이언트에 대해서는 세션 초기화를 시도해서는 안 된다 (MUST NOT). 28 28
29 29
### 5.2. 세션 명시적 종료 (Session Termination) 30 30
31 31
**Endpoint**: `DELETE /v1/session/{session_id}` (Local Client 제공) 32 32
33 33
**제약 조건**: 클라이언트는 자식 프로세스에 `SIGTERM`을 전송하고, 5초 내 종료되지 않으면 `SIGKILL`로 강제 종료 후 메모리 버퍼 및 소켓을 파기해야 한다 (MUST). 34 34
35 35
**WSS 알림 의무**: 세션 종료 처리 후 WSS 연결을 닫기 전에, 해당 세션에 바인딩된 모든 WSS Connection에 RAWP-DPS 1.0.1 §7.7의 `session.deleted` 이벤트를 발송해야 한다 (MUST). 발송 순서는 RAWP-DPS 1.0.1 §7.5.3(진행 중 항목 정리) → `session.deleted` → WSS 종료이다. 36 36
37 37
### 5.3. WebSocket 연결 수립 (WebSocket Upgrade) 38 38
39 39
**Connection URI**: `wss://{client_address}/v1/ws/stream?ticket={ticket}&session_id={session_id}` 40 40
41 41
**인가 절차** (MUST): 42 42
43 43
1. 클라이언트는 HTTP Upgrade 요청 수신 시 HTTP 헤더의 `Sec-WebSocket-Protocol`을 확인해야 한다. RAWP-DPS 1.0.1 적용 시 `rawp-dps-1.0`을 우선 수락하고, 미지원 시 `rawp-1.0`으로 폴백한다. 양측 모두 미지원 시 `426` 또는 `400`을 반환해야 한다. 44 44
2. Query Parameter의 `ticket`과 `session_id`를 검증하고, 만료되거나 불일치 시 `401 Unauthorized`를 반환 후 연결을 거절한다. 45 45
3. 사용된 티켓은 1회 사용 시 즉시 무효화(Burn)해야 한다. 46 46
47 47
### 5.4. 세션 이름 변경 (Session Rename) 48 48
49 49
세션에 사용자 친화적 이름을 부여하고, 변경 사항을 모든 참여자에게 전파한다. 50 50
51 51
**SSOT (Single Source of Truth)**: 이름 변경의 권한 있는 요청은 반드시 HTTP 엔드포인트(§5.4.1, §5.4.2, §5.4.3)를 통해 수행해야 한다 (MUST). RAWP-DPS 1.0.1 §7.6의 `session.renamed` 이벤트는 HTTP 변경 후 발송되는 **알림 전용**이며, 엣지나 마스터가 DPS WSS를 통해 `session.renamed`를 직접 발송하여 이름 변경을 요청하는 것은 허용되지 않는다 (MUST NOT). 52 52
53 53
#### 5.4.1. 마스터 → 클라이언트 이름 변경 54 54
55 55
**Endpoint**: `PATCH /v1/session/{session_id}` (Local Client 제공) 56 56
57 57
**Request**: 58 58
59 59
```json 60 60
{ 61 61
"name": "String (필수, 새 세션 이름. 최대 200자)" 62 62
} 63 63
``` 64 64
65 65
**Response (200 OK)**: 66 66
67 67
```json 68 68
{ 69 69
"session_id": "String (필수)", 70 70
"name": "String (필수, 변경된 이름)", 71 71
"updated_at": "String (필수, ISO 8601)" 72 72
} 73 73
``` 74 74
75 75
**제약 조건**: 존재하지 않거나 `TERMINATED` 상태인 세션에 대한 요청은 `404 Not Found`를 반환한다 (MUST). 76 76
77 77
**WSS 알림 의무**: HTTP 응답 후, 클라이언트는 해당 세션에 바인딩된 모든 WSS Connection에 RAWP-DPS 1.0.1 §7.6의 `session.renamed` 이벤트를 발송해야 한다 (MUST). 이는 로컬 UI WSS(§10.6)와 마스터 WSS를 모두 포함한다. 78 78
79 79
#### 5.4.2. 엣지 → 마스터 이름 변경 80 80
81 81
**Endpoint**: `PATCH /v1/edge/sessions/{session_id}` (Master Server 제공) 82 82
83 83
**Request/Response**: §5.4.1과 동일한 스키마. 84 84
85 85
마스터는 이름 변경 수신 시 대상 로컬 클라이언트의 `PATCH /v1/session/{session_id}`를 호출하여 전파하고, 해당 세션에 연결된 모든 엣지 WSS에 RAWP-DPS 1.0.1 §7.6의 `session.renamed` 이벤트를 브로드캐스트해야 한다 (MUST). 86 86
87 87
#### 5.4.3. 로컬 세션 이름 변경 88 88
89 89
**Endpoint**: `PATCH /local/v1/sessions/{session_id}` (Local Client 제공) 90 90
91 91
**Request/Response**: §5.4.1과 동일한 스키마. 92 92
93 93
HTTP 응답 후, 클라이언트는 해당 세션에 바인딩된 모든 WSS Connection에 `session.renamed` 이벤트를 발송해야 한다 (MUST). 94 94
95
### 5.5. 세션 동기화 프로토콜 (Session Synchronization) 96
97
클라이언트가 보유한 열려있는 세션 목록을 마스터 서버에 체계적으로 동기화하기 위한 프로토콜이다. §4.3.1의 에이전트 동기화와 동일한 Push/Pull 패턴을 적용한다. 98
99
#### A) 클라이언트 → 마스터 세션 보고 (Push) 100
101
클라이언트가 자신의 활성 세션 목록을 마스터 서버에 전체 교체(Full Replacement) 방식으로 푸시한다. 102
103
**Endpoint**: `PUT /v1/nodes/sessions` (Master Server 제공) 104
105
**Request** (Client → Master): 106
107
```json 108
{ 109
"sessions": [ 110
{ 111
"session_id": "String (필수, UUID v4)", 112
"session_name": "String (필수)", 113
"agent_id": "String (필수)", 114
"workspace_path": "String (필수)", 115
"status": "String (필수, 'INIT' | 'RUNNING' | 'DETACHED')", 116
"origin": "String (필수, §10.3 참조)", 117
"created_at": "String (필수, ISO 8601)", 118
"last_activity_at": "String (필수, ISO 8601)", 119
"total_turns": "Number (필수)" 120
} 121
] 122
} 123
``` 124
125
**Response**: `204 No Content` 126
127
**트리거 조건**: 클라이언트는 다음 시점에 세션 목록을 보고해야 한다 (MUST): 128
129
- 페어링 완료(§3.3) 직후 초기 보고 130
- 세션 생성, 종료, 상태 변경 시 131
- 구현체가 정의한 주기적 갱신 시점 132
133
**디렉토리 필터링 (MUST)**: 클라이언트가 복수의 마스터에 연결된 경우, 각 마스터의 관할 디렉토리 범위에 해당하는 세션(`workspace_path` 기준)만 필터링하여 전달해야 한다. 관할 디렉토리 설정 방식은 프로토콜 범위 밖이다. 134
135
**버전 관리**: 세션 동기화는 §4.1의 Config Scope와 달리 `config_version` 기반 버전 관리를 사용하지 않는다. 매 요청이 전체 교체이며, 마스터는 수신한 목록으로 기존 캐시를 덮어쓴다. 136
137
`TERMINATED` 상태의 세션은 목록에 포함하지 않는다 (MUST NOT). 138
139
#### B) 마스터 → 클라이언트 세션 요청 (Pull) 140
141
마스터 서버가 최신 세션 목록이 필요할 때 온디맨드로 호출한다. 142
143
**Endpoint**: `GET /v1/sessions` (Local Client 제공) 144
145
**트리거 조건**: 엣지의 노드별 세션 조회 요청(§9.2.5) 수신 시 캐시 미스, 마스터가 최신 정보를 확보해야 하는 시점에 호출한다. 146
147
**Response (200 OK)**: Push의 `sessions` 스키마와 동일한 배열을 반환한다. 148
149
이 엔드포인트는 마스터 전용이며, 마스터의 관할 디렉토리에 따른 필터링은 클라이언트가 수행한다. 150
95 151
--- 수정됨
7. 보안 및 예외 처리 (Security & Errors)
추가된 줄 2 삭제된 줄 2
1 1
## 7. 보안 및 예외 처리 (Security & Errors) 2 2
3 3
### 7.1. HTTP 에러 응답 표준 규격 4 4
5 5
클라이언트나 서버가 HTTP 오류를 반환할 때, 다음 JSON 포맷을 Body에 포함해야 한다 (MUST). 6 6
7 7
```json 8 8
{ 9 9
"error_code": "String (e.g., TOKEN_EXPIRED, SESSION_NOT_FOUND, INVALID_PATH)", 10 10
"message": "String (휴먼 리더블 디버깅 메시지)" 11 11
} 12 12
``` 13 13
14 14
**상태 코드 매핑 규칙**: 15 15
16 16
| HTTP 상태 코드 | 사유 | 17 17
| --------------------------- | --------------------------------------------------------------------------------------------------------------- | 18 18
| `400 Bad Request` | 필수 파라미터 누락, 잘못된 UUID 포맷. | 19 19
| `401 Unauthorized` | 토큰 만료, 서명 불일치, WSS Upgrade 티켓 무효. | 20 20
| `403 Forbidden` | White-list 없는 명령어, 인가되지 않은 디렉토리 접근. | 21 21
| `404 Not Found` | 존재하지 않는 `session_id` 제어 시도. | 22 22
| `409 Conflict` | 이미 `RUNNING` 중인 세션에 `reattach: false`로 초기화 시도. Config Scope의 `config_version` 역전 감지 (§4.1.4). | 23 23
| `422 Unprocessable Entity` | 설정 갱신 시 필드값 형식, 범위, 상호 정합성 위반 (§4.1.7). | 24 24
| `429 Too Many Requests` | `rate_limit` 초과. | 25 25
| `500 Internal Server Error` | 내부 파일 시스템 오류, 바인딩 충돌 등. | 26 26
| `503 Service Unavailable` | 초기 설정 동기화 미완료 상태에서 세션 초기화 시도 (§4.1.2). | 27 27
28 28
### 7.2. 보안 제약 및 필수 보호 장치 (MUST) 29 29
30
- **경로 검증 (Path Normalization)**: 클라이언트는 모든 파일/디렉토리 조회 요청 시 경로 내 `../` 등을 정규화하고 인가된 `workspace_path` 범위 내인지 반드시 검증해야 한다 (Directory Traversal 방어). 31
- **명령어 및 에이전트 보호 (Whitelisting)**: 활성화된 `agent_name`에 매핑된 명령어만 실행 가능해야 하며, 임의의 쉘 스크립트나 바이너리를 원격으로 주입받아 실행하는 시도는 차단해야 한다 (RCE 방어). 30
- **경로 검증 (Path Normalization)**: 클라이언트는 모든 파일/디렉토리 조회 요청 시 경로 내 `../` 등을 정규화하고 인가된 `workspace_path` 범위 내인지 반드시 검증해야 한다 (Directory Traversal 방어). 세션 외 파일시스템 브라우징(§4.5) 시에도 `allowed_directories` 범위를 동일하게 적용해야 한다 (MUST). 31
- **명령어 및 에이전트 보호 (Whitelisting)**: 활성화된 `agent_id`에 매핑된 명령어만 실행 가능해야 하며, 임의의 쉘 스크립트나 바이너리를 원격으로 주입받아 실행하는 시도는 차단해야 한다 (RCE 방어). 32 32
- **버퍼 관리 및 OOM 방어**: WSS 단절 중 자식 프로세스를 유지할 때 발생하는 출력은 버퍼에 저장하되, `max_buffer_size`에 도달하면 반드시 사전에 합의된 `buffer_overflow_policy`(RING/DROP)에 따라 강제로 메모리를 관리하여 게이트웨이 다운을 방지해야 한다. 33 33
- **도구 호출 보안**: RAWP-DPS 1.0.1 적용 시, `tool.catalog.publish`에서 고지된 도구만 `tool.invoke.request`에 사용 가능하며, 고지되지 않은 도구의 호출 요청은 클라이언트가 거부해야 한다 (MUST). 자세한 도구 보안 규칙은 RAWP-DPS 1.0.1 §15를 참조한다. 34 34
35 35
### 7.3. Rate Limiting 36 36
37 37
**적용 대상**: Rate Limiting은 HTTP 엔드포인트에 대한 요청에 적용된다. WSS 프레임은 Rate Limit 대상이 아니다. WSS는 이미 세션당 단일 연결이므로 별도의 프레임 수준 제한을 두지 않는다. 38 38
39 39
**`limits.rate_limit` 필드의 의미**: §4.1.5에서 클라이언트가 보고하는 `limits.rate_limit`은 해당 클라이언트가 초당 처리 가능한 최대 HTTP 요청 수를 의미한다. 마스터 서버는 이 값을 초과하는 빈도로 해당 클라이언트에 HTTP 요청을 보내서는 안 된다 (MUST NOT). 40 40
41 41
**429 응답 형식**: Rate Limit을 초과한 요청에 대해 `429 Too Many Requests`를 반환할 때, 다음을 포함해야 한다 (MUST): 42 42
43 43
```json 44 44
{ 45 45
"error_code": "RATE_LIMITED", 46 46
"message": "String (휴먼 리더블 디버깅 메시지)", 47 47
"retry_after": "Number (필수, 초 단위, 다음 요청까지 대기해야 할 최소 시간)" 48 48
} 49 49
``` 50 50
51 51
`Retry-After` HTTP 헤더에도 동일한 값을 초 단위로 포함해야 한다 (MUST). 52 52
53 53
**양방향 적용**: 마스터가 클라이언트의 `rate_limit`을 초과하면 클라이언트가 429를 반환하고, 클라이언트가 마스터의 내부 Rate Limit을 초과하면 마스터가 429를 반환한다. 양측 모두 본 절의 응답 형식을 따른다 (MUST). 54 54
55 55
--- 수정됨
9. Edge Node API (사용자 접점 통신 규약)
추가된 줄 77 삭제된 줄 3
1 1
## 9. Edge Node API (사용자 접점 통신 규약) 2 2
3 3
본 장은 사용자가 원격지에서 UI(웹 대시보드, 모바일 앱, 챗봇 게이트웨이 등)를 통해 마스터 서버(Master Server)에 접속하여 로컬 클라이언트(Local Client)를 제어하기 위한 Northbound API를 정의한다. 4 4
5 5
**아키텍처 정의 (3-Tier):** 6 6
7 7
``` 8 8
[Edge Node (User UI)] ↔ (Edge API) ↔ [Master Server] ↔ (RAWP/Southbound) ↔ [Local Client] 9 9
``` 10 10
11 11
### 9.1. Edge Node 인증 및 권한 (Authentication) 12 12
13 13
엣지 노드는 마스터 서버의 로컬 클라이언트 인증 방식(Pairing Token)과 분리된 사용자 인증 체계를 사용한다. 마스터 서버는 **OIDC(OpenID Connect)** 와 **API Key** 두 가지 인증 방식을 지원하며, 세부 구성은 Discovery 엔드포인트를 통해 공개하고 엣지 노드가 이를 조회하여 협상한다. 모든 Edge API 요청은 `Authorization: Bearer {access_token}` 헤더를 포함해야 한다 (MUST). 14 14
15 15
#### 9.1.1. 인증 구성 Discovery 16 16
17 17
엣지 노드는 로그인 UI를 구성하기 전에 마스터 서버의 인증 구성을 조회해야 한다 (MUST). 18 18
19 19
**Endpoint**: `GET /v1/edge/auth/configuration` (Master Server 제공, 인증 불필요) 20 20
21 21
**Response (200 OK)**: 22 22
23 23
```json 24 24
{ 25 25
"issuer": "String (조건부 필수, OIDC Issuer URL. OIDC 흐름 지원 시 필수)", 26 26
"authorization_endpoint": "String (조건부 필수, OIDC Authorization Endpoint URL. OIDC 흐름 지원 시 필수)", 27 27
"token_endpoint": "String (필수, 토큰 교환 Endpoint URL)", 28 28
"userinfo_endpoint": "String (선택, OIDC UserInfo Endpoint URL)", 29 29
"end_session_endpoint": "String (선택, OIDC End Session Endpoint URL)", 30 30
"supported_edge_auth_methods": [ 31 31
"String (필수, 지원하는 인증 흐름 목록. 아래 열거 값)" 32 32
], 33 33
"supported_scopes": [ 34 34
"String (조건부 필수, 허용하는 OIDC Scope 목록. OIDC 흐름 지원 시 필수, 최소 'openid' 포함)" 35 35
], 36 36
"client_id": "String (조건부 필수, 엣지 노드가 사용할 OIDC Client ID. OIDC 흐름 지원 시 필수)", 37 37
"redirect_uris": [ 38 38
"String (조건부 필수, 허용되는 Redirect URI 패턴 목록. OIDC 흐름 지원 시 필수)" 39 39
], 40 40
"api_key_endpoint": "String (조건부 필수, API Key 인증 엔드포인트 URL. api_key 흐름 지원 시 필수)" 41 41
} 42 42
``` 43 43
44 44
**`supported_edge_auth_methods` 열거 값:** 45 45
46 46
| 값 | 설명 | 47 47
| -------------------------------- | --------------------------------------------------------------------------------------- | 48 48
| `authorization_code` | Authorization Code Flow (PKCE 필수). 브라우저 기반 엣지 노드의 기본 흐름. | 49 49
| `authorization_code_with_device` | Device Authorization Grant (RFC 8628). 브라우저가 없는 환경(TV, CLI 등). | 50 50
| `api_key` | API Key 기반 인증. 사전 발급된 API Key로 일회성 토큰을 발급받아 인증하는 흐름 (§9.1.5). | 51 51
52 52
마스터 서버는 최소 `authorization_code` 또는 `api_key` 중 하나를 지원해야 한다 (MUST). 53 53
54 54
#### 9.1.2. Authorization Code Flow (PKCE) 55 55
56 56
브라우저 기반 엣지 노드의 표준 인증 흐름이다. RFC 7636(PKCE)을 필수 적용한다 (MUST). 57 57
58 58
**흐름:** 59 59
60 60
1. 엣지 노드가 `code_verifier`(최소 43자 랜덤 문자열)를 생성하고, SHA-256 해시하여 `code_challenge`를 만든다. 61 61
2. 엣지 노드가 사용자를 `authorization_endpoint`로 리디렉트한다: 62 62
63 63
``` 64 64
{authorization_endpoint}? 65 65
response_type=code 66 66
&client_id={client_id} 67 67
&redirect_uri={redirect_uri} 68 68
&scope=openid {추가_scope} 69 69
&state={csrf_state} 70 70
&code_challenge={code_challenge} 71 71
&code_challenge_method=S256 72 72
``` 73 73
74 74
3. 사용자가 인증을 완료하면, OIDC 프로바이더가 `redirect_uri`로 `code`와 `state`를 반환한다. 75 75
4. 엣지 노드가 `state`를 검증한 후, 마스터 서버의 토큰 교환 엔드포인트를 호출한다. 76 76
77 77
#### 9.1.3. 토큰 교환 78 78
79 79
엣지 노드가 OIDC Authorization Code를 마스터 서버의 Access/Refresh Token으로 교환한다. 마스터 서버는 OIDC 프로바이더에 코드를 검증하고, 자체 토큰을 발급한다. 80 80
81 81
**Endpoint**: `POST /v1/edge/auth/token` (Master Server 제공) 82 82
83 83
**Request**: 84 84
85 85
```json 86 86
{ 87 87
"grant_type": "String (필수, 'authorization_code')", 88 88
"code": "String (필수, OIDC Authorization Code)", 89 89
"redirect_uri": "String (필수, 인증 요청 시 사용한 redirect_uri와 동일해야 함)", 90 90
"code_verifier": "String (필수, PKCE code_verifier)" 91 91
} 92 92
``` 93 93
94 94
**Response (200 OK)**: 95 95
96 96
```json 97 97
{ 98 98
"access_token": "String (필수, 마스터 서버 발급 Bearer 토큰)", 99 99
"refresh_token": "String (필수, 갱신용 토큰)", 100 100
"token_type": "String (필수, 'Bearer')", 101 101
"expires_in": "Number (필수, access_token 만료까지 초)", 102 102
"user_id": "String (필수, 인증된 사용자 식별자)", 103 103
"id_token": "String (선택, OIDC ID Token. 마스터가 전달하는 경우)" 104 104
} 105 105
``` 106 106
107 107
**에러 응답 (401 Unauthorized)**: 108 108
109 109
```json 110 110
{ 111 111
"error_code": "String (필수, 'INVALID_CODE' | 'CODE_EXPIRED' | 'INVALID_REDIRECT_URI' | 'PKCE_MISMATCH')", 112 112
"message": "String (필수, 휴먼 리더블 에러 메시지)" 113 113
} 114 114
``` 115 115
116 116
#### 9.1.4. 토큰 갱신 및 폐기 117 117
118 118
**토큰 갱신**: §3.4.4의 `POST /v1/edge/auth/refresh`를 사용한다. 119 119
120 120
**토큰 폐기 (로그아웃)**: §3.4.4의 `POST /v1/edge/auth/revoke`를 사용한다. OIDC `end_session_endpoint`가 Discovery에 포함된 경우, 엣지 노드는 마스터 토큰 폐기와 함께 OIDC 세션도 종료해야 한다 (SHOULD). 121 121
122 122
§3.4.3의 Refresh Token Rotation 및 Replay 감지 규칙이 Edge 인증에도 동일하게 적용된다 (MUST). 123 123
124 124
#### 9.1.5. API Key 인증 (API Key Authentication) 125 125
126 126
OIDC 프로바이더 없이 **사전 발급된 API Key**로 인증하는 흐름이다. 서버 간 통신, 자동화 파이프라인, CLI 도구 등 브라우저 기반 OIDC 흐름이 적합하지 않은 환경에서 사용한다. 127 127
128 128
**사전 조건**: API Key는 마스터 서버의 관리 인터페이스 또는 별도 채널을 통해 사전 발급된다. API Key 발급 절차는 본 프로토콜의 범위 밖이며, 마스터 서버의 구현에 따른다. 129 129
130 130
**인증 흐름:** 131 131
132 132
1. 엣지 노드가 API Key를 사용하여 **일회성 인증 토큰(one-time token)**을 요청한다. 133 133
2. 마스터 서버가 API Key를 검증하고, 단기 유효(최대 60초)의 일회성 토큰을 발급한다. 134 134
3. 엣지 노드가 일회성 토큰을 마스터 서버의 토큰 교환 엔드포인트에 제출하여 `access_token`/`refresh_token`을 발급받는다. 135 135
4. 이후 표준 Bearer 토큰 인증 및 §3.4.3의 갱신 규칙을 따른다. 136 136
137 137
**Step 1: 일회성 토큰 발급** 138 138
139 139
**Endpoint**: `POST {api_key_endpoint}` (Discovery의 `api_key_endpoint` 참조) 140 140
141 141
**Request**: 142 142
143 143
```json 144 144
{ 145 145
"api_key": "String (필수, 사전 발급된 API Key)" 146 146
} 147 147
``` 148 148
149 149
**Response (200 OK)**: 150 150
151 151
```json 152 152
{ 153 153
"one_time_token": "String (필수, 일회성 인증 토큰. 최소 64자 랜덤 문자열)", 154 154
"expires_in": "Number (필수, 토큰 유효 기간. 초 단위, 최대 60)" 155 155
} 156 156
``` 157 157
158 158
**에러 응답 (401 Unauthorized)**: 159 159
160 160
```json 161 161
{ 162 162
"error_code": "String (필수, 'INVALID_API_KEY' | 'API_KEY_EXPIRED' | 'API_KEY_REVOKED')", 163 163
"message": "String (필수, 휴먼 리더블 에러 메시지)" 164 164
} 165 165
``` 166 166
167 167
**일회성 토큰 규칙:** 168 168
169 169
- 일회성 토큰은 **1회 사용** 후 즉시 무효화해야 한다 (MUST). 사용 여부와 관계없이 `expires_in` 경과 후 만료된다. 170 170
- 마스터 서버는 동일 API Key에 대해 동시에 유효한 일회성 토큰을 최대 1개로 제한해야 한다 (MUST). 새 일회성 토큰 발급 시 기존 미사용 토큰은 즉시 무효화한다. 171 171
- 일회성 토큰은 전송 중 탈취를 방지하기 위해 TLS(HTTPS) 환경에서만 발급해야 한다 (MUST). 172 172
173 173
**Step 2: 토큰 교환** 174 174
175 175
**Endpoint**: `POST /v1/edge/auth/token` (§9.1.3과 동일 엔드포인트) 176 176
177 177
**Request**: 178 178
179 179
```json 180 180
{ 181 181
"grant_type": "String (필수, 'one_time_token')", 182 182
"token": "String (필수, Step 1에서 발급받은 일회성 토큰)" 183 183
} 184 184
``` 185 185
186 186
**Response (200 OK)**: 187 187
188 188
```json 189 189
{ 190 190
"access_token": "String (필수, 마스터 서버 발급 Bearer 토큰)", 191 191
"refresh_token": "String (필수, 갱신용 토큰)", 192 192
"token_type": "String (필수, 'Bearer')", 193 193
"expires_in": "Number (필수, access_token 만료까지 초)", 194 194
"user_id": "String (필수, API Key에 바인딩된 사용자 식별자)" 195 195
} 196 196
``` 197 197
198 198
**에러 응답 (401 Unauthorized)**: 199 199
200 200
```json 201 201
{ 202 202
"error_code": "String (필수, 'INVALID_TOKEN' | 'TOKEN_EXPIRED' | 'TOKEN_ALREADY_USED')", 203 203
"message": "String (필수, 휴먼 리더블 에러 메시지)" 204 204
} 205 205
``` 206 206
207 207
발급된 `access_token`/`refresh_token`은 OIDC 흐름으로 발급된 토큰과 동일한 권한 및 갱신 규칙(§9.1.4, §3.4.3)을 따른다. 208 208
209 209
**보안 고려사항:** 210 210
211 211
- API Key는 장기 유효 자격 증명이므로, 클라이언트 측에서 안전하게 저장해야 한다 (MUST). 환경 변수, 시크릿 매니저, 암호화된 설정 파일 등 일반 텍스트 노출을 방지하는 저장 방식을 사용한다. 212 212
- 마스터 서버는 API Key 단위로 Rate Limiting을 적용해야 한다 (MUST). §7.3의 Rate Limiting 규칙을 따르되, 일회성 토큰 발급 엔드포인트에는 추가적으로 분당 최대 10회의 요청 제한을 권장한다 (SHOULD). 213 213
- API Key가 유출된 것으로 판단되면, 마스터 서버는 해당 API Key로 발급된 모든 토큰(일회성 토큰, access_token, refresh_token)을 즉시 무효화할 수 있어야 한다 (MUST). 214 214
215 215
### 9.2. 로컬 머신(Node) 풀 관리 및 상태 모니터링 216 216
217 217
사용자 계정에 바인딩된 로컬 클라이언트 목록과 현재 상태를 조회한다. 218 218
219 219
#### 9.2.1. 연결된 노드 목록 조회 220 220
221 221
**Endpoint:** `GET /v1/edge/nodes` 222 222
223 223
**Response (200 OK):** 224 224
225 225
```json 226 226
{ 227 227
"nodes": [ 228 228
{ 229 229
"node_id": "String (필수, 클라이언트 식별자)", 230 230
"device_name": "String (필수, 등록 시 제공된 이름)", 231 231
"status": "String (필수, 'online' | 'busy' | 'offline')", 232 232
"last_seen": "String (필수, ISO 8601, 마스터 서버와 마지막 통신 시각)", 233 233
"capabilities": [ 234 234
"String (선택, 로컬 머신이 지원하는 기능 목록. §4.1.6에서 동기화된 최신 값 반영)" 235 235
], 236 236
"active_sessions_count": "Number (필수)", 237 237
"config_versions": { 238 238
"limits": "Number (선택, 현재 보유 중인 Resource Limits config_version)", 239 239
"capabilities": "Number (선택, 현재 보유 중인 Capabilities config_version)" 240 240
} 241 241
} 242 242
] 243 243
} 244 244
``` 245 245
246 246
#### 9.2.2. 로컬 머신별 사용량 및 지표 (Metrics) 247 247
248 248
해당 노드에서 발생한 세션들의 누적/실시간 사용량을 조회한다. 249 249
250 250
**Endpoint:** `GET /v1/edge/nodes/{node_id}/metrics` 251 251
252 252
**Query Parameters:** `?period=today` (선택, `'today'`, `'week'`, `'month'`) 253 253
254 254
**Response (200 OK):** 255 255
256 256
```json 257 257
{ 258 258
"node_id": "String (필수)", 259 259
"period": "String (필수)", 260 260
"aggregated_usage": { 261 261
"total_input_tokens": "Number", 262 262
"total_output_tokens": "Number", 263 263
"estimated_cost": "Number (USD 기준)", 264 264
"total_tool_invocations": "Number" 265 265
}, 266 266
"health": { 267 267
"uptime_seconds": "Number", 268 268
"last_error": "String (선택, 최근 발생한 치명적 에러)" 269 269
} 270 270
} 271 271
``` 272 272
273 273
#### 9.2.3. 노드 설정 조회 (Node Configuration Lookup) 274 274
275 275
엣지 노드가 특정 로컬 머신의 현재 설정 상태를 조회한다. 276 276
277 277
**Endpoint:** `GET /v1/edge/nodes/{node_id}/config` 278 278
279 279
**Response (200 OK):** 280 280
281 281
```json 282 282
{ 283 283
"node_id": "String (필수)", 284 284
"limits": { 285 285
"config_version": "Number (필수)", 286 286
"effective_at": "String (필수, ISO 8601)", 287 287
"limits": { 288 288
"rate_limit": "Number", 289 289
"max_frame_size": "Number", 290 290
"reattach_window": "Number", 291 291
"max_buffer_size": "Number", 292 292
"buffer_overflow_policy": "String" 293 293
} 294 294
}, 295 295
"capabilities": { 296 296
"config_version": "Number (필수)", 297 297
"effective_at": "String (필수, ISO 8601)", 298 298
"capabilities": ["String"] 299 299
} 300 300
} 301 301
``` 302 302
303 303
이 엔드포인트는 Pull 방식으로 최신 설정을 확인하기 위한 것이다. 실시간 Push 알림은 §9.4.4를 참조한다. 304 304
305
#### 9.2.4. 노드 에이전트 목록 조회 (Node Agent List) 306
307
엣지 노드가 특정 로컬 머신의 에이전트 목록을 마스터 서버를 통해 조회한다. 308
309
**Endpoint:** `GET /v1/edge/nodes/{node_id}/agents` (Master Server 제공) 310
311
**Response (200 OK):** 312
313
```json 314
{ 315
"node_id": "String (필수)", 316
"agents": ["Object (§4.3의 에이전트 스키마와 동일)"] 317
} 318
``` 319
320
마스터 서버는 §4.3.1의 에이전트 동기화 프로토콜을 통해 캐시한 에이전트 목록을 반환한다. 캐시가 존재하지 않거나 마스터가 최신 정보를 확보해야 한다고 판단하면, 대상 클라이언트의 `GET /v1/agents`(§4.3)를 호출하여 갱신한 후 응답해야 한다 (MUST). 321
322
**에러**: 해당 `node_id`의 노드가 존재하지 않거나 `offline` 상태이면 `404 Not Found`를 반환한다. 323
324
#### 9.2.5. 노드 세션 목록 조회 (Node Session List) 325
326
엣지 노드가 특정 로컬 머신의 세션 목록을 마스터 서버를 통해 조회한다. 327
328
**Endpoint:** `GET /v1/edge/nodes/{node_id}/sessions` (Master Server 제공) 329
330
**Response (200 OK):** 331
332
```json 333
{ 334
"node_id": "String (필수)", 335
"sessions": ["Object (§5.5의 세션 스키마와 동일)"] 336
} 337
``` 338
339
마스터 서버는 §5.5의 세션 동기화 프로토콜을 통해 캐시한 해당 노드의 세션 목록을 반환한다. 캐시가 존재하지 않거나 마스터가 최신 정보를 확보해야 한다고 판단하면, 대상 클라이언트의 `GET /v1/sessions`(§5.5)를 호출하여 갱신한 후 응답해야 한다 (MUST). 340
341
**§9.3.2와의 차이**: §9.3.2(`GET /v1/edge/sessions`)는 사용자 계정 기준으로 모든 노드에 걸친 전체 세션을 조회하는 반면, 본 엔드포인트는 특정 노드 기준으로 해당 머신의 세션(로컬 세션 포함)을 조회한다. 342
343
**에러**: 해당 `node_id`의 노드가 존재하지 않거나 `offline` 상태이면 `404 Not Found`를 반환한다. 344
345
#### 9.2.6. 노드 파일시스템 브라우징 (Node Filesystem Browsing) 346
347
엣지 노드가 특정 로컬 머신의 파일시스템을 마스터 서버를 통해 탐색한다. 세션 생성(§9.3.1) 전 `workspace_path` 선택에 사용한다. 348
349
**Endpoint:** `POST /v1/edge/nodes/{node_id}/fs/browse` (Master Server 제공) 350
351
**Request:** §4.5의 Request 스키마와 동일하다. 352
353
**Response (200 OK):** 354
355
```json 356
{ 357
"node_id": "String (필수)", 358
"agent_id": "String (필수)", 359
"base_path": "String (필수, 루트 모드 시 빈 문자열)", 360
"os_type": "String (필수, 'unix' | 'windows')", 361
"entries": ["Entry (§4.5의 Entry 스키마와 동일)"], 362
"truncated": "Boolean (필수)", 363
"total_entries_count": "Number (선택)" 364
} 365
``` 366
367
마스터 서버는 대상 클라이언트의 `POST /v1/fs/browse`(§4.5)로 요청을 투명하게 프록시한다. 클라이언트가 반환한 4xx 에러는 그대로 패스스루한다. 368
369
**에러:** 370
371
| HTTP 상태 | error_code | 조건 | 372
| ----------- | ---------------- | ------------------------ | 373
| 400/403/404 | (§4.5 동일) | 클라이언트 에러 패스스루 | 374
| 404 | `NODE_NOT_FOUND` | 노드 미존재 | 375
| 404 | `NODE_OFFLINE` | 노드 오프라인 | 376
| 502 | `CLIENT_ERROR` | 클라이언트 5xx 에러 | 377
| 504 | `CLIENT_TIMEOUT` | 클라이언트 응답 타임아웃 | 378
305 379
### 9.3. 엣지 세션(Edge Session) 라이프사이클 관리 306 380
307 381
엣지 노드는 마스터 서버를 통해 특정 로컬 머신에 새로운 세션을 생성하거나 기존 세션을 조회할 수 있다. 308 382
309 383
#### 9.3.1. 원격 세션 생성 (Edge → Master) 310 384
311
사용자가 특정 로컬 머신을 선택하여 대화를 시작할 때 호출한다. 385
사용자가 특정 로컬 머신을 선택하여 대화를 시작할 때 호출한다. `workspace_path` 선택을 위해 §9.2.6의 파일시스템 브라우징을 사전에 사용할 수 있다. 312 386
313 387
**Endpoint:** `POST /v1/edge/nodes/{node_id}/sessions` 314 388
315 389
**Request:** 316 390
317 391
```json 318 392
{ 319
"agent_name": "String (필수, 실행할 에이전트)", 393
"agent_id": "String (필수, 실행할 에이전트)", 320 394
"workspace_path": "String (선택, 작업 디렉토리)" 321 395
} 322 396
``` 323 397
324 398
**Response (201 Created):** 325 399
326 400
```json 327 401
{ 328 402
"session_id": "String (필수, UUID v4)", 329 403
"edge_ws_ticket": "String (필수, 엣지용 WSS 연결 티켓. 1회용)", 330 404
"status": "String (필수, 'INIT' | 'RUNNING')", 331 405
"pinned_config_versions": { 332 406
"limits": "Number (필수, 이 세션에 바인딩된 Resource Limits config_version)", 333 407
"capabilities": "Number (필수, 이 세션에 바인딩된 Capabilities config_version)" 334 408
} 335 409
} 336 410
``` 337 411
338 412
**동작 원리:** 이 요청을 받은 마스터 서버는 즉시 대상 로컬 머신의 `POST /v1/session/init` (본 스펙 §5.1)을 호출하여 로컬 세션을 확보한 뒤 응답해야 한다. 339 413
340 414
#### 9.3.2. 활성 세션 목록 조회 341 415
342 416
사용자 계정에 귀속된 모든 세션의 현재 상태를 조회한다. 343 417
344 418
**Endpoint:** `GET /v1/edge/sessions` 345 419
346 420
**Query Parameters:** 347 421
348 422
| 파라미터 | 타입 | 필수 | 설명 | 349 423
| --------- | ------ | ---- | ----------------------------------------------------------------------------------------------- | 350 424
| `status` | String | 선택 | 세션 상태 필터. `'INIT'`, `'RUNNING'`, `'DETACHED'` 중 하나. 미지정 시 모든 상태의 세션을 반환. | 351 425
| `node_id` | String | 선택 | 특정 노드에 귀속된 세션만 필터링. | 352 426
353 427
**Response (200 OK):** 354 428
355 429
```json 356 430
{ 357 431
"sessions": [ 358 432
{ 359 433
"session_id": "String (필수, UUID v4)", 360 434
"node_id": "String (필수, 세션이 실행 중인 로컬 클라이언트 식별자)", 361
"agent_name": "String (필수, 실행 중인 에이전트 이름)", 435
"agent_id": "String (필수, 실행 중인 에이전트 식별자)", 362 436
"status": "String (필수, 'INIT' | 'RUNNING' | 'DETACHED')", 363 437
"created_at": "String (필수, ISO 8601, 세션 생성 시각)", 364 438
"last_activity_at": "String (필수, ISO 8601, 마지막 이벤트 수신 시각)", 365 439
"total_turns": "Number (필수, 해당 세션의 총 Turn 수)", 366 440
"pinned_config_versions": { 367 441
"limits": "Number (필수, 이 세션에 바인딩된 Resource Limits config_version)", 368 442
"capabilities": "Number (필수, 이 세션에 바인딩된 Capabilities config_version)" 369 443
} 370 444
} 371 445
] 372 446
} 373 447
``` 374 448
375 449
`pinned_config_versions`를 통해 엣지 노드는 각 세션이 어떤 시점의 설정으로 동작 중인지를 목록 수준에서 확인할 수 있다. 노드의 현재 `config_versions`(§9.2.1)와 비교하면, 세션이 최신 설정인지 이전 설정인지를 즉시 판별할 수 있다. `TERMINATED` 상태의 세션은 이미 종료되었으므로 목록에 포함하지 않는다 (MUST NOT). 종료된 세션의 이력 조회가 필요한 경우 §9.3.3의 턴 히스토리 API를 사용한다. 376 450
377 451
#### 9.3.3. 세션 턴 히스토리 조회 (Session Turn History) 378 452
379 453
기존 세션의 대화 이력을 **Turn 단위**로 페이지네이션하여 조회한다. RAWP-CRS 1.0.1 §2.6(대화 이력 점진적 로딩)에서 클라이언트가 이전 Turn을 점진적으로 로딩할 때 사용한다. 380 454
381 455
**Endpoint:** `GET /v1/edge/sessions/{session_id}/turns` 382 456
383 457
**Query Parameters:** 384 458
385 459
| 파라미터 | 타입 | 필수 | 설명 | 386 460
| -------- | ------ | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------- | 387 461
| `limit` | Number | 선택 | 한 번에 반환할 최대 Turn 수. 기본값 10, 최대 100. 클라이언트의 CRS §2.6.1 N 값과 동일하게 설정한다 (SHOULD). | 388 462
| `before` | String | 선택 | 커서 기반 페이지네이션. 이 값보다 이전의 Turn을 반환한다. 미지정 시 가장 최신 Turn부터 반환한다. §9.3.2의 `total_turns`로 전체 수를 확인할 수 있다. | 389 463
390 464
**Response (200 OK):** 391 465
392 466
```json 393 467
{ 394 468
"session_id": "String (필수, UUID v4)", 395 469
"turns": [ 396 470
{ 397 471
"turn_id": "String (필수, Turn 식별자)", 398 472
"turn_index": "Number (필수, 0부터 시작하는 Turn 순서 인덱스)", 399 473
"started_at": "String (필수, ISO 8601, Turn 시작 시각)", 400 474
"ended_at": "String (선택, ISO 8601, Turn 종료 시각. 진행 중이면 null)", 401 475
"user_message": { 402 476
"text": "String (필수, 사용자 입력 텍스트)", 403 477
"file_references": ["Object (선택, 파일 참조 목록)"] 404 478
}, 405 479
"frames": [ 406 480
"Object[] (필수, 해당 Turn 내 모든 DPS 프레임의 배열. 각 요소는 RAWP-DPS 1.0.1 Envelope 전체)" 407 481
] 408 482
} 409 483
], 410 484
"pagination": { 411 485
"has_more": "Boolean (필수, 이전 Turn이 더 존재하는지 여부)", 412 486
"next_cursor": "String (선택, 다음 요청의 before 파라미터에 사용할 커서. has_more가 true일 때 포함)", 413 487
"total_turns": "Number (필수, 해당 세션의 총 Turn 수)" 414 488
} 415 489
} 416 490
``` 417 491
418 492
**Turn 정렬**: `turns` 배열은 **최신 Turn이 먼저** 오는 역순(descending)으로 정렬된다 (MUST). 클라이언트는 이를 역순으로 렌더링하여 가장 오래된 Turn이 상단에, 최신 Turn이 하단에 위치하도록 한다. 419 493
420 494
**에러 응답:** 421 495
422 496
| 상태 코드 | 조건 | 423 497
| ----------------- | ---------------------------------------------------------------- | 424 498
| `404 Not Found` | 해당 `session_id`의 세션이 존재하지 않거나 권한이 없는 경우. | 425 499
| `400 Bad Request` | `limit`가 범위를 초과하거나, `before` 커서가 유효하지 않은 경우. | 426 500
427 501
### 9.4. 엣지 데이터 평면 (Edge WebSocket 통신) 428 502
429 503
엣지 노드는 발급받은 티켓을 사용하여 마스터 서버와 WSS를 연결하며, 마스터 서버는 이 연결을 대상 로컬 머신과 투명하게 중계(Transparent Proxy)한다. 430 504
431 505
**Connection URI:** `wss://{master_host}/v1/edge/ws/stream?ticket={edge_ws_ticket}&session_id={session_id}` 432 506
433 507
#### 9.4.1. 프로토콜 투명성 (Protocol Transparency) 434 508
435 509
엣지 노드와 마스터 서버 간의 WSS 통신은 RAWP-DPS 1.0.1 규격을 완전히 동일하게 사용한다 (MUST). 436 510
437 511
엣지 노드는 본 스펙 §6의 `control.*` 이벤트(사용자 프롬프트, 인터랙션 응답 등)를 마스터 서버로 발송하며, 마스터는 이를 로컬 머신으로 포워딩한다. 438 512
439 513
엣지 노드는 로컬 머신이 발송한 `agent.*`, `tool.*`, `session.*` 이벤트를 마스터 서버를 통해 수신하여 사용자 UI에 렌더링한다. 440 514
441 515
#### 9.4.2. 마스터 서버의 중계 및 개입 규칙 442 516
443 517
마스터 서버는 단순한 파이프 역할을 넘어 다음의 제어 책임을 가진다: 444 518
445 519
**라우팅**: 엣지 노드의 발신 프레임을 올바른 로컬 머신의 소켓으로 전달. 446 520
447 521
**사용량 집계**: 중계되는 `session.usage` 이벤트를 인터셉트하여 데이터베이스에 누적 저장 (§9.2.2 지표 제공용). 448 522
449 523
**파일 검색 중계**: 파일 검색 이벤트는 마스터가 양방향으로 중계한다. `control.file.search`와 `control.file.search.cancel`은 엣지 → 마스터 → 로컬 클라이언트 방향으로 포워딩하고, `session.file.candidates`는 로컬 클라이언트 → 마스터 → 엣지 방향으로 포워딩한다. `session.file.candidates`는 `session.usage`와 달리 인터셉트 없이 단순 포워딩 대상이다. 450 524
451 525
**보안 필터링**: 엣지 노드가 허가되지 않은 `control.prompt.request` 내장 도구(e.g., 로컬 시스템 파괴성 명령어)를 전송하려 할 경우, 마스터 서버 단에서 프레임을 드롭하고 엣지 노드에 `session.error`를 반환해야 한다 (MUST). 452 526
453 527
#### 9.4.3. 다중 접점 동기화 (Multi-device Synchronization) 454 528
455 529
동일한 `session_id`에 대해 여러 엣지 노드(예: 모바일 앱과 데스크톱 브라우저 동시 접속)가 연결을 요청할 경우, 마스터 서버는 RAWP-DPS 1.0.1 §2.4의 브로드캐스트 원칙과 동일한 정책을 적용해야 한다 (MUST). 456 530
457 531
- 로컬 클라이언트로부터 수신한 모든 DPS 프레임(`agent.*`, `tool.*`, `session.*`)을 해당 세션에 연결된 모든 엣지 WSS로 브로드캐스트해야 한다 (MUST). 458 532
- 엣지 노드가 발송한 `control.*` 프레임(예: `control.prompt.request`)도 동일 세션에 연결된 다른 모든 엣지 WSS로 중계해야 한다 (MUST). 이를 통해 엣지 A가 전송한 프롬프트를 엣지 B도 수신하여 표시할 수 있다. 459 533
- 중계 시 `message_id` 기반 멱등성 처리를 수행하여 발신 엣지에 자신의 프레임이 되돌아오는 것을 방지해야 한다 (MUST). 460 534
461 535
**동시 프롬프트 충돌 처리**: 동일 세션에 대해 여러 엣지 노드가 동시에 `control.prompt.request`를 발송한 경우, 마스터 서버는 `message_id` 타임스탬프 기준 선착순(first-come)으로 하나만 수락하고, 후발 요청에 대해서는 RAWP-DPS 1.0.1 §7.4.1의 `session.error` (error_code: `"CONCURRENT_PROMPT"`, fatal: `false`)를 반환해야 한다 (MUST). 462 536
463 537
#### 9.4.4. 노드 설정 변경 Push 알림 (Node Config Change Notification) 464 538
465 539
로컬 클라이언트가 §4.1.5 또는 §4.1.6을 통해 설정을 갱신하면, 마스터 서버는 해당 노드에 연결된 모든 활성 엣지 WSS 소켓에 설정 변경 알림 프레임을 전송해야 한다 (MUST). 이 알림을 통해 엣지 노드는 UI에 반영할 설정 변경(예: capabilities 변경으로 인한 기능 목록 갱신)을 실시간으로 인지할 수 있다. 466 540
467 541
**알림 프레임 구조**: 468 542
469 543
이 알림은 DPS Envelope(RAWP-DPS 1.0.1 §2)과 독립된 **Edge 전용 알림 프레임**이다. 세션 단위가 아닌 노드 단위의 이벤트이므로 DPS Envelope의 `session_id`, `turn_id`, `v` 필드를 사용하지 않는다. 마스터 서버가 자체적으로 생성하여 엣지 WSS로 발송한다. 470 544
471 545
```json 472 546
{ 473 547
"type": "node.config.changed", 474 548
"message_id": "String (필수, UUID v4)", 475 549
"timestamp": "String (필수, ISO 8601)", 476 550
"payload": { 477 551
"node_id": "String (필수, 설정이 변경된 노드의 식별자)", 478 552
"changed_scope": "String (필수, 'limits' | 'capabilities')", 479 553
"config_version": "Number (필수, 갱신된 스코프의 새 config_version)", 480 554
"reason": "String (선택, 클라이언트가 갱신 요청 시 전달한 reason 값 전파)" 481 555
} 482 556
} 483 557
``` 484 558
485 559
엣지 노드는 `type` 필드로 이 프레임을 DPS 프레임과 구분한다. DPS 프레임은 항상 `v` 필드가 존재하며, Edge 전용 알림 프레임은 `v` 필드가 없다. 486 560
487 561
**엣지 노드의 처리 규칙**: 488 562
489 563
- `node.config.changed` 수신 시, 엣지 노드는 `changed_scope`에 따라 관련된 로컬 캐시를 무효화해야 한다 (MUST). 490 564
- 상세 설정 값이 필요하면 §9.2.3의 `GET /v1/edge/nodes/{node_id}/config`을 Pull 호출하여 최신 값을 획득한다. 알림 프레임 자체에는 변경된 설정의 전체 값을 포함하지 않는다. 이는 알림 프레임의 크기를 최소화하고, 엣지 노드가 필요한 시점에만 상세 데이터를 가져오도록 하기 위함이다. 491 565
- `changed_scope: "capabilities"` 수신 시, 엣지 UI가 기능 목록이나 에이전트 지원 기능 표시를 동적으로 갱신하는 경우, Pull 호출 후 UI를 갱신해야 한다 (SHOULD). 492 566
- 활성 세션에는 §4.1.8에 따라 이전 설정이 유지되므로, 현재 진행 중인 세션의 동작을 변경할 필요는 없다. 493 567
494 568
**전송 범위**: 마스터 서버는 해당 노드(`node_id`)에 연결된 모든 엣지 WSS 소켓에 알림을 브로드캐스트한다. 특정 세션의 WSS가 아닌, 해당 노드와 관련된 모든 엣지 연결이 대상이다. 세션 WSS가 아직 연결되지 않았더라도, 해당 노드에 대해 다른 세션의 WSS가 활성 상태이면 해당 소켓을 통해 알림이 전달된다. 495 569
496 570
--- 수정됨
10. 로컬 세션 관리 (Local Session Management)
추가된 줄 3 삭제된 줄 3
1 1
## 10. 로컬 세션 관리 (Local Session Management) 2 2
3 3
본 장은 사용자가 데스크톱 앱에서 마스터 서버 없이 직접 에이전트와 대화하기 위한 로컬 세션 프로토콜을 정의한다. 로컬 세션은 §5의 원격 세션과 동일한 세션 상태 머신(`INIT → RUNNING → DETACHED → TERMINATED`)과 RAWP-DPS 1.0.1 프로토콜을 공유하되, 인증과 보안 모델이 다르다. 4 4
5 5
### 10.1. 로컬 세션 엔드포인트 6 6
7 7
로컬 세션 전용 엔드포인트는 `/local/v1/` 접두사를 사용하여 원격(마스터 발) 엔드포인트(`/v1/`)와 네임스페이스를 분리한다. 8 8
9 9
#### 10.1.1. 로컬 세션 생성 10 10
11 11
**Endpoint**: `POST /local/v1/sessions` (Local Client 제공) 12 12
13 13
**Request**: 14 14
15 15
```json 16 16
{ 17
"agent_name": "String (필수, 실행할 에이전트 이름. §4.3의 에이전트 목록 참조)", 17
"agent_id": "String (필수, 실행할 에이전트 식별자. §4.3의 에이전트 목록 참조)", 18 18
"workspace_path": "String (필수, 작업 디렉토리 절대 경로)", 19
"session_name": "String (선택, 세션 표시명. 생략 시 agent_name + 생성 시각으로 자동 생성)" 19
"session_name": "String (선택, 세션 표시명. 생략 시 agent_id + 생성 시각으로 자동 생성)" 20 20
} 21 21
``` 22 22
23 23
**Response (201 Created)**: 24 24
25 25
```json 26 26
{ 27 27
"session_id": "String (필수, UUID v4)", 28 28
"session_name": "String (필수, 세션 표시명. 요청에서 생략된 경우 자동 생성된 이름)", 29 29
"ws_ticket": "String (필수, 1회용 WSS 연결 티켓)", 30 30
"status": "String (필수, 'INIT')" 31 31
} 32 32
``` 33 33
34 34
WSS 연결: `ws://127.0.0.1:{port}/v1/ws/stream?ticket={ws_ticket}&session_id={session_id}` 35 35
36 36
#### 10.1.2. 로컬 세션 재개 37 37
38 38
**Endpoint**: `POST /local/v1/sessions/{session_id}/resume` (Local Client 제공) 39 39
40 40
**Request**: 41 41
42 42
```json 43 43
{ 44 44
"last_sync_timestamp": "String (선택, ISO 8601)", 45 45
"last_message_id": "String (선택, UUID)" 46 46
} 47 47
``` 48 48
49 49
**Response (200 OK)**: 50 50
51 51
```json 52 52
{ 53 53
"session_id": "String (필수)", 54 54
"ws_ticket": "String (필수, 1회용 WSS 연결 티켓)", 55 55
"status": "String (필수, 'RUNNING')" 56 56
} 57 57
``` 58 58
59 59
**제약 조건**: 60 60
61 61
- 존재하지 않거나 `TERMINATED` 상태인 세션에 대한 요청은 `404 Not Found`를 반환한다 (MUST). 62 62
- `RUNNING` 상태인 세션에 대한 요청은 새 WSS 티켓을 발급하여 `200 OK`로 응답한다 (MUST). 이는 추가 WSS Connection을 수립하는 경우(로컬 UI 연결)에 해당한다. 63 63
- `INIT` 상태인 세션에 대한 요청은 `409 Conflict`를 반환한다 (MUST). 초기화가 완료되지 않은 세션은 재개할 수 없다. 64 64
65 65
#### 10.1.3. 로컬 세션 종료 66 66
67 67
**Endpoint**: `DELETE /local/v1/sessions/{session_id}` (Local Client 제공) 68 68
69 69
**제약 조건**: §5.2와 동일한 종료 절차를 따른다. `session.deleted` WSS 알림 의무(§5.2)도 동일하게 적용된다. 70 70
71 71
**Response**: `204 No Content`. 72 72
73 73
#### 10.1.4. 로컬 세션 목록 조회 74 74
75 75
**Endpoint**: `GET /local/v1/sessions` (Local Client 제공) 76 76
77 77
**Query Parameters**: 78 78
79 79
| 파라미터 | 타입 | 필수 | 설명 | 80 80
| -------- | ------ | ---- | --------------------------------------------------------------------------- | 81 81
| `status` | String | 선택 | `'INIT'`, `'RUNNING'`, `'DETACHED'` 중 하나. 미지정 시 모든 활성 세션 반환. | 82 82
| `origin` | String | 선택 | `'local'` 또는 `'remote'`. §10.3 참조. 미지정 시 모든 origin의 세션 반환. | 83 83
84 84
**Response (200 OK)**: 85 85
86 86
```json 87 87
{ 88 88
"sessions": [ 89 89
{ 90 90
"session_id": "String (필수)", 91
"agent_name": "String (필수)", 91
"agent_id": "String (필수)", 92 92
"workspace_path": "String (필수)", 93 93
"session_name": "String (필수)", 94 94
"status": "String (필수, 'INIT' | 'RUNNING' | 'DETACHED')", 95 95
"origin": "String (필수, §10.3 참조)", 96 96
"created_at": "String (필수, ISO 8601)", 97 97
"last_activity_at": "String (필수, ISO 8601)" 98 98
} 99 99
] 100 100
} 101 101
``` 102 102
103 103
### 10.2. 로컬 인증 면제 104 104
105 105
- 클라이언트의 HTTP/WSS 서버가 루프백 주소(`127.0.0.1` 또는 `::1`)에 바인딩된 경우, `/local/v1/` 접두 엔드포인트에 대해 `Authorization` 헤더 검증을 면제해야 한다 (MUST). 106 106
- 클라이언트가 외부 네트워크 주소에 바인딩된 경우, `/local/v1/` 엔드포인트를 노출해서는 안 된다 (MUST NOT). 이는 인증 없는 외부 접근을 차단하기 위함이다. 107 107
108 108
### 10.3. 세션 식별 (Session Origin) 109 109
110 110
모든 세션은 `origin` 속성을 가진다: 111 111
112 112
| `origin` 값 | 설명 | 113 113
| ---------------------- | ------------------------------------------------------------------------------- | 114 114
| `"local"` | §10.1.1을 통해 로컬에서 생성된 세션 | 115 115
| `"remote:{server_id}"` | §5.1을 통해 마스터 서버가 생성한 원격 세션. `server_id`는 마스터의 고유 식별자. | 116 116
117 117
`origin`은 세션 생성 시 결정되며 변경할 수 없다 (MUST NOT). 118 118
119 119
### 10.4. 로컬 세션 보안 모델 120 120
121 121
- **경로 검증 면제**: 로컬 세션(`origin: "local"`)은 §7.2의 경로 검증(`workspace_path` 범위 제한)을 적용하지 않는다. 사용자가 직접 조작하는 환경이므로 파일시스템 접근 범위를 제한하지 않는다. 122 122
- **도구 승인**: 로컬 세션에서의 도구 승인 요청은 RAWP-DPS 1.0.1 §4.3.1의 `agent.interaction.request`를 통해 사용자에게 직접 표시된다. 마스터를 경유하지 않는다. 123 123
124 124
### 10.5. 로컬-원격 세션 공존 125 125
126 126
- 동일 에이전트에 대해 로컬 세션과 원격 세션이 동시에 존재할 수 있다 (MUST 지원). 127 127
- 각 세션은 독립된 에이전트 프로세스 또는 SDK 인스턴스를 가진다. 세션 간 에이전트 상태를 공유하지 않는다 (MUST NOT). 128 128
129 129
### 10.6. 원격 세션 로컬 감지 130 130
131 131
마스터가 `POST /v1/session/init`(§5.1)로 원격 세션을 생성하면, 클라이언트는 해당 세션을 `origin: "remote:{server_id}"`로 등록한다. 이 세션은 `GET /local/v1/sessions`(§10.1.4) 응답에 포함되어야 한다 (MUST). 132 132
133 133
로컬 UI가 원격 세션에 연결할 때는 동일 클라이언트의 로컬 WSS 엔드포인트를 사용한다: 134 134
135 135
``` 136 136
ws://127.0.0.1:{port}/v1/ws/stream?ticket={local_ticket}&session_id={session_id} 137 137
``` 138 138
139 139
이때 `local_ticket`은 `/local/v1/sessions/{session_id}/resume`(§10.1.2)을 통해 발급받는다. 로컬 UI WSS 연결과 마스터 WSS 연결은 독립적이며, 에이전트의 DPS 이벤트는 양쪽 소켓 모두에 전달되어야 한다 (MUST). 140 140
141 141
---