envpkt 0.5.0 → 0.6.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/README.md +14 -14
- package/dist/cli.js +175 -166
- package/dist/index.d.ts +22 -8
- package/dist/index.js +166 -155
- package/package.json +1 -1
- package/schemas/envpkt.schema.json +11 -11
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ And a more complete one:
|
|
|
97
97
|
|
|
98
98
|
version = 1
|
|
99
99
|
|
|
100
|
-
[
|
|
100
|
+
[identity]
|
|
101
101
|
name = "billing-service"
|
|
102
102
|
consumer = "agent"
|
|
103
103
|
description = "Payment processing agent"
|
|
@@ -158,13 +158,13 @@ age-keygen -o identity.txt
|
|
|
158
158
|
Add the public key to your config and the identity file to `.gitignore`:
|
|
159
159
|
|
|
160
160
|
```toml
|
|
161
|
-
[
|
|
161
|
+
[identity]
|
|
162
162
|
name = "my-agent"
|
|
163
163
|
recipient = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
|
|
164
|
-
|
|
164
|
+
key_file = "identity.txt"
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
The `
|
|
167
|
+
The `key_file` path supports `~` expansion and environment variables (`$VAR`, `${VAR}`), so you can use paths like `~/keys/identity.txt` or `$KEYS_DIR/identity.txt`. Relative paths are resolved from the config file's directory. When omitted, envpkt falls back to `ENVPKT_AGE_KEY_FILE` env var, then `~/.envpkt/age-key.txt`.
|
|
168
168
|
|
|
169
169
|
### Seal
|
|
170
170
|
|
|
@@ -233,7 +233,7 @@ expires = "2026-11-01"
|
|
|
233
233
|
version = 1
|
|
234
234
|
catalog = "../../infra/envpkt.toml"
|
|
235
235
|
|
|
236
|
-
[
|
|
236
|
+
[identity]
|
|
237
237
|
name = "data-pipeline"
|
|
238
238
|
consumer = "agent"
|
|
239
239
|
secrets = ["DATABASE_URL", "REDIS_URL"]
|
|
@@ -255,7 +255,7 @@ This produces a self-contained config with catalog metadata merged in and agent
|
|
|
255
255
|
|
|
256
256
|
- Each field in the agent's `[secret.KEY]` override **replaces** the catalog field (shallow merge)
|
|
257
257
|
- Omitted fields keep the catalog value
|
|
258
|
-
- `
|
|
258
|
+
- `identity.secrets` is the source of truth for which keys the agent needs
|
|
259
259
|
|
|
260
260
|
## How envpkt Compares
|
|
261
261
|
|
|
@@ -267,7 +267,7 @@ The agentic credential space is splitting into approaches. Here's where envpkt f
|
|
|
267
267
|
| **What agents see** | Structured metadata (capabilities, constraints, expiration) | Raw secret values | Nothing (proxy handles it) | Nothing (autofill handles it) | Raw secret values |
|
|
268
268
|
| **MCP server** | Yes | Yes | No | No | Yes |
|
|
269
269
|
| **Encryption at rest** | age sealed packets | Git-crypt | N/A (proxy model) | Vault encryption | Vault encryption |
|
|
270
|
-
| **Per-agent scoping** | Yes (
|
|
270
|
+
| **Per-agent scoping** | Yes (identity.secrets, capabilities) | Yes (policies) | Yes (proxy rules) | No | Yes (policies) |
|
|
271
271
|
| **Fleet health monitoring** | Yes (fleet scan, aggregated audit) | No | No | No | No |
|
|
272
272
|
| **Credential metadata** | Rich (purpose, capabilities, rotation, lifecycle) | Minimal | Minimal | Minimal | Moderate |
|
|
273
273
|
| **Adoption path** | Scan existing env vars, add metadata incrementally | New secret storage workflow | Proxy configuration | Browser extension | API integration |
|
|
@@ -285,9 +285,9 @@ Generate an `envpkt.toml` template in the current directory.
|
|
|
285
285
|
```bash
|
|
286
286
|
envpkt init # Basic template
|
|
287
287
|
envpkt init --from-fnox # Scaffold from fnox.toml
|
|
288
|
-
envpkt init --
|
|
288
|
+
envpkt init --identity --name "my-agent" # Include identity section
|
|
289
289
|
envpkt init --catalog "../infra/envpkt.toml" # Reference a shared catalog
|
|
290
|
-
envpkt init --
|
|
290
|
+
envpkt init --identity --name "bot" --capabilities "read,write" --expires "2027-01-01"
|
|
291
291
|
```
|
|
292
292
|
|
|
293
293
|
### `envpkt audit`
|
|
@@ -354,13 +354,13 @@ envpkt seal -c path/to/envpkt.toml # Specify config path
|
|
|
354
354
|
envpkt seal --profile staging # Use a specific fnox profile for value resolution
|
|
355
355
|
```
|
|
356
356
|
|
|
357
|
-
Requires `
|
|
357
|
+
Requires `identity.recipient` (age public key) in your config. Values are resolved via cascade:
|
|
358
358
|
|
|
359
359
|
1. **fnox** (if available)
|
|
360
360
|
2. **Environment variables** (e.g. `OPENAI_API_KEY` in your shell)
|
|
361
361
|
3. **Interactive prompt** (asks you to paste each value)
|
|
362
362
|
|
|
363
|
-
After sealing, each secret gets an `encrypted_value` field. At boot time, `envpkt exec` or `boot()` automatically decrypts sealed values using the `
|
|
363
|
+
After sealing, each secret gets an `encrypted_value` field. At boot time, `envpkt exec` or `boot()` automatically decrypts sealed values using the `identity.key_file` path (or the default `~/.envpkt/age-key.txt`).
|
|
364
364
|
|
|
365
365
|
See [`examples/sealed-agent.toml`](./examples/sealed-agent.toml) for a complete example.
|
|
366
366
|
|
|
@@ -645,12 +645,12 @@ Each `[secret.<KEY>]` section describes a secret:
|
|
|
645
645
|
| **Sealed** | `encrypted_value` | Age-encrypted secret value (safe to commit) |
|
|
646
646
|
| **Enforcement** | `required`, `tags` | Filtering, grouping, and policy |
|
|
647
647
|
|
|
648
|
-
###
|
|
648
|
+
### Identity
|
|
649
649
|
|
|
650
|
-
The optional `[
|
|
650
|
+
The optional `[identity]` section identifies the consumer of these credentials:
|
|
651
651
|
|
|
652
652
|
```toml
|
|
653
|
-
[
|
|
653
|
+
[identity]
|
|
654
654
|
name = "data-pipeline-agent"
|
|
655
655
|
consumer = "agent" # agent | service | developer | ci
|
|
656
656
|
description = "ETL pipeline processor"
|
package/dist/cli.js
CHANGED
|
@@ -87,7 +87,7 @@ const computeAudit = (config, fnoxKeys, today) => {
|
|
|
87
87
|
missing,
|
|
88
88
|
missing_metadata,
|
|
89
89
|
orphaned,
|
|
90
|
-
|
|
90
|
+
identity: config.identity
|
|
91
91
|
};
|
|
92
92
|
};
|
|
93
93
|
const computeEnvAudit = (config, env = process.env) => {
|
|
@@ -125,20 +125,20 @@ const ConsumerType = Type.Union([
|
|
|
125
125
|
Type.Literal("developer"),
|
|
126
126
|
Type.Literal("ci")
|
|
127
127
|
], { description: "Classification of the agent's consumer type" });
|
|
128
|
-
const
|
|
129
|
-
name: Type.String({ description: "
|
|
128
|
+
const IdentitySchema = Type.Object({
|
|
129
|
+
name: Type.String({ description: "Display name" }),
|
|
130
130
|
consumer: Type.Optional(ConsumerType),
|
|
131
|
-
description: Type.Optional(Type.String({ description: "
|
|
132
|
-
capabilities: Type.Optional(Type.Array(Type.String(), { description: "List of capabilities this
|
|
131
|
+
description: Type.Optional(Type.String({ description: "Description or role" })),
|
|
132
|
+
capabilities: Type.Optional(Type.Array(Type.String(), { description: "List of capabilities this identity provides" })),
|
|
133
133
|
expires: Type.Optional(Type.String({
|
|
134
134
|
format: "date",
|
|
135
|
-
description: "
|
|
135
|
+
description: "Credential expiration date (YYYY-MM-DD)"
|
|
136
136
|
})),
|
|
137
|
-
services: Type.Optional(Type.Array(Type.String(), { description: "Service dependencies
|
|
138
|
-
|
|
139
|
-
recipient: Type.Optional(Type.String({ description: "
|
|
140
|
-
secrets: Type.Optional(Type.Array(Type.String(), { description: "Secret keys
|
|
141
|
-
}, { description: "Identity and capabilities of the
|
|
137
|
+
services: Type.Optional(Type.Array(Type.String(), { description: "Service dependencies" })),
|
|
138
|
+
key_file: Type.Optional(Type.String({ description: "Path to age identity file (relative to config directory)" })),
|
|
139
|
+
recipient: Type.Optional(Type.String({ description: "Age public key for encryption" })),
|
|
140
|
+
secrets: Type.Optional(Type.Array(Type.String(), { description: "Secret keys needed from the catalog" }))
|
|
141
|
+
}, { description: "Identity and capabilities of the principal using this envpkt" });
|
|
142
142
|
const SecretMetaSchema = Type.Object({
|
|
143
143
|
service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
|
|
144
144
|
expires: Type.Optional(Type.String({
|
|
@@ -196,7 +196,7 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
196
196
|
default: 1
|
|
197
197
|
}),
|
|
198
198
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
199
|
-
|
|
199
|
+
identity: Type.Optional(IdentitySchema),
|
|
200
200
|
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
201
201
|
env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
|
|
202
202
|
lifecycle: Type.Optional(LifecycleConfigSchema),
|
|
@@ -399,12 +399,12 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
399
399
|
overridden: [],
|
|
400
400
|
warnings: []
|
|
401
401
|
});
|
|
402
|
-
if (!agentConfig.
|
|
402
|
+
if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
|
|
403
403
|
_tag: "MissingSecretsList",
|
|
404
|
-
message: "Config has 'catalog' but
|
|
404
|
+
message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
|
|
405
405
|
});
|
|
406
406
|
const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
|
|
407
|
-
const agentSecrets = agentConfig.
|
|
407
|
+
const agentSecrets = agentConfig.identity.secrets;
|
|
408
408
|
const agentSecretEntries = agentConfig.secret ?? {};
|
|
409
409
|
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
410
410
|
const merged = [];
|
|
@@ -415,16 +415,16 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
415
415
|
if (agentSecretEntries[key]) overridden.push(key);
|
|
416
416
|
}
|
|
417
417
|
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
418
|
-
const
|
|
419
|
-
const { secrets: _secrets, ...rest } = agentConfig.
|
|
418
|
+
const identityData = agentConfig.identity ? (() => {
|
|
419
|
+
const { secrets: _secrets, ...rest } = agentConfig.identity;
|
|
420
420
|
return rest;
|
|
421
421
|
})() : void 0;
|
|
422
422
|
return {
|
|
423
423
|
config: {
|
|
424
424
|
...agentWithoutCatalog,
|
|
425
|
-
|
|
426
|
-
...
|
|
427
|
-
name:
|
|
425
|
+
identity: identityData ? {
|
|
426
|
+
...identityData,
|
|
427
|
+
name: identityData.name
|
|
428
428
|
} : void 0,
|
|
429
429
|
secret: resolvedMeta
|
|
430
430
|
},
|
|
@@ -521,9 +521,9 @@ const formatFleetJson = (fleet) => JSON.stringify({
|
|
|
521
521
|
expiring_soon: fleet.expiring_soon,
|
|
522
522
|
agents: fleet.agents.map((a) => ({
|
|
523
523
|
path: a.path,
|
|
524
|
-
name: a.
|
|
525
|
-
consumer: a.
|
|
526
|
-
description: a.
|
|
524
|
+
name: a.identity?.name ?? null,
|
|
525
|
+
consumer: a.identity?.consumer ?? null,
|
|
526
|
+
description: a.identity?.description ?? null,
|
|
527
527
|
status: a.audit.status,
|
|
528
528
|
secrets: a.audit.total
|
|
529
529
|
})).toArray()
|
|
@@ -820,6 +820,106 @@ const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((er
|
|
|
820
820
|
/** Extract the set of secret key names from a parsed fnox config */
|
|
821
821
|
const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
|
|
822
822
|
|
|
823
|
+
//#endregion
|
|
824
|
+
//#region src/core/keygen.ts
|
|
825
|
+
/** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
|
|
826
|
+
const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
|
|
827
|
+
/** Generate an age keypair and write to disk */
|
|
828
|
+
const generateKeypair = (options) => {
|
|
829
|
+
if (!ageAvailable()) return Left({
|
|
830
|
+
_tag: "AgeNotFound",
|
|
831
|
+
message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
|
|
832
|
+
});
|
|
833
|
+
const outputPath = options?.outputPath ?? resolveKeyPath();
|
|
834
|
+
if (existsSync(outputPath) && !options?.force) return Left({
|
|
835
|
+
_tag: "KeyExists",
|
|
836
|
+
path: outputPath
|
|
837
|
+
});
|
|
838
|
+
return Try(() => execFileSync("age-keygen", [], {
|
|
839
|
+
stdio: [
|
|
840
|
+
"pipe",
|
|
841
|
+
"pipe",
|
|
842
|
+
"pipe"
|
|
843
|
+
],
|
|
844
|
+
encoding: "utf-8"
|
|
845
|
+
})).fold((err) => Left({
|
|
846
|
+
_tag: "KeygenFailed",
|
|
847
|
+
message: `age-keygen failed: ${err}`
|
|
848
|
+
}), (output) => {
|
|
849
|
+
const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
|
|
850
|
+
if (!recipientLine) return Left({
|
|
851
|
+
_tag: "KeygenFailed",
|
|
852
|
+
message: "Could not parse public key from age-keygen output"
|
|
853
|
+
});
|
|
854
|
+
const recipient = recipientLine.replace("# public key: ", "").trim();
|
|
855
|
+
const dir = dirname(outputPath);
|
|
856
|
+
const mkdirFailed = Try(() => {
|
|
857
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
858
|
+
}).fold((err) => ({
|
|
859
|
+
_tag: "WriteError",
|
|
860
|
+
message: `Failed to create directory ${dir}: ${err}`
|
|
861
|
+
}), () => void 0);
|
|
862
|
+
if (mkdirFailed) return Left(mkdirFailed);
|
|
863
|
+
return Try(() => {
|
|
864
|
+
writeFileSync(outputPath, output, { mode: 384 });
|
|
865
|
+
chmodSync(outputPath, 384);
|
|
866
|
+
}).fold((err) => Left({
|
|
867
|
+
_tag: "WriteError",
|
|
868
|
+
message: `Failed to write identity file: ${err}`
|
|
869
|
+
}), () => Right({
|
|
870
|
+
recipient,
|
|
871
|
+
identityPath: outputPath,
|
|
872
|
+
configUpdated: false
|
|
873
|
+
}));
|
|
874
|
+
});
|
|
875
|
+
};
|
|
876
|
+
/** Update identity.recipient in an envpkt.toml file, preserving structure */
|
|
877
|
+
const updateConfigRecipient = (configPath, recipient) => {
|
|
878
|
+
return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
|
|
879
|
+
_tag: "ConfigUpdateError",
|
|
880
|
+
message: `Failed to read config: ${err}`
|
|
881
|
+
}), (raw) => {
|
|
882
|
+
const lines = raw.split("\n");
|
|
883
|
+
const output = [];
|
|
884
|
+
let inIdentitySection = false;
|
|
885
|
+
let recipientUpdated = false;
|
|
886
|
+
let hasIdentitySection = false;
|
|
887
|
+
for (const line of lines) {
|
|
888
|
+
if (/^\[identity\]\s*$/.test(line)) {
|
|
889
|
+
inIdentitySection = true;
|
|
890
|
+
hasIdentitySection = true;
|
|
891
|
+
output.push(line);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (/^\[/.test(line) && !/^\[identity\]\s*$/.test(line)) {
|
|
895
|
+
if (inIdentitySection && !recipientUpdated) {
|
|
896
|
+
output.push(`recipient = "${recipient}"`);
|
|
897
|
+
recipientUpdated = true;
|
|
898
|
+
}
|
|
899
|
+
inIdentitySection = false;
|
|
900
|
+
output.push(line);
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (inIdentitySection && /^recipient\s*=/.test(line)) {
|
|
904
|
+
output.push(`recipient = "${recipient}"`);
|
|
905
|
+
recipientUpdated = true;
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
output.push(line);
|
|
909
|
+
}
|
|
910
|
+
if (inIdentitySection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
|
|
911
|
+
if (!hasIdentitySection) {
|
|
912
|
+
output.push("");
|
|
913
|
+
output.push("[identity]");
|
|
914
|
+
output.push(`recipient = "${recipient}"`);
|
|
915
|
+
}
|
|
916
|
+
return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
|
|
917
|
+
_tag: "ConfigUpdateError",
|
|
918
|
+
message: `Failed to write config: ${err}`
|
|
919
|
+
}), () => Right(true));
|
|
920
|
+
});
|
|
921
|
+
};
|
|
922
|
+
|
|
823
923
|
//#endregion
|
|
824
924
|
//#region src/core/seal.ts
|
|
825
925
|
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
@@ -931,9 +1031,17 @@ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) =
|
|
|
931
1031
|
configSource
|
|
932
1032
|
}));
|
|
933
1033
|
}));
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
return
|
|
1034
|
+
/** Resolve identity file path with explicit fallback control */
|
|
1035
|
+
const resolveIdentityFilePath = (config, configDir, useDefaultFallback) => {
|
|
1036
|
+
if (config.identity?.key_file) return resolve(configDir, expandPath(config.identity.key_file));
|
|
1037
|
+
if (!useDefaultFallback) return void 0;
|
|
1038
|
+
const defaultPath = resolveKeyPath();
|
|
1039
|
+
return existsSync(defaultPath) ? defaultPath : void 0;
|
|
1040
|
+
};
|
|
1041
|
+
const resolveIdentityKey = (config, configDir) => {
|
|
1042
|
+
const identityPath = resolveIdentityFilePath(config, configDir, false);
|
|
1043
|
+
if (!identityPath) return Right(void 0);
|
|
1044
|
+
return unwrapAgentKey(identityPath).fold((err) => Left(err), (key) => Right(key));
|
|
937
1045
|
};
|
|
938
1046
|
const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
|
|
939
1047
|
const checkExpiration = (audit, failOnExpired, warnOnly) => {
|
|
@@ -976,10 +1084,10 @@ const bootSafe = (options) => {
|
|
|
976
1084
|
const secretEntries = config.secret ?? {};
|
|
977
1085
|
const metaKeys = Object.keys(secretEntries);
|
|
978
1086
|
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]?.encrypted_value);
|
|
979
|
-
const
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
if (
|
|
1087
|
+
const identityKeyResult = resolveIdentityKey(config, configDir);
|
|
1088
|
+
const identityKey = identityKeyResult.fold(() => void 0, (k) => k);
|
|
1089
|
+
const identityKeyError = identityKeyResult.fold((err) => err, () => void 0);
|
|
1090
|
+
if (identityKeyError && !hasSealedValues) return Left(identityKeyError);
|
|
983
1091
|
const audit = computeAudit(config, detectFnoxKeys(configDir));
|
|
984
1092
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
985
1093
|
const secrets = {};
|
|
@@ -994,7 +1102,8 @@ const bootSafe = (options) => {
|
|
|
994
1102
|
if (inject) process.env[key] = entry.value;
|
|
995
1103
|
} else overridden.push(key);
|
|
996
1104
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
997
|
-
|
|
1105
|
+
const identityFilePath = resolveIdentityFilePath(config, configDir, true);
|
|
1106
|
+
if (hasSealedValues && identityFilePath) unsealSecrets(secretEntries, identityFilePath).fold((err) => {
|
|
998
1107
|
warnings.push(`Sealed value decryption failed: ${err.message}`);
|
|
999
1108
|
}, (unsealed) => {
|
|
1000
1109
|
for (const [key, value] of Object.entries(unsealed)) {
|
|
@@ -1004,7 +1113,7 @@ const bootSafe = (options) => {
|
|
|
1004
1113
|
}
|
|
1005
1114
|
});
|
|
1006
1115
|
const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
|
|
1007
|
-
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile,
|
|
1116
|
+
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey).fold((err) => {
|
|
1008
1117
|
warnings.push(`fnox export failed: ${err.message}`);
|
|
1009
1118
|
for (const key of remainingKeys) skipped.push(key);
|
|
1010
1119
|
}, (exported) => {
|
|
@@ -2005,7 +2114,7 @@ const scanFleet = (rootDir, options) => {
|
|
|
2005
2114
|
const audit = computeAudit(config);
|
|
2006
2115
|
agents.push({
|
|
2007
2116
|
path: configPath,
|
|
2008
|
-
|
|
2117
|
+
identity: config.identity,
|
|
2009
2118
|
min_expiry_days: audit.secrets.toArray().reduce((min, s) => s.days_remaining.fold(() => min, (d) => min === void 0 ? d : Math.min(min, d)), void 0),
|
|
2010
2119
|
audit
|
|
2011
2120
|
});
|
|
@@ -2050,7 +2159,7 @@ const runFleet = (options) => {
|
|
|
2050
2159
|
if (fleet.expiring_soon > 0) console.log(` ${YELLOW}${fleet.expiring_soon}${RESET} expiring soon`);
|
|
2051
2160
|
console.log("");
|
|
2052
2161
|
for (const agent of agents) {
|
|
2053
|
-
const name = agent.
|
|
2162
|
+
const name = agent.identity?.name ? BOLD + agent.identity.name + RESET : DIM + agent.path + RESET;
|
|
2054
2163
|
const icon = statusIcon(agent.audit.status);
|
|
2055
2164
|
console.log(` ${icon} ${name} ${DIM}(${agent.audit.total} secrets)${RESET}`);
|
|
2056
2165
|
}
|
|
@@ -2073,8 +2182,8 @@ created = "${todayIso()}"
|
|
|
2073
2182
|
# tags = {}
|
|
2074
2183
|
`;
|
|
2075
2184
|
};
|
|
2076
|
-
const
|
|
2077
|
-
return `[
|
|
2185
|
+
const generateIdentitySection = (name, capabilities, expires) => {
|
|
2186
|
+
return `[identity]
|
|
2078
2187
|
name = "${name}"
|
|
2079
2188
|
# consumer = "agent" # agent | service | developer | ci${capabilities ? `\ncapabilities = [${capabilities.split(",").map((c) => `"${c.trim()}"`).join(", ")}]` : ""}${expires ? `\nexpires = "${expires}"` : ""}
|
|
2080
2189
|
`;
|
|
@@ -2089,8 +2198,8 @@ const generateTemplate = (options, fnoxKeys) => {
|
|
|
2089
2198
|
lines.push(`catalog = "${options.catalog}"`);
|
|
2090
2199
|
lines.push(``);
|
|
2091
2200
|
}
|
|
2092
|
-
if (options.
|
|
2093
|
-
lines.push(
|
|
2201
|
+
if (options.identity && options.name) {
|
|
2202
|
+
lines.push(generateIdentitySection(options.name, options.capabilities, options.expires));
|
|
2094
2203
|
if (options.catalog) lines.push(`secrets = [] # Add catalog secret keys this agent needs`);
|
|
2095
2204
|
lines.push(``);
|
|
2096
2205
|
}
|
|
@@ -2203,14 +2312,14 @@ const printConfig = (config, path, resolveResult, opts) => {
|
|
|
2203
2312
|
if (resolveResult?.catalogPath) console.log(`${DIM}Catalog: ${CYAN}${resolveResult.catalogPath}${RESET}`);
|
|
2204
2313
|
console.log(`version: ${config.version}`);
|
|
2205
2314
|
console.log("");
|
|
2206
|
-
if (config.
|
|
2207
|
-
console.log(`${BOLD}
|
|
2208
|
-
if (config.
|
|
2209
|
-
if (config.
|
|
2210
|
-
if (config.
|
|
2211
|
-
if (config.
|
|
2212
|
-
if (config.
|
|
2213
|
-
if (config.
|
|
2315
|
+
if (config.identity) {
|
|
2316
|
+
console.log(`${BOLD}Identity:${RESET} ${config.identity.name}`);
|
|
2317
|
+
if (config.identity.consumer) console.log(` consumer: ${config.identity.consumer}`);
|
|
2318
|
+
if (config.identity.description) console.log(` description: ${config.identity.description}`);
|
|
2319
|
+
if (config.identity.capabilities) console.log(` capabilities: ${config.identity.capabilities.join(", ")}`);
|
|
2320
|
+
if (config.identity.expires) console.log(` expires: ${config.identity.expires}`);
|
|
2321
|
+
if (config.identity.services) console.log(` services: ${config.identity.services.join(", ")}`);
|
|
2322
|
+
if (config.identity.secrets) console.log(` secrets: ${config.identity.secrets.join(", ")}`);
|
|
2214
2323
|
console.log("");
|
|
2215
2324
|
}
|
|
2216
2325
|
const secretEntries = config.secret ?? {};
|
|
@@ -2284,106 +2393,6 @@ const runInspect = (options) => {
|
|
|
2284
2393
|
});
|
|
2285
2394
|
};
|
|
2286
2395
|
|
|
2287
|
-
//#endregion
|
|
2288
|
-
//#region src/core/keygen.ts
|
|
2289
|
-
/** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
|
|
2290
|
-
const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
|
|
2291
|
-
/** Generate an age keypair and write to disk */
|
|
2292
|
-
const generateKeypair = (options) => {
|
|
2293
|
-
if (!ageAvailable()) return Left({
|
|
2294
|
-
_tag: "AgeNotFound",
|
|
2295
|
-
message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
|
|
2296
|
-
});
|
|
2297
|
-
const outputPath = options?.outputPath ?? resolveKeyPath();
|
|
2298
|
-
if (existsSync(outputPath) && !options?.force) return Left({
|
|
2299
|
-
_tag: "KeyExists",
|
|
2300
|
-
path: outputPath
|
|
2301
|
-
});
|
|
2302
|
-
return Try(() => execFileSync("age-keygen", [], {
|
|
2303
|
-
stdio: [
|
|
2304
|
-
"pipe",
|
|
2305
|
-
"pipe",
|
|
2306
|
-
"pipe"
|
|
2307
|
-
],
|
|
2308
|
-
encoding: "utf-8"
|
|
2309
|
-
})).fold((err) => Left({
|
|
2310
|
-
_tag: "KeygenFailed",
|
|
2311
|
-
message: `age-keygen failed: ${err}`
|
|
2312
|
-
}), (output) => {
|
|
2313
|
-
const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
|
|
2314
|
-
if (!recipientLine) return Left({
|
|
2315
|
-
_tag: "KeygenFailed",
|
|
2316
|
-
message: "Could not parse public key from age-keygen output"
|
|
2317
|
-
});
|
|
2318
|
-
const recipient = recipientLine.replace("# public key: ", "").trim();
|
|
2319
|
-
const dir = dirname(outputPath);
|
|
2320
|
-
const mkdirFailed = Try(() => {
|
|
2321
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2322
|
-
}).fold((err) => ({
|
|
2323
|
-
_tag: "WriteError",
|
|
2324
|
-
message: `Failed to create directory ${dir}: ${err}`
|
|
2325
|
-
}), () => void 0);
|
|
2326
|
-
if (mkdirFailed) return Left(mkdirFailed);
|
|
2327
|
-
return Try(() => {
|
|
2328
|
-
writeFileSync(outputPath, output, { mode: 384 });
|
|
2329
|
-
chmodSync(outputPath, 384);
|
|
2330
|
-
}).fold((err) => Left({
|
|
2331
|
-
_tag: "WriteError",
|
|
2332
|
-
message: `Failed to write identity file: ${err}`
|
|
2333
|
-
}), () => Right({
|
|
2334
|
-
recipient,
|
|
2335
|
-
identityPath: outputPath,
|
|
2336
|
-
configUpdated: false
|
|
2337
|
-
}));
|
|
2338
|
-
});
|
|
2339
|
-
};
|
|
2340
|
-
/** Update agent.recipient in an envpkt.toml file, preserving structure */
|
|
2341
|
-
const updateConfigRecipient = (configPath, recipient) => {
|
|
2342
|
-
return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
|
|
2343
|
-
_tag: "ConfigUpdateError",
|
|
2344
|
-
message: `Failed to read config: ${err}`
|
|
2345
|
-
}), (raw) => {
|
|
2346
|
-
const lines = raw.split("\n");
|
|
2347
|
-
const output = [];
|
|
2348
|
-
let inAgentSection = false;
|
|
2349
|
-
let recipientUpdated = false;
|
|
2350
|
-
let hasAgentSection = false;
|
|
2351
|
-
for (const line of lines) {
|
|
2352
|
-
if (/^\[agent\]\s*$/.test(line)) {
|
|
2353
|
-
inAgentSection = true;
|
|
2354
|
-
hasAgentSection = true;
|
|
2355
|
-
output.push(line);
|
|
2356
|
-
continue;
|
|
2357
|
-
}
|
|
2358
|
-
if (/^\[/.test(line) && !/^\[agent\]\s*$/.test(line)) {
|
|
2359
|
-
if (inAgentSection && !recipientUpdated) {
|
|
2360
|
-
output.push(`recipient = "${recipient}"`);
|
|
2361
|
-
recipientUpdated = true;
|
|
2362
|
-
}
|
|
2363
|
-
inAgentSection = false;
|
|
2364
|
-
output.push(line);
|
|
2365
|
-
continue;
|
|
2366
|
-
}
|
|
2367
|
-
if (inAgentSection && /^recipient\s*=/.test(line)) {
|
|
2368
|
-
output.push(`recipient = "${recipient}"`);
|
|
2369
|
-
recipientUpdated = true;
|
|
2370
|
-
continue;
|
|
2371
|
-
}
|
|
2372
|
-
output.push(line);
|
|
2373
|
-
}
|
|
2374
|
-
if (inAgentSection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
|
|
2375
|
-
if (!hasAgentSection) {
|
|
2376
|
-
output.push("");
|
|
2377
|
-
output.push("[agent]");
|
|
2378
|
-
output.push(`recipient = "${recipient}"`);
|
|
2379
|
-
}
|
|
2380
|
-
return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
|
|
2381
|
-
_tag: "ConfigUpdateError",
|
|
2382
|
-
message: `Failed to write config: ${err}`
|
|
2383
|
-
}), () => Right(true));
|
|
2384
|
-
});
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
2396
|
//#endregion
|
|
2388
2397
|
//#region src/cli/commands/keygen.ts
|
|
2389
2398
|
const runKeygen = (options) => {
|
|
@@ -2407,10 +2416,10 @@ const runKeygen = (options) => {
|
|
|
2407
2416
|
if (existsSync(configPath)) updateConfigRecipient(configPath, recipient).fold((err) => {
|
|
2408
2417
|
console.error(`${YELLOW}Warning:${RESET} Could not update config: ${"message" in err ? err.message : err._tag}`);
|
|
2409
2418
|
console.log(`${DIM}Manually add to your envpkt.toml:${RESET}`);
|
|
2410
|
-
console.log(` [
|
|
2419
|
+
console.log(` [identity]`);
|
|
2411
2420
|
console.log(` recipient = "${recipient}"`);
|
|
2412
2421
|
}, () => {
|
|
2413
|
-
console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with
|
|
2422
|
+
console.log(`${GREEN}Updated${RESET} ${CYAN}${configPath}${RESET} with identity.recipient`);
|
|
2414
2423
|
});
|
|
2415
2424
|
else {
|
|
2416
2425
|
console.log(`${BOLD}Next steps:${RESET}`);
|
|
@@ -2472,7 +2481,7 @@ const readCapabilities = () => {
|
|
|
2472
2481
|
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
2473
2482
|
}] };
|
|
2474
2483
|
const { config } = loaded;
|
|
2475
|
-
const agentCapabilities = config.
|
|
2484
|
+
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2476
2485
|
const secretCapabilities = {};
|
|
2477
2486
|
const secretEntries = config.secret ?? {};
|
|
2478
2487
|
for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
@@ -2480,10 +2489,10 @@ const readCapabilities = () => {
|
|
|
2480
2489
|
uri: "envpkt://capabilities",
|
|
2481
2490
|
mimeType: "application/json",
|
|
2482
2491
|
text: JSON.stringify({
|
|
2483
|
-
|
|
2484
|
-
name: config.
|
|
2485
|
-
consumer: config.
|
|
2486
|
-
description: config.
|
|
2492
|
+
identity: config.identity ? {
|
|
2493
|
+
name: config.identity.name,
|
|
2494
|
+
consumer: config.identity.consumer,
|
|
2495
|
+
description: config.identity.description,
|
|
2487
2496
|
capabilities: agentCapabilities
|
|
2488
2497
|
} : null,
|
|
2489
2498
|
secrets: secretCapabilities
|
|
@@ -2625,15 +2634,15 @@ const handleListCapabilities = (args) => {
|
|
|
2625
2634
|
const loaded = loadConfigForTool(args.configPath);
|
|
2626
2635
|
if (!loaded.ok) return loaded.result;
|
|
2627
2636
|
const { config } = loaded;
|
|
2628
|
-
const agentCapabilities = config.
|
|
2637
|
+
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2629
2638
|
const secretCapabilities = {};
|
|
2630
2639
|
const secretEntries = config.secret ?? {};
|
|
2631
2640
|
for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2632
2641
|
return textResult(JSON.stringify({
|
|
2633
|
-
|
|
2634
|
-
name: config.
|
|
2635
|
-
consumer: config.
|
|
2636
|
-
description: config.
|
|
2642
|
+
identity: config.identity ? {
|
|
2643
|
+
name: config.identity.name,
|
|
2644
|
+
consumer: config.identity.consumer,
|
|
2645
|
+
description: config.identity.description,
|
|
2637
2646
|
capabilities: agentCapabilities
|
|
2638
2647
|
} : null,
|
|
2639
2648
|
secrets: secretCapabilities,
|
|
@@ -2895,16 +2904,16 @@ const runSeal = async (options) => {
|
|
|
2895
2904
|
console.error(formatError(err));
|
|
2896
2905
|
process.exit(2);
|
|
2897
2906
|
}, (c) => c);
|
|
2898
|
-
if (!config.
|
|
2899
|
-
console.error(`${RED}Error:${RESET}
|
|
2907
|
+
if (!config.identity?.recipient) {
|
|
2908
|
+
console.error(`${RED}Error:${RESET} identity.recipient is required for sealing (age public key)`);
|
|
2900
2909
|
console.error("");
|
|
2901
2910
|
console.error(`${BOLD}Quick fix:${RESET} run ${CYAN}envpkt keygen${RESET} to generate a key and auto-configure recipient`);
|
|
2902
2911
|
console.error(`${DIM}Or manually add to your envpkt.toml:${RESET}`);
|
|
2903
|
-
console.error(`${DIM} [
|
|
2912
|
+
console.error(`${DIM} [identity]${RESET}`);
|
|
2904
2913
|
console.error(`${DIM} recipient = "age1..."${RESET}`);
|
|
2905
2914
|
process.exit(2);
|
|
2906
2915
|
}
|
|
2907
|
-
const { recipient } = config.
|
|
2916
|
+
const { recipient } = config.identity;
|
|
2908
2917
|
const configDir = dirname(configPath);
|
|
2909
2918
|
const envEntries = config.env ?? {};
|
|
2910
2919
|
const secretEntries0 = config.secret ?? {};
|
|
@@ -2914,7 +2923,7 @@ const runSeal = async (options) => {
|
|
|
2914
2923
|
console.error(`${DIM}Move these to [secret.*] only, or remove from [env.*] before sealing.${RESET}`);
|
|
2915
2924
|
process.exit(2);
|
|
2916
2925
|
}
|
|
2917
|
-
const
|
|
2926
|
+
const identityKey = config.identity.key_file ? unwrapAgentKey(resolve(configDir, expandPath(config.identity.key_file))).fold((err) => {
|
|
2918
2927
|
const msg = err._tag === "IdentityNotFound" ? `not found: ${err.path}` : err.message;
|
|
2919
2928
|
console.error(`${YELLOW}Warning:${RESET} Could not unwrap agent key: ${msg}`);
|
|
2920
2929
|
}, (k) => k) : void 0;
|
|
@@ -2934,7 +2943,7 @@ const runSeal = async (options) => {
|
|
|
2934
2943
|
const metaKeys = targetKeys;
|
|
2935
2944
|
console.log(`${BOLD}Sealing ${metaKeys.length} secret(s)${RESET} with recipient ${CYAN}${recipient.slice(0, 20)}...${RESET}`);
|
|
2936
2945
|
console.log("");
|
|
2937
|
-
const values = await resolveValues(metaKeys, options.profile,
|
|
2946
|
+
const values = await resolveValues(metaKeys, options.profile, identityKey);
|
|
2938
2947
|
const resolved = Object.keys(values).length;
|
|
2939
2948
|
const skipped = metaKeys.length - resolved;
|
|
2940
2949
|
if (resolved === 0) {
|
|
@@ -3007,10 +3016,10 @@ program.name("envpkt").description("Credential lifecycle and fleet management fo
|
|
|
3007
3016
|
const pkgPath = findPkgJson(dirname(fileURLToPath(import.meta.url)));
|
|
3008
3017
|
return pkgPath ? JSON.parse(readFileSync(pkgPath, "utf-8")).version : "0.0.0";
|
|
3009
3018
|
})());
|
|
3010
|
-
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("--
|
|
3019
|
+
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("--identity", "Include [identity] section").option("--name <name>", "Identity name (requires --identity)").option("--capabilities <caps>", "Comma-separated capabilities (requires --identity)").option("--expires <date>", "Credential expiration YYYY-MM-DD (requires --identity)").option("--force", "Overwrite existing envpkt.toml").action((options) => {
|
|
3011
3020
|
runInit(process.cwd(), options);
|
|
3012
3021
|
});
|
|
3013
|
-
program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates
|
|
3022
|
+
program.command("keygen").description("Generate an age keypair for sealing secrets — run this before `seal` if you don't have a key yet").option("-c, --config <path>", "Path to envpkt.toml (updates identity.recipient if found)").option("--force", "Overwrite existing identity file").option("-o, --output <path>", "Output path for identity file (default: ~/.envpkt/age-key.txt)").action((options) => {
|
|
3014
3023
|
runKeygen(options);
|
|
3015
3024
|
});
|
|
3016
3025
|
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) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,19 @@ import { CallToolResult, ReadResourceResult, Resource } from "@modelcontextproto
|
|
|
7
7
|
//#region src/core/schema.d.ts
|
|
8
8
|
declare const ConsumerType: _sinclair_typebox0.TUnion<[_sinclair_typebox0.TLiteral<"agent">, _sinclair_typebox0.TLiteral<"service">, _sinclair_typebox0.TLiteral<"developer">, _sinclair_typebox0.TLiteral<"ci">]>;
|
|
9
9
|
type ConsumerType = Static<typeof ConsumerType>;
|
|
10
|
+
declare const IdentitySchema: _sinclair_typebox0.TObject<{
|
|
11
|
+
name: _sinclair_typebox0.TString;
|
|
12
|
+
consumer: _sinclair_typebox0.TOptional<_sinclair_typebox0.TUnion<[_sinclair_typebox0.TLiteral<"agent">, _sinclair_typebox0.TLiteral<"service">, _sinclair_typebox0.TLiteral<"developer">, _sinclair_typebox0.TLiteral<"ci">]>>;
|
|
13
|
+
description: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
14
|
+
capabilities: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
15
|
+
expires: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
16
|
+
services: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
17
|
+
key_file: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
18
|
+
recipient: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
19
|
+
secrets: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
20
|
+
}>;
|
|
21
|
+
type Identity = Static<typeof IdentitySchema>;
|
|
22
|
+
/** @deprecated Use `IdentitySchema` instead */
|
|
10
23
|
declare const AgentIdentitySchema: _sinclair_typebox0.TObject<{
|
|
11
24
|
name: _sinclair_typebox0.TString;
|
|
12
25
|
consumer: _sinclair_typebox0.TOptional<_sinclair_typebox0.TUnion<[_sinclair_typebox0.TLiteral<"agent">, _sinclair_typebox0.TLiteral<"service">, _sinclair_typebox0.TLiteral<"developer">, _sinclair_typebox0.TLiteral<"ci">]>>;
|
|
@@ -14,11 +27,10 @@ declare const AgentIdentitySchema: _sinclair_typebox0.TObject<{
|
|
|
14
27
|
capabilities: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
15
28
|
expires: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
16
29
|
services: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
17
|
-
|
|
30
|
+
key_file: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
18
31
|
recipient: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
19
32
|
secrets: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
20
33
|
}>;
|
|
21
|
-
type AgentIdentity = Static<typeof AgentIdentitySchema>;
|
|
22
34
|
declare const SecretMetaSchema: _sinclair_typebox0.TObject<{
|
|
23
35
|
service: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
24
36
|
expires: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
@@ -60,14 +72,14 @@ type EnvMeta = Static<typeof EnvMetaSchema>;
|
|
|
60
72
|
declare const EnvpktConfigSchema: _sinclair_typebox0.TObject<{
|
|
61
73
|
version: _sinclair_typebox0.TNumber;
|
|
62
74
|
catalog: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
63
|
-
|
|
75
|
+
identity: _sinclair_typebox0.TOptional<_sinclair_typebox0.TObject<{
|
|
64
76
|
name: _sinclair_typebox0.TString;
|
|
65
77
|
consumer: _sinclair_typebox0.TOptional<_sinclair_typebox0.TUnion<[_sinclair_typebox0.TLiteral<"agent">, _sinclair_typebox0.TLiteral<"service">, _sinclair_typebox0.TLiteral<"developer">, _sinclair_typebox0.TLiteral<"ci">]>>;
|
|
66
78
|
description: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
67
79
|
capabilities: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
68
80
|
expires: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
69
81
|
services: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
70
|
-
|
|
82
|
+
key_file: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
71
83
|
recipient: _sinclair_typebox0.TOptional<_sinclair_typebox0.TString>;
|
|
72
84
|
secrets: _sinclair_typebox0.TOptional<_sinclair_typebox0.TArray<_sinclair_typebox0.TString>>;
|
|
73
85
|
}>>;
|
|
@@ -108,6 +120,8 @@ declare const EnvpktConfigSchema: _sinclair_typebox0.TObject<{
|
|
|
108
120
|
type EnvpktConfig = Static<typeof EnvpktConfigSchema>;
|
|
109
121
|
//#endregion
|
|
110
122
|
//#region src/core/types.d.ts
|
|
123
|
+
/** @deprecated Use `Identity` instead */
|
|
124
|
+
type AgentIdentity = Identity;
|
|
111
125
|
type HealthStatus = "healthy" | "degraded" | "critical";
|
|
112
126
|
type SecretStatus = "healthy" | "expiring_soon" | "expired" | "stale" | "missing" | "missing_metadata";
|
|
113
127
|
type SecretHealth = {
|
|
@@ -132,7 +146,7 @@ type AuditResult = {
|
|
|
132
146
|
readonly missing: number;
|
|
133
147
|
readonly missing_metadata: number;
|
|
134
148
|
readonly orphaned: number;
|
|
135
|
-
readonly
|
|
149
|
+
readonly identity?: Identity;
|
|
136
150
|
};
|
|
137
151
|
type EnvDriftStatus = "default" | "overridden" | "missing";
|
|
138
152
|
type EnvDriftEntry = {
|
|
@@ -151,7 +165,7 @@ type EnvAuditResult = {
|
|
|
151
165
|
};
|
|
152
166
|
type FleetAgent = {
|
|
153
167
|
readonly path: string;
|
|
154
|
-
readonly
|
|
168
|
+
readonly identity?: Identity;
|
|
155
169
|
readonly min_expiry_days?: number;
|
|
156
170
|
readonly audit: AuditResult;
|
|
157
171
|
};
|
|
@@ -437,7 +451,7 @@ declare const generateKeypair: (options?: {
|
|
|
437
451
|
readonly force?: boolean;
|
|
438
452
|
readonly outputPath?: string;
|
|
439
453
|
}) => Either<KeygenError, KeygenResult>;
|
|
440
|
-
/** Update
|
|
454
|
+
/** Update identity.recipient in an envpkt.toml file, preserving structure */
|
|
441
455
|
declare const updateConfigRecipient: (configPath: string, recipient: string) => Either<KeygenError, true>;
|
|
442
456
|
//#endregion
|
|
443
457
|
//#region src/core/resolve-values.d.ts
|
|
@@ -501,4 +515,4 @@ type ToolDef = {
|
|
|
501
515
|
declare const toolDefinitions: readonly ToolDef[];
|
|
502
516
|
declare const callTool: (name: string, args: Record<string, unknown>) => CallToolResult;
|
|
503
517
|
//#endregion
|
|
504
|
-
export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type IdentityError, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
|
|
518
|
+
export { type AgentIdentity, AgentIdentitySchema, type AuditResult, type BootError, type BootOptions, type BootResult, type CallbackConfig, CallbackConfigSchema, type CatalogError, type CheckResult, type ConfidenceLevel, type ConfigError, type ConfigSource, type ConsumerType, type CredentialPattern, type DriftEntry, type DriftStatus, type EnvAuditResult, type EnvDriftEntry, type EnvDriftStatus, type EnvMeta, EnvMetaSchema, EnvpktBootError, type EnvpktConfig, EnvpktConfigSchema, type FleetAgent, type FleetHealth, type FnoxConfig, type FnoxError, type FnoxSecret, type FormatPacketOptions, type HealthStatus, type Identity, type IdentityError, IdentitySchema, type KeygenError, type KeygenResult, type LifecycleConfig, LifecycleConfigSchema, type MatchResult, type ResolveOptions, type ResolveResult, type ResolvedPath, type ScanOptions, type ScanResult, type SealError, type SecretDisplay, type SecretHealth, type SecretMeta, SecretMetaSchema, type SecretStatus, type ToolsConfig, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { TypeCompiler } from "@sinclair/typebox/compiler";
|
|
|
4
4
|
import { Cond, Left, List, None, Option, Right, Some, Try } from "functype";
|
|
5
5
|
import { Env, Fs, Path } from "functype-os";
|
|
6
6
|
import { TomlDate, parse } from "smol-toml";
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
7
|
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { createInterface } from "node:readline";
|
|
11
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -23,20 +23,22 @@ const ConsumerType = Type.Union([
|
|
|
23
23
|
Type.Literal("developer"),
|
|
24
24
|
Type.Literal("ci")
|
|
25
25
|
], { description: "Classification of the agent's consumer type" });
|
|
26
|
-
const
|
|
27
|
-
name: Type.String({ description: "
|
|
26
|
+
const IdentitySchema = Type.Object({
|
|
27
|
+
name: Type.String({ description: "Display name" }),
|
|
28
28
|
consumer: Type.Optional(ConsumerType),
|
|
29
|
-
description: Type.Optional(Type.String({ description: "
|
|
30
|
-
capabilities: Type.Optional(Type.Array(Type.String(), { description: "List of capabilities this
|
|
29
|
+
description: Type.Optional(Type.String({ description: "Description or role" })),
|
|
30
|
+
capabilities: Type.Optional(Type.Array(Type.String(), { description: "List of capabilities this identity provides" })),
|
|
31
31
|
expires: Type.Optional(Type.String({
|
|
32
32
|
format: "date",
|
|
33
|
-
description: "
|
|
33
|
+
description: "Credential expiration date (YYYY-MM-DD)"
|
|
34
34
|
})),
|
|
35
|
-
services: Type.Optional(Type.Array(Type.String(), { description: "Service dependencies
|
|
36
|
-
|
|
37
|
-
recipient: Type.Optional(Type.String({ description: "
|
|
38
|
-
secrets: Type.Optional(Type.Array(Type.String(), { description: "Secret keys
|
|
39
|
-
}, { description: "Identity and capabilities of the
|
|
35
|
+
services: Type.Optional(Type.Array(Type.String(), { description: "Service dependencies" })),
|
|
36
|
+
key_file: Type.Optional(Type.String({ description: "Path to age identity file (relative to config directory)" })),
|
|
37
|
+
recipient: Type.Optional(Type.String({ description: "Age public key for encryption" })),
|
|
38
|
+
secrets: Type.Optional(Type.Array(Type.String(), { description: "Secret keys needed from the catalog" }))
|
|
39
|
+
}, { description: "Identity and capabilities of the principal using this envpkt" });
|
|
40
|
+
/** @deprecated Use `IdentitySchema` instead */
|
|
41
|
+
const AgentIdentitySchema = IdentitySchema;
|
|
40
42
|
const SecretMetaSchema = Type.Object({
|
|
41
43
|
service: Type.Optional(Type.String({ description: "Service or system this secret authenticates to" })),
|
|
42
44
|
expires: Type.Optional(Type.String({
|
|
@@ -94,7 +96,7 @@ const EnvpktConfigSchema = Type.Object({
|
|
|
94
96
|
default: 1
|
|
95
97
|
}),
|
|
96
98
|
catalog: Type.Optional(Type.String({ description: "Path to shared secret catalog (relative to this config file)" })),
|
|
97
|
-
|
|
99
|
+
identity: Type.Optional(IdentitySchema),
|
|
98
100
|
secret: Type.Optional(Type.Record(Type.String(), SecretMetaSchema, { description: "Per-secret metadata keyed by secret name" })),
|
|
99
101
|
env: Type.Optional(Type.Record(Type.String(), EnvMetaSchema, { description: "Plaintext environment defaults keyed by variable name" })),
|
|
100
102
|
lifecycle: Type.Optional(LifecycleConfigSchema),
|
|
@@ -311,12 +313,12 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
311
313
|
overridden: [],
|
|
312
314
|
warnings: []
|
|
313
315
|
});
|
|
314
|
-
if (!agentConfig.
|
|
316
|
+
if (!agentConfig.identity?.secrets || agentConfig.identity.secrets.length === 0) return Left({
|
|
315
317
|
_tag: "MissingSecretsList",
|
|
316
|
-
message: "Config has 'catalog' but
|
|
318
|
+
message: "Config has 'catalog' but identity.secrets is missing — declare which catalog secrets this agent needs"
|
|
317
319
|
});
|
|
318
320
|
const catalogPath = resolve(agentConfigDir, agentConfig.catalog);
|
|
319
|
-
const agentSecrets = agentConfig.
|
|
321
|
+
const agentSecrets = agentConfig.identity.secrets;
|
|
320
322
|
const agentSecretEntries = agentConfig.secret ?? {};
|
|
321
323
|
return loadCatalog(catalogPath).flatMap((catalogConfig) => resolveSecrets(agentSecretEntries, catalogConfig.secret ?? {}, agentSecrets, catalogPath).map((resolvedMeta) => {
|
|
322
324
|
const merged = [];
|
|
@@ -327,16 +329,16 @@ const resolveConfig = (agentConfig, agentConfigDir) => {
|
|
|
327
329
|
if (agentSecretEntries[key]) overridden.push(key);
|
|
328
330
|
}
|
|
329
331
|
const { catalog: _catalog, ...agentWithoutCatalog } = agentConfig;
|
|
330
|
-
const
|
|
331
|
-
const { secrets: _secrets, ...rest } = agentConfig.
|
|
332
|
+
const identityData = agentConfig.identity ? (() => {
|
|
333
|
+
const { secrets: _secrets, ...rest } = agentConfig.identity;
|
|
332
334
|
return rest;
|
|
333
335
|
})() : void 0;
|
|
334
336
|
return {
|
|
335
337
|
config: {
|
|
336
338
|
...agentWithoutCatalog,
|
|
337
|
-
|
|
338
|
-
...
|
|
339
|
-
name:
|
|
339
|
+
identity: identityData ? {
|
|
340
|
+
...identityData,
|
|
341
|
+
name: identityData.name
|
|
340
342
|
} : void 0,
|
|
341
343
|
secret: resolvedMeta
|
|
342
344
|
},
|
|
@@ -379,16 +381,16 @@ const formatSecretFields = (meta, indent) => {
|
|
|
379
381
|
const formatPacket = (result, options) => {
|
|
380
382
|
const { config } = result;
|
|
381
383
|
const sections = [];
|
|
382
|
-
if (config.
|
|
383
|
-
const consumer = config.
|
|
384
|
-
sections.push(`envpkt packet: ${config.
|
|
384
|
+
if (config.identity) {
|
|
385
|
+
const consumer = config.identity.consumer ? ` (${config.identity.consumer})` : "";
|
|
386
|
+
sections.push(`envpkt packet: ${config.identity.name}${consumer}`);
|
|
385
387
|
} else sections.push("envpkt packet");
|
|
386
|
-
if (config.
|
|
388
|
+
if (config.identity) {
|
|
387
389
|
const agentLines = [];
|
|
388
|
-
if (config.
|
|
389
|
-
if (config.
|
|
390
|
-
if (config.
|
|
391
|
-
if (config.
|
|
390
|
+
if (config.identity.description) agentLines.push(` ${config.identity.description}`);
|
|
391
|
+
if (config.identity.capabilities) agentLines.push(` capabilities: ${config.identity.capabilities.join(", ")}`);
|
|
392
|
+
if (config.identity.services) agentLines.push(` services: ${config.identity.services.join(", ")}`);
|
|
393
|
+
if (config.identity.expires) agentLines.push(` expires: ${config.identity.expires}`);
|
|
392
394
|
if (agentLines.length > 0) sections.push(agentLines.join("\n"));
|
|
393
395
|
}
|
|
394
396
|
const secretConfig = config.secret ?? {};
|
|
@@ -494,7 +496,7 @@ const computeAudit = (config, fnoxKeys, today) => {
|
|
|
494
496
|
missing,
|
|
495
497
|
missing_metadata,
|
|
496
498
|
orphaned,
|
|
497
|
-
|
|
499
|
+
identity: config.identity
|
|
498
500
|
};
|
|
499
501
|
};
|
|
500
502
|
const computeEnvAudit = (config, env = process.env) => {
|
|
@@ -1421,6 +1423,111 @@ const readFnoxConfig = (path) => Try(() => readFileSync(path, "utf-8")).fold((er
|
|
|
1421
1423
|
/** Extract the set of secret key names from a parsed fnox config */
|
|
1422
1424
|
const extractFnoxKeys = (config) => new Set(Object.keys(config.secrets));
|
|
1423
1425
|
|
|
1426
|
+
//#endregion
|
|
1427
|
+
//#region src/core/keygen.ts
|
|
1428
|
+
/** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
|
|
1429
|
+
const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
|
|
1430
|
+
/** Resolve an inline age key from ENVPKT_AGE_KEY env var (for CI) */
|
|
1431
|
+
const resolveInlineKey = () => {
|
|
1432
|
+
const key = process.env["ENVPKT_AGE_KEY"];
|
|
1433
|
+
return key ? Some(key) : None();
|
|
1434
|
+
};
|
|
1435
|
+
/** Generate an age keypair and write to disk */
|
|
1436
|
+
const generateKeypair = (options) => {
|
|
1437
|
+
if (!ageAvailable()) return Left({
|
|
1438
|
+
_tag: "AgeNotFound",
|
|
1439
|
+
message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
|
|
1440
|
+
});
|
|
1441
|
+
const outputPath = options?.outputPath ?? resolveKeyPath();
|
|
1442
|
+
if (existsSync(outputPath) && !options?.force) return Left({
|
|
1443
|
+
_tag: "KeyExists",
|
|
1444
|
+
path: outputPath
|
|
1445
|
+
});
|
|
1446
|
+
return Try(() => execFileSync("age-keygen", [], {
|
|
1447
|
+
stdio: [
|
|
1448
|
+
"pipe",
|
|
1449
|
+
"pipe",
|
|
1450
|
+
"pipe"
|
|
1451
|
+
],
|
|
1452
|
+
encoding: "utf-8"
|
|
1453
|
+
})).fold((err) => Left({
|
|
1454
|
+
_tag: "KeygenFailed",
|
|
1455
|
+
message: `age-keygen failed: ${err}`
|
|
1456
|
+
}), (output) => {
|
|
1457
|
+
const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
|
|
1458
|
+
if (!recipientLine) return Left({
|
|
1459
|
+
_tag: "KeygenFailed",
|
|
1460
|
+
message: "Could not parse public key from age-keygen output"
|
|
1461
|
+
});
|
|
1462
|
+
const recipient = recipientLine.replace("# public key: ", "").trim();
|
|
1463
|
+
const dir = dirname(outputPath);
|
|
1464
|
+
const mkdirFailed = Try(() => {
|
|
1465
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1466
|
+
}).fold((err) => ({
|
|
1467
|
+
_tag: "WriteError",
|
|
1468
|
+
message: `Failed to create directory ${dir}: ${err}`
|
|
1469
|
+
}), () => void 0);
|
|
1470
|
+
if (mkdirFailed) return Left(mkdirFailed);
|
|
1471
|
+
return Try(() => {
|
|
1472
|
+
writeFileSync(outputPath, output, { mode: 384 });
|
|
1473
|
+
chmodSync(outputPath, 384);
|
|
1474
|
+
}).fold((err) => Left({
|
|
1475
|
+
_tag: "WriteError",
|
|
1476
|
+
message: `Failed to write identity file: ${err}`
|
|
1477
|
+
}), () => Right({
|
|
1478
|
+
recipient,
|
|
1479
|
+
identityPath: outputPath,
|
|
1480
|
+
configUpdated: false
|
|
1481
|
+
}));
|
|
1482
|
+
});
|
|
1483
|
+
};
|
|
1484
|
+
/** Update identity.recipient in an envpkt.toml file, preserving structure */
|
|
1485
|
+
const updateConfigRecipient = (configPath, recipient) => {
|
|
1486
|
+
return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
|
|
1487
|
+
_tag: "ConfigUpdateError",
|
|
1488
|
+
message: `Failed to read config: ${err}`
|
|
1489
|
+
}), (raw) => {
|
|
1490
|
+
const lines = raw.split("\n");
|
|
1491
|
+
const output = [];
|
|
1492
|
+
let inIdentitySection = false;
|
|
1493
|
+
let recipientUpdated = false;
|
|
1494
|
+
let hasIdentitySection = false;
|
|
1495
|
+
for (const line of lines) {
|
|
1496
|
+
if (/^\[identity\]\s*$/.test(line)) {
|
|
1497
|
+
inIdentitySection = true;
|
|
1498
|
+
hasIdentitySection = true;
|
|
1499
|
+
output.push(line);
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (/^\[/.test(line) && !/^\[identity\]\s*$/.test(line)) {
|
|
1503
|
+
if (inIdentitySection && !recipientUpdated) {
|
|
1504
|
+
output.push(`recipient = "${recipient}"`);
|
|
1505
|
+
recipientUpdated = true;
|
|
1506
|
+
}
|
|
1507
|
+
inIdentitySection = false;
|
|
1508
|
+
output.push(line);
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
if (inIdentitySection && /^recipient\s*=/.test(line)) {
|
|
1512
|
+
output.push(`recipient = "${recipient}"`);
|
|
1513
|
+
recipientUpdated = true;
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
output.push(line);
|
|
1517
|
+
}
|
|
1518
|
+
if (inIdentitySection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
|
|
1519
|
+
if (!hasIdentitySection) {
|
|
1520
|
+
output.push("");
|
|
1521
|
+
output.push("[identity]");
|
|
1522
|
+
output.push(`recipient = "${recipient}"`);
|
|
1523
|
+
}
|
|
1524
|
+
return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
|
|
1525
|
+
_tag: "ConfigUpdateError",
|
|
1526
|
+
message: `Failed to write config: ${err}`
|
|
1527
|
+
}), () => Right(true));
|
|
1528
|
+
});
|
|
1529
|
+
};
|
|
1530
|
+
|
|
1424
1531
|
//#endregion
|
|
1425
1532
|
//#region src/core/seal.ts
|
|
1426
1533
|
/** Encrypt a plaintext string using age with the given recipient public key (armored output) */
|
|
@@ -1532,9 +1639,17 @@ const resolveAndLoad = (opts) => resolveConfigPath(opts.configPath).fold((err) =
|
|
|
1532
1639
|
configSource
|
|
1533
1640
|
}));
|
|
1534
1641
|
}));
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
return
|
|
1642
|
+
/** Resolve identity file path with explicit fallback control */
|
|
1643
|
+
const resolveIdentityFilePath = (config, configDir, useDefaultFallback) => {
|
|
1644
|
+
if (config.identity?.key_file) return resolve(configDir, expandPath(config.identity.key_file));
|
|
1645
|
+
if (!useDefaultFallback) return void 0;
|
|
1646
|
+
const defaultPath = resolveKeyPath();
|
|
1647
|
+
return existsSync(defaultPath) ? defaultPath : void 0;
|
|
1648
|
+
};
|
|
1649
|
+
const resolveIdentityKey = (config, configDir) => {
|
|
1650
|
+
const identityPath = resolveIdentityFilePath(config, configDir, false);
|
|
1651
|
+
if (!identityPath) return Right(void 0);
|
|
1652
|
+
return unwrapAgentKey(identityPath).fold((err) => Left(err), (key) => Right(key));
|
|
1538
1653
|
};
|
|
1539
1654
|
const detectFnoxKeys = (configDir) => detectFnox(configDir).fold(() => /* @__PURE__ */ new Set(), (fnoxPath) => readFnoxConfig(fnoxPath).fold(() => /* @__PURE__ */ new Set(), (fnoxConfig) => extractFnoxKeys(fnoxConfig)));
|
|
1540
1655
|
const checkExpiration = (audit, failOnExpired, warnOnly) => {
|
|
@@ -1577,10 +1692,10 @@ const bootSafe = (options) => {
|
|
|
1577
1692
|
const secretEntries = config.secret ?? {};
|
|
1578
1693
|
const metaKeys = Object.keys(secretEntries);
|
|
1579
1694
|
const hasSealedValues = metaKeys.some((k) => !!secretEntries[k]?.encrypted_value);
|
|
1580
|
-
const
|
|
1581
|
-
const
|
|
1582
|
-
const
|
|
1583
|
-
if (
|
|
1695
|
+
const identityKeyResult = resolveIdentityKey(config, configDir);
|
|
1696
|
+
const identityKey = identityKeyResult.fold(() => void 0, (k) => k);
|
|
1697
|
+
const identityKeyError = identityKeyResult.fold((err) => err, () => void 0);
|
|
1698
|
+
if (identityKeyError && !hasSealedValues) return Left(identityKeyError);
|
|
1584
1699
|
const audit = computeAudit(config, detectFnoxKeys(configDir));
|
|
1585
1700
|
return checkExpiration(audit, failOnExpired, warnOnly).map((warnings) => {
|
|
1586
1701
|
const secrets = {};
|
|
@@ -1595,7 +1710,8 @@ const bootSafe = (options) => {
|
|
|
1595
1710
|
if (inject) process.env[key] = entry.value;
|
|
1596
1711
|
} else overridden.push(key);
|
|
1597
1712
|
const sealedKeys = /* @__PURE__ */ new Set();
|
|
1598
|
-
|
|
1713
|
+
const identityFilePath = resolveIdentityFilePath(config, configDir, true);
|
|
1714
|
+
if (hasSealedValues && identityFilePath) unsealSecrets(secretEntries, identityFilePath).fold((err) => {
|
|
1599
1715
|
warnings.push(`Sealed value decryption failed: ${err.message}`);
|
|
1600
1716
|
}, (unsealed) => {
|
|
1601
1717
|
for (const [key, value] of Object.entries(unsealed)) {
|
|
@@ -1605,7 +1721,7 @@ const bootSafe = (options) => {
|
|
|
1605
1721
|
}
|
|
1606
1722
|
});
|
|
1607
1723
|
const remainingKeys = metaKeys.filter((k) => !sealedKeys.has(k));
|
|
1608
|
-
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile,
|
|
1724
|
+
if (remainingKeys.length > 0) if (fnoxAvailable()) fnoxExport(opts.profile, identityKey).fold((err) => {
|
|
1609
1725
|
warnings.push(`fnox export failed: ${err.message}`);
|
|
1610
1726
|
for (const key of remainingKeys) skipped.push(key);
|
|
1611
1727
|
}, (exported) => {
|
|
@@ -1667,111 +1783,6 @@ const formatBootError = (error) => {
|
|
|
1667
1783
|
}
|
|
1668
1784
|
};
|
|
1669
1785
|
|
|
1670
|
-
//#endregion
|
|
1671
|
-
//#region src/core/keygen.ts
|
|
1672
|
-
/** Resolve the age identity file path: ENVPKT_AGE_KEY_FILE env var > ~/.envpkt/age-key.txt */
|
|
1673
|
-
const resolveKeyPath = () => process.env["ENVPKT_AGE_KEY_FILE"] ?? join(homedir(), ".envpkt", "age-key.txt");
|
|
1674
|
-
/** Resolve an inline age key from ENVPKT_AGE_KEY env var (for CI) */
|
|
1675
|
-
const resolveInlineKey = () => {
|
|
1676
|
-
const key = process.env["ENVPKT_AGE_KEY"];
|
|
1677
|
-
return key ? Some(key) : None();
|
|
1678
|
-
};
|
|
1679
|
-
/** Generate an age keypair and write to disk */
|
|
1680
|
-
const generateKeypair = (options) => {
|
|
1681
|
-
if (!ageAvailable()) return Left({
|
|
1682
|
-
_tag: "AgeNotFound",
|
|
1683
|
-
message: "age-keygen CLI not found on PATH. Install age: https://github.com/FiloSottile/age"
|
|
1684
|
-
});
|
|
1685
|
-
const outputPath = options?.outputPath ?? resolveKeyPath();
|
|
1686
|
-
if (existsSync(outputPath) && !options?.force) return Left({
|
|
1687
|
-
_tag: "KeyExists",
|
|
1688
|
-
path: outputPath
|
|
1689
|
-
});
|
|
1690
|
-
return Try(() => execFileSync("age-keygen", [], {
|
|
1691
|
-
stdio: [
|
|
1692
|
-
"pipe",
|
|
1693
|
-
"pipe",
|
|
1694
|
-
"pipe"
|
|
1695
|
-
],
|
|
1696
|
-
encoding: "utf-8"
|
|
1697
|
-
})).fold((err) => Left({
|
|
1698
|
-
_tag: "KeygenFailed",
|
|
1699
|
-
message: `age-keygen failed: ${err}`
|
|
1700
|
-
}), (output) => {
|
|
1701
|
-
const recipientLine = output.split("\n").find((l) => l.startsWith("# public key:"));
|
|
1702
|
-
if (!recipientLine) return Left({
|
|
1703
|
-
_tag: "KeygenFailed",
|
|
1704
|
-
message: "Could not parse public key from age-keygen output"
|
|
1705
|
-
});
|
|
1706
|
-
const recipient = recipientLine.replace("# public key: ", "").trim();
|
|
1707
|
-
const dir = dirname(outputPath);
|
|
1708
|
-
const mkdirFailed = Try(() => {
|
|
1709
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1710
|
-
}).fold((err) => ({
|
|
1711
|
-
_tag: "WriteError",
|
|
1712
|
-
message: `Failed to create directory ${dir}: ${err}`
|
|
1713
|
-
}), () => void 0);
|
|
1714
|
-
if (mkdirFailed) return Left(mkdirFailed);
|
|
1715
|
-
return Try(() => {
|
|
1716
|
-
writeFileSync(outputPath, output, { mode: 384 });
|
|
1717
|
-
chmodSync(outputPath, 384);
|
|
1718
|
-
}).fold((err) => Left({
|
|
1719
|
-
_tag: "WriteError",
|
|
1720
|
-
message: `Failed to write identity file: ${err}`
|
|
1721
|
-
}), () => Right({
|
|
1722
|
-
recipient,
|
|
1723
|
-
identityPath: outputPath,
|
|
1724
|
-
configUpdated: false
|
|
1725
|
-
}));
|
|
1726
|
-
});
|
|
1727
|
-
};
|
|
1728
|
-
/** Update agent.recipient in an envpkt.toml file, preserving structure */
|
|
1729
|
-
const updateConfigRecipient = (configPath, recipient) => {
|
|
1730
|
-
return Try(() => readFileSync(configPath, "utf-8")).fold((err) => Left({
|
|
1731
|
-
_tag: "ConfigUpdateError",
|
|
1732
|
-
message: `Failed to read config: ${err}`
|
|
1733
|
-
}), (raw) => {
|
|
1734
|
-
const lines = raw.split("\n");
|
|
1735
|
-
const output = [];
|
|
1736
|
-
let inAgentSection = false;
|
|
1737
|
-
let recipientUpdated = false;
|
|
1738
|
-
let hasAgentSection = false;
|
|
1739
|
-
for (const line of lines) {
|
|
1740
|
-
if (/^\[agent\]\s*$/.test(line)) {
|
|
1741
|
-
inAgentSection = true;
|
|
1742
|
-
hasAgentSection = true;
|
|
1743
|
-
output.push(line);
|
|
1744
|
-
continue;
|
|
1745
|
-
}
|
|
1746
|
-
if (/^\[/.test(line) && !/^\[agent\]\s*$/.test(line)) {
|
|
1747
|
-
if (inAgentSection && !recipientUpdated) {
|
|
1748
|
-
output.push(`recipient = "${recipient}"`);
|
|
1749
|
-
recipientUpdated = true;
|
|
1750
|
-
}
|
|
1751
|
-
inAgentSection = false;
|
|
1752
|
-
output.push(line);
|
|
1753
|
-
continue;
|
|
1754
|
-
}
|
|
1755
|
-
if (inAgentSection && /^recipient\s*=/.test(line)) {
|
|
1756
|
-
output.push(`recipient = "${recipient}"`);
|
|
1757
|
-
recipientUpdated = true;
|
|
1758
|
-
continue;
|
|
1759
|
-
}
|
|
1760
|
-
output.push(line);
|
|
1761
|
-
}
|
|
1762
|
-
if (inAgentSection && !recipientUpdated) output.push(`recipient = "${recipient}"`);
|
|
1763
|
-
if (!hasAgentSection) {
|
|
1764
|
-
output.push("");
|
|
1765
|
-
output.push("[agent]");
|
|
1766
|
-
output.push(`recipient = "${recipient}"`);
|
|
1767
|
-
}
|
|
1768
|
-
return Try(() => writeFileSync(configPath, output.join("\n"))).fold((err) => Left({
|
|
1769
|
-
_tag: "ConfigUpdateError",
|
|
1770
|
-
message: `Failed to write config: ${err}`
|
|
1771
|
-
}), () => Right(true));
|
|
1772
|
-
});
|
|
1773
|
-
};
|
|
1774
|
-
|
|
1775
1786
|
//#endregion
|
|
1776
1787
|
//#region src/core/resolve-values.ts
|
|
1777
1788
|
/** Resolve plaintext values for the given keys via cascade: fnox → env → interactive prompt */
|
|
@@ -1851,7 +1862,7 @@ const scanFleet = (rootDir, options) => {
|
|
|
1851
1862
|
const audit = computeAudit(config);
|
|
1852
1863
|
agents.push({
|
|
1853
1864
|
path: configPath,
|
|
1854
|
-
|
|
1865
|
+
identity: config.identity,
|
|
1855
1866
|
min_expiry_days: audit.secrets.toArray().reduce((min, s) => s.days_remaining.fold(() => min, (d) => min === void 0 ? d : Math.min(min, d)), void 0),
|
|
1856
1867
|
audit
|
|
1857
1868
|
});
|
|
@@ -1934,7 +1945,7 @@ const readCapabilities = () => {
|
|
|
1934
1945
|
text: JSON.stringify({ error: "No envpkt.toml found" })
|
|
1935
1946
|
}] };
|
|
1936
1947
|
const { config } = loaded;
|
|
1937
|
-
const agentCapabilities = config.
|
|
1948
|
+
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
1938
1949
|
const secretCapabilities = {};
|
|
1939
1950
|
const secretEntries = config.secret ?? {};
|
|
1940
1951
|
for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
@@ -1942,10 +1953,10 @@ const readCapabilities = () => {
|
|
|
1942
1953
|
uri: "envpkt://capabilities",
|
|
1943
1954
|
mimeType: "application/json",
|
|
1944
1955
|
text: JSON.stringify({
|
|
1945
|
-
|
|
1946
|
-
name: config.
|
|
1947
|
-
consumer: config.
|
|
1948
|
-
description: config.
|
|
1956
|
+
identity: config.identity ? {
|
|
1957
|
+
name: config.identity.name,
|
|
1958
|
+
consumer: config.identity.consumer,
|
|
1959
|
+
description: config.identity.description,
|
|
1949
1960
|
capabilities: agentCapabilities
|
|
1950
1961
|
} : null,
|
|
1951
1962
|
secrets: secretCapabilities
|
|
@@ -2087,15 +2098,15 @@ const handleListCapabilities = (args) => {
|
|
|
2087
2098
|
const loaded = loadConfigForTool(args.configPath);
|
|
2088
2099
|
if (!loaded.ok) return loaded.result;
|
|
2089
2100
|
const { config } = loaded;
|
|
2090
|
-
const agentCapabilities = config.
|
|
2101
|
+
const agentCapabilities = config.identity?.capabilities ?? [];
|
|
2091
2102
|
const secretCapabilities = {};
|
|
2092
2103
|
const secretEntries = config.secret ?? {};
|
|
2093
2104
|
for (const [key, meta] of Object.entries(secretEntries)) if (meta.capabilities && meta.capabilities.length > 0) secretCapabilities[key] = meta.capabilities;
|
|
2094
2105
|
return textResult(JSON.stringify({
|
|
2095
|
-
|
|
2096
|
-
name: config.
|
|
2097
|
-
consumer: config.
|
|
2098
|
-
description: config.
|
|
2106
|
+
identity: config.identity ? {
|
|
2107
|
+
name: config.identity.name,
|
|
2108
|
+
consumer: config.identity.consumer,
|
|
2109
|
+
description: config.identity.description,
|
|
2099
2110
|
capabilities: agentCapabilities
|
|
2100
2111
|
} : null,
|
|
2101
2112
|
secrets: secretCapabilities,
|
|
@@ -2194,4 +2205,4 @@ const startServer = async () => {
|
|
|
2194
2205
|
};
|
|
2195
2206
|
|
|
2196
2207
|
//#endregion
|
|
2197
|
-
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, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
|
|
2208
|
+
export { AgentIdentitySchema, CallbackConfigSchema, ConsumerType, EnvMetaSchema, EnvpktBootError, EnvpktConfigSchema, IdentitySchema, LifecycleConfigSchema, SecretMetaSchema, ToolsConfigSchema, ageAvailable, ageDecrypt, ageEncrypt, boot, bootSafe, callTool, compareFnoxAndEnvpkt, computeAudit, computeEnvAudit, createServer, deriveServiceFromName, detectFnox, discoverConfig, envCheck, envScan, extractFnoxKeys, findConfigPath, fnoxAvailable, fnoxExport, fnoxGet, formatPacket, generateKeypair, generateTomlFromScan, loadCatalog, loadConfig, loadConfigFromCwd, maskValue, matchEnvVar, matchValueShape, parseToml, readConfigFile, readFnoxConfig, readResource, resolveConfig, resolveConfigPath, resolveInlineKey, resolveKeyPath, resolveSecrets, resolveValues, resourceDefinitions, scanEnv, scanFleet, sealSecrets, startServer, toolDefinitions, unsealSecrets, unwrapAgentKey, updateConfigRecipient, validateConfig };
|
package/package.json
CHANGED
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"description": "Path to shared secret catalog (relative to this config file)",
|
|
18
18
|
"type": "string"
|
|
19
19
|
},
|
|
20
|
-
"
|
|
21
|
-
"description": "Identity and capabilities of the
|
|
20
|
+
"identity": {
|
|
21
|
+
"description": "Identity and capabilities of the principal using this envpkt",
|
|
22
22
|
"type": "object",
|
|
23
23
|
"required": [
|
|
24
24
|
"name"
|
|
25
25
|
],
|
|
26
26
|
"properties": {
|
|
27
27
|
"name": {
|
|
28
|
-
"description": "
|
|
28
|
+
"description": "Display name",
|
|
29
29
|
"type": "string"
|
|
30
30
|
},
|
|
31
31
|
"consumer": {
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
]
|
|
51
51
|
},
|
|
52
52
|
"description": {
|
|
53
|
-
"description": "
|
|
53
|
+
"description": "Description or role",
|
|
54
54
|
"type": "string"
|
|
55
55
|
},
|
|
56
56
|
"capabilities": {
|
|
57
|
-
"description": "List of capabilities this
|
|
57
|
+
"description": "List of capabilities this identity provides",
|
|
58
58
|
"type": "array",
|
|
59
59
|
"items": {
|
|
60
60
|
"type": "string"
|
|
@@ -62,26 +62,26 @@
|
|
|
62
62
|
},
|
|
63
63
|
"expires": {
|
|
64
64
|
"format": "date",
|
|
65
|
-
"description": "
|
|
65
|
+
"description": "Credential expiration date (YYYY-MM-DD)",
|
|
66
66
|
"type": "string"
|
|
67
67
|
},
|
|
68
68
|
"services": {
|
|
69
|
-
"description": "Service dependencies
|
|
69
|
+
"description": "Service dependencies",
|
|
70
70
|
"type": "array",
|
|
71
71
|
"items": {
|
|
72
72
|
"type": "string"
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
-
"
|
|
76
|
-
"description": "Path to
|
|
75
|
+
"key_file": {
|
|
76
|
+
"description": "Path to age identity file (relative to config directory)",
|
|
77
77
|
"type": "string"
|
|
78
78
|
},
|
|
79
79
|
"recipient": {
|
|
80
|
-
"description": "
|
|
80
|
+
"description": "Age public key for encryption",
|
|
81
81
|
"type": "string"
|
|
82
82
|
},
|
|
83
83
|
"secrets": {
|
|
84
|
-
"description": "Secret keys
|
|
84
|
+
"description": "Secret keys needed from the catalog",
|
|
85
85
|
"type": "array",
|
|
86
86
|
"items": {
|
|
87
87
|
"type": "string"
|