1use clap::ValueEnum;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum ExplainTopicSurface {
7 AlwaysAvailable,
8 CoreDefault,
9 GatedNonCore,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
14pub enum ExplainTopic {
15 Exec,
17 Export,
19 Namespaces,
21 #[cfg(feature = "agent")]
23 Agent,
24 #[value(name = "exec-security")]
26 ExecSecurity,
27 Contracts,
29 #[value(name = "vault-recovery")]
31 VaultRecovery,
32 #[cfg(any(
34 feature = "akv-pull",
35 feature = "cloud-pull-aws",
36 feature = "cloud-pull-gcp",
37 feature = "cloud-pull-vault",
38 feature = "cloud-pull-1password",
39 feature = "multi-pull"
40 ))]
41 Pull,
42 #[value(name = "pull-auth")]
44 #[cfg(any(
45 feature = "akv-pull",
46 feature = "cloud-pull-aws",
47 feature = "cloud-pull-gcp",
48 feature = "cloud-pull-vault",
49 feature = "cloud-pull-1password",
50 feature = "multi-pull"
51 ))]
52 PullAuth,
53 #[value(name = "pull-reliability")]
55 #[cfg(any(
56 feature = "akv-pull",
57 feature = "cloud-pull-aws",
58 feature = "cloud-pull-gcp",
59 feature = "cloud-pull-vault",
60 feature = "cloud-pull-1password",
61 feature = "multi-pull"
62 ))]
63 PullReliability,
64}
65
66impl ExplainTopic {
67 pub const fn surface(self) -> ExplainTopicSurface {
68 match self {
69 ExplainTopic::Exec
70 | ExplainTopic::Export
71 | ExplainTopic::Namespaces
72 | ExplainTopic::ExecSecurity
73 | ExplainTopic::Contracts
74 | ExplainTopic::VaultRecovery => ExplainTopicSurface::AlwaysAvailable,
75 #[cfg(feature = "agent")]
76 ExplainTopic::Agent => ExplainTopicSurface::CoreDefault,
77 #[cfg(any(
78 feature = "akv-pull",
79 feature = "cloud-pull-aws",
80 feature = "cloud-pull-gcp",
81 feature = "cloud-pull-vault",
82 feature = "cloud-pull-1password",
83 feature = "multi-pull"
84 ))]
85 ExplainTopic::Pull | ExplainTopic::PullAuth | ExplainTopic::PullReliability => {
86 pull_topic_surface()
87 }
88 }
89 }
90
91 pub const fn as_str(self) -> &'static str {
92 match self {
93 ExplainTopic::Exec => "exec",
94 ExplainTopic::Export => "export",
95 ExplainTopic::Namespaces => "namespaces",
96 #[cfg(feature = "agent")]
97 ExplainTopic::Agent => "agent",
98 ExplainTopic::ExecSecurity => "exec-security",
99 ExplainTopic::Contracts => "contracts",
100 ExplainTopic::VaultRecovery => "vault-recovery",
101 #[cfg(any(
102 feature = "akv-pull",
103 feature = "cloud-pull-aws",
104 feature = "cloud-pull-gcp",
105 feature = "cloud-pull-vault",
106 feature = "cloud-pull-1password",
107 feature = "multi-pull"
108 ))]
109 ExplainTopic::Pull => "pull",
110 #[cfg(any(
111 feature = "akv-pull",
112 feature = "cloud-pull-aws",
113 feature = "cloud-pull-gcp",
114 feature = "cloud-pull-vault",
115 feature = "cloud-pull-1password",
116 feature = "multi-pull"
117 ))]
118 ExplainTopic::PullAuth => "pull-auth",
119 #[cfg(any(
120 feature = "akv-pull",
121 feature = "cloud-pull-aws",
122 feature = "cloud-pull-gcp",
123 feature = "cloud-pull-vault",
124 feature = "cloud-pull-1password",
125 feature = "multi-pull"
126 ))]
127 ExplainTopic::PullReliability => "pull-reliability",
128 }
129 }
130
131 pub fn blurb(self) -> &'static str {
132 match self {
133 ExplainTopic::Exec => "inject secrets into one child process (primary workflow)",
134 ExplainTopic::Export => "print secrets to stdout / shell (secondary)",
135 ExplainTopic::Namespaces => "isolate keys per app or repo in one vault",
136 #[cfg(feature = "agent")]
137 ExplainTopic::Agent => "keep vault unlocked for a TTL; reuse across commands",
138 ExplainTopic::ExecSecurity => {
139 "inheritance, stripped vars, NODE_OPTIONS / LD_* warnings"
140 }
141 ExplainTopic::Contracts => {
142 "named exec policy in .tsafe.yml (allowed secrets, targets, trust level)"
143 }
144 ExplainTopic::VaultRecovery => "options when you forget the master password",
145 #[cfg(any(
146 feature = "akv-pull",
147 feature = "cloud-pull-aws",
148 feature = "cloud-pull-gcp",
149 feature = "cloud-pull-vault",
150 feature = "cloud-pull-1password",
151 feature = "multi-pull"
152 ))]
153 ExplainTopic::Pull => {
154 if has_non_azure_pull_family() {
155 "sync secrets from compiled-in pull sources into the local vault"
156 } else {
157 "sync secrets from Azure Key Vault into the local vault"
158 }
159 }
160 #[cfg(any(
161 feature = "akv-pull",
162 feature = "cloud-pull-aws",
163 feature = "cloud-pull-gcp",
164 feature = "cloud-pull-vault",
165 feature = "cloud-pull-1password",
166 feature = "multi-pull"
167 ))]
168 ExplainTopic::PullAuth => {
169 if has_non_azure_pull_family() {
170 "credential setup for compiled-in pull sources"
171 } else {
172 "credential setup for Azure Key Vault pull"
173 }
174 }
175 #[cfg(any(
176 feature = "akv-pull",
177 feature = "cloud-pull-aws",
178 feature = "cloud-pull-gcp",
179 feature = "cloud-pull-vault",
180 feature = "cloud-pull-1password",
181 feature = "multi-pull"
182 ))]
183 ExplainTopic::PullReliability => "retries, transient failures, and --on-error modes",
184 }
185 }
186}
187
188fn surface_label(surface: ExplainTopicSurface) -> &'static str {
189 match surface {
190 ExplainTopicSurface::AlwaysAvailable => "always",
191 ExplainTopicSurface::CoreDefault => "core/default",
192 ExplainTopicSurface::GatedNonCore => "gated non-core",
193 }
194}
195
196#[cfg(any(
197 feature = "akv-pull",
198 feature = "cloud-pull-aws",
199 feature = "cloud-pull-gcp",
200 feature = "cloud-pull-vault",
201 feature = "cloud-pull-1password",
202 feature = "multi-pull"
203))]
204const fn pull_topic_surface() -> ExplainTopicSurface {
205 if cfg!(feature = "akv-pull")
206 && !cfg!(feature = "cloud-pull-aws")
207 && !cfg!(feature = "cloud-pull-gcp")
208 && !cfg!(feature = "cloud-pull-vault")
209 && !cfg!(feature = "cloud-pull-1password")
210 && !cfg!(feature = "multi-pull")
211 {
212 ExplainTopicSurface::CoreDefault
213 } else {
214 ExplainTopicSurface::GatedNonCore
215 }
216}
217
218pub static ALL_TOPICS: &[ExplainTopic] = &[
219 ExplainTopic::Exec,
220 ExplainTopic::Export,
221 ExplainTopic::Namespaces,
222 #[cfg(feature = "agent")]
223 ExplainTopic::Agent,
224 ExplainTopic::ExecSecurity,
225 ExplainTopic::Contracts,
226 ExplainTopic::VaultRecovery,
227 #[cfg(any(
228 feature = "akv-pull",
229 feature = "cloud-pull-aws",
230 feature = "cloud-pull-gcp",
231 feature = "cloud-pull-vault",
232 feature = "cloud-pull-1password",
233 feature = "multi-pull"
234 ))]
235 ExplainTopic::Pull,
236 #[cfg(any(
237 feature = "akv-pull",
238 feature = "cloud-pull-aws",
239 feature = "cloud-pull-gcp",
240 feature = "cloud-pull-vault",
241 feature = "cloud-pull-1password",
242 feature = "multi-pull"
243 ))]
244 ExplainTopic::PullAuth,
245 #[cfg(any(
246 feature = "akv-pull",
247 feature = "cloud-pull-aws",
248 feature = "cloud-pull-gcp",
249 feature = "cloud-pull-vault",
250 feature = "cloud-pull-1password",
251 feature = "multi-pull"
252 ))]
253 ExplainTopic::PullReliability,
254];
255
256#[cfg(any(
257 feature = "akv-pull",
258 feature = "cloud-pull-aws",
259 feature = "cloud-pull-gcp",
260 feature = "cloud-pull-vault",
261 feature = "cloud-pull-1password",
262 feature = "multi-pull"
263))]
264struct PullSourceDoc {
265 label: &'static str,
266 command: &'static str,
267}
268
269#[cfg(any(
270 feature = "akv-pull",
271 feature = "cloud-pull-aws",
272 feature = "cloud-pull-gcp",
273 feature = "cloud-pull-vault",
274 feature = "cloud-pull-1password",
275 feature = "multi-pull"
276))]
277static PULL_SOURCES: &[PullSourceDoc] = &[
278 #[cfg(feature = "akv-pull")]
279 PullSourceDoc {
280 label: "Azure Key Vault",
281 command: "tsafe kv-pull",
282 },
283 #[cfg(feature = "cloud-pull-aws")]
284 PullSourceDoc {
285 label: "AWS Secrets Manager",
286 command: "tsafe aws-pull",
287 },
288 #[cfg(feature = "cloud-pull-aws")]
289 PullSourceDoc {
290 label: "AWS SSM Parameter Store",
291 command: "tsafe ssm-pull",
292 },
293 #[cfg(feature = "cloud-pull-gcp")]
294 PullSourceDoc {
295 label: "GCP Secret Manager",
296 command: "tsafe gcp-pull",
297 },
298 #[cfg(feature = "cloud-pull-vault")]
299 PullSourceDoc {
300 label: "HashiCorp Vault KV v2",
301 command: "tsafe vault-pull",
302 },
303 #[cfg(feature = "cloud-pull-1password")]
304 PullSourceDoc {
305 label: "1Password",
306 command: "tsafe op-pull",
307 },
308];
309
310#[cfg(any(
311 feature = "akv-pull",
312 feature = "cloud-pull-aws",
313 feature = "cloud-pull-gcp",
314 feature = "cloud-pull-vault",
315 feature = "cloud-pull-1password",
316 feature = "multi-pull"
317))]
318struct PullAuthSection {
319 heading: &'static str,
320 body: &'static str,
321}
322
323#[cfg(any(
324 feature = "akv-pull",
325 feature = "cloud-pull-aws",
326 feature = "cloud-pull-gcp",
327 feature = "cloud-pull-vault",
328 feature = "cloud-pull-1password",
329 feature = "multi-pull"
330))]
331static PULL_AUTH_SECTIONS: &[PullAuthSection] = &[
332 #[cfg(feature = "cloud-pull-aws")]
333 PullAuthSection {
334 heading: "AWS SECRETS MANAGER (tsafe aws-pull)",
335 body: r#" Static credentials (local dev):
336 export AWS_ACCESS_KEY_ID=AKIA...
337 export AWS_SECRET_ACCESS_KEY=...
338 export AWS_DEFAULT_REGION=us-east-1
339
340 Temporary credentials (assumed role / SSO):
341 eval $(aws sts assume-role ... --query Credentials --output text | ...)
342 # or: aws configure sso, then run tsafe — SDK picks up the profile
343
344 EC2 / ECS / Lambda — nothing to set; IMDSv2 / ECS task role used automatically."#,
345 },
346 #[cfg(feature = "cloud-pull-aws")]
347 PullAuthSection {
348 heading: "AWS SSM PARAMETER STORE (tsafe ssm-pull)",
349 body: r#" Same credentials as Secrets Manager. The IAM policy needs:
350 ssm:GetParametersByPath on arn:aws:ssm:REGION:ACCOUNT:parameter/PATH*
351 kms:Decrypt on the KMS key for SecureString parameters"#,
352 },
353 #[cfg(feature = "cloud-pull-gcp")]
354 PullAuthSection {
355 heading: "GCP SECRET MANAGER (tsafe gcp-pull)",
356 body: r#" Local dev (gcloud CLI):
357 gcloud auth application-default login
358 export GOOGLE_CLOUD_PROJECT=my-project
359
360 CI / short-lived token:
361 export GOOGLE_OAUTH_TOKEN=$(gcloud auth print-access-token)
362 export GOOGLE_CLOUD_PROJECT=my-project
363
364 GKE / Cloud Run / GCE — metadata server used automatically.
365 Required IAM role: roles/secretmanager.secretAccessor on the project."#,
366 },
367 #[cfg(feature = "akv-pull")]
368 PullAuthSection {
369 heading: "AZURE KEY VAULT (tsafe kv-pull)",
370 body: r#" Service principal (CI):
371 export AZURE_TENANT_ID=...
372 export AZURE_CLIENT_ID=...
373 export AZURE_CLIENT_SECRET=...
374 export TSAFE_AKV_URL=https://myvault.vault.azure.net
375
376 Azure VM / ACI — IMDS managed identity used automatically.
377 Required role: Key Vault Secrets User on the vault."#,
378 },
379 #[cfg(feature = "cloud-pull-vault")]
380 PullAuthSection {
381 heading: "HASHICORP VAULT (tsafe vault-pull)",
382 body: r#" export VAULT_TOKEN=hvs....
383 export TSAFE_HCP_URL=http://vault:8200 # or use --addr"#,
384 },
385 #[cfg(feature = "cloud-pull-1password")]
386 PullAuthSection {
387 heading: "1PASSWORD (tsafe op-pull)",
388 body: r#" Install the op CLI and authenticate:
389 op signin
390 tsafe op-pull "Database Credentials""#,
391 },
392];
393
394#[cfg(any(
395 feature = "akv-pull",
396 feature = "cloud-pull-aws",
397 feature = "cloud-pull-gcp",
398 feature = "cloud-pull-vault",
399 feature = "cloud-pull-1password",
400 feature = "multi-pull"
401))]
402fn has_non_azure_pull_family() -> bool {
403 cfg!(feature = "cloud-pull-aws")
404 || cfg!(feature = "cloud-pull-gcp")
405 || cfg!(feature = "cloud-pull-vault")
406 || cfg!(feature = "cloud-pull-1password")
407 || cfg!(feature = "multi-pull")
408}
409
410#[cfg(any(
411 feature = "akv-pull",
412 feature = "cloud-pull-aws",
413 feature = "cloud-pull-gcp",
414 feature = "cloud-pull-vault",
415 feature = "cloud-pull-1password",
416 feature = "multi-pull"
417))]
418fn is_akv_only_pull_build() -> bool {
419 cfg!(feature = "akv-pull") && !has_non_azure_pull_family()
420}
421
422fn print_compiled_truth() {
423 println!("\nCompiled truth for this binary:\n");
424
425 #[cfg(feature = "agent")]
426 println!(
427 " - `agent` is a core/default workflow here, but the background `tsafe-agent` runtime is still a separate companion binary."
428 );
429 #[cfg(not(feature = "agent"))]
430 println!(" - `agent` help is omitted because this build does not compile the core `agent` workflow.");
431
432 #[cfg(any(
433 feature = "akv-pull",
434 feature = "cloud-pull-aws",
435 feature = "cloud-pull-gcp",
436 feature = "cloud-pull-vault",
437 feature = "cloud-pull-1password",
438 feature = "multi-pull"
439 ))]
440 {
441 if is_akv_only_pull_build() {
442 println!(
443 " - Cloud pull coverage is Azure Key Vault only (`tsafe kv-pull`); AWS/GCP/Vault/1Password pulls and multi-source `tsafe pull` are not compiled in."
444 );
445 } else {
446 println!(
447 " - Cloud pull topics describe only the providers and orchestration commands compiled into this binary."
448 );
449 }
450 }
451 #[cfg(not(any(
452 feature = "akv-pull",
453 feature = "cloud-pull-aws",
454 feature = "cloud-pull-gcp",
455 feature = "cloud-pull-vault",
456 feature = "cloud-pull-1password",
457 feature = "multi-pull"
458 )))]
459 println!(
460 " - Cloud pull help is omitted because this build has no pull providers compiled in."
461 );
462
463 #[cfg(not(feature = "nativehost"))]
464 println!(
465 " - Browser/native-host flows are not compiled into this `tsafe` binary; they require browser/nativehost-enabled builds plus the separate `tsafe-nativehost` artifact."
466 );
467 #[cfg(not(feature = "ssh"))]
468 println!(" - SSH helper commands (`tsafe ssh-add`, `tsafe ssh-import`) are not compiled into this build.");
469 #[cfg(not(feature = "plugins"))]
470 println!(" - The plugin command surface (`tsafe plugin`) is not compiled into this build.");
471 #[cfg(not(feature = "git-helpers"))]
472 println!(
473 " - Git helper flows such as `tsafe credential-helper` are not compiled into this build."
474 );
475}
476
477pub fn run(topic: Option<ExplainTopic>) {
479 match topic {
480 None => {
481 println!("Usage: tsafe explain <topic>\n");
482 println!("Topics:\n");
483 for t in ALL_TOPICS {
484 println!(
485 " {:<16} [{:<13}] {}",
486 t.as_str(),
487 surface_label(t.surface()),
488 t.blurb()
489 );
490 }
491 println!("\nExample: tsafe explain exec");
492 print_compiled_truth();
493 }
494 Some(ExplainTopic::Exec) => print_exec(),
495 Some(ExplainTopic::Export) => print_export(),
496 Some(ExplainTopic::Namespaces) => print_namespaces(),
497 #[cfg(feature = "agent")]
498 Some(ExplainTopic::Agent) => print_agent(),
499 Some(ExplainTopic::ExecSecurity) => print_exec_security(),
500 Some(ExplainTopic::Contracts) => print_contracts(),
501 Some(ExplainTopic::VaultRecovery) => print_vault_recovery(),
502 #[cfg(any(
503 feature = "akv-pull",
504 feature = "cloud-pull-aws",
505 feature = "cloud-pull-gcp",
506 feature = "cloud-pull-vault",
507 feature = "cloud-pull-1password",
508 feature = "multi-pull"
509 ))]
510 Some(ExplainTopic::Pull) => print_pull(),
511 #[cfg(any(
512 feature = "akv-pull",
513 feature = "cloud-pull-aws",
514 feature = "cloud-pull-gcp",
515 feature = "cloud-pull-vault",
516 feature = "cloud-pull-1password",
517 feature = "multi-pull"
518 ))]
519 Some(ExplainTopic::PullAuth) => print_pull_auth(),
520 #[cfg(any(
521 feature = "akv-pull",
522 feature = "cloud-pull-aws",
523 feature = "cloud-pull-gcp",
524 feature = "cloud-pull-vault",
525 feature = "cloud-pull-1password",
526 feature = "multi-pull"
527 ))]
528 Some(ExplainTopic::PullReliability) => print_pull_reliability(),
529 }
530}
531
532fn print_exec() {
533 print!(
534 r#"
535exec — run a program with vault secrets as normal environment variables
536━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
537
538This is the default way to use tsafe day-to-day. Your app keeps reading
539process.env / %ENV% like every tutorial; you do not need a plaintext .env
540on disk for that run.
541
542 tsafe exec -- <command> [args...]
543
544Default scope
545 Without --contract / --keys / --ns, exec injects every non-alias secret
546 in the active vault into the child environment. Parent ambient credentials
547 are still stripped via the SENSITIVE_VARS strip list and dangerous-name
548 vault entries (LD_PRELOAD, NODE_OPTIONS, ...) still abort the spawn — but
549 the *vault scope* is the whole vault until you adopt a contract or one of
550 the narrowing flags. For least-authority workflows, see `tsafe explain
551 contracts`.
552
553Preview which names would be set (sorted, values never printed):
554
555 tsafe exec --dry-run
556
557Fail before starting if required keys are missing (after --ns mapping):
558
559 tsafe exec --require API_KEY,DB_URL -- npm test
560
561Namespaces — only inject keys under a prefix; child sees names without it:
562
563 tsafe exec --ns myapp -- python app.py
564
565Shell: always put a bare "--" before the real command so flags are not parsed
566by tsafe. Quote carefully so the shell does not expand $VAR before the child.
567
568Layering: if you use direnv, Docker, or CI-injected env, you may have both
569parent env and vault keys. Parent tokens (GITHUB_TOKEN, ADO_PAT, …) are
570stripped from the child before injection so they do not leak accidentally.
571Use `tsafe explain exec-security` for the shipped inheritance presets and
572`--allow-dangerous-env` / `--no-inherit` / `--only` details.
573
574Docs: docs/features/export-and-exec.md · tsafe explain exec-security
575
576"#
577 );
578}
579
580fn print_export() {
581 print!(
582 r#"
583export / get — when not to use exec
584━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
585
586Use `tsafe export` or `tsafe get` when something must read secrets from
587stdout, a file, or the clipboard — not from a subprocess environment.
588
589Examples:
590 · CI: tsafe export --format github-actions >> $GITHUB_ENV
591 · Shell session: eval "$(tsafe export --format dotenv)" (widens blast radius)
592 · One-off: tsafe get API_KEY --copy
593
594Prefer `exec` when the consumer is a single command: no plaintext hop through
595a file or your interactive shell.
596
597"#
598 );
599}
600
601fn print_namespaces() {
602 print!(
603 r#"
604namespaces — multiple logical apps in one vault
605━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
606
607Store keys as "ns/KEY" (e.g. web/DB_URL and batch/DB_URL). List and export
608can filter with --ns.
609
610With exec, --ns injects only matching keys and strips the prefix so the
611child sees plain DB_URL.
612
613 tsafe set web/API_KEY --tag env=dev
614 tsafe exec --ns web -- npm run dev
615
616Bulk copy or rename every key under a prefix (clone or migrate a namespace):
617
618 tsafe ns copy prod staging # prod/* also exists under staging/*
619 tsafe ns move oldapp newapp # oldapp/* → newapp/* (sources removed)
620 tsafe ns copy a b --force # overwrite destination keys if present
621
622Single-key rename still uses: tsafe mv FROM_KEY TO_KEY
623
624"#
625 );
626}
627
628#[cfg(feature = "agent")]
629fn print_agent() {
630 print!(
631 r#"
632agent — fewer password prompts
633━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
634
635`tsafe agent unlock` starts a session (TTL, e.g. 30m). While active, CLI and
636tools that use the agent socket can open the vault without typing the master
637password each time.
638
639 tsafe agent unlock --ttl 30m
640 tsafe exec -- npm test
641 tsafe agent lock
642
643See: tsafe agent --help
644
645"#
646 );
647}
648
649fn print_exec_security() {
650 print!(
651 r#"
652exec-security — what exec does not guarantee
653━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
654
655Inheritance
656 `standard` starts from your current environment, strips sensitive parent
657 variables, then adds vault keys. `hardened` uses a minimal inherited env;
658 `--no-inherit` starts clean; `--only` whitelists explicit parent names.
659 Sensitive parent variables (TSAFE_PASSWORD, cloud tokens, PATs, etc.) are
660 removed before spawn — see SENSITIVE_VARS in crates/tsafe-core/src/env.rs.
661
662High-risk names
663 If a vault key would set NODE_OPTIONS, LD_PRELOAD, or macOS DYLD_* (among
664 others), tsafe aborts exec by default. Use `--allow-dangerous-env` to inject
665 them with a warning instead. `--deny-dangerous-env` is now just an explicit
666 compatibility spelling of the default.
667
668Same-user threat model
669 On Unix, environment is not a hard security boundary against malware or
670 debuggers running as you. exec scopes secrets to one process tree and
671 avoids argv and dotfiles; it does not replace OS-level isolation.
672
673Docs: docs/features/security.md · docs/research/exec-as-product-2026-04.md
674
675"#
676 );
677}
678
679fn print_contracts() {
680 print!(
681 r#"
682contracts — named, reusable exec authority in .tsafe.yml
683━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
684
685A contract replaces ad-hoc flag bundles with a named, auditable authority
686definition committed to your repository. Contracts answer in one command:
687"what exactly am I allowing this process to receive?"
688
689MANIFEST (.tsafe.yml in your project root)
690 contracts:
691 deploy:
692 profile: prod # which vault to open (overridden by -p)
693 namespace: deploy # --ns equivalent
694 allowed_secrets: # injection ceiling — only these keys pass through
695 - ARM_SUBSCRIPTION_ID
696 - TF_BACKEND_KEY
697 required_secrets: # fail before spawn if any are missing
698 - TF_BACKEND_KEY
699 allowed_targets: # which binaries this contract may run
700 - terraform
701 trust_level: hardened # standard | hardened | custom
702
703TRUST LEVELS
704 standard — full inherited env minus strip list; dangerous names denied
705 hardened — minimal env, dangerous names denied, child output redacted
706 custom — contract-defined `inherit`, `deny_dangerous_env`, and
707 `redact_output` settings in the manifest
708
709USAGE
710 tsafe exec --contract deploy -- terraform apply
711
712 Without --contract, --keys, or --ns, exec injects EVERY non-alias secret
713 in the active vault (parent strip and dangerous-name guard still apply).
714 --contract is the recommended primary model and the only way to declare
715 the authorisation set up front; flags (--keys, --ns) are escape hatches.
716
717PREVIEW AUTHORITY (no secrets ever printed)
718 tsafe exec --contract deploy --plan
719
720WHAT GETS INJECTED
721 Intersection of: vault keys ∩ allowed_secrets (if set) ∩ --keys (if set)
722 Union of: required_secrets + --require
723 Omitting allowed_secrets leaves the secret set unrestricted by the contract.
724 Setting allowed_secrets: [] is an explicit no-secret diagnostic contract.
725 If a --keys name is not in allowed_secrets, exec fails with a clear error.
726 When allowed_secrets is set, manifest validation also requires every
727 `required_secret` to appear in `allowed_secrets`.
728
729OVERRIDING CONTRACT VALUES
730 Explicit flags always win over contract values:
731 -p / --profile overrides contract profile
732 --ns overrides contract namespace
733 --keys narrows injection (must be subset of allowed_secrets)
734 --require adds to contract required_secrets
735 --mode / --minimal override trust_level
736 Those overrides can narrow or restyle execution, but they cannot widen the
737 contract ceiling for allowed secrets or allowed targets.
738
739ERROR MESSAGES
740 "command 'X' is not in allowed_targets"
741 → Add X (or a matching basename) to allowed_targets in .tsafe.yml, or run
742 without --contract.
743 "selected key(s) not allowed by contract"
744 → The --keys list includes a name not in allowed_secrets; fix either.
745 "required secret 'X' not found"
746 → tsafe set X <value>
747
748Docs: docs/features/export-and-exec.md (Authority contracts section)
749
750"#
751 );
752}
753
754fn print_vault_recovery() {
755 print!(
756 r#"
757vault-recovery — options when you forget the master password
758━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
759
760tsafe vaults use Argon2id + XChaCha20-Poly1305. There is no backdoor.
761Recovery depends on which of these you set up before forgetting.
762
7631. Quick unlock / biometric (most common — you may not need your password)
764 tsafe biometric status
765 If enabled, the vault opens without a password:
766 tsafe get any-key
767 tsafe unlock # if the vault file is currently locked
768
7692. Main vault bridging
770 If another vault stores this profile's password under profile-passwords/<name>:
771 tsafe -p main get profile-passwords/<this-profile>
772
7733. Agent still running
774 If tsafe agent unlock is active in your session, the password is cached:
775 tsafe agent status
776 tsafe get any-key # will use agent if socket is alive
777
7784. TSAFE_PASSWORD env var
779 If you set it in your shell or CI environment, read it from there.
780 (Remove it from dotfiles, scripts, and CI secrets after recovery.)
781
7825. Password manager
783 Check if you stored the vault password during tsafe init.
784
785If none of the above work, the vault is not recoverable. Encrypted data
786cannot be decrypted without the key. To start over:
787 tsafe -p <profile> init # creates a new empty vault under that profile
788 re-pull or re-import from any source compiled into this build if configured
789
790"#
791 );
792}
793
794#[cfg(any(
795 feature = "akv-pull",
796 feature = "cloud-pull-aws",
797 feature = "cloud-pull-gcp",
798 feature = "cloud-pull-vault",
799 feature = "cloud-pull-1password",
800 feature = "multi-pull"
801))]
802fn print_pull() {
803 if is_akv_only_pull_build() {
804 print!(
805 r#"
806pull — sync secrets from Azure Key Vault into the local vault
807━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
808
809This build ships the core/default Azure Key Vault pull lane only. tsafe still
810acts as a local-first runtime authority: Azure is a read-only source, secrets
811are synced into the local vault, and `tsafe exec` scopes them to one process.
812Nothing is written back to Azure.
813
814Additional provider pulls (AWS/GCP/Vault/1Password) and multi-source `tsafe pull`
815orchestration are gated non-core surfaces and are not compiled into this binary.
816
817AVAILABLE SOURCES IN THIS BUILD
818
819"#
820 );
821 } else {
822 print!(
823 r#"
824pull — sync secrets from cloud providers into the local vault
825━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
826
827tsafe acts as a local-first runtime authority. Cloud providers are read-only
828sources — secrets are synced in, then tsafe exec scopes them to one process.
829Nothing is written back to the cloud.
830
831AVAILABLE SOURCES IN THIS BUILD
832
833"#
834 );
835 }
836 for source in PULL_SOURCES {
837 println!(" {:<26} {}", source.label, source.command);
838 }
839 if cfg!(feature = "multi-pull") {
840 print!(
841 r#"
842
843ONE COMMAND FOR ALL SOURCES (.tsafe.yml)
844
845 Define all your sources in .tsafe.yml at the repo root:
846
847 pulls:
848 - source: aws
849 region: us-east-1
850 prefix: myapp/
851 - source: gcp
852 project: my-project
853 - source: akv
854 vault_url: https://myvault.vault.azure.net
855
856 Then pull everything with one command:
857
858 tsafe pull
859
860KEY NORMALISATION (all sources)
861
862 Cloud names → Local key
863 my-secret → MY_SECRET
864 myapp/db-password → MYAPP_DB_PASSWORD (AWS / SSM path)
865 /prod/myapp/api-key → PROD_MYAPP_API_KEY (SSM absolute path)
866 db.password → DB_PASSWORD (GCP dot separator)
867
868OVERWRITE BEHAVIOUR
869
870 By default, existing local secrets are not overwritten:
871 tsafe pull # safe: new keys only
872 tsafe pull --overwrite # replace all matching keys
873
874AFTER PULLING
875
876 Preview what would be injected (no secret values printed):
877 tsafe exec --dry-run
878
879 Run your app:
880 tsafe exec -- npm start
881
882Credential setup: tsafe explain pull-auth
883
884"#
885 );
886 } else {
887 print!(
888 r#"
889
890KEY NORMALISATION
891
892 Provider names are normalised into local vault keys before storage.
893
894OVERWRITE BEHAVIOUR
895
896 By default, existing local secrets are not overwritten:
897 tsafe kv-pull # safe: new keys only
898 tsafe kv-pull --overwrite # replace all matching keys
899
900AFTER PULLING
901
902 Preview what would be injected (no secret values printed):
903 tsafe exec --dry-run
904
905 Run your app:
906 tsafe exec -- npm start
907
908Credential setup: tsafe explain pull-auth
909
910This page is intentionally build-scoped: if you need AWS/GCP/Vault/1Password
911or multi-source `tsafe pull`, use a binary compiled with those gated features.
912
913"#
914 );
915 }
916}
917
918#[cfg(any(
919 feature = "akv-pull",
920 feature = "cloud-pull-aws",
921 feature = "cloud-pull-gcp",
922 feature = "cloud-pull-vault",
923 feature = "cloud-pull-1password",
924 feature = "multi-pull"
925))]
926fn print_pull_auth() {
927 if is_akv_only_pull_build() {
928 print!(
929 r#"
930pull-auth — credential setup for compiled-in pull sources
931━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
932
933This build ships Azure Key Vault pull only. The provider sections below are
934limited to commands that exist in this binary; broader cloud pulls are gated
935non-core surfaces and are omitted here.
936"#
937 );
938 } else {
939 print!(
940 r#"
941pull-auth — credential setup for cloud pull sources
942━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
943
944Each provider has its own credential model. Set the env vars before running
945the pull command compiled into this build.
946"#
947 );
948 }
949 for section in PULL_AUTH_SECTIONS {
950 println!("\n{}\n{}", section.heading, section.body);
951 }
952 if cfg!(feature = "multi-pull") {
953 print!(
954 r#"
955
956TESTING CREDENTIALS
957
958 Dry-run your pull config without writing to the vault:
959 tsafe pull --dry-run
960
961 Check what is in the vault after pulling:
962 tsafe list
963
964"#
965 );
966 } else {
967 print!(
968 r#"
969
970TESTING CREDENTIALS
971
972 Run the provider-specific pull command for this build, then inspect:
973 tsafe list
974
975"#
976 );
977 }
978}
979
980#[cfg(any(
981 feature = "akv-pull",
982 feature = "cloud-pull-aws",
983 feature = "cloud-pull-gcp",
984 feature = "cloud-pull-vault",
985 feature = "cloud-pull-1password",
986 feature = "multi-pull"
987))]
988fn print_pull_reliability() {
989 print!(
990 r#"
991pull-reliability — retries, transient errors, and continuation behavior
992━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
993
994PULL RETRIES
995
996Cloud pull clients retry two classes of failures:
997 · HTTP 429 (throttling): up to 3 retries, honors Retry-After header when present
998 · transient transport failures: up to 5 retries with jittered exponential backoff
999
1000Transient transport examples:
1001 timeout, connection refused, connection reset, temporary network faults
1002
1003JITTERED BACKOFF
1004
1005Retry delay uses exponential backoff with +0..25% jitter to reduce synchronized
1006retry storms when many jobs pull at once.
1007
1008PULL COMMAND ERROR MODES
1009
1010All pull commands now accept:
1011 --on-error fail-all # default, abort on first provider/source error
1012 --on-error skip-failed # continue remaining sources, skip failed source
1013 --on-error warn-only # continue and warn; useful for best-effort sync
1014
1015Examples:
1016 "#
1017 );
1018 #[cfg(feature = "akv-pull")]
1019 println!(" tsafe kv-pull --on-error warn-only");
1020 #[cfg(feature = "cloud-pull-aws")]
1021 println!(" tsafe aws-pull --on-error warn-only");
1022 #[cfg(feature = "cloud-pull-gcp")]
1023 println!(" tsafe gcp-pull --on-error skip-failed");
1024 #[cfg(feature = "multi-pull")]
1025 println!(" tsafe pull --config .tsafe.yml --on-error skip-failed");
1026 if cfg!(feature = "multi-pull") {
1027 print!(
1028 r#"
1029
1030MULTI-SOURCE BEHAVIOR (`tsafe pull`)
1031
1032With skip-failed/warn-only, a failed source does not stop later sources.
1033The command prints per-source failures and a final failure count.
1034"#
1035 );
1036 }
1037 print!(
1038 r#"
1039
1040OPERATOR GUIDANCE
1041
1042Use fail-all for CI gates that require every source to succeed.
1043Use skip-failed for mixed environments where some sources are optional.
1044Use warn-only for local/dev best-effort hydration.
1045
1046"#
1047 );
1048 if is_akv_only_pull_build() {
1049 print!(
1050 r#"
1051This binary only exposes the Azure Key Vault pull lane. Multi-source continuation
1052semantics for `tsafe pull` are a gated non-core surface and are not available here.
1053
1054"#
1055 );
1056 }
1057}