@nomos-arc/arc 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.
- package/.claude/settings.local.json +10 -0
- package/.nomos-config.json +5 -0
- package/CLAUDE.md +108 -0
- package/LICENSE +190 -0
- package/README.md +569 -0
- package/dist/cli.js +21120 -0
- package/docs/auth/googel_plan.yaml +1093 -0
- package/docs/auth/google_task.md +235 -0
- package/docs/auth/hardened_blueprint.yaml +1658 -0
- package/docs/auth/red_team_report.yaml +336 -0
- package/docs/auth/session_state.yaml +162 -0
- package/docs/certificate/cer_enhance_plan.md +605 -0
- package/docs/certificate/certificate_report.md +338 -0
- package/docs/dev_overview.md +419 -0
- package/docs/feature_assessment.md +156 -0
- package/docs/how_it_works.md +78 -0
- package/docs/infrastructure/map.md +867 -0
- package/docs/init/master_plan.md +3581 -0
- package/docs/init/red_team_report.md +215 -0
- package/docs/init/report_phase_1a.md +304 -0
- package/docs/integrity-gate/enhance_drift.md +703 -0
- package/docs/integrity-gate/overview.md +108 -0
- package/docs/management/manger-task.md +99 -0
- package/docs/management/scafffold.md +76 -0
- package/docs/map/ATOMIC_BLUEPRINT.md +1349 -0
- package/docs/map/RED_TEAM_REPORT.md +159 -0
- package/docs/map/map_task.md +147 -0
- package/docs/map/semantic_graph_task.md +792 -0
- package/docs/map/semantic_master_plan.md +705 -0
- package/docs/phase7/TEAM_RED.md +249 -0
- package/docs/phase7/plan.md +1682 -0
- package/docs/phase7/task.md +275 -0
- package/docs/prompts/USAGE.md +312 -0
- package/docs/prompts/architect.md +165 -0
- package/docs/prompts/executer.md +190 -0
- package/docs/prompts/hardener.md +190 -0
- package/docs/prompts/red_team.md +146 -0
- package/docs/verification/goveranance-overview.md +396 -0
- package/docs/verification/governance-overview.md +245 -0
- package/docs/verification/verification-arc-ar.md +560 -0
- package/docs/verification/verification-architecture.md +560 -0
- package/docs/very_next.md +52 -0
- package/docs/whitepaper.md +89 -0
- package/overview.md +1469 -0
- package/package.json +63 -0
- package/src/adapters/__tests__/git.test.ts +296 -0
- package/src/adapters/__tests__/stdio.test.ts +70 -0
- package/src/adapters/git.ts +226 -0
- package/src/adapters/pty.ts +159 -0
- package/src/adapters/stdio.ts +113 -0
- package/src/cli.ts +83 -0
- package/src/commands/apply.ts +47 -0
- package/src/commands/auth.ts +301 -0
- package/src/commands/certificate.ts +89 -0
- package/src/commands/discard.ts +24 -0
- package/src/commands/drift.ts +116 -0
- package/src/commands/index.ts +78 -0
- package/src/commands/init.ts +121 -0
- package/src/commands/list.ts +75 -0
- package/src/commands/map.ts +55 -0
- package/src/commands/plan.ts +30 -0
- package/src/commands/review.ts +58 -0
- package/src/commands/run.ts +63 -0
- package/src/commands/search.ts +147 -0
- package/src/commands/show.ts +63 -0
- package/src/commands/status.ts +59 -0
- package/src/core/__tests__/budget.test.ts +213 -0
- package/src/core/__tests__/certificate.test.ts +385 -0
- package/src/core/__tests__/config.test.ts +191 -0
- package/src/core/__tests__/preflight.test.ts +24 -0
- package/src/core/__tests__/prompt.test.ts +358 -0
- package/src/core/__tests__/review.test.ts +161 -0
- package/src/core/__tests__/state.test.ts +362 -0
- package/src/core/auth/__tests__/manager.test.ts +166 -0
- package/src/core/auth/__tests__/server.test.ts +220 -0
- package/src/core/auth/gcp-projects.ts +160 -0
- package/src/core/auth/manager.ts +114 -0
- package/src/core/auth/server.ts +141 -0
- package/src/core/budget.ts +119 -0
- package/src/core/certificate.ts +502 -0
- package/src/core/config.ts +212 -0
- package/src/core/errors.ts +54 -0
- package/src/core/factory.ts +49 -0
- package/src/core/graph/__tests__/builder.test.ts +272 -0
- package/src/core/graph/__tests__/contract-writer.test.ts +175 -0
- package/src/core/graph/__tests__/enricher.test.ts +299 -0
- package/src/core/graph/__tests__/parser.test.ts +200 -0
- package/src/core/graph/__tests__/pipeline.test.ts +202 -0
- package/src/core/graph/__tests__/renderer.test.ts +128 -0
- package/src/core/graph/__tests__/resolver.test.ts +185 -0
- package/src/core/graph/__tests__/scanner.test.ts +231 -0
- package/src/core/graph/__tests__/show.test.ts +134 -0
- package/src/core/graph/builder.ts +303 -0
- package/src/core/graph/constraints.ts +94 -0
- package/src/core/graph/contract-writer.ts +93 -0
- package/src/core/graph/drift/__tests__/classifier.test.ts +215 -0
- package/src/core/graph/drift/__tests__/comparator.test.ts +335 -0
- package/src/core/graph/drift/__tests__/drift.test.ts +453 -0
- package/src/core/graph/drift/__tests__/reporter.test.ts +203 -0
- package/src/core/graph/drift/classifier.ts +165 -0
- package/src/core/graph/drift/comparator.ts +205 -0
- package/src/core/graph/drift/reporter.ts +77 -0
- package/src/core/graph/enricher.ts +251 -0
- package/src/core/graph/grammar-paths.ts +30 -0
- package/src/core/graph/html-template.ts +493 -0
- package/src/core/graph/map-schema.ts +137 -0
- package/src/core/graph/parser.ts +336 -0
- package/src/core/graph/pipeline.ts +209 -0
- package/src/core/graph/renderer.ts +92 -0
- package/src/core/graph/resolver.ts +195 -0
- package/src/core/graph/scanner.ts +145 -0
- package/src/core/logger.ts +46 -0
- package/src/core/orchestrator.ts +792 -0
- package/src/core/plan-file-manager.ts +66 -0
- package/src/core/preflight.ts +64 -0
- package/src/core/prompt.ts +173 -0
- package/src/core/review.ts +95 -0
- package/src/core/state.ts +294 -0
- package/src/core/worktree-coordinator.ts +77 -0
- package/src/search/__tests__/chunk-extractor.test.ts +339 -0
- package/src/search/__tests__/embedder-auth.test.ts +124 -0
- package/src/search/__tests__/embedder.test.ts +267 -0
- package/src/search/__tests__/graph-enricher.test.ts +178 -0
- package/src/search/__tests__/indexer.test.ts +518 -0
- package/src/search/__tests__/integration.test.ts +649 -0
- package/src/search/__tests__/query-engine.test.ts +334 -0
- package/src/search/__tests__/similarity.test.ts +78 -0
- package/src/search/__tests__/vector-store.test.ts +281 -0
- package/src/search/chunk-extractor.ts +167 -0
- package/src/search/embedder.ts +209 -0
- package/src/search/graph-enricher.ts +95 -0
- package/src/search/indexer.ts +483 -0
- package/src/search/lexical-searcher.ts +190 -0
- package/src/search/query-engine.ts +225 -0
- package/src/search/vector-store.ts +311 -0
- package/src/types/index.ts +572 -0
- package/src/utils/__tests__/ansi.test.ts +54 -0
- package/src/utils/__tests__/frontmatter.test.ts +79 -0
- package/src/utils/__tests__/sanitize.test.ts +229 -0
- package/src/utils/ansi.ts +19 -0
- package/src/utils/context.ts +44 -0
- package/src/utils/frontmatter.ts +27 -0
- package/src/utils/sanitize.ts +78 -0
- package/test/e2e/lifecycle.test.ts +330 -0
- package/test/fixtures/mock-planner-hang.ts +5 -0
- package/test/fixtures/mock-planner.ts +26 -0
- package/test/fixtures/mock-reviewer-bad.ts +8 -0
- package/test/fixtures/mock-reviewer-retry.ts +34 -0
- package/test/fixtures/mock-reviewer.ts +18 -0
- package/test/fixtures/sample-project/src/circular-a.ts +6 -0
- package/test/fixtures/sample-project/src/circular-b.ts +6 -0
- package/test/fixtures/sample-project/src/config.ts +15 -0
- package/test/fixtures/sample-project/src/main.ts +19 -0
- package/test/fixtures/sample-project/src/services/product-service.ts +20 -0
- package/test/fixtures/sample-project/src/services/user-service.ts +18 -0
- package/test/fixtures/sample-project/src/types.ts +14 -0
- package/test/fixtures/sample-project/src/utils/index.ts +14 -0
- package/test/fixtures/sample-project/src/utils/validate.ts +12 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +12 -0
|
@@ -0,0 +1,1658 @@
|
|
|
1
|
+
final_blueprint:
|
|
2
|
+
task_id: "auth-oauth-login"
|
|
3
|
+
task_title: "Frictionless OAuth 2.0 Google Login for arc CLI"
|
|
4
|
+
version: "2.0-HARDENED"
|
|
5
|
+
finalized_at: "2026-04-06T14:00:00Z"
|
|
6
|
+
original_plan_ref: "1.0"
|
|
7
|
+
audit_verdict: "REVISE"
|
|
8
|
+
|
|
9
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
# RESOLUTION LOG
|
|
11
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
resolution_log:
|
|
13
|
+
total_findings_received: 9
|
|
14
|
+
resolved: 5
|
|
15
|
+
deferred: 4 # MEDIUM/LOW marked as optional or integrated as constraints
|
|
16
|
+
resolutions:
|
|
17
|
+
- finding_id: "F-001"
|
|
18
|
+
severity: "critical"
|
|
19
|
+
target_step: "3.1"
|
|
20
|
+
action_taken: |
|
|
21
|
+
Removed `--client-secret <secret>` CLI flag entirely from step 3.1.
|
|
22
|
+
Secret resolution now follows: config file (`auth.client_secret` in
|
|
23
|
+
.nomos-config.json) → env var `NOMOS_GOOGLE_CLIENT_SECRET` → interactive
|
|
24
|
+
masked prompt via `readline`. `--client-id` flag is retained (not sensitive).
|
|
25
|
+
Updated the priority resolution logic and code block accordingly.
|
|
26
|
+
verification: |
|
|
27
|
+
Confirm `arc auth login --help` does NOT show `--client-secret`.
|
|
28
|
+
Confirm login works via config file, env var, and interactive prompt paths.
|
|
29
|
+
Confirm `grep -r 'client-secret' src/commands/auth.ts` returns zero matches.
|
|
30
|
+
|
|
31
|
+
- finding_id: "F-002"
|
|
32
|
+
severity: "high"
|
|
33
|
+
target_step: "2.2"
|
|
34
|
+
action_taken: |
|
|
35
|
+
Added cryptographic `state` parameter generation using
|
|
36
|
+
`crypto.randomBytes(32).toString('hex')` to `generateAuthUrl()` call.
|
|
37
|
+
Added `state` validation in the callback handler — rejects with HTTP 403
|
|
38
|
+
and specific error message if state does not match. Added `crypto` import.
|
|
39
|
+
verification: |
|
|
40
|
+
Confirm `generateAuthUrl` call includes `state` field.
|
|
41
|
+
Confirm callback handler compares `state` from query params to generated value.
|
|
42
|
+
Confirm mismatched state returns 403 response and does not exchange the code.
|
|
43
|
+
|
|
44
|
+
- finding_id: "F-003"
|
|
45
|
+
severity: "high"
|
|
46
|
+
target_step: "NEW step 3.5"
|
|
47
|
+
action_taken: |
|
|
48
|
+
Inserted new step 3.5 "Validate OAuth token compatibility with GoogleGenerativeAI SDK"
|
|
49
|
+
between phase 3 (CLI commands) and phase 4 (credential chain integration).
|
|
50
|
+
This step performs a concrete spike test: obtain an OAuth access_token via
|
|
51
|
+
`arc auth login`, pass it to `new GoogleGenerativeAI(token)`, and attempt a
|
|
52
|
+
minimal `embedContent` call. If it fails, step 3.5 provides two concrete
|
|
53
|
+
alternative implementation paths as sub-steps (3.5a: custom Authorization
|
|
54
|
+
header wrapper, 3.5b: GoogleAuth credential injection). Phase 4 steps now
|
|
55
|
+
depend on 3.5 passing.
|
|
56
|
+
verification: |
|
|
57
|
+
Run step 3.5 after `arc auth login` succeeds. If embedContent returns a
|
|
58
|
+
valid response, proceed to Phase 4 as planned. If it returns 401/403,
|
|
59
|
+
execute the alternative path steps before Phase 4.
|
|
60
|
+
|
|
61
|
+
- finding_id: "F-004"
|
|
62
|
+
severity: "high"
|
|
63
|
+
target_step: "3.1"
|
|
64
|
+
action_taken: |
|
|
65
|
+
Replaced all `{ ... }` placeholders in the `arc auth login`, `arc auth logout`,
|
|
66
|
+
and `arc auth status` action handlers with full pseudocode comments listing
|
|
67
|
+
every operation in sequence. The login handler now includes 8 explicit steps,
|
|
68
|
+
logout has 3 steps, and status has 5 steps — all matching the prose spec.
|
|
69
|
+
verification: |
|
|
70
|
+
Confirm no literal `...` appears inside any `.action()` callback in step 3.1.
|
|
71
|
+
Confirm each action handler has numbered pseudocode comments matching the
|
|
72
|
+
described flow.
|
|
73
|
+
|
|
74
|
+
- finding_id: "F-005"
|
|
75
|
+
severity: "medium"
|
|
76
|
+
target_step: "4.6 → split into 4.6a, 4.6b, 4.6c"
|
|
77
|
+
action_taken: |
|
|
78
|
+
Split step 4.6 into three separate steps:
|
|
79
|
+
- 4.6a: Wire AuthManager into src/commands/index.ts (SearchIndexer)
|
|
80
|
+
- 4.6b: Wire AuthManager into src/commands/search.ts (QueryEngine)
|
|
81
|
+
- 4.6c: Wire AuthManager into src/commands/map.ts (MapPipeline)
|
|
82
|
+
Each step has explicit code showing the exact constructor call, especially
|
|
83
|
+
4.6c which shows MapPipeline's different parameter order:
|
|
84
|
+
`new MapPipeline(config, projectRoot, logger, authManager)`.
|
|
85
|
+
verification: |
|
|
86
|
+
`npx tsc --noEmit` passes after each sub-step.
|
|
87
|
+
Each command file has the correct constructor call with correct parameter order.
|
|
88
|
+
|
|
89
|
+
- finding_id: "F-006"
|
|
90
|
+
severity: "medium"
|
|
91
|
+
target_step: "5.1"
|
|
92
|
+
action_taken: |
|
|
93
|
+
Changed step 5.1 action from "MODIFY" to "VERIFY". Removed conditional
|
|
94
|
+
language. Step now explicitly states: "Verify that the regex covers Google
|
|
95
|
+
OAuth env vars. Expected result: no changes needed. If the regex does NOT
|
|
96
|
+
match GOOGLE_CLIENT_SECRET, add it and report the gap."
|
|
97
|
+
verification: |
|
|
98
|
+
Step 5.1 action field reads "VERIFY". No conditional modification language
|
|
99
|
+
remains in the description.
|
|
100
|
+
|
|
101
|
+
- finding_id: "F-007"
|
|
102
|
+
severity: "medium"
|
|
103
|
+
target_step: "4.3, 4.4, 4.5"
|
|
104
|
+
action_taken: |
|
|
105
|
+
Added explicit rollback ordering to steps 4.3, 4.4, and 4.5.
|
|
106
|
+
Each rollback now states: "BEFORE rolling back this step, first rollback
|
|
107
|
+
steps 4.6c, 4.6b, 4.6a (in that order)." Added `rollback_requires_first`
|
|
108
|
+
field to each step referencing the downstream steps that must be reverted
|
|
109
|
+
before this step can be safely rolled back.
|
|
110
|
+
verification: |
|
|
111
|
+
Confirm each step 4.3-4.5 rollback section mentions rolling back 4.6a-4.6c first.
|
|
112
|
+
Confirm rollback cascade order is: 4.6c → 4.6b → 4.6a → 4.5 → 4.4 → 4.3.
|
|
113
|
+
|
|
114
|
+
- finding_id: "F-008"
|
|
115
|
+
severity: "medium"
|
|
116
|
+
target_step: "2.1"
|
|
117
|
+
action_taken: |
|
|
118
|
+
Accepted the synchronous design for loadCredentials() and isLoggedIn() as-is.
|
|
119
|
+
Injected negative constraint "DO NOT use readFileSync in any method that could
|
|
120
|
+
be called in a loop or hot path" into step 2.1 description.
|
|
121
|
+
verification: |
|
|
122
|
+
Confirm constraint appears in step 2.1 description.
|
|
123
|
+
Confirm loadCredentials() remains synchronous (acceptable for CLI startup path).
|
|
124
|
+
|
|
125
|
+
- finding_id: "F-009"
|
|
126
|
+
severity: "low"
|
|
127
|
+
target_step: "2.2"
|
|
128
|
+
action_taken: |
|
|
129
|
+
Added explicit requirement in step 2.2: success response must include
|
|
130
|
+
`Connection: close` header, and `server.close()` must be called immediately
|
|
131
|
+
after sending the response (not only on timeout). Marked as optional improvement.
|
|
132
|
+
verification: |
|
|
133
|
+
Confirm code template in step 2.2 shows `res.setHeader('Connection', 'close')`
|
|
134
|
+
and `server.close()` called in the callback handler after `res.end()`.
|
|
135
|
+
|
|
136
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
137
|
+
# THE HARDENED PLAN
|
|
138
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
139
|
+
context_analysis:
|
|
140
|
+
tech_stack:
|
|
141
|
+
language: "TypeScript (strict mode)"
|
|
142
|
+
framework: "Commander.js CLI, Zod validation"
|
|
143
|
+
runtime: "Node.js >= 20 (ESM)"
|
|
144
|
+
package_manager: "npm"
|
|
145
|
+
architecture_pattern: "Modular CLI — Commander.js registerXCommand(program) pattern, Zod config schemas with per-field defaults, NomosError typed error codes, Winston logger injection, no singletons"
|
|
146
|
+
touch_zone:
|
|
147
|
+
- "package.json"
|
|
148
|
+
- "src/types/index.ts"
|
|
149
|
+
- "src/core/errors.ts"
|
|
150
|
+
- "src/core/config.ts"
|
|
151
|
+
- "src/core/auth/manager.ts"
|
|
152
|
+
- "src/core/auth/server.ts"
|
|
153
|
+
- "src/commands/auth.ts"
|
|
154
|
+
- "src/cli.ts"
|
|
155
|
+
- "src/search/embedder.ts"
|
|
156
|
+
- "src/core/graph/enricher.ts"
|
|
157
|
+
- "src/search/indexer.ts"
|
|
158
|
+
- "src/search/query-engine.ts"
|
|
159
|
+
- "src/core/graph/pipeline.ts"
|
|
160
|
+
- "src/commands/index.ts"
|
|
161
|
+
- "src/commands/search.ts"
|
|
162
|
+
- "src/commands/map.ts"
|
|
163
|
+
fragile_zone:
|
|
164
|
+
- "src/core/state.ts — Task state management — DO NOT MODIFY"
|
|
165
|
+
- "src/core/orchestrator.ts — Orchestration loop — DO NOT MODIFY"
|
|
166
|
+
- "src/core/budget.ts — Budget tracking — DO NOT MODIFY"
|
|
167
|
+
- "src/search/vector-store.ts — Vector storage — DO NOT MODIFY"
|
|
168
|
+
- "src/search/chunk-extractor.ts — Chunk extraction — DO NOT MODIFY"
|
|
169
|
+
- "src/utils/sanitize.ts — Sanitization — VERIFY only (step 5.1)"
|
|
170
|
+
dependencies:
|
|
171
|
+
- "google-auth-library (new npm dependency)"
|
|
172
|
+
- "open (already in package.json ^11.0.0)"
|
|
173
|
+
- "@google/generative-ai (existing ^0.24.1)"
|
|
174
|
+
- "commander (existing ^14.0.3)"
|
|
175
|
+
- "winston (existing ^3.19.0)"
|
|
176
|
+
- "zod (existing ^4.3.6)"
|
|
177
|
+
- "Node.js built-in: http, net, fs, path, os, crypto"
|
|
178
|
+
hard_constraints:
|
|
179
|
+
- constraint: "DO NOT pass client_secret via CLI flags — it leaks to shell history and process lists"
|
|
180
|
+
source: "F-001"
|
|
181
|
+
applies_to: ["3.1"]
|
|
182
|
+
- constraint: "DO NOT exchange OAuth authorization codes without validating the state parameter"
|
|
183
|
+
source: "F-002"
|
|
184
|
+
applies_to: ["2.2"]
|
|
185
|
+
- constraint: "DO NOT modify src/core/state.ts — it manages task state and is unrelated to auth"
|
|
186
|
+
source: "audit"
|
|
187
|
+
applies_to: ["*"]
|
|
188
|
+
- constraint: "DO NOT modify src/core/orchestrator.ts — orchestration loop must remain unchanged"
|
|
189
|
+
source: "audit"
|
|
190
|
+
applies_to: ["*"]
|
|
191
|
+
- constraint: "DO NOT modify src/core/budget.ts — budget tracking is unrelated to auth"
|
|
192
|
+
source: "audit"
|
|
193
|
+
applies_to: ["*"]
|
|
194
|
+
- constraint: "DO NOT modify src/search/vector-store.ts or src/search/chunk-extractor.ts"
|
|
195
|
+
source: "audit"
|
|
196
|
+
applies_to: ["*"]
|
|
197
|
+
- constraint: "DO NOT add new dependencies beyond google-auth-library — open is already installed"
|
|
198
|
+
source: "audit"
|
|
199
|
+
applies_to: ["1.1"]
|
|
200
|
+
- constraint: "DO NOT store tokens in environment variables — use file-based storage only"
|
|
201
|
+
source: "audit"
|
|
202
|
+
applies_to: ["2.1"]
|
|
203
|
+
- constraint: "DO NOT log access_token, refresh_token, or client_secret values at any log level"
|
|
204
|
+
source: "audit"
|
|
205
|
+
applies_to: ["2.1", "2.2", "3.1"]
|
|
206
|
+
- constraint: "DO NOT use readFileSync in any method that could be called in a loop or hot path"
|
|
207
|
+
source: "F-008"
|
|
208
|
+
applies_to: ["2.1"]
|
|
209
|
+
|
|
210
|
+
steps:
|
|
211
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
+
# PHASE 1: CONTRACT FIRST — Types, Error Codes, Config Schema
|
|
213
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
214
|
+
|
|
215
|
+
- step_id: "1.1"
|
|
216
|
+
title: "Install google-auth-library dependency"
|
|
217
|
+
action: "CONFIGURE"
|
|
218
|
+
file_path: "package.json"
|
|
219
|
+
description: |
|
|
220
|
+
Run `npm install google-auth-library`.
|
|
221
|
+
This adds the Google OAuth2 client library needed for token exchange,
|
|
222
|
+
refresh, and auth URL generation. The `open` package is already installed.
|
|
223
|
+
Also update the esbuild build script in package.json to add
|
|
224
|
+
`--external:google-auth-library` to the existing externals list.
|
|
225
|
+
CONSTRAINT: DO NOT add new dependencies beyond google-auth-library (ref: audit)
|
|
226
|
+
inputs:
|
|
227
|
+
- "Current package.json with existing dependencies"
|
|
228
|
+
outputs:
|
|
229
|
+
- "package.json updated with google-auth-library dependency"
|
|
230
|
+
- "package-lock.json updated"
|
|
231
|
+
- "esbuild externals list includes google-auth-library"
|
|
232
|
+
validation: |
|
|
233
|
+
`npm ls google-auth-library` exits 0.
|
|
234
|
+
`grep 'google-auth-library' package.json` returns a match.
|
|
235
|
+
Build script contains `--external:google-auth-library`.
|
|
236
|
+
depends_on: []
|
|
237
|
+
can_parallel: true
|
|
238
|
+
risk_level: "low"
|
|
239
|
+
rollback: |
|
|
240
|
+
Run `npm uninstall google-auth-library`.
|
|
241
|
+
Revert the esbuild externals list change in package.json by removing
|
|
242
|
+
`--external:google-auth-library` from the build script.
|
|
243
|
+
resolved_findings: []
|
|
244
|
+
|
|
245
|
+
- step_id: "1.2"
|
|
246
|
+
title: "Add AuthCredentials interface and auth config to NomosConfig"
|
|
247
|
+
action: "MODIFY"
|
|
248
|
+
file_path: "src/types/index.ts"
|
|
249
|
+
description: |
|
|
250
|
+
Add the following AFTER the existing `ExecutionMode` type declaration block
|
|
251
|
+
and BEFORE the `NomosConfig` interface:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// ─── Auth Types ──────────────────────────────────────────────────────────────
|
|
255
|
+
export interface AuthCredentials {
|
|
256
|
+
access_token: string;
|
|
257
|
+
refresh_token: string;
|
|
258
|
+
expiry_date: number;
|
|
259
|
+
token_type: string;
|
|
260
|
+
scope: string;
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Then add an `auth` section to the `NomosConfig` interface, after the `search`
|
|
265
|
+
block:
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
auth: {
|
|
269
|
+
client_id?: string;
|
|
270
|
+
client_secret?: string;
|
|
271
|
+
credentials_path: string;
|
|
272
|
+
redirect_port: number;
|
|
273
|
+
};
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The `credentials_path` defaults to `~/.nomos/credentials.json`.
|
|
277
|
+
The `redirect_port` defaults to `3000` (fixed port for Google Cloud Console
|
|
278
|
+
redirect URI matching, with fallback logic handled in the server module).
|
|
279
|
+
inputs:
|
|
280
|
+
- "Existing NomosConfig interface (lines 15-75 of src/types/index.ts)"
|
|
281
|
+
outputs:
|
|
282
|
+
- "AuthCredentials interface available for import"
|
|
283
|
+
- "NomosConfig.auth section defined"
|
|
284
|
+
validation: |
|
|
285
|
+
`npx tsc --noEmit` passes. AuthCredentials and NomosConfig['auth'] are
|
|
286
|
+
importable from '../types/index.js'.
|
|
287
|
+
depends_on: []
|
|
288
|
+
can_parallel: true
|
|
289
|
+
risk_level: "low"
|
|
290
|
+
rollback: |
|
|
291
|
+
Remove the AuthCredentials interface and the auth section from
|
|
292
|
+
the NomosConfig interface in src/types/index.ts.
|
|
293
|
+
resolved_findings: []
|
|
294
|
+
|
|
295
|
+
- step_id: "1.3"
|
|
296
|
+
title: "Register auth error codes in NomosErrorCode union"
|
|
297
|
+
action: "MODIFY"
|
|
298
|
+
file_path: "src/core/errors.ts"
|
|
299
|
+
description: |
|
|
300
|
+
Add four new error codes to the `NomosErrorCode` union type, after the
|
|
301
|
+
existing `search_api_key_missing` entry:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
| 'auth_not_logged_in'
|
|
305
|
+
| 'auth_token_expired'
|
|
306
|
+
| 'auth_login_failed'
|
|
307
|
+
| 'auth_client_config_missing';
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
These codes are used by AuthManager and the auth commands to throw
|
|
311
|
+
typed NomosError instances.
|
|
312
|
+
inputs:
|
|
313
|
+
- "Existing NomosErrorCode union (lines 1-38 of src/core/errors.ts)"
|
|
314
|
+
outputs:
|
|
315
|
+
- "Four new error codes available in the NomosErrorCode type"
|
|
316
|
+
validation: |
|
|
317
|
+
`npx tsc --noEmit` passes. Code `new NomosError('auth_not_logged_in', '...')`
|
|
318
|
+
compiles without error.
|
|
319
|
+
depends_on: []
|
|
320
|
+
can_parallel: true
|
|
321
|
+
risk_level: "low"
|
|
322
|
+
rollback: |
|
|
323
|
+
Remove the four added error code lines ('auth_not_logged_in',
|
|
324
|
+
'auth_token_expired', 'auth_login_failed', 'auth_client_config_missing')
|
|
325
|
+
from the NomosErrorCode union in src/core/errors.ts.
|
|
326
|
+
resolved_findings: []
|
|
327
|
+
|
|
328
|
+
- step_id: "1.4"
|
|
329
|
+
title: "Add AuthSchema to Zod config with defaults"
|
|
330
|
+
action: "MODIFY"
|
|
331
|
+
file_path: "src/core/config.ts"
|
|
332
|
+
description: |
|
|
333
|
+
Add a new `AuthSchema` Zod object AFTER the existing `SearchConfigSchema`
|
|
334
|
+
(around line 105) and BEFORE `NomosConfigSchema`:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
const AuthSchema = z.object({
|
|
338
|
+
client_id: z.string().optional(),
|
|
339
|
+
client_secret: z.string().optional(),
|
|
340
|
+
credentials_path: z.string().default(
|
|
341
|
+
path.join(os.homedir(), '.nomos', 'credentials.json'),
|
|
342
|
+
),
|
|
343
|
+
redirect_port: z.number().int().positive().default(3000),
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Add `import * as os from 'node:os';` to the top imports.
|
|
348
|
+
|
|
349
|
+
Then add the `auth` key to `NomosConfigSchema`:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
auth: AuthSchema.default(() => AuthSchema.parse({})),
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
This follows the exact same pattern as every other config section
|
|
356
|
+
(e.g., `search: SearchConfigSchema.default(...)`).
|
|
357
|
+
inputs:
|
|
358
|
+
- "Existing NomosConfigSchema (lines 118-129 of src/core/config.ts)"
|
|
359
|
+
- "Step 1.2 must be complete (NomosConfig type has auth section)"
|
|
360
|
+
outputs:
|
|
361
|
+
- "Auth config section parsed and defaulted by Zod"
|
|
362
|
+
- ".nomos-config.json can optionally contain auth.client_id, auth.client_secret, auth.redirect_port"
|
|
363
|
+
validation: |
|
|
364
|
+
`npx tsc --noEmit` passes. `getDefaultConfig().auth.credentials_path` ends
|
|
365
|
+
with `.nomos/credentials.json`.
|
|
366
|
+
depends_on: ["1.2"]
|
|
367
|
+
can_parallel: false
|
|
368
|
+
risk_level: "low"
|
|
369
|
+
rollback: |
|
|
370
|
+
Remove the AuthSchema definition and the `auth` key from NomosConfigSchema
|
|
371
|
+
in src/core/config.ts. Remove the `import * as os from 'node:os'` line.
|
|
372
|
+
resolved_findings: []
|
|
373
|
+
|
|
374
|
+
- step_id: "1.5"
|
|
375
|
+
title: "Create src/core/auth/ directory"
|
|
376
|
+
action: "CREATE"
|
|
377
|
+
file_path: "src/core/auth/"
|
|
378
|
+
description: |
|
|
379
|
+
Create the directory `src/core/auth/`. This is where manager.ts and
|
|
380
|
+
server.ts will live. No index barrel file needed — direct imports are
|
|
381
|
+
used throughout the project (e.g., `import { Embedder } from './embedder.js'`).
|
|
382
|
+
inputs: []
|
|
383
|
+
outputs:
|
|
384
|
+
- "Directory src/core/auth/ exists"
|
|
385
|
+
validation: |
|
|
386
|
+
`ls src/core/auth/` succeeds.
|
|
387
|
+
depends_on: []
|
|
388
|
+
can_parallel: true
|
|
389
|
+
risk_level: "low"
|
|
390
|
+
rollback: |
|
|
391
|
+
Run `rm -rf src/core/auth/` (only if no files were created inside it yet).
|
|
392
|
+
resolved_findings: []
|
|
393
|
+
|
|
394
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
395
|
+
# PHASE 2: ISOLATED LOGIC — AuthManager, Loopback Server
|
|
396
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
397
|
+
|
|
398
|
+
- step_id: "2.1"
|
|
399
|
+
title: "Implement AuthManager class"
|
|
400
|
+
action: "CREATE"
|
|
401
|
+
file_path: "src/core/auth/manager.ts"
|
|
402
|
+
description: |
|
|
403
|
+
Create `AuthManager` class with the following contract:
|
|
404
|
+
|
|
405
|
+
**Constructor:** `constructor(authConfig: NomosConfig['auth'], logger: Logger)`
|
|
406
|
+
- Stores config and logger. Does NOT read files or perform I/O in constructor.
|
|
407
|
+
- Logger type: `import type { Logger } from 'winston'`.
|
|
408
|
+
|
|
409
|
+
**Methods:**
|
|
410
|
+
|
|
411
|
+
1. `async saveCredentials(tokens: AuthCredentials): Promise<void>`
|
|
412
|
+
- Ensure `~/.nomos/` directory exists (`fs.mkdir(dir, { recursive: true })`).
|
|
413
|
+
- Write JSON to `authConfig.credentials_path`.
|
|
414
|
+
- Set file permissions: `fs.chmod(path, 0o600)`.
|
|
415
|
+
- Log success via `logger.info()`. Never log token values.
|
|
416
|
+
|
|
417
|
+
2. `loadCredentials(): AuthCredentials | null`
|
|
418
|
+
- Synchronous read using `fs.readFileSync`. Return `null` if file doesn't
|
|
419
|
+
exist or is invalid JSON.
|
|
420
|
+
- Parse and validate shape matches `AuthCredentials` interface.
|
|
421
|
+
CONSTRAINT: DO NOT use readFileSync in any method that could be called
|
|
422
|
+
in a loop or hot path. loadCredentials() is acceptable because it is only
|
|
423
|
+
called during CLI startup (non-hot-path). (ref: F-008)
|
|
424
|
+
|
|
425
|
+
3. `async getAuthenticatedClient(): Promise<OAuth2Client>`
|
|
426
|
+
- Load credentials via `loadCredentials()`.
|
|
427
|
+
- If null, throw `NomosError('auth_not_logged_in', ...)`.
|
|
428
|
+
- Create `OAuth2Client` from `google-auth-library`, set credentials.
|
|
429
|
+
- Check `expiry_date < Date.now()`:
|
|
430
|
+
- If expired, call `oauth2Client.refreshAccessToken()`.
|
|
431
|
+
- Persist refreshed tokens via `saveCredentials()`.
|
|
432
|
+
- Return the configured `OAuth2Client`.
|
|
433
|
+
|
|
434
|
+
4. `isLoggedIn(): boolean`
|
|
435
|
+
- Synchronous. Check if credentials file exists AND contains `refresh_token`.
|
|
436
|
+
|
|
437
|
+
5. `async clearCredentials(): Promise<void>`
|
|
438
|
+
- Delete credentials file if it exists (`fs.unlink`).
|
|
439
|
+
- Log confirmation.
|
|
440
|
+
|
|
441
|
+
6. `async getAccessToken(): Promise<string>`
|
|
442
|
+
- Convenience method. Calls `getAuthenticatedClient()`, then
|
|
443
|
+
`client.getAccessToken()`. Returns the token string.
|
|
444
|
+
- This is the method `Embedder` and `SemanticEnricher` will use
|
|
445
|
+
to get a bearer token when `GEMINI_API_KEY` is absent.
|
|
446
|
+
|
|
447
|
+
**Imports:** `fs` (node:fs and node:fs/promises), `path`, `OAuth2Client`
|
|
448
|
+
from `google-auth-library`, `NomosError`, `AuthCredentials`, `NomosConfig`.
|
|
449
|
+
|
|
450
|
+
CONSTRAINT: DO NOT log access_token, refresh_token, or client_secret
|
|
451
|
+
values at any log level. (ref: audit)
|
|
452
|
+
CONSTRAINT: DO NOT store tokens in environment variables — use
|
|
453
|
+
file-based storage only. (ref: audit)
|
|
454
|
+
inputs:
|
|
455
|
+
- "AuthCredentials interface from step 1.2"
|
|
456
|
+
- "NomosConfig['auth'] type from step 1.2 + 1.4"
|
|
457
|
+
- "NomosError auth codes from step 1.3"
|
|
458
|
+
outputs:
|
|
459
|
+
- "AuthManager class with full token lifecycle management"
|
|
460
|
+
validation: |
|
|
461
|
+
`npx tsc --noEmit` passes. All methods have correct return types.
|
|
462
|
+
No `console.log` calls — only `logger.*`.
|
|
463
|
+
Grep for access_token/refresh_token/client_secret in logger calls — zero matches.
|
|
464
|
+
depends_on: ["1.2", "1.3", "1.4", "1.5"]
|
|
465
|
+
can_parallel: false
|
|
466
|
+
risk_level: "medium"
|
|
467
|
+
rollback: |
|
|
468
|
+
Delete src/core/auth/manager.ts.
|
|
469
|
+
resolved_findings: ["F-008"]
|
|
470
|
+
|
|
471
|
+
- step_id: "2.2"
|
|
472
|
+
title: "Implement OAuth loopback server with CSRF state parameter"
|
|
473
|
+
action: "CREATE"
|
|
474
|
+
file_path: "src/core/auth/server.ts"
|
|
475
|
+
description: |
|
|
476
|
+
Create a single exported function:
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
export async function startLoopbackServer(
|
|
480
|
+
oauth2Client: OAuth2Client,
|
|
481
|
+
scopes: string[],
|
|
482
|
+
port: number,
|
|
483
|
+
logger: Logger,
|
|
484
|
+
): Promise<AuthCredentials>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
**Implementation details:**
|
|
488
|
+
|
|
489
|
+
1. **Server creation:** Use Node.js built-in `http.createServer()`.
|
|
490
|
+
|
|
491
|
+
2. **Port binding:** Attempt to listen on `port` (default 3000 from config).
|
|
492
|
+
If EADDRINUSE, try `listen(0)` for a random available port.
|
|
493
|
+
Log the actual port used.
|
|
494
|
+
|
|
495
|
+
3. **CSRF State parameter generation:**
|
|
496
|
+
```typescript
|
|
497
|
+
import * as crypto from 'node:crypto';
|
|
498
|
+
const state = crypto.randomBytes(32).toString('hex');
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
4. **Auth URL generation:**
|
|
502
|
+
```typescript
|
|
503
|
+
const redirectUri = `http://localhost:${actualPort}`;
|
|
504
|
+
oauth2Client.redirectUri = redirectUri;
|
|
505
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
506
|
+
access_type: 'offline',
|
|
507
|
+
scope: scopes,
|
|
508
|
+
redirect_uri: redirectUri,
|
|
509
|
+
state, // CSRF protection per RFC 6749 Section 10.12
|
|
510
|
+
});
|
|
511
|
+
```
|
|
512
|
+
CONSTRAINT: DO NOT exchange OAuth authorization codes without
|
|
513
|
+
validating the state parameter. (ref: F-002)
|
|
514
|
+
|
|
515
|
+
5. **Browser launch:** `import open from 'open'`. Call `await open(authUrl)`.
|
|
516
|
+
Wrap in try/catch — if browser launch fails (headless), log the URL:
|
|
517
|
+
`logger.info('[nomos:auth:info] Open this URL in your browser: <url>')`.
|
|
518
|
+
|
|
519
|
+
6. **Callback handler:** On incoming GET request:
|
|
520
|
+
- Parse `req.url` for `code` and `state` query parameters.
|
|
521
|
+
- **Validate state:** Compare received `state` to the generated `state`.
|
|
522
|
+
If they do not match, respond with HTTP 403 and body
|
|
523
|
+
"Authentication failed: state mismatch (possible CSRF attack)."
|
|
524
|
+
Log warning. Continue listening for a valid callback.
|
|
525
|
+
- If no code, respond 400 and continue listening.
|
|
526
|
+
- Exchange code: `const { tokens } = await oauth2Client.getToken(code)`.
|
|
527
|
+
- Respond with success HTML including `Connection: close` header:
|
|
528
|
+
```typescript
|
|
529
|
+
res.writeHead(200, {
|
|
530
|
+
'Content-Type': 'text/html',
|
|
531
|
+
'Connection': 'close',
|
|
532
|
+
});
|
|
533
|
+
res.end('<html><body><h1>Login successful!</h1><p>You can close this tab.</p></body></html>');
|
|
534
|
+
```
|
|
535
|
+
- Call `server.close()` immediately after sending the response.
|
|
536
|
+
- Map tokens to `AuthCredentials` shape and resolve the Promise.
|
|
537
|
+
|
|
538
|
+
7. **Timeout:** 120-second timeout via `setTimeout`. On timeout:
|
|
539
|
+
- Destroy all tracked sockets.
|
|
540
|
+
- `server.close()`.
|
|
541
|
+
- Reject with `NomosError('auth_login_failed', 'Login timed out after 120 seconds.')`.
|
|
542
|
+
|
|
543
|
+
8. **Socket tracking:** Maintain `const sockets = new Set<net.Socket>()`.
|
|
544
|
+
Track on `server.on('connection', socket => sockets.add(socket))`.
|
|
545
|
+
On cleanup, iterate and `socket.destroy()`.
|
|
546
|
+
|
|
547
|
+
9. **Cleanup:** After receiving callback OR on timeout, destroy all sockets
|
|
548
|
+
and close server. The function must never leave a dangling server.
|
|
549
|
+
|
|
550
|
+
**Imports:** `http`, `net`, `crypto` (node:crypto), `url` (node:url for URL
|
|
551
|
+
parsing), `open`, `OAuth2Client` from google-auth-library.
|
|
552
|
+
|
|
553
|
+
CONSTRAINT: DO NOT log access_token, refresh_token, or client_secret
|
|
554
|
+
values at any log level. (ref: audit)
|
|
555
|
+
inputs:
|
|
556
|
+
- "OAuth2Client instance (created by caller with client_id/client_secret)"
|
|
557
|
+
- "AuthCredentials interface from step 1.2"
|
|
558
|
+
outputs:
|
|
559
|
+
- "AuthCredentials object with access_token, refresh_token, expiry_date, etc."
|
|
560
|
+
validation: |
|
|
561
|
+
`npx tsc --noEmit` passes. Function signature matches spec.
|
|
562
|
+
Server cleanup is guaranteed (finally block or equivalent).
|
|
563
|
+
`state` parameter is generated, passed to generateAuthUrl, and validated in callback.
|
|
564
|
+
HARDENED: Confirm `crypto.randomBytes(32)` is used for state generation.
|
|
565
|
+
HARDENED: Confirm callback rejects with 403 on state mismatch.
|
|
566
|
+
HARDENED: Confirm `Connection: close` header is set on success response.
|
|
567
|
+
HARDENED: Confirm `server.close()` is called immediately after response, not only on timeout.
|
|
568
|
+
depends_on: ["1.2", "1.3", "1.5"]
|
|
569
|
+
can_parallel: true
|
|
570
|
+
risk_level: "high"
|
|
571
|
+
rollback: |
|
|
572
|
+
Delete src/core/auth/server.ts.
|
|
573
|
+
resolved_findings: ["F-002", "F-009"]
|
|
574
|
+
|
|
575
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
576
|
+
# PHASE 3: CLI COMMANDS — arc auth login/logout/status
|
|
577
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
578
|
+
|
|
579
|
+
- step_id: "3.1"
|
|
580
|
+
title: "Create arc auth command group"
|
|
581
|
+
action: "CREATE"
|
|
582
|
+
file_path: "src/commands/auth.ts"
|
|
583
|
+
description: |
|
|
584
|
+
Create `registerAuthCommand(program: Command): void` following the exact
|
|
585
|
+
same pattern as every other command in `src/commands/`.
|
|
586
|
+
|
|
587
|
+
CONSTRAINT: DO NOT pass client_secret via CLI flags — it leaks to shell
|
|
588
|
+
history and process lists. (ref: F-001)
|
|
589
|
+
|
|
590
|
+
**Structure:**
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
import type { Command } from 'commander';
|
|
594
|
+
import * as readline from 'node:readline';
|
|
595
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
596
|
+
import { loadConfig } from '../core/config.js';
|
|
597
|
+
import { createLogger } from '../core/logger.js';
|
|
598
|
+
import { NomosError } from '../core/errors.js';
|
|
599
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
600
|
+
import { startLoopbackServer } from '../core/auth/server.js';
|
|
601
|
+
|
|
602
|
+
const SCOPES = ['https://www.googleapis.com/auth/generative-language'];
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Prompt the user for a secret value with masked input (shows * characters).
|
|
606
|
+
* Used as last-resort fallback when client_secret is not in config or env.
|
|
607
|
+
*/
|
|
608
|
+
async function promptSecret(prompt: string): Promise<string> {
|
|
609
|
+
const rl = readline.createInterface({
|
|
610
|
+
input: process.stdin,
|
|
611
|
+
output: process.stdout,
|
|
612
|
+
});
|
|
613
|
+
return new Promise((resolve) => {
|
|
614
|
+
// Mask input by writing * for each character
|
|
615
|
+
process.stdout.write(prompt);
|
|
616
|
+
let secret = '';
|
|
617
|
+
process.stdin.setRawMode?.(true);
|
|
618
|
+
process.stdin.resume();
|
|
619
|
+
process.stdin.on('data', (char: Buffer) => {
|
|
620
|
+
const c = char.toString('utf8');
|
|
621
|
+
if (c === '\n' || c === '\r') {
|
|
622
|
+
process.stdin.setRawMode?.(false);
|
|
623
|
+
rl.close();
|
|
624
|
+
process.stdout.write('\n');
|
|
625
|
+
resolve(secret);
|
|
626
|
+
} else if (c === '\u0003') { // Ctrl+C
|
|
627
|
+
process.stdin.setRawMode?.(false);
|
|
628
|
+
rl.close();
|
|
629
|
+
process.exit(1);
|
|
630
|
+
} else if (c === '\u007f') { // Backspace
|
|
631
|
+
if (secret.length > 0) {
|
|
632
|
+
secret = secret.slice(0, -1);
|
|
633
|
+
process.stdout.write('\b \b');
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
secret += c;
|
|
637
|
+
process.stdout.write('*');
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function registerAuthCommand(program: Command): void {
|
|
644
|
+
const auth = program
|
|
645
|
+
.command('auth')
|
|
646
|
+
.description('Manage Google OAuth authentication');
|
|
647
|
+
|
|
648
|
+
// ─── arc auth login ──────────────────────────────────────────────
|
|
649
|
+
auth
|
|
650
|
+
.command('login')
|
|
651
|
+
.description('Authenticate with Google via OAuth 2.0')
|
|
652
|
+
.option('--client-id <id>', 'Google OAuth client ID')
|
|
653
|
+
.action(async (opts) => {
|
|
654
|
+
// 1. Load project config
|
|
655
|
+
const config = loadConfig();
|
|
656
|
+
const logger = createLogger(config);
|
|
657
|
+
|
|
658
|
+
// 2. Resolve clientId: CLI flag > config file
|
|
659
|
+
const clientId = opts.clientId ?? config.auth.client_id;
|
|
660
|
+
if (!clientId) {
|
|
661
|
+
throw new NomosError(
|
|
662
|
+
'auth_client_config_missing',
|
|
663
|
+
'Google OAuth client_id not found. Provide via --client-id flag or set auth.client_id in .nomos-config.json',
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// 3. Resolve clientSecret: config file > env var > interactive prompt
|
|
668
|
+
// NOTE: --client-secret CLI flag intentionally NOT supported (F-001)
|
|
669
|
+
let clientSecret = config.auth.client_secret
|
|
670
|
+
?? process.env['NOMOS_GOOGLE_CLIENT_SECRET'];
|
|
671
|
+
if (!clientSecret) {
|
|
672
|
+
logger.info('[nomos:auth:info] Client secret not found in config or environment.');
|
|
673
|
+
clientSecret = await promptSecret('Enter Google OAuth client secret: ');
|
|
674
|
+
if (!clientSecret) {
|
|
675
|
+
throw new NomosError(
|
|
676
|
+
'auth_client_config_missing',
|
|
677
|
+
'Google OAuth client_secret not provided. Set auth.client_secret in .nomos-config.json or NOMOS_GOOGLE_CLIENT_SECRET env var.',
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 4. Create OAuth2Client
|
|
683
|
+
const oauth2Client = new OAuth2Client({ clientId, clientSecret });
|
|
684
|
+
|
|
685
|
+
// 5. Create AuthManager
|
|
686
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
687
|
+
|
|
688
|
+
// 6. Start loopback server, open browser, wait for callback
|
|
689
|
+
const tokens = await startLoopbackServer(
|
|
690
|
+
oauth2Client, SCOPES, config.auth.redirect_port, logger,
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// 7. Save credentials to disk
|
|
694
|
+
await authManager.saveCredentials(tokens);
|
|
695
|
+
|
|
696
|
+
// 8. Confirm success
|
|
697
|
+
console.log(`✓ Logged in successfully. Credentials saved to ${config.auth.credentials_path}`);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// ─── arc auth logout ─────────────────────────────────────────────
|
|
701
|
+
auth
|
|
702
|
+
.command('logout')
|
|
703
|
+
.description('Remove stored OAuth credentials')
|
|
704
|
+
.action(async () => {
|
|
705
|
+
// 1. Load config, create AuthManager
|
|
706
|
+
const config = loadConfig();
|
|
707
|
+
const logger = createLogger(config);
|
|
708
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
709
|
+
|
|
710
|
+
// 2. Delete credentials file
|
|
711
|
+
await authManager.clearCredentials();
|
|
712
|
+
|
|
713
|
+
// 3. Confirm
|
|
714
|
+
console.log('✓ Logged out. Credentials removed.');
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// ─── arc auth status ─────────────────────────────────────────────
|
|
718
|
+
auth
|
|
719
|
+
.command('status')
|
|
720
|
+
.description('Show current authentication state')
|
|
721
|
+
.action(async () => {
|
|
722
|
+
// 1. Load config, create AuthManager
|
|
723
|
+
const config = loadConfig();
|
|
724
|
+
const logger = createLogger(config);
|
|
725
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
726
|
+
|
|
727
|
+
// 2. Check login state
|
|
728
|
+
const loggedIn = authManager.isLoggedIn();
|
|
729
|
+
|
|
730
|
+
// 3. If logged in, show token info
|
|
731
|
+
if (loggedIn) {
|
|
732
|
+
const creds = authManager.loadCredentials()!;
|
|
733
|
+
const expiryDate = new Date(creds.expiry_date);
|
|
734
|
+
const isExpired = creds.expiry_date < Date.now();
|
|
735
|
+
console.log(`OAuth: Logged in (token ${isExpired ? 'EXPIRED' : 'valid until ' + expiryDate.toLocaleString()})`);
|
|
736
|
+
} else {
|
|
737
|
+
console.log('OAuth: Not logged in');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 4. Check API key
|
|
741
|
+
const hasApiKey = !!process.env['GEMINI_API_KEY'];
|
|
742
|
+
console.log(`API Key: ${hasApiKey ? 'Set (GEMINI_API_KEY)' : 'Not set'}`);
|
|
743
|
+
|
|
744
|
+
// 5. Show credential chain resolution
|
|
745
|
+
if (hasApiKey) {
|
|
746
|
+
console.log('Active credential: GEMINI_API_KEY (takes priority over OAuth)');
|
|
747
|
+
} else if (loggedIn) {
|
|
748
|
+
console.log('Active credential: OAuth access token');
|
|
749
|
+
} else {
|
|
750
|
+
console.log('Active credential: None — run `arc auth login` or set GEMINI_API_KEY');
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**Error handling:** Wrap each action in try/catch, matching the pattern
|
|
757
|
+
in `src/commands/index.ts` (lines 67-75): NomosError → `console.error` + exit 1.
|
|
758
|
+
inputs:
|
|
759
|
+
- "AuthManager from step 2.1"
|
|
760
|
+
- "startLoopbackServer from step 2.2"
|
|
761
|
+
- "Config with auth section from step 1.4"
|
|
762
|
+
outputs:
|
|
763
|
+
- "`arc auth login`, `arc auth logout`, `arc auth status` commands functional"
|
|
764
|
+
validation: |
|
|
765
|
+
`npx tsc --noEmit` passes.
|
|
766
|
+
`npx tsx src/cli.ts auth --help` lists login, logout, status.
|
|
767
|
+
`npx tsx src/cli.ts auth login --help` shows --client-id but NOT --client-secret.
|
|
768
|
+
HARDENED: Confirm no `--client-secret` option exists in the code.
|
|
769
|
+
HARDENED: Confirm client_secret resolution order is: config > env > prompt.
|
|
770
|
+
HARDENED: Confirm no `{ ... }` placeholder exists in any .action() callback.
|
|
771
|
+
depends_on: ["2.1", "2.2"]
|
|
772
|
+
can_parallel: false
|
|
773
|
+
risk_level: "medium"
|
|
774
|
+
rollback: |
|
|
775
|
+
Delete src/commands/auth.ts.
|
|
776
|
+
resolved_findings: ["F-001", "F-004"]
|
|
777
|
+
|
|
778
|
+
- step_id: "3.2"
|
|
779
|
+
title: "Register auth command in CLI entrypoint"
|
|
780
|
+
action: "MODIFY"
|
|
781
|
+
file_path: "src/cli.ts"
|
|
782
|
+
description: |
|
|
783
|
+
1. Add import at the top (after the existing command imports, around line 16):
|
|
784
|
+
```typescript
|
|
785
|
+
import { registerAuthCommand } from './commands/auth.js';
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
2. Add `registerAuthCommand` to the registration array (after
|
|
789
|
+
`registerSearchCommand`, around line 59):
|
|
790
|
+
```typescript
|
|
791
|
+
registerAuthCommand,
|
|
792
|
+
```
|
|
793
|
+
inputs:
|
|
794
|
+
- "registerAuthCommand function from step 3.1"
|
|
795
|
+
outputs:
|
|
796
|
+
- "`arc auth` appears in `arc --help` output"
|
|
797
|
+
validation: |
|
|
798
|
+
`npx tsc --noEmit` passes.
|
|
799
|
+
`npx tsx src/cli.ts --help` lists 'auth' command.
|
|
800
|
+
depends_on: ["3.1"]
|
|
801
|
+
can_parallel: false
|
|
802
|
+
risk_level: "low"
|
|
803
|
+
rollback: |
|
|
804
|
+
Remove the `import { registerAuthCommand }` line and the `registerAuthCommand`
|
|
805
|
+
entry from the registration array in src/cli.ts.
|
|
806
|
+
resolved_findings: []
|
|
807
|
+
|
|
808
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
809
|
+
# PHASE 3.5: VALIDATION — OAuth Token SDK Compatibility
|
|
810
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
811
|
+
|
|
812
|
+
- step_id: "3.5"
|
|
813
|
+
title: "Validate OAuth token compatibility with GoogleGenerativeAI SDK"
|
|
814
|
+
action: "TEST"
|
|
815
|
+
file_path: ""
|
|
816
|
+
description: |
|
|
817
|
+
This is a critical validation gate before Phase 4. The entire credential
|
|
818
|
+
chain (steps 4.1 through 4.6c) depends on whether the @google/generative-ai
|
|
819
|
+
SDK accepts an OAuth access_token passed as the `apiKey` constructor parameter.
|
|
820
|
+
|
|
821
|
+
**Procedure:**
|
|
822
|
+
1. Ensure `arc auth login` works (step 3.1 must be complete and tested).
|
|
823
|
+
2. Run a spike test script:
|
|
824
|
+
```typescript
|
|
825
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
826
|
+
import { AuthManager } from './src/core/auth/manager.js';
|
|
827
|
+
import { loadConfig } from './src/core/config.js';
|
|
828
|
+
import { createLogger } from './src/core/logger.js';
|
|
829
|
+
|
|
830
|
+
const config = loadConfig();
|
|
831
|
+
const logger = createLogger(config);
|
|
832
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
833
|
+
const token = await authManager.getAccessToken();
|
|
834
|
+
|
|
835
|
+
const genAI = new GoogleGenerativeAI(token);
|
|
836
|
+
const model = genAI.getGenerativeModel({ model: 'text-embedding-004' });
|
|
837
|
+
const result = await model.embedContent('test');
|
|
838
|
+
console.log('SUCCESS: OAuth token works as API key', result.embedding.values.length);
|
|
839
|
+
```
|
|
840
|
+
3. **If SUCCESS:** Proceed to Phase 4 as planned. Delete the spike script.
|
|
841
|
+
4. **If FAILURE (401/403 from Gemini API):** The OAuth token cannot be used
|
|
842
|
+
as an API key. In this case, do NOT proceed with Phase 4 as written.
|
|
843
|
+
Instead, implement the alternative approach:
|
|
844
|
+
- Modify `Embedder` and `SemanticEnricher` to accept a `credentials`
|
|
845
|
+
parameter (the `OAuth2Client` instance, not just the token string).
|
|
846
|
+
- Use `GoogleAuth` from `google-auth-library` to create authenticated
|
|
847
|
+
requests with proper `Authorization: Bearer <token>` headers.
|
|
848
|
+
- The factory methods (`Embedder.create`, `SemanticEnricher.create`)
|
|
849
|
+
must inject the authenticated client into the SDK's request pipeline.
|
|
850
|
+
- Document the specific error message received for future reference.
|
|
851
|
+
|
|
852
|
+
This step is a decision gate. Phase 4 must not begin until this validation
|
|
853
|
+
completes and the approach is confirmed.
|
|
854
|
+
inputs:
|
|
855
|
+
- "Working `arc auth login` from steps 3.1 + 3.2"
|
|
856
|
+
- "@google/generative-ai SDK (existing dependency)"
|
|
857
|
+
outputs:
|
|
858
|
+
- "Confirmed: OAuth access_token works/does not work as GoogleGenerativeAI API key"
|
|
859
|
+
- "Decision: proceed with Phase 4 as planned, or use alternative auth approach"
|
|
860
|
+
validation: |
|
|
861
|
+
Spike script either succeeds (token works) or fails with a specific error.
|
|
862
|
+
The outcome is documented before proceeding.
|
|
863
|
+
depends_on: ["3.2"]
|
|
864
|
+
can_parallel: false
|
|
865
|
+
risk_level: "high"
|
|
866
|
+
rollback: |
|
|
867
|
+
Delete the spike test script. No source code changes in this step.
|
|
868
|
+
resolved_findings: ["F-003"]
|
|
869
|
+
|
|
870
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
871
|
+
# PHASE 4: CREDENTIAL CHAIN INTEGRATION
|
|
872
|
+
# (Proceed only after step 3.5 confirms token compatibility)
|
|
873
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
874
|
+
|
|
875
|
+
- step_id: "4.1"
|
|
876
|
+
title: "Add async factory method to Embedder with credential chain"
|
|
877
|
+
action: "MODIFY"
|
|
878
|
+
file_path: "src/search/embedder.ts"
|
|
879
|
+
description: |
|
|
880
|
+
The Embedder constructor is currently synchronous and reads GEMINI_API_KEY
|
|
881
|
+
from process.env. The OAuth fallback requires async operations (file I/O,
|
|
882
|
+
token refresh). To avoid breaking the constructor contract, introduce
|
|
883
|
+
a static factory method while keeping the constructor functional for the
|
|
884
|
+
API key path.
|
|
885
|
+
|
|
886
|
+
**Changes:**
|
|
887
|
+
|
|
888
|
+
1. Modify constructor to accept an optional `apiKey` parameter:
|
|
889
|
+
```typescript
|
|
890
|
+
constructor(
|
|
891
|
+
private readonly config: NomosConfig['search'],
|
|
892
|
+
private readonly logger: Logger,
|
|
893
|
+
apiKey?: string,
|
|
894
|
+
) {
|
|
895
|
+
const key = apiKey ?? process.env['GEMINI_API_KEY'];
|
|
896
|
+
if (!key) {
|
|
897
|
+
throw new NomosError(
|
|
898
|
+
'search_api_key_missing',
|
|
899
|
+
'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
this.client = new GoogleGenerativeAI(key);
|
|
903
|
+
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
2. Add a static async factory method:
|
|
907
|
+
```typescript
|
|
908
|
+
static async create(
|
|
909
|
+
config: NomosConfig['search'],
|
|
910
|
+
logger: Logger,
|
|
911
|
+
authManager?: AuthManager | null,
|
|
912
|
+
): Promise<Embedder> {
|
|
913
|
+
// Priority 1: GEMINI_API_KEY
|
|
914
|
+
const envKey = process.env['GEMINI_API_KEY'];
|
|
915
|
+
if (envKey) {
|
|
916
|
+
return new Embedder(config, logger, envKey);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Priority 2: OAuth credentials
|
|
920
|
+
if (authManager?.isLoggedIn()) {
|
|
921
|
+
const token = await authManager.getAccessToken();
|
|
922
|
+
return new Embedder(config, logger, token);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Priority 3: Neither — throw
|
|
926
|
+
throw new NomosError(
|
|
927
|
+
'search_api_key_missing',
|
|
928
|
+
'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
3. Add import for AuthManager:
|
|
934
|
+
```typescript
|
|
935
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Backward compatibility:** The existing `new Embedder(config, logger)`
|
|
939
|
+
constructor still works for any caller that doesn't need OAuth. The factory
|
|
940
|
+
method is the new preferred path.
|
|
941
|
+
inputs:
|
|
942
|
+
- "Existing Embedder class (src/search/embedder.ts)"
|
|
943
|
+
- "AuthManager from step 2.1"
|
|
944
|
+
- "Confirmed token compatibility from step 3.5"
|
|
945
|
+
outputs:
|
|
946
|
+
- "Embedder.create() factory method available"
|
|
947
|
+
- "Existing constructor still works for API key path"
|
|
948
|
+
validation: |
|
|
949
|
+
`npx tsc --noEmit` passes.
|
|
950
|
+
`new Embedder(config, logger)` still compiles (backward compat).
|
|
951
|
+
`Embedder.create(config, logger, authManager)` compiles.
|
|
952
|
+
depends_on: ["2.1", "3.5"]
|
|
953
|
+
can_parallel: true
|
|
954
|
+
risk_level: "high"
|
|
955
|
+
rollback: |
|
|
956
|
+
Revert src/search/embedder.ts to original constructor (remove optional
|
|
957
|
+
apiKey parameter). Remove the `static async create()` method. Remove the
|
|
958
|
+
`import { AuthManager }` line.
|
|
959
|
+
NOTE: Before rolling back this step, first rollback steps 4.6a, 4.6b, 4.6c,
|
|
960
|
+
and 4.3 (in that order).
|
|
961
|
+
resolved_findings: []
|
|
962
|
+
|
|
963
|
+
- step_id: "4.2"
|
|
964
|
+
title: "Add credential chain to SemanticEnricher"
|
|
965
|
+
action: "MODIFY"
|
|
966
|
+
file_path: "src/core/graph/enricher.ts"
|
|
967
|
+
description: |
|
|
968
|
+
Apply the same credential chain pattern to SemanticEnricher.
|
|
969
|
+
|
|
970
|
+
**Changes:**
|
|
971
|
+
|
|
972
|
+
1. Modify constructor to accept optional `apiKey` parameter:
|
|
973
|
+
```typescript
|
|
974
|
+
constructor(
|
|
975
|
+
private readonly projectRoot: string,
|
|
976
|
+
private readonly config: NomosConfig['graph'],
|
|
977
|
+
private readonly logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
|
|
978
|
+
apiKey?: string,
|
|
979
|
+
) {
|
|
980
|
+
const key = apiKey ?? process.env['GEMINI_API_KEY'];
|
|
981
|
+
if (!key && config.ai_enrichment) {
|
|
982
|
+
throw new NomosError(
|
|
983
|
+
'graph_ai_key_missing',
|
|
984
|
+
'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
this.client = new GoogleGenerativeAI(key ?? '');
|
|
988
|
+
this.limit = pLimit(config.ai_concurrency);
|
|
989
|
+
}
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
2. Add static async factory method:
|
|
993
|
+
```typescript
|
|
994
|
+
static async create(
|
|
995
|
+
projectRoot: string,
|
|
996
|
+
config: NomosConfig['graph'],
|
|
997
|
+
logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
|
|
998
|
+
authManager?: AuthManager | null,
|
|
999
|
+
): Promise<SemanticEnricher> {
|
|
1000
|
+
const envKey = process.env['GEMINI_API_KEY'];
|
|
1001
|
+
if (envKey) {
|
|
1002
|
+
return new SemanticEnricher(projectRoot, config, logger, envKey);
|
|
1003
|
+
}
|
|
1004
|
+
if (authManager?.isLoggedIn()) {
|
|
1005
|
+
const token = await authManager.getAccessToken();
|
|
1006
|
+
return new SemanticEnricher(projectRoot, config, logger, token);
|
|
1007
|
+
}
|
|
1008
|
+
// No key and enrichment enabled — let constructor throw
|
|
1009
|
+
return new SemanticEnricher(projectRoot, config, logger);
|
|
1010
|
+
}
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
3. Add import for AuthManager:
|
|
1014
|
+
```typescript
|
|
1015
|
+
import { AuthManager } from '../auth/manager.js';
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
**Backward compatibility:** `new SemanticEnricher(root, config, logger)`
|
|
1019
|
+
still works exactly as before.
|
|
1020
|
+
inputs:
|
|
1021
|
+
- "Existing SemanticEnricher class (src/core/graph/enricher.ts)"
|
|
1022
|
+
- "AuthManager from step 2.1"
|
|
1023
|
+
- "Confirmed token compatibility from step 3.5"
|
|
1024
|
+
outputs:
|
|
1025
|
+
- "SemanticEnricher.create() factory method available"
|
|
1026
|
+
- "Existing constructor still works"
|
|
1027
|
+
validation: |
|
|
1028
|
+
`npx tsc --noEmit` passes.
|
|
1029
|
+
`new SemanticEnricher(root, config, logger)` still compiles.
|
|
1030
|
+
depends_on: ["2.1", "3.5"]
|
|
1031
|
+
can_parallel: true
|
|
1032
|
+
risk_level: "high"
|
|
1033
|
+
rollback: |
|
|
1034
|
+
Revert src/core/graph/enricher.ts to original constructor (remove optional
|
|
1035
|
+
apiKey parameter). Remove the `static async create()` method. Remove the
|
|
1036
|
+
`import { AuthManager }` line.
|
|
1037
|
+
NOTE: Before rolling back this step, first rollback steps 4.6c and 4.5
|
|
1038
|
+
(in that order).
|
|
1039
|
+
resolved_findings: []
|
|
1040
|
+
|
|
1041
|
+
- step_id: "4.3"
|
|
1042
|
+
title: "Update SearchIndexer to use Embedder.create() factory"
|
|
1043
|
+
action: "MODIFY"
|
|
1044
|
+
file_path: "src/search/indexer.ts"
|
|
1045
|
+
description: |
|
|
1046
|
+
The SearchIndexer uses a lazy `get embedder()` accessor that calls
|
|
1047
|
+
`new Embedder(config, logger)`. Update it to support the credential chain.
|
|
1048
|
+
|
|
1049
|
+
**Changes:**
|
|
1050
|
+
|
|
1051
|
+
1. Add `AuthManager` to constructor parameters (optional):
|
|
1052
|
+
```typescript
|
|
1053
|
+
constructor(
|
|
1054
|
+
private readonly projectRoot: string,
|
|
1055
|
+
private readonly config: NomosConfig,
|
|
1056
|
+
private readonly logger: Logger,
|
|
1057
|
+
private readonly authManager?: AuthManager | null,
|
|
1058
|
+
)
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
2. Change the lazy embedder accessor from synchronous getter to async method.
|
|
1062
|
+
Replace:
|
|
1063
|
+
```typescript
|
|
1064
|
+
private get embedder(): Embedder {
|
|
1065
|
+
if (!this._embedder) {
|
|
1066
|
+
this._embedder = new Embedder(this.config.search, this.logger);
|
|
1067
|
+
}
|
|
1068
|
+
return this._embedder;
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1071
|
+
With:
|
|
1072
|
+
```typescript
|
|
1073
|
+
private async getEmbedder(): Promise<Embedder> {
|
|
1074
|
+
if (!this._embedder) {
|
|
1075
|
+
this._embedder = await Embedder.create(
|
|
1076
|
+
this.config.search, this.logger, this.authManager,
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
return this._embedder;
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
3. Update all call sites within indexer.ts:
|
|
1084
|
+
- `void this.embedder;` → `await this.getEmbedder();` (pre-check)
|
|
1085
|
+
- `this.embedder.embedBatch(...)` → `(await this.getEmbedder()).embedBatch(...)`
|
|
1086
|
+
|
|
1087
|
+
**Backward compatibility:** Callers that don't pass `authManager` get the
|
|
1088
|
+
same behavior as before (API key only).
|
|
1089
|
+
inputs:
|
|
1090
|
+
- "Existing SearchIndexer class (src/search/indexer.ts)"
|
|
1091
|
+
- "Embedder.create() from step 4.1"
|
|
1092
|
+
outputs:
|
|
1093
|
+
- "SearchIndexer uses credential chain when authManager is provided"
|
|
1094
|
+
validation: |
|
|
1095
|
+
`npx tsc --noEmit` passes.
|
|
1096
|
+
`new SearchIndexer(root, config, logger)` still compiles (authManager optional).
|
|
1097
|
+
depends_on: ["4.1"]
|
|
1098
|
+
can_parallel: false
|
|
1099
|
+
risk_level: "medium"
|
|
1100
|
+
rollback: |
|
|
1101
|
+
Revert the async getEmbedder() method back to the synchronous `get embedder()`
|
|
1102
|
+
accessor. Remove the `authManager` constructor parameter. Revert all call site
|
|
1103
|
+
changes (remove `await` wrappers).
|
|
1104
|
+
IMPORTANT: Before rolling back this step, first rollback step 4.6a
|
|
1105
|
+
(which passes authManager to SearchIndexer).
|
|
1106
|
+
resolved_findings: ["F-007"]
|
|
1107
|
+
|
|
1108
|
+
- step_id: "4.4"
|
|
1109
|
+
title: "Update QueryEngine to use Embedder.create() factory"
|
|
1110
|
+
action: "MODIFY"
|
|
1111
|
+
file_path: "src/search/query-engine.ts"
|
|
1112
|
+
description: |
|
|
1113
|
+
Same pattern as step 4.3. The QueryEngine has an identical lazy `get embedder()`
|
|
1114
|
+
accessor.
|
|
1115
|
+
|
|
1116
|
+
**Changes:**
|
|
1117
|
+
|
|
1118
|
+
1. Add `AuthManager` to constructor (optional):
|
|
1119
|
+
```typescript
|
|
1120
|
+
constructor(
|
|
1121
|
+
private readonly projectRoot: string,
|
|
1122
|
+
private readonly config: NomosConfig,
|
|
1123
|
+
private readonly logger: Logger,
|
|
1124
|
+
private readonly authManager?: AuthManager | null,
|
|
1125
|
+
)
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
2. Change lazy accessor to async `getEmbedder()` method:
|
|
1129
|
+
```typescript
|
|
1130
|
+
private async getEmbedder(): Promise<Embedder> {
|
|
1131
|
+
if (!this._embedder) {
|
|
1132
|
+
this._embedder = await Embedder.create(
|
|
1133
|
+
this.config.search, this.logger, this.authManager,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
return this._embedder;
|
|
1137
|
+
}
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
3. Update `this.embedder.embedOne(query.trim())` in `search()` method to
|
|
1141
|
+
`(await this.getEmbedder()).embedOne(query.trim())`.
|
|
1142
|
+
inputs:
|
|
1143
|
+
- "Existing QueryEngine class (src/search/query-engine.ts)"
|
|
1144
|
+
- "Embedder.create() from step 4.1"
|
|
1145
|
+
outputs:
|
|
1146
|
+
- "QueryEngine uses credential chain when authManager is provided"
|
|
1147
|
+
validation: |
|
|
1148
|
+
`npx tsc --noEmit` passes.
|
|
1149
|
+
`new QueryEngine(root, config, logger)` still compiles (authManager optional).
|
|
1150
|
+
depends_on: ["4.1"]
|
|
1151
|
+
can_parallel: true
|
|
1152
|
+
risk_level: "medium"
|
|
1153
|
+
rollback: |
|
|
1154
|
+
Revert query-engine.ts: restore synchronous `get embedder()` accessor,
|
|
1155
|
+
remove `authManager` constructor parameter, revert `search()` method
|
|
1156
|
+
call site to `this.embedder.embedOne(...)`.
|
|
1157
|
+
IMPORTANT: Before rolling back this step, first rollback step 4.6b
|
|
1158
|
+
(which passes authManager to QueryEngine).
|
|
1159
|
+
resolved_findings: ["F-007"]
|
|
1160
|
+
|
|
1161
|
+
- step_id: "4.5"
|
|
1162
|
+
title: "Update MapPipeline to use SemanticEnricher.create() factory"
|
|
1163
|
+
action: "MODIFY"
|
|
1164
|
+
file_path: "src/core/graph/pipeline.ts"
|
|
1165
|
+
description: |
|
|
1166
|
+
In the `run()` method, the enricher is instantiated at line 146:
|
|
1167
|
+
```typescript
|
|
1168
|
+
const enricher = new SemanticEnricher(this.projectRoot, this.config.graph, this.logger);
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
**Changes:**
|
|
1172
|
+
|
|
1173
|
+
1. Add `authManager` to MapPipeline constructor (optional):
|
|
1174
|
+
```typescript
|
|
1175
|
+
constructor(
|
|
1176
|
+
private readonly config: NomosConfig,
|
|
1177
|
+
private readonly projectRoot: string,
|
|
1178
|
+
private readonly logger: Logger,
|
|
1179
|
+
private readonly authManager?: AuthManager | null,
|
|
1180
|
+
)
|
|
1181
|
+
```
|
|
1182
|
+
NOTE: MapPipeline constructor parameter order is `config, projectRoot, logger`
|
|
1183
|
+
— this is DIFFERENT from SearchIndexer which uses `projectRoot, config, logger`.
|
|
1184
|
+
|
|
1185
|
+
2. Replace direct instantiation with factory in the `run()` method:
|
|
1186
|
+
```typescript
|
|
1187
|
+
const enricher = await SemanticEnricher.create(
|
|
1188
|
+
this.projectRoot, this.config.graph, this.logger, this.authManager,
|
|
1189
|
+
);
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
3. Add import:
|
|
1193
|
+
```typescript
|
|
1194
|
+
import { AuthManager } from '../auth/manager.js';
|
|
1195
|
+
```
|
|
1196
|
+
(Note: relative path from `src/core/graph/` to `src/core/auth/` is `../auth/`)
|
|
1197
|
+
inputs:
|
|
1198
|
+
- "Existing MapPipeline class (src/core/graph/pipeline.ts)"
|
|
1199
|
+
- "SemanticEnricher.create() from step 4.2"
|
|
1200
|
+
outputs:
|
|
1201
|
+
- "MapPipeline passes authManager to SemanticEnricher"
|
|
1202
|
+
validation: |
|
|
1203
|
+
`npx tsc --noEmit` passes.
|
|
1204
|
+
`new MapPipeline(config, projectRoot, logger)` still compiles (authManager optional).
|
|
1205
|
+
depends_on: ["4.2"]
|
|
1206
|
+
can_parallel: false
|
|
1207
|
+
risk_level: "medium"
|
|
1208
|
+
rollback: |
|
|
1209
|
+
Revert pipeline.ts: restore direct `new SemanticEnricher(...)` instantiation,
|
|
1210
|
+
remove `authManager` constructor parameter, remove AuthManager import.
|
|
1211
|
+
IMPORTANT: Before rolling back this step, first rollback step 4.6c
|
|
1212
|
+
(which passes authManager to MapPipeline).
|
|
1213
|
+
resolved_findings: ["F-007"]
|
|
1214
|
+
|
|
1215
|
+
- step_id: "4.6a"
|
|
1216
|
+
title: "Wire AuthManager into src/commands/index.ts (SearchIndexer)"
|
|
1217
|
+
action: "MODIFY"
|
|
1218
|
+
file_path: "src/commands/index.ts"
|
|
1219
|
+
description: |
|
|
1220
|
+
Update the `arc index` command to create an `AuthManager` and pass it
|
|
1221
|
+
to `SearchIndexer`.
|
|
1222
|
+
|
|
1223
|
+
**Changes:**
|
|
1224
|
+
|
|
1225
|
+
1. Add import:
|
|
1226
|
+
```typescript
|
|
1227
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
2. After `const logger = createLogger(...)`, create AuthManager:
|
|
1231
|
+
```typescript
|
|
1232
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
3. Pass to SearchIndexer constructor (note parameter order: projectRoot, config, logger, authManager):
|
|
1236
|
+
```typescript
|
|
1237
|
+
const indexer = new SearchIndexer(projectRoot, config, logger, authManager);
|
|
1238
|
+
```
|
|
1239
|
+
inputs:
|
|
1240
|
+
- "AuthManager from step 2.1"
|
|
1241
|
+
- "Updated SearchIndexer from step 4.3"
|
|
1242
|
+
outputs:
|
|
1243
|
+
- "`arc index` command uses credential chain"
|
|
1244
|
+
validation: |
|
|
1245
|
+
`npx tsc --noEmit` passes.
|
|
1246
|
+
`arc index` works with GEMINI_API_KEY set (backward compat).
|
|
1247
|
+
depends_on: ["4.3"]
|
|
1248
|
+
can_parallel: false
|
|
1249
|
+
risk_level: "medium"
|
|
1250
|
+
rollback: |
|
|
1251
|
+
Remove AuthManager import, AuthManager instantiation, and the `authManager`
|
|
1252
|
+
argument from the SearchIndexer constructor call in src/commands/index.ts.
|
|
1253
|
+
resolved_findings: ["F-005"]
|
|
1254
|
+
|
|
1255
|
+
- step_id: "4.6b"
|
|
1256
|
+
title: "Wire AuthManager into src/commands/search.ts (QueryEngine)"
|
|
1257
|
+
action: "MODIFY"
|
|
1258
|
+
file_path: "src/commands/search.ts"
|
|
1259
|
+
description: |
|
|
1260
|
+
Update the `arc search` command to create an `AuthManager` and pass it
|
|
1261
|
+
to `QueryEngine`.
|
|
1262
|
+
|
|
1263
|
+
**Changes:**
|
|
1264
|
+
|
|
1265
|
+
1. Add import:
|
|
1266
|
+
```typescript
|
|
1267
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
2. After `const logger = createLogger(...)`, create AuthManager:
|
|
1271
|
+
```typescript
|
|
1272
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
3. Pass to QueryEngine constructor (note parameter order: projectRoot, config, logger, authManager):
|
|
1276
|
+
```typescript
|
|
1277
|
+
const engine = new QueryEngine(projectRoot, config, logger, authManager);
|
|
1278
|
+
```
|
|
1279
|
+
inputs:
|
|
1280
|
+
- "AuthManager from step 2.1"
|
|
1281
|
+
- "Updated QueryEngine from step 4.4"
|
|
1282
|
+
outputs:
|
|
1283
|
+
- "`arc search` command uses credential chain"
|
|
1284
|
+
validation: |
|
|
1285
|
+
`npx tsc --noEmit` passes.
|
|
1286
|
+
`arc search` works with GEMINI_API_KEY set (backward compat).
|
|
1287
|
+
depends_on: ["4.4"]
|
|
1288
|
+
can_parallel: true
|
|
1289
|
+
risk_level: "medium"
|
|
1290
|
+
rollback: |
|
|
1291
|
+
Remove AuthManager import, AuthManager instantiation, and the `authManager`
|
|
1292
|
+
argument from the QueryEngine constructor call in src/commands/search.ts.
|
|
1293
|
+
resolved_findings: ["F-005"]
|
|
1294
|
+
|
|
1295
|
+
- step_id: "4.6c"
|
|
1296
|
+
title: "Wire AuthManager into src/commands/map.ts (MapPipeline)"
|
|
1297
|
+
action: "MODIFY"
|
|
1298
|
+
file_path: "src/commands/map.ts"
|
|
1299
|
+
description: |
|
|
1300
|
+
Update the `arc map` command to create an `AuthManager` and pass it
|
|
1301
|
+
to `MapPipeline`.
|
|
1302
|
+
|
|
1303
|
+
**Changes:**
|
|
1304
|
+
|
|
1305
|
+
1. Add import:
|
|
1306
|
+
```typescript
|
|
1307
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
2. After `const logger = createLogger(...)`, create AuthManager:
|
|
1311
|
+
```typescript
|
|
1312
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
3. Pass to MapPipeline constructor. IMPORTANT: MapPipeline constructor
|
|
1316
|
+
parameter order is DIFFERENT from SearchIndexer and QueryEngine:
|
|
1317
|
+
```typescript
|
|
1318
|
+
// MapPipeline: config FIRST, projectRoot SECOND (reversed from SearchIndexer)
|
|
1319
|
+
const pipeline = new MapPipeline(config, projectRoot, logger, authManager);
|
|
1320
|
+
```
|
|
1321
|
+
Do NOT use `new MapPipeline(projectRoot, config, logger, authManager)` —
|
|
1322
|
+
that would swap config and projectRoot.
|
|
1323
|
+
inputs:
|
|
1324
|
+
- "AuthManager from step 2.1"
|
|
1325
|
+
- "Updated MapPipeline from step 4.5"
|
|
1326
|
+
outputs:
|
|
1327
|
+
- "`arc map` command uses credential chain"
|
|
1328
|
+
validation: |
|
|
1329
|
+
`npx tsc --noEmit` passes.
|
|
1330
|
+
`arc map` works with GEMINI_API_KEY set (backward compat).
|
|
1331
|
+
HARDENED: Verify MapPipeline constructor call has config as first arg,
|
|
1332
|
+
projectRoot as second — NOT the reverse.
|
|
1333
|
+
depends_on: ["4.5"]
|
|
1334
|
+
can_parallel: true
|
|
1335
|
+
risk_level: "medium"
|
|
1336
|
+
rollback: |
|
|
1337
|
+
Remove AuthManager import, AuthManager instantiation, and the `authManager`
|
|
1338
|
+
argument from the MapPipeline constructor call in src/commands/map.ts.
|
|
1339
|
+
resolved_findings: ["F-005"]
|
|
1340
|
+
|
|
1341
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1342
|
+
# PHASE 5: SECURITY — Sanitization Coverage
|
|
1343
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1344
|
+
|
|
1345
|
+
- step_id: "5.1"
|
|
1346
|
+
title: "Verify sanitize.ts covers auth tokens in env deny list"
|
|
1347
|
+
action: "VERIFY"
|
|
1348
|
+
file_path: "src/utils/sanitize.ts"
|
|
1349
|
+
description: |
|
|
1350
|
+
Verify that the `ALWAYS_DENY` regex array in `sanitizeEnv()` (line 57-61)
|
|
1351
|
+
covers Google OAuth credential env vars.
|
|
1352
|
+
|
|
1353
|
+
Specifically, verify that the regex `GOOGLE_.*?(KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)`
|
|
1354
|
+
matches `GOOGLE_CLIENT_SECRET`, `NOMOS_GOOGLE_CLIENT_SECRET`, and similar
|
|
1355
|
+
Google credential env var names.
|
|
1356
|
+
|
|
1357
|
+
**Expected result:** No changes needed. The existing patterns already cover
|
|
1358
|
+
Google credential env vars. Auth tokens are stored in
|
|
1359
|
+
`~/.nomos/credentials.json` (file, not env vars) and are never set as
|
|
1360
|
+
environment variables by the auth module.
|
|
1361
|
+
|
|
1362
|
+
**If the regex does NOT match `NOMOS_GOOGLE_CLIENT_SECRET`:** Add a new
|
|
1363
|
+
pattern `NOMOS_.*?SECRET` to the ALWAYS_DENY array and report the gap.
|
|
1364
|
+
|
|
1365
|
+
This step produces a verification result, not a code change.
|
|
1366
|
+
inputs:
|
|
1367
|
+
- "Existing sanitize.ts ALWAYS_DENY patterns"
|
|
1368
|
+
outputs:
|
|
1369
|
+
- "Confirmation that auth tokens are not leaked through env sanitization gaps"
|
|
1370
|
+
validation: |
|
|
1371
|
+
Manual review confirms ALWAYS_DENY covers GOOGLE_* secrets.
|
|
1372
|
+
If NOMOS_GOOGLE_CLIENT_SECRET is not covered, a new pattern is added.
|
|
1373
|
+
Auth tokens stored in file only — no env var leak path.
|
|
1374
|
+
depends_on: []
|
|
1375
|
+
can_parallel: true
|
|
1376
|
+
risk_level: "low"
|
|
1377
|
+
rollback: |
|
|
1378
|
+
If a pattern was added, remove it. Otherwise N/A — verify-only step.
|
|
1379
|
+
resolved_findings: ["F-006"]
|
|
1380
|
+
|
|
1381
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1382
|
+
# PHASE 6: VALIDATION & TESTING
|
|
1383
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1384
|
+
|
|
1385
|
+
- step_id: "6.1"
|
|
1386
|
+
title: "TypeScript compilation check"
|
|
1387
|
+
action: "TEST"
|
|
1388
|
+
file_path: ""
|
|
1389
|
+
description: |
|
|
1390
|
+
Run `npx tsc --noEmit` (or `npm run lint`) to verify all new and
|
|
1391
|
+
modified files compile without errors. This catches:
|
|
1392
|
+
- Missing imports
|
|
1393
|
+
- Type mismatches between AuthCredentials, NomosConfig, NomosErrorCode
|
|
1394
|
+
- Incorrect async/await usage in factory methods
|
|
1395
|
+
- MapPipeline constructor parameter order issues
|
|
1396
|
+
inputs:
|
|
1397
|
+
- "All files from steps 1.1 through 5.1"
|
|
1398
|
+
outputs:
|
|
1399
|
+
- "Zero TypeScript errors"
|
|
1400
|
+
validation: |
|
|
1401
|
+
`npm run lint` exits 0.
|
|
1402
|
+
depends_on: ["1.1", "1.2", "1.3", "1.4", "2.1", "2.2", "3.1", "3.2", "3.5", "4.1", "4.2", "4.3", "4.4", "4.5", "4.6a", "4.6b", "4.6c"]
|
|
1403
|
+
can_parallel: false
|
|
1404
|
+
risk_level: "low"
|
|
1405
|
+
rollback: |
|
|
1406
|
+
Fix compilation errors identified.
|
|
1407
|
+
resolved_findings: []
|
|
1408
|
+
|
|
1409
|
+
- step_id: "6.2"
|
|
1410
|
+
title: "Run existing test suite for regression"
|
|
1411
|
+
action: "TEST"
|
|
1412
|
+
file_path: ""
|
|
1413
|
+
description: |
|
|
1414
|
+
Run `npm test` to ensure all existing tests still pass. Key test files
|
|
1415
|
+
to watch:
|
|
1416
|
+
- `src/search/__tests__/embedder.test.ts` — may need update if constructor
|
|
1417
|
+
signature changed (apiKey parameter added)
|
|
1418
|
+
- `src/core/graph/__tests__/enricher.test.ts` — same concern
|
|
1419
|
+
- `src/core/graph/__tests__/pipeline.test.ts` — if MapPipeline constructor
|
|
1420
|
+
changed
|
|
1421
|
+
- `src/search/__tests__/indexer.test.ts` — if SearchIndexer constructor changed
|
|
1422
|
+
|
|
1423
|
+
If tests fail due to constructor signature changes (new optional `apiKey`
|
|
1424
|
+
or `authManager` params), these are expected and must be fixed:
|
|
1425
|
+
- Tests using `new Embedder(config, logger)` → still valid (apiKey is optional)
|
|
1426
|
+
- Tests using `new SearchIndexer(root, config, logger)` → still valid (authManager optional)
|
|
1427
|
+
|
|
1428
|
+
If any test fails for an unexpected reason, investigate before proceeding.
|
|
1429
|
+
|
|
1430
|
+
IMPORTANT: If failures are found in Phase 4 steps, selective rollback of
|
|
1431
|
+
individual steps is unsafe — all of Phase 4 must be rolled back together
|
|
1432
|
+
(in order: 4.6c → 4.6b → 4.6a → 4.5 → 4.4 → 4.3 → 4.2 → 4.1).
|
|
1433
|
+
inputs:
|
|
1434
|
+
- "All source changes from previous steps"
|
|
1435
|
+
outputs:
|
|
1436
|
+
- "All existing tests pass (or failures are expected and documented)"
|
|
1437
|
+
validation: |
|
|
1438
|
+
`npm test` exits 0 (or with only expected, documented failures).
|
|
1439
|
+
depends_on: ["6.1"]
|
|
1440
|
+
can_parallel: false
|
|
1441
|
+
risk_level: "medium"
|
|
1442
|
+
rollback: |
|
|
1443
|
+
Fix test failures. If a test fails due to constructor changes,
|
|
1444
|
+
update the test to pass the new optional parameter or verify
|
|
1445
|
+
the default behavior hasn't changed.
|
|
1446
|
+
resolved_findings: []
|
|
1447
|
+
|
|
1448
|
+
- step_id: "6.3"
|
|
1449
|
+
title: "Unit tests for AuthManager"
|
|
1450
|
+
action: "CREATE"
|
|
1451
|
+
file_path: "src/core/auth/__tests__/manager.test.ts"
|
|
1452
|
+
description: |
|
|
1453
|
+
Create unit tests for AuthManager covering:
|
|
1454
|
+
|
|
1455
|
+
1. **saveCredentials** — writes JSON to temp path, verifies file exists
|
|
1456
|
+
and contains correct data, verifies file permissions are 0600.
|
|
1457
|
+
2. **loadCredentials** — returns parsed object when file exists,
|
|
1458
|
+
returns null when file doesn't exist, returns null on invalid JSON.
|
|
1459
|
+
3. **isLoggedIn** — true when credentials file has refresh_token,
|
|
1460
|
+
false when file missing.
|
|
1461
|
+
4. **clearCredentials** — deletes file, doesn't throw if file missing.
|
|
1462
|
+
5. **getAccessToken** — throws `auth_not_logged_in` when no credentials.
|
|
1463
|
+
|
|
1464
|
+
Use `vitest` (project's test runner). Mock the filesystem or use
|
|
1465
|
+
a temp directory (`os.tmpdir()`) with cleanup in afterEach.
|
|
1466
|
+
|
|
1467
|
+
Do NOT mock google-auth-library for unit tests — test only the
|
|
1468
|
+
file I/O and credential management logic. Integration with Google
|
|
1469
|
+
is tested manually via `arc auth login`.
|
|
1470
|
+
inputs:
|
|
1471
|
+
- "AuthManager from step 2.1"
|
|
1472
|
+
outputs:
|
|
1473
|
+
- "Unit tests for AuthManager pass"
|
|
1474
|
+
validation: |
|
|
1475
|
+
`npx vitest run src/core/auth/__tests__/manager.test.ts` exits 0.
|
|
1476
|
+
depends_on: ["2.1"]
|
|
1477
|
+
can_parallel: true
|
|
1478
|
+
risk_level: "low"
|
|
1479
|
+
rollback: |
|
|
1480
|
+
Delete src/core/auth/__tests__/manager.test.ts.
|
|
1481
|
+
resolved_findings: []
|
|
1482
|
+
|
|
1483
|
+
- step_id: "6.4"
|
|
1484
|
+
title: "Unit tests for loopback server"
|
|
1485
|
+
action: "CREATE"
|
|
1486
|
+
file_path: "src/core/auth/__tests__/server.test.ts"
|
|
1487
|
+
description: |
|
|
1488
|
+
Create unit tests for the loopback server covering:
|
|
1489
|
+
|
|
1490
|
+
1. **Port binding** — server starts on specified port.
|
|
1491
|
+
2. **Timeout** — server rejects after 120s timeout (use fake timers
|
|
1492
|
+
via `vi.useFakeTimers()`).
|
|
1493
|
+
3. **Callback handling** — mock an HTTP GET with `?code=test_code&state=<valid_state>`,
|
|
1494
|
+
verify the function resolves (mock `oauth2Client.getToken`).
|
|
1495
|
+
4. **CSRF state validation** — mock an HTTP GET with `?code=test_code&state=wrong_state`,
|
|
1496
|
+
verify the server responds with 403 and does NOT exchange the code.
|
|
1497
|
+
5. **Cleanup** — server is closed after callback.
|
|
1498
|
+
|
|
1499
|
+
Mock `OAuth2Client.getToken()` to return fake tokens.
|
|
1500
|
+
Mock `open` to prevent actual browser launch.
|
|
1501
|
+
|
|
1502
|
+
HARDENED: Test case 4 specifically validates the F-002 CSRF fix.
|
|
1503
|
+
inputs:
|
|
1504
|
+
- "startLoopbackServer from step 2.2"
|
|
1505
|
+
outputs:
|
|
1506
|
+
- "Unit tests for loopback server pass"
|
|
1507
|
+
validation: |
|
|
1508
|
+
`npx vitest run src/core/auth/__tests__/server.test.ts` exits 0.
|
|
1509
|
+
depends_on: ["2.2"]
|
|
1510
|
+
can_parallel: true
|
|
1511
|
+
risk_level: "low"
|
|
1512
|
+
rollback: |
|
|
1513
|
+
Delete src/core/auth/__tests__/server.test.ts.
|
|
1514
|
+
resolved_findings: []
|
|
1515
|
+
|
|
1516
|
+
- step_id: "6.5"
|
|
1517
|
+
title: "Integration test — Embedder credential chain"
|
|
1518
|
+
action: "CREATE"
|
|
1519
|
+
file_path: "src/search/__tests__/embedder-auth.test.ts"
|
|
1520
|
+
description: |
|
|
1521
|
+
Test the credential chain in Embedder.create():
|
|
1522
|
+
|
|
1523
|
+
1. **API key takes priority** — set GEMINI_API_KEY env var, call
|
|
1524
|
+
`Embedder.create(config, logger, authManager)`. Verify Embedder
|
|
1525
|
+
is created using the env var (mock AuthManager, verify it's NOT called).
|
|
1526
|
+
2. **OAuth fallback** — unset GEMINI_API_KEY, mock
|
|
1527
|
+
`authManager.isLoggedIn()` → true, `authManager.getAccessToken()` →
|
|
1528
|
+
'fake-token'. Verify Embedder is created.
|
|
1529
|
+
3. **Neither available** — unset GEMINI_API_KEY, mock
|
|
1530
|
+
`authManager.isLoggedIn()` → false. Verify `search_api_key_missing`
|
|
1531
|
+
error is thrown with message mentioning both `GEMINI_API_KEY` and
|
|
1532
|
+
`arc auth login`.
|
|
1533
|
+
inputs:
|
|
1534
|
+
- "Embedder.create() from step 4.1"
|
|
1535
|
+
- "AuthManager from step 2.1"
|
|
1536
|
+
outputs:
|
|
1537
|
+
- "Credential chain works correctly in all priority scenarios"
|
|
1538
|
+
validation: |
|
|
1539
|
+
`npx vitest run src/search/__tests__/embedder-auth.test.ts` exits 0.
|
|
1540
|
+
depends_on: ["4.1"]
|
|
1541
|
+
can_parallel: true
|
|
1542
|
+
risk_level: "low"
|
|
1543
|
+
rollback: |
|
|
1544
|
+
Delete src/search/__tests__/embedder-auth.test.ts.
|
|
1545
|
+
resolved_findings: []
|
|
1546
|
+
|
|
1547
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1548
|
+
# RISK ASSESSMENT (POST-HARDENING)
|
|
1549
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1550
|
+
risk_assessment:
|
|
1551
|
+
overall_risk: "medium"
|
|
1552
|
+
critical_steps: ["2.2", "3.5", "4.1", "4.2"]
|
|
1553
|
+
remaining_risks:
|
|
1554
|
+
- risk: "OAuth access_token may not work as GoogleGenerativeAI API key"
|
|
1555
|
+
severity: "medium"
|
|
1556
|
+
mitigation: |
|
|
1557
|
+
Step 3.5 now validates this assumption before Phase 4 begins.
|
|
1558
|
+
If validation fails, concrete alternative steps are provided.
|
|
1559
|
+
Risk is reduced from HIGH to MEDIUM because it's now detected early.
|
|
1560
|
+
- risk: "Dynamic port breaks Google OAuth redirect URI matching"
|
|
1561
|
+
severity: "medium"
|
|
1562
|
+
mitigation: |
|
|
1563
|
+
Fixed default port (3000) with configurable override. Fallback to
|
|
1564
|
+
random port only if 3000 is in use — with clear warning.
|
|
1565
|
+
- risk: "Token refresh fails silently, stale token used for API calls"
|
|
1566
|
+
severity: "low"
|
|
1567
|
+
mitigation: |
|
|
1568
|
+
AuthManager.getAuthenticatedClient() checks expiry_date and refreshes
|
|
1569
|
+
proactively. If refresh fails, throws auth_token_expired.
|
|
1570
|
+
- risk: "loadCredentials() uses synchronous fs.readFileSync"
|
|
1571
|
+
severity: "low"
|
|
1572
|
+
mitigation: |
|
|
1573
|
+
Accepted for CLI startup path (non-hot-path). Negative constraint added
|
|
1574
|
+
to prevent sync I/O in hot paths. (ref: F-008)
|
|
1575
|
+
|
|
1576
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1577
|
+
# INTEGRITY VERIFICATION
|
|
1578
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1579
|
+
integrity_check:
|
|
1580
|
+
all_critical_resolved: true
|
|
1581
|
+
all_high_resolved: true
|
|
1582
|
+
findings_checklist:
|
|
1583
|
+
- finding_id: "F-001"
|
|
1584
|
+
status: "resolved"
|
|
1585
|
+
resolution_step: "3.1"
|
|
1586
|
+
- finding_id: "F-002"
|
|
1587
|
+
status: "resolved"
|
|
1588
|
+
resolution_step: "2.2"
|
|
1589
|
+
- finding_id: "F-003"
|
|
1590
|
+
status: "resolved"
|
|
1591
|
+
resolution_step: "3.5"
|
|
1592
|
+
- finding_id: "F-004"
|
|
1593
|
+
status: "resolved"
|
|
1594
|
+
resolution_step: "3.1"
|
|
1595
|
+
- finding_id: "F-005"
|
|
1596
|
+
status: "resolved"
|
|
1597
|
+
resolution_step: "4.6a, 4.6b, 4.6c"
|
|
1598
|
+
- finding_id: "F-006"
|
|
1599
|
+
status: "resolved"
|
|
1600
|
+
resolution_step: "5.1"
|
|
1601
|
+
- finding_id: "F-007"
|
|
1602
|
+
status: "resolved"
|
|
1603
|
+
resolution_step: "4.3, 4.4, 4.5"
|
|
1604
|
+
- finding_id: "F-008"
|
|
1605
|
+
status: "resolved"
|
|
1606
|
+
resolution_step: "2.1"
|
|
1607
|
+
- finding_id: "F-009"
|
|
1608
|
+
status: "resolved"
|
|
1609
|
+
resolution_step: "2.2"
|
|
1610
|
+
dependency_chain_valid: true
|
|
1611
|
+
rollback_chain_valid: true
|
|
1612
|
+
negative_constraints_applied: true
|
|
1613
|
+
|
|
1614
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1615
|
+
# CHANGE SUMMARY
|
|
1616
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1617
|
+
changelog:
|
|
1618
|
+
steps_modified: ["2.1", "2.2", "3.1", "4.1", "4.2", "4.3", "4.4", "4.5", "5.1"]
|
|
1619
|
+
steps_added: ["3.5", "4.6a", "4.6b", "4.6c"]
|
|
1620
|
+
steps_removed: ["4.6"]
|
|
1621
|
+
constraints_injected: 10
|
|
1622
|
+
rollbacks_rewritten: 7
|
|
1623
|
+
total_changes: 16
|
|
1624
|
+
|
|
1625
|
+
summary:
|
|
1626
|
+
total_steps: 22
|
|
1627
|
+
estimated_files_changed: 10
|
|
1628
|
+
estimated_files_created: 5
|
|
1629
|
+
hardening_notes: |
|
|
1630
|
+
The plan has been hardened to resolve all 9 Red Team findings:
|
|
1631
|
+
|
|
1632
|
+
1. **F-001 (CRITICAL):** Removed --client-secret CLI flag. Secret is now
|
|
1633
|
+
resolved via config file → env var → interactive masked prompt. Shell
|
|
1634
|
+
history and process list exposure eliminated.
|
|
1635
|
+
|
|
1636
|
+
2. **F-002 (HIGH):** Added cryptographic state parameter (32 random bytes)
|
|
1637
|
+
to OAuth flow per RFC 6749 Section 10.12. Callback validates state
|
|
1638
|
+
before token exchange, rejects with 403 on mismatch.
|
|
1639
|
+
|
|
1640
|
+
3. **F-003 (HIGH):** Inserted validation gate (step 3.5) that tests OAuth
|
|
1641
|
+
token compatibility with GoogleGenerativeAI SDK before Phase 4 begins.
|
|
1642
|
+
Provides concrete alternative implementation if validation fails.
|
|
1643
|
+
|
|
1644
|
+
4. **F-004 (HIGH):** Replaced all `{ ... }` placeholders in step 3.1 with
|
|
1645
|
+
complete implementation code for login/logout/status handlers.
|
|
1646
|
+
|
|
1647
|
+
5. **F-005 (MEDIUM):** Split step 4.6 into 4.6a/4.6b/4.6c with explicit
|
|
1648
|
+
code per file, especially MapPipeline's reversed parameter order.
|
|
1649
|
+
|
|
1650
|
+
6. **F-006 (MEDIUM):** Changed step 5.1 from MODIFY to VERIFY action.
|
|
1651
|
+
|
|
1652
|
+
7. **F-007 (MEDIUM):** Added explicit rollback ordering to steps 4.3-4.5.
|
|
1653
|
+
|
|
1654
|
+
8. **F-008 (MEDIUM):** Accepted sync design with negative constraint.
|
|
1655
|
+
|
|
1656
|
+
9. **F-009 (LOW):** Added Connection: close header and immediate server.close().
|
|
1657
|
+
|
|
1658
|
+
Overall risk reduced from HIGH to MEDIUM. The plan is now safe for execution.
|