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.
Files changed (104) hide show
  1. package/harness/infisical-config.ts +395 -0
  2. package/harness/infisical-setup.ts +270 -0
  3. package/harness/key-loader.ts +129 -29
  4. package/harness/secret-provider.ts +437 -0
  5. package/harness/secret-schema.ts +163 -0
  6. package/harness/varlock-worker.ts +379 -0
  7. package/index.ts +12 -3
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/agent-policy/package.json +1 -1
  11. package/packages/architecture-generator/package.json +1 -1
  12. package/packages/auth/package.json +1 -1
  13. package/packages/autonomous-refactor/package.json +1 -1
  14. package/packages/autonomous-runtime/package.json +1 -1
  15. package/packages/cache-strategy/package.json +1 -1
  16. package/packages/capability-registry/package.json +1 -1
  17. package/packages/checkpoint/package.json +1 -1
  18. package/packages/cli-plugin-sdk/package.json +1 -1
  19. package/packages/clipboard/package.json +1 -1
  20. package/packages/clipboard-plugin/package.json +1 -1
  21. package/packages/code-generation/package.json +1 -1
  22. package/packages/code-review/package.json +1 -1
  23. package/packages/codex-adapter/package.json +1 -1
  24. package/packages/codex-watcher/package.json +1 -1
  25. package/packages/config-capture/package.json +1 -1
  26. package/packages/context-compiler/package.json +1 -1
  27. package/packages/context-discovery/package.json +1 -1
  28. package/packages/context-manager/package.json +1 -1
  29. package/packages/cookie-sanitizer/package.json +1 -1
  30. package/packages/cost-optimizer/package.json +1 -1
  31. package/packages/dependency-analyzer/package.json +1 -1
  32. package/packages/django-plugin/package.json +1 -1
  33. package/packages/doc-generator/package.json +1 -1
  34. package/packages/evaluation-engine/package.json +1 -1
  35. package/packages/evaluation-runner/package.json +1 -1
  36. package/packages/event-bus/package.json +1 -1
  37. package/packages/event-store/package.json +1 -1
  38. package/packages/experience-replay/package.json +1 -1
  39. package/packages/feedback-collector/package.json +1 -1
  40. package/packages/file-copy-helper/package.json +1 -1
  41. package/packages/framework-detector/package.json +1 -1
  42. package/packages/framework-plugin-sdk/package.json +1 -1
  43. package/packages/frappe-plugin/package.json +1 -1
  44. package/packages/generic-web-plugin/package.json +1 -1
  45. package/packages/health-monitor/package.json +1 -1
  46. package/packages/intent-analyzer/package.json +1 -1
  47. package/packages/jev-judge/package.json +1 -1
  48. package/packages/knowledge-graph/package.json +1 -1
  49. package/packages/knowledge-retrieval/package.json +1 -1
  50. package/packages/laravel-plugin/package.json +1 -1
  51. package/packages/learning-engine/package.json +1 -1
  52. package/packages/mcp-adapter/package.json +1 -1
  53. package/packages/memory-engine/package.json +1 -1
  54. package/packages/milestone-manager/package.json +1 -1
  55. package/packages/model-registry/package.json +1 -1
  56. package/packages/nextjs-plugin/package.json +1 -1
  57. package/packages/notification/package.json +1 -1
  58. package/packages/observability/package.json +1 -1
  59. package/packages/okf-indexer/package.json +1 -1
  60. package/packages/paperclip-integration/package.json +1 -1
  61. package/packages/performance-optimizer/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/release-manager/package.json +1 -1
  77. package/packages/requirement-compiler/package.json +1 -1
  78. package/packages/runtime/package.json +1 -1
  79. package/packages/scheduler/package.json +1 -1
  80. package/packages/scheduler-adapter/package.json +1 -1
  81. package/packages/session/package.json +1 -1
  82. package/packages/session-api/package.json +1 -1
  83. package/packages/session-export/package.json +1 -1
  84. package/packages/shared-context/package.json +1 -1
  85. package/packages/skill-mcp-client/package.json +1 -1
  86. package/packages/skill-registry/package.json +1 -1
  87. package/packages/skills/package.json +1 -1
  88. package/packages/sprint-planner/package.json +1 -1
  89. package/packages/subscription-engine/package.json +1 -1
  90. package/packages/task-compiler/package.json +1 -1
  91. package/packages/tencentdb-memory/package.json +1 -1
  92. package/packages/tencentdb-sync/package.json +1 -1
  93. package/packages/test-data-generator/package.json +1 -1
  94. package/packages/test-generator/package.json +1 -1
  95. package/packages/todo-bd-sync/package.json +1 -1
  96. package/packages/token-estimation/package.json +1 -1
  97. package/packages/token-optimizer/package.json +1 -1
  98. package/packages/trajectory/package.json +1 -1
  99. package/packages/tui/package.json +1 -1
  100. package/packages/types/package.json +1 -1
  101. package/packages/workflow-events/package.json +1 -1
  102. package/packages/workspace-scanner/package.json +1 -1
  103. package/packages/worktree/package.json +1 -1
  104. package/packages/write-review/package.json +1 -1
