@pinta-ai/pinta-gemini 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ # pinta-gemini — Background Research & Feasibility
2
+
3
+ > 목적: `pinta-cc`(Claude Code), `pinta-codex`(OpenAI Codex CLI), `pinta-copilot`(GitHub Copilot)
4
+ > 와 동일한 역할의 **`pinta-gemini`** 어댑터를 만든다. 단, 하나의 어댑터로 **3개 호스트를 동시 지원**한다:
5
+ >
6
+ > 1. **Gemini CLI** (Google, 오픈소스 — `gemini-cli/`)
7
+ > 2. **Antigravity CLI 1.0** (Google DeepMind, 클로즈드 바이너리 — `~/.gemini/antigravity-cli/`)
8
+ > 3. **Antigravity 2.0** (Google, 문서화된 hook 스펙 — `.agents/` / `~/.gemini/config/`)
9
+ >
10
+ > 본 문서는 (A) 기존 3개 어댑터의 역할/규약, (B) 3개 타깃 호스트의 hook 분석과 능력 검증,
11
+ > (C) **2개 프로토콜 패밀리** 간 hook 차이 매칭, (D) 통합 어댑터 설계·구현 TODO 를 정리한다.
12
+
13
+ 작성일: 2026-06-12 (multi-host 개정)
14
+ 근거 코드/문서:
15
+ - `/Users/pintaai/PINTA/{pinta-cc,pinta-codex,pinta-copilot}` (선례 어댑터)
16
+ - `/Users/pintaai/PINTA/gemini-cli/` (Gemini CLI 소스 + `HOOKS.md`)
17
+ - `/Users/pintaai/PINTA/gemini-cli/ANTIGRAVITY_ANALYSIS.md`, `ANTIGRAVITY_HOOKS.md` (Antigravity 1.0 바이너리 분석)
18
+ - `/Users/pintaai/PINTA/gemini-cli/ANTIGRAVITY2_HOOKS.md` (Antigravity 2.0 공식 hook 스펙)
19
+
20
+ ---
21
+
22
+ ## 0. TL;DR (결론 먼저)
23
+
24
+ **3개 호스트 모두 단일 어댑터로 동시 지원 가능하다.** 단, **2개의 hook 프로토콜 패밀리**를 흡수하는
25
+ normalization 레이어가 필요하다 (pinta-copilot 이 CLI/ext payload 를 흡수한 것과 동일 전략).
26
+
27
+ | 요구 능력 | Gemini CLI | Antigravity 1.0 | Antigravity 2.0 |
28
+ |---|---|---|---|
29
+ | 라이프사이클 이벤트 구독 | ✅ 11종 (settings.json) | ✅ 5종 (named hooks.json) | ✅ 5종 (named hooks.json) |
30
+ | Tool use **allow/deny** | ✅ `decision:"deny"` | ✅ `PreToolHookResult` (바이너리) | ✅ `decision:"deny"` (+`ask`/`force_ask`) |
31
+ | **사유(reason) 출력** | ✅ `reason`(+`systemMessage`) | ✅ (proto 상 지원) | ✅ `reason` |
32
+ | OTLP 텔레메트리 forward | ✅ hook→OTLP POST | ✅ 동일 | ✅ 동일 |
33
+ | event 이름이 payload 에 있나 | ✅ `hook_event_name` | ❌ 없음 | ❌ 없음 |
34
+ | 필드 casing | snake_case | camelCase | camelCase |
35
+ | 설치 위치 (실측 교정 → **PART F**) | **`~/.gemini/extensions/pinta-gemini/`** (extension; settings.json은 trust 게이트에 막힘) | **`~/.gemini/config/hooks.json`** (전역; agy v1.0.7 실측) | `~/.gemini/config/hooks.json`(전역) or `.agents/`(workspace) |
36
+ | timeout 단위 | ms (기본 60000) | **초** (기본 30) | **초** (기본 30) |
37
+
38
+ → **핵심 난점은 "Antigravity payload 에 이벤트 이름이 없다"는 점**. handler config 에 `env` 필드도 없어
39
+ 환경변수로 stamp 도 불가. → **설치 시 command 에 `--agent`/`--event` 인자를 박아 넣는 것을 1차(확정) 수단**으로
40
+ 구분한다 (DG1). payload-shape 추론은 probe 실패 시에만 켜는 보조 수단으로 강등.
41
+ 나머지는 normalization 으로 흡수하면 pinta-cc core(otlp/transport/retry-queue/redact/guard/trace)를 그대로 재사용한다.
42
+
43
+ > ✅ **2026-06-15 실측 검증 완료**: 3개 호스트 모두 실제로 hook을 발사해 payload를 캡처·검증함
44
+ > (gemini 8/8, antigravity 5/5 커버리지). **확정된 설치 경로·구조·payload 형상은 § PART F 참조**
45
+ > — 이 표의 가정들은 PART F에서 교정/확정되었다. Antigravity 1.0(agy v1.0.7)은 2.0과 **동일 프로토콜·동일 전역 경로**로 확인됨.
46
+
47
+ ---
48
+
49
+ ## PART A — 기존 어댑터가 하는 일 (선례)
50
+
51
+ 세 선례 어댑터는 동일 패턴이다 (pinta-copilot DESIGNDOC D1: "fork pinta-cc, reuse core").
52
+
53
+ - **역할**: 호스트의 hook 이벤트 → (1) OTLP/HTTP span 으로 변환·forward(관측), (2) 선택적으로 tool 호출을 원격 guard 로 allow/deny(집행).
54
+ - **통신**: 호스트가 `node dist/index.js` spawn → 이벤트 JSON 을 **stdin** → deny 시 **stdout** JSON → **exit 0**.
55
+ - **공유 core**: `otlp.ts`(Bronze flatten, ULID→traceId), `transport.ts`(OTLP POST+timeout), `retry-queue.ts`(JSONL, cap1000, 파일락), `trace.ts`(턴당 ULID), `redact.ts`(시크릿 마스킹+truncation), `guard.ts`(`PINTA_GUARD_ENDPOINT` POST, 50ms, **fail-open**), `config.ts`/env 로더.
56
+ - **Fail-open**: 텔레메트리/guard 실패가 호스트를 막지 않음. 항상 exit 0.
57
+ - **Guard 요청**: `{ input:{ spanId, toolName, toolInput, rawTextFields:{toolInput} } }`, 헤더 `x-pinta-relay-token`.
58
+ - **Guard 응답**: `{ decision:'ALLOW'|'DENY'|'REVIEW', reason, userMessage?, durationMs }`.
59
+ - **배포**: `@pinta-ai/pinta-<host>` npm, PolyForm Noncommercial, OSS+Manager 2채널.
60
+
61
+ 호스트별 인터랙션 차이 요약:
62
+
63
+ | | Claude Code | Codex | Copilot |
64
+ |---|---|---|---|
65
+ | 등록 | plugin marketplace `hooks.json`(자동) | `~/.codex/hooks.json`(수동)+`config.toml` | `~/.copilot/hooks/*.json`(수동) |
66
+ | 이벤트 수 | 14 | 5 | 12 (CLI+ext+cloud) |
67
+ | deny 키 | `permissionDecision:"deny"`+`permissionDecisionReason` | 동일 | `permissionDecision` or `behavior:"deny"`+`message` |
68
+ | fail 정책 | fail-open | fail-open | **fail-closed**→exit0 필수 |
69
+ | 특이점 | userConfig→env | env 미주입→env파일 | snake/camel 흡수, `PINTA_COPILOT_EVENT` env fallback |
70
+
71
+ → pinta-gemini 가 흡수해야 할 패턴: **stdin JSON 파싱 → 정규화 → OTLP forward → guard deny stdout JSON → 항상 exit 0**.
72
+
73
+ ---
74
+
75
+ ## PART B — 타깃 호스트 능력 검증 (코드/문서 근거)
76
+
77
+ ### B.1 Gemini CLI (오픈소스, 신뢰도 높음)
78
+
79
+ 근거: `gemini-cli/packages/core/src/hooks/`, `HOOKS.md`
80
+
81
+ - **이벤트(11)**: `BeforeTool`, `AfterTool`, `BeforeAgent`, `AfterAgent`, `SessionStart`, `SessionEnd`, `BeforeModel`, `AfterModel`, `BeforeToolSelection`, `PreCompress`, `Notification`.
82
+ - **CC 호환 클론**: command hook 이 stdin 에 `JSON.stringify(input)` 주입, stdout 을 `HookOutput` 으로 parse, 그리고 hook env 에 `CLAUDE_PROJECT_DIR` 까지 하위호환 주입 (`hookRunner.ts:354`).
83
+ - **등록**: `~/.gemini/settings.json` 의 `hooks` 키 (또는 `.gemini/hooks.json`, 확장). 구조: `{ "hooks": { "BeforeTool": [{ matcher, sequential?, hooks:[{name,type:"command",command,timeout}] }] } }`. **timeout=ms** (기본 60000).
84
+ - **입력(snake_case)**: base `{session_id, transcript_path, cwd, hook_event_name, timestamp}`. BeforeTool: `+{tool_name, tool_input, mcp_context?, original_request_name?}`.
85
+ - **allow/deny**: `HookDecision = 'ask'|'block'|'deny'|'approve'|'allow'`. BeforeTool deny → `scheduler/hook-utils.ts:64` `getBlockingError()` → `POLICY_VIOLATION` 에러로 tool 차단, reason 을 모델에 surface.
86
+ - **deny 출력(top-level)**: `{"decision":"deny","reason":"...","systemMessage":"..."}`. exit 2 = emergency block(stderr→reason).
87
+ - **주의**: `hookRunner.ts:455` 가 `stdout.trim() || stderr.trim()` 로 parse → **stdout 이 비면 stderr 가 systemMessage 로 노출**. 우리 어댑터는 항상 stdout 에 JSON(`{}` 이상)을 써야 함.
88
+ - **AfterModel 은 청크마다 발화**(`HOOKS.md §AfterModel`) → 등록 금지(span 폭발).
89
+
90
+ ### B.2 Antigravity CLI 1.0 (바이너리 분석, 신뢰도 중간)
91
+
92
+ 근거: `ANTIGRAVITY_ANALYSIS.md §4`, `ANTIGRAVITY_HOOKS.md`
93
+
94
+ - **정체**: Go 1.27 google3 사내 빌드, Antigravity(DeepMind 에이전트 코딩 도구) CLI. 원본 소스 ❌.
95
+ - **hook 지원 확정**: 문자열 `exa.hooks_pb / hooks_go_proto`, `"loaded %d named hooks from %d hooks.json file(s)"`, `DefaultHooksPath/UserConfigPath/enableJsonHooks`.
96
+ - **이벤트(5)**: `PreInvocation`, `PreToolUse`(proto: PreTool), `PostToolUse`(PostTool), `PostInvocation`, `Stop`(+`enableAfkStopHook`).
97
+ - **설치 위치/구조**: `~/.gemini/antigravity-cli/hooks.json`, **named-hook** 구조:
98
+ ```json
99
+ { "a": { "PreInvocation": null, "PostInvocation": null, "Stop": null,
100
+ "PreToolUse": [{ "matcher": "", "hooks": [{ "type":"command", "command":"echo 1", "timeout":0 }] }],
101
+ "PostToolUse": null } }
102
+ ```
103
+ - **allow/deny(guard gate)**: proto `PreToolHookResult` = 툴 차단/수정. `HookInjectedStep_UserMessage/SystemMessage` = 프롬프트 주입(PreInvocation).
104
+ - **로컬 command + HTTP 웹훅**(`webhookUrl`/`webhookId`) 둘 다 지원. TUI 편집기 존재.
105
+ - **I/O 필드 상세**: 바이너리에서 미복구 → **2.0 과 동일하다고 가정** (named-hook + 이벤트셋이 2.0 과 일치하므로 합리적). 실측 검증 필요.
106
+
107
+ ### B.3 Antigravity 2.0 (공식 문서, 신뢰도 높음)
108
+
109
+ 근거: `ANTIGRAVITY2_HOOKS.md` (전체)
110
+
111
+ - **설치 위치**: `hooks.json` — 워크스페이스 `.agents/` 또는 사용자 `~/.gemini/config/`.
112
+ - **구조(named-hook)**: 최상위 키 = hook **이름**, 그 아래 이벤트 키. `enabled:false` 로 비활성 가능.
113
+ ```json
114
+ { "safety-gate": { "enabled": false,
115
+ "PreToolUse": [{ "matcher":"run_command", "hooks":[{ "type":"command","command":"./safety.sh","timeout":10 }] }] } }
116
+ ```
117
+ - **이벤트(5)**: `PreToolUse`, `PostToolUse`, `PreInvocation`, `PostInvocation`, `Stop`.
118
+ - **matcher**: PreToolUse/PostToolUse 는 **tool 이름 정규식**(`""`/`"*"`=all, `"browser_.*"` 등). 나머지는 무시.
119
+ - **handler 필드**: `type`("command" 기본), `command`(필수), `timeout`(**초**, 기본 30). → **`env` 필드 없음** (이벤트 stamp 불가).
120
+ - **I/O 계약 (camelCase)**: stdin JSON, stdout JSON.
121
+ - **공통 입력**: `conversationId`, `workspacePaths`(string 배열!), `transcriptPath`, `artifactDirectoryPath`. **→ `session_id`/`cwd`/`hook_event_name` 없음.**
122
+ - **PreToolUse 입력**: `{ toolCall:{ name, args }, stepIdx, ...공통 }`. (args 는 PascalCase: `CommandLine`,`Cwd`,`TargetFile`…)
123
+ - **PreToolUse 출력**: `{ decision:"allow"|"deny"|"ask"|"force_ask"(필수), reason?, permissionOverrides?:string[] }`. **decision 은 필수** → allow 도 `{"decision":"allow"}` 명시 필요.
124
+ - **PostToolUse**: 입력 `{ stepIdx, error? }`, 출력 `{}`.
125
+ - **PreInvocation**: 입력 `{ invocationNum, initialNumSteps }`, 출력 `{ injectSteps:[{toolCall|userMessage|ephemeralMessage}] }`.
126
+ - **PostInvocation**: 입력=PreInvocation, 출력 `{ injectSteps, terminationBehavior:"force_continue"|"terminate"|"" }`.
127
+ - **Stop**: 입력 `{ executionNum, terminationReason, error?, fullyIdle }`, 출력 `{ decision:"continue"(재진입)|기타, reason? }`.
128
+ - **지원 tool 이름**: `view_file, write_to_file, replace_file_content, multi_replace_file_content, list_dir, find_by_name, grep_search, search_web, read_url_content, run_command, manage_task, schedule, list_permissions, ask_permission, invoke_subagent, define_subagent, send_message, manage_subagents, ask_question, generate_image`.
129
+
130
+ ### B.4 텔레메트리/Guard (공통)
131
+
132
+ - 세 호스트 모두 hook 에서 **직접 OTLP span 을 만들어 pinta collector 로 forward** (Gemini 자체 OTLP 와 별개, vendor-neutral).
133
+ - guard: `BeforeTool`(Gemini)/`PreToolUse`(Antigravity) 에서 `PINTA_GUARD_ENDPOINT` POST → DENY 시 deny 출력. **fail-open**.
134
+ - 공유 env 파일 `~/.gemini/pinta-gemini.env` 를 세 호스트 모두 읽을 수 있음 (전부 `~/.gemini/` 하위).
135
+
136
+ ---
137
+
138
+ ## PART C — Hook 차이 매칭 (2개 프로토콜 패밀리)
139
+
140
+ ### C.1 정규화(canonical) 이벤트 매핑
141
+
142
+ | Canonical 역할 | Gemini CLI | Antigravity 1.0 | Antigravity 2.0 | guard? | trace |
143
+ |---|---|---|---|:--:|---|
144
+ | **tool gate** | `BeforeTool` | `PreToolUse` | `PreToolUse` | ✅ | reuse |
145
+ | tool result | `AfterTool` | `PostToolUse` | `PostToolUse` | – | reuse |
146
+ | 턴/호출 시작 | `BeforeAgent` | `PreInvocation` | `PreInvocation` | – | new |
147
+ | 턴/호출 종료 | `AfterAgent` | `PostInvocation` | `PostInvocation` | – | reuse |
148
+ | stop | (`AfterAgent`) | `Stop` | `Stop` | – | reuse |
149
+ | session 시작 | `SessionStart` | — | — | – | reuse |
150
+ | session 종료 | `SessionEnd` | — | — | – | reuse |
151
+
152
+ - Antigravity 엔 SessionStart/End 없음. `PreInvocation` 은 **유저 프롬프트가 아니라 모델 호출 직전**(턴당 여러 번). → trace 시작 신호로 `invocationNum===1` 사용, 아니면 conversationId 로 reuse.
153
+
154
+ ### C.2 I/O 계약 비교 (Gemini CLI ↔ Antigravity 2.0)
155
+
156
+ | 항목 | Gemini CLI | Antigravity 2.0 |
157
+ |---|---|---|
158
+ | config 파일 | `~/.gemini/settings.json`(`hooks`) / `.gemini/hooks.json` | `~/.gemini/config/hooks.json` 또는 `.agents/hooks.json` |
159
+ | config 구조 | `event → [{matcher, hooks}]` | `name → { event → [{matcher, hooks}], enabled? }` |
160
+ | payload 에 event 이름 | ✅ `hook_event_name` | ❌ 없음 → **command 인자로 stamp** |
161
+ | 필드 casing | snake_case | camelCase |
162
+ | 공통 입력 | `session_id, cwd, transcript_path, timestamp` | `conversationId, workspacePaths[], transcriptPath, artifactDirectoryPath` |
163
+ | tool 필드 | `tool_name, tool_input` | `toolCall.name, toolCall.args`(args PascalCase) |
164
+ | **deny 출력** | `{decision:"deny", reason, systemMessage}` | `{decision:"deny", reason, permissionOverrides?}` |
165
+ | decision 값 | `allow/deny/block/ask/approve` | `allow/deny/ask/force_ask` |
166
+ | allow 출력 | `{}`(또는 `{"decision":"allow"}`) | PreToolUse: **`{"decision":"allow"}` 필수**; 그 외 `{}` |
167
+ | timeout 단위 | ms (기본 60000) | **초** (기본 30) |
168
+ | exit 2 emergency | ✅ stderr→reason | 문서화 안 됨 → **의존 금지**, 항상 JSON+exit0 |
169
+
170
+ ### C.3 핵심 차이 요약
171
+
172
+ 1. **공통점(쉬움)**: 둘 다 stdin JSON → stdout JSON, deny 가 **top-level `{decision,reason}`** 로 거의 동일. guard DENY → `{decision:"deny",reason}` 한 형태가 양쪽에서 동작.
173
+ 2. **차이1 — event 식별**: Antigravity 는 payload 에 event 이름이 없고 handler 에 `env` 도 없음 → **command 에 `--agent`/`--event` 인자 baking** 이 유일하게 신뢰 가능. (fallback: payload shape 추론.)
174
+ 3. **차이2 — casing/필드**: snake vs camel, `cwd` vs `workspacePaths[0]`, `session_id` vs `conversationId`, `tool_input` vs `toolCall.args` → normalization 레이어로 흡수.
175
+ 4. **차이3 — config 구조/위치/단위**: settings.json(event-map, ms) vs named-hook hooks.json(name-map, 초). → 호스트별 install 로직 분리.
176
+ 5. **차이4 — allow 출력**: Antigravity PreToolUse 는 `decision` 필수 → allow 도 명시.
177
+ 6. **공통 안전장치**: 항상 stdout 에 JSON, 항상 exit 0 (Gemini 의 stderr-leak, Antigravity 의 미문서 exit semantics 양쪽 회피).
178
+
179
+ ---
180
+
181
+ ## PART D — 통합 어댑터 설계 결정 (DG) & 구현 TODO
182
+
183
+ ### D.1 설계 결정 (pinta-copilot DESIGNDOC 스타일)
184
+
185
+ - **DG1 — 단일 바이너리 + CLI 인자 stamp (확정 primary)**: `node dist/index.js --agent <gemini|antigravity1|antigravity2> --event <HostEvent>`. Antigravity payload 에 event 이름이 없고 handler `env` 도 없으므로, **이벤트·agent 식별의 유일한 1차 수단은 install 시 command 에 박는 `--event`/`--agent` 인자**다. 런타임은 `process.argv` 만 읽어 분기하며 payload 추론에 의존하지 않는다 (host 가 command 를 shell 문자열로 그대로 실행 → 인자 자동 전달).
186
+ - **선행 검증(필수, 구현 전 1회)**: Antigravity 가 등록된 multi-token command 를 인자 보존하여 실행하는지 probe — 예: `"command": "node dist/index.js --event PROBE; echo \"$@\" >> /tmp/pinta-probe"` 로 `--event` 가 전달되는지 확인. (`ANTIGRAVITY_HOOKS.md` 샘플은 `"echo 1"` 단일 토큰뿐이라 미확인.)
187
+ - **fallback(probe 실패 시에만)**: payload-shape 추론 — `toolCall`→PreToolUse, `invocationNum`→PreInvocation, `executionNum`/`terminationReason`→Stop, `stepIdx`+`error`&toolCall없음→PostToolUse. **기본 비활성**; probe 가 인자 미보존을 증명할 때만 켠다.
188
+ - **DG2 — normalization 레이어**: 호스트 payload → canonical `{ agent, hook, session_id, cwd, transcript_path, tool_name, tool_input, raw }`. 매핑: `conversationId→session_id`, `workspacePaths[0]→cwd`, `toolCall.name→tool_name`, `toolCall.args→tool_input`. 원본 필드는 손실 없이 Bronze flatten 유지.
189
+ - **DG3 — Bronze prefix / ingest.type per agent**: `gemini.*`+`ingest.type="gemini"`+`service.name="gemini-cli"`; `antigravity.*`+`ingest.type="antigravity"`+`service.name="antigravity-cli"`. 하나의 codebase, 런타임에 agent 판별.
190
+ - **DG4 — 호스트별 install**:
191
+ - gemini → `~/.gemini/settings.json` 의 `hooks` 머지 (event-map, **ms**). 단일 entrypoint 가 `hook_event_name` 으로 라우팅(인자 stamp 도 병행 가능).
192
+ - antigravity1 → `~/.gemini/antigravity-cli/hooks.json` named-hook (**초**), command 에 `--agent antigravity1 --event <E>`.
193
+ - antigravity2 → `~/.gemini/config/hooks.json`(user) 및/또는 `.agents/hooks.json`(workspace) named-hook (**초**), `--agent antigravity2 --event <E>`.
194
+ - 머지는 우리 hook 이름(`pinta-gemini`) 키만 덮어쓰기(idempotent), 타 설정 보존.
195
+ - **DG5 — deny/allow 출력 (host-aware `formatDecision`)**:
196
+ - guard DENY → gemini: `{decision:"deny",reason,systemMessage:userMessage}`; antigravity: `{decision:"deny",reason}`.
197
+ - allow → gemini: `{}`; antigravity **PreToolUse**: `{"decision":"allow"}`(필수); antigravity 그 외: `{}`.
198
+ - 항상 stdout 에 정확히 1개 JSON.
199
+ - **DG6 — 항상 exit 0(fail-open)**: 어댑터 오류가 tool/턴을 막지 않게. stderr 진단이 systemMessage 로 새지 않게 stdout JSON 보장.
200
+ - **DG7 — trace store(멀티호스트 안전)**: `~/.gemini/pinta-gemini-data/trace.json` 를 **session_id/conversationId 로 keyed map** 저장(동시 실행 충돌 방지, pinta-copilot 방식). gemini `BeforeAgent`=새 trace; antigravity `PreInvocation invocationNum===1`=새 trace; 그 외 reuse.
201
+ - **DG8 — guard gate**: gemini `BeforeTool`, antigravity `PreToolUse`. `mcp_context`(gemini) / `toolCall.args`(antigravity) 를 guard `rawTextFields` 에 포함. (선택) antigravity `permissionOverrides` 로 REVIEW 허용 전달 — 후순위.
202
+ - **DG9 — 공유 env 파일**: `~/.gemini/pinta-gemini.env` (endpoint/headers/`PINTA_GUARD_ENDPOINT`/`PINTA_RELAY_TOKEN`). namespaced `GEMINI_PLUGIN_OPTION_*` 우선, `OTEL_*` fallback (Gemini 자체 OTLP 충돌 방지).
203
+ - **DG10 — Stop/Notification 등 advisory**: 텔레메트리만. antigravity `Stop` 은 `{}`(정지 허용), `PostInvocation` 은 `{}`(주입 없음). 모델 강제 재진입 같은 흐름제어는 범위 외.
204
+
205
+ ### D.2 구현 TODO
206
+
207
+ 스캐폴딩
208
+ - [ ] pinta-cc fork → core(`otlp/transport/retry-queue/redact/guard/trace`) 재사용.
209
+ - [ ] `package.json`: `@pinta-ai/pinta-gemini`, scripts `install-hooks`/`uninstall-hooks`/`doctor`/`mock-server`.
210
+
211
+ 런타임 분기
212
+ - [ ] `src/agent.ts`: `--agent`/`--event` 파싱 + payload-shape fallback. agent∈{gemini,antigravity1,antigravity2}.
213
+ - [ ] `src/core/normalize.ts`: 호스트 payload → canonical event (snake/camel, workspacePaths, toolCall 흡수).
214
+ - [ ] `src/core/otlp.ts`: agent 별 prefix/ingest.type/service.name.
215
+ - [ ] `src/handlers/`: tool-gate(guard+deny), telemetry(나머지). host-aware `formatDecision`.
216
+ - [ ] trace store: keyed map + agent 별 new-trace 규칙.
217
+
218
+ 설치/설정
219
+ - [ ] `tools/install-hooks.ts`: `--agent` 별 분기 — gemini(settings.json/ms) · antigravity1(antigravity-cli/hooks.json/초) · antigravity2(config|.agents/hooks.json/초). 모두 command 에 abs path + `--agent`/`--event` baking.
220
+ - [ ] `tools/doctor.ts`: 호스트별 hook 등록/endpoint/guard 헬스체크.
221
+ - [ ] `tools/mock-server.ts`: 로컬 OTLP collector.
222
+
223
+ 검증
224
+ - [ ] golden fixture: 3호스트 실제 payload(Gemini BeforeTool snake / Antigravity2 PreToolUse camel / Antigravity1 실측) 회귀 테스트.
225
+ - [ ] e2e: 각 agent 모드로 spawn → stdin 이벤트 → span POST + guard deny stdout(형식별) → 항상 exit 0.
226
+
227
+ 백엔드 (선례와 동일 패턴, 본 repo 범위 밖)
228
+ - [ ] `aware-backend`: `ingest.type∈{gemini,antigravity}` slice, `GEMINISPAN#`/`ANTIGRAVITYSPAN#`.
229
+ - [ ] `pinta-catalog`: `pinta-gemini/<ver>.yaml`. `pinta-manager`: enroll(3호스트). relay/guard 무변경.
230
+
231
+ ---
232
+
233
+ ## PART F — 실측 검증 결과 (VERIFIED 2026-06-15) ⭐
234
+
235
+ > **이 섹션이 실제 개발의 1차 진실 소스다.** Part B~E의 일부 가정은 실제 호스트(gemini-cli,
236
+ > agy v1.0.7, Antigravity 2.0)에서 hook을 발사해 `~/.gemini/pinta-gemini-data/invocations.jsonl`로
237
+ > 캡처·검증한 결과로 **교정/확정**되었다. 검증 도구: `tools/hook-verify.ts`(watcher) + `tools/install-hooks.ts`.
238
+ > 결과: **gemini 8/8, antigravity 5/5 이벤트 커버리지 달성.**
239
+
240
+ ### F.1 확정된 설치 모델 (Part C/DG4 교정)
241
+
242
+ | 호스트 | 읽는 위치 (실측) | 설치 방식 | 비고 |
243
+ |---|---|---|---|
244
+ | **gemini-cli** | `~/.gemini/extensions/pinta-gemini/` | **extension** (`gemini-extension.json` + `hooks/hooks.json`) | settings.json hooks는 **folder-trust 게이트**에 막힘(`hookRegistry`: `getHooks()`는 `isTrustedFolder()`일 때만). extension hooks는 `ConfigSource.Extensions`라 **trust 무조건 우회**. 디렉터리 drop-in만으로 자동 활성. **gemini 재시작 필요.** |
245
+ | **antigravity-cli (agy v1.0.x)** | `~/.gemini/config/hooks.json` (전역) | named-hook | 바이너리 분석 문서의 `~/.gemini/antigravity-cli/hooks.json`은 **안 읽힘**(오인식). |
246
+ | **Antigravity 2.0** | `~/.gemini/config/hooks.json` (전역, **agy와 동일 파일**) | named-hook (동일) | 또는 `<workspace>/.agents/hooks.json`(프로젝트 한정). 둘 다 같은 camelCase 프로토콜. |
247
+
248
+ → **gemini는 extension, antigravity(둘 다)는 전역 config/hooks.json** = 2개 설치 메커니즘이 3개 앱을 커버.
249
+ extension은 **gemini 전용**(antigravity는 안 읽음 — antigravity 문서에 `extensions/` 언급 전무, 별도 Go hook 로더).
250
+
251
+ ### F.2 hooks.json 구조 규칙 (치명적 — 잘못하면 발사 안 됨)
252
+
253
+ Antigravity named-hook에서 **이벤트 종류별로 구조가 다르다**:
254
+ - **Tool 이벤트**(PreToolUse/PostToolUse): `[{ "matcher": "", "hooks": [ {handler} ] }]` (matcher+hooks 래퍼)
255
+ - **Lifecycle 이벤트**(PreInvocation/PostInvocation/Stop): `[ {handler} ]` — **핸들러를 배열에 직접**, matcher/hooks 래퍼 **없음** (문서: *"a list of handlers directly under the event key, matcher ignored"*).
256
+ - ⚠ lifecycle을 tool처럼 `[{hooks:[...]}]`로 감싸면 **agy가 파싱 못 해 발사 안 됨** (실제로 이 버그로 PreInvocation/PostInvocation/Stop이 0개였다가, flat 교정 후 발사됨).
257
+
258
+ Gemini extension hooks.json은 **모든 이벤트가 `{ "hooks": { "<Event>": [ {matcher?, hooks:[handler]} ] } }`** 단일 구조. `timeout`=ms(기본 60000). Antigravity `timeout`=초(기본 30).
259
+
260
+ ### F.3 확정된 이벤트별 payload 형상 (실측 키셋)
261
+
262
+ **Gemini (snake_case, `hook_event_name`·`session_id`·`cwd`·`transcript_path`·`timestamp` 공통):**
263
+
264
+ | 이벤트 | 추가 필드(실측) |
265
+ |---|---|
266
+ | SessionStart | `source` (startup/resume/clear) |
267
+ | BeforeAgent | `prompt` |
268
+ | BeforeTool | `tool_name`, `tool_input`(snake 인자 예: `{dir_path:"."}`) — 빌트인은 `tool_use_id`/`mcp_context` **없음** |
269
+ | AfterTool | `tool_name`, `tool_input`, `tool_response` |
270
+ | AfterAgent | `prompt`, `prompt_response`, `stop_hook_active` |
271
+ | PreCompress | `trigger` |
272
+ | Notification | `notification_type`, `message`, `details` |
273
+ | SessionEnd | `reason` |
274
+
275
+ **Antigravity (camelCase, `conversationId`·`workspacePaths[]`·`transcriptPath`·`artifactDirectoryPath` 공통, `hook_event_name` 없음):**
276
+
277
+ | 이벤트 | 추가 필드(실측) |
278
+ |---|---|
279
+ | PreInvocation | `invocationNum`, `initialNumSteps` |
280
+ | PreToolUse | `toolCall:{name, args(PascalCase 예: CommandLine/DirectoryPath)}`, `stepIdx` |
281
+ | PostToolUse | `stepIdx`, `error`, **`toolCall`(nullable — `null`일 수 있음)** ← 문서엔 없던 보강 |
282
+ | PostInvocation | `invocationNum`, `initialNumSteps` |
283
+ | Stop | `executionNum`, `terminationReason`, `error`, `fullyIdle` |
284
+
285
+ ### F.4 확정 사항 (가정 → 사실)
286
+
287
+ - **DG1 인자 보존: 확정** — `--event`/`--agent` 누락 0건(3개 호스트 전부). payload-shape fallback은 dead code, **미탑재 확정**.
288
+ - **deny/allow 형식: 확정** — gemini deny=`{decision,reason,systemMessage}`, antigravity deny=`{decision,reason}`(systemMessage 없음); antigravity PreToolUse allow=`{decision:"allow"}`(필수), gemini allow=`{}`. 실측 일치.
289
+ - **정규화: 확정** — `conversationId→session_id`, `workspacePaths[0]→cwd`, `toolCall.name/args→tool_name/tool_input` 정상 동작. (단 PostToolUse `toolCall`은 null 가능 → tool_name 의존 금지.)
290
+ - **agy vs Antigravity 2.0 런타임 구분 가능(신규)** — 같은 config를 읽지만 `transcriptPath`로 구별됨:
291
+ agy = `.../antigravity-cli/brain/<id>/.../transcript_full.jsonl`, 2.0 = `.../antigravity/brain/<id>/.../transcript.jsonl`.
292
+ → 제품별 텔레메트리 서브라벨이 필요하면 `transcriptPath` 패턴으로 파생(향후 DG11 후보). 단일 `antigravity` 라벨 + 파생 속성으로 충분.
293
+ - **계측 메커니즘: 확정** — 실제 호스트는 hook subprocess에 우리 env를 안 줌 → `~/.gemini/pinta-gemini.env`(어댑터 `loadEnvFile`이 읽음)로 `PINTA_GEMINI_DEBUG`/endpoints 주입. `invocations.jsonl`은 **첫 호출 때 lazy 생성**.
294
+
295
+ ### F.5 개발 진입 게이트: ✅ 통과 가능
296
+
297
+ 3개 호스트 모두 이벤트 풀커버리지 + payload 형상 + deny/allow + 인자보존이 실측 확인됨.
298
+ 남은 수동 항목은 **deny가 실제로 툴을 차단했는지**(H6) 육안 확인 1건뿐. → **본개발(@pinta-ai 백엔드 연동 등) 진입 가능.**
299
+
300
+ ---
301
+
302
+ ## PART E — 리스크 / 미검증 가정 (대부분 PART F에서 해소됨)
303
+
304
+ 1. ~~**Antigravity 1.0 I/O 계약**~~ → **해소**: agy v1.0.7은 2.0과 동일 camelCase 프로토콜·동일 전역 config 경로(F.1/F.3). 단일 `antigravity` 프로파일로 통합.
305
+ 2. **Antigravity exit-code semantics 미문서화**: 여전히 exit 2 의존 금지. 항상 `{decision}` JSON + exit 0 (유지).
306
+ 3. ~~**event 식별(인자 보존)**~~ → **해소(확정)**: 누락 0건(F.4). fallback 미탑재.
307
+ 4. **PreInvocation 빈도**: 실측상 conversation당 여러 번 발화(모델 호출마다) 확인 → trace 경계는 `conversationId` 키 재사용으로 처리(턴 경계 신호 없음). 세분화 허용.
308
+ 5. ~~**config 위치 다중성**~~ → **해소**: 전역 `~/.gemini/config/hooks.json` 기본, `.agents/`는 `--workspace` 옵션(F.1).
309
+ 6. **timeout 단위**: Gemini=ms / Antigravity=초 — install 코드에서 분리됨(유지).
310
+ 7. ~~**1.0/2.0 동시 설치**~~ → **해소**: 같은 전역 config 1파일·1라벨, `transcriptPath`로 런타임 구분(F.4).
311
+
312
+ ---
313
+
314
+ ## 부록 — 핵심 파일/문서 레퍼런스
315
+
316
+ | 목적 | 위치 |
317
+ |---|---|
318
+ | Gemini hook 타입/decision | `gemini-cli/packages/core/src/hooks/types.ts` (HookDecision:130, isBlockingDecision:212) |
319
+ | Gemini command hook 실행 | `gemini-cli/packages/core/src/hooks/hookRunner.ts` (stdin:413, parse:455, exit2→deny:553) |
320
+ | Gemini BeforeTool deny 집행 | `gemini-cli/packages/core/src/scheduler/hook-utils.ts:64` (POLICY_VIOLATION) |
321
+ | Gemini hook 작성 가이드 | `gemini-cli/HOOKS.md` |
322
+ | Antigravity 1.0 바이너리 분석 | `gemini-cli/ANTIGRAVITY_ANALYSIS.md §4`, `ANTIGRAVITY_HOOKS.md` |
323
+ | Antigravity 2.0 hook 스펙 | `gemini-cli/ANTIGRAVITY2_HOOKS.md` |
324
+ | 선례 어댑터 | `pinta-cc`(core), `pinta-copilot`(payload 흡수/install), `pinta-codex`(env-file/수동 install) |
325
+ | **어댑터 (prototype)** | `pinta-gemini/src/index.ts` → `dist/index.js` (multi-host, 라벨 무관 normalize) |
326
+ | **설치기** | `pinta-gemini/tools/install-hooks.ts` (gemini=extension / antigravity=전역 config; lifecycle=flat 구조) |
327
+ | **실측 검증 watcher** | `pinta-gemini/tools/hook-verify.ts` (watch/report/teardown/selftest — 호스트 업데이트마다 재실행) |
328
+ | **검증 데이터** | `~/.gemini/pinta-gemini-data/invocations.jsonl` (호출별 argv·raw payload·정규화·guard·decision 감사 로그) |
package/docs/SPEC.md ADDED
@@ -0,0 +1,264 @@
1
+ # pinta-gemini — Engineering Specification
2
+
3
+ > 상태: **Draft v0.1** (2026-06-15) · 검증: 3개 호스트 실측 완료(§12)
4
+ > 배경/근거: [`BACKGROUND_RESEARCH.md`](./BACKGROUND_RESEARCH.md) (특히 **PART F = 실측 진실 소스**)
5
+ > 선례: `pinta-cc`(core), `pinta-copilot`(payload 흡수/install), `pinta-codex`(env-file)
6
+ > 키워드 MUST/SHOULD/MAY 는 RFC 2119 의미로 사용한다.
7
+
8
+ ---
9
+
10
+ ## 1. 개요 & 범위
11
+
12
+ `pinta-gemini`는 **단일 어댑터 바이너리**로 세 호스트의 hook 이벤트를 받아
13
+ (1) OTLP/HTTP span 으로 변환·forward(관측), (2) tool 호출을 원격 guard 로 allow/deny(집행)한다.
14
+
15
+ **지원 호스트:**
16
+
17
+ | 호스트 | 식별자(`--agent`) | hook 메커니즘 |
18
+ |---|---|---|
19
+ | Google Gemini CLI | `gemini` | extension (`~/.gemini/extensions/pinta-gemini/`) |
20
+ | Antigravity CLI (agy v1.0.x) | `antigravity` | 전역 `~/.gemini/config/hooks.json` |
21
+ | Antigravity 2.0 | `antigravity` | 전역 `~/.gemini/config/hooks.json` (동일) 또는 workspace `.agents/hooks.json` |
22
+
23
+ **범위 밖(별도 repo):** aware-backend ingest slice, pinta-catalog 등록, pinta-manager enroll. (§13)
24
+
25
+ ---
26
+
27
+ ## 2. 목표 / 비목표
28
+
29
+ **목표**
30
+ - G1. 세 호스트의 라이프사이클 이벤트를 손실 없이 OTLP span 으로 forward.
31
+ - G2. tool 게이트 이벤트에서 guard 로 allow/deny + 사유(reason) 출력.
32
+ - G3. **fail-open**: 어댑터/네트워크 오류가 호스트 작업을 절대 막지 않음.
33
+ - G4. `pinta-cc` core(otlp/transport/retry-queue/redact/guard/trace) 재사용.
34
+ - G5. 호스트 업데이트 회귀를 잡는 **재현 가능한 검증 도구**(`tools/hook-verify.ts`) 동반.
35
+
36
+ **비목표**
37
+ - N1. 호스트별 제품 버전을 강하게 구분(텔레메트리 서브라벨은 best-effort, §9.4).
38
+ - N2. Antigravity 의 흐름제어(injectSteps/terminationBehavior, Stop "continue") 활용 — 관측/차단만.
39
+ - N3. Gemini `BeforeModel`/`AfterModel`/`BeforeToolSelection` 캡처(노이즈/저가치 — 기본 미등록).
40
+
41
+ ---
42
+
43
+ ## 3. 호스트 통합 모델 (NORMATIVE)
44
+
45
+ ### 3.1 두 메커니즘이 세 앱을 커버
46
+ - Gemini CLI 는 **extension** 으로 설치한다. settings.json hooks 는 **folder-trust 게이트**(`isTrustedFolder()`)에 막히지만, extension hooks(`ConfigSource.Extensions`)는 **무조건 실행**된다. → 어댑터는 extension 경로를 MUST 사용.
47
+ - Antigravity(agy + 2.0)는 **전역 `~/.gemini/config/hooks.json`** 하나를 같은 camelCase 프로토콜로 읽는다. 하나의 `antigravity` 프로파일이 둘 다 커버한다.
48
+ - extension 은 **Gemini 전용**이다(Antigravity 는 `~/.gemini/extensions/` 를 읽지 않음). 두 메커니즘을 모두 설치해야 3개 앱이 커버된다.
49
+
50
+ ### 3.2 이벤트 식별 (DG1, 확정)
51
+ - 호스트는 hook subprocess 에 이벤트 이름·우리 env 를 주지 않는다(Antigravity), 또는 다른 키 이름을 쓴다.
52
+ - 따라서 **이벤트·agent 식별의 유일한 수단은 install 시 command 에 박는 `--agent`/`--event` 인자**다.
53
+ - 어댑터는 `process.argv` 만 읽어 분기하며 payload 추론에 의존하지 **않는다**. (실측: 3개 호스트 모두 인자 보존, 누락 0건.)
54
+
55
+ ### 3.3 설치 산출물 (정확 규격)
56
+
57
+ **gemini** → `~/.gemini/extensions/pinta-gemini/`
58
+ - `gemini-extension.json`: `{ "name": "pinta-gemini", "version": "<x>" }` (name·version MUST).
59
+ - `hooks/hooks.json`: `{ "hooks": { "<Event>": [ { "matcher"?: "", "hooks": [ {handler} ] } ] } }`.
60
+ - 모든 등록 이벤트가 `{matcher?, hooks:[handler]}` 단일 구조. `timeout` 단위 = **ms**(기본 60000).
61
+ - 등록 이벤트(8): `SessionStart, BeforeAgent, BeforeTool, AfterTool, AfterAgent, PreCompress, Notification, SessionEnd`. (tool 이벤트 `BeforeTool/AfterTool` 에 `matcher:""`.)
62
+ - 디렉터리 drop-in 으로 자동 활성. **변경 후 gemini 재시작 필요.**
63
+
64
+ **antigravity** → `~/.gemini/config/hooks.json` (전역; `--workspace DIR` 시 `<DIR>/.agents/hooks.json`)
65
+ - named-hook 루트: `{ "pinta-gemini": { "<Event>": [...] } }` — 우리 키만 교체, 사용자 기존 키 보존, 최초 1회 `.pinta-bak` 백업.
66
+ - **구조가 이벤트 종류별로 다르다(치명적):**
67
+ - tool 이벤트(`PreToolUse`,`PostToolUse`): `[ { "matcher": "", "hooks": [ {handler} ] } ]`
68
+ - lifecycle 이벤트(`PreInvocation`,`PostInvocation`,`Stop`): `[ {handler} ]` ← 핸들러 **직접**, matcher/hooks 래퍼 없음. (래퍼로 감싸면 agy 가 파싱 못 해 **발사 안 됨**.)
69
+ - `handler` = `{ "type": "command", "command": "<node> <abs dist/index.js> --agent antigravity --event <E>", "timeout": 30 }`. `timeout` 단위 = **초**(기본 30).
70
+ - 등록 이벤트(5): `PreInvocation, PreToolUse, PostToolUse, PostInvocation, Stop`.
71
+
72
+ ---
73
+
74
+ ## 4. 아키텍처 & 데이터 흐름
75
+
76
+ ```
77
+ 호스트 hook 발사
78
+ → node dist/index.js --agent <a> --event <e> (stdin = 이벤트 JSON)
79
+ 1. loadEnvFile() ~/.gemini/pinta-gemini.env → process.env (unset 키만)
80
+ 2. parse argv --agent, --event
81
+ 3. read stdin raw 이벤트 payload
82
+ 4. normalize() 호스트 payload → canonical (snake/camel 흡수)
83
+ 5. guard (gate 이벤트만) PINTA_GUARD_ENDPOINT POST, 50ms, fail-open
84
+ 6. forward() OTLP span POST (실패 시 retry-queue)
85
+ 7. formatDecision() 호스트별 allow/deny JSON
86
+ 8. logInvocation() (DEBUG 시) invocations.jsonl 감사 로그
87
+ → stdout 에 JSON 1개, exit 0 (항상)
88
+ ```
89
+
90
+ core 레이어(`pinta-cc` 재사용): `otlp.ts`, `transport.ts`(5s timeout), `retry-queue.ts`(JSONL, cap 1000, 파일락), `redact.ts`(시크릿 마스킹+102KB truncation), `guard.ts`, `trace.ts`(ULID).
91
+
92
+ ---
93
+
94
+ ## 5. 어댑터 CLI 인터페이스
95
+
96
+ ```
97
+ node dist/index.js --agent <gemini|antigravity> --event <HostEvent>
98
+ ```
99
+ - `--agent gemini` → Gemini 프로토콜(snake, gate=`BeforeTool`, prefix=`gemini`).
100
+ - 그 외 모든 라벨 → Antigravity 프로토콜(camel, gate=`PreToolUse`, prefix=`antigravity`). (라벨 무관 처리.)
101
+ - stdin: 호스트 이벤트 JSON. stdout: **정확히 1개** JSON 객체. exit: **항상 0**.
102
+
103
+ ---
104
+
105
+ ## 6. 정규화 (Normalization)
106
+
107
+ 호스트 payload → canonical `{ hook, session_id, cwd, tool_name, tool_input }`:
108
+
109
+ | canonical | gemini (snake) | antigravity (camel) |
110
+ |---|---|---|
111
+ | `hook` | `--event`(= `hook_event_name`) | `--event` |
112
+ | `session_id` | `session_id` | `conversationId` |
113
+ | `cwd` | `cwd` | `workspacePaths[0]` |
114
+ | `tool_name` | `tool_name` | `toolCall?.name` |
115
+ | `tool_input` | `tool_input` | `toolCall?.args` (PascalCase) |
116
+
117
+ - 원본 모든 top-level 필드는 Bronze flatten 으로 보존(§9).
118
+ - **주의(실측):** Antigravity `PostToolUse.toolCall` 은 `null` 일 수 있다 → PostToolUse 에서 `tool_name` 부재 허용 MUST.
119
+
120
+ ---
121
+
122
+ ## 7. Hook I/O 계약 (allow/deny)
123
+
124
+ ### 7.1 입력
125
+ stdin 으로 호스트 이벤트 JSON(원본 그대로). 어댑터는 payload 의 이벤트 이름 필드에 의존하지 않는다(§3.2).
126
+
127
+ ### 7.2 게이트 이벤트
128
+ guard 평가는 gate 이벤트에서만: gemini=`BeforeTool`, antigravity=`PreToolUse`.
129
+
130
+ ### 7.3 출력 (호스트별, NORMATIVE)
131
+
132
+ | 상황 | gemini | antigravity |
133
+ |---|---|---|
134
+ | guard DENY | `{"decision":"deny","reason":<msg>,"systemMessage":<msg>}` | `{"decision":"deny","reason":<msg>}` (systemMessage 없음) |
135
+ | allow / 비게이트 | `{}` | gate(`PreToolUse`): `{"decision":"allow"}` (필수) · 그 외: `{}` |
136
+
137
+ - `reason` = guard `userMessage` ?? `reason` ?? `"guard_deny"`.
138
+ - stdout 은 **항상 단일 JSON**(빈 객체라도). 이유: Gemini 가 stdout 이 비면 stderr 를 systemMessage 로 노출(`stdout.trim() || stderr.trim()`)하므로, 진단 stderr 누출 방지.
139
+ - exit code 로 차단(exit 2)에 **의존 금지** — Antigravity exit semantics 미문서화. 항상 `{decision}` + exit 0.
140
+
141
+ ---
142
+
143
+ ## 8. Guard 통합
144
+
145
+ - 엔드포인트: `PINTA_GUARD_ENDPOINT` (없으면 guard skip = ALLOW). `PINTA_GUARD_DISABLED=1` 로 비활성.
146
+ - 요청: `POST { "input": { spanId, toolName, toolInput, rawTextFields:{toolInput} } }`, 헤더 `x-pinta-relay-token: $PINTA_RELAY_TOKEN`.
147
+ - 타임아웃 50ms, **fail-open**(timeout/비200/에러 → ALLOW).
148
+ - 응답: `{ decision:'ALLOW'|'DENY'|'REVIEW', reason, userMessage?, durationMs? }`.
149
+ - MCP/PascalCase 인자도 `rawTextFields.toolInput`(JSON 문자열)에 포함해 정책 매칭 가능하게 한다.
150
+
151
+ ---
152
+
153
+ ## 9. 텔레메트리 (OTLP forward)
154
+
155
+ ### 9.1 엔드포인트 해석 (우선순위)
156
+ `GEMINI_PLUGIN_OPTION_ENDPOINT` > `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` > `OTEL_EXPORTER_OTLP_ENDPOINT`(+`/v1/traces`). 헤더: `GEMINI_PLUGIN_OPTION_HEADERS` > `OTEL_EXPORTER_OTLP_HEADERS`(+`GEMINI_PLUGIN_OPTION_API_KEY`→`x-pinta-relay-token`). namespaced 변수 우선(Gemini 자체 OTLP 충돌 방지).
157
+
158
+ ### 9.2 span
159
+ - 이름: `<ingest>.<snake(hook)>` (예: `gemini.before_tool`, `antigravity.pre_tool_use`).
160
+ - resource: `service.name` = `gemini-cli`(gemini) | `antigravity`(antigravity), `host.*`/`process.*`.
161
+ - attributes: `ingest.type`(=`gemini`|`antigravity`, Bronze 판별자), `<prefix>.hook`, `<prefix>.agent`, `<prefix>.<원본필드>`(flatten), guard 시 `pinta.guard.decision`/`.matched_rule`.
162
+ - traceId: ULID→32hex. spanId: 8byte hex.
163
+
164
+ ### 9.3 Bronze flatten
165
+ 모든 top-level 이벤트 필드를 `<prefix>.<key>` 속성으로. 문자열은 redact+truncate(§11). prefix = `gemini` | `antigravity`.
166
+
167
+ ### 9.4 제품 서브라벨 (best-effort, 실측 기반)
168
+ agy 와 Antigravity 2.0 은 `transcriptPath` 로 구분 가능:
169
+ - agy: `.../antigravity-cli/brain/<id>/.../transcript_full.jsonl`
170
+ - 2.0: `.../antigravity/brain/<id>/.../transcript.jsonl`
171
+ SHOULD: `antigravity.product` 속성을 `transcriptPath` 패턴으로 파생(`agy`|`antigravity2`|`unknown`). 식별 불가 시 생략.
172
+
173
+ ---
174
+
175
+ ## 10. Trace 상관
176
+
177
+ - 저장: `<dataDir>/trace.json` 을 **session_id/conversationId 로 keyed map** (동시 실행 충돌 방지).
178
+ - 새 trace: gemini=`BeforeAgent`, antigravity=`PreInvocation` 첫 발생(또는 conversationId 미존재 시). 그 외 reuse.
179
+ - 실측: Antigravity `PreInvocation` 은 모델 호출마다 발화(턴당 다수) → conversationId 단위 reuse 로 처리.
180
+
181
+ ---
182
+
183
+ ## 11. 시크릿 마스킹 (pinta-cc redact.ts 재사용)
184
+ - Tier1: AWS/GitHub/JWT/DB URL/CLI password 등 고신뢰 패턴 → `[REDACTED:<type>]`.
185
+ - Tier3: 문자열당 102,400 byte truncation → `…[TRUNCATED:<n>]`.
186
+ - skip-list: `<prefix>.{hook,tool_name,session_id,transcript_path,cwd}` 등 식별자.
187
+ - bash 컨텍스트: `<prefix>.tool_input`/`tool_response` 에 한해 context-gated 패턴 적용.
188
+
189
+ ---
190
+
191
+ ## 12. 설정 & 환경
192
+
193
+ - env 파일: `~/.gemini/pinta-gemini.env` (`loadEnvFile`, unset 키만 병합). 실제 호스트가 hook subprocess 에 우리 env 를 안 주므로 **주입 벡터는 이 파일**이다.
194
+ - data dir: `GEMINI_PLUGIN_DATA` || `~/.gemini/pinta-gemini-data/` (cwd 독립, 안정 경로). 포함: `trace.json`, `failed-spans.jsonl`(retry), `invocations.jsonl`(DEBUG 감사).
195
+ - `PINTA_GEMINI_DEBUG=1`: 매 호출을 `invocations.jsonl` 에 기록(argv·raw payload·normalized·guard·decision). 검증/디버그용, 기본 off.
196
+
197
+ ---
198
+
199
+ ## 13. 실패 모드
200
+
201
+ | 실패 | 동작 |
202
+ |---|---|
203
+ | guard timeout/에러 | ALLOW (fail-open) |
204
+ | OTLP POST 실패 | retry-queue enqueue, 다음 호출에 flush, **차단 안 함** |
205
+ | 엔드포인트 미설정 | telemetry silent disable |
206
+ | stdin 파싱 실패 / 어댑터 예외 | `{}` 출력 + exit 0 (fail-open) |
207
+ | 항상 | stdout 단일 JSON + **exit 0** |
208
+
209
+ ---
210
+
211
+ ## 14. 검증 & 수용 기준 (Acceptance)
212
+
213
+ 검증 도구: `tools/hook-verify.ts` (watch/report/teardown/selftest), `tools/install-hooks.ts`. 감사 로그 = `invocations.jsonl`.
214
+
215
+ **수용 기준 (실측 2026-06-15 충족):**
216
+ - AC1. 이벤트 커버리지: gemini **8/8**, antigravity **5/5**. ✅
217
+ - AC2. `--event`/`--agent` 인자 보존 누락 **0건**(3개 호스트). ✅
218
+ - AC3. payload 형상이 PART F.3 키셋과 일치(gemini snake+`hook_event_name`, antigravity camel+`conversationId`/`toolCall`). ✅
219
+ - AC4. deny 출력이 §7.3 형식과 일치(gemini `systemMessage` 포함, antigravity 미포함; PreToolUse allow=`{decision:"allow"}`). ✅
220
+ - AC5. 정규화가 실제 payload 에서 `tool_name`/`cwd`/`session_id` 추출(PostToolUse `toolCall:null` 허용). ✅
221
+ - AC6. 모든 호출 exit 0, stdout 단일 JSON. ✅
222
+ - **AC7. (수동) deny 가 실제 CLI 에서 툴을 차단** — 육안 확인 필요. ⏳
223
+
224
+ → AC1~6 충족. **AC7 확인 후 본개발 진입.**
225
+
226
+ 회귀 검증: 호스트(gemini-cli/agy/antigravity2) 업데이트마다 `hook-verify` 재실행 → 커버리지/형상/계약 변화 감지.
227
+
228
+ ---
229
+
230
+ ## 15. 패키징 & 배포
231
+
232
+ - 패키지: `@pinta-ai/pinta-gemini`, 라이선스 PolyForm-Noncommercial-1.0.0, `type: module`.
233
+ - 빌드: `esbuild src/index.ts → dist/index.js` (bundle, esm, node18+).
234
+ - 스크립트: `build`, `install-hooks`, `uninstall-hooks`, `doctor`, `e2e`, `test`.
235
+ - 설치 채널: OSS(git clone + `install-hooks`) / Pinta Manager(enroll, §16).
236
+
237
+ ---
238
+
239
+ ## 16. 미해결 / 향후 작업
240
+
241
+ - O1. **AC7**: deny 실차단 육안 확인(호스트별 1회).
242
+ - O2. Antigravity exit-code semantics 미문서화 — 계속 의존 금지.
243
+ - O3. `antigravity.product` 서브라벨(`transcriptPath` 파생) 구현(§9.4) — 선택.
244
+ - O4. Gemini `BeforeModel`/`BeforeToolSelection` 등록 옵션(저노이즈, 필요 시).
245
+ - O5. SessionEnd best-effort(호스트가 완료 대기 안 함) — log 기록을 forward 전에 수행할지 검토.
246
+ - O6. 백엔드 연동(별도 repo): aware-backend `ingest.type∈{gemini,antigravity}` slice, `pinta-catalog` 등록, `pinta-manager` enroll(3호스트). relay/guard 무변경.
247
+
248
+ ---
249
+
250
+ ## 17. 핵심 결정 추적 (DG, BACKGROUND_RESEARCH 연계)
251
+
252
+ | ID | 결정 | 상태 |
253
+ |---|---|---|
254
+ | DG1 | `--agent`/`--event` 인자가 유일 식별자 | ✅ 확정(누락 0건) |
255
+ | DG2 | 정규화 레이어(snake/camel 흡수) | ✅ |
256
+ | DG3 | agent별 prefix/ingest.type/service.name | ✅ |
257
+ | DG4 | 호스트별 install(gemini=extension/antigravity=전역 config) | ✅ 교정·확정 |
258
+ | DG5 | 호스트별 deny/allow 출력 | ✅ |
259
+ | DG6 | 항상 stdout 단일 JSON + exit 0 | ✅ |
260
+ | DG7 | trace keyed map(session/conversationId) | ✅ |
261
+ | DG8 | guard gate = BeforeTool/PreToolUse | ✅ |
262
+ | DG9 | 공유 env 파일 `~/.gemini/pinta-gemini.env` | ✅ |
263
+ | DG10 | advisory 이벤트는 텔레메트리만 | ✅ |
264
+ | **DG11** | `transcriptPath` 로 제품 서브라벨 파생(agy/2.0) | 🆕 후보 |
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@pinta-ai/pinta-gemini",
3
+ "version": "0.1.0",
4
+ "description": "Unified OTLP forwarder + guard adapter for Gemini CLI and Antigravity (agy v1.0.x / 2.0) hooks",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "license": "PolyForm-Noncommercial-1.0.0",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE",
11
+ "README.md",
12
+ "docs"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/pinta-ai/pinta-gemini.git"
17
+ },
18
+ "scripts": {
19
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node18 --outfile=dist/index.js",
20
+ "test": "vitest run",
21
+ "install-hooks": "tsx tools/install-hooks.ts",
22
+ "uninstall-hooks": "tsx tools/install-hooks.ts --uninstall",
23
+ "doctor": "tsx tools/doctor.ts",
24
+ "verify": "tsx tools/hook-verify.ts",
25
+ "e2e": "tsx tools/e2e-hooks.ts",
26
+ "e2e:config": "tsx tools/e2e-from-config.ts",
27
+ "demo": "tsx tools/demo-trace.ts"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "esbuild": "^0.24.0",
32
+ "tsx": "^4.0.0",
33
+ "typescript": "^5.7.0",
34
+ "vitest": "^2.1.0"
35
+ }
36
+ }