@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,1093 @@
|
|
|
1
|
+
plan:
|
|
2
|
+
task_id: "auth-oauth-login"
|
|
3
|
+
task_title: "Frictionless OAuth 2.0 Google Login for arc CLI"
|
|
4
|
+
created_at: "2026-04-06T00:00:00Z"
|
|
5
|
+
|
|
6
|
+
context_analysis:
|
|
7
|
+
tech_stack:
|
|
8
|
+
language: "TypeScript (strict mode)"
|
|
9
|
+
framework: "Commander.js CLI, Zod validation"
|
|
10
|
+
runtime: "Node.js >= 20 (ESM)"
|
|
11
|
+
package_manager: "npm"
|
|
12
|
+
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"
|
|
13
|
+
touch_zone:
|
|
14
|
+
- "package.json" # Add google-auth-library dependency (open already present)
|
|
15
|
+
- "src/types/index.ts" # Add AuthCredentials interface, auth config section to NomosConfig
|
|
16
|
+
- "src/core/errors.ts" # Add auth_* error codes to NomosErrorCode union
|
|
17
|
+
- "src/core/config.ts" # Add AuthSchema with Zod defaults to NomosConfigSchema
|
|
18
|
+
- "src/core/auth/manager.ts" # CREATE — AuthManager class
|
|
19
|
+
- "src/core/auth/server.ts" # CREATE — OAuth loopback HTTP server
|
|
20
|
+
- "src/commands/auth.ts" # CREATE — arc auth login/logout/status
|
|
21
|
+
- "src/cli.ts" # Register registerAuthCommand
|
|
22
|
+
- "src/search/embedder.ts" # Credential chain fallback (sync→async factory)
|
|
23
|
+
- "src/core/graph/enricher.ts" # Credential chain fallback
|
|
24
|
+
- "src/search/indexer.ts" # Update Embedder instantiation to use factory
|
|
25
|
+
- "src/search/query-engine.ts" # Update Embedder instantiation to use factory
|
|
26
|
+
- "src/core/graph/pipeline.ts" # Update SemanticEnricher instantiation
|
|
27
|
+
- "src/commands/index.ts" # Pass authManager or config to indexer
|
|
28
|
+
- "src/commands/search.ts" # Pass authManager or config to query engine
|
|
29
|
+
- "src/commands/map.ts" # Pass authManager or config to pipeline
|
|
30
|
+
fragile_zone:
|
|
31
|
+
- "src/core/state.ts" # Task state management — no changes
|
|
32
|
+
- "src/core/orchestrator.ts" # Orchestration loop — no changes
|
|
33
|
+
- "src/core/budget.ts" # Budget tracking — no changes
|
|
34
|
+
- "src/search/vector-store.ts" # Vector storage — no changes
|
|
35
|
+
- "src/search/chunk-extractor.ts" # Chunk extraction — no changes
|
|
36
|
+
- "src/utils/sanitize.ts" # Sanitization — review only (ensure token coverage)
|
|
37
|
+
dependencies:
|
|
38
|
+
- "google-auth-library (new npm dependency)"
|
|
39
|
+
- "open (already in package.json ^11.0.0)"
|
|
40
|
+
- "@google/generative-ai (existing ^0.24.1)"
|
|
41
|
+
- "commander (existing ^14.0.3)"
|
|
42
|
+
- "winston (existing ^3.19.0)"
|
|
43
|
+
- "zod (existing ^4.3.6)"
|
|
44
|
+
- "Node.js built-in: http, net, fs, path, os, crypto"
|
|
45
|
+
|
|
46
|
+
steps:
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
# PHASE 1: CONTRACT FIRST — Types, Error Codes, Config Schema
|
|
49
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
- step_id: "1.1"
|
|
52
|
+
title: "Install google-auth-library dependency"
|
|
53
|
+
action: "CONFIGURE"
|
|
54
|
+
file_path: "package.json"
|
|
55
|
+
description: |
|
|
56
|
+
Run `npm install google-auth-library`.
|
|
57
|
+
This adds the Google OAuth2 client library needed for token exchange,
|
|
58
|
+
refresh, and auth URL generation. The `open` package is already installed.
|
|
59
|
+
Also update the esbuild build script in package.json to add
|
|
60
|
+
`--external:google-auth-library` to the existing externals list.
|
|
61
|
+
inputs:
|
|
62
|
+
- "Current package.json with existing dependencies"
|
|
63
|
+
outputs:
|
|
64
|
+
- "package.json updated with google-auth-library dependency"
|
|
65
|
+
- "package-lock.json updated"
|
|
66
|
+
- "esbuild externals list includes google-auth-library"
|
|
67
|
+
validation: |
|
|
68
|
+
`npm ls google-auth-library` exits 0.
|
|
69
|
+
`grep 'google-auth-library' package.json` returns a match.
|
|
70
|
+
Build script contains `--external:google-auth-library`.
|
|
71
|
+
depends_on: []
|
|
72
|
+
can_parallel: true
|
|
73
|
+
risk_level: "low"
|
|
74
|
+
rollback: |
|
|
75
|
+
`npm uninstall google-auth-library`. Revert esbuild script change.
|
|
76
|
+
|
|
77
|
+
- step_id: "1.2"
|
|
78
|
+
title: "Add AuthCredentials interface and auth config to NomosConfig"
|
|
79
|
+
action: "MODIFY"
|
|
80
|
+
file_path: "src/types/index.ts"
|
|
81
|
+
description: |
|
|
82
|
+
Add the following AFTER the existing `ExecutionMode` type declaration block
|
|
83
|
+
and BEFORE the `NomosConfig` interface:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// ─── Auth Types ──────────────────────────────────────────────────────────────
|
|
87
|
+
export interface AuthCredentials {
|
|
88
|
+
access_token: string;
|
|
89
|
+
refresh_token: string;
|
|
90
|
+
expiry_date: number;
|
|
91
|
+
token_type: string;
|
|
92
|
+
scope: string;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then add an `auth` section to the `NomosConfig` interface, after the `search`
|
|
97
|
+
block:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
auth: {
|
|
101
|
+
client_id?: string;
|
|
102
|
+
client_secret?: string;
|
|
103
|
+
credentials_path: string;
|
|
104
|
+
redirect_port: number;
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The `credentials_path` defaults to `~/.nomos/credentials.json`.
|
|
109
|
+
The `redirect_port` defaults to `3000` (fixed port for Google Cloud Console
|
|
110
|
+
redirect URI matching, with fallback logic handled in the server module).
|
|
111
|
+
inputs:
|
|
112
|
+
- "Existing NomosConfig interface (lines 15-75 of src/types/index.ts)"
|
|
113
|
+
outputs:
|
|
114
|
+
- "AuthCredentials interface available for import"
|
|
115
|
+
- "NomosConfig.auth section defined"
|
|
116
|
+
validation: |
|
|
117
|
+
`npx tsc --noEmit` passes. AuthCredentials and NomosConfig['auth'] are
|
|
118
|
+
importable from '../types/index.js'.
|
|
119
|
+
depends_on: []
|
|
120
|
+
can_parallel: true
|
|
121
|
+
risk_level: "low"
|
|
122
|
+
rollback: |
|
|
123
|
+
Remove the added interface and config section from src/types/index.ts.
|
|
124
|
+
|
|
125
|
+
- step_id: "1.3"
|
|
126
|
+
title: "Register auth error codes in NomosErrorCode union"
|
|
127
|
+
action: "MODIFY"
|
|
128
|
+
file_path: "src/core/errors.ts"
|
|
129
|
+
description: |
|
|
130
|
+
Add four new error codes to the `NomosErrorCode` union type, after the
|
|
131
|
+
existing `search_api_key_missing` entry:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
| 'auth_not_logged_in'
|
|
135
|
+
| 'auth_token_expired'
|
|
136
|
+
| 'auth_login_failed'
|
|
137
|
+
| 'auth_client_config_missing';
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
These codes are used by AuthManager and the auth commands to throw
|
|
141
|
+
typed NomosError instances.
|
|
142
|
+
inputs:
|
|
143
|
+
- "Existing NomosErrorCode union (lines 1-38 of src/core/errors.ts)"
|
|
144
|
+
outputs:
|
|
145
|
+
- "Four new error codes available in the NomosErrorCode type"
|
|
146
|
+
validation: |
|
|
147
|
+
`npx tsc --noEmit` passes. Code `new NomosError('auth_not_logged_in', '...')`
|
|
148
|
+
compiles without error.
|
|
149
|
+
depends_on: []
|
|
150
|
+
can_parallel: true
|
|
151
|
+
risk_level: "low"
|
|
152
|
+
rollback: |
|
|
153
|
+
Remove the four added lines from the NomosErrorCode union.
|
|
154
|
+
|
|
155
|
+
- step_id: "1.4"
|
|
156
|
+
title: "Add AuthSchema to Zod config with defaults"
|
|
157
|
+
action: "MODIFY"
|
|
158
|
+
file_path: "src/core/config.ts"
|
|
159
|
+
description: |
|
|
160
|
+
Add a new `AuthSchema` Zod object AFTER the existing `SearchConfigSchema`
|
|
161
|
+
(around line 105) and BEFORE `NomosConfigSchema`:
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const AuthSchema = z.object({
|
|
165
|
+
client_id: z.string().optional(),
|
|
166
|
+
client_secret: z.string().optional(),
|
|
167
|
+
credentials_path: z.string().default(
|
|
168
|
+
path.join(os.homedir(), '.nomos', 'credentials.json'),
|
|
169
|
+
),
|
|
170
|
+
redirect_port: z.number().int().positive().default(3000),
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Add `import * as os from 'node:os';` to the top imports.
|
|
175
|
+
|
|
176
|
+
Then add the `auth` key to `NomosConfigSchema`:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
auth: AuthSchema.default(() => AuthSchema.parse({})),
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This follows the exact same pattern as every other config section
|
|
183
|
+
(e.g., `search: SearchConfigSchema.default(...)`).
|
|
184
|
+
inputs:
|
|
185
|
+
- "Existing NomosConfigSchema (lines 118-129 of src/core/config.ts)"
|
|
186
|
+
- "Step 1.2 must be complete (NomosConfig type has auth section)"
|
|
187
|
+
outputs:
|
|
188
|
+
- "Auth config section parsed and defaulted by Zod"
|
|
189
|
+
- ".nomos-config.json can optionally contain auth.client_id, auth.client_secret, auth.redirect_port"
|
|
190
|
+
validation: |
|
|
191
|
+
`npx tsc --noEmit` passes. `getDefaultConfig().auth.credentials_path` ends
|
|
192
|
+
with `.nomos/credentials.json`.
|
|
193
|
+
depends_on: ["1.2"]
|
|
194
|
+
can_parallel: false
|
|
195
|
+
risk_level: "low"
|
|
196
|
+
rollback: |
|
|
197
|
+
Remove AuthSchema definition and the auth key from NomosConfigSchema.
|
|
198
|
+
Remove os import.
|
|
199
|
+
|
|
200
|
+
- step_id: "1.5"
|
|
201
|
+
title: "Create src/core/auth/ directory"
|
|
202
|
+
action: "CREATE"
|
|
203
|
+
file_path: "src/core/auth/"
|
|
204
|
+
description: |
|
|
205
|
+
Create the directory `src/core/auth/`. This is where manager.ts and
|
|
206
|
+
server.ts will live. No index barrel file needed — direct imports are
|
|
207
|
+
used throughout the project (e.g., `import { Embedder } from './embedder.js'`).
|
|
208
|
+
inputs: []
|
|
209
|
+
outputs:
|
|
210
|
+
- "Directory src/core/auth/ exists"
|
|
211
|
+
validation: |
|
|
212
|
+
`ls src/core/auth/` succeeds.
|
|
213
|
+
depends_on: []
|
|
214
|
+
can_parallel: true
|
|
215
|
+
risk_level: "low"
|
|
216
|
+
rollback: |
|
|
217
|
+
`rm -rf src/core/auth/`
|
|
218
|
+
|
|
219
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
220
|
+
# PHASE 2: ISOLATED LOGIC — AuthManager, Loopback Server
|
|
221
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
222
|
+
|
|
223
|
+
- step_id: "2.1"
|
|
224
|
+
title: "Implement AuthManager class"
|
|
225
|
+
action: "CREATE"
|
|
226
|
+
file_path: "src/core/auth/manager.ts"
|
|
227
|
+
description: |
|
|
228
|
+
Create `AuthManager` class with the following contract:
|
|
229
|
+
|
|
230
|
+
**Constructor:** `constructor(authConfig: NomosConfig['auth'], logger: Logger)`
|
|
231
|
+
- Stores config and logger. Does NOT read files or perform I/O in constructor.
|
|
232
|
+
- Logger type: `import type { Logger } from 'winston'`.
|
|
233
|
+
|
|
234
|
+
**Methods:**
|
|
235
|
+
|
|
236
|
+
1. `async saveCredentials(tokens: AuthCredentials): Promise<void>`
|
|
237
|
+
- Ensure `~/.nomos/` directory exists (`fs.mkdir(dir, { recursive: true })`).
|
|
238
|
+
- Write JSON to `authConfig.credentials_path`.
|
|
239
|
+
- Set file permissions: `fs.chmod(path, 0o600)`.
|
|
240
|
+
- Log success via `logger.info()`. Never log token values.
|
|
241
|
+
|
|
242
|
+
2. `loadCredentials(): AuthCredentials | null`
|
|
243
|
+
- Synchronous read. Return `null` if file doesn't exist or is invalid JSON.
|
|
244
|
+
- Parse and validate shape matches `AuthCredentials` interface.
|
|
245
|
+
|
|
246
|
+
3. `async getAuthenticatedClient(): Promise<OAuth2Client>`
|
|
247
|
+
- Load credentials via `loadCredentials()`.
|
|
248
|
+
- If null, throw `NomosError('auth_not_logged_in', ...)`.
|
|
249
|
+
- Create `OAuth2Client` from `google-auth-library`, set credentials.
|
|
250
|
+
- Check `expiry_date < Date.now()`:
|
|
251
|
+
- If expired, call `oauth2Client.refreshAccessToken()`.
|
|
252
|
+
- Persist refreshed tokens via `saveCredentials()`.
|
|
253
|
+
- Return the configured `OAuth2Client`.
|
|
254
|
+
|
|
255
|
+
4. `isLoggedIn(): boolean`
|
|
256
|
+
- Synchronous. Check if credentials file exists AND contains `refresh_token`.
|
|
257
|
+
|
|
258
|
+
5. `async clearCredentials(): Promise<void>`
|
|
259
|
+
- Delete credentials file if it exists (`fs.unlink`).
|
|
260
|
+
- Log confirmation.
|
|
261
|
+
|
|
262
|
+
6. `async getAccessToken(): Promise<string>`
|
|
263
|
+
- Convenience method. Calls `getAuthenticatedClient()`, then
|
|
264
|
+
`client.getAccessToken()`. Returns the token string.
|
|
265
|
+
- This is the method `Embedder` and `SemanticEnricher` will use
|
|
266
|
+
to get a bearer token when `GEMINI_API_KEY` is absent.
|
|
267
|
+
|
|
268
|
+
**Imports:** `fs` (node:fs and node:fs/promises), `path`, `OAuth2Client`
|
|
269
|
+
from `google-auth-library`, `NomosError`, `AuthCredentials`, `NomosConfig`.
|
|
270
|
+
|
|
271
|
+
**Security:** Never log `access_token`, `refresh_token`, or `client_secret`.
|
|
272
|
+
Use `logger.info('[nomos:auth:info] Credentials saved.')` style messages.
|
|
273
|
+
inputs:
|
|
274
|
+
- "AuthCredentials interface from step 1.2"
|
|
275
|
+
- "NomosConfig['auth'] type from step 1.2 + 1.4"
|
|
276
|
+
- "NomosError auth codes from step 1.3"
|
|
277
|
+
outputs:
|
|
278
|
+
- "AuthManager class with full token lifecycle management"
|
|
279
|
+
validation: |
|
|
280
|
+
`npx tsc --noEmit` passes. All methods have correct return types.
|
|
281
|
+
No `console.log` calls — only `logger.*`.
|
|
282
|
+
depends_on: ["1.2", "1.3", "1.4", "1.5"]
|
|
283
|
+
can_parallel: false
|
|
284
|
+
risk_level: "medium"
|
|
285
|
+
rollback: |
|
|
286
|
+
Delete src/core/auth/manager.ts.
|
|
287
|
+
|
|
288
|
+
- step_id: "2.2"
|
|
289
|
+
title: "Implement OAuth loopback server"
|
|
290
|
+
action: "CREATE"
|
|
291
|
+
file_path: "src/core/auth/server.ts"
|
|
292
|
+
description: |
|
|
293
|
+
Create a single exported function:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
export async function startLoopbackServer(
|
|
297
|
+
oauth2Client: OAuth2Client,
|
|
298
|
+
scopes: string[],
|
|
299
|
+
port: number,
|
|
300
|
+
logger: Logger,
|
|
301
|
+
): Promise<AuthCredentials>
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
**Implementation details:**
|
|
305
|
+
|
|
306
|
+
1. **Server creation:** Use Node.js built-in `http.createServer()`.
|
|
307
|
+
|
|
308
|
+
2. **Port binding:** Attempt to listen on `port` (default 3000 from config).
|
|
309
|
+
If EADDRINUSE, try `listen(0)` for a random available port.
|
|
310
|
+
Log the actual port used.
|
|
311
|
+
|
|
312
|
+
3. **Auth URL generation:**
|
|
313
|
+
```typescript
|
|
314
|
+
const redirectUri = `http://localhost:${actualPort}`;
|
|
315
|
+
oauth2Client.redirectUri = redirectUri; // must be set before generateAuthUrl
|
|
316
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
317
|
+
access_type: 'offline',
|
|
318
|
+
scope: scopes,
|
|
319
|
+
redirect_uri: redirectUri,
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
4. **Browser launch:** `import open from 'open'`. Call `await open(authUrl)`.
|
|
324
|
+
Wrap in try/catch — if browser launch fails (headless), log the URL:
|
|
325
|
+
`logger.info('[nomos:auth:info] Open this URL in your browser: <url>')`.
|
|
326
|
+
|
|
327
|
+
5. **Callback handler:** On incoming GET request:
|
|
328
|
+
- Parse `req.url` for `code` query parameter.
|
|
329
|
+
- If no code, respond 400 and continue listening.
|
|
330
|
+
- Exchange code: `const { tokens } = await oauth2Client.getToken(code)`.
|
|
331
|
+
- Respond with success HTML: simple page saying "Login successful! You can close this tab."
|
|
332
|
+
- Map tokens to `AuthCredentials` shape and resolve the Promise.
|
|
333
|
+
|
|
334
|
+
6. **Timeout:** 120-second timeout via `setTimeout`. On timeout:
|
|
335
|
+
- Destroy all tracked sockets.
|
|
336
|
+
- `server.close()`.
|
|
337
|
+
- Reject with `NomosError('auth_login_failed', 'Login timed out after 120 seconds.')`.
|
|
338
|
+
|
|
339
|
+
7. **Socket tracking:** Maintain `const sockets = new Set<net.Socket>()`.
|
|
340
|
+
Track on `server.on('connection', socket => sockets.add(socket))`.
|
|
341
|
+
On cleanup, iterate and `socket.destroy()`.
|
|
342
|
+
|
|
343
|
+
8. **Cleanup:** After receiving callback OR on timeout, destroy all sockets
|
|
344
|
+
and close server. The function must never leave a dangling server.
|
|
345
|
+
|
|
346
|
+
**Imports:** `http`, `net`, `url` (node:url for URL parsing), `open`,
|
|
347
|
+
`OAuth2Client` from google-auth-library.
|
|
348
|
+
inputs:
|
|
349
|
+
- "OAuth2Client instance (created by caller with client_id/client_secret)"
|
|
350
|
+
- "AuthCredentials interface from step 1.2"
|
|
351
|
+
outputs:
|
|
352
|
+
- "AuthCredentials object with access_token, refresh_token, expiry_date, etc."
|
|
353
|
+
validation: |
|
|
354
|
+
`npx tsc --noEmit` passes. Function signature matches spec.
|
|
355
|
+
Server cleanup is guaranteed (finally block or equivalent).
|
|
356
|
+
depends_on: ["1.2", "1.3", "1.5"]
|
|
357
|
+
can_parallel: true
|
|
358
|
+
risk_level: "high"
|
|
359
|
+
rollback: |
|
|
360
|
+
Delete src/core/auth/server.ts.
|
|
361
|
+
|
|
362
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
363
|
+
# PHASE 3: CLI COMMANDS — arc auth login/logout/status
|
|
364
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
365
|
+
|
|
366
|
+
- step_id: "3.1"
|
|
367
|
+
title: "Create arc auth command group"
|
|
368
|
+
action: "CREATE"
|
|
369
|
+
file_path: "src/commands/auth.ts"
|
|
370
|
+
description: |
|
|
371
|
+
Create `registerAuthCommand(program: Command): void` following the exact
|
|
372
|
+
same pattern as every other command in `src/commands/`.
|
|
373
|
+
|
|
374
|
+
**Structure:**
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import type { Command } from 'commander';
|
|
378
|
+
import { OAuth2Client } from 'google-auth-library';
|
|
379
|
+
import { loadConfig } from '../core/config.js';
|
|
380
|
+
import { createLogger } from '../core/logger.js';
|
|
381
|
+
import { NomosError } from '../core/errors.js';
|
|
382
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
383
|
+
import { startLoopbackServer } from '../core/auth/server.js';
|
|
384
|
+
|
|
385
|
+
const SCOPES = ['https://www.googleapis.com/auth/generative-language'];
|
|
386
|
+
|
|
387
|
+
export function registerAuthCommand(program: Command): void {
|
|
388
|
+
const auth = program
|
|
389
|
+
.command('auth')
|
|
390
|
+
.description('Manage Google OAuth authentication');
|
|
391
|
+
|
|
392
|
+
// ─── arc auth login ──────────────────────────────────────────────
|
|
393
|
+
auth
|
|
394
|
+
.command('login')
|
|
395
|
+
.description('Authenticate with Google via OAuth 2.0')
|
|
396
|
+
.option('--client-id <id>', 'Google OAuth client ID')
|
|
397
|
+
.option('--client-secret <secret>', 'Google OAuth client secret')
|
|
398
|
+
.action(async (opts) => { ... });
|
|
399
|
+
|
|
400
|
+
// ─── arc auth logout ─────────────────────────────────────────────
|
|
401
|
+
auth
|
|
402
|
+
.command('logout')
|
|
403
|
+
.description('Remove stored OAuth credentials')
|
|
404
|
+
.action(async () => { ... });
|
|
405
|
+
|
|
406
|
+
// ─── arc auth status ─────────────────────────────────────────────
|
|
407
|
+
auth
|
|
408
|
+
.command('status')
|
|
409
|
+
.description('Show current authentication state')
|
|
410
|
+
.action(async () => { ... });
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**`arc auth login` flow:**
|
|
415
|
+
1. Load config via `loadConfig()`.
|
|
416
|
+
2. Resolve `client_id` and `client_secret`:
|
|
417
|
+
- Priority: CLI flags `--client-id`/`--client-secret` > config `auth.client_id`/`auth.client_secret`.
|
|
418
|
+
- If neither available, throw `NomosError('auth_client_config_missing', ...)` with
|
|
419
|
+
message explaining how to provide them.
|
|
420
|
+
3. Create `OAuth2Client` with `{ clientId, clientSecret }`.
|
|
421
|
+
4. Create `AuthManager` with config.auth and logger.
|
|
422
|
+
5. Call `startLoopbackServer(oauth2Client, SCOPES, config.auth.redirect_port, logger)`.
|
|
423
|
+
6. On success: `authManager.saveCredentials(tokens)`.
|
|
424
|
+
7. Print: `✓ Logged in successfully. Credentials saved to <path>`.
|
|
425
|
+
|
|
426
|
+
**`arc auth logout` flow:**
|
|
427
|
+
1. Load config, create AuthManager.
|
|
428
|
+
2. Call `authManager.clearCredentials()`.
|
|
429
|
+
3. Print: `✓ Logged out. Credentials removed.`
|
|
430
|
+
|
|
431
|
+
**`arc auth status` flow:**
|
|
432
|
+
1. Load config, create AuthManager.
|
|
433
|
+
2. Check `authManager.isLoggedIn()`.
|
|
434
|
+
3. If logged in: load credentials, display token expiry (human-readable).
|
|
435
|
+
4. Check `process.env['GEMINI_API_KEY']` — display whether API key is set.
|
|
436
|
+
5. Print credential chain resolution order being used.
|
|
437
|
+
|
|
438
|
+
**Error handling:** Wrap each action in try/catch, matching the pattern
|
|
439
|
+
in `src/commands/index.ts` (lines 67-75): NomosError → `console.error` + exit 1.
|
|
440
|
+
inputs:
|
|
441
|
+
- "AuthManager from step 2.1"
|
|
442
|
+
- "startLoopbackServer from step 2.2"
|
|
443
|
+
- "Config with auth section from step 1.4"
|
|
444
|
+
outputs:
|
|
445
|
+
- "`arc auth login`, `arc auth logout`, `arc auth status` commands functional"
|
|
446
|
+
validation: |
|
|
447
|
+
`npx tsc --noEmit` passes.
|
|
448
|
+
`npx tsx src/cli.ts auth --help` lists login, logout, status.
|
|
449
|
+
`npx tsx src/cli.ts auth login --help` shows --client-id and --client-secret options.
|
|
450
|
+
depends_on: ["2.1", "2.2"]
|
|
451
|
+
can_parallel: false
|
|
452
|
+
risk_level: "medium"
|
|
453
|
+
rollback: |
|
|
454
|
+
Delete src/commands/auth.ts.
|
|
455
|
+
|
|
456
|
+
- step_id: "3.2"
|
|
457
|
+
title: "Register auth command in CLI entrypoint"
|
|
458
|
+
action: "MODIFY"
|
|
459
|
+
file_path: "src/cli.ts"
|
|
460
|
+
description: |
|
|
461
|
+
1. Add import at the top (after the existing command imports, around line 16):
|
|
462
|
+
```typescript
|
|
463
|
+
import { registerAuthCommand } from './commands/auth.js';
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
2. Add `registerAuthCommand` to the registration array (after
|
|
467
|
+
`registerSearchCommand`, around line 59):
|
|
468
|
+
```typescript
|
|
469
|
+
registerAuthCommand,
|
|
470
|
+
```
|
|
471
|
+
inputs:
|
|
472
|
+
- "registerAuthCommand function from step 3.1"
|
|
473
|
+
outputs:
|
|
474
|
+
- "`arc auth` appears in `arc --help` output"
|
|
475
|
+
validation: |
|
|
476
|
+
`npx tsc --noEmit` passes.
|
|
477
|
+
`npx tsx src/cli.ts --help` lists 'auth' command.
|
|
478
|
+
depends_on: ["3.1"]
|
|
479
|
+
can_parallel: false
|
|
480
|
+
risk_level: "low"
|
|
481
|
+
rollback: |
|
|
482
|
+
Remove the import line and the array entry from src/cli.ts.
|
|
483
|
+
|
|
484
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
485
|
+
# PHASE 4: CREDENTIAL CHAIN INTEGRATION
|
|
486
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
487
|
+
|
|
488
|
+
- step_id: "4.1"
|
|
489
|
+
title: "Add async factory method to Embedder with credential chain"
|
|
490
|
+
action: "MODIFY"
|
|
491
|
+
file_path: "src/search/embedder.ts"
|
|
492
|
+
description: |
|
|
493
|
+
The Embedder constructor is currently synchronous and reads GEMINI_API_KEY
|
|
494
|
+
from process.env. The OAuth fallback requires async operations (file I/O,
|
|
495
|
+
token refresh). To avoid breaking the constructor contract, we introduce
|
|
496
|
+
a static factory method while keeping the constructor functional for the
|
|
497
|
+
API key path.
|
|
498
|
+
|
|
499
|
+
**Changes:**
|
|
500
|
+
|
|
501
|
+
1. Add a private constructor overload that accepts an API key string
|
|
502
|
+
directly (instead of reading from env):
|
|
503
|
+
|
|
504
|
+
Change the constructor to accept an optional `apiKey` parameter:
|
|
505
|
+
```typescript
|
|
506
|
+
constructor(
|
|
507
|
+
private readonly config: NomosConfig['search'],
|
|
508
|
+
private readonly logger: Logger,
|
|
509
|
+
apiKey?: string,
|
|
510
|
+
) {
|
|
511
|
+
const key = apiKey ?? process.env['GEMINI_API_KEY'];
|
|
512
|
+
if (!key) {
|
|
513
|
+
throw new NomosError(
|
|
514
|
+
'search_api_key_missing',
|
|
515
|
+
'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
this.client = new GoogleGenerativeAI(key);
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
2. Add a static async factory method:
|
|
523
|
+
```typescript
|
|
524
|
+
static async create(
|
|
525
|
+
config: NomosConfig['search'],
|
|
526
|
+
logger: Logger,
|
|
527
|
+
authManager?: AuthManager | null,
|
|
528
|
+
): Promise<Embedder> {
|
|
529
|
+
// Priority 1: GEMINI_API_KEY
|
|
530
|
+
const envKey = process.env['GEMINI_API_KEY'];
|
|
531
|
+
if (envKey) {
|
|
532
|
+
return new Embedder(config, logger, envKey);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Priority 2: OAuth credentials
|
|
536
|
+
if (authManager?.isLoggedIn()) {
|
|
537
|
+
const token = await authManager.getAccessToken();
|
|
538
|
+
return new Embedder(config, logger, token);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Priority 3: Neither — throw
|
|
542
|
+
throw new NomosError(
|
|
543
|
+
'search_api_key_missing',
|
|
544
|
+
'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
3. Add import for AuthManager:
|
|
550
|
+
```typescript
|
|
551
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Backward compatibility:** The existing `new Embedder(config, logger)`
|
|
555
|
+
constructor still works for any caller that doesn't need OAuth. The factory
|
|
556
|
+
method is the new preferred path.
|
|
557
|
+
inputs:
|
|
558
|
+
- "Existing Embedder class (src/search/embedder.ts)"
|
|
559
|
+
- "AuthManager from step 2.1"
|
|
560
|
+
outputs:
|
|
561
|
+
- "Embedder.create() factory method available"
|
|
562
|
+
- "Existing constructor still works for API key path"
|
|
563
|
+
validation: |
|
|
564
|
+
`npx tsc --noEmit` passes.
|
|
565
|
+
`new Embedder(config, logger)` still compiles (backward compat).
|
|
566
|
+
`Embedder.create(config, logger, authManager)` compiles.
|
|
567
|
+
depends_on: ["2.1"]
|
|
568
|
+
can_parallel: true
|
|
569
|
+
risk_level: "high"
|
|
570
|
+
rollback: |
|
|
571
|
+
Revert src/search/embedder.ts to original constructor.
|
|
572
|
+
Remove AuthManager import and create() method.
|
|
573
|
+
|
|
574
|
+
- step_id: "4.2"
|
|
575
|
+
title: "Add credential chain to SemanticEnricher"
|
|
576
|
+
action: "MODIFY"
|
|
577
|
+
file_path: "src/core/graph/enricher.ts"
|
|
578
|
+
description: |
|
|
579
|
+
Apply the same credential chain pattern to SemanticEnricher.
|
|
580
|
+
|
|
581
|
+
**Changes:**
|
|
582
|
+
|
|
583
|
+
1. Modify constructor to accept optional `apiKey` parameter:
|
|
584
|
+
```typescript
|
|
585
|
+
constructor(
|
|
586
|
+
private readonly projectRoot: string,
|
|
587
|
+
private readonly config: NomosConfig['graph'],
|
|
588
|
+
private readonly logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
|
|
589
|
+
apiKey?: string,
|
|
590
|
+
) {
|
|
591
|
+
const key = apiKey ?? process.env['GEMINI_API_KEY'];
|
|
592
|
+
if (!key && config.ai_enrichment) {
|
|
593
|
+
throw new NomosError(
|
|
594
|
+
'graph_ai_key_missing',
|
|
595
|
+
'No credentials found. Set GEMINI_API_KEY or run: arc auth login',
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
this.client = new GoogleGenerativeAI(key ?? '');
|
|
599
|
+
this.limit = pLimit(config.ai_concurrency);
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
2. Add static async factory method:
|
|
604
|
+
```typescript
|
|
605
|
+
static async create(
|
|
606
|
+
projectRoot: string,
|
|
607
|
+
config: NomosConfig['graph'],
|
|
608
|
+
logger: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
|
|
609
|
+
authManager?: AuthManager | null,
|
|
610
|
+
): Promise<SemanticEnricher> {
|
|
611
|
+
const envKey = process.env['GEMINI_API_KEY'];
|
|
612
|
+
if (envKey) {
|
|
613
|
+
return new SemanticEnricher(projectRoot, config, logger, envKey);
|
|
614
|
+
}
|
|
615
|
+
if (authManager?.isLoggedIn()) {
|
|
616
|
+
const token = await authManager.getAccessToken();
|
|
617
|
+
return new SemanticEnricher(projectRoot, config, logger, token);
|
|
618
|
+
}
|
|
619
|
+
// No key and enrichment enabled — let constructor throw
|
|
620
|
+
return new SemanticEnricher(projectRoot, config, logger);
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
3. Add import for AuthManager:
|
|
625
|
+
```typescript
|
|
626
|
+
import { AuthManager } from '../auth/manager.js';
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Backward compatibility:** `new SemanticEnricher(root, config, logger)`
|
|
630
|
+
still works exactly as before.
|
|
631
|
+
inputs:
|
|
632
|
+
- "Existing SemanticEnricher class (src/core/graph/enricher.ts)"
|
|
633
|
+
- "AuthManager from step 2.1"
|
|
634
|
+
outputs:
|
|
635
|
+
- "SemanticEnricher.create() factory method available"
|
|
636
|
+
- "Existing constructor still works"
|
|
637
|
+
validation: |
|
|
638
|
+
`npx tsc --noEmit` passes.
|
|
639
|
+
`new SemanticEnricher(root, config, logger)` still compiles.
|
|
640
|
+
depends_on: ["2.1"]
|
|
641
|
+
can_parallel: true
|
|
642
|
+
risk_level: "high"
|
|
643
|
+
rollback: |
|
|
644
|
+
Revert src/core/graph/enricher.ts to original constructor.
|
|
645
|
+
|
|
646
|
+
- step_id: "4.3"
|
|
647
|
+
title: "Update SearchIndexer to use Embedder.create() factory"
|
|
648
|
+
action: "MODIFY"
|
|
649
|
+
file_path: "src/search/indexer.ts"
|
|
650
|
+
description: |
|
|
651
|
+
The SearchIndexer uses a lazy `get embedder()` accessor that calls
|
|
652
|
+
`new Embedder(config, logger)`. Update it to support the credential chain.
|
|
653
|
+
|
|
654
|
+
**Changes:**
|
|
655
|
+
|
|
656
|
+
1. Add `AuthManager` to constructor parameters (optional):
|
|
657
|
+
```typescript
|
|
658
|
+
constructor(
|
|
659
|
+
private readonly projectRoot: string,
|
|
660
|
+
private readonly config: NomosConfig,
|
|
661
|
+
private readonly logger: Logger,
|
|
662
|
+
private readonly authManager?: AuthManager | null,
|
|
663
|
+
)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
2. Change the lazy embedder accessor to async. Replace:
|
|
667
|
+
```typescript
|
|
668
|
+
private get embedder(): Embedder {
|
|
669
|
+
if (!this._embedder) {
|
|
670
|
+
this._embedder = new Embedder(this.config.search, this.logger);
|
|
671
|
+
}
|
|
672
|
+
return this._embedder;
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
With:
|
|
676
|
+
```typescript
|
|
677
|
+
private async getEmbedder(): Promise<Embedder> {
|
|
678
|
+
if (!this._embedder) {
|
|
679
|
+
this._embedder = await Embedder.create(
|
|
680
|
+
this.config.search, this.logger, this.authManager,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return this._embedder;
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
3. Update all call sites within indexer.ts:
|
|
688
|
+
- `void this.embedder;` → `await this.getEmbedder();` (pre-check)
|
|
689
|
+
- `this.embedder.embedBatch(...)` → `(await this.getEmbedder()).embedBatch(...)`
|
|
690
|
+
|
|
691
|
+
**Backward compatibility:** Callers that don't pass `authManager` get the
|
|
692
|
+
same behavior as before (API key only).
|
|
693
|
+
inputs:
|
|
694
|
+
- "Existing SearchIndexer class (src/search/indexer.ts)"
|
|
695
|
+
- "Embedder.create() from step 4.1"
|
|
696
|
+
outputs:
|
|
697
|
+
- "SearchIndexer uses credential chain when authManager is provided"
|
|
698
|
+
validation: |
|
|
699
|
+
`npx tsc --noEmit` passes.
|
|
700
|
+
`new SearchIndexer(root, config, logger)` still compiles (authManager optional).
|
|
701
|
+
depends_on: ["4.1"]
|
|
702
|
+
can_parallel: false
|
|
703
|
+
risk_level: "medium"
|
|
704
|
+
rollback: |
|
|
705
|
+
Revert the lazy accessor and constructor changes in indexer.ts.
|
|
706
|
+
|
|
707
|
+
- step_id: "4.4"
|
|
708
|
+
title: "Update QueryEngine to use Embedder.create() factory"
|
|
709
|
+
action: "MODIFY"
|
|
710
|
+
file_path: "src/search/query-engine.ts"
|
|
711
|
+
description: |
|
|
712
|
+
Same pattern as step 4.3. The QueryEngine has an identical lazy `get embedder()`
|
|
713
|
+
accessor.
|
|
714
|
+
|
|
715
|
+
**Changes:**
|
|
716
|
+
|
|
717
|
+
1. Add `AuthManager` to constructor (optional):
|
|
718
|
+
```typescript
|
|
719
|
+
constructor(
|
|
720
|
+
private readonly projectRoot: string,
|
|
721
|
+
private readonly config: NomosConfig,
|
|
722
|
+
private readonly logger: Logger,
|
|
723
|
+
private readonly authManager?: AuthManager | null,
|
|
724
|
+
)
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
2. Change lazy accessor to async `getEmbedder()`.
|
|
728
|
+
|
|
729
|
+
3. Update `this.embedder.embedOne(query.trim())` in `search()` method to
|
|
730
|
+
`(await this.getEmbedder()).embedOne(query.trim())`.
|
|
731
|
+
inputs:
|
|
732
|
+
- "Existing QueryEngine class (src/search/query-engine.ts)"
|
|
733
|
+
- "Embedder.create() from step 4.1"
|
|
734
|
+
outputs:
|
|
735
|
+
- "QueryEngine uses credential chain when authManager is provided"
|
|
736
|
+
validation: |
|
|
737
|
+
`npx tsc --noEmit` passes.
|
|
738
|
+
depends_on: ["4.1"]
|
|
739
|
+
can_parallel: true
|
|
740
|
+
risk_level: "medium"
|
|
741
|
+
rollback: |
|
|
742
|
+
Revert query-engine.ts changes.
|
|
743
|
+
|
|
744
|
+
- step_id: "4.5"
|
|
745
|
+
title: "Update MapPipeline to use SemanticEnricher.create() factory"
|
|
746
|
+
action: "MODIFY"
|
|
747
|
+
file_path: "src/core/graph/pipeline.ts"
|
|
748
|
+
description: |
|
|
749
|
+
In the `run()` method, the enricher is instantiated at line 146:
|
|
750
|
+
```typescript
|
|
751
|
+
const enricher = new SemanticEnricher(this.projectRoot, this.config.graph, this.logger);
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Changes:**
|
|
755
|
+
|
|
756
|
+
1. Add `authManager` to MapPipeline constructor (optional):
|
|
757
|
+
```typescript
|
|
758
|
+
constructor(
|
|
759
|
+
private readonly config: NomosConfig,
|
|
760
|
+
private readonly projectRoot: string,
|
|
761
|
+
private readonly logger: Logger,
|
|
762
|
+
private readonly authManager?: AuthManager | null,
|
|
763
|
+
)
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
2. Replace direct instantiation with factory:
|
|
767
|
+
```typescript
|
|
768
|
+
const enricher = await SemanticEnricher.create(
|
|
769
|
+
this.projectRoot, this.config.graph, this.logger, this.authManager,
|
|
770
|
+
);
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
3. Add import:
|
|
774
|
+
```typescript
|
|
775
|
+
import { AuthManager } from '../auth/manager.js';
|
|
776
|
+
```
|
|
777
|
+
(Note: relative path from `src/core/graph/` to `src/core/auth/` is `../auth/`)
|
|
778
|
+
inputs:
|
|
779
|
+
- "Existing MapPipeline class (src/core/graph/pipeline.ts)"
|
|
780
|
+
- "SemanticEnricher.create() from step 4.2"
|
|
781
|
+
outputs:
|
|
782
|
+
- "MapPipeline passes authManager to SemanticEnricher"
|
|
783
|
+
validation: |
|
|
784
|
+
`npx tsc --noEmit` passes.
|
|
785
|
+
depends_on: ["4.2"]
|
|
786
|
+
can_parallel: false
|
|
787
|
+
risk_level: "medium"
|
|
788
|
+
rollback: |
|
|
789
|
+
Revert pipeline.ts to direct `new SemanticEnricher(...)` instantiation.
|
|
790
|
+
|
|
791
|
+
- step_id: "4.6"
|
|
792
|
+
title: "Wire AuthManager into command call sites"
|
|
793
|
+
action: "MODIFY"
|
|
794
|
+
file_path: "src/commands/index.ts"
|
|
795
|
+
description: |
|
|
796
|
+
Update the `arc index` command to create an `AuthManager` and pass it
|
|
797
|
+
to `SearchIndexer`.
|
|
798
|
+
|
|
799
|
+
**Changes to src/commands/index.ts:**
|
|
800
|
+
|
|
801
|
+
1. Add import:
|
|
802
|
+
```typescript
|
|
803
|
+
import { AuthManager } from '../core/auth/manager.js';
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
2. After `const logger = createLogger(...)`, create AuthManager:
|
|
807
|
+
```typescript
|
|
808
|
+
const authManager = new AuthManager(config.auth, logger);
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
3. Pass to SearchIndexer:
|
|
812
|
+
```typescript
|
|
813
|
+
const indexer = new SearchIndexer(projectRoot, config, logger, authManager);
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
**Also update src/commands/search.ts:**
|
|
817
|
+
|
|
818
|
+
Same pattern — create `AuthManager` and pass to `QueryEngine` constructor.
|
|
819
|
+
|
|
820
|
+
**Also update src/commands/map.ts:**
|
|
821
|
+
|
|
822
|
+
Same pattern — create `AuthManager` and pass to `MapPipeline` constructor.
|
|
823
|
+
inputs:
|
|
824
|
+
- "AuthManager from step 2.1"
|
|
825
|
+
- "Updated SearchIndexer from step 4.3"
|
|
826
|
+
- "Updated QueryEngine from step 4.4"
|
|
827
|
+
- "Updated MapPipeline from step 4.5"
|
|
828
|
+
outputs:
|
|
829
|
+
- "All Gemini-dependent commands use the credential chain"
|
|
830
|
+
validation: |
|
|
831
|
+
`npx tsc --noEmit` passes.
|
|
832
|
+
`arc index`, `arc search`, `arc map` commands work with GEMINI_API_KEY set
|
|
833
|
+
(backward compat).
|
|
834
|
+
depends_on: ["4.3", "4.4", "4.5"]
|
|
835
|
+
can_parallel: false
|
|
836
|
+
risk_level: "medium"
|
|
837
|
+
rollback: |
|
|
838
|
+
Remove AuthManager creation and parameter passing from all three command files.
|
|
839
|
+
|
|
840
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
841
|
+
# PHASE 5: SECURITY — Sanitization Coverage
|
|
842
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
843
|
+
|
|
844
|
+
- step_id: "5.1"
|
|
845
|
+
title: "Verify sanitize.ts covers auth tokens in env deny list"
|
|
846
|
+
action: "MODIFY"
|
|
847
|
+
file_path: "src/utils/sanitize.ts"
|
|
848
|
+
description: |
|
|
849
|
+
Review the `ALWAYS_DENY` regex array in `sanitizeEnv()` (line 57-61).
|
|
850
|
+
The existing pattern already covers `GOOGLE_.*?(KEY|SECRET|TOKEN|...)`.
|
|
851
|
+
This would catch env vars like `GOOGLE_ACCESS_TOKEN` but NOT the
|
|
852
|
+
internal OAuth tokens (which are in a JSON file, not env vars).
|
|
853
|
+
|
|
854
|
+
**No code change needed if** the existing patterns already cover Google
|
|
855
|
+
credential env vars. Verify and confirm.
|
|
856
|
+
|
|
857
|
+
**If the patterns do NOT cover a scenario where tokens could leak into
|
|
858
|
+
env vars**, add coverage. But per the design, OAuth tokens live in
|
|
859
|
+
`~/.nomos/credentials.json` (file, not env) and are never set as env
|
|
860
|
+
vars by the auth module.
|
|
861
|
+
|
|
862
|
+
**Action:** Read the file, verify coverage, document finding. No modification
|
|
863
|
+
expected unless a gap is found.
|
|
864
|
+
inputs:
|
|
865
|
+
- "Existing sanitize.ts ALWAYS_DENY patterns"
|
|
866
|
+
outputs:
|
|
867
|
+
- "Confirmation that auth tokens are not leaked through env sanitization gaps"
|
|
868
|
+
validation: |
|
|
869
|
+
Manual review confirms ALWAYS_DENY covers GOOGLE_* secrets.
|
|
870
|
+
Auth tokens stored in file only — no env var leak path.
|
|
871
|
+
depends_on: []
|
|
872
|
+
can_parallel: true
|
|
873
|
+
risk_level: "low"
|
|
874
|
+
rollback: |
|
|
875
|
+
N/A — review-only step.
|
|
876
|
+
|
|
877
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
878
|
+
# PHASE 6: VALIDATION & TESTING
|
|
879
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
880
|
+
|
|
881
|
+
- step_id: "6.1"
|
|
882
|
+
title: "TypeScript compilation check"
|
|
883
|
+
action: "TEST"
|
|
884
|
+
file_path: ""
|
|
885
|
+
description: |
|
|
886
|
+
Run `npx tsc --noEmit` (or `npm run lint`) to verify all new and
|
|
887
|
+
modified files compile without errors. This catches:
|
|
888
|
+
- Missing imports
|
|
889
|
+
- Type mismatches between AuthCredentials, NomosConfig, NomosErrorCode
|
|
890
|
+
- Incorrect async/await usage in factory methods
|
|
891
|
+
inputs:
|
|
892
|
+
- "All files from steps 1.1 through 5.1"
|
|
893
|
+
outputs:
|
|
894
|
+
- "Zero TypeScript errors"
|
|
895
|
+
validation: |
|
|
896
|
+
`npm run lint` exits 0.
|
|
897
|
+
depends_on: ["1.1", "1.2", "1.3", "1.4", "2.1", "2.2", "3.1", "3.2", "4.1", "4.2", "4.3", "4.4", "4.5", "4.6"]
|
|
898
|
+
can_parallel: false
|
|
899
|
+
risk_level: "low"
|
|
900
|
+
rollback: |
|
|
901
|
+
Fix compilation errors identified.
|
|
902
|
+
|
|
903
|
+
- step_id: "6.2"
|
|
904
|
+
title: "Run existing test suite for regression"
|
|
905
|
+
action: "TEST"
|
|
906
|
+
file_path: ""
|
|
907
|
+
description: |
|
|
908
|
+
Run `npm test` to ensure all existing tests still pass. Key test files
|
|
909
|
+
to watch:
|
|
910
|
+
- `src/search/__tests__/embedder.test.ts` — may need update if constructor
|
|
911
|
+
signature changed (apiKey parameter added)
|
|
912
|
+
- `src/core/graph/__tests__/enricher.test.ts` — same concern
|
|
913
|
+
- `src/core/graph/__tests__/pipeline.test.ts` — if MapPipeline constructor
|
|
914
|
+
changed
|
|
915
|
+
- `src/search/__tests__/indexer.test.ts` — if SearchIndexer constructor changed
|
|
916
|
+
|
|
917
|
+
If tests fail due to constructor signature changes (new optional `apiKey`
|
|
918
|
+
or `authManager` params), these are expected and must be fixed:
|
|
919
|
+
- Tests using `new Embedder(config, logger)` → still valid (apiKey is optional)
|
|
920
|
+
- Tests using `new SearchIndexer(root, config, logger)` → still valid (authManager optional)
|
|
921
|
+
|
|
922
|
+
If any test fails for an unexpected reason, investigate before proceeding.
|
|
923
|
+
inputs:
|
|
924
|
+
- "All source changes from previous steps"
|
|
925
|
+
outputs:
|
|
926
|
+
- "All existing tests pass (or failures are expected and documented)"
|
|
927
|
+
validation: |
|
|
928
|
+
`npm test` exits 0 (or with only expected, documented failures).
|
|
929
|
+
depends_on: ["6.1"]
|
|
930
|
+
can_parallel: false
|
|
931
|
+
risk_level: "medium"
|
|
932
|
+
rollback: |
|
|
933
|
+
Fix test failures. If a test fails due to constructor changes,
|
|
934
|
+
update the test to pass the new optional parameter or verify
|
|
935
|
+
the default behavior hasn't changed.
|
|
936
|
+
|
|
937
|
+
- step_id: "6.3"
|
|
938
|
+
title: "Unit tests for AuthManager"
|
|
939
|
+
action: "CREATE"
|
|
940
|
+
file_path: "src/core/auth/__tests__/manager.test.ts"
|
|
941
|
+
description: |
|
|
942
|
+
Create unit tests for AuthManager covering:
|
|
943
|
+
|
|
944
|
+
1. **saveCredentials** — writes JSON to temp path, verifies file exists
|
|
945
|
+
and contains correct data, verifies file permissions are 0600.
|
|
946
|
+
2. **loadCredentials** — returns parsed object when file exists,
|
|
947
|
+
returns null when file doesn't exist, returns null on invalid JSON.
|
|
948
|
+
3. **isLoggedIn** — true when credentials file has refresh_token,
|
|
949
|
+
false when file missing.
|
|
950
|
+
4. **clearCredentials** — deletes file, doesn't throw if file missing.
|
|
951
|
+
5. **getAccessToken** — throws `auth_not_logged_in` when no credentials.
|
|
952
|
+
|
|
953
|
+
Use `vitest` (project's test runner). Mock the filesystem or use
|
|
954
|
+
a temp directory (`os.tmpdir()`) with cleanup in afterEach.
|
|
955
|
+
|
|
956
|
+
Do NOT mock google-auth-library for unit tests — test only the
|
|
957
|
+
file I/O and credential management logic. Integration with Google
|
|
958
|
+
is tested manually via `arc auth login`.
|
|
959
|
+
inputs:
|
|
960
|
+
- "AuthManager from step 2.1"
|
|
961
|
+
outputs:
|
|
962
|
+
- "Unit tests for AuthManager pass"
|
|
963
|
+
validation: |
|
|
964
|
+
`npx vitest run src/core/auth/__tests__/manager.test.ts` exits 0.
|
|
965
|
+
depends_on: ["2.1"]
|
|
966
|
+
can_parallel: true
|
|
967
|
+
risk_level: "low"
|
|
968
|
+
rollback: |
|
|
969
|
+
Delete test file.
|
|
970
|
+
|
|
971
|
+
- step_id: "6.4"
|
|
972
|
+
title: "Unit tests for loopback server"
|
|
973
|
+
action: "CREATE"
|
|
974
|
+
file_path: "src/core/auth/__tests__/server.test.ts"
|
|
975
|
+
description: |
|
|
976
|
+
Create unit tests for the loopback server covering:
|
|
977
|
+
|
|
978
|
+
1. **Port binding** — server starts on specified port.
|
|
979
|
+
2. **Timeout** — server rejects after 120s timeout (use fake timers
|
|
980
|
+
via `vi.useFakeTimers()`).
|
|
981
|
+
3. **Callback handling** — mock an HTTP GET with `?code=test_code`,
|
|
982
|
+
verify the function resolves (mock `oauth2Client.getToken`).
|
|
983
|
+
4. **Cleanup** — server is closed after callback.
|
|
984
|
+
|
|
985
|
+
Mock `OAuth2Client.getToken()` to return fake tokens.
|
|
986
|
+
Mock `open` to prevent actual browser launch.
|
|
987
|
+
inputs:
|
|
988
|
+
- "startLoopbackServer from step 2.2"
|
|
989
|
+
outputs:
|
|
990
|
+
- "Unit tests for loopback server pass"
|
|
991
|
+
validation: |
|
|
992
|
+
`npx vitest run src/core/auth/__tests__/server.test.ts` exits 0.
|
|
993
|
+
depends_on: ["2.2"]
|
|
994
|
+
can_parallel: true
|
|
995
|
+
risk_level: "low"
|
|
996
|
+
rollback: |
|
|
997
|
+
Delete test file.
|
|
998
|
+
|
|
999
|
+
- step_id: "6.5"
|
|
1000
|
+
title: "Integration test — Embedder credential chain"
|
|
1001
|
+
action: "CREATE"
|
|
1002
|
+
file_path: "src/search/__tests__/embedder-auth.test.ts"
|
|
1003
|
+
description: |
|
|
1004
|
+
Test the credential chain in Embedder.create():
|
|
1005
|
+
|
|
1006
|
+
1. **API key takes priority** — set GEMINI_API_KEY env var, call
|
|
1007
|
+
`Embedder.create(config, logger, authManager)`. Verify Embedder
|
|
1008
|
+
is created using the env var (mock AuthManager, verify it's NOT called).
|
|
1009
|
+
2. **OAuth fallback** — unset GEMINI_API_KEY, mock
|
|
1010
|
+
`authManager.isLoggedIn()` → true, `authManager.getAccessToken()` →
|
|
1011
|
+
'fake-token'. Verify Embedder is created.
|
|
1012
|
+
3. **Neither available** — unset GEMINI_API_KEY, mock
|
|
1013
|
+
`authManager.isLoggedIn()` → false. Verify `search_api_key_missing`
|
|
1014
|
+
error is thrown with message mentioning both `GEMINI_API_KEY` and
|
|
1015
|
+
`arc auth login`.
|
|
1016
|
+
inputs:
|
|
1017
|
+
- "Embedder.create() from step 4.1"
|
|
1018
|
+
- "AuthManager from step 2.1"
|
|
1019
|
+
outputs:
|
|
1020
|
+
- "Credential chain works correctly in all priority scenarios"
|
|
1021
|
+
validation: |
|
|
1022
|
+
`npx vitest run src/search/__tests__/embedder-auth.test.ts` exits 0.
|
|
1023
|
+
depends_on: ["4.1"]
|
|
1024
|
+
can_parallel: true
|
|
1025
|
+
risk_level: "low"
|
|
1026
|
+
rollback: |
|
|
1027
|
+
Delete test file.
|
|
1028
|
+
|
|
1029
|
+
risk_assessment:
|
|
1030
|
+
overall_risk: "high"
|
|
1031
|
+
critical_steps: ["2.2", "4.1", "4.2"]
|
|
1032
|
+
failure_scenarios:
|
|
1033
|
+
- scenario: "Google SDK rejects access_token passed as API key string"
|
|
1034
|
+
impact: "OAuth tokens cannot authenticate Gemini API calls. Entire credential chain is non-functional."
|
|
1035
|
+
mitigation: |
|
|
1036
|
+
Step 4.1 description notes this is Risk 2 from the task spec. Start with
|
|
1037
|
+
spike test: pass access_token as apiKey to GoogleGenerativeAI constructor.
|
|
1038
|
+
If it fails, fall back to custom Authorization header injection (option 3
|
|
1039
|
+
from task spec). The plan's architecture (factory method returning Embedder)
|
|
1040
|
+
supports either approach without further refactoring.
|
|
1041
|
+
- scenario: "Dynamic port breaks Google OAuth redirect URI matching"
|
|
1042
|
+
impact: "OAuth callback fails with 'redirect_uri_mismatch' error."
|
|
1043
|
+
mitigation: |
|
|
1044
|
+
Fixed default port (3000) with configurable override via
|
|
1045
|
+
`config.auth.redirect_port`. Users register `http://localhost:3000` in
|
|
1046
|
+
Google Cloud Console. Fallback to random port only if 3000 is in use —
|
|
1047
|
+
with clear warning that the user must register the actual port.
|
|
1048
|
+
- scenario: "Sync-to-async constructor migration breaks existing callers"
|
|
1049
|
+
impact: "SearchIndexer, QueryEngine, MapPipeline fail to compile or behave differently."
|
|
1050
|
+
mitigation: |
|
|
1051
|
+
Optional apiKey parameter preserves backward compatibility. The `new Embedder(config, logger)`
|
|
1052
|
+
path is unchanged. Factory method is additive. Existing tests that use
|
|
1053
|
+
the constructor directly continue to work. Step 6.2 explicitly validates this.
|
|
1054
|
+
- scenario: "Token refresh fails silently, stale token used for API calls"
|
|
1055
|
+
impact: "Gemini API returns 401, user sees cryptic error."
|
|
1056
|
+
mitigation: |
|
|
1057
|
+
AuthManager.getAuthenticatedClient() checks expiry_date and refreshes
|
|
1058
|
+
proactively. If refresh fails, it throws auth_token_expired with clear
|
|
1059
|
+
message to re-run `arc auth login`.
|
|
1060
|
+
- scenario: "Loopback server hangs, blocking CLI process"
|
|
1061
|
+
impact: "User's terminal is stuck, requires manual kill."
|
|
1062
|
+
mitigation: |
|
|
1063
|
+
120-second hard timeout with socket tracking and forced destroy.
|
|
1064
|
+
Step 2.2 explicitly tracks sockets in a Set and destroys all on cleanup.
|
|
1065
|
+
|
|
1066
|
+
compliance:
|
|
1067
|
+
rules_checked: true
|
|
1068
|
+
violations: []
|
|
1069
|
+
notes: |
|
|
1070
|
+
- JSON source of truth: Token state in ~/.nomos/credentials.json (JSON). ✓
|
|
1071
|
+
- NomosError codes: All auth failures use typed codes. ✓
|
|
1072
|
+
- Commander.js pattern: registerAuthCommand(program) matches existing commands. ✓
|
|
1073
|
+
- Config-driven: Auth config in .nomos-config.json with Zod schema. ✓
|
|
1074
|
+
- Security: client_secret never logged. Sanitize pipeline reviewed. ✓
|
|
1075
|
+
- No global state: AuthManager instantiated per-command. ✓
|
|
1076
|
+
- Winston logging: All auth logs via injected logger. ✓
|
|
1077
|
+
- Open/Closed: Existing constructor signatures preserved; factory methods are additive. ✓
|
|
1078
|
+
- Backward compatibility: GEMINI_API_KEY path unchanged; OAuth is fallback only. ✓
|
|
1079
|
+
|
|
1080
|
+
summary:
|
|
1081
|
+
total_steps: 18
|
|
1082
|
+
estimated_files_changed: 10
|
|
1083
|
+
estimated_files_created: 5
|
|
1084
|
+
approach_justification: |
|
|
1085
|
+
The plan uses additive factory methods (Embedder.create, SemanticEnricher.create)
|
|
1086
|
+
rather than replacing synchronous constructors, preserving full backward
|
|
1087
|
+
compatibility. The credential chain (env var → OAuth → error) is implemented
|
|
1088
|
+
at the factory level, keeping the core Embedder/Enricher classes unaware of
|
|
1089
|
+
auth complexity. AuthManager is instantiated per-command (no singleton), matching
|
|
1090
|
+
the project's existing patterns. The loopback server uses a fixed default port
|
|
1091
|
+
(3000) to simplify Google Cloud Console configuration while supporting fallback
|
|
1092
|
+
to random ports. All new code follows existing conventions: NomosError codes,
|
|
1093
|
+
Zod config schemas, Commander.js registration, Winston logging.
|