@@ -1,84 +1,184 @@
1
1
  /**
2
- * Key Loader — Loads API keys from ~/.pi-harness-runtime/keys/ into process.env
2
+ * Key Loader — Unified credential loading
3
3
  *
4
- * Usage:
5
- * import { loadKeys } from "./key-loader.js";
6
- * loadKeys(); // Loads all keys into process.env
4
+ * Loads API keys from Infisical (via Varlock) with file fallback.
5
+ * Priority: Infisical > Environment > Key Files
7
6
  *
8
7
  * Environment variables set:
9
- * Telegram: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_WEBHOOK_URL, TELEGRAM_WEBHOOK_SECRET
10
- * Models: PLANNER_API_KEY, PLANNER_MODEL, PLANNER_BASE_URL
11
- * GLM_API_KEY
12
- * MINIMAX_API_KEY
8
+ * Jev: TYPESAFE_API_KEY, OPENROUTER_API_KEY
9
+ * Telegram: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_WEBHOOK_SECRET
10
+ * Honcho: HONCHO_API_KEY
11
+ * Qdrant: QDRANT_CLUSTER_ENDPOINT, QDRANT_API_KEY, QDRANT_COLLECTION
12
+ * Embeddings: OPENAI_API_KEY
13
+ * Planners: PLANNER_API_KEY, PLANNER_BASE_URL, PLANNER_MODEL
14
+ * GLM_API_KEY, GLM_BASE_URL, GLM_MODEL
15
+ * MINIMAX_API_KEY, MINIMAX_BASE_URL, MINIMAX_MODEL
13
16
  */
14
17
 
15
18
  import { existsSync, readFileSync } from "node:fs";
16
19
  import { join } from "node:path";
17
20
  import { homedir } from "node:os";
21
+ import { isConfigured, loadConfig } from "./infisical-config.js";
22
+ import { getSecretProvider, SecretProvider } from "./secret-provider.js";
18
23
 
19
24
  const KEYS_DIR = join(homedir(), ".pi-harness-runtime", "keys");
20
25
 
