# AGY CLI 자동 부트스트랩 · 설치 · 인증 자동화 설계 > **이 문서의 역할**: `agy` (Google Antigravity CLI) 가 **없거나 / 낡았거나 / 로그인이 풀렸을 때**, 06:00 무인 배치가 스스로 복구하거나 사용자에게 정확히 개입을 요청하도록 만드는 **부트스트랩 정본**이다. CLI 자체의 사용법·플래그·출력 포맷은 [`05a-agy-cli-ssot.md`](./05a-agy-cli-ssot.md) 가 정본이며 이 문서는 **그 위에 프로비저닝 계층만 쌓는다**. 스케줄러 작업 구성·토스트 라이브러리 선택·Session 0 일반론은 [`08-windows-scheduling-and-resilience.md`](./08-windows-scheduling-and-resilience.md) 가 정본이다. **작성 기준일**: 2026-09-02 **검증 방식**: 공식 문서 직접 열람(WebFetch) + **로컬 머신 실측** **실측 환경**: Windows 11 Pro 10.0.26220 / PowerShell 7.6.5 / winget v1.29.250 / `agy` **1.1.24** **실측 계정**: `encep` (`C:\Users\encep`) --- ## 0. 한눈에 보기 1. **⚠️ 05a 의 인증 서술을 정정한다. `agy` 의 실제 자격증명 저장소는 파일이 아니라 Windows 자격 증명 관리자다.** 실측: `cmdkey /list` 에 `Target: gemini:antigravity`, `Type: Generic`, `User: antigravity`, `Local machine persistence` 항목이 존재한다. `CredRead` P/Invoke 로 읽으면 **BlobSize = 504 바이트**, 내용은 `{"token":{"access_token","token_type","refresh_token","expiry"},"auth_method":"consumer"}` 이며 **파일 `~/.gemini/antigravity-cli/antigravity-oauth-token` 과 정확히 같은 504 바이트**다. 결정적으로 **`LastWritten` 이 매 실행마다 갱신된다**(실측: 테스트 실행 시각 `2026-09-02T14:27:08Z`, expiry `+1h`). 반면 **파일 쪽 mtime 은 3일 전(Aug 30)에서 멈춰 있다.** → **자격 증명 관리자가 정본(authoritative), 파일은 낡은 미러다.** 2. **그 결과 배치의 인증 헬스체크는 공짜가 된다.** 05a 가 권했던 "PONG 프롬프트"(input 28,317 토큰 / 33.7초) 대신 **`CredRead("gemini:antigravity")`(오프라인·0토큰·수 ms)** + **`agy models`(3.1초 / 0토큰 / 네트워크 실검증)** 2단 프로브를 쓴다. 미존재 시 `GetLastError = 1168 (ERROR_NOT_FOUND)`. 3. **작업 스케줄러를 S4U("암호를 저장하지 않음")로 만들면 인증이 깨진다.** `CredRead` 문서 원문: *"The credential set used is the one associated with the logon session of the current token."* / *"ERROR_NO_SUCH_LOGON_SESSION … **Network logon sessions do not have an associated credential set.**"* → **`TASK_LOGON_PASSWORD`(암호 저장) 또는 `TASK_LOGON_INTERACTIVE_TOKEN`(로그온 시에만 실행)만 허용**. 이는 08번 문서의 스케줄러 설계에 대한 **제약 추가**다. 4. **winget 은 SYSTEM 컨텍스트에서 원천적으로 못 쓴다 — 확정.** Microsoft Learn 원문: *"As packages can be registered for any user except NT AUTHORITY\SYSTEM (aka LocalSystem, aka System), **the WinGet CLI is not supported in the system context.**"* 사용자 계정 세션에서는 동작하지만, 아래 5번 때문에 **설치·업데이트 수단으로는 탈락**한다. 5. **winget 의 버전 정보는 신뢰할 수 없다 — 실측으로 확정.** 이 머신에 `agy.exe` 가 **3개, 서로 다른 3개 버전**으로 존재한다: `%LOCALAPPDATA%\agy\bin\agy.exe` = **1.1.24**, `%LOCALAPPDATA%\Microsoft\WinGet\Links\agy.EXE` = **1.1.22**, `winget list` 가 보고하는 ARP 버전 = **1.1.10**(available 1.1.23), 업스트림 매니페스트 = **1.1.24**. self-update 가 winget 바깥에서 바이너리를 갈아끼우기 때문이다. → **탐지·실행 모두 `%LOCALAPPDATA%\agy\bin\agy.exe` 절대경로 1개로 고정한다.** 6. **설치 수단은 `install.ps1` 을 채택하되 `irm | iex` 파이프는 쓰지 않는다.** 전문을 읽은 결과, 스크립트는 `$isSourced` 가 참일 때(= `iex` 로 실행될 때) 실패 시 `exit 1` 이 아니라 **`throw`** 한다. 파이프 방식은 **종료 코드로 실패를 알 수 없다.** → **파일로 내려받아 `-File` 로 실행**하고 종료 코드를 받는다. 3순위로 **매니페스트 직접 다운로드**(엔드포인트·SHA512 검증 포함) 폴백을 둔다. 7. **`update.lock` 은 "업데이트 중" 신호가 아니다 — 실측.** 0바이트 파일이고 mtime 이 **2026-07-26** 에 고정되어 있으며 `agy update` 를 돌려도 변하지 않는다(파일 존재 = 락 핸들 대상). 대신 **`updater/update_status.json`** 이 사람이 읽을 수 있는 결과를 남긴다: `{"success":true,"message":"Update successful, restart CLI to use"}` / `{"success":true,"message":"Already on the latest version."}`. **`last_check.timestamp` 도 0바이트이며 의미는 mtime 에만 있다.** 8. **배치 전용 프로필 격리를 권장한다(신규 설계).** 실측: 자식 프로세스의 `USERPROFILE` 을 바꾸면 `agy` 는 **설정·로그·대화·MCP 를 그 경로 아래에 새로 만들지만, 자격 증명은 자격 증명 관리자에서 그대로 읽어 인증에 성공한다**(exit 0, `status:SUCCESS`). → 사용자의 전역 `settings.json`(현재 `command(*.exe)` 같은 위험한 allow 규칙과 8개 `trustedWorkspaces` 보유)을 건드리지 않고, **프로젝트 전용 permissions / MCP 없음 / 로그 격리**를 동시에 얻는다. 같은 실측에서 input_tokens 가 **28,317 → 14,056** 으로 떨어졌다(교란요인 있음, §5.6). 9. **`--dangerously-skip-permissions` 는 쓰지 않는다.** 대신 격리 프로필의 `permissions` 에 **`deny: read_url(*), execute_url(*), mcp(*)`** 를 넣어 프롬프트 인젝션 표면을 물리적으로 잘라낸다. 크롤링은 Python 이 하고 agy 는 텍스트만 요약한다. 10. **사용자 개입 창은 배치 프로세스가 직접 띄우지 않는다.** 06:00 에 사용자가 로그온해 있지 않을 수 있고, 비대화형 세션에서는 창이 보이지 않을 수 있다. **배치는 `state/auth_required.json` 플래그만 쓰고**, "로그온할 때만 실행"으로 등록된 **별도 대화형 작업**이 그 플래그를 보고 콘솔 창·토스트·MessageBox 를 띄운다(§7). --- ## 1. 목차 - [2. 이 문서의 경계 — 05a / 08 과 무엇이 다른가](#2-이-문서의-경계--05a--08-과-무엇이-다른가) - [3. 탐지 — agy 가 설치되어 있는가](#3-탐지--agy-가-설치되어-있는가) - [4. 설치 자동화](#4-설치-자동화) - [5. 버전 관리와 자동 업데이트](#5-버전-관리와-자동-업데이트) - [6. 인증 부트스트랩 — 이 문서의 핵심](#6-인증-부트스트랩--이-문서의-핵심) - [7. Windows 프롬프트 창 띄우기 설계](#7-windows-프롬프트-창-띄우기-설계) - [8. GEMINI_API_KEY 대체 경로](#8-gemini_api_key-대체-경로) - [9. 권한 사전 승인 설계](#9-권한-사전-승인-설계) - [10. 종료 코드 규약](#10-종료-코드-규약) - [11. scripts/ensure_agy.ps1 전체 코드](#11-scriptsensure_agyps1-전체-코드) - [12. 부속 스크립트 전체 코드](#12-부속-스크립트-전체-코드) - [13. 운영 체크리스트](#13-운영-체크리스트) - [부록 A. 출처 목록](#부록-a-출처-목록) - [부록 B. 미해결 질문 / 실측 필요 항목](#부록-b-미해결-질문--실측-필요-항목) --- ## 2. 이 문서의 경계 — 05a / 08 과 무엇이 다른가 | 주제 | 정본 문서 | 이 문서에서의 취급 | |---|---|---| | `agy` 플래그 전체표, 출력 포맷 3종, `--json-schema` 함정, 모델 목록, 크레딧 | **05a** | 참조만. 재서술 금지 | | 설치 경로, `install.ps1` 동작 9단계, `agy install` 서브커맨드 | **05a §3** | **전문 코드 기준으로 심화**(종료 코드·`iex` 함정·프록시) | | 인증 방식 4종 개요, 토큰 파일 위치 | **05a §4** | **⚠️ 정정 + 자격 증명 관리자 실측으로 대체**(§6) | | 작업 스케줄러 작업 3개 구성, 재부팅 복구, healthchecks.io, 웹훅 | **08** | 참조만. **로그온 타입 제약만 추가**(§6.5) | | 토스트 라이브러리 비교(BurntToast / win11toast / windows-toasts), Session 0 일반론 | **08 §8, §11** | 참조만. **agy 인증 전용 배선만 신규**(§7) | | 권한 엔진 문법(`action(target)`, Deny>Ask>Allow, 경로 정규화) | **05a §9** | 참조. **이 프로젝트용 실제 규칙 세트만 신규**(§9) | **중복 서술 금지 원칙**: 위 표에서 "참조만"인 항목은 이 문서에서 결론과 링크만 남기고 근거는 원 문서에 둔다. --- ## 3. 탐지 — agy 가 설치되어 있는가 ### 3.1 실측: 이 머신에는 `agy.exe` 가 3개, 버전이 3개다 ```text $ where.exe agy C:\Users\encep\AppData\Local\agy\bin\agy.exe C:\Users\encep\AppData\Local\Microsoft\WinGet\Links\agy.EXE ``` 각각을 직접 실행해 본 결과: | 경로 | `--version` | 파일 크기 | mtime | 출처 | |---|---|---|---|---| | `%LOCALAPPDATA%\agy\bin\agy.exe` | **1.1.24** | 187,601,560 B | 2026-09-02 22:08 | `install.ps1` 설치 + **self-update 가 갱신** | | `%LOCALAPPDATA%\Microsoft\WinGet\Links\agy.EXE` | **1.1.22** | 186,767,512 B | 2026-08-29 22:06 | winget portable 패키지 | | `winget list --id Google.AntigravityCLI` 보고값 | **1.1.10** (available `1.1.23`) | — | — | winget ARP DB | | 업스트림 매니페스트 `windows_amd64.json` | **1.1.24** | — | — | 자동 업데이터 서버 | `%LOCALAPPDATA%\agy\bin\` 실측 목록 — **self-update 가 이전 바이너리를 지우지 않는다**: ```text -rwxr-xr-x 187,601,560 2026-09-02 22:08 agy.exe -rwxr-xr-x 186,767,512 2026-08-27 13:12 agy.exe.1788354501993998300.old ``` 업스트림 매니페스트 실측 응답(전문): ```json { "version": "1.1.24", "url": "https://storage.googleapis.com/antigravity-public/antigravity-cli/1.1.24-6130423206641664/windows-x64/cli_windows_x64.exe", "sha512": "8d45e36d0f66bb5d5b809c10b108dbd411e621f6e7e37d908f2e0369e88d3b809fb1a0ad8cd26858ed63fb4f08a1cf84a3658ba6907938a267dcdd3a387f0c11" } ``` winget-pkgs 매니페스트 실측(전문): ```yaml # Created with YamlCreate.ps1 Dumplings Mod # yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json PackageIdentifier: Google.AntigravityCLI PackageVersion: 1.1.23 InstallerType: portable Commands: - agy ReleaseDate: 2026-08-31 Installers: - Architecture: x64 InstallerUrl: https://storage.googleapis.com/antigravity-public/antigravity-cli/1.1.23-6260551186251776/windows-x64/cli_windows_x64.exe InstallerSha256: BFFA9C1227A517D0DBD7DDFC71A64BF6473C52FAB95369CABE09FF42BD9B3B3E - Architecture: arm64 InstallerUrl: https://storage.googleapis.com/antigravity-public/antigravity-cli/1.1.23-6260551186251776/windows-arm/cli_windows_arm64.exe InstallerSha256: 519ED7E208ADFAE7DC07F62EA83623AE311CCA02F7F64DF5FD886336B25BC002 ManifestType: installer ManifestVersion: 1.12.0 ``` `InstallerType: portable` + `Commands: [agy]` 이므로 winget 은 실행 파일을 `WinGet\Packages\Google.AntigravityCLI_Microsoft.Winget.Source_8wekyb3d8bbwe\` 에 풀고 `WinGet\Links\agy.EXE` 에 심(shim)을 만든다. 실측상 이 Links 항목은 **심볼릭 링크가 아니라 186 MB 실파일 복사본**이었고(다른 winget portable 패키지들은 심볼릭 링크였다), **self-update 대상이 아니므로 시간이 지날수록 낡는다**. > **결론 1**: `where agy` / `Get-Command agy` 는 **PATH 순서에 따라 낡은 1.1.22 를 반환할 수 있다.** 실제로 이 머신의 User PATH 는 `...\agy\bin` 이 `...\WinGet\Links` 보다 앞서 있어 우연히 최신이 잡히지만, **이 순서에 의존해선 안 된다.** > **결론 2**: `winget list` 의 버전 필드는 **14개 릴리스만큼 낡아 있었다.** 버전 판정에 절대 쓰지 않는다. ### 3.2 탐지 방법 5종 비교 | # | 방법 | 명령 | 비용 | 실패 모드 | 채택 | |---|---|---|---|---|---| | 1 | **절대경로 존재 확인** | `Test-Path "$env:LOCALAPPDATA\agy\bin\agy.exe"` | ~0 ms | 사용자가 `-d/--dir` 로 다른 경로에 설치한 경우 놓침. `%LOCALAPPDATA%` 자체가 다른 세션(SYSTEM 은 `C:\Windows\system32\config\systemprofile\AppData\Local`) | ✅ **1순위** | | 2 | **버전 실행** | `& $agy --version` | **79 ms** (실측), 종료코드 0, 출력 `1.1.24` | 파일은 있으나 손상/차단(MotW)/AV 격리 시 비0. 187 MB 바이너리라 디스크 캐시 미스면 첫 로드가 느릴 수 있음 | ✅ **2순위(무결성 확인)** | | 3 | `Get-Command` / `where.exe` | `(Get-Command agy -EA SilentlyContinue).Source` | ~10 ms | **PATH 미반영 시 null.** 실측: `$env:PATH='C:\Windows\System32'` 로 좁히면 `Get-Command agy` → `$null`. `agy\bin` 은 **User PATH 에만** 있고 Machine PATH 에 없다 → SYSTEM 세션·서비스에서는 절대 못 찾는다 | ⚠️ 진단용으로만 | | 4 | `winget list` | `winget list --id Google.AntigravityCLI -e --disable-interactivity` | ~1–3 s | **버전이 낡음(1.1.10)**. SYSTEM 컨텍스트 미지원. 미설치 시 종료코드 `-1978335212` = `0x8A150014` = `APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND` (실측) | ❌ 버전 판정 금지 | | 5 | 레지스트리 Uninstall 키 | `HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*` | ~50 ms | portable 패키지는 winget 자체 ARP 항목만 만들고 `install.ps1` 설치분은 항목이 없음 | ❌ | ### 3.3 PATH 가 스케줄러 세션에 반영되지 않는 문제 실측한 PATH 구성: ```text Machine PATH : (agy 관련 항목 없음) User PATH : C:\Users\encep\AppData\Local\agy\bin C:\Users\encep\AppData\Local\Microsoft\WinGet\Links ... ``` - `agy\bin` 은 **User 범위 PATH** 에만 등록된다(`agy install` 이 그렇게 한다). - 작업 스케줄러가 **해당 사용자 계정**으로 작업을 실행하면 사용자 환경 블록이 로드되므로 User PATH 가 들어온다. - 그러나 **작업 등록 이후에 PATH 가 바뀌었다면** 이미 떠 있던 프로세스 트리는 갱신을 못 본다. 그리고 SYSTEM/서비스 계정은 애초에 User PATH 가 없다. - **해결책은 단 하나: 절대경로 사용.** 부트스트랩과 배치 모두 `$AgyExe = Join-Path $env:LOCALAPPDATA 'agy\bin\agy.exe'` 를 쓰고 PATH 를 신뢰하지 않는다. - `%LOCALAPPDATA%` 조차 신뢰할 수 없는 경우(다른 계정으로 실행)를 대비해, 부트스트랩은 **실행 계정을 먼저 검증**한다(§11 의 `Assert-RunContext`). ### 3.4 채택 탐지 알고리즘 ```text 1) 실행 컨텍스트 검증 - [Security.Principal.WindowsIdentity]::GetCurrent().Name 이 'NT AUTHORITY\SYSTEM' / 'NT AUTHORITY\LOCAL SERVICE' / 'NT AUTHORITY\NETWORK SERVICE' 이면 → 즉시 exit 30 (자격 증명 관리자·User PATH·%LOCALAPPDATA% 가 전부 다르다) 2) 후보 경로 순서대로 Test-Path a) $env:DMF_AGY_EXE (환경변수 오버라이드, 선택) b) %LOCALAPPDATA%\agy\bin\agy.exe ← 정본 c) %LOCALAPPDATA%\Microsoft\WinGet\Links\agy.exe ← 낡을 수 있음. 발견 시 경고만 3) 찾은 실행 파일로 `--version` 실행 (타임아웃 30초) - 종료코드 0 && 출력이 semver 정규식 `^\d+\.\d+\.\d+` 매치 → 설치됨 - 아니면 손상으로 간주 → 재설치 경로로 4) 두 개 이상 발견되면 버전을 모두 로그에 남기고, 경로는 (b) 를 강제 선호한다(버전 비교로 (c) 를 고르지 않는다 — 갱신되지 않는 사본이므로) ``` --- ## 4. 설치 자동화 ### 4.1 세 경로 비교 | 항목 | (a) `install.ps1` | (b) `winget install` | (c) 매니페스트 직접 다운로드 | |---|---|---|---| | 비대화형 적합성 | ✅ 완전 무인 | ⚠️ 사용자 세션 한정 | ✅ 완전 무인 | | SYSTEM/서비스 세션 | ⚠️ 동작하나 경로가 systemprofile 로 감 | ❌ **원천 불가(공식)** | ⚠️ 동일 | | 최신 버전 보장 | ✅ 업데이터 매니페스트 직결 (1.1.24) | ❌ 커뮤니티 지연 (1.1.23) | ✅ (1.1.24) | | 무결성 검증 | ✅ SHA512 내장 | ✅ SHA256 (매니페스트) | 직접 구현 필요(SHA512) | | PATH 등록 | ✅ `agy install` 핸드오프 | ✅ Links 심 | ❌ 직접 해야 함 | | 재실행 안전성 | ✅ 이미 있으면 no-op, exit 0 | ✅ (`--no-upgrade`) | 직접 구현 | | 종료 코드 신뢰성 | ⚠️ **`iex` 사용 시 깨짐(§4.2)** | ✅ 명확한 HRESULT | ✅ 직접 통제 | | 프록시 | `Invoke-WebRequest` 기본 프록시 | `--proxy` 플래그 지원 | 직접 통제 | | **채택** | ✅ **1순위(파일 실행 방식)** | ❌ | ✅ **2순위 폴백** | ### 4.2 `install.ps1` — `irm | iex` 를 쓰면 안 되는 이유 스크립트 전문을 내려받아 읽었다. 핵심 분기: ```powershell $hasPath = ($null -ne $MyInvocation.MyCommand) -and ($null -ne $MyInvocation.MyCommand.PSObject.Properties['Path']) $scriptPath = if ($hasPath) { $MyInvocation.MyCommand.Path } else { $null } $isSourced = [string]::IsNullOrEmpty($scriptPath) -or ($MyInvocation.InvocationName -eq '.') ``` 그리고 마지막: ```powershell $exitCode = $script:installExitCode if ($exitCode -ne 0) { if ($isSourced) { throw "Fatal: Installation failed." } else { exit $exitCode } } ``` - `irm https://antigravity.google/cli/install.ps1 | iex` 로 실행하면 `$MyInvocation.MyCommand.Path` 가 비어 있으므로 **`$isSourced = $true`** 가 된다. - 그러면 실패는 **`exit 1` 이 아니라 `throw`** 로 표면화된다. 호출 스크립트가 `try/catch` 로 감싸지 않으면 `$LASTEXITCODE` 는 0 인 채로 흘러가고, **부트스트랩이 "설치 성공" 으로 오판한다.** - 파일로 저장해 `powershell.exe -File install.ps1` 로 실행하면 `$isSourced = $false` 가 되어 **정상적으로 `exit 1`** 한다. > **규칙**: 이 프로젝트는 `install.ps1` 을 **파일로 내려받아 `-File` 로 실행**한다. `iex` 파이프는 금지. 스크립트가 뱉는 실패 문자열(정확한 매칭용, 전문에서 발췌): | 상황 | 정확한 문자열 | |---|---| | 이미 설치됨(정상 종료, exit 0) | `Notice: 'agy.exe' is already installed at .` | | 〃 (후속 3줄) | `The Antigravity CLI automatically self-updates in the background.` / `If you want to perform a fresh installation, delete the binary first:` / ` Remove-Item "" -Force` | | 디렉터리 옵션 값 누락 | `Error: Missing value for directory option.` | | 아키텍처 미지원 | `Fatal: Unsupported CPU architecture.` | | 매니페스트 실패 | `Fatal: Failed to download release manifest from . Network or DNS issue?` | | 스테이징 디렉터리 실패 | `Fatal: Failed to create staging directory at . Please check write permissions.` | | 바이너리 다운로드 실패 | `Fatal: Failed to download binary from . Network or DNS issue?` | | 해시 계산 실패 | `Fatal: Failed to compute file hash for verification.` | | **체크섬 불일치** | `Security Halt: Checksum verification failed. The downloaded file may be corrupted or compromised.` | | 배치 실패(파일 잠김 등) | `Write Error: Permission denied or failed to write binary to .` + `Please check directory permissions or if the file is locked (e.g. if 'agy.exe' is currently running).` | **추가로 확인한 동작들**: - **TLS**: `if ($ExecutionContext.SessionState.LanguageMode -ne 'ConstrainedLanguage') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }` — ConstrainedLanguage 모드에서는 건너뛴다(AppLocker/WDAC 환경 주의). - **해시**: `Get-FileHash -Algorithm SHA512` 우선, 실패 시 `certutil -hashfile SHA512` 폴백. **ConstrainedLanguage 에서는 `Get-FileHash` 경로가 통째로 스킵되어 certutil 로 간다.** - **`--force` 같은 재설치 플래그가 없다.** 기존 바이너리가 있으면 무조건 no-op. **강제 재설치는 `Remove-Item -Force` 후 재실행**이 유일한 방법이다. - **스테이징 경로**: `%LOCALAPPDATA%\antigravity\staging\agy.exe` (주의: `agy` 가 아니라 `antigravity`). `finally` 블록에서 삭제한다. - **핸드오프**: `& $binaryPath install $setupFlags` 를 `try/catch` 로 감싸 **실패를 삼킨다**("Absorb setup warnings/failures to align with Unix '|| true'. The binary is successfully copied and functional on disk."). 즉 **PATH 등록 실패는 설치 성공으로 보고된다.** → 부트스트랩은 설치 후 PATH 를 **직접 확인·보정**해야 한다. - **`-d` / `--dir` 이외의 인자는 전부 `agy install` 로 패스스루**된다. 즉 `-File install.ps1 --skip-aliases` 라고 주면 `agy install --skip-aliases` 가 실행된다. - **프록시**: `Invoke-RestMethod` / `Invoke-WebRequest` 를 그대로 쓴다. PowerShell 7 은 `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` 환경변수를 존중하고, Windows PowerShell 5.1 은 시스템(IE) 프록시 설정을 쓴다. 사내 프록시 환경이라면 **작업 스케줄러 작업의 환경에 `HTTPS_PROXY` 를 명시**해야 한다. ### 4.3 winget — 스케줄러/서비스 세션에서 쓸 수 있는가? **결론: 쓰지 않는다** Microsoft Learn 「Debugging and troubleshooting issues with WinGet」 의 **System Context** 절 원문: > "WinGet is delivered via the App Installer as a packaged application. MSIX (packaged) applications depend on the package being registered for the user. As packages can be registered for any user except NT AUTHORITY\SYSTEM (aka LocalSystem, aka System), **the WinGet CLI is not supported in the system context.** The Microsoft.WinGet.Client PowerShell module can be used in the system context with applications that are installed machine wide." 또한 winget 개요 문서: > "WinGet will not be available until you have logged into Windows as a user for the first time, triggering Microsoft Store to register the Windows Package Manager as part of an asynchronous process. If you have recently logged in as a user for the first time and find that WinGet is not yet available, you can open PowerShell and enter the following command to request this WinGet registration: `Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe`." 관련 이슈(제목·상태 확인): | 번호 | 제목 | 상태 | |---|---|---| | [#4422](https://github.com/microsoft/winget-cli/issues/4422) | Add support for SYSTEM account when using Winget PowerShell Module | Open | | [#548](https://github.com/microsoft/winget-cli/issues/548) | Usage with System Account | Closed | | [#2937](https://github.com/microsoft/winget-cli/issues/2937) | App install using System Account fails (`0x80070520`) | Closed | **정리한 결론**: | 실행 컨텍스트 | winget 동작 | 이 프로젝트에서 | |---|---|---| | 대화형 사용자 세션 | ✅ 동작 | 수동 진단용으로만 허용 | | 작업 스케줄러 / 사용자 계정 / 암호 저장(`TASK_LOGON_PASSWORD`) | ✅ 동작(패키지가 그 사용자에게 등록돼 있어야 함) | ❌ **버전 신뢰 불가(§3.1)** 이므로 미사용 | | 작업 스케줄러 / 사용자 계정 / S4U | ⚠️ 대체로 동작하나 자격 증명이 필요한 작업은 실패 | ❌ | | 작업 스케줄러 / SYSTEM, LocalService, NetworkService | ❌ **공식 미지원** | ❌ 금지 | 무인 실행 시 필요한 플래그(참고용, 실제로는 안 씀): ```powershell winget install --id Google.AntigravityCLI --exact ` --silent ` --accept-package-agreements ` --accept-source-agreements ` --disable-interactivity ` --nowarn ``` 각 플래그의 공식 정의: | 플래그 | 공식 설명(원문) | |---|---| | `-h`, `--silent` | "Runs the installer in silent mode. This suppresses all UI. The default experience shows installer progress." | | `--accept-package-agreements` | "Accepts any license agreements or EULAs presented by the package installer, suppressing the interactive prompt. This applies to the package's own license terms only … For a fully non-interactive install, combine with `--silent` (`-h`)." | | `--accept-source-agreements` | "Accepts the license agreement for the WinGet source (repository), suppressing the interactive prompt. This is separate from any package license." | | `--disable-interactivity` | "Disable interactive prompts." | | `-e`, `--exact` | "Uses the exact string in the query, including checking for case-sensitivity. It will not use the default behavior of a substring." | | `--proxy` | "Set a proxy to use for this execution." / `--no-proxy` "Disable the use of proxy for this execution." | | `--no-upgrade` | "Skips upgrade if an installed version already exists." | | `-o`, `--log` | "Directs the logging to a log file. You must provide a path to a file that you have the write rights to." | winget 로그 기본 경로(공식): `%LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir\*.log` 실측한 종료 코드: ```text winget list --id Google.AntigravityCLI --exact --disable-interactivity → exit 0 winget list --id No.Such.Package.Xyz --exact --disable-interactivity → exit -1978335212 (0x8A150014) ``` 관련 HRESULT (winget-cli `returnCodes.md` 발췌 — 부트스트랩에서 분기할 값만): | Hex | 심볼 | 의미 | |---|---|---| | `0x8A150014` | `APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND` | 패키지 없음 | | `0x8A150011` | `APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH` | 설치 파일 해시가 매니페스트와 불일치 | | `0x8A15002D` | `APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED` | 설치 파일 보안 검사 실패 | | `0x8A150041` | `APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED` | 패키지 약관 미동의 | | `0x8A150046` | `APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED` | 소스 약관 미동의 | | `0x8A150019` | `APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN` | 관리자 권한 필요 | | `0x8A15003A` | `APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY` | 그룹 정책 차단 | | `0x8A150061` | `APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED` | 동일 버전 이미 설치 | | `0x8A150107` | `APPINSTALLER_CLI_ERROR_INSTALL_NO_NETWORK` | 네트워크 없음 | | `0x8A150086` | `APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE` | 0바이트 다운로드 | | `0x8A15006D` | `APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE` | 필요 서비스 busy/unavailable | | `0x8A150052` | `APPINSTALLER_CLI_ERROR_PORTABLE_INSTALL_FAILED` | **portable 패키지 설치 실패(agy 가 portable 타입)** | | `0x8A150054` | `APPINSTALLER_CLI_ERROR_PORTABLE_PACKAGE_ALREADY_EXISTS` | 다른 소스의 portable 패키지가 이미 있음 | > ⚠️ **주의**: PowerShell 에서 `$LASTEXITCODE` 는 **부호 있는 32비트**로 나온다. `0x8A150014` 는 `-1978335212` 로 보인다. 비교할 때는 `('0x{0:X8}' -f $LASTEXITCODE)` 로 정규화하라. ### 4.4 매니페스트 직접 다운로드 (2순위 폴백) `install.ps1` 이 하는 일을 그대로 재현하되, **모든 실패를 우리 종료 코드로 통제**한다. 완결 함수는 §12.1 `Install-AgyFromManifest` 에 있다. 요지: 1. `PROCESSOR_ARCHITEW6432` → 없으면 `PROCESSOR_ARCHITECTURE` 로 플랫폼 결정 (`AMD64`→`windows_amd64`, `ARM64`→`windows_arm64`) 2. `GET https://antigravity-cli-auto-updater-974169037036.us-central1.run.app/manifests/.json` → `version` / `url` / `sha512` 3. `%TEMP%\dmf-agy-staging\agy.exe` 로 다운로드 (**`install.ps1` 의 스테이징 경로와 일부러 다르게 둔다** — 동시 실행 충돌 방지) 4. `Get-FileHash -Algorithm SHA512` 비교 (대소문자 무시). 불일치 → **즉시 중단, 파일 삭제, exit 20** 5. `%LOCALAPPDATA%\agy\bin\` 생성 후 `Copy-Item -Force` + `Unblock-File` 6. `& $binaryPath install --skip-aliases` 로 PATH 등록 (실패해도 계속하되 **로그에 남긴다**) 7. `& $binaryPath --version` 으로 최종 검증 ### 4.5 실행 정책 (ExecutionPolicy) 실측: ```text Scope ExecutionPolicy ----- --------------- MachinePolicy Undefined UserPolicy Undefined Process Bypass CurrentUser Undefined LocalMachine RemoteSigned ``` - 유효 정책은 `LocalMachine = RemoteSigned`. 인터넷에서 내려받은 `install.ps1` 은 **Mark-of-the-Web 때문에 차단될 수 있다.** - 공식 문서: 우선순위는 `Process` > `CurrentUser` > `LocalMachine` (그룹 정책 `MachinePolicy` / `UserPolicy` 가 있으면 그것이 최상위). `Process` 범위는 `$Env:PSExecutionPolicyPreference` 에 저장되며 세션 종료 시 사라진다. - **채택**: 스케줄러 작업의 액션을 항상 `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "