Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
name: 2026-10-11-google-drive-auth-gateway-appproperties
description: Accepted decision record with rationale, alternatives, and consequences.
---

# Google Drive 연동은 auth-gateway가 토큰을 교환하고 appProperties로 무상태 동기화한다

- Date: 2026-10-11
- Kind: `adr`
- Source: issueops-docs io-21fd3b1e1f06 (galpi#12)
- Summary: Galpi는 drive.file 범위의 데스크톱 OAuth client ID만 갖고, 코드 교환과 갱신은 auth-gateway의 POST /drive/native/token이 Galpi 세션 bearer로 대행한다. 회의 폴더와 결과 파일은 Drive appProperties(galpiRoot, galpiMeeting, galpiKind, galpiSha256)와 sha256Checksum으로 다시 찾고 충돌을 판정하므로 로컬에 Drive 파일 기록을 두지 않는다.
- Context: galpi#12. 데스크톱 OAuth 클라이언트는 보안 비밀번호 없이 교환하면 400 client_secret is missing을 받는다. 바이너리를 배포하지 않고 소스 빌드로 배포하는 저장소라 비밀번호를 소스에 둘 수 없다(사용자 결정). 같은 오디오를 다시 전사하면 출력 폴더가 `{stem} 2`로 바뀌어 출력 폴더 기반 기록은 끊긴다.
- Decision: 1) 클라이언트 보안 비밀번호는 auth-gateway 서버 설정에만 두고, 앱은 AppAccessGate::bearer()로 얻은 gateway access token을 bearer로 붙여 /drive/native/token에 교환·갱신을 맡긴다. bearer는 브라우저 동의가 끝난 뒤 얻는다. 2) 400 invalid_grant만 Drive 연결을 지우고(DRIVE_RECONNECT_REQUIRED), gateway 401은 호스트 세션도 닫는다(AppAccessGate::expire). 그 밖의 실패는 DRIVE_UNAVAILABLE로 끝내고 재시도하지 않는다. 3) 회의 키는 Drive에서 가져온 녹음이면 `drive:<fileId>`, 아니면 정경화한 원본 경로의 FNV-1a 해시(`local:<16hex>`)다. domain은 sha2를 쓰지 않는다. 4) 충돌 판정은 순수 함수 decide(remote, local_sha256, overwrite): Drive sha256Checksum == galpiSha256이면 아무도 손대지 않은 파일이다. 5) 업로드는 전사 명령과 분리된 sync_meeting_to_drive IPC로 하고 한 파일의 실패는 그 파일 상태로만 보고한다.
- Consequences: auth-gateway#10의 /drive/native/token이 배포되지 않으면 Drive 연결이 DRIVE_UNAVAILABLE로 실패할 뿐 다른 기능은 영향이 없다. Drive에서 고른 파일 외의 사용자 파일에는 접근하지 않는다. sha256Checksum이 없는 파일은 충돌로 취급되므로, 실제 스모크에서 업로드 직후 checksum이 있음을 확인했다(.issueops/evidence/task-12-drive-smoke.md). 포트가 3개(DriveAuthPort, DriveFilesPort, DriveSessionStore) 늘어 18개, IPC 명령이 7개 늘어 35개다.
- Evidence:
- src-tauri/src/domain/drive.rs (decide, MeetingKey, stable_hash)
- src-tauri/src/application/drive.rs (DriveAccounts::consent, token_failure)
- src-tauri/src/application/gateway.rs (bearer, expire)
- src-tauri/src/adapters/outbound/drive/tokens.rs
- .issueops/evidence/task-12-drive-smoke.md
- Alternatives / rejected options:
- 클라이언트 보안 비밀번호를 소스 상수로 포함: 사용자가 gateway 대행으로 재결정해 기각.
- 빌드 때 비밀번호 주입, 주입 안 된 빌드는 Drive 비활성: 소스 빌드 사용자가 기능을 쓸 수 없어 기각.
- 로컬 사이드카 파일에 Drive 파일 ID·리비전 저장: 재전사 때 새 출력 폴더가 생겨 기록이 끊겨 기각.
- drive.readonly로 Galpi 폴더 전체 동기화: 제한 범위 심사가 필요해 기각.
- 앱 안 웹 Picker(iframe): CSP frame-src 'none'과 WebKit 서드파티 쿠키 차단으로 기각, 시스템 브라우저 데스크톱 Picker 채택.
- 전사 명령 안에서 업로드: 업로드 실패가 전사 결과를 막아 기각.
1 change: 1 addition & 0 deletions .issueops/adr/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ Record structural choices, rejected alternatives, and decisions that affect long
- <alternative>: <why rejected>
- Consequences: <tradeoffs and follow-up>
- Evidence: <files, commands, issues, docs>
- [Google Drive 연동은 auth-gateway가 토큰을 교환하고 appProperties로 무상태 동기화한다](2026-10-11-google-drive-auth-gateway-appproperties.md)
11 changes: 7 additions & 4 deletions .issueops/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ rule, with DDD tactical patterns — applied consistently across three runtimes:
```text
TypeScript (WebView) Rust (Tauri host) Python (WhisperX sidecar)
ui/ outer adapters/inbound/tauri.rs __main__.py outer
application/ application/ (15 ports) engine/refine use cases
application/ application/ (18 ports) engine/refine use cases
domain/ inner domain/ (inner) protocol.py (port: stdout)
adapters/tauri-backend composition.rs (root) domain pure modules
```
Expand All @@ -32,11 +32,14 @@ adapters/tauri-backend composition.rs (root) domain pure modules
- DDD tactical: value objects (`SpeakerHint`, `Participant`, `GlossaryEntry`,
`AssistantSettings`), aggregate root `Artifacts::path_for(kind)`, domain
services as pure functions, conceptual in-memory `JobRegistry`.
- Inbound adapters: `tauri.rs` (28 `#[tauri::command]`s incl. `save_compute_device`, + event bridge),
- Inbound adapters: `tauri.rs` (35 `#[tauri::command]`s incl. `save_compute_device` and
the 7 Drive commands, + event bridge),
`tauri-backend.ts` (Zod boundary), worker `__main__.py`.
- Outbound adapters: `process.rs` (worker supervision), `NativeRecorder`
(CPAL), settings/paths adapters, `ChatGptOAuthAdapter` and
`GatewayOAuthAdapter` over the shared `browser_sign_in/` loopback and PKCE,
(CPAL), settings/paths adapters, `ChatGptOAuthAdapter`, `GatewayOAuthAdapter`,
and `DriveOAuthAdapter` over the shared `browser_sign_in/` loopback and PKCE,
`DriveRestAdapter` (Drive v3; tokens exchanged by auth-gateway
`/drive/native/token`, see adr/2026-10-11-google-drive-auth-gateway-appproperties.md),
worker `EventWriter` (stdout JSONL v1).
- Platform branching lives only in outbound adapters and `composition.rs`:
outbound code branches on `Os` (`platform.rs`, `Os::current()` is the single
Expand Down
16 changes: 16 additions & 0 deletions .issueops/cautions/2026-10-11-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 2026-10-11-check
description: Caution record for a solved false case or recurring risk.
---

# 게이트 CHECK 인자에 비밀처럼 보이는 문자열을 넣으면 명령 정책이 실행을 거부한다

- Date: 2026-10-11
- Kind: `caution`
- Source: issueops-docs io-21fd3b1e1f06 (galpi#12)
- Summary: '소스에 Google 클라이언트 비밀번호가 없다'를 확인하는 게이트가 CHECK 인자에 비밀번호 접두어와 SECRET이 들어간 표식 문자열을 담자 issueops 명령 정책이 secret_like_argument로 거부해 게이트가 unchecked로 남았다.
- Context: galpi#12 G11. 같은 명령을 셸에서 실행하면 정상 동작해 원인이 보이지 않는다. 출력에 자격 증명처럼 보이는 값이 있어도 EVIDENCE가 가려져 EXPECT가 맞지 않는다.
- Resolution: 검색 패턴은 실행 중에 조립하고(chr(71)+'OCSPX'), 출력 표식은 SOURCE_CLEAN/SOURCE_LEAK처럼 중립 단어를 쓴다. 게이트가 unchecked로 남으면 `issueops gates check --json` 결과의 check_error에서 policy_denied 여부부터 본다. 커밋 전에는 새 파일이 미추적이라 `git grep`에 `--untracked`를 붙여야 하고, 접두어만 찾으면 계획·게이트 문서의 설명 문구에도 걸리므로 실제 값 모양(접두어 뒤 `-`와 20자 이상)을 `-E`로 찾는다.
- Evidence:
- .issueops/issues/12/gates.md G11
- issueops gates check --json: check_error 'check denied by policy: secret_like_argument'
13 changes: 13 additions & 0 deletions .issueops/cautions/2026-10-11-drive-path-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
name: 2026-10-11-drive-path-windows
description: Caution record for a solved false case or recurring risk.
---

# Drive 파일 이름을 Path로 해석하면 Windows에서 결과가 달라진다

- Date: 2026-10-11
- Kind: `caution`
- Source: issueops io-21fd3b1e1f06 generation 3 (CI run 38081588654)
- Summary: Drive 파일 이름 같은 외부 문자열을 std::path::Path로 나누면 OS마다 결과가 다르다. Windows는 a:b?.WAV의 a:를 드라이브 접두어로 보고 버려 b.WAV를 내고, macOS는 \를 일반 문자로 보아 C:\x\y.m4a를 C--x-y.m4a로 만든다.
- Context: issue #12 PR #13 CI의 rust (windows-latest)에서 drive::rest::tests::downloaded_names_are_sanitized_and_keep_the_extension이 실패했다. macOS 로컬과 macOS CI는 통과해 머지 미리보기의 checks_failing에서야 드러났다.
- Resolution: local_name(drive/rest.rs)은 Path 없이 문자열을 / 와 \ 로 나눠 빈 조각과 . 을 건너뛴 마지막 조각을 쓰고(..이면 meeting), 확장자는 Path::file_stem과 같은 규칙으로 직접 나눈다. 이름 정리는 paths::sanitized_stem을 재사용한다. 실제 로컬 파일 경로가 아닌 문자열(원격 이름, 사용자 입력)에는 Path를 쓰지 않고, 테스트에 C:\x\y.m4a, ..\..\boot.ini, C:, c:.wav 같은 Windows 구문을 넣어 macOS에서도 RED가 나게 한다.
16 changes: 16 additions & 0 deletions .issueops/cautions/2026-10-11-picker-google-500.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 2026-10-11-picker-google-500
description: Caution record for a solved false case or recurring risk.
---

# 데스크톱 Picker 동의 화면의 Google 500은 일시 오류이며 새로고침으로 이어진다

- Date: 2026-10-11
- Kind: `caution`
- Source: issueops-docs io-21fd3b1e1f06 (galpi#12)
- Summary: Drive 연결 동의 직후 Picker(trigger_onepick) 동의 URL이 accounts.google.com/signin/oauth/v3/consent에서 500을 한 번 돌려줬고, 같은 탭을 새로고침하자 drive.google.com/picker/oauth/consent로 정상 진행됐다.
- Context: galpi#12 실제 스모크(2026-10-10 17:40Z). 같은 파라미터 조합(기본, login_hint, mimetypes, 둘 다)을 다른 redirect로 다시 열었을 때는 500이 재현되지 않았다. 루프백 대기는 5분이라 새로고침해도 같은 시도로 콜백이 도착한다.
- Resolution: URL 파라미터를 바꾸거나 자동 재시도를 넣지 않는다. 스모크·QA에서 500을 보면 같은 탭을 새로고침하고, 사용자에게는 대기 문구('창이 보이지 않으면 취소 후 다시 시도')가 같은 해결을 안내한다. 재현되면 파라미터를 하나씩 빼서 별도 redirect로 원인을 좁힌다.
- Evidence:
- .issueops/evidence/task-12-drive-smoke.md
- src-tauri/src/adapters/outbound/drive/authorize.rs consent_url
17 changes: 17 additions & 0 deletions .issueops/cautions/2026-10-11-setbusy-kind-renderjob.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
name: 2026-10-11-setbusy-kind-renderjob
description: Caution record for a solved false case or recurring risk.
---

# 작업 시작 시 setBusy(kind)를 renderJob보다 먼저 불러야 진행 카드 제목이 새 작업을 따른다

- Date: 2026-10-11
- Kind: `caution`
- Source: issueops-docs io-21fd3b1e1f06 (galpi#12)
- Summary: AppController.begin()이 renderJob()을 setBusy(kind)보다 먼저 부르면 AppView.jobKind가 이전 작업 종류로 남아, 같은 #job-panel을 쓰는 새 작업(Drive 가져오기)의 제목과 가시성이 이전 작업 기준으로 그려진다.
- Context: galpi#12 Drive 가져오기는 전사와 같은 #job-panel 진행 카드를 쓴다. 템플릿의 기본 제목이 '회의를 전사하고 있습니다'라서 전사 작업에서는 문제가 드러나지 않았고, 계획 6차 리뷰는 '다음 전사 시작 직후 제목이 잠깐 틀림'을 비차단으로 남겼다.
- Resolution: begin()에서 setBusy(kind)로 jobKind를 먼저 정한 뒤 renderJob(this.job)을 부른다. setBusy(null)에서 jobKind를 비우지는 않는다(취소 제목이 깨진다). src/ui/drive.dom.test.ts의 'a pending import shows a running card…' 테스트가 #job-title을 고정한다. 같은 카드를 쓰는 작업 종류를 추가할 때는 app-view.ts의 usesJobPanel()과 renderJobTitle() 분기를 함께 고친다.
- Evidence:
- src/ui/controller.ts begin()
- src/ui/app-view.ts usesJobPanel, renderJobTitle, driveImportTitles
- src/ui/drive.dom.test.ts
4 changes: 4 additions & 0 deletions .issueops/cautions/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ Canonical index: [CAUTIONS.md](../CAUTIONS.md)

- Generated docs are drafts; directly verify weak evidence.
- Do not commit secrets, credentials, local state, or generated artifacts.
- [작업 시작 시 setBusy(kind)를 renderJob보다 먼저 불러야 진행 카드 제목이 새 작업을 따른다](2026-10-11-setbusy-kind-renderjob.md)
- [게이트 CHECK 인자에 비밀처럼 보이는 문자열을 넣으면 명령 정책이 실행을 거부한다](2026-10-11-check.md)
- [데스크톱 Picker 동의 화면의 Google 500은 일시 오류이며 새로고침으로 이어진다](2026-10-11-picker-google-500.md)
- [Drive 파일 이름을 Path로 해석하면 Windows에서 결과가 달라진다](2026-10-11-drive-path-windows.md)
29 changes: 29 additions & 0 deletions .issueops/evidence/task-12-drive-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# galpi#12 Drive 스모크 (2026-10-10, 실제 Google·auth-gateway·Drive)

- 실행 방식: 운영 어댑터(`GatewayOAuthAdapter`, `DriveOAuthAdapter`, `DriveRestAdapter`)와 운영 use case(`AppAccessGate`, `DriveAccounts`)를 그대로 쓰는 임시 하네스(`cargo test --lib drive::smoke -- --ignored --nocapture`, 실행 후 삭제). 시스템 기본 브라우저(Aside)에서 실제 Google 동의·Picker를 조작했다.
- 앱 화면(`bun run dev`)으로 실행하지 않은 이유: 이 컴퓨터에는 Galpi 앱 데이터(엔진, 모델 약 3GB, gateway 세션)와 사용자 Hugging Face 토큰이 없어 전사 단계를 돌릴 수 없다. 화면 배선은 `src/ui/drive.dom.test.ts`(14개)와 Vite + 가짜 IPC 화면 확인으로 검증했다.
- 전제: auth-gateway `POST /drive/native/token` 배포 확인(bearer 없이 401 `missing bearer token`, 다른 경로는 404). auth-gateway#10 이슈·PR #11은 아직 열림 상태다.
- 저장소: 하네스 메모리 저장소. 사용자 설정 파일은 건드리지 않았다. 스모크가 만든 Drive 파일·폴더는 마지막에 휴지통으로 옮겼다.

| 시각(UTC) | 단계 | 결과 |
|---|---|---|
| 17:39:12 → 17:39:56 | gateway Google 로그인 | `SignedIn { email: m16khb@gmail.com, offline: false }` |
| 17:39:56 → 17:40:45 | Drive 연결(`drive.file` 동의, "확인되지 않은 앱" 경고 없음) → gateway 대행 교환 → about → 폴더 | `Connected`, `Galpi` 폴더 생성 |
| 17:40:46 | 폴더 ID 없이 다시 찾기 | 같은 `Galpi` 폴더(`galpiRoot` appProperties 조회) |
| 17:40:48 → 17:44:32 | Picker 가져오기(`trigger_onepick`, `login_hint`, `mimetypes`) | 고른 ID가 업로드한 파일과 일치, 32,044바이트 다운로드, 로컬 수정 시각 17:40:47Z = Drive `modifiedTime` |
| 17:44:38 | 동기화 1(처음) | `Srt=Uploaded, SpeakerText=Uploaded` |
| 17:44:39 | 업로드 직후 Drive `sha256Checksum` | 있음, `galpiSha256`과 일치 |
| 17:44:40 | 동기화 2(같은 내용) | `Srt=Unchanged, SpeakerText=Unchanged` |
| 17:44:44 | 동기화 3(새 로컬 출력 폴더 `smoke meeting 2`로 재전사, 텍스트 변경) | `Srt=Unchanged, SpeakerText=Updated`, 같은 Drive 회의 폴더, 같은 파일 ID |
| 17:44:47 | 동기화 4(Drive에서 직접 수정 + 로컬 수정) | `SpeakerText=Conflict`(덮어쓰지 않음) |
| 17:44:50 | 동기화 5(덮어쓰기) | `SpeakerText=Updated` |
| 17:44:53 | 동기화 6(증강 뒤 같은 회의) | `Minutes=Uploaded`, 폴더 링크 있음 |
| 17:44:54 → 17:44:56 | 정리 | 회의 폴더·스모크 녹음·`Galpi` 폴더 휴지통 이동(200) |
| 17:44:57 | 연결 해제 | `Disconnected`, 저장소 비움, 이전 refresh token으로 갱신 시 `Rejected`(Google 폐기 확인) |
| 17:44:58 | gateway 로그아웃 | 완료 |

## 관찰과 남은 확인

- Picker 동의 첫 화면에서 Google이 한 번 500을 돌려줬고 새로고침으로 진행됐다. 같은 파라미터 조합(기본, `login_hint`, `mimetypes`, 둘 다)을 별도 redirect로 다시 열었을 때는 500이 재현되지 않아 Google 쪽 일시 오류로 판단했다. 앱은 이 경우 사용자가 브라우저에서 새로고침하거나 취소 후 다시 시도하면 된다(대기 5분).
- 50MB 이상 녹음 다운로드 중 메모리 측정(계획의 성능 영향 절)은 하지 않았다. 스모크 파일은 32KB였다. 다운로드는 `Response::chunk()`를 파일에 바로 쓰는 구조이며 `downloads_land_in_a_fresh_folder_with_the_drive_modification_time` 어댑터 테스트가 `.part` 경유 저장을 고정한다.
- 다른 계정으로 고른 파일 거부(`DRIVE_ACCOUNT_MISMATCH`)와 실제 전사 뒤 자동 업로드는 실계정·엔진이 없어 application 테스트로만 확인했다.
Loading
Loading