26
+ /**
27
+ * Key file mapping
28
+ */
21
29
  const KEY_FILE_MAP: Record<string, string> = {
22
- // Telegram notification keys
30
+ // Jev keys
31
+ TYPESAFE_API_KEY: "typesafe-api-key.txt",
32
+ OPENROUTER_API_KEY: "openrouter-api-key.txt",
33
+
34
+ // Telegram keys
23
35
  TELEGRAM_BOT_TOKEN: "telegram-bot-token.txt",
24
36
  TELEGRAM_CHAT_ID: "telegram-chat-id.txt",
25
37
  TELEGRAM_WEBHOOK_URL: "telegram-webhook-url.txt",
26
38
  TELEGRAM_WEBHOOK_SECRET: "telegram-webhook-secret.txt",
27
- // Model API keys (for daemon loop)
39
+
40
+ // Honcho keys
41
+ HONCHO_API_KEY: "honcho-api-key.txt",
42
+
43
+ // Qdrant keys
44
+ QDRANT_CLUSTER_ENDPOINT: "qdrant-cluster-endpoint.txt",
45
+ QDRANT_API_KEY: "qdrant-api-key.txt",
46
+ QDRANT_COLLECTION: "qdrant-collection.txt",
47
+
48
+ // Embeddings
49
+ OPENAI_API_KEY: "openai-api-key.txt",
50
+
51
+ // Planner keys
28
52
  PLANNER_API_KEY: "planner-api-key.txt",
29
53
  PLANNER_MODEL: "planner-model.txt",
30
54
  PLANNER_BASE_URL: "planner-base-url.txt",
55
+
56
+ // GLM keys
31
57
  GLM_API_KEY: "glm-api-key.txt",
32
58
  GLM_MODEL: "glm-model.txt",
33
59
  GLM_BASE_URL: "glm-base-url.txt",
60
+
61
+ // Minimax keys
34
62
  MINIMAX_API_KEY: "minimax-api-key.txt",
35
63
  MINIMAX_MODEL: "minimax-model.txt",
36
64
  MINIMAX_BASE_URL: "minimax-base-url.txt",
37
65
  };
38
66
 
