envpkt 0.2.0 → 0.4.1
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/README.md +245 -144
- package/dist/cli.js +685 -297
- package/dist/index.d.ts +54 -7
- package/dist/index.js +243 -80
- package/package.json +1 -1
- package/schemas/envpkt.schema.json +42 -3
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import { Cond, Left, List, Option, Right, Try } from "functype";
|
|
5
6
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
6
8
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
7
9
|
import { TomlDate, parse, stringify } from "smol-toml";
|
|
8
10
|
import { FormatRegistry, Type } from "@sinclair/typebox";
|
|
@@ -17,7 +19,7 @@ const MS_PER_DAY = 864e5;
|
|
|
17
19
|
const WARN_BEFORE_DAYS = 30;
|
|
18
20
|
const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
|
|
19
21
|
const parseDate = (dateStr) => {
|
|
20
|
-
const d = /* @__PURE__ */ new Date(dateStr
|
|
22
|
+
const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
|
|
21
23
|
return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
|
|
22
24
|
};
|
|
23
25
|
const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
|
|
@@ -62,8 +64,9 @@ const computeAudit = (config, fnoxKeys, today) => {
|
|
|
62
64
|
const requireExpiration = lifecycle.require_expiration ?? false;
|
|
63
65
|
const requireService = lifecycle.require_service ?? false;
|
|
64
66
|
const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
+
const secretEntries = config.secret ?? {};
|
|
68
|
+
const metaKeys = new Set(Object.keys(secretEntries));
|
|
69
|
+
const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
|
|
67
70
|
const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
|
|
68
71
|
const total = secrets.size;
|
|
69
72
|
const expired = secrets.count((s) => s.status === "expired");
|
|
@@ -86,6 +89,28 @@ const computeAudit = (config, fnoxKeys, today) => {
|
|
|
86
89
|
agent: config.agent
|
|
87
90
|
};
|
|
88
91
|
};
|
|
92
|
+
const computeEnvAudit = (config, env = process.env) => {
|
|
93
|
+
const envEntries = config.env ?? {};
|
|
94
|
+
const entries = [];
|
|
95
|
+
for (const [key, entry] of Object.entries(envEntries)) {
|
|
96
|
+
const currentValue = env[key];
|
|
97
|
+
const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
|
|
98
|
+
entries.push({
|
|
99
|
+
key,
|
|
100
|
+
defaultValue: entry.value,
|
|
101
|
+
currentValue,
|
|
102
|
+
status,
|
|
103
|
+
purpose: entry.purpose
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
entries,
|
|
108
|
+
total: entries.length,
|
|
109
|
+
defaults_applied: entries.filter((e) => e.status === "default").length,
|
|
110
|
+
overridden: entries.filter((e) => e.status === "overridden").length,
|
|
111
|
+
missing: entries.filter((e) => e.status === "missing").length
|
|
112
|
+
};
|
|
113
|
+
};
|
|
89
114
|
|
|
90
115
|
//#endregion
|
|
91
116
|
//#region src/core/schema.ts
|
|
@@ -124,6 +149,7 @@ const SecretMetaSchema = Type.Object({
|
|
|
124
149
|
description: "URL or reference for secret rotation procedure"
|
|
125
150
|
})),
|
|
126
151
|
purpose: Type.Optional(Type.String({ description: "Why this secret exists and what it enables" })),
|
|
152
|
+
comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
|
|
127
153
|
capabilities: Type.Optional(Type.Array(Type.String(), { description: "What operations this secret grants (e.g. read, write, admin)" })),
|
|
128
154
|
created: Type.Optional(Type.String({
|
|
129
155
|
format: "date",
|
|
@@ -157,6 +183,12 @@ const CallbackConfigSchema = Type.Object({
|
|
|
157
183
|
on_audit_fail: Type.Optional(Type.String({ description: "Command or webhook on audit failure" }))
|
|
158
184
|
}, { description: "Automation callbacks for lifecycle events" });
|
|
159
185
|
const ToolsConfigSchema = Type.Record(Type.String(), Type.Unknown(), { description: "Tool integration configuration — open namespace for third-party extensions" });
|
|
186
|
+
const EnvMetaSchema = Type.Object({
|
|
187
|
+
value: Type.String({ description: "Default value for this environment variable" }),
|
|
188
|
+
purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
|
|
189
|
+
comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
|
|
190
|
+
tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
|
|
191
|
+
}, { description: "Metadata for a plaintext environment default (non-secret)" });
|
|
160
192
|
const EnvpktConfigSchema = Type.Object({
|
|
161
193
|
version: Type.Number({
|
|
162
194
|
description: "Schema version number",
|
|
@@ -164,7 +196,8 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
164
196
|
}),
|
|
165
197
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
166
198
|
agent: Type.Optional(AgentIdentitySchema),
|
|
167
|
-
|
|
199
|
+
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
200
|
+
env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
|
|
168
201
|
lifecycle: Type.Optional(LifecycleConfigSchema),
|
|
169
202
|
callbacks: Type.Optional(CallbackConfigSchema),
|
|
170
203
|
tools: Type.Optional(ToolsConfigSchema)
|
|
@@ -186,10 +219,69 @@ const normalizeDates = (obj) => {
|
|
|
186
219
|
if (obj !== null && typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]));
|
|
187
220
|
return obj;
|
|
188
221
|
};
|
|
189
|
-
/**
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
222
|
+
/** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string */
|
|
223
|
+
const expandPath = (p) => {
|
|
224
|
+
return (p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
|
|
225
|
+
const name = braced ?? bare ?? "";
|
|
226
|
+
return process.env[name] ?? "";
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Expand a path template that may contain a single `*` glob segment.
|
|
231
|
+
* Returns all matching paths (or empty array if parent doesn't exist).
|
|
232
|
+
* Non-glob paths return a single-element array if they exist.
|
|
233
|
+
*/
|
|
234
|
+
const expandGlobPath = (expanded) => {
|
|
235
|
+
if (!expanded.includes("*")) return existsSync(expanded) ? [expanded] : [];
|
|
236
|
+
const segments = expanded.split("/");
|
|
237
|
+
const globIdx = segments.findIndex((s) => s.includes("*"));
|
|
238
|
+
if (globIdx < 0) return [];
|
|
239
|
+
const parentDir = segments.slice(0, globIdx).join("/");
|
|
240
|
+
const globSegment = segments[globIdx];
|
|
241
|
+
const suffix = segments.slice(globIdx + 1).join("/");
|
|
242
|
+
if (!existsSync(parentDir)) return [];
|
|
243
|
+
const prefix = globSegment.replace(/\*.*$/, "");
|
|
244
|
+
return readdirSync(parentDir).filter((entry) => entry.startsWith(prefix)).map((entry) => join(parentDir, entry, suffix)).filter((p) => existsSync(p));
|
|
245
|
+
};
|
|
246
|
+
/** Ordered candidate paths for config discovery beyond CWD */
|
|
247
|
+
const CONFIG_SEARCH_PATHS = [
|
|
248
|
+
"~/.envpkt/envpkt.toml",
|
|
249
|
+
"~/OneDrive/.envpkt/envpkt.toml",
|
|
250
|
+
"~/Library/CloudStorage/OneDrive-Personal/.envpkt/envpkt.toml",
|
|
251
|
+
"~/Library/CloudStorage/OneDrive-SharedLibraries-*/.envpkt/envpkt.toml",
|
|
252
|
+
"$WINHOME/OneDrive/.envpkt/envpkt.toml",
|
|
253
|
+
"$USERPROFILE/OneDrive/.envpkt/envpkt.toml",
|
|
254
|
+
"$OneDrive/.envpkt/envpkt.toml",
|
|
255
|
+
"$OneDriveConsumer/.envpkt/envpkt.toml",
|
|
256
|
+
"$OneDriveCommercial/.envpkt/envpkt.toml",
|
|
257
|
+
"/mnt/c/Users/$USER/OneDrive/.envpkt/envpkt.toml",
|
|
258
|
+
"~/Library/Mobile Documents/com~apple~CloudDocs/.envpkt/envpkt.toml",
|
|
259
|
+
"~/Dropbox/.envpkt/envpkt.toml",
|
|
260
|
+
"$DROPBOX_PATH/.envpkt/envpkt.toml",
|
|
261
|
+
"~/Google Drive/My Drive/.envpkt/envpkt.toml",
|
|
262
|
+
"~/Library/CloudStorage/GoogleDrive-*/.envpkt/envpkt.toml",
|
|
263
|
+
"$GOOGLE_DRIVE/.envpkt/envpkt.toml",
|
|
264
|
+
"$WINHOME/.envpkt/envpkt.toml",
|
|
265
|
+
"$USERPROFILE/.envpkt/envpkt.toml"
|
|
266
|
+
];
|
|
267
|
+
/** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then built-in candidate paths */
|
|
268
|
+
const discoverConfig = (cwd) => {
|
|
269
|
+
const cwdCandidate = join(cwd ?? process.cwd(), CONFIG_FILENAME$2);
|
|
270
|
+
if (existsSync(cwdCandidate)) return Option({
|
|
271
|
+
path: cwdCandidate,
|
|
272
|
+
source: "cwd"
|
|
273
|
+
});
|
|
274
|
+
const customPaths = process.env.ENVPKT_SEARCH_PATH?.split(":").filter(Boolean) ?? [];
|
|
275
|
+
for (const template of [...customPaths, ...CONFIG_SEARCH_PATHS]) {
|
|
276
|
+
const expanded = expandPath(template);
|
|
277
|
+
if (!expanded || expanded.startsWith("/.envpkt")) continue;
|
|
278
|
+
const matches = expandGlobPath(expanded);
|
|
279
|
+
if (matches.length > 0) return Option({
|
|
280
|
+
path: matches[0],
|
|
281
|
+
source: "search"
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return Option(void 0);
|
|
193
285
|
};
|
|
194
286
|
/** Read a config file, returning Either<ConfigError, string> */
|
|
195
287
|
const readConfigFile = (path) => {
|
|
@@ -202,14 +294,13 @@ const readConfigFile = (path) => {
|
|
|
202
294
|
message: String(err)
|
|
203
295
|
}), (content) => Right(content));
|
|
204
296
|
};
|
|
205
|
-
/** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit
|
|
297
|
+
/** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit secret) */
|
|
206
298
|
const applyDefaults = (data) => {
|
|
207
299
|
if (data !== null && typeof data === "object" && !Array.isArray(data)) {
|
|
208
|
-
const
|
|
209
|
-
if (!("
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
};
|
|
300
|
+
const result = { ...data };
|
|
301
|
+
if (!("secret" in result)) result.secret = {};
|
|
302
|
+
if (!("env" in result)) result.env = {};
|
|
303
|
+
return result;
|
|
213
304
|
}
|
|
214
305
|
return data;
|
|
215
306
|
};
|
|
@@ -232,12 +323,16 @@ const loadConfig = (path) => readConfigFile(path).flatMap(parseToml).flatMap(val
|
|
|
232
323
|
* Resolve config path via priority chain:
|
|
233
324
|
* 1. Explicit flag path
|
|
234
325
|
* 2. ENVPKT_CONFIG env var
|
|
235
|
-
* 3. CWD discovery
|
|
326
|
+
* 3. CWD + discovery chain (home dir, cloud storage, custom search paths)
|
|
236
327
|
*/
|
|
237
328
|
const resolveConfigPath = (flagPath, envVar, cwd) => {
|
|
238
329
|
if (flagPath) {
|
|
239
330
|
const resolved = resolve(flagPath);
|
|
240
|
-
|
|
331
|
+
const result = {
|
|
332
|
+
path: resolved,
|
|
333
|
+
source: "flag"
|
|
334
|
+
};
|
|
335
|
+
return existsSync(resolved) ? Right(result) : Left({
|
|
241
336
|
_tag: "FileNotFound",
|
|
242
337
|
path: resolved
|
|
243
338
|
});
|
|
@@ -245,16 +340,22 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
|
|
|
245
340
|
const envPath = envVar ?? process.env[ENV_VAR_CONFIG];
|
|
246
341
|
if (envPath) {
|
|
247
342
|
const resolved = resolve(envPath);
|
|
248
|
-
|
|
343
|
+
const result = {
|
|
344
|
+
path: resolved,
|
|
345
|
+
source: "env"
|
|
346
|
+
};
|
|
347
|
+
return existsSync(resolved) ? Right(result) : Left({
|
|
249
348
|
_tag: "FileNotFound",
|
|
250
349
|
path: resolved
|
|
251
350
|
});
|
|
252
351
|
}
|
|
253
|
-
|
|
254
|
-
return findConfigPath(dir).fold(() => Left({
|
|
352
|
+
return discoverConfig(cwd).fold(() => Left({
|
|
255
353
|
_tag: "FileNotFound",
|
|
256
|
-
path: join(
|
|
257
|
-
}), (path) => Right(
|
|
354
|
+
path: join(cwd ?? process.cwd(), CONFIG_FILENAME$2)
|
|
355
|
+
}), ({ path, source }) => Right({
|
|
356
|
+
path,
|
|
357
|
+
source
|
|
358
|
+
}));
|
|
258
359
|
};
|
|
259
360
|
|
|
260
361
|
//#endregion
|
|
@@ -303,13 +404,14 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
303
404
|
});
|
|
304
405
|
const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
|
|
305
406
|
const agentSecrets = agentConfig.agent.secrets;
|
|
306
|
-
|
|
407
|
+
const agentSecretEntries = agentConfig.secret ?? {};
|
|
408
|
+
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
307
409
|
const merged = [];
|
|
308
410
|
const overridden = [];
|
|
309
411
|
const warnings = [];
|
|
310
412
|
for (const key of agentSecrets) {
|
|
311
413
|
merged.push(key);
|
|
312
|
-
if (
|
|
414
|
+
if (agentSecretEntries[key]) overridden.push(key);
|
|
313
415
|
}
|
|
314
416
|
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
315
417
|
const agentIdentity = agentConfig.agent ? (() => {
|
|
@@ -323,7 +425,7 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
323
425
|
...agentIdentity,
|
|
324
426
|
name: agentIdentity.name
|
|
325
427
|
} : void 0,
|
|
326
|
-
|
|
428
|
+
secret: resolvedMeta
|
|
327
429
|
},
|
|
328
430
|
catalogPath,
|
|
329
431
|
merged,
|
|
@@ -531,6 +633,10 @@ const formatAuditMinimal = (audit) => {
|
|
|
531
633
|
if (audit.missing > 0) parts.push(`${audit.missing} missing`);
|
|
532
634
|
return `${audit.status === "critical" ? `${RED}✗${RESET}` : `${YELLOW}⚠${RESET}`} ${parts.join(", ")}`;
|
|
533
635
|
};
|
|
636
|
+
const formatConfigSource = (path, source) => {
|
|
637
|
+
if (source === "cwd") return "";
|
|
638
|
+
return `${DIM}envpkt: loaded ${path}${RESET}`;
|
|
639
|
+
};
|
|
534
640
|
|
|
535
641
|
//#endregion
|
|
536
642
|
//#region src/cli/commands/audit.ts
|
|
@@ -538,7 +644,9 @@ const runAudit = (options) => {
|
|
|
538
644
|
resolveConfigPath(options.config).fold((err) => {
|
|
539
645
|
console.error(formatError(err));
|
|
540
646
|
process.exit(2);
|
|
541
|
-
}, (path) => {
|
|
647
|
+
}, ({ path, source }) => {
|
|
648
|
+
const sourceMsg = formatConfigSource(path, source);
|
|
649
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
542
650
|
loadConfig(path).fold((err) => {
|
|
543
651
|
console.error(formatError(err));
|
|
544
652
|
process.exit(2);
|
|
@@ -553,32 +661,373 @@ const runAudit = (options) => {
|
|
|
553
661
|
});
|
|
554
662
|
});
|
|
555
663
|
};
|
|
664
|
+
const formatEnvAuditTable = (config) => {
|
|
665
|
+
const envAudit = computeEnvAudit(config);
|
|
666
|
+
if (envAudit.total === 0) {
|
|
667
|
+
console.log(`${DIM}No [env.*] entries configured.${RESET}`);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
console.log(`\n${BOLD}Environment Defaults${RESET} (${envAudit.total} entries)`);
|
|
671
|
+
for (const entry of envAudit.entries) {
|
|
672
|
+
const statusIcon = entry.status === "default" ? `${GREEN}=${RESET}` : entry.status === "overridden" ? `${YELLOW}~${RESET}` : `${RED}!${RESET}`;
|
|
673
|
+
const statusLabel = entry.status === "default" ? `${DIM}using default${RESET}` : entry.status === "overridden" ? `${YELLOW}overridden${RESET} (${entry.currentValue})` : `${RED}not set${RESET}`;
|
|
674
|
+
console.log(` ${statusIcon} ${BOLD}${entry.key}${RESET} = "${entry.defaultValue}" ${statusLabel}`);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const formatEnvAuditJson = (config) => {
|
|
678
|
+
const envAudit = computeEnvAudit(config);
|
|
679
|
+
return JSON.stringify(envAudit, null, 2);
|
|
680
|
+
};
|
|
556
681
|
const runAuditOnConfig = (config, options) => {
|
|
682
|
+
if (options.envOnly) {
|
|
683
|
+
if (options.format === "json") console.log(formatEnvAuditJson(config));
|
|
684
|
+
else formatEnvAuditTable(config);
|
|
685
|
+
process.exit(0);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
557
688
|
const audit = computeAudit(config);
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const filteredSecrets = audit.secrets.filter((s) => s.status === statusFilter);
|
|
562
|
-
filtered = {
|
|
689
|
+
const afterSealed = options.sealed ? (() => {
|
|
690
|
+
const secretEntries = config.secret ?? {};
|
|
691
|
+
return {
|
|
563
692
|
...audit,
|
|
564
|
-
secrets:
|
|
693
|
+
secrets: audit.secrets.filter((s) => !!secretEntries[s.key]?.encrypted_value)
|
|
565
694
|
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
secrets: filteredSecrets
|
|
695
|
+
})() : audit;
|
|
696
|
+
const afterExternal = options.external ? (() => {
|
|
697
|
+
const secretEntries = config.secret ?? {};
|
|
698
|
+
return {
|
|
699
|
+
...afterSealed,
|
|
700
|
+
secrets: afterSealed.secrets.filter((s) => !secretEntries[s.key]?.encrypted_value)
|
|
573
701
|
};
|
|
574
|
-
}
|
|
702
|
+
})() : afterSealed;
|
|
703
|
+
const afterStatus = options.status ? {
|
|
704
|
+
...afterExternal,
|
|
705
|
+
secrets: afterExternal.secrets.filter((s) => s.status === options.status)
|
|
706
|
+
} : afterExternal;
|
|
707
|
+
const filtered = options.expiring !== void 0 ? {
|
|
708
|
+
...afterStatus,
|
|
709
|
+
secrets: afterStatus.secrets.filter((s) => s.days_remaining.fold(() => false, (d) => d >= 0 && d <= options.expiring))
|
|
710
|
+
} : afterStatus;
|
|
575
711
|
if (options.format === "json") console.log(formatAuditJson(filtered));
|
|
576
712
|
else if (options.format === "minimal") console.log(formatAuditMinimal(filtered));
|
|
577
713
|
else console.log(formatAudit(filtered));
|
|
714
|
+
if (options.all) if (options.format === "json") console.log(formatEnvAuditJson(config));
|
|
715
|
+
else formatEnvAuditTable(config);
|
|
578
716
|
const code = options.strict ? exitCodeForAudit(audit) : audit.status === "critical" ? 2 : 0;
|
|
579
717
|
process.exit(code);
|
|
580
718
|
};
|
|
581
719
|
|
|
720
|
+
//#endregion
|
|
721
|
+
//#region src/fnox/cli.ts
|
|
722
|
+
/** Export all secrets from fnox as key=value pairs for a given profile */
|
|
723
|
+
const fnoxExport = (profile, agentKey) => {
|
|
724
|
+
const args = profile ? [
|
|
725
|
+
"export",
|
|
726
|
+
"--profile",
|
|
727
|
+
profile
|
|
728
|
+
] : ["export"];
|
|
729
|
+
const env = agentKey ? {
|
|
730
|
+
...process.env,
|
|
731
|
+
FNOX_AGE_KEY: agentKey
|
|
732
|
+
} : void 0;
|
|
733
|
+
return Try(() => execFileSync("fnox", args, {
|
|
734
|
+
stdio: "pipe",
|
|
735
|
+
encoding: "utf-8",
|
|
736
|
+
env
|
|
737
|
+
})).fold((err) => Left({
|
|
738
|
+
_tag: "FnoxCliError",
|
|
739
|
+
message: `fnox export failed: ${err}`
|
|
740
|
+
}), (output) => {
|
|
741
|
+
const entries = {};
|
|
742
|
+
for (const line of output.split("\n")) {
|
|
743
|
+
const eq = line.indexOf("=");
|
|
744
|
+
if (eq > 0) {
|
|
745
|
+
const key = line.slice(0, eq).trim();
|
|
746
|
+
entries[key] = line.slice(eq + 1).trim();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return Right(entries);
|
|
750
|
+
});
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
//#endregion
|
|
754
|
+
//#region src/fnox/detect.ts
|
|
755
|
+
const FNOX_CONFIG = "fnox.toml";
|
|
756
|
+
/** Detect fnox.toml in the given directory */
|
|
757
|
+
const detectFnox = (dir) => {
|
|
758
|
+
const candidate = join(dir, FNOX_CONFIG);
|
|
759
|
+
return existsSync(candidate) ? Option(candidate) : Option(void 0);
|
|
760
|
+
};
|
|
761
|
+
/** Check if fnox CLI is available on PATH */
|
|
762
|
+
const fnoxAvailable = () => Try(() => {
|
|
763
|
+
execFileSync("fnox", ["--version"], { stdio: "pipe" });
|
|
764
|
+
return true;
|
|
765
|
+
}).fold(() => false, (v) => v);
|
|
766
|
+
|
|
767
|
+
//#endregion
|
|
768
|
+
//#region src/fnox/identity.ts
|
|
769
|
+
/** Check if the age CLI is available on PATH */
|
|
770
|
+
const ageAvailable = () => Try(() => {
|
|
771
|
+
execFileSync("age", ["--version"], { stdio: "pipe" });
|
|
772
|
+
return true;
|
|
773
|
+
}).fold(() => false, (v) => v);
|
|
774
|
+
/** Unwrap an encrypted agent key using age --decrypt */
|
|
775
|
+
const unwrapAgentKey = (identityPath) => {
|
|
776
|
+
if (!existsSync(identityPath)) return Left({
|
|
777
|
+
_tag: "IdentityNotFound",
|
|
778
|
+
path: identityPath
|
|
779
|
+
});
|
|
780
|
+
if (!ageAvailable()) return Left({
|
|
781
|
+
_tag: "AgeNotFound",
|
|
782
|
+
message: "age CLI not found on PATH"
|
|
783
|
+
});
|
|
784
|
+
return Try(() => execFileSync("age", ["--decrypt", identityPath], {
|
|
785
|
+
stdio: [
|
|
786
|
+
"pipe",
|
|
787
|
+
"pipe",
|
|
788
|
+
"pipe"
|
|
789
|
+
],
|
|
790
|
+
encoding: "utf-8"
|
|
791
|
+
})).fold((err) => Left({
|
|
792
|
+
_tag: "DecryptFailed",
|
|
793
|
+
message: `age decrypt failed: ${err}`
|
|
794
|
+
}), (output) => Right(output.trim()));
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
//#endregion
|
|
798
|
+
//#region src/fnox/parse.ts
|
|
799
|
+
/** Read and parse fnox.toml, extracting secret keys and profiles */
|
|
800
|
+
const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((err) => Left({
|
|
801
|
+
_tag: "FnoxParseError",
|
|
802
|
+
message: `Failed to read ${path}: ${err}`
|
|
803
|
+
}), (content) => Try(() => parse(content)).fold((err) => Left({
|
|
804
|
+
_tag: "FnoxParseError",
|
|
805
|
+
message: `Failed to parse fnox.toml: ${err}`
|
|
806
|
+
}), (data) => {
|
|
807
|
+
const profiles = data["profiles"] && typeof data["profiles"] === "object" ? Option(data["profiles"]) : Option(void 0);
|
|
808
|
+
const secrets = { ...data };
|
|
809
|
+
delete secrets["profiles"];
|
|
810
|
+
return Right({
|
|
811
|
+
secrets,
|
|
812
|
+
profiles
|
|
813
|
+
});
|
|
814
|
+
}));
|
|
815
|
+
/** Extract the set of secret key names from a parsed fnox config */
|
|
816
|
+
const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
|
|
817
|
+
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/core/seal.ts
|
|
820
|
+
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
821
|
+
const ageEncrypt = (plaintext, recipient) => {
|
|
822
|
+
if (!ageAvailable()) return Left({
|
|
823
|
+
_tag: "AgeNotFound",
|
|
824
|
+
message: "age CLI not found on PATH"
|
|
825
|
+
});
|
|
826
|
+
return Try(() => execFileSync("age", [
|
|
827
|
+
"--encrypt",
|
|
828
|
+
"--recipient",
|
|
829
|
+
recipient,
|
|
830
|
+
"--armor"
|
|
831
|
+
], {
|
|
832
|
+
input: plaintext,
|
|
833
|
+
stdio: [
|
|
834
|
+
"pipe",
|
|
835
|
+
"pipe",
|
|
836
|
+
"pipe"
|
|
837
|
+
],
|
|
838
|
+
encoding: "utf-8"
|
|
839
|
+
})).fold((err) => Left({
|
|
840
|
+
_tag: "EncryptFailed",
|
|
841
|
+
key: "",
|
|
842
|
+
message: `age encrypt failed: ${err}`
|
|
843
|
+
}), (output) => Right(output.trim()));
|
|
844
|
+
};
|
|
845
|
+
/** Decrypt an age-armored ciphertext using the given identity file */
|
|
846
|
+
const ageDecrypt = (ciphertext, identityPath) => {
|
|
847
|
+
if (!ageAvailable()) return Left({
|
|
848
|
+
_tag: "AgeNotFound",
|
|
849
|
+
message: "age CLI not found on PATH"
|
|
850
|
+
});
|
|
851
|
+
return Try(() => execFileSync("age", [
|
|
852
|
+
"--decrypt",
|
|
853
|
+
"--identity",
|
|
854
|
+
identityPath
|
|
855
|
+
], {
|
|
856
|
+
input: ciphertext,
|
|
857
|
+
stdio: [
|
|
858
|
+
"pipe",
|
|
859
|
+
"pipe",
|
|
860
|
+
"pipe"
|
|
861
|
+
],
|
|
862
|
+
encoding: "utf-8"
|
|
863
|
+
})).fold((err) => Left({
|
|
864
|
+
_tag: "DecryptFailed",
|
|
865
|
+
key: "",
|
|
866
|
+
message: `age decrypt failed: ${err}`
|
|
867
|
+
}), (output) => Right(output.trim()));
|
|
868
|
+
};
|
|
869
|
+
/** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
|
|
870
|
+
const sealSecrets = (meta, values, recipient) => {
|
|
871
|
+
if (!ageAvailable()) return Left({
|
|
872
|
+
_tag: "AgeNotFound",
|
|
873
|
+
message: "age CLI not found on PATH"
|
|
874
|
+
});
|
|
875
|
+
const result = {};
|
|
876
|
+
for (const [key, secretMeta] of Object.entries(meta)) {
|
|
877
|
+
const plaintext = values[key];
|
|
878
|
+
if (plaintext === void 0) {
|
|
879
|
+
result[key] = secretMeta;
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
|
|
883
|
+
_tag: "EncryptFailed",
|
|
884
|
+
key,
|
|
885
|
+
message: err.message
|
|
886
|
+
}), (ciphertext) => Right(ciphertext));
|
|
887
|
+
const failed = outcome.fold((err) => err, () => void 0);
|
|
888
|
+
if (failed) return Left(failed);
|
|
889
|
+
const ciphertext = outcome.fold(() => "", (v) => v);
|
|
890
|
+
result[key] = {
|
|
891
|
+
...secretMeta,
|
|
892
|
+
encrypted_value: ciphertext
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
return Right(result);
|
|
896
|
+
};
|
|
897
|
+
/** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
|
|
898
|
+
const unsealSecrets = (meta, identityPath) => {
|
|
899
|
+
if (!ageAvailable()) return Left({
|
|
900
|
+
_tag: "AgeNotFound",
|
|
901
|
+
message: "age CLI not found on PATH"
|
|
902
|
+
});
|
|
903
|
+
const result = {};
|
|
904
|
+
for (const [key, secretMeta] of Object.entries(meta)) {
|
|
905
|
+
if (!secretMeta.encrypted_value) continue;
|
|
906
|
+
const outcome = ageDecrypt(secretMeta.encrypted_value, identityPath).fold((err) => Left({
|
|
907
|
+
_tag: "DecryptFailed",
|
|
908
|
+
key,
|
|
909
|
+
message: err.message
|
|
910
|
+
}), (plaintext) => Right(plaintext));
|
|
911
|
+
const failed = outcome.fold((err) => err, () => void 0);
|
|
912
|
+
if (failed) return Left(failed);
|
|
913
|
+
result[key] = outcome.fold(() => "", (v) => v);
|
|
914
|
+
}
|
|
915
|
+
return Right(result);
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
//#endregion
|
|
919
|
+
//#region src/core/boot.ts
|
|
920
|
+
const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), ({ path: configPath, source: configSource }) => loadConfig(configPath).fold((err) => Left(err), (config) => {
|
|
921
|
+
const configDir = dirname(configPath);
|
|
922
|
+
return resolveConfig(config, configDir).fold((err) => Left(err), (result) => Right({
|
|
923
|
+
config: result.config,
|
|
924
|
+
configPath,
|
|
925
|
+
configDir,
|
|
926
|
+
configSource
|
|
927
|
+
}));
|
|
928
|
+
}));
|
|
929
|
+
const resolveAgentKey = (config, configDir) => {
|
|
930
|
+
if (!config.agent?.identity) return Right(void 0);
|
|
931
|
+
return unwrapAgentKey(resolve(configDir, expandPath(config.agent.identity))).fold((err) => Left(err), (key) => Right(key));
|
|
932
|
+
};
|
|
933
|
+
const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
|
|
934
|
+
const checkExpiration = (audit, failOnExpired, warnOnly) => {
|
|
935
|
+
const warnings = [];
|
|
936
|
+
if (audit.expired > 0 && failOnExpired && !warnOnly) return Left({
|
|
937
|
+
_tag: "AuditFailed",
|
|
938
|
+
audit,
|
|
939
|
+
message: `${audit.expired} secret(s) have expired`
|
|
940
|
+
});
|
|
941
|
+
if (audit.expired > 0 && warnOnly) warnings.push(`${audit.expired} secret(s) have expired (warn-only mode)`);
|
|
942
|
+
return Right(warnings);
|
|
943
|
+
};
|
|
944
|
+
const SECRET_PATTERNS = [
|
|
945
|
+
/^sk-/,
|
|
946
|
+
/^ghp_/,
|
|
947
|
+
/^ghu_/,
|
|
948
|
+
/^AKIA[0-9A-Z]{16}/,
|
|
949
|
+
/^xox[bpras]-/,
|
|
950
|
+
/:\/\/[^:]+:[^@]+@/,
|
|
951
|
+
/^ey[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/
|
|
952
|
+
];
|
|
953
|
+
const looksLikeSecret = (value) => {
|
|
954
|
+
if (SECRET_PATTERNS.some((p) => p.test(value))) return true;
|
|
955
|
+
if (value.length > 40 && /^[A-Za-z0-9+/=]+$/.test(value)) return true;
|
|
956
|
+
return false;
|
|
957
|
+
};
|
|
958
|
+
const checkEnvMisclassification = (config) => {
|
|
959
|
+
const warnings = [];
|
|
960
|
+
const envEntries = config.env ?? {};
|
|
961
|
+
for (const [key, entry] of Object.entries(envEntries)) if (looksLikeSecret(entry.value)) warnings.push(`[env.${key}] value looks like a secret — consider moving to [secret.${key}]`);
|
|
962
|
+
return warnings;
|
|
963
|
+
};
|
|
964
|
+
/** Programmatic boot — returns Either<BootError, BootResult> */
|
|
965
|
+
const bootSafe = (options) => {
|
|
966
|
+
const opts = options ?? {};
|
|
967
|
+
const inject = opts.inject !== false;
|
|
968
|
+
const failOnExpired = opts.failOnExpired !== false;
|
|
969
|
+
const warnOnly = opts.warnOnly ?? false;
|
|
970
|
+
return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
|
|
971
|
+
const secretEntries = config.secret ?? {};
|
|
972
|
+
const metaKeys = Object.keys(secretEntries);
|
|
973
|
+
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]?.encrypted_value);
|
|
974
|
+
const agentKeyResult = resolveAgentKey(config, configDir);
|
|
975
|
+
const agentKey = agentKeyResult.fold(() => void 0, (k) => k);
|
|
976
|
+
const agentKeyError = agentKeyResult.fold((err) => err, () => void 0);
|
|
977
|
+
if (agentKeyError && !hasSealedValues) return Left(agentKeyError);
|
|
978
|
+
const audit = computeAudit(config, detectFnoxKeys(configDir));
|
|
979
|
+
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
980
|
+
const secrets = {};
|
|
981
|
+
const injected = [];
|
|
982
|
+
const skipped = [];
|
|
983
|
+
warnings.push(...checkEnvMisclassification(config));
|
|
984
|
+
const envEntries = config.env ?? {};
|
|
985
|
+
const envDefaults = {};
|
|
986
|
+
const overridden = [];
|
|
987
|
+
for (const [key, entry] of Object.entries(envEntries)) if (process.env[key] === void 0) {
|
|
988
|
+
envDefaults[key] = entry.value;
|
|
989
|
+
if (inject) process.env[key] = entry.value;
|
|
990
|
+
} else overridden.push(key);
|
|
991
|
+
const sealedKeys = /* @__PURE__ */ new Set();
|
|
992
|
+
if (hasSealedValues && config.agent?.identity) unsealSecrets(secretEntries, resolve(configDir, expandPath(config.agent.identity))).fold((err) => {
|
|
993
|
+
warnings.push(`Sealed value decryption failed: ${err.message}`);
|
|
994
|
+
}, (unsealed) => {
|
|
995
|
+
for (const [key, value] of Object.entries(unsealed)) {
|
|
996
|
+
secrets[key] = value;
|
|
997
|
+
injected.push(key);
|
|
998
|
+
sealedKeys.add(key);
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
|
|
1002
|
+
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, agentKey).fold((err) => {
|
|
1003
|
+
warnings.push(`fnox export failed: ${err.message}`);
|
|
1004
|
+
for (const key of remainingKeys) skipped.push(key);
|
|
1005
|
+
}, (exported) => {
|
|
1006
|
+
for (const key of remainingKeys) if (key in exported) {
|
|
1007
|
+
secrets[key] = exported[key];
|
|
1008
|
+
injected.push(key);
|
|
1009
|
+
} else skipped.push(key);
|
|
1010
|
+
});
|
|
1011
|
+
else {
|
|
1012
|
+
if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
|
|
1013
|
+
for (const key of remainingKeys) skipped.push(key);
|
|
1014
|
+
}
|
|
1015
|
+
if (inject) for (const [key, value] of Object.entries(secrets)) process.env[key] = value;
|
|
1016
|
+
return {
|
|
1017
|
+
audit,
|
|
1018
|
+
injected,
|
|
1019
|
+
skipped,
|
|
1020
|
+
secrets,
|
|
1021
|
+
warnings,
|
|
1022
|
+
envDefaults,
|
|
1023
|
+
overridden,
|
|
1024
|
+
configPath,
|
|
1025
|
+
configSource
|
|
1026
|
+
};
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
};
|
|
1030
|
+
|
|
582
1031
|
//#endregion
|
|
583
1032
|
//#region src/core/patterns.ts
|
|
584
1033
|
const EXCLUDED_VARS = new Set([
|
|
@@ -1210,7 +1659,7 @@ const matchValueShape = (value) => {
|
|
|
1210
1659
|
};
|
|
1211
1660
|
/** Strip common suffixes and derive a service name from an env var name */
|
|
1212
1661
|
const deriveServiceFromName = (name) => {
|
|
1213
|
-
const
|
|
1662
|
+
const matchedSuffix = [
|
|
1214
1663
|
"_API_KEY",
|
|
1215
1664
|
"_SECRET_KEY",
|
|
1216
1665
|
"_ACCESS_KEY",
|
|
@@ -1228,13 +1677,8 @@ const deriveServiceFromName = (name) => {
|
|
|
1228
1677
|
"_DSN",
|
|
1229
1678
|
"_URL",
|
|
1230
1679
|
"_URI"
|
|
1231
|
-
];
|
|
1232
|
-
|
|
1233
|
-
for (const suffix of suffixes) if (stripped.endsWith(suffix)) {
|
|
1234
|
-
stripped = stripped.slice(0, -suffix.length);
|
|
1235
|
-
break;
|
|
1236
|
-
}
|
|
1237
|
-
return stripped.toLowerCase().replace(/_/g, "-");
|
|
1680
|
+
].find((s) => name.endsWith(s));
|
|
1681
|
+
return (matchedSuffix ? name.slice(0, -matchedSuffix.length) : name).toLowerCase().replace(/_/g, "-");
|
|
1238
1682
|
};
|
|
1239
1683
|
/** Match a single env var against all patterns */
|
|
1240
1684
|
const matchEnvVar = (name, value) => {
|
|
@@ -1304,10 +1748,11 @@ const envScan = (env, options) => {
|
|
|
1304
1748
|
/** Bidirectional drift detection between config and live environment */
|
|
1305
1749
|
const envCheck = (config, env) => {
|
|
1306
1750
|
const entries = [];
|
|
1307
|
-
const
|
|
1751
|
+
const secretEntries = config.secret ?? {};
|
|
1752
|
+
const metaKeys = Object.keys(secretEntries);
|
|
1308
1753
|
const trackedSet = new Set(metaKeys);
|
|
1309
1754
|
for (const key of metaKeys) {
|
|
1310
|
-
const meta =
|
|
1755
|
+
const meta = secretEntries[key];
|
|
1311
1756
|
const present = env[key] !== void 0 && env[key] !== "";
|
|
1312
1757
|
entries.push({
|
|
1313
1758
|
envVar: key,
|
|
@@ -1316,6 +1761,17 @@ const envCheck = (config, env) => {
|
|
|
1316
1761
|
confidence: Option(void 0)
|
|
1317
1762
|
});
|
|
1318
1763
|
}
|
|
1764
|
+
const envDefaults = config.env ?? {};
|
|
1765
|
+
for (const key of Object.keys(envDefaults)) if (!trackedSet.has(key)) {
|
|
1766
|
+
trackedSet.add(key);
|
|
1767
|
+
const present = env[key] !== void 0 && env[key] !== "";
|
|
1768
|
+
entries.push({
|
|
1769
|
+
envVar: key,
|
|
1770
|
+
service: Option(void 0),
|
|
1771
|
+
status: present ? "tracked" : "missing_from_env",
|
|
1772
|
+
confidence: Option(void 0)
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1319
1775
|
const envMatches = scanEnv(env);
|
|
1320
1776
|
for (const match of envMatches) if (!trackedSet.has(match.envVar)) entries.push({
|
|
1321
1777
|
envVar: match.envVar,
|
|
@@ -1335,12 +1791,12 @@ const envCheck = (config, env) => {
|
|
|
1335
1791
|
};
|
|
1336
1792
|
};
|
|
1337
1793
|
const todayIso$1 = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1338
|
-
/** Generate TOML [
|
|
1794
|
+
/** Generate TOML [secret.*] blocks from scan results, mirroring init.ts pattern */
|
|
1339
1795
|
const generateTomlFromScan = (matches) => {
|
|
1340
1796
|
const blocks = [];
|
|
1341
1797
|
for (const match of matches) {
|
|
1342
1798
|
const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
|
|
1343
|
-
blocks.push(`[
|
|
1799
|
+
blocks.push(`[secret.${match.envVar}]
|
|
1344
1800
|
service = "${svc}"
|
|
1345
1801
|
# purpose = "" # Why: what this secret enables
|
|
1346
1802
|
# capabilities = [] # What operations this grants
|
|
@@ -1371,16 +1827,16 @@ const runEnvScan = (options) => {
|
|
|
1371
1827
|
console.log(toml);
|
|
1372
1828
|
return;
|
|
1373
1829
|
}
|
|
1374
|
-
const configPath = join(process.cwd(), "envpkt.toml");
|
|
1830
|
+
const configPath = resolve(options.config ?? join(process.cwd(), "envpkt.toml"));
|
|
1375
1831
|
if (existsSync(configPath)) {
|
|
1376
1832
|
const existing = Try(() => readFileSync(configPath, "utf-8")).fold(() => "", (c) => c);
|
|
1377
|
-
const newEntries = scan.discovered.toArray().filter((m) => !existing.includes(`[
|
|
1833
|
+
const newEntries = scan.discovered.toArray().filter((m) => !existing.includes(`[secret.${m.envVar}]`));
|
|
1378
1834
|
if (newEntries.length === 0) {
|
|
1379
|
-
console.log(`\n${GREEN}✓${RESET} All discovered credentials already tracked in
|
|
1835
|
+
console.log(`\n${GREEN}✓${RESET} All discovered credentials already tracked in ${CYAN}${configPath}${RESET}`);
|
|
1380
1836
|
return;
|
|
1381
1837
|
}
|
|
1382
1838
|
const newToml = generateTomlFromScan(newEntries);
|
|
1383
|
-
Try(() => writeFileSync(configPath, existing.trimEnd()
|
|
1839
|
+
Try(() => writeFileSync(configPath, `${existing.trimEnd()}\n\n${newToml}`, "utf-8")).fold((err) => {
|
|
1384
1840
|
console.error(`\n${RED}Error:${RESET} Failed to write: ${err}`);
|
|
1385
1841
|
process.exit(1);
|
|
1386
1842
|
}, () => {
|
|
@@ -1392,7 +1848,7 @@ const runEnvScan = (options) => {
|
|
|
1392
1848
|
console.error(`\n${RED}Error:${RESET} Failed to write: ${err}`);
|
|
1393
1849
|
process.exit(1);
|
|
1394
1850
|
}, () => {
|
|
1395
|
-
console.log(`\n${GREEN}✓${RESET} Created ${
|
|
1851
|
+
console.log(`\n${GREEN}✓${RESET} Created ${CYAN}${configPath}${RESET} with ${BOLD}${scan.discovered.size}${RESET} credential(s)`);
|
|
1396
1852
|
});
|
|
1397
1853
|
}
|
|
1398
1854
|
}
|
|
@@ -1401,7 +1857,9 @@ const runEnvCheck = (options) => {
|
|
|
1401
1857
|
resolveConfigPath(options.config).fold((err) => {
|
|
1402
1858
|
console.error(formatError(err));
|
|
1403
1859
|
process.exit(2);
|
|
1404
|
-
}, (path) => {
|
|
1860
|
+
}, ({ path, source }) => {
|
|
1861
|
+
const sourceMsg = formatConfigSource(path, source);
|
|
1862
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
1405
1863
|
loadConfig(path).fold((err) => {
|
|
1406
1864
|
console.error(formatError(err));
|
|
1407
1865
|
process.exit(2);
|
|
@@ -1418,43 +1876,23 @@ const runEnvCheck = (options) => {
|
|
|
1418
1876
|
});
|
|
1419
1877
|
});
|
|
1420
1878
|
};
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}).fold(() =>
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
/** Unwrap an encrypted agent key using age --decrypt */
|
|
1438
|
-
const unwrapAgentKey = (identityPath) => {
|
|
1439
|
-
if (!existsSync(identityPath)) return Left({
|
|
1440
|
-
_tag: "IdentityNotFound",
|
|
1441
|
-
path: identityPath
|
|
1442
|
-
});
|
|
1443
|
-
if (!ageAvailable()) return Left({
|
|
1444
|
-
_tag: "AgeNotFound",
|
|
1445
|
-
message: "age CLI not found on PATH"
|
|
1879
|
+
const shellEscape = (value) => value.replace(/'/g, "'\\''");
|
|
1880
|
+
const runEnvExport = (options) => {
|
|
1881
|
+
bootSafe({
|
|
1882
|
+
inject: false,
|
|
1883
|
+
configPath: options.config,
|
|
1884
|
+
profile: options.profile,
|
|
1885
|
+
warnOnly: true
|
|
1886
|
+
}).fold((err) => {
|
|
1887
|
+
console.error(formatError(err));
|
|
1888
|
+
process.exit(2);
|
|
1889
|
+
}, (boot) => {
|
|
1890
|
+
const sourceMsg = formatConfigSource(boot.configPath, boot.configSource);
|
|
1891
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
1892
|
+
for (const warning of boot.warnings) console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
1893
|
+
for (const [key, value] of Object.entries(boot.envDefaults)) console.log(`export ${key}='${shellEscape(value)}'`);
|
|
1894
|
+
for (const [key, value] of Object.entries(boot.secrets)) console.log(`export ${key}='${shellEscape(value)}'`);
|
|
1446
1895
|
});
|
|
1447
|
-
return Try(() => execFileSync("age", ["--decrypt", identityPath], {
|
|
1448
|
-
stdio: [
|
|
1449
|
-
"pipe",
|
|
1450
|
-
"pipe",
|
|
1451
|
-
"pipe"
|
|
1452
|
-
],
|
|
1453
|
-
encoding: "utf-8"
|
|
1454
|
-
})).fold((err) => Left({
|
|
1455
|
-
_tag: "DecryptFailed",
|
|
1456
|
-
message: `age decrypt failed: ${err}`
|
|
1457
|
-
}), (output) => Right(output.trim()));
|
|
1458
1896
|
};
|
|
1459
1897
|
|
|
1460
1898
|
//#endregion
|
|
@@ -1465,71 +1903,40 @@ const runExec = (args, options) => {
|
|
|
1465
1903
|
process.exit(2);
|
|
1466
1904
|
return;
|
|
1467
1905
|
}
|
|
1468
|
-
const skipAudit = options.skipAudit
|
|
1469
|
-
const
|
|
1906
|
+
const skipAudit = options.skipAudit ?? options.check === false;
|
|
1907
|
+
const boot = bootSafe({
|
|
1908
|
+
inject: false,
|
|
1909
|
+
configPath: options.config,
|
|
1910
|
+
profile: options.profile,
|
|
1911
|
+
failOnExpired: false,
|
|
1912
|
+
warnOnly: true
|
|
1913
|
+
}).fold((err) => {
|
|
1470
1914
|
console.error(formatError(err));
|
|
1471
1915
|
process.exit(2);
|
|
1472
|
-
}, (
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
config,
|
|
1477
|
-
path
|
|
1478
|
-
})));
|
|
1479
|
-
if (!configData) return;
|
|
1480
|
-
const { config, path } = configData;
|
|
1481
|
-
const configDir = dirname(path);
|
|
1916
|
+
}, (b) => b);
|
|
1917
|
+
if (!boot) return;
|
|
1918
|
+
const sourceMsg = formatConfigSource(boot.configPath, boot.configSource);
|
|
1919
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
1482
1920
|
if (!skipAudit) {
|
|
1483
|
-
|
|
1484
|
-
console.error(
|
|
1485
|
-
console.error(formatAudit(audit));
|
|
1921
|
+
console.error(`${BOLD}envpkt${RESET} pre-flight audit`);
|
|
1922
|
+
console.error(formatAudit(boot.audit));
|
|
1486
1923
|
console.error("");
|
|
1487
|
-
if (options.strict && audit.status !== "healthy") {
|
|
1488
|
-
console.error(`${RED}Aborting:${RESET} --strict mode and audit status is ${audit.status}`);
|
|
1489
|
-
process.exit(exitCodeForAudit(audit));
|
|
1924
|
+
if (options.strict && boot.audit.status !== "healthy") {
|
|
1925
|
+
console.error(`${RED}Aborting:${RESET} --strict mode and audit status is ${boot.audit.status}`);
|
|
1926
|
+
process.exit(exitCodeForAudit(boot.audit));
|
|
1490
1927
|
return;
|
|
1491
1928
|
}
|
|
1492
|
-
if (audit.status === "critical" && !options.warnOnly) {
|
|
1929
|
+
if (boot.audit.status === "critical" && !options.warnOnly) {
|
|
1493
1930
|
console.error(`${RED}Aborting:${RESET} audit status is critical (use --warn-only to proceed)`);
|
|
1494
|
-
process.exit(exitCodeForAudit(audit));
|
|
1931
|
+
process.exit(exitCodeForAudit(boot.audit));
|
|
1495
1932
|
return;
|
|
1496
1933
|
}
|
|
1497
|
-
if (audit.status === "critical" && options.warnOnly) console.error(`${YELLOW}Warning:${RESET} Proceeding despite critical audit status (--warn-only)`);
|
|
1934
|
+
if (boot.audit.status === "critical" && options.warnOnly) console.error(`${YELLOW}Warning:${RESET} Proceeding despite critical audit status (--warn-only)`);
|
|
1498
1935
|
}
|
|
1499
|
-
|
|
1500
|
-
if (config.agent?.identity) unwrapAgentKey(resolve(configDir, config.agent.identity)).fold((err) => {
|
|
1501
|
-
console.error(`${YELLOW}Warning:${RESET} Agent key unwrap failed: ${err._tag}`);
|
|
1502
|
-
}, (key) => {
|
|
1503
|
-
agentKey = key;
|
|
1504
|
-
});
|
|
1505
|
-
if (!fnoxAvailable()) console.error(`${YELLOW}Warning:${RESET} fnox not available — running command without secret injection`);
|
|
1936
|
+
for (const warning of boot.warnings) console.error(`${YELLOW}Warning:${RESET} ${warning}`);
|
|
1506
1937
|
const env = { ...process.env };
|
|
1507
|
-
if (
|
|
1508
|
-
|
|
1509
|
-
"export",
|
|
1510
|
-
"--profile",
|
|
1511
|
-
options.profile
|
|
1512
|
-
] : ["export"];
|
|
1513
|
-
const fnoxEnv = agentKey ? {
|
|
1514
|
-
...process.env,
|
|
1515
|
-
FNOX_AGE_KEY: agentKey
|
|
1516
|
-
} : void 0;
|
|
1517
|
-
Try(() => execFileSync("fnox", fnoxArgs, {
|
|
1518
|
-
stdio: "pipe",
|
|
1519
|
-
encoding: "utf-8",
|
|
1520
|
-
env: fnoxEnv
|
|
1521
|
-
})).fold((err) => {
|
|
1522
|
-
console.error(`${YELLOW}Warning:${RESET} fnox export failed: ${err}`);
|
|
1523
|
-
}, (output) => {
|
|
1524
|
-
for (const line of output.split("\n")) {
|
|
1525
|
-
const eq = line.indexOf("=");
|
|
1526
|
-
if (eq > 0) {
|
|
1527
|
-
const key = line.slice(0, eq).trim();
|
|
1528
|
-
env[key] = line.slice(eq + 1).trim();
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1938
|
+
for (const [key, value] of Object.entries(boot.envDefaults)) if (!(key in env)) env[key] = value;
|
|
1939
|
+
for (const [key, value] of Object.entries(boot.secrets)) env[key] = value;
|
|
1533
1940
|
const [cmd, ...cmdArgs] = args;
|
|
1534
1941
|
try {
|
|
1535
1942
|
execFileSync(cmd, cmdArgs, {
|
|
@@ -1575,10 +1982,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
|
|
|
1575
1982
|
const configPath = join(dir, CONFIG_FILENAME$1);
|
|
1576
1983
|
if (Try(() => statSync(configPath).isFile()).fold(() => false, (v) => v)) yield configPath;
|
|
1577
1984
|
if (currentDepth >= maxDepth) return;
|
|
1578
|
-
|
|
1579
|
-
Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => {}, (e) => {
|
|
1580
|
-
entries = e;
|
|
1581
|
-
});
|
|
1985
|
+
const entries = Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => [], (e) => e);
|
|
1582
1986
|
for (const entry of entries) if (entry.isDirectory() && !SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) yield* findEnvpktFiles(join(dir, entry.name), maxDepth, currentDepth + 1);
|
|
1583
1987
|
}
|
|
1584
1988
|
const scanFleet = (rootDir, options) => {
|
|
@@ -1645,7 +2049,7 @@ const runFleet = (options) => {
|
|
|
1645
2049
|
const CONFIG_FILENAME = "envpkt.toml";
|
|
1646
2050
|
const todayIso = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1647
2051
|
const generateSecretBlock = (key, service) => {
|
|
1648
|
-
return `[
|
|
2052
|
+
return `[secret.${key}]
|
|
1649
2053
|
service = "${service ?? key}"
|
|
1650
2054
|
# purpose = "" # Why: what this secret enables
|
|
1651
2055
|
# capabilities = [] # What operations this grants
|
|
@@ -1684,18 +2088,26 @@ const generateTemplate = (options, fnoxKeys) => {
|
|
|
1684
2088
|
lines.push(`# require_expiration = false`);
|
|
1685
2089
|
lines.push(`# require_service = false`);
|
|
1686
2090
|
lines.push(``);
|
|
2091
|
+
lines.push(`# Plaintext environment defaults (non-secret, safe to commit)`);
|
|
2092
|
+
lines.push(`# [env.PORT]`);
|
|
2093
|
+
lines.push(`# value = "3000"`);
|
|
2094
|
+
lines.push(`# purpose = "Application port"`);
|
|
2095
|
+
lines.push(`# [env.NODE_ENV]`);
|
|
2096
|
+
lines.push(`# value = "production"`);
|
|
2097
|
+
lines.push(`# purpose = "Runtime environment"`);
|
|
2098
|
+
lines.push(``);
|
|
1687
2099
|
if (fnoxKeys && fnoxKeys.length > 0) {
|
|
1688
2100
|
lines.push(`# Secrets detected from fnox.toml`);
|
|
1689
2101
|
for (const key of fnoxKeys) lines.push(generateSecretBlock(key));
|
|
1690
2102
|
} else {
|
|
1691
2103
|
lines.push(`# Add your secret metadata below.`);
|
|
1692
|
-
lines.push(`# Each [
|
|
2104
|
+
lines.push(`# Each [secret.<key>] describes a secret your agent needs.`);
|
|
1693
2105
|
lines.push(``);
|
|
1694
2106
|
lines.push(generateSecretBlock("EXAMPLE_API_KEY", "example-service"));
|
|
1695
2107
|
}
|
|
1696
2108
|
} else {
|
|
1697
2109
|
lines.push(`# Optional: override catalog metadata for specific secrets`);
|
|
1698
|
-
lines.push(`# [
|
|
2110
|
+
lines.push(`# [secret.KEY_NAME]`);
|
|
1699
2111
|
lines.push(`# capabilities = ["read"] # narrows catalog's broader definition`);
|
|
1700
2112
|
}
|
|
1701
2113
|
return lines.join("\n");
|
|
@@ -1721,20 +2133,17 @@ const runInit = (dir, options) => {
|
|
|
1721
2133
|
console.error(`${RED}Error:${RESET} ${CONFIG_FILENAME} already exists. Use --force to overwrite.`);
|
|
1722
2134
|
process.exit(1);
|
|
1723
2135
|
}
|
|
1724
|
-
|
|
1725
|
-
if (options.fromFnox) {
|
|
2136
|
+
const fnoxKeys = options.fromFnox ? (() => {
|
|
1726
2137
|
const fnoxPath = options.fromFnox === "true" || options.fromFnox === "" ? join(dir, "fnox.toml") : options.fromFnox;
|
|
1727
2138
|
if (!existsSync(fnoxPath)) {
|
|
1728
2139
|
console.error(`${RED}Error:${RESET} fnox.toml not found at ${fnoxPath}`);
|
|
1729
2140
|
process.exit(1);
|
|
1730
2141
|
}
|
|
1731
|
-
readFnoxKeys(fnoxPath).fold((err) => {
|
|
2142
|
+
return readFnoxKeys(fnoxPath).fold((err) => {
|
|
1732
2143
|
console.error(`${RED}Error:${RESET} Failed to read fnox.toml: ${formatConfigError(err)}`);
|
|
1733
2144
|
process.exit(1);
|
|
1734
|
-
}, (keys) =>
|
|
1735
|
-
|
|
1736
|
-
});
|
|
1737
|
-
}
|
|
2145
|
+
}, (keys) => keys);
|
|
2146
|
+
})() : void 0;
|
|
1738
2147
|
const content = generateTemplate(options, fnoxKeys);
|
|
1739
2148
|
Try(() => writeFileSync(outPath, content, "utf-8")).fold((err) => {
|
|
1740
2149
|
console.error(`${RED}Error:${RESET} Failed to write ${CONFIG_FILENAME}: ${err}`);
|
|
@@ -1757,6 +2166,7 @@ const maskValue = (value) => {
|
|
|
1757
2166
|
//#region src/cli/commands/inspect.ts
|
|
1758
2167
|
const printSecretMeta = (meta, indent) => {
|
|
1759
2168
|
if (meta.purpose) console.log(`${indent}purpose: ${meta.purpose}`);
|
|
2169
|
+
if (meta.comment) console.log(`${indent}comment: ${DIM}${meta.comment}${RESET}`);
|
|
1760
2170
|
if (meta.capabilities) console.log(`${indent}capabilities: ${DIM}${meta.capabilities.join(", ")}${RESET}`);
|
|
1761
2171
|
const dateParts = [];
|
|
1762
2172
|
if (meta.created) dateParts.push(`created: ${meta.created}`);
|
|
@@ -1790,14 +2200,29 @@ const printConfig = (config, path, resolveResult, opts) => {
|
|
|
1790
2200
|
if (config.agent.secrets) console.log(` secrets: ${config.agent.secrets.join(", ")}`);
|
|
1791
2201
|
console.log("");
|
|
1792
2202
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
2203
|
+
const secretEntries = config.secret ?? {};
|
|
2204
|
+
console.log(`${BOLD}Secrets:${RESET} ${Object.keys(secretEntries).length}`);
|
|
2205
|
+
for (const [key, meta] of Object.entries(secretEntries)) {
|
|
1795
2206
|
const secretValue = opts?.secrets?.[key];
|
|
1796
2207
|
const valueSuffix = secretValue !== void 0 ? ` = ${YELLOW}${(opts?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}${RESET}` : "";
|
|
1797
2208
|
const sealedTag = meta.encrypted_value ? ` ${CYAN}[sealed]${RESET}` : "";
|
|
1798
2209
|
console.log(` ${BOLD}${key}${RESET} → ${meta.service ?? key}${sealedTag}${valueSuffix}`);
|
|
1799
2210
|
printSecretMeta(meta, " ");
|
|
1800
2211
|
}
|
|
2212
|
+
const envEntries = config.env ?? {};
|
|
2213
|
+
const envKeys = Object.keys(envEntries);
|
|
2214
|
+
if (envKeys.length > 0) {
|
|
2215
|
+
console.log("");
|
|
2216
|
+
console.log(`${BOLD}Environment Defaults:${RESET} ${envKeys.length}`);
|
|
2217
|
+
for (const [key, entry] of Object.entries(envEntries)) {
|
|
2218
|
+
const currentValue = process.env[key];
|
|
2219
|
+
const statusIcon = currentValue === void 0 ? `${RED}!${RESET}` : currentValue === entry.value ? `${GREEN}=${RESET}` : `${YELLOW}~${RESET}`;
|
|
2220
|
+
const statusLabel = currentValue === void 0 ? `${DIM}not set${RESET}` : currentValue === entry.value ? `${DIM}using default${RESET}` : `${YELLOW}overridden${RESET}`;
|
|
2221
|
+
console.log(` ${statusIcon} ${BOLD}${key}${RESET} = "${entry.value}" ${statusLabel}`);
|
|
2222
|
+
if (entry.purpose) console.log(` purpose: ${entry.purpose}`);
|
|
2223
|
+
if (entry.comment) console.log(` comment: ${DIM}${entry.comment}${RESET}`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1801
2226
|
if (config.lifecycle) {
|
|
1802
2227
|
console.log("");
|
|
1803
2228
|
console.log(`${BOLD}Lifecycle:${RESET}`);
|
|
@@ -1818,7 +2243,9 @@ const runInspect = (options) => {
|
|
|
1818
2243
|
resolveConfigPath(options.config).fold((err) => {
|
|
1819
2244
|
console.error(formatError(err));
|
|
1820
2245
|
process.exit(2);
|
|
1821
|
-
}, (path) => {
|
|
2246
|
+
}, ({ path, source }) => {
|
|
2247
|
+
const sourceMsg = formatConfigSource(path, source);
|
|
2248
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
1822
2249
|
loadConfig(path).fold((err) => {
|
|
1823
2250
|
console.error(formatError(err));
|
|
1824
2251
|
process.exit(2);
|
|
@@ -1827,14 +2254,15 @@ const runInspect = (options) => {
|
|
|
1827
2254
|
console.error(formatError(err));
|
|
1828
2255
|
process.exit(2);
|
|
1829
2256
|
}, (resolveResult) => {
|
|
1830
|
-
const showResolved = options.resolved
|
|
2257
|
+
const showResolved = options.resolved ?? !!resolveResult.catalogPath;
|
|
1831
2258
|
const showConfig = showResolved ? resolveResult.config : config;
|
|
1832
2259
|
if (options.format === "json") {
|
|
1833
2260
|
console.log(JSON.stringify(showConfig, null, 2));
|
|
1834
2261
|
return;
|
|
1835
2262
|
}
|
|
2263
|
+
const showSecrets = showConfig.secret ?? {};
|
|
1836
2264
|
const printOpts = options.secrets ? {
|
|
1837
|
-
secrets: Object.fromEntries(Object.keys(
|
|
2265
|
+
secrets: Object.fromEntries(Object.keys(showSecrets).filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]])),
|
|
1838
2266
|
secretDisplay: options.plaintext ? "plaintext" : "encrypted"
|
|
1839
2267
|
} : void 0;
|
|
1840
2268
|
printConfig(showConfig, path, showResolved ? resolveResult : void 0, printOpts);
|
|
@@ -1846,7 +2274,7 @@ const runInspect = (options) => {
|
|
|
1846
2274
|
//#endregion
|
|
1847
2275
|
//#region src/mcp/resources.ts
|
|
1848
2276
|
const loadConfigSafe = () => {
|
|
1849
|
-
return resolveConfigPath().fold(() => void 0, (path) => loadConfig(path).fold(() => void 0, (config) => ({
|
|
2277
|
+
return resolveConfigPath().fold(() => void 0, ({ path }) => loadConfig(path).fold(() => void 0, (config) => ({
|
|
1850
2278
|
config,
|
|
1851
2279
|
path
|
|
1852
2280
|
})));
|
|
@@ -1896,7 +2324,8 @@ const readCapabilities = () => {
|
|
|
1896
2324
|
const { config } = loaded;
|
|
1897
2325
|
const agentCapabilities = config.agent?.capabilities ?? [];
|
|
1898
2326
|
const secretCapabilities = {};
|
|
1899
|
-
|
|
2327
|
+
const secretEntries = config.secret ?? {};
|
|
2328
|
+
for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
1900
2329
|
return { contents: [{
|
|
1901
2330
|
uri: "envpkt://capabilities",
|
|
1902
2331
|
mimeType: "application/json",
|
|
@@ -1937,7 +2366,7 @@ const loadConfigForTool = (configPath) => {
|
|
|
1937
2366
|
return resolveConfigPath(configPath).fold((err) => ({
|
|
1938
2367
|
ok: false,
|
|
1939
2368
|
result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
|
|
1940
|
-
}), (path) => loadConfig(path).fold((err) => ({
|
|
2369
|
+
}), ({ path }) => loadConfig(path).fold((err) => ({
|
|
1941
2370
|
ok: false,
|
|
1942
2371
|
result: errorResult(`Config error: ${err._tag} — ${err._tag === "ValidationError" ? err.errors.toArray().join(", ") : ""}`)
|
|
1943
2372
|
}), (config) => ({
|
|
@@ -2004,6 +2433,17 @@ const toolDefinitions = [
|
|
|
2004
2433
|
},
|
|
2005
2434
|
required: ["key"]
|
|
2006
2435
|
}
|
|
2436
|
+
},
|
|
2437
|
+
{
|
|
2438
|
+
name: "getEnvMeta",
|
|
2439
|
+
description: "Get metadata for environment defaults — returns configured default values, purposes, and current drift status",
|
|
2440
|
+
inputSchema: {
|
|
2441
|
+
type: "object",
|
|
2442
|
+
properties: { configPath: {
|
|
2443
|
+
type: "string",
|
|
2444
|
+
description: "Optional path to envpkt.toml"
|
|
2445
|
+
} }
|
|
2446
|
+
}
|
|
2007
2447
|
}
|
|
2008
2448
|
];
|
|
2009
2449
|
const handleGetPacketHealth = (args) => {
|
|
@@ -2037,7 +2477,8 @@ const handleListCapabilities = (args) => {
|
|
|
2037
2477
|
const { config } = loaded;
|
|
2038
2478
|
const agentCapabilities = config.agent?.capabilities ?? [];
|
|
2039
2479
|
const secretCapabilities = {};
|
|
2040
|
-
|
|
2480
|
+
const secretEntries = config.secret ?? {};
|
|
2481
|
+
for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2041
2482
|
return textResult(JSON.stringify({
|
|
2042
2483
|
agent: config.agent ? {
|
|
2043
2484
|
name: config.agent.name,
|
|
@@ -2045,7 +2486,8 @@ const handleListCapabilities = (args) => {
|
|
|
2045
2486
|
description: config.agent.description,
|
|
2046
2487
|
capabilities: agentCapabilities
|
|
2047
2488
|
} : null,
|
|
2048
|
-
secrets: secretCapabilities
|
|
2489
|
+
secrets: secretCapabilities,
|
|
2490
|
+
env_defaults: Object.keys(config.env ?? {}).length
|
|
2049
2491
|
}, null, 2));
|
|
2050
2492
|
};
|
|
2051
2493
|
const handleGetSecretMeta = (args) => {
|
|
@@ -2054,11 +2496,12 @@ const handleGetSecretMeta = (args) => {
|
|
|
2054
2496
|
const loaded = loadConfigForTool(args.configPath);
|
|
2055
2497
|
if (!loaded.ok) return loaded.result;
|
|
2056
2498
|
const { config } = loaded;
|
|
2057
|
-
const meta = config.
|
|
2499
|
+
const meta = (config.secret ?? {})[key];
|
|
2058
2500
|
if (!meta) return errorResult(`Secret not found: ${key}`);
|
|
2501
|
+
const { encrypted_value: _, ...safeMeta } = meta;
|
|
2059
2502
|
return textResult(JSON.stringify({
|
|
2060
2503
|
key,
|
|
2061
|
-
...
|
|
2504
|
+
...safeMeta
|
|
2062
2505
|
}, null, 2));
|
|
2063
2506
|
};
|
|
2064
2507
|
const handleCheckExpiration = (args) => {
|
|
@@ -2077,11 +2520,19 @@ const handleCheckExpiration = (args) => {
|
|
|
2077
2520
|
issues: s.issues.toArray()
|
|
2078
2521
|
}, null, 2)));
|
|
2079
2522
|
};
|
|
2523
|
+
const handleGetEnvMeta = (args) => {
|
|
2524
|
+
const loaded = loadConfigForTool(args.configPath);
|
|
2525
|
+
if (!loaded.ok) return loaded.result;
|
|
2526
|
+
const { config } = loaded;
|
|
2527
|
+
const envAudit = computeEnvAudit(config);
|
|
2528
|
+
return textResult(JSON.stringify(envAudit, null, 2));
|
|
2529
|
+
};
|
|
2080
2530
|
const handlers = {
|
|
2081
2531
|
getPacketHealth: handleGetPacketHealth,
|
|
2082
2532
|
listCapabilities: handleListCapabilities,
|
|
2083
2533
|
getSecretMeta: handleGetSecretMeta,
|
|
2084
|
-
checkExpiration: handleCheckExpiration
|
|
2534
|
+
checkExpiration: handleCheckExpiration,
|
|
2535
|
+
getEnvMeta: handleGetEnvMeta
|
|
2085
2536
|
};
|
|
2086
2537
|
const callTool = (name, args) => {
|
|
2087
2538
|
const handler = handlers[name];
|
|
@@ -2102,17 +2553,17 @@ const createServer = () => {
|
|
|
2102
2553
|
},
|
|
2103
2554
|
instructions: "envpkt provides credential lifecycle awareness for AI agents. Use tools to check health, capabilities, and secret metadata. No secret values are ever exposed."
|
|
2104
2555
|
});
|
|
2105
|
-
server.setRequestHandler(ListToolsRequestSchema,
|
|
2556
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: toolDefinitions.map((t) => ({
|
|
2106
2557
|
name: t.name,
|
|
2107
2558
|
description: t.description,
|
|
2108
2559
|
inputSchema: t.inputSchema
|
|
2109
2560
|
})) }));
|
|
2110
|
-
server.setRequestHandler(CallToolRequestSchema,
|
|
2561
|
+
server.setRequestHandler(CallToolRequestSchema, (request) => {
|
|
2111
2562
|
const { name, arguments: args } = request.params;
|
|
2112
2563
|
return callTool(name, args ?? {});
|
|
2113
2564
|
});
|
|
2114
|
-
server.setRequestHandler(ListResourcesRequestSchema,
|
|
2115
|
-
server.setRequestHandler(ReadResourceRequestSchema,
|
|
2565
|
+
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
|
|
2566
|
+
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
|
|
2116
2567
|
const { uri } = request.params;
|
|
2117
2568
|
const result = readResource(uri);
|
|
2118
2569
|
if (!result) return { contents: [{
|
|
@@ -2145,7 +2596,9 @@ const runResolve = (options) => {
|
|
|
2145
2596
|
resolveConfigPath(options.config).fold((err) => {
|
|
2146
2597
|
console.error(formatError(err));
|
|
2147
2598
|
process.exit(2);
|
|
2148
|
-
}, (configPath) => {
|
|
2599
|
+
}, ({ path: configPath, source }) => {
|
|
2600
|
+
const sourceMsg = formatConfigSource(configPath, source);
|
|
2601
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
2149
2602
|
loadConfig(configPath).fold((err) => {
|
|
2150
2603
|
console.error(formatError(err));
|
|
2151
2604
|
process.exit(2);
|
|
@@ -2154,10 +2607,7 @@ const runResolve = (options) => {
|
|
|
2154
2607
|
console.error(formatError(err));
|
|
2155
2608
|
process.exit(2);
|
|
2156
2609
|
}, (result) => {
|
|
2157
|
-
const
|
|
2158
|
-
let content;
|
|
2159
|
-
if (outputFormat === "json") content = JSON.stringify(result.config, null, 2) + "\n";
|
|
2160
|
-
else content = `# Generated by envpkt resolve — do not edit\n${stringify(result.config)}\n`;
|
|
2610
|
+
const content = (options.format ?? "toml") === "json" ? `${JSON.stringify(result.config, null, 2)}\n` : `# Generated by envpkt resolve — do not edit\n${stringify(result.config)}\n`;
|
|
2161
2611
|
if (options.dryRun) {
|
|
2162
2612
|
console.log(`${DIM}# Dry run — would write:${RESET}`);
|
|
2163
2613
|
console.log(content);
|
|
@@ -2167,7 +2617,7 @@ const runResolve = (options) => {
|
|
|
2167
2617
|
} else process.stdout.write(content);
|
|
2168
2618
|
if (result.catalogPath) {
|
|
2169
2619
|
const summaryTarget = options.output ? process.stdout : process.stderr;
|
|
2170
|
-
summaryTarget.write(`\n${CYAN}Catalog:${RESET} ${result.catalogPath}\n${GREEN}Merged:${RESET} ${result.merged.length} key(s)
|
|
2620
|
+
summaryTarget.write(`\n${CYAN}Catalog:${RESET} ${result.catalogPath}\n${GREEN}Merged:${RESET} ${result.merged.length} key(s)${result.overridden.length > 0 ? ` ${YELLOW}(${result.overridden.length} overridden: ${result.overridden.join(", ")})${RESET}` : ""}\n`);
|
|
2171
2621
|
for (const w of result.warnings) summaryTarget.write(`${RED}Warning:${RESET} ${w}\n`);
|
|
2172
2622
|
}
|
|
2173
2623
|
});
|
|
@@ -2175,39 +2625,6 @@ const runResolve = (options) => {
|
|
|
2175
2625
|
});
|
|
2176
2626
|
};
|
|
2177
2627
|
|
|
2178
|
-
//#endregion
|
|
2179
|
-
//#region src/fnox/cli.ts
|
|
2180
|
-
/** Export all secrets from fnox as key=value pairs for a given profile */
|
|
2181
|
-
const fnoxExport = (profile, agentKey) => {
|
|
2182
|
-
const args = profile ? [
|
|
2183
|
-
"export",
|
|
2184
|
-
"--profile",
|
|
2185
|
-
profile
|
|
2186
|
-
] : ["export"];
|
|
2187
|
-
const env = agentKey ? {
|
|
2188
|
-
...process.env,
|
|
2189
|
-
FNOX_AGE_KEY: agentKey
|
|
2190
|
-
} : void 0;
|
|
2191
|
-
return Try(() => execFileSync("fnox", args, {
|
|
2192
|
-
stdio: "pipe",
|
|
2193
|
-
encoding: "utf-8",
|
|
2194
|
-
env
|
|
2195
|
-
})).fold((err) => Left({
|
|
2196
|
-
_tag: "FnoxCliError",
|
|
2197
|
-
message: `fnox export failed: ${err}`
|
|
2198
|
-
}), (output) => {
|
|
2199
|
-
const entries = {};
|
|
2200
|
-
for (const line of output.split("\n")) {
|
|
2201
|
-
const eq = line.indexOf("=");
|
|
2202
|
-
if (eq > 0) {
|
|
2203
|
-
const key = line.slice(0, eq).trim();
|
|
2204
|
-
entries[key] = line.slice(eq + 1).trim();
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
return Right(entries);
|
|
2208
|
-
});
|
|
2209
|
-
};
|
|
2210
|
-
|
|
2211
2628
|
//#endregion
|
|
2212
2629
|
//#region src/core/resolve-values.ts
|
|
2213
2630
|
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
@@ -2244,62 +2661,6 @@ const resolveValues = async (keys, profile, agentKey) => {
|
|
|
2244
2661
|
return result;
|
|
2245
2662
|
};
|
|
2246
2663
|
|
|
2247
|
-
//#endregion
|
|
2248
|
-
//#region src/core/seal.ts
|
|
2249
|
-
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
2250
|
-
const ageEncrypt = (plaintext, recipient) => {
|
|
2251
|
-
if (!ageAvailable()) return Left({
|
|
2252
|
-
_tag: "AgeNotFound",
|
|
2253
|
-
message: "age CLI not found on PATH"
|
|
2254
|
-
});
|
|
2255
|
-
return Try(() => execFileSync("age", [
|
|
2256
|
-
"--encrypt",
|
|
2257
|
-
"--recipient",
|
|
2258
|
-
recipient,
|
|
2259
|
-
"--armor"
|
|
2260
|
-
], {
|
|
2261
|
-
input: plaintext,
|
|
2262
|
-
stdio: [
|
|
2263
|
-
"pipe",
|
|
2264
|
-
"pipe",
|
|
2265
|
-
"pipe"
|
|
2266
|
-
],
|
|
2267
|
-
encoding: "utf-8"
|
|
2268
|
-
})).fold((err) => Left({
|
|
2269
|
-
_tag: "EncryptFailed",
|
|
2270
|
-
key: "",
|
|
2271
|
-
message: `age encrypt failed: ${err}`
|
|
2272
|
-
}), (output) => Right(output.trim()));
|
|
2273
|
-
};
|
|
2274
|
-
/** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
|
|
2275
|
-
const sealSecrets = (meta, values, recipient) => {
|
|
2276
|
-
if (!ageAvailable()) return Left({
|
|
2277
|
-
_tag: "AgeNotFound",
|
|
2278
|
-
message: "age CLI not found on PATH"
|
|
2279
|
-
});
|
|
2280
|
-
const result = {};
|
|
2281
|
-
for (const [key, secretMeta] of Object.entries(meta)) {
|
|
2282
|
-
const plaintext = values[key];
|
|
2283
|
-
if (plaintext === void 0) {
|
|
2284
|
-
result[key] = secretMeta;
|
|
2285
|
-
continue;
|
|
2286
|
-
}
|
|
2287
|
-
const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
|
|
2288
|
-
_tag: "EncryptFailed",
|
|
2289
|
-
key,
|
|
2290
|
-
message: err.message
|
|
2291
|
-
}), (ciphertext) => Right(ciphertext));
|
|
2292
|
-
const failed = outcome.fold((err) => err, () => void 0);
|
|
2293
|
-
if (failed) return Left(failed);
|
|
2294
|
-
const ciphertext = outcome.fold(() => "", (v) => v);
|
|
2295
|
-
result[key] = {
|
|
2296
|
-
...secretMeta,
|
|
2297
|
-
encrypted_value: ciphertext
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
return Right(result);
|
|
2301
|
-
};
|
|
2302
|
-
|
|
2303
2664
|
//#endregion
|
|
2304
2665
|
//#region src/cli/commands/seal.ts
|
|
2305
2666
|
/** Write sealed values back into the TOML file, preserving structure */
|
|
@@ -2311,7 +2672,7 @@ const writeSealedToml = (configPath, sealedMeta) => {
|
|
|
2311
2672
|
let hasEncryptedValue = false;
|
|
2312
2673
|
const pendingSeals = /* @__PURE__ */ new Map();
|
|
2313
2674
|
for (const [key, meta] of Object.entries(sealedMeta)) if (meta.encrypted_value) pendingSeals.set(key, meta.encrypted_value);
|
|
2314
|
-
const metaSectionRe = /^\[
|
|
2675
|
+
const metaSectionRe = /^\[secret\.(.+)\]\s*$/;
|
|
2315
2676
|
const encryptedValueRe = /^encrypted_value\s*=/;
|
|
2316
2677
|
const newSectionRe = /^\[/;
|
|
2317
2678
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2370,11 +2731,16 @@ const writeSealedToml = (configPath, sealedMeta) => {
|
|
|
2370
2731
|
writeFileSync(configPath, output.join("\n"));
|
|
2371
2732
|
};
|
|
2372
2733
|
const runSeal = async (options) => {
|
|
2373
|
-
const configPath = resolveConfigPath(options.config).fold((err) => {
|
|
2734
|
+
const { path: configPath, source: configSource } = resolveConfigPath(options.config).fold((err) => {
|
|
2374
2735
|
console.error(formatError(err));
|
|
2375
2736
|
process.exit(2);
|
|
2376
|
-
return
|
|
2377
|
-
|
|
2737
|
+
return {
|
|
2738
|
+
path: "",
|
|
2739
|
+
source: "flag"
|
|
2740
|
+
};
|
|
2741
|
+
}, (r) => r);
|
|
2742
|
+
const sourceMsg = formatConfigSource(configPath, configSource);
|
|
2743
|
+
if (sourceMsg) console.error(sourceMsg);
|
|
2378
2744
|
const config = loadConfig(configPath).fold((err) => {
|
|
2379
2745
|
console.error(formatError(err));
|
|
2380
2746
|
process.exit(2);
|
|
@@ -2384,14 +2750,34 @@ const runSeal = async (options) => {
|
|
|
2384
2750
|
console.error(`${DIM}Add [agent] section with recipient = "age1..." to your envpkt.toml${RESET}`);
|
|
2385
2751
|
process.exit(2);
|
|
2386
2752
|
}
|
|
2387
|
-
const recipient = config.agent
|
|
2753
|
+
const { recipient } = config.agent;
|
|
2388
2754
|
const configDir = dirname(configPath);
|
|
2389
|
-
|
|
2390
|
-
|
|
2755
|
+
const envEntries = config.env ?? {};
|
|
2756
|
+
const secretEntries0 = config.secret ?? {};
|
|
2757
|
+
const envConflicts = Object.keys(secretEntries0).filter((k) => k in envEntries);
|
|
2758
|
+
if (envConflicts.length > 0) {
|
|
2759
|
+
console.error(`${RED}Error:${RESET} Cannot seal keys that are also defined in [env.*]: ${envConflicts.join(", ")}`);
|
|
2760
|
+
console.error(`${DIM}Move these to [secret.*] only, or remove from [env.*] before sealing.${RESET}`);
|
|
2761
|
+
process.exit(2);
|
|
2762
|
+
}
|
|
2763
|
+
const agentKey = config.agent.identity ? unwrapAgentKey(resolve(configDir, expandPath(config.agent.identity))).fold((err) => {
|
|
2391
2764
|
const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
|
|
2392
2765
|
console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
|
|
2393
|
-
}, (k) => k);
|
|
2394
|
-
const
|
|
2766
|
+
}, (k) => k) : void 0;
|
|
2767
|
+
const allSecretEntries = config.secret ?? {};
|
|
2768
|
+
const allKeys = Object.keys(allSecretEntries);
|
|
2769
|
+
const alreadySealed = allKeys.filter((k) => allSecretEntries[k]?.encrypted_value);
|
|
2770
|
+
const unsealed = allKeys.filter((k) => !allSecretEntries[k]?.encrypted_value);
|
|
2771
|
+
if (!options.reseal && alreadySealed.length > 0) {
|
|
2772
|
+
if (unsealed.length === 0) {
|
|
2773
|
+
console.log(`${GREEN}✓${RESET} All ${BOLD}${alreadySealed.length}${RESET} secret(s) already sealed. Use ${CYAN}--reseal${RESET} to re-encrypt.`);
|
|
2774
|
+
process.exit(0);
|
|
2775
|
+
}
|
|
2776
|
+
console.log(`${DIM}Skipping ${alreadySealed.length} already-sealed secret(s). Use --reseal to re-encrypt all.${RESET}`);
|
|
2777
|
+
}
|
|
2778
|
+
const targetKeys = options.reseal ? allKeys : unsealed;
|
|
2779
|
+
const secretEntries = Object.fromEntries(targetKeys.map((k) => [k, allSecretEntries[k]]));
|
|
2780
|
+
const metaKeys = targetKeys;
|
|
2395
2781
|
console.log(`${BOLD}Sealing ${metaKeys.length} secret(s)${RESET} with recipient ${CYAN}${recipient.slice(0, 20)}...${RESET}`);
|
|
2396
2782
|
console.log("");
|
|
2397
2783
|
const values = await resolveValues(metaKeys, options.profile, agentKey);
|
|
@@ -2405,12 +2791,15 @@ const runSeal = async (options) => {
|
|
|
2405
2791
|
const skippedKeys = metaKeys.filter((k) => !(k in values));
|
|
2406
2792
|
console.log(`${YELLOW}Skipped${RESET} ${skipped} key(s) with no value: ${skippedKeys.join(", ")}`);
|
|
2407
2793
|
}
|
|
2408
|
-
sealSecrets(
|
|
2794
|
+
sealSecrets(secretEntries, values, recipient).fold((err) => {
|
|
2409
2795
|
console.error(`${RED}Error:${RESET} Seal failed: ${err.message}`);
|
|
2410
2796
|
process.exit(2);
|
|
2411
2797
|
}, (sealedMeta) => {
|
|
2412
2798
|
writeSealedToml(configPath, sealedMeta);
|
|
2413
|
-
|
|
2799
|
+
const sealedCount = resolved;
|
|
2800
|
+
const prevSealed = options.reseal ? 0 : alreadySealed.length;
|
|
2801
|
+
const summary = prevSealed > 0 ? ` (${prevSealed} previously sealed kept)` : "";
|
|
2802
|
+
console.log(`${GREEN}Sealed${RESET} ${sealedCount} secret(s) into ${DIM}${configPath}${RESET}${summary}`);
|
|
2414
2803
|
});
|
|
2415
2804
|
};
|
|
2416
2805
|
|
|
@@ -2418,9 +2807,7 @@ const runSeal = async (options) => {
|
|
|
2418
2807
|
//#region src/cli/commands/shell-hook.ts
|
|
2419
2808
|
const ZSH_HOOK = `# envpkt shell hook — add to your .zshrc
|
|
2420
2809
|
_envpkt_chpwd() {
|
|
2421
|
-
|
|
2422
|
-
envpkt audit --format minimal 2>/dev/null
|
|
2423
|
-
fi
|
|
2810
|
+
envpkt audit --format minimal 2>/dev/null
|
|
2424
2811
|
}
|
|
2425
2812
|
|
|
2426
2813
|
if (( $+functions[add-zsh-hook] )); then
|
|
@@ -2433,9 +2820,7 @@ fi
|
|
|
2433
2820
|
`;
|
|
2434
2821
|
const BASH_HOOK = `# envpkt shell hook — add to your .bashrc
|
|
2435
2822
|
_envpkt_prompt() {
|
|
2436
|
-
|
|
2437
|
-
envpkt audit --format minimal 2>/dev/null
|
|
2438
|
-
fi
|
|
2823
|
+
envpkt audit --format minimal 2>/dev/null
|
|
2439
2824
|
}
|
|
2440
2825
|
|
|
2441
2826
|
if [[ ! "$PROMPT_COMMAND" == *"_envpkt_prompt"* ]]; then
|
|
@@ -2459,39 +2844,42 @@ const runShellHook = (shell) => {
|
|
|
2459
2844
|
//#endregion
|
|
2460
2845
|
//#region src/cli/index.ts
|
|
2461
2846
|
const program = new Command();
|
|
2462
|
-
program.name("envpkt").description("Credential lifecycle and fleet management for AI agents").version(
|
|
2847
|
+
program.name("envpkt").description("Credential lifecycle and fleet management for AI agents\n\n Developer workflow: env scan → catalog → cloud-synced folder → eval $(envpkt env export)\n Agent / CI workflow: catalog → audit --strict → seal → exec --strict → fleet").version(createRequire(import.meta.url)("../../package.json").version);
|
|
2463
2848
|
program.command("init").description("Initialize a new envpkt.toml in the current directory").option("--from-fnox [path]", "Scaffold from fnox.toml (optionally specify path)").option("--catalog <path>", "Path to shared secret catalog").option("--agent", "Include [agent] section").option("--name <name>", "Agent name (requires --agent)").option("--capabilities <caps>", "Comma-separated capabilities (requires --agent)").option("--expires <date>", "Agent credential expiration YYYY-MM-DD (requires --agent)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
|
|
2464
2849
|
runInit(process.cwd(), options);
|
|
2465
2850
|
});
|
|
2466
|
-
program.command("audit").description("Audit credential health from envpkt.toml").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").action((options) => {
|
|
2851
|
+
program.command("audit").description("Audit credential health from envpkt.toml (use --strict in CI pipelines to gate deploys)").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json | minimal", "table").option("--expiring <days>", "Show secrets expiring within N days", parseInt).option("--status <status>", "Filter by status: healthy | expiring_soon | expired | stale | missing").option("--strict", "Exit non-zero on any non-healthy secret").option("--all", "Show both secrets and env defaults").option("--env-only", "Show only env defaults (drift detection)").option("--sealed", "Show only secrets with encrypted_value").option("--external", "Show only secrets without encrypted_value").action((options) => {
|
|
2467
2852
|
runAudit(options);
|
|
2468
2853
|
});
|
|
2469
|
-
program.command("fleet").description("Scan directory tree for envpkt.toml files and aggregate health").option("-d, --dir <path>", "Root directory to scan", ".").option("--depth <n>", "Max directory depth", parseInt).option("--format <format>", "Output format: table | json", "table").option("--status <status>", "Filter agents by health status").action((options) => {
|
|
2854
|
+
program.command("fleet").description("Scan directory tree for envpkt.toml files and aggregate health (use in CI for fleet-wide monitoring)").option("-d, --dir <path>", "Root directory to scan", ".").option("--depth <n>", "Max directory depth", parseInt).option("--format <format>", "Output format: table | json", "table").option("--status <status>", "Filter agents by health status").action((options) => {
|
|
2470
2855
|
runFleet(options);
|
|
2471
2856
|
});
|
|
2472
2857
|
program.command("inspect").description("Display structured view of envpkt.toml").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--resolved", "Show resolved view (catalog merged)").option("--secrets", "Show secret values from environment (masked by default)").option("--plaintext", "Show secret values in plaintext (requires --secrets)").action((options) => {
|
|
2473
2858
|
runInspect(options);
|
|
2474
2859
|
});
|
|
2475
|
-
program.command("exec").description("Run pre-flight audit then execute a command with
|
|
2860
|
+
program.command("exec").description("Run pre-flight audit then execute a command with injected secrets (sealed → fnox → env cascade)").argument("<command...>", "Command to execute").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit (alias: --no-check)").option("--no-check", "Skip the pre-flight audit").option("--warn-only", "Warn on critical audit but do not abort").option("--strict", "Abort on any non-healthy secret").action((args, options) => {
|
|
2476
2861
|
runExec(args, options);
|
|
2477
2862
|
});
|
|
2478
2863
|
program.command("resolve").description("Resolve catalog references and output a flat, self-contained config").option("-c, --config <path>", "Path to envpkt.toml").option("-o, --output <path>", "Write resolved config to file (default: stdout)").option("--format <format>", "Output format: toml | json", "toml").option("--dry-run", "Show what would be resolved without writing").action((options) => {
|
|
2479
2864
|
runResolve(options);
|
|
2480
2865
|
});
|
|
2481
|
-
program.command("seal").description("Encrypt secret values into envpkt.toml using age
|
|
2866
|
+
program.command("seal").description("Encrypt secret values into envpkt.toml using age — sealed packets are safe to commit to git").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use for value resolution").option("--reseal", "Re-encrypt all secrets, including already sealed (for key rotation)").action(async (options) => {
|
|
2482
2867
|
await runSeal(options);
|
|
2483
2868
|
});
|
|
2484
2869
|
program.command("mcp").description("Start the envpkt MCP server (stdio transport)").option("-c, --config <path>", "Path to envpkt.toml").action((options) => {
|
|
2485
2870
|
runMcp(options);
|
|
2486
2871
|
});
|
|
2487
2872
|
const env = program.command("env").description("Discover and check credentials in your shell environment");
|
|
2488
|
-
env.command("scan").description("Auto-discover credentials from process.env and scaffold TOML entries").option("--format <format>", "Output format: table | json", "table").option("--write", "Write discovered credentials to envpkt.toml").option("--dry-run", "Preview TOML that would be written (implies --write)").option("--include-unknown", "Include vars where service could not be inferred").action((options) => {
|
|
2873
|
+
env.command("scan").description("Auto-discover credentials from process.env and scaffold TOML entries — first step in the developer workflow").option("-c, --config <path>", "Path to envpkt.toml (write target for --write)").option("--format <format>", "Output format: table | json", "table").option("--write", "Write discovered credentials to envpkt.toml").option("--dry-run", "Preview TOML that would be written (implies --write)").option("--include-unknown", "Include vars where service could not be inferred").action((options) => {
|
|
2489
2874
|
runEnvScan(options);
|
|
2490
2875
|
});
|
|
2491
2876
|
env.command("check").description("Bidirectional drift detection between envpkt.toml and live environment").option("-c, --config <path>", "Path to envpkt.toml").option("--format <format>", "Output format: table | json", "table").option("--strict", "Exit non-zero on any drift").action((options) => {
|
|
2492
2877
|
runEnvCheck(options);
|
|
2493
2878
|
});
|
|
2494
|
-
|
|
2879
|
+
env.command("export").description("Output export statements for eval-ing secrets into the current shell. Usage: eval \"$(envpkt env export)\"").option("-c, --config <path>", "Path to envpkt.toml").option("--profile <profile>", "fnox profile to use").option("--skip-audit", "Skip the pre-flight audit").action((options) => {
|
|
2880
|
+
runEnvExport(options);
|
|
2881
|
+
});
|
|
2882
|
+
program.command("shell-hook").description("Output shell function for ambient credential warnings on cd — combine with env export for full setup").argument("<shell>", "Shell type: zsh | bash").action((shell) => {
|
|
2495
2883
|
runShellHook(shell);
|
|
2496
2884
|
});
|
|
2497
2885
|
program.parse();
|