pi-harness-runtime 1.1.158 → 1.1.159
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/harness/infisical-config.ts +395 -0
- package/harness/infisical-setup.ts +270 -0
- package/harness/key-loader.ts +129 -29
- package/harness/secret-provider.ts +437 -0
- package/harness/secret-schema.ts +163 -0
- package/harness/varlock-worker.ts +379 -0
- package/index.ts +12 -3
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infisical Configuration Module
|
|
3
|
+
*
|
|
4
|
+
* Manages validated non-secret configuration for Infisical/Varlock integration.
|
|
5
|
+
* Stores metadata at ~/.pi-harness-runtime/infisical/config.json
|
|
6
|
+
*
|
|
7
|
+
* Does NOT store secrets, tokens, or resolved values.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync, rmSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Infisical site URLs
|
|
16
|
+
*/
|
|
17
|
+
export type InfisicalSite = "us" | "eu" | "custom";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Infisical authentication method
|
|
21
|
+
*/
|
|
22
|
+
export type InfisicalAuthMethod = "universal" | "oidc";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Service that can use Infisical secrets
|
|
26
|
+
*/
|
|
27
|
+
export type ServiceName =
|
|
28
|
+
| "infisical" // Infisical provider itself (for logging)
|
|
29
|
+
| "jev" // TypeSafe Jev
|
|
30
|
+
| "honcho" // Honcho memory
|
|
31
|
+
| "qdrant" // Qdrant vector search
|
|
32
|
+
| "telegram" // Telegram bot
|
|
33
|
+
| "embeddings" // OpenAI/embedding provider
|
|
34
|
+
| "planner" // LangChain planner
|
|
35
|
+
| "minimax" // Minimax provider
|
|
36
|
+
| "glm"; // GLM provider
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Secret name mapping for a service
|
|
40
|
+
*/
|
|
41
|
+
export interface ServiceSecretMapping {
|
|
42
|
+
service: ServiceName;
|
|
43
|
+
required: boolean;
|
|
44
|
+
keys: Record<string, string>; // local key -> Infisical secret name
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bootstrap reference type
|
|
49
|
+
*/
|
|
50
|
+
export type BootstrapRef =
|
|
51
|
+
| { type: "env"; name: string } // env:VAR_NAME
|
|
52
|
+
| { type: "file"; path: string }; // file:/absolute/path
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Infisical configuration (non-secret metadata only)
|
|
56
|
+
*/
|
|
57
|
+
export interface InfisicalConfig {
|
|
58
|
+
/** Schema version for future migrations */
|
|
59
|
+
schemaVersion: 1;
|
|
60
|
+
|
|
61
|
+
/** Whether Infisical integration is enabled */
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
|
|
64
|
+
/** Active revision ID */
|
|
65
|
+
activeRevision: string | null;
|
|
66
|
+
|
|
67
|
+
/** Infisical site */
|
|
68
|
+
site: InfisicalSite;
|
|
69
|
+
customSiteUrl?: string; // Only if site === "custom"
|
|
70
|
+
|
|
71
|
+
/** Project and environment */
|
|
72
|
+
projectId: string;
|
|
73
|
+
environment: string; // e.g., "production", "development"
|
|
74
|
+
secretPath: string; // e.g., "/harness"
|
|
75
|
+
|
|
76
|
+
/** Authentication method */
|
|
77
|
+
authMethod: InfisicalAuthMethod;
|
|
78
|
+
|
|
79
|
+
/** Bootstrap reference (not actual credentials) */
|
|
80
|
+
bootstrapRef: BootstrapRef;
|
|
81
|
+
|
|
82
|
+
/** Enabled services and their secret mappings */
|
|
83
|
+
services: ServiceSecretMapping[];
|
|
84
|
+
|
|
85
|
+
/** Source policy */
|
|
86
|
+
sourcePolicy: {
|
|
87
|
+
/** Use remote as authoritative for selected services */
|
|
88
|
+
remoteAuthoritative: boolean;
|
|
89
|
+
/** Allow environment variable overrides */
|
|
90
|
+
envOverride: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Last resolution timestamp */
|
|
94
|
+
lastResolvedAt: string | null;
|
|
95
|
+
|
|
96
|
+
/** Last error if any */
|
|
97
|
+
lastError: string | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Schema revision directory metadata
|
|
102
|
+
*/
|
|
103
|
+
export interface SchemaRevision {
|
|
104
|
+
revisionId: string;
|
|
105
|
+
createdAt: string;
|
|
106
|
+
environment: string;
|
|
107
|
+
secretPath: string;
|
|
108
|
+
services: ServiceName[];
|
|
109
|
+
validated: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get Infisical config directory
|
|
114
|
+
*/
|
|
115
|
+
export function getInfisicalDir(): string {
|
|
116
|
+
return join(homedir(), ".pi-harness-runtime", "infisical");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get config file path
|
|
121
|
+
*/
|
|
122
|
+
export function getConfigPath(): string {
|
|
123
|
+
return join(getInfisicalDir(), "config.json");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get revisions directory
|
|
128
|
+
*/
|
|
129
|
+
export function getRevisionsDir(): string {
|
|
130
|
+
return join(getInfisicalDir(), "revisions");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Ensure directories exist with proper permissions
|
|
135
|
+
*/
|
|
136
|
+
export function ensureDirectories(): void {
|
|
137
|
+
const baseDir = getInfisicalDir();
|
|
138
|
+
const revDir = getRevisionsDir();
|
|
139
|
+
|
|
140
|
+
if (!existsSync(baseDir)) {
|
|
141
|
+
mkdirSync(baseDir, { recursive: true, mode: 0o700 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!existsSync(revDir)) {
|
|
145
|
+
mkdirSync(revDir, { recursive: true, mode: 0o700 });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Load existing configuration
|
|
151
|
+
*/
|
|
152
|
+
export function loadConfig(): InfisicalConfig | null {
|
|
153
|
+
const configPath = getConfigPath();
|
|
154
|
+
|
|
155
|
+
if (!existsSync(configPath)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const content = readFileSync(configPath, "utf8");
|
|
161
|
+
const config = JSON.parse(content) as InfisicalConfig;
|
|
162
|
+
|
|
163
|
+
// Validate schema version
|
|
164
|
+
if (config.schemaVersion !== 1) {
|
|
165
|
+
console.warn("[infisical-config] Unknown schema version:", config.schemaVersion);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return config;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error("[infisical-config] Failed to load config:", err instanceof Error ? err.message : String(err));
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Save configuration atomically
|
|
178
|
+
*/
|
|
179
|
+
export function saveConfig(config: InfisicalConfig): void {
|
|
180
|
+
ensureDirectories();
|
|
181
|
+
|
|
182
|
+
const configPath = getConfigPath();
|
|
183
|
+
const tempPath = `${configPath}.tmp`;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Write to temp file with restricted permissions
|
|
187
|
+
writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
188
|
+
chmodSync(tempPath, 0o600);
|
|
189
|
+
|
|
190
|
+
// Atomic rename
|
|
191
|
+
writeFileSync(configPath, readFileSync(tempPath));
|
|
192
|
+
unlinkSync(tempPath);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
// Clean up temp file if it exists
|
|
195
|
+
if (existsSync(tempPath)) {
|
|
196
|
+
try { unlinkSync(tempPath); } catch { /* ignore */ }
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create a new revision directory
|
|
204
|
+
*/
|
|
205
|
+
export function createRevision(config: InfisicalConfig): string {
|
|
206
|
+
ensureDirectories();
|
|
207
|
+
|
|
208
|
+
const revId = `rev_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
209
|
+
const revDir = join(getRevisionsDir(), revId);
|
|
210
|
+
|
|
211
|
+
mkdirSync(revDir, { mode: 0o700 });
|
|
212
|
+
|
|
213
|
+
// Save revision metadata
|
|
214
|
+
const revision: SchemaRevision = {
|
|
215
|
+
revisionId: revId,
|
|
216
|
+
createdAt: new Date().toISOString(),
|
|
217
|
+
environment: config.environment,
|
|
218
|
+
secretPath: config.secretPath,
|
|
219
|
+
services: config.services.map(s => s.service),
|
|
220
|
+
validated: false,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
writeFileSync(
|
|
224
|
+
join(revDir, "revision.json"),
|
|
225
|
+
JSON.stringify(revision, null, 2),
|
|
226
|
+
{ mode: 0o600 }
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return revId;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get revision metadata
|
|
234
|
+
*/
|
|
235
|
+
export function getRevision(revisionId: string): SchemaRevision | null {
|
|
236
|
+
const revPath = join(getRevisionsDir(), revisionId, "revision.json");
|
|
237
|
+
|
|
238
|
+
if (!existsSync(revPath)) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const content = readFileSync(revPath, "utf8");
|
|
244
|
+
return JSON.parse(content) as SchemaRevision;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Get schema file path for a revision
|
|
252
|
+
*/
|
|
253
|
+
export function getSchemaPath(revisionId: string): string {
|
|
254
|
+
return join(getRevisionsDir(), revisionId, ".env.schema");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Mark revision as validated
|
|
259
|
+
*/
|
|
260
|
+
export function markRevisionValidated(revisionId: string): void {
|
|
261
|
+
const revDir = join(getRevisionsDir(), revisionId);
|
|
262
|
+
const revPath = join(revDir, "revision.json");
|
|
263
|
+
|
|
264
|
+
if (!existsSync(revPath)) {
|
|
265
|
+
throw new Error(`Revision not found: ${revisionId}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let revision: SchemaRevision;
|
|
269
|
+
try {
|
|
270
|
+
revision = JSON.parse(readFileSync(revPath, "utf8")) as SchemaRevision;
|
|
271
|
+
} catch {
|
|
272
|
+
throw new Error(`Failed to parse revision: ${revisionId}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
revision.validated = true;
|
|
276
|
+
writeFileSync(revPath, JSON.stringify(revision, null, 2), { mode: 0o600 });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Activate a revision
|
|
281
|
+
*/
|
|
282
|
+
export function activateRevision(revisionId: string): void {
|
|
283
|
+
const config = loadConfig();
|
|
284
|
+
if (!config) {
|
|
285
|
+
throw new Error("No config loaded");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const revision = getRevision(revisionId);
|
|
289
|
+
if (!revision) {
|
|
290
|
+
throw new Error(`Revision not found: ${revisionId}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!revision.validated) {
|
|
294
|
+
throw new Error(`Revision not validated: ${revisionId}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Update config with new active revision
|
|
298
|
+
const updatedConfig: InfisicalConfig = {
|
|
299
|
+
...config,
|
|
300
|
+
activeRevision: revisionId,
|
|
301
|
+
lastResolvedAt: new Date().toISOString(),
|
|
302
|
+
lastError: null,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
saveConfig(updatedConfig);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get the site URL from config
|
|
310
|
+
*/
|
|
311
|
+
export function getSiteUrl(config: InfisicalConfig): string {
|
|
312
|
+
switch (config.site) {
|
|
313
|
+
case "us":
|
|
314
|
+
return "https://app.infisical.com";
|
|
315
|
+
case "eu":
|
|
316
|
+
return "https://eu.infisical.com";
|
|
317
|
+
case "custom":
|
|
318
|
+
if (!config.customSiteUrl) {
|
|
319
|
+
throw new Error("Custom site URL not set");
|
|
320
|
+
}
|
|
321
|
+
return config.customSiteUrl;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Validate site URL
|
|
327
|
+
*/
|
|
328
|
+
export function isValidSiteUrl(url: string): boolean {
|
|
329
|
+
try {
|
|
330
|
+
const parsed = new URL(url);
|
|
331
|
+
return parsed.protocol === "https:" && !parsed.username && !parsed.password;
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Clean up old revisions (keep last N)
|
|
339
|
+
*/
|
|
340
|
+
export function cleanupOldRevisions(keepCount: number = 5): void {
|
|
341
|
+
const revDir = getRevisionsDir();
|
|
342
|
+
if (!existsSync(revDir)) return;
|
|
343
|
+
|
|
344
|
+
const config = loadConfig();
|
|
345
|
+
const activeRev = config?.activeRevision;
|
|
346
|
+
|
|
347
|
+
// List all revision directories
|
|
348
|
+
const revisions = existsSync(revDir)
|
|
349
|
+
? readdirSync(revDir).filter(f => f.startsWith("rev_"))
|
|
350
|
+
: [];
|
|
351
|
+
|
|
352
|
+
// Sort by creation time (oldest first)
|
|
353
|
+
revisions.sort((a, b) => {
|
|
354
|
+
const metaA = getRevision(a);
|
|
355
|
+
const metaB = getRevision(b);
|
|
356
|
+
return (metaA?.createdAt ?? "").localeCompare(metaB?.createdAt ?? "");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Delete old revisions (but never delete active)
|
|
360
|
+
const toDelete = revisions.slice(0, Math.max(0, revisions.length - keepCount));
|
|
361
|
+
|
|
362
|
+
for (const rev of toDelete) {
|
|
363
|
+
if (rev === activeRev) continue;
|
|
364
|
+
|
|
365
|
+
const revPath = join(revDir, rev);
|
|
366
|
+
try {
|
|
367
|
+
rmSync(revPath, { recursive: true, force: true });
|
|
368
|
+
} catch {
|
|
369
|
+
// Ignore cleanup errors
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Disable Infisical integration
|
|
376
|
+
*/
|
|
377
|
+
export function disableIntegration(): void {
|
|
378
|
+
const config = loadConfig();
|
|
379
|
+
if (!config) return;
|
|
380
|
+
|
|
381
|
+
const updatedConfig: InfisicalConfig = {
|
|
382
|
+
...config,
|
|
383
|
+
enabled: false,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
saveConfig(updatedConfig);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Check if Infisical is configured and enabled
|
|
391
|
+
*/
|
|
392
|
+
export function isConfigured(): boolean {
|
|
393
|
+
const config = loadConfig();
|
|
394
|
+
return config !== null && config.enabled;
|
|
395
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infisical Setup Command Module
|
|
3
|
+
*
|
|
4
|
+
* /setup-infisical - Interactive configuration wizard
|
|
5
|
+
* /setup-infisical status - Show current status
|
|
6
|
+
* /setup-infisical test - Test resolution
|
|
7
|
+
* /setup-infisical refresh - Refresh secrets
|
|
8
|
+
* /setup-infisical disable - Disable integration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import {
|
|
13
|
+
InfisicalConfig,
|
|
14
|
+
InfisicalSite,
|
|
15
|
+
ServiceName,
|
|
16
|
+
loadConfig,
|
|
17
|
+
saveConfig,
|
|
18
|
+
isConfigured,
|
|
19
|
+
disableIntegration,
|
|
20
|
+
getSiteUrl,
|
|
21
|
+
isValidSiteUrl,
|
|
22
|
+
cleanupOldRevisions,
|
|
23
|
+
} from "./infisical-config.js";
|
|
24
|
+
import {
|
|
25
|
+
SecretProvider,
|
|
26
|
+
getSecretProvider,
|
|
27
|
+
awaitProviderReady,
|
|
28
|
+
} from "./secret-provider.js";
|
|
29
|
+
import {
|
|
30
|
+
getVarlockVersion,
|
|
31
|
+
resolveSecrets,
|
|
32
|
+
} from "./varlock-worker.js";
|
|
33
|
+
import { getDefaultMapping } from "./secret-schema.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse command arguments
|
|
37
|
+
*/
|
|
38
|
+
function parseArgs(args: string): {
|
|
39
|
+
subcommand: string;
|
|
40
|
+
flags: Record<string, boolean | string>;
|
|
41
|
+
} {
|
|
42
|
+
const parts = args.trim().split(/\s+/);
|
|
43
|
+
const subcommand = parts[0] || "status";
|
|
44
|
+
const flags: Record<string, boolean | string> = {};
|
|
45
|
+
|
|
46
|
+
for (let i = 1; i < parts.length; i++) {
|
|
47
|
+
const part = parts[i]!;
|
|
48
|
+
if (part.startsWith("--")) {
|
|
49
|
+
const flagName = part.slice(2);
|
|
50
|
+
if (i + 1 < parts.length && !parts[i + 1]!.startsWith("--")) {
|
|
51
|
+
flags[flagName] = parts[i + 1]!;
|
|
52
|
+
i++;
|
|
53
|
+
} else {
|
|
54
|
+
flags[flagName] = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { subcommand, flags };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handle /setup-infisical status
|
|
64
|
+
*/
|
|
65
|
+
async function handleStatus(ctx: ExtensionContext): Promise<void> {
|
|
66
|
+
const config = loadConfig();
|
|
67
|
+
const provider = getSecretProvider();
|
|
68
|
+
const state = provider.getStatus();
|
|
69
|
+
const varlockVersion = await getVarlockVersion();
|
|
70
|
+
|
|
71
|
+
const lines: string[] = [
|
|
72
|
+
"# Infisical Status",
|
|
73
|
+
"",
|
|
74
|
+
`**Varlock version:** ${varlockVersion ?? "not found"}`,
|
|
75
|
+
`**Provider status:** ${state.status}`,
|
|
76
|
+
"",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
if (!config || !config.enabled) {
|
|
80
|
+
lines.push("Infisical is **not configured**.");
|
|
81
|
+
lines.push("Run `/setup-infisical` to configure.");
|
|
82
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push(`**Site:** ${getSiteUrl(config)}`);
|
|
87
|
+
lines.push(`**Project:** ${config.projectId}`);
|
|
88
|
+
lines.push(`**Environment:** ${config.environment}`);
|
|
89
|
+
lines.push(`**Secret path:** ${config.secretPath}`);
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push("**Enabled services:**");
|
|
92
|
+
|
|
93
|
+
for (const svc of config.services) {
|
|
94
|
+
const required = svc.required ? "(required)" : "(optional)";
|
|
95
|
+
lines.push(`- ${svc.service} ${required}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(`**Active revision:** ${config.activeRevision ?? "none"}`);
|
|
100
|
+
lines.push(`**Last resolved:** ${config.lastResolvedAt ?? "never"}`);
|
|
101
|
+
|
|
102
|
+
if (state.lastError) {
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(`**Last error:** ${state.lastError}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handle /setup-infisical test
|
|
112
|
+
*/
|
|
113
|
+
async function handleTest(ctx: ExtensionContext): Promise<void> {
|
|
114
|
+
ctx.ui.notify("[setup-infisical] Testing Infisical resolution...", "info");
|
|
115
|
+
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
if (!config || !config.enabled) {
|
|
118
|
+
ctx.ui.notify("[setup-infisical] Infisical not configured", "error");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Quick test - just check Varlock version and config validity
|
|
123
|
+
const varlockVersion = await getVarlockVersion();
|
|
124
|
+
|
|
125
|
+
if (!varlockVersion) {
|
|
126
|
+
ctx.ui.notify("[setup-infisical] Varlock not found - install with: npm install -g varlock", "error");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ctx.ui.notify(`[setup-infisical] Varlock ${varlockVersion} found`, "info");
|
|
131
|
+
ctx.ui.notify(`[setup-infisical] Project: ${config.projectId}`, "info");
|
|
132
|
+
ctx.ui.notify(`[setup-infisical] Environment: ${config.environment}`, "info");
|
|
133
|
+
ctx.ui.notify("[setup-infisical] Configuration appears valid", "info");
|
|
134
|
+
ctx.ui.notify("Run `/setup-infisical refresh` to resolve secrets", "info");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle /setup-infisical refresh
|
|
139
|
+
*/
|
|
140
|
+
async function handleRefresh(ctx: ExtensionContext): Promise<void> {
|
|
141
|
+
ctx.ui.notify("[setup-infisical] Refreshing secrets...", "info");
|
|
142
|
+
|
|
143
|
+
const provider = getSecretProvider();
|
|
144
|
+
const result = await provider.refresh();
|
|
145
|
+
|
|
146
|
+
if (result.success) {
|
|
147
|
+
ctx.ui.notify("[setup-infisical] Secrets refreshed successfully", "info");
|
|
148
|
+
} else {
|
|
149
|
+
ctx.ui.notify(`[setup-infisical] Refresh failed: ${result.error}`, "error");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle /setup-infisical disable
|
|
155
|
+
*/
|
|
156
|
+
async function handleDisable(ctx: ExtensionContext): Promise<void> {
|
|
157
|
+
ctx.ui.notify("[setup-infisical] Disabling Infisical integration...", "info");
|
|
158
|
+
|
|
159
|
+
disableIntegration();
|
|
160
|
+
|
|
161
|
+
ctx.ui.notify("[setup-infisical] Infisical disabled", "info");
|
|
162
|
+
ctx.ui.notify("Legacy key files will be used instead", "info");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Handle interactive /setup-infisical wizard
|
|
167
|
+
*/
|
|
168
|
+
async function handleSetup(ctx: ExtensionContext): Promise<void> {
|
|
169
|
+
// Check if already configured
|
|
170
|
+
const existing = loadConfig();
|
|
171
|
+
if (existing?.enabled) {
|
|
172
|
+
ctx.ui.notify("Infisical is already configured.", "info");
|
|
173
|
+
ctx.ui.notify("Run `/setup-infisical status` for details", "info");
|
|
174
|
+
ctx.ui.notify("Run `/setup-infisical disable` to remove configuration", "info");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Interactive setup would use ctx.ui.select/input/confirm
|
|
179
|
+
// For now, show instructions
|
|
180
|
+
const instructions = [
|
|
181
|
+
"# Infisical Setup",
|
|
182
|
+
"",
|
|
183
|
+
"This wizard will help you configure Infisical integration.",
|
|
184
|
+
"",
|
|
185
|
+
"## Prerequisites",
|
|
186
|
+
"",
|
|
187
|
+
"1. Create an Infisical account at https://infisical.com",
|
|
188
|
+
"2. Create a project and note the Project ID",
|
|
189
|
+
"3. Create a Machine Identity with Universal Auth",
|
|
190
|
+
"4. Note the Client ID and Client Secret",
|
|
191
|
+
"",
|
|
192
|
+
"## Configuration steps:",
|
|
193
|
+
"",
|
|
194
|
+
"1. Site: Choose US Cloud (https://app.infisical.com) or EU Cloud",
|
|
195
|
+
"2. Enter Project ID from your Infisical project",
|
|
196
|
+
"3. Enter Environment (e.g., production, development)",
|
|
197
|
+
"4. Enter Secret Path (e.g., /harness)",
|
|
198
|
+
"5. Choose bootstrap method:",
|
|
199
|
+
" - env:INFISICAL_CLIENT_SECRET for environment variable",
|
|
200
|
+
" - file:/path/to/client.secret for file-based bootstrap",
|
|
201
|
+
"6. Select services to configure",
|
|
202
|
+
"",
|
|
203
|
+
"## Quick setup:",
|
|
204
|
+
"",
|
|
205
|
+
"Create ~/.pi-harness-runtime/infisical/config.json manually:",
|
|
206
|
+
"",
|
|
207
|
+
"```json",
|
|
208
|
+
'{',
|
|
209
|
+
' "schemaVersion": 1,',
|
|
210
|
+
' "enabled": true,',
|
|
211
|
+
' "site": "us",',
|
|
212
|
+
' "projectId": "your-project-id",',
|
|
213
|
+
' "environment": "production",',
|
|
214
|
+
' "secretPath": "/harness",',
|
|
215
|
+
' "authMethod": "universal",',
|
|
216
|
+
' "bootstrapRef": { "type": "env", "name": "INFISICAL_CLIENT_SECRET" },',
|
|
217
|
+
' "services": [',
|
|
218
|
+
' { "service": "jev", "required": true, "keys": { "TYPESAFE_API_KEY": "TYPESAFE_API_KEY" } },',
|
|
219
|
+
' { "service": "telegram", "required": false, "keys": { "TELEGRAM_BOT_TOKEN": "TELEGRAM_BOT_TOKEN" } }',
|
|
220
|
+
" ],",
|
|
221
|
+
' "sourcePolicy": { "remoteAuthoritative": true, "envOverride": false },',
|
|
222
|
+
' "activeRevision": null,',
|
|
223
|
+
' "lastResolvedAt": null,',
|
|
224
|
+
' "lastError": null',
|
|
225
|
+
"}",
|
|
226
|
+
"```",
|
|
227
|
+
"",
|
|
228
|
+
"Then run `/setup-infisical refresh` to resolve secrets.",
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
ctx.ui.notify(instructions.join("\n"), "info");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Register /setup-infisical command
|
|
236
|
+
*/
|
|
237
|
+
export function registerInfisicalSetup(pi: ExtensionAPI): void {
|
|
238
|
+
pi.registerCommand("setup-infisical", {
|
|
239
|
+
description: "Configure Infisical secrets. Usage: /setup-infisical [status|test|refresh|disable]",
|
|
240
|
+
handler: async (args: string, ctx: ExtensionContext): Promise<void> => {
|
|
241
|
+
const { subcommand, flags } = parseArgs(args);
|
|
242
|
+
|
|
243
|
+
switch (subcommand) {
|
|
244
|
+
case "status":
|
|
245
|
+
return handleStatus(ctx);
|
|
246
|
+
|
|
247
|
+
case "test":
|
|
248
|
+
return handleTest(ctx);
|
|
249
|
+
|
|
250
|
+
case "refresh":
|
|
251
|
+
return handleRefresh(ctx);
|
|
252
|
+
|
|
253
|
+
case "disable":
|
|
254
|
+
return handleDisable(ctx);
|
|
255
|
+
|
|
256
|
+
case "setup":
|
|
257
|
+
return handleSetup(ctx);
|
|
258
|
+
|
|
259
|
+
default:
|
|
260
|
+
if (subcommand === "") {
|
|
261
|
+
return handleSetup(ctx);
|
|
262
|
+
}
|
|
263
|
+
ctx.ui.notify(
|
|
264
|
+
`Unknown subcommand: ${subcommand}. Use: status, test, refresh, disable`,
|
|
265
|
+
"warning"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|