39
67
  /**
40
- * Load all keys from ~/.pi-harness-runtime/keys/ into process.env
41
- * Only sets env vars that are not already set (allows CLI args to override)
68
+ * Try to get from Infisical provider
69
+ */
70
+ function getFromProvider(key: string): string | undefined {
71
+ try {
72
+ if (!isConfigured()) {
73
+ return undefined;
74
+ }
75
+
76
+ const provider = getSecretProvider();
77
+ if (!provider.isReady()) {
78
+ return undefined;
79
+ }
80
+
81
+ return provider.get(key);
82
+ } catch {
83
+ return undefined;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Load a single key with precedence: Infisical > Environment > File
89
+ */
90
+ function loadKey(key: string, filename: string): string | undefined {
91
+ // 1. Check if already in environment
92
+ if (process.env[key]) {
93
+ return process.env[key];
94
+ }
95
+
96
+ // 2. Check Infisical provider
97
+ const providerValue = getFromProvider(key);
98
+ if (providerValue) {
99
+ return providerValue;
100
+ }
101
+
102
+ // 3. Fall back to file
103
+ const filePath = join(KEYS_DIR, filename);
104
+ if (existsSync(filePath)) {
105
+ const value = readFileSync(filePath, "utf8").trim();
106
+ if (value) {
107
+ return value;
108
+ }
109
+ }
110
+
111
+ return undefined;
112
+ }
113
+
114
+ /**
115
+ * Load all keys into process.env
116
+ * Only sets env vars that are not already set
42
117
  */
43
118
  export function loadKeys(): void {
44
119
  if (!existsSync(KEYS_DIR)) {
45
- console.warn(`[key-loader] Keys directory not found: ${KEYS_DIR}`);
46
- return;
120
+ console.log(`[key-loader] Keys directory not found: ${KEYS_DIR}`);
47
121
  }
48
122
 
49
123
  let loadedCount = 0;
124
+ let infisicalCount = 0;
125
+ let fileCount = 0;
126
+
50
127
  for (const [envVar, filename] of Object.entries(KEY_FILE_MAP)) {
51
- // Skip if already set via environment
128
+ // Skip if already set
52
129
  if (process.env[envVar]) {
53
- console.log(`[key-loader] ${envVar} already set via env, skipping`);
54
130
  continue;
55
131
  }
56
132
 
57
- const filePath = join(KEYS_DIR, filename);
58
- if (existsSync(filePath)) {
59
- const value = readFileSync(filePath, "utf8").trim();
60
- if (value) {
61
- process.env[envVar] = value;
62
- loadedCount++;
63
- console.log(`[key-loader] Loaded ${envVar} from ${filename}`);
133
+ const value = loadKey(envVar, filename);
134
+ if (value) {
135
+ process.env[envVar] = value;
136
+ loadedCount++;
137
+
138
+ // Track source
139
+ if (getFromProvider(envVar)) {
140
+ infisicalCount++;
141
+ } else {
142
+ fileCount++;
64
143
  }
65
144
  }
66
145
  }
67
146
 
68
147
  if (loadedCount > 0) {
69
- console.log(`[key-loader] Loaded ${loadedCount} key(s) from ${KEYS_DIR}`);
148
+ const source = infisicalCount > 0 ? `Infisical(${infisicalCount})` : "";
149
+ const sourceParts = [source, fileCount > 0 ? `Files(${fileCount})` : ""].filter(Boolean);
150
+ console.log(`[key-loader] Loaded ${loadedCount} key(s): ${sourceParts.join(", ")}`);
70
151
  }
71
152
  }
72
153
 
73
154
  /**
74
- * Get a specific key by name (without .txt extension)
155
+ * Get a specific key by name
75
156
  */
76
157
  export function getKey(keyName: string): string | undefined {
77
- const filePath = join(KEYS_DIR, `${keyName}.txt`);
78
- if (existsSync(filePath)) {
79
- return readFileSync(filePath, "utf8").trim();
158
+ const filename = KEY_FILE_MAP[keyName];
159
+ if (!filename) {
160
+ return undefined;
80
161
  }
81
- return undefined;
162
+ return loadKey(keyName, filename);
163
+ }
164
+
165
+ /**
166
+ * Check if keys directory exists
167
+ */
168
+ export function hasKeysDir(): boolean {
169
+ return existsSync(KEYS_DIR);
170
+ }
171
+
172
+ /**
173
+ * List available key files
174
+ */
175
+ export function listKeyFiles(): string[] {
176
+ if (!existsSync(KEYS_DIR)) {
177
+ return [];
178
+ }
179
+
180
+ const { readdirSync } = require("node:fs");
181
+ return readdirSync(KEYS_DIR).filter((f: string) => f.endsWith(".txt"));
82
182
  }
83
183
 
84
184
  export default loadKeys;
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Secret Provider Module
3
+ *
4
+ * Shared async resolution, immutable snapshots, and precedence handling.
5
+ * All services await this provider for credentials.
6
+ */
7
+
8
+ import {
9
+ InfisicalConfig,
10
+ loadConfig,
11
+ isConfigured,
12
+ activateRevision,
13
+ createRevision,
14
+ getSchemaPath,
15
+ getRevision,
16
+ markRevisionValidated,
17
+ getSiteUrl,
18
+ ensureDirectories,
19
+ } from "./infisical-config.js";
20
+ import {
21
+ resolveSecrets,
22
+ ResolutionResult,
23
+ getVarlockVersion,
24
+ validateSchema,
25
+ } from "./varlock-worker.js";
26
+ import {
27
+ generateSchema,
28
+ ServiceSecretMapping,
29
+ ServiceName,
30
+ } from "./secret-schema.js";
31
+ import { logServiceEvent } from "./service-diagnostics.js";
32
+
33
+ /**
34
+ * Immutable credential snapshot
35
+ */
36
+ export interface CredentialSnapshot {
37
+ revisionId: string;
38
+ createdAt: string;
39
+ secrets: Record<string, string>; // key -> value
40
+ sources: Record<string, "infisical" | "env" | "file">; // key -> source
41
+ }
42
+
43
+ /**
44
+ * Provider status
45
+ */
46
+ export type ProviderStatus =
47
+ | "unconfigured"
48
+ | "validating"
49
+ | "resolved"
50
+ | "saved_awaiting_restart"
51
+ | "active"
52
+ | "degraded";
53
+
54
+ /**
55
+ * Provider state
56
+ */
57
+ export interface ProviderState {
58
+ status: ProviderStatus;
59
+ config: InfisicalConfig | null;
60
+ currentSnapshot: CredentialSnapshot | null;
61
+ activeRevision: string | null;
62
+ lastResolvedAt: string | null;
63
+ lastError: string | null;
64
+ errorCode: string | null;
65
+ }
66
+
67
+ /**
68
+ * Secret Provider singleton
69
+ */
70
+ export class SecretProvider {
71
+ private static instance: SecretProvider | null = null;
72
+
73
+ private state: ProviderState = {
74
+ status: "unconfigured",
75
+ config: null,
76
+ currentSnapshot: null,
77
+ activeRevision: null,
78
+ lastResolvedAt: null,
79
+ lastError: null,
80
+ errorCode: null,
81
+ };
82
+
83
+ private resolutionPromise: Promise<void> | null = null;
84
+ private abortController: AbortController | null = null;
85
+
86
+ private constructor() {}
87
+
88
+ /**
89
+ * Get singleton instance
90
+ */
91
+ static getInstance(): SecretProvider {
92
+ if (!SecretProvider.instance) {
93
+ SecretProvider.instance = new SecretProvider();
94
+ }
95
+ return SecretProvider.instance;
96
+ }
97
+
98
+ /**
99
+ * Initialize provider from config
100
+ */
101
+ async initialize(): Promise<void> {
102
+ const config = loadConfig();
103
+
104
+ if (!config || !config.enabled) {
105
+ this.state.status = "unconfigured";
106
+ return;
107
+ }
108
+
109
+ this.state.config = config;
110
+ this.state.activeRevision = config.activeRevision;
111
+
112
+ // Load current snapshot if we have an active revision
113
+ if (config.activeRevision) {
114
+ const snapshot = await this.loadSnapshot(config.activeRevision);
115
+ if (snapshot) {
116
+ this.state.currentSnapshot = snapshot;
117
+ this.state.status = "active";
118
+ this.state.lastResolvedAt = config.lastResolvedAt;
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Check if provider is ready
125
+ */
126
+ isReady(): boolean {
127
+ return this.state.status === "active" && this.state.currentSnapshot !== null;
128
+ }
129
+
130
+ /**
131
+ * Get current status
132
+ */
133
+ getStatus(): ProviderState {
134
+ return { ...this.state };
135
+ }
136
+
137
+ /**
138
+ * Get a secret value
139
+ */
140
+ get(key: string): string | undefined {
141
+ if (!this.state.currentSnapshot) {
142
+ return undefined;
143
+ }
144
+ return this.state.currentSnapshot.secrets[key];
145
+ }
146
+
147
+ /**
148
+ * Get all secrets for a service
149
+ */
150
+ getServiceSecrets(service: ServiceName): Record<string, string> {
151
+ const result: Record<string, string> = {};
152
+
153
+ if (!this.state.currentSnapshot || !this.state.config) {
154
+ return result;
155
+ }
156
+
157
+ const mapping = this.state.config.services.find(s => s.service === service);
158
+ if (!mapping) {
159
+ return result;
160
+ }
161
+
162
+ for (const [localKey, secretName] of Object.entries(mapping.keys)) {
163
+ const value = this.state.currentSnapshot.secrets[secretName];
164
+ if (value !== undefined) {
165
+ result[localKey] = value;
166
+ }
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ /**
173
+ * Refresh secrets (resolve new snapshot)
174
+ */
175
+ async refresh(): Promise<{ success: boolean; error?: string }> {
176
+ // Don't allow concurrent refresh
177
+ if (this.resolutionPromise) {
178
+ return { success: false, error: "Refresh already in progress" };
179
+ }
180
+
181
+ const config = loadConfig();
182
+ if (!config || !config.enabled) {
183
+ return { success: false, error: "Infisical not configured" };
184
+ }
185
+
186
+ this.abortController = new AbortController();
187
+ this.state.status = "validating";
188
+
189
+ logServiceEvent({
190
+ ts: new Date().toISOString(),
191
+ level: "info",
192
+ service: "infisical",
193
+ operation: "refresh_started",
194
+ phase: "start",
195
+ projectId: config.projectId,
196
+ environment: config.environment,
197
+ });
198
+
199
+ this.resolutionPromise = this.doRefresh(config, this.abortController.signal);
200
+
201
+ try {
202
+ await this.resolutionPromise;
203
+ return { success: true };
204
+ } catch (err) {
205
+ return {
206
+ success: false,
207
+ error: err instanceof Error ? err.message : String(err),
208
+ };
209
+ } finally {
210
+ this.resolutionPromise = null;
211
+ this.abortController = null;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Internal refresh implementation
217
+ */
218
+ private async doRefresh(
219
+ config: InfisicalConfig,
220
+ signal: AbortSignal
221
+ ): Promise<void> {
222
+ try {
223
+ // Generate schema for this revision
224
+ const revisionId = createRevision(config);
225
+ const schemaPath = getSchemaPath(revisionId);
226
+
227
+ const schema = generateSchema(config);
228
+ ensureDirectories();
229
+
230
+ // Write schema with proper permissions
231
+ const { writeFileSync, chmodSync } = await import("node:fs");
232
+ writeFileSync(schemaPath, schema, { mode: 0o600 });
233
+ chmodSync(schemaPath, 0o600);
234
+
235
+ // Validate schema
236
+ const validation = await validateSchema(schemaPath);
237
+ if (!validation.valid) {
238
+ logServiceEvent({
239
+ ts: new Date().toISOString(),
240
+ level: "error",
241
+ service: "infisical",
242
+ operation: "schema_validation_failed",
243
+ phase: "failed",
244
+ error: validation.error,
245
+ });
246
+
247
+ this.state.status = "degraded";
248
+ this.state.lastError = validation.error ?? "Schema validation failed";
249
+ this.state.errorCode = "validation";
250
+ return;
251
+ }
252
+
253
+ // Resolve secrets
254
+ const bootstrapEnv = this.getBootstrapEnv(config);
255
+ const result = await resolveSecrets(
256
+ {
257
+ schemaPath,
258
+ cwd: process.cwd(),
259
+ env: bootstrapEnv,
260
+ skipCache: true,
261
+ },
262
+ signal
263
+ );
264
+
265
+ if (!result.success) {
266
+ logServiceEvent({
267
+ ts: new Date().toISOString(),
268
+ level: "error",
269
+ service: "infisical",
270
+ operation: "secrets_resolve_failed",
271
+ phase: "failed",
272
+ errorCode: result.errorCode,
273
+ });
274
+
275
+ this.state.status = "degraded";
276
+ this.state.lastError = result.error ?? null;
277
+ this.state.errorCode = result.errorCode ?? "unknown";
278
+ return;
279
+ }
280
+
281
+ // Build snapshot
282
+ const secrets: Record<string, string> = {};
283
+ const sources: Record<string, "infisical" | "env" | "file"> = {};
284
+
285
+ for (const secret of result.secrets ?? []) {
286
+ secrets[secret.key] = secret.value;
287
+ sources[secret.key] = "infisical";
288
+ }
289
+
290
+ const snapshot: CredentialSnapshot = {
291
+ revisionId,
292
+ createdAt: new Date().toISOString(),
293
+ secrets,
294
+ sources,
295
+ };
296
+
297
+ // Save snapshot
298
+ await this.saveSnapshot(revisionId, snapshot);
299
+ markRevisionValidated(revisionId);
300
+
301
+ // Activate revision
302
+ activateRevision(revisionId);
303
+
304
+ // Update state
305
+ this.state.currentSnapshot = snapshot;
306
+ this.state.activeRevision = revisionId;
307
+ this.state.status = "active";
308
+ this.state.lastResolvedAt = new Date().toISOString();
309
+ this.state.lastError = null;
310
+ this.state.errorCode = null;
311
+
312
+ logServiceEvent({
313
+ ts: new Date().toISOString(),
314
+ level: "info",
315
+ service: "infisical",
316
+ operation: "snapshot_activated",
317
+ phase: "completed",
318
+ revisionId,
319
+ secretCount: result.secrets?.length ?? 0,
320
+ });
321
+ } catch (err) {
322
+ if (err instanceof Error && err.name === "AbortError") {
323
+ this.state.status = "degraded";
324
+ this.state.lastError = "Cancelled";
325
+ this.state.errorCode = "cancelled";
326
+ } else {
327
+ this.state.status = "degraded";
328
+ this.state.lastError = err instanceof Error ? err.message : String(err);
329
+ this.state.errorCode = "unknown";
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Get bootstrap environment for Varlock
336
+ */
337
+ private getBootstrapEnv(config: InfisicalConfig): Record<string, string> {
338
+ const env: Record<string, string> = {};
339
+
340
+ if (config.bootstrapRef.type === "env") {
341
+ const value = process.env[config.bootstrapRef.name];
342
+ if (value) {
343
+ env[config.bootstrapRef.name] = value;
344
+ }
345
+ } else if (config.bootstrapRef.type === "file") {
346
+ // Read bootstrap from file
347
+ try {
348
+ const { readFileSync } = require("node:fs");
349
+ const content = readFileSync(config.bootstrapRef.path, "utf8").trim();
350
+ // Parse KEY=value lines
351
+ for (const line of content.split("\n")) {
352
+ const match = line.match(/^([^=]+)=(.*)$/);
353
+ if (match) {
354
+ env[match[1]!.trim()] = match[2]!.trim();
355
+ }
356
+ }
357
+ } catch {
358
+ // Bootstrap file not accessible
359
+ }
360
+ }
361
+
362
+ return env;
363
+ }
364
+
365
+ /**
366
+ * Load snapshot from disk
367
+ */
368
+ private async loadSnapshot(revisionId: string): Promise<CredentialSnapshot | null> {
369
+ const { loadConfig: reloadConfig } = await import("./infisical-config.js");
370
+ const { existsSync, readFileSync } = await import("node:fs");
371
+ const { join } = await import("node:path");
372
+
373
+ const snapshotPath = join(
374
+ (await import("./infisical-config.js")).getRevisionsDir(),
375
+ revisionId,
376
+ "snapshot.json"
377
+ );
378
+
379
+ if (!existsSync(snapshotPath)) {
380
+ return null;
381
+ }
382
+
383
+ try {
384
+ const content = readFileSync(snapshotPath, "utf8");
385
+ return JSON.parse(content) as CredentialSnapshot;
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Save snapshot to disk
393
+ */
394
+ private async saveSnapshot(revisionId: string, snapshot: CredentialSnapshot): Promise<void> {
395
+ const { getRevisionsDir } = await import("./infisical-config.js");
396
+ const { writeFileSync, chmodSync } = await import("node:fs");
397
+ const { join } = await import("node:path");
398
+
399
+ const snapshotPath = join(getRevisionsDir(), revisionId, "snapshot.json");
400
+
401
+ writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), { mode: 0o600 });
402
+ chmodSync(snapshotPath, 0o600);
403
+ }
404
+
405
+ /**
406
+ * Cancel ongoing refresh
407
+ */
408
+ cancel(): void {
409
+ if (this.abortController) {
410
+ this.abortController.abort();
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Get the secret provider instance
417
+ */
418
+ export function getSecretProvider(): SecretProvider {
419
+ return SecretProvider.getInstance();
420
+ }
421
+
422
+ /**
423
+ * Await provider readiness
424
+ */
425
+ export async function awaitProviderReady(timeoutMs: number = 30000): Promise<boolean> {
426
+ const provider = SecretProvider.getInstance();
427
+ const start = Date.now();
428
+
429
+ while (!provider.isReady() && provider.getStatus().status !== "unconfigured") {
430
+ if (Date.now() - start > timeoutMs) {
431
+ return false;
432
+ }
433
+ await new Promise(resolve => setTimeout(resolve, 100));
434
+ }
435
+
436
+ return provider.isReady();
437
+ }