envpkt 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { FormatRegistry, Type } from "@sinclair/typebox";
2
2
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { TypeCompiler } from "@sinclair/typebox/compiler";
5
6
  import { Cond, Left, List, Option, Right, Try } from "functype";
6
7
  import { TomlDate, parse } from "smol-toml";
7
8
  import { execFileSync } from "node:child_process";
9
+ import { createInterface } from "node:readline";
8
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
12
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -45,6 +47,7 @@ const SecretMetaSchema = Type.Object({
45
47
  description: "URL or reference for secret rotation procedure"
46
48
  })),
47
49
  purpose: Type.Optional(Type.String({ description: "Why this secret exists and what it enables" })),
50
+ comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
48
51
  capabilities: Type.Optional(Type.Array(Type.String(), { description: "What operations this secret grants (e.g. read, write, admin)" })),
49
52
  created: Type.Optional(Type.String({
50
53
  format: "date",
@@ -54,6 +57,7 @@ const SecretMetaSchema = Type.Object({
54
57
  rate_limit: Type.Optional(Type.String({ description: "Rate limit or quota info (e.g. '1000/min')" })),
55
58
  model_hint: Type.Optional(Type.String({ description: "Suggested model or tier for this credential" })),
56
59
  source: Type.Optional(Type.String({ description: "Where the secret value originates (e.g. 'vault', 'ci')" })),
60
+ encrypted_value: Type.Optional(Type.String({ description: "Age-encrypted secret value (armored ciphertext, safe to commit)" })),
57
61
  required: Type.Optional(Type.Boolean({ description: "Whether this secret is required for operation" })),
58
62
  tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
59
63
  }, { description: "Metadata about a single secret" });
@@ -77,6 +81,12 @@ const CallbackConfigSchema = Type.Object({
77
81
  on_audit_fail: Type.Optional(Type.String({ description: "Command or webhook on audit failure" }))
78
82
  }, { description: "Automation callbacks for lifecycle events" });
79
83
  const ToolsConfigSchema = Type.Record(Type.String(), Type.Unknown(), { description: "Tool integration configuration — open namespace for third-party extensions" });
84
+ const EnvMetaSchema = Type.Object({
85
+ value: Type.String({ description: "Default value for this environment variable" }),
86
+ purpose: Type.Optional(Type.String({ description: "Why this env var exists" })),
87
+ comment: Type.Optional(Type.String({ description: "Free-form annotation or note" })),
88
+ tags: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Key-value tags for grouping and filtering" }))
89
+ }, { description: "Metadata for a plaintext environment default (non-secret)" });
80
90
  const EnvpktConfigSchema = Type.Object({
81
91
  version: Type.Number({
82
92
  description: "Schema version number",
@@ -84,7 +94,8 @@ const EnvpktConfigSchema = Type.Object({
84
94
  }),
85
95
  catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
86
96
  agent: Type.Optional(AgentIdentitySchema),
87
- meta: Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" }),
97
+ secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
98
+ env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
88
99
  lifecycle: Type.Optional(LifecycleConfigSchema),
89
100
  callbacks: Type.Optional(CallbackConfigSchema),
90
101
  tools: Type.Optional(ToolsConfigSchema)
@@ -106,11 +117,47 @@ const normalizeDates = (obj) => {
106
117
  if (obj !== null && typeof obj === "object") return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, normalizeDates(v)]));
107
118
  return obj;
108
119
  };
120
+ /** Expand ~ and $ENV_VAR / ${ENV_VAR} in a path string */
121
+ const expandPath = (p) => {
122
+ return (p.startsWith("~/") || p === "~" ? join(homedir(), p.slice(1)) : p).replace(/\$\{(\w+)\}|\$(\w+)/g, (_, braced, bare) => {
123
+ const name = braced ?? bare ?? "";
124
+ return process.env[name] ?? "";
125
+ });
126
+ };
109
127
  /** Find envpkt.toml in the given directory */
110
128
  const findConfigPath = (dir) => {
111
129
  const candidate = join(dir, CONFIG_FILENAME$1);
112
130
  return existsSync(candidate) ? Option(candidate) : Option(void 0);
113
131
  };
132
+ /** Ordered candidate paths for config discovery beyond CWD */
133
+ const CONFIG_SEARCH_PATHS = [
134
+ "~/.envpkt/envpkt.toml",
135
+ "$WINHOME/OneDrive/.envpkt/envpkt.toml",
136
+ "$USERPROFILE/OneDrive/.envpkt/envpkt.toml",
137
+ "~/Library/Mobile Documents/com~apple~CloudDocs/.envpkt/envpkt.toml",
138
+ "~/Dropbox/.envpkt/envpkt.toml",
139
+ "$DROPBOX_PATH/.envpkt/envpkt.toml",
140
+ "$GOOGLE_DRIVE/.envpkt/envpkt.toml",
141
+ "$WINHOME/.envpkt/envpkt.toml",
142
+ "$USERPROFILE/.envpkt/envpkt.toml"
143
+ ];
144
+ /** Discover config by checking CWD, then ENVPKT_SEARCH_PATH, then built-in candidate paths */
145
+ const discoverConfig = (cwd) => {
146
+ const cwdCandidate = join(cwd ?? process.cwd(), CONFIG_FILENAME$1);
147
+ if (existsSync(cwdCandidate)) return Option({
148
+ path: cwdCandidate,
149
+ source: "cwd"
150
+ });
151
+ const customPaths = process.env.ENVPKT_SEARCH_PATH?.split(":").filter(Boolean) ?? [];
152
+ for (const template of [...customPaths, ...CONFIG_SEARCH_PATHS]) {
153
+ const expanded = expandPath(template);
154
+ if (expanded && !expanded.startsWith("/.envpkt") && existsSync(expanded)) return Option({
155
+ path: expanded,
156
+ source: "search"
157
+ });
158
+ }
159
+ return Option(void 0);
160
+ };
114
161
  /** Read a config file, returning Either<ConfigError, string> */
115
162
  const readConfigFile = (path) => {
116
163
  if (!existsSync(path)) return Left({
@@ -122,14 +169,13 @@ const readConfigFile = (path) => {
122
169
  message: String(err)
123
170
  }), (content) => Right(content));
124
171
  };
125
- /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit meta) */
172
+ /** Ensure required fields have defaults for valid configs (e.g. agent configs with catalog may omit secret) */
126
173
  const applyDefaults = (data) => {
127
174
  if (data !== null && typeof data === "object" && !Array.isArray(data)) {
128
- const obj = data;
129
- if (!("meta" in obj)) return {
130
- ...obj,
131
- meta: {}
132
- };
175
+ const result = { ...data };
176
+ if (!("secret" in result)) result.secret = {};
177
+ if (!("env" in result)) result.env = {};
178
+ return result;
133
179
  }
134
180
  return data;
135
181
  };
@@ -148,27 +194,29 @@ const validateConfig = (data) => {
148
194
  };
149
195
  /** Load and validate an envpkt.toml from a file path */
150
196
  const loadConfig = (path) => readConfigFile(path).flatMap(parseToml).flatMap(validateConfig);
151
- /** Load config from CWD, returning both path and parsed config */
152
- const loadConfigFromCwd = (cwd) => {
153
- const dir = cwd ?? process.cwd();
154
- return findConfigPath(dir).fold(() => Left({
155
- _tag: "FileNotFound",
156
- path: join(dir, CONFIG_FILENAME$1)
157
- }), (path) => loadConfig(path).map((config) => ({
158
- path,
159
- config
160
- })));
161
- };
197
+ /** Load config from CWD or discovery chain, returning path, source, and parsed config */
198
+ const loadConfigFromCwd = (cwd) => discoverConfig(cwd).fold(() => Left({
199
+ _tag: "FileNotFound",
200
+ path: join(cwd ?? process.cwd(), CONFIG_FILENAME$1)
201
+ }), ({ path, source }) => loadConfig(path).map((config) => ({
202
+ path,
203
+ source,
204
+ config
205
+ })));
162
206
  /**
163
207
  * Resolve config path via priority chain:
164
208
  * 1. Explicit flag path
165
209
  * 2. ENVPKT_CONFIG env var
166
- * 3. CWD discovery
210
+ * 3. CWD + discovery chain (home dir, cloud storage, custom search paths)
167
211
  */
168
212
  const resolveConfigPath = (flagPath, envVar, cwd) => {
169
213
  if (flagPath) {
170
214
  const resolved = resolve(flagPath);
171
- return existsSync(resolved) ? Right(resolved) : Left({
215
+ const result = {
216
+ path: resolved,
217
+ source: "flag"
218
+ };
219
+ return existsSync(resolved) ? Right(result) : Left({
172
220
  _tag: "FileNotFound",
173
221
  path: resolved
174
222
  });
@@ -176,16 +224,22 @@ const resolveConfigPath = (flagPath, envVar, cwd) => {
176
224
  const envPath = envVar ?? process.env[ENV_VAR_CONFIG];
177
225
  if (envPath) {
178
226
  const resolved = resolve(envPath);
179
- return existsSync(resolved) ? Right(resolved) : Left({
227
+ const result = {
228
+ path: resolved,
229
+ source: "env"
230
+ };
231
+ return existsSync(resolved) ? Right(result) : Left({
180
232
  _tag: "FileNotFound",
181
233
  path: resolved
182
234
  });
183
235
  }
184
- const dir = cwd ?? process.cwd();
185
- return findConfigPath(dir).fold(() => Left({
236
+ return discoverConfig(cwd).fold(() => Left({
186
237
  _tag: "FileNotFound",
187
- path: join(dir, CONFIG_FILENAME$1)
188
- }), (path) => Right(path));
238
+ path: join(cwd ?? process.cwd(), CONFIG_FILENAME$1)
239
+ }), ({ path, source }) => Right({
240
+ path,
241
+ source
242
+ }));
189
243
  };
190
244
 
191
245
  //#endregion
@@ -234,13 +288,14 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
234
288
  });
235
289
  const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
236
290
  const agentSecrets = agentConfig.agent.secrets;
237
- return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentConfig.meta, catalogConfig.meta, agentSecrets, catalogPath).map((resolvedMeta) => {
291
+ const agentSecretEntries = agentConfig.secret ?? {};
292
+ return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
238
293
  const merged = [];
239
294
  const overridden = [];
240
295
  const warnings = [];
241
296
  for (const key of agentSecrets) {
242
297
  merged.push(key);
243
- if (agentConfig.meta[key]) overridden.push(key);
298
+ if (agentSecretEntries[key]) overridden.push(key);
244
299
  }
245
300
  const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
246
301
  const agentIdentity = agentConfig.agent ? (() => {
@@ -254,7 +309,7 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
254
309
  ...agentIdentity,
255
310
  name: agentIdentity.name
256
311
  } : void 0,
257
- meta: resolvedMeta
312
+ secret: resolvedMeta
258
313
  },
259
314
  catalogPath,
260
315
  merged,
@@ -307,12 +362,14 @@ const formatPacket = (result, options) => {
307
362
  if (config.agent.expires) agentLines.push(` expires: ${config.agent.expires}`);
308
363
  if (agentLines.length > 0) sections.push(agentLines.join("\n"));
309
364
  }
310
- const metaEntries = Object.entries(config.meta);
365
+ const secretConfig = config.secret ?? {};
366
+ const metaEntries = Object.entries(secretConfig);
311
367
  const secretHeader = `secrets: ${metaEntries.length}`;
312
368
  const secretLines = metaEntries.map(([key, meta]) => {
313
369
  const service = meta.service ?? key;
370
+ const sealedTag = meta.encrypted_value ? " [sealed]" : "";
314
371
  const secretValue = options?.secrets?.[key];
315
- const header = ` ${key} → ${service}${secretValue !== void 0 ? ` = ${(options?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}` : ""}`;
372
+ const header = ` ${key} → ${service}${sealedTag}${secretValue !== void 0 ? ` = ${(options?.secretDisplay ?? "encrypted") === "plaintext" ? secretValue : maskValue(secretValue)}` : ""}`;
316
373
  const fields = formatSecretFields(meta, " ");
317
374
  return fields ? `${header}\n${fields}` : header;
318
375
  });
@@ -341,7 +398,7 @@ const MS_PER_DAY = 864e5;
341
398
  const WARN_BEFORE_DAYS = 30;
342
399
  const daysBetween = (from, to) => Math.floor((to.getTime() - from.getTime()) / MS_PER_DAY);
343
400
  const parseDate = (dateStr) => {
344
- const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00Z");
401
+ const d = /* @__PURE__ */ new Date(`${dateStr}T00:00:00Z`);
345
402
  return Number.isNaN(d.getTime()) ? Option(void 0) : Option(d);
346
403
  };
347
404
  const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration, requireService, today) => {
@@ -356,7 +413,8 @@ const classifySecret = (key, meta, fnoxKeys, staleWarningDays, requireExpiration
356
413
  const isExpired = daysRemaining.fold(() => false, (d) => d < 0);
357
414
  const isExpiringSoon = daysRemaining.fold(() => false, (d) => d >= 0 && d <= WARN_BEFORE_DAYS);
358
415
  const isStale = daysSinceCreated.fold(() => false, (d) => d > staleWarningDays);
359
- const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key);
416
+ const hasSealed = !!meta?.encrypted_value;
417
+ const isMissing = fnoxKeys.size > 0 && !fnoxKeys.has(key) && !hasSealed;
360
418
  const isMissingMetadata = requireExpiration && expires.isNone() || requireService && service.isNone();
361
419
  if (isExpired) issues.push("Secret has expired");
362
420
  if (isExpiringSoon) issues.push(`Expires in ${daysRemaining.fold(() => "?", (d) => String(d))} days`);
@@ -385,8 +443,9 @@ const computeAudit = (config, fnoxKeys, today) => {
385
443
  const requireExpiration = lifecycle.require_expiration ?? false;
386
444
  const requireService = lifecycle.require_service ?? false;
387
445
  const keys = fnoxKeys ?? /* @__PURE__ */ new Set();
388
- const metaKeys = new Set(Object.keys(config.meta));
389
- const secrets = List(Object.entries(config.meta).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
446
+ const secretEntries = config.secret ?? {};
447
+ const metaKeys = new Set(Object.keys(secretEntries));
448
+ const secrets = List(Object.entries(secretEntries).map(([key, meta]) => classifySecret(key, meta, keys, staleWarningDays, requireExpiration, requireService, now)));
390
449
  const orphaned = keys.size > 0 ? [...metaKeys].filter((k) => !keys.has(k)).length : 0;
391
450
  const total = secrets.size;
392
451
  const expired = secrets.count((s) => s.status === "expired");
@@ -409,6 +468,28 @@ const computeAudit = (config, fnoxKeys, today) => {
409
468
  agent: config.agent
410
469
  };
411
470
  };
471
+ const computeEnvAudit = (config, env = process.env) => {
472
+ const envEntries = config.env ?? {};
473
+ const entries = [];
474
+ for (const [key, entry] of Object.entries(envEntries)) {
475
+ const currentValue = env[key];
476
+ const status = Cond.of().when(currentValue === void 0, "missing").elseWhen(currentValue !== entry.value, "overridden").else("default");
477
+ entries.push({
478
+ key,
479
+ defaultValue: entry.value,
480
+ currentValue,
481
+ status,
482
+ purpose: entry.purpose
483
+ });
484
+ }
485
+ return {
486
+ entries,
487
+ total: entries.length,
488
+ defaults_applied: entries.filter((e) => e.status === "default").length,
489
+ overridden: entries.filter((e) => e.status === "overridden").length,
490
+ missing: entries.filter((e) => e.status === "missing").length
491
+ };
492
+ };
412
493
 
413
494
  //#endregion
414
495
  //#region src/core/patterns.ts
@@ -1041,7 +1122,7 @@ const matchValueShape = (value) => {
1041
1122
  };
1042
1123
  /** Strip common suffixes and derive a service name from an env var name */
1043
1124
  const deriveServiceFromName = (name) => {
1044
- const suffixes = [
1125
+ const matchedSuffix = [
1045
1126
  "_API_KEY",
1046
1127
  "_SECRET_KEY",
1047
1128
  "_ACCESS_KEY",
@@ -1059,13 +1140,8 @@ const deriveServiceFromName = (name) => {
1059
1140
  "_DSN",
1060
1141
  "_URL",
1061
1142
  "_URI"
1062
- ];
1063
- let stripped = name;
1064
- for (const suffix of suffixes) if (stripped.endsWith(suffix)) {
1065
- stripped = stripped.slice(0, -suffix.length);
1066
- break;
1067
- }
1068
- return stripped.toLowerCase().replace(/_/g, "-");
1143
+ ].find((s) => name.endsWith(s));
1144
+ return (matchedSuffix ? name.slice(0, -matchedSuffix.length) : name).toLowerCase().replace(/_/g, "-");
1069
1145
  };
1070
1146
  /** Match a single env var against all patterns */
1071
1147
  const matchEnvVar = (name, value) => {
@@ -1135,10 +1211,11 @@ const envScan = (env, options) => {
1135
1211
  /** Bidirectional drift detection between config and live environment */
1136
1212
  const envCheck = (config, env) => {
1137
1213
  const entries = [];
1138
- const metaKeys = Object.keys(config.meta);
1214
+ const secretEntries = config.secret ?? {};
1215
+ const metaKeys = Object.keys(secretEntries);
1139
1216
  const trackedSet = new Set(metaKeys);
1140
1217
  for (const key of metaKeys) {
1141
- const meta = config.meta[key];
1218
+ const meta = secretEntries[key];
1142
1219
  const present = env[key] !== void 0 && env[key] !== "";
1143
1220
  entries.push({
1144
1221
  envVar: key,
@@ -1147,6 +1224,17 @@ const envCheck = (config, env) => {
1147
1224
  confidence: Option(void 0)
1148
1225
  });
1149
1226
  }
1227
+ const envDefaults = config.env ?? {};
1228
+ for (const key of Object.keys(envDefaults)) if (!trackedSet.has(key)) {
1229
+ trackedSet.add(key);
1230
+ const present = env[key] !== void 0 && env[key] !== "";
1231
+ entries.push({
1232
+ envVar: key,
1233
+ service: Option(void 0),
1234
+ status: present ? "tracked" : "missing_from_env",
1235
+ confidence: Option(void 0)
1236
+ });
1237
+ }
1150
1238
  const envMatches = scanEnv(env);
1151
1239
  for (const match of envMatches) if (!trackedSet.has(match.envVar)) entries.push({
1152
1240
  envVar: match.envVar,
@@ -1166,12 +1254,12 @@ const envCheck = (config, env) => {
1166
1254
  };
1167
1255
  };
1168
1256
  const todayIso = () => (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1169
- /** Generate TOML [meta.*] blocks from scan results, mirroring init.ts pattern */
1257
+ /** Generate TOML [secret.*] blocks from scan results, mirroring init.ts pattern */
1170
1258
  const generateTomlFromScan = (matches) => {
1171
1259
  const blocks = [];
1172
1260
  for (const match of matches) {
1173
1261
  const svc = match.service.fold(() => match.envVar.toLowerCase().replace(/_/g, "-"), (s) => s);
1174
- blocks.push(`[meta.${match.envVar}]
1262
+ blocks.push(`[secret.${match.envVar}]
1175
1263
  service = "${svc}"
1176
1264
  # purpose = "" # Why: what this secret enables
1177
1265
  # capabilities = [] # What operations this grants
@@ -1304,19 +1392,120 @@ const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((er
1304
1392
  /** Extract the set of secret key names from a parsed fnox config */
1305
1393
  const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
1306
1394
 
1395
+ //#endregion
1396
+ //#region src/core/seal.ts
1397
+ /** Encrypt a plaintext string using age with the given recipient public key (armored output) */
1398
+ const ageEncrypt = (plaintext, recipient) => {
1399
+ if (!ageAvailable()) return Left({
1400
+ _tag: "AgeNotFound",
1401
+ message: "age CLI not found on PATH"
1402
+ });
1403
+ return Try(() => execFileSync("age", [
1404
+ "--encrypt",
1405
+ "--recipient",
1406
+ recipient,
1407
+ "--armor"
1408
+ ], {
1409
+ input: plaintext,
1410
+ stdio: [
1411
+ "pipe",
1412
+ "pipe",
1413
+ "pipe"
1414
+ ],
1415
+ encoding: "utf-8"
1416
+ })).fold((err) => Left({
1417
+ _tag: "EncryptFailed",
1418
+ key: "",
1419
+ message: `age encrypt failed: ${err}`
1420
+ }), (output) => Right(output.trim()));
1421
+ };
1422
+ /** Decrypt an age-armored ciphertext using the given identity file */
1423
+ const ageDecrypt = (ciphertext, identityPath) => {
1424
+ if (!ageAvailable()) return Left({
1425
+ _tag: "AgeNotFound",
1426
+ message: "age CLI not found on PATH"
1427
+ });
1428
+ return Try(() => execFileSync("age", [
1429
+ "--decrypt",
1430
+ "--identity",
1431
+ identityPath
1432
+ ], {
1433
+ input: ciphertext,
1434
+ stdio: [
1435
+ "pipe",
1436
+ "pipe",
1437
+ "pipe"
1438
+ ],
1439
+ encoding: "utf-8"
1440
+ })).fold((err) => Left({
1441
+ _tag: "DecryptFailed",
1442
+ key: "",
1443
+ message: `age decrypt failed: ${err}`
1444
+ }), (output) => Right(output.trim()));
1445
+ };
1446
+ /** Seal multiple secrets: encrypt each value with the recipient key and set encrypted_value on meta */
1447
+ const sealSecrets = (meta, values, recipient) => {
1448
+ if (!ageAvailable()) return Left({
1449
+ _tag: "AgeNotFound",
1450
+ message: "age CLI not found on PATH"
1451
+ });
1452
+ const result = {};
1453
+ for (const [key, secretMeta] of Object.entries(meta)) {
1454
+ const plaintext = values[key];
1455
+ if (plaintext === void 0) {
1456
+ result[key] = secretMeta;
1457
+ continue;
1458
+ }
1459
+ const outcome = ageEncrypt(plaintext, recipient).fold((err) => Left({
1460
+ _tag: "EncryptFailed",
1461
+ key,
1462
+ message: err.message
1463
+ }), (ciphertext) => Right(ciphertext));
1464
+ const failed = outcome.fold((err) => err, () => void 0);
1465
+ if (failed) return Left(failed);
1466
+ const ciphertext = outcome.fold(() => "", (v) => v);
1467
+ result[key] = {
1468
+ ...secretMeta,
1469
+ encrypted_value: ciphertext
1470
+ };
1471
+ }
1472
+ return Right(result);
1473
+ };
1474
+ /** Unseal secrets: decrypt encrypted_value for each meta entry that has one */
1475
+ const unsealSecrets = (meta, identityPath) => {
1476
+ if (!ageAvailable()) return Left({
1477
+ _tag: "AgeNotFound",
1478
+ message: "age CLI not found on PATH"
1479
+ });
1480
+ const result = {};
1481
+ for (const [key, secretMeta] of Object.entries(meta)) {
1482
+ if (!secretMeta.encrypted_value) continue;
1483
+ const outcome = ageDecrypt(secretMeta.encrypted_value, identityPath).fold((err) => Left({
1484
+ _tag: "DecryptFailed",
1485
+ key,
1486
+ message: err.message
1487
+ }), (plaintext) => Right(plaintext));
1488
+ const failed = outcome.fold((err) => err, () => void 0);
1489
+ if (failed) return Left(failed);
1490
+ result[key] = outcome.fold(() => "", (v) => v);
1491
+ }
1492
+ return Right(result);
1493
+ };
1494
+
1307
1495
  //#endregion
1308
1496
  //#region src/core/boot.ts
1309
- const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), (configPath) => loadConfig(configPath).fold((err) => Left(err), (config) => {
1497
+ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) => Left(err), ({ path: configPath, source: configSource }) => loadConfig(configPath).fold((err) => Left(err), (config) => {
1310
1498
  const configDir = dirname(configPath);
1311
1499
  return resolveConfig(config, configDir).fold((err) => Left(err), (result) => Right({
1312
1500
  config: result.config,
1313
1501
  configPath,
1314
- configDir
1502
+ configDir,
1503
+ configSource
1315
1504
  }));
1316
1505
  }));
1317
1506
  const resolveAgentKey = (config, configDir) => {
1318
1507
  if (!config.agent?.identity) return Right(void 0);
1319
- return unwrapAgentKey(resolve(configDir, config.agent.identity)).fold((err) => Left(err), (key) => Right(key));
1508
+ return unwrapAgentKey(resolve(configDir, expandPath(config.agent.identity))).fold((err) => Left(err), (key) => Right(key));
1320
1509
  };
1321
1510
  const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
1322
1511
  const checkExpiration = (audit, failOnExpired, warnOnly) => {
@@ -1329,31 +1518,76 @@ const checkExpiration = (audit, failOnExpired, warnOnly) => {
1329
1518
  if (audit.expired > 0 && warnOnly) warnings.push(`${audit.expired} secret(s) have expired (warn-only mode)`);
1330
1519
  return Right(warnings);
1331
1520
  };
1521
+ const SECRET_PATTERNS = [
1522
+ /^sk-/,
1523
+ /^ghp_/,
1524
+ /^ghu_/,
1525
+ /^AKIA[0-9A-Z]{16}/,
1526
+ /^xox[bpras]-/,
1527
+ /:\/\/[^:]+:[^@]+@/,
1528
+ /^ey[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/
1529
+ ];
1530
+ const looksLikeSecret = (value) => {
1531
+ if (SECRET_PATTERNS.some((p) => p.test(value))) return true;
1532
+ if (value.length > 40 && /^[A-Za-z0-9+/=]+$/.test(value)) return true;
1533
+ return false;
1534
+ };
1535
+ const checkEnvMisclassification = (config) => {
1536
+ const warnings = [];
1537
+ const envEntries = config.env ?? {};
1538
+ 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}]`);
1539
+ return warnings;
1540
+ };
1332
1541
  /** Programmatic boot — returns Either<BootError, BootResult> */
1333
1542
  const bootSafe = (options) => {
1334
1543
  const opts = options ?? {};
1335
1544
  const inject = opts.inject !== false;
1336
1545
  const failOnExpired = opts.failOnExpired !== false;
1337
1546
  const warnOnly = opts.warnOnly ?? false;
1338
- return resolveAndLoad(opts).flatMap(({ config, configDir }) => resolveAgentKey(config, configDir).flatMap((agentKey) => {
1547
+ return resolveAndLoad(opts).flatMap(({ config, configPath, configDir, configSource }) => {
1548
+ const secretEntries = config.secret ?? {};
1549
+ const metaKeys = Object.keys(secretEntries);
1550
+ const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]?.encrypted_value);
1551
+ const agentKeyResult = resolveAgentKey(config, configDir);
1552
+ const agentKey = agentKeyResult.fold(() => void 0, (k) => k);
1553
+ const agentKeyError = agentKeyResult.fold((err) => err, () => void 0);
1554
+ if (agentKeyError && !hasSealedValues) return Left(agentKeyError);
1339
1555
  const audit = computeAudit(config, detectFnoxKeys(configDir));
1340
1556
  return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
1341
1557
  const secrets = {};
1342
1558
  const injected = [];
1343
1559
  const skipped = [];
1344
- const metaKeys = Object.keys(config.meta);
1345
- if (fnoxAvailable()) fnoxExport(opts.profile, agentKey).fold((err) => {
1560
+ warnings.push(...checkEnvMisclassification(config));
1561
+ const envEntries = config.env ?? {};
1562
+ const envDefaults = {};
1563
+ const overridden = [];
1564
+ for (const [key, entry] of Object.entries(envEntries)) if (process.env[key] === void 0) {
1565
+ envDefaults[key] = entry.value;
1566
+ if (inject) process.env[key] = entry.value;
1567
+ } else overridden.push(key);
1568
+ const sealedKeys = /* @__PURE__ */ new Set();
1569
+ if (hasSealedValues && config.agent?.identity) unsealSecrets(secretEntries, resolve(configDir, expandPath(config.agent.identity))).fold((err) => {
1570
+ warnings.push(`Sealed value decryption failed: ${err.message}`);
1571
+ }, (unsealed) => {
1572
+ for (const [key, value] of Object.entries(unsealed)) {
1573
+ secrets[key] = value;
1574
+ injected.push(key);
1575
+ sealedKeys.add(key);
1576
+ }
1577
+ });
1578
+ const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
1579
+ if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, agentKey).fold((err) => {
1346
1580
  warnings.push(`fnox export failed: ${err.message}`);
1347
- for (const key of metaKeys) skipped.push(key);
1581
+ for (const key of remainingKeys) skipped.push(key);
1348
1582
  }, (exported) => {
1349
- for (const key of metaKeys) if (key in exported) {
1583
+ for (const key of remainingKeys) if (key in exported) {
1350
1584
  secrets[key] = exported[key];
1351
1585
  injected.push(key);
1352
1586
  } else skipped.push(key);
1353
1587
  });
1354
1588
  else {
1355
- warnings.push("fnox not available — no secrets injected");
1356
- for (const key of metaKeys) skipped.push(key);
1589
+ if (!hasSealedValues) warnings.push("fnox not available — no secrets injected");
1590
+ for (const key of remainingKeys) skipped.push(key);
1357
1591
  }
1358
1592
  if (inject) for (const [key, value] of Object.entries(secrets)) process.env[key] = value;
1359
1593
  return {
@@ -1361,10 +1595,14 @@ const bootSafe = (options) => {
1361
1595
  injected,
1362
1596
  skipped,
1363
1597
  secrets,
1364
- warnings
1598
+ warnings,
1599
+ envDefaults,
1600
+ overridden,
1601
+ configPath,
1602
+ configSource
1365
1603
  };
1366
1604
  });
1367
- }));
1605
+ });
1368
1606
  };
1369
1607
  /** Programmatic boot — throws EnvpktBootError on failure */
1370
1608
  const boot = (options) => bootSafe(options).fold((err) => {
@@ -1400,6 +1638,42 @@ const formatBootError = (error) => {
1400
1638
  }
1401
1639
  };
1402
1640
 
1641
+ //#endregion
1642
+ //#region src/core/resolve-values.ts
1643
+ /** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
1644
+ const resolveValues = async (keys, profile, agentKey) => {
1645
+ const result = {};
1646
+ const remaining = new Set(keys);
1647
+ if (fnoxAvailable()) fnoxExport(profile, agentKey).fold(() => {}, (exported) => {
1648
+ for (const key of [...remaining]) if (key in exported) {
1649
+ result[key] = exported[key];
1650
+ remaining.delete(key);
1651
+ }
1652
+ });
1653
+ for (const key of [...remaining]) {
1654
+ const envValue = process.env[key];
1655
+ if (envValue !== void 0 && envValue !== "") {
1656
+ result[key] = envValue;
1657
+ remaining.delete(key);
1658
+ }
1659
+ }
1660
+ if (remaining.size > 0 && process.stdin.isTTY) {
1661
+ const rl = createInterface({
1662
+ input: process.stdin,
1663
+ output: process.stderr
1664
+ });
1665
+ const prompt = (question) => new Promise((resolve) => {
1666
+ rl.question(question, (answer) => resolve(answer));
1667
+ });
1668
+ for (const key of remaining) {
1669
+ const value = await prompt(`Enter value for ${key}: `);
1670
+ if (value !== "") result[key] = value;
1671
+ }
1672
+ rl.close();
1673
+ }
1674
+ return result;
1675
+ };
1676
+
1403
1677
  //#endregion
1404
1678
  //#region src/core/fleet.ts
1405
1679
  const CONFIG_FILENAME = "envpkt.toml";
@@ -1433,10 +1707,7 @@ function* findEnvpktFiles(dir, maxDepth, currentDepth = 0) {
1433
1707
  const configPath = join(dir, CONFIG_FILENAME);
1434
1708
  if (Try(() => statSync(configPath).isFile()).fold(() => false, (v) => v)) yield configPath;
1435
1709
  if (currentDepth >= maxDepth) return;
1436
- let entries = [];
1437
- Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => {}, (e) => {
1438
- entries = e;
1439
- });
1710
+ const entries = Try(() => readdirSync(dir, { withFileTypes: true })).fold(() => [], (e) => e);
1440
1711
  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);
1441
1712
  }
1442
1713
  const scanFleet = (rootDir, options) => {
@@ -1481,7 +1752,7 @@ const compareFnoxAndEnvpkt = (fnoxKeys, envpktKeys) => {
1481
1752
  //#endregion
1482
1753
  //#region src/mcp/resources.ts
1483
1754
  const loadConfigSafe = () => {
1484
- return resolveConfigPath().fold(() => void 0, (path) => loadConfig(path).fold(() => void 0, (config) => ({
1755
+ return resolveConfigPath().fold(() => void 0, ({ path }) => loadConfig(path).fold(() => void 0, (config) => ({
1485
1756
  config,
1486
1757
  path
1487
1758
  })));
@@ -1531,7 +1802,8 @@ const readCapabilities = () => {
1531
1802
  const { config } = loaded;
1532
1803
  const agentCapabilities = config.agent?.capabilities ?? [];
1533
1804
  const secretCapabilities = {};
1534
- for (const [key, meta] of Object.entries(config.meta)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1805
+ const secretEntries = config.secret ?? {};
1806
+ for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1535
1807
  return { contents: [{
1536
1808
  uri: "envpkt://capabilities",
1537
1809
  mimeType: "application/json",
@@ -1572,7 +1844,7 @@ const loadConfigForTool = (configPath) => {
1572
1844
  return resolveConfigPath(configPath).fold((err) => ({
1573
1845
  ok: false,
1574
1846
  result: errorResult(`Config error: ${err._tag} — ${err._tag === "FileNotFound" ? err.path : ""}`)
1575
- }), (path) => loadConfig(path).fold((err) => ({
1847
+ }), ({ path }) => loadConfig(path).fold((err) => ({
1576
1848
  ok: false,
1577
1849
  result: errorResult(`Config error: ${err._tag} — ${err._tag === "ValidationError" ? err.errors.toArray().join(", ") : ""}`)
1578
1850
  }), (config) => ({
@@ -1639,6 +1911,17 @@ const toolDefinitions = [
1639
1911
  },
1640
1912
  required: ["key"]
1641
1913
  }
1914
+ },
1915
+ {
1916
+ name: "getEnvMeta",
1917
+ description: "Get metadata for environment defaults — returns configured default values, purposes, and current drift status",
1918
+ inputSchema: {
1919
+ type: "object",
1920
+ properties: { configPath: {
1921
+ type: "string",
1922
+ description: "Optional path to envpkt.toml"
1923
+ } }
1924
+ }
1642
1925
  }
1643
1926
  ];
1644
1927
  const handleGetPacketHealth = (args) => {
@@ -1672,7 +1955,8 @@ const handleListCapabilities = (args) => {
1672
1955
  const { config } = loaded;
1673
1956
  const agentCapabilities = config.agent?.capabilities ?? [];
1674
1957
  const secretCapabilities = {};
1675
- for (const [key, meta] of Object.entries(config.meta)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1958
+ const secretEntries = config.secret ?? {};
1959
+ for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
1676
1960
  return textResult(JSON.stringify({
1677
1961
  agent: config.agent ? {
1678
1962
  name: config.agent.name,
@@ -1680,7 +1964,8 @@ const handleListCapabilities = (args) => {
1680
1964
  description: config.agent.description,
1681
1965
  capabilities: agentCapabilities
1682
1966
  } : null,
1683
- secrets: secretCapabilities
1967
+ secrets: secretCapabilities,
1968
+ env_defaults: Object.keys(config.env ?? {}).length
1684
1969
  }, null, 2));
1685
1970
  };
1686
1971
  const handleGetSecretMeta = (args) => {
@@ -1689,11 +1974,12 @@ const handleGetSecretMeta = (args) => {
1689
1974
  const loaded = loadConfigForTool(args.configPath);
1690
1975
  if (!loaded.ok) return loaded.result;
1691
1976
  const { config } = loaded;
1692
- const meta = config.meta[key];
1977
+ const meta = (config.secret ?? {})[key];
1693
1978
  if (!meta) return errorResult(`Secret not found: ${key}`);
1979
+ const { encrypted_value: _, ...safeMeta } = meta;
1694
1980
  return textResult(JSON.stringify({
1695
1981
  key,
1696
- ...meta
1982
+ ...safeMeta
1697
1983
  }, null, 2));
1698
1984
  };
1699
1985
  const handleCheckExpiration = (args) => {
@@ -1712,11 +1998,19 @@ const handleCheckExpiration = (args) => {
1712
1998
  issues: s.issues.toArray()
1713
1999
  }, null, 2)));
1714
2000
  };
2001
+ const handleGetEnvMeta = (args) => {
2002
+ const loaded = loadConfigForTool(args.configPath);
2003
+ if (!loaded.ok) return loaded.result;
2004
+ const { config } = loaded;
2005
+ const envAudit = computeEnvAudit(config);
2006
+ return textResult(JSON.stringify(envAudit, null, 2));
2007
+ };
1715
2008
  const handlers = {
1716
2009
  getPacketHealth: handleGetPacketHealth,
1717
2010
  listCapabilities: handleListCapabilities,
1718
2011
  getSecretMeta: handleGetSecretMeta,
1719
- checkExpiration: handleCheckExpiration
2012
+ checkExpiration: handleCheckExpiration,
2013
+ getEnvMeta: handleGetEnvMeta
1720
2014
  };
1721
2015
  const callTool = (name, args) => {
1722
2016
  const handler = handlers[name];
@@ -1737,17 +2031,17 @@ const createServer = () => {
1737
2031
  },
1738
2032
  instructions: "envpkt provides credential lifecycle awareness for AI agents. Use tools to check health, capabilities, and secret metadata. No secret values are ever exposed."
1739
2033
  });
1740
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions.map((t) => ({
2034
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: toolDefinitions.map((t) => ({
1741
2035
  name: t.name,
1742
2036
  description: t.description,
1743
2037
  inputSchema: t.inputSchema
1744
2038
  })) }));
1745
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
2039
+ server.setRequestHandler(CallToolRequestSchema, (request) => {
1746
2040
  const { name, arguments: args } = request.params;
1747
2041
  return callTool(name, args ?? {});
1748
2042
  });
1749
- server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [...resourceDefinitions] }));
1750
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
2043
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [...resourceDefinitions] }));
2044
+ server.setRequestHandler(ReadResourceRequestSchema, (request) => {
1751
2045
  const { uri } = request.params;
1752
2046
  const result = readResource(uri);
1753
2047
  if (!result) return { contents: [{
@@ -1766,4 +2060,4 @@ const startServer = async () => {
1766
2060
  };
1767
2061
 
1768
2062
  //#endregion
1769
- export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, createServer, deriveServiceFromName, detectFnox, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveSecrets, resourceDefinitions, scanEnv, scanFleet, startServer, toolDefinitions, unwrapAgentKey, validateConfig };
2063
+ export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, validateConfig };