tsafe-core 1.2.0

Core runtime engine for tsafe โ€” encrypted credential storage, process injection contracts, audit log, RBAC
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Attestation contract โ€” algol-merged, env-injection-shaped policy.
//!
//! # Provenance
//!
//! Lifted from `algol/src/model.rs` @ `6956cfd347cd8ce492231ba5aaa4952227d72689`
//! (commit `6956cfd`, branch `master`, repo `0ryant/algol`).
//!
//! Re-licensed `AGPL-3.0-or-later` per:
//!
//! - `ecosystem-catalog/docs/adr/draft-algol-into-tsafe-merge.md`
//! - `ecosystem-catalog/portfolio-algol-tsafe-migration-2026-05-21.md`
//! - `ecosystem-catalog/portfolio-algol-tsafe-phase0-audit-2026-05-21.md`
//! - operator decision 2026-05-21
//!
//! # Why `AttestContract` is distinct from `AuthorityContract`
//!
//! `tsafe-core::contracts::AuthorityContract` (vault-policy semantics) and
//! algol's original `AuthorityContract` (env-injection semantics) share a
//! name but have **zero field overlap**. Per ec Phase 0 audit ยง1.5.1:
//!
//! | tsafe `AuthorityContract` fields | algol's contract fields |
//! |---|---|
//! | name, profile, namespace | repo_path, repo_commit |
//! | access_profile, allow_all_secrets | created_at, created_by, command |
//! | allowed_secrets, required_secrets | policy (default_env, allow_safe_baseline, redaction) |
//! | allowed_targets, trust, network | allowed_env, denied_env, safe_baseline_env, source_scan |
//!
//! The two types represent different abstractions:
//!
//! - `AuthorityContract` (tsafe) is a **named, vault-backed policy selector**
//!   describing which profile to resolve from, which secrets are allowed,
//!   and which trust posture applies.
//! - `AttestContract` (this module) is a **per-run env-injection contract**
//!   tied to a specific repo + commit + command, describing which env
//!   variables to inject from which sources and which to deny.
//!
//! Field-merging the two would be wrong; they need to coexist. This module
//! is the algol-merged side of that boundary.
//!
//! # Phase 4 wire-format changes (ec ADR-0003 + schema rename)
//!
//! - Schema: `algol.contract.v1` -> `tsafe.contract.v1`. Both accepted on
//!   parse during the v1.x compat window.
//! - Redaction algorithm: `sha256` is still accepted but `blake3` is the
//!   new canonical value. Either is valid during compat.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

/// Schema version for the `AttestContract` artifact (current canonical).
pub const ATTEST_CONTRACT_SCHEMA: &str = "tsafe.contract.v1";

/// Legacy schema name accepted during the v1.x compat window.
pub const LEGACY_ATTEST_CONTRACT_SCHEMA: &str = "algol.contract.v1";

/// Canonical redaction algorithm name (Phase 4 BLAKE3 convergence).
pub const REDACTION_BLAKE3: &str = "blake3";

/// Legacy redaction algorithm name accepted during the v1.x compat window.
pub const REDACTION_SHA256_LEGACY: &str = "sha256";

/// Policy block governing the contract's overall stance.
///
/// `default_env = "deny"` is the only currently supported value
/// (env injection is opt-in per name; everything else is denied).
/// `redaction` accepts either `blake3` (Phase 4 canonical) or `sha256`
/// (legacy, compat window).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestContractPolicy {
    pub default_env: String,
    pub allow_safe_baseline: bool,
    pub redaction: String,
}

/// A single env variable explicitly allowed by the contract.
///
/// `source` is a URI describing where the value comes from. MVP supports
/// `literal://demo/<name>` and `env://<name>`; future sources include
/// vault references and external secret managers.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestAllowedEnv {
    pub name: String,
    pub source: String,
    pub required: bool,
    pub reason: String,
}

/// A single env variable explicitly denied by the contract.
///
/// Denied entries are stripped from the parent env before the child
/// process starts. The `reason` is recorded so operators can audit why.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestDeniedEnv {
    pub name: String,
    pub reason: String,
}

/// Per-run env-injection contract for an attested command execution.
///
/// `AttestContract` is the input to the attestation pipeline: a frozen
/// description of which env variables to inject (from which sources) and
/// which to deny, scoped to a single repo + commit + command tuple.
///
/// It is **distinct** from [`crate::contracts::AuthorityContract`] โ€” see
/// module docs for the rationale.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttestContract {
    pub schema: String,
    pub repo_path: String,
    pub repo_commit: Option<String>,
    pub created_at: DateTime<Utc>,
    pub created_by: String,
    pub command: Vec<String>,
    pub policy: AttestContractPolicy,
    pub allowed_env: Vec<AttestAllowedEnv>,
    pub denied_env: Vec<AttestDeniedEnv>,
    pub safe_baseline_env: Vec<String>,
    pub source_scan: Option<String>,
}

impl AttestContract {
    /// Return all validation errors for this artifact.
    pub fn validation_errors(&self) -> Vec<String> {
        let mut errors = Vec::new();

        if !is_supported_contract_schema(&self.schema) {
            errors.push(format!("unsupported schema {}", self.schema));
        }
        if self.repo_path.trim().is_empty() {
            errors.push("repo_path must not be empty".to_string());
        }
        if self.command.is_empty() || self.command.iter().any(|part| part.trim().is_empty()) {
            errors.push("command must contain non-empty argv entries".to_string());
        }
        if self.policy.default_env != "deny" {
            errors.push(format!(
                "unsupported policy.default_env {}; MVP requires deny",
                self.policy.default_env
            ));
        }
        if self.policy.redaction != REDACTION_BLAKE3
            && self.policy.redaction != REDACTION_SHA256_LEGACY
        {
            errors.push(format!(
                "unsupported policy.redaction {}; expected blake3 (or sha256 during compat window)",
                self.policy.redaction
            ));
        }

        validate_allowed_env(&self.allowed_env, &mut errors);
        validate_denied_env(&self.denied_env, &mut errors);
        validate_safe_baseline(&self.safe_baseline_env, &mut errors);

        errors
    }

    /// Convert the validation-error list into a `Result`.
    pub fn ensure_valid(&self) -> Result<(), String> {
        let errors = self.validation_errors();
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors.join("; "))
        }
    }
}

/// Test whether `schema` is one of the supported contract schema names.
pub fn is_supported_contract_schema(schema: &str) -> bool {
    schema == ATTEST_CONTRACT_SCHEMA || schema == LEGACY_ATTEST_CONTRACT_SCHEMA
}

/// Test whether an `allowed_env[].source` URI is one of the supported
/// MVP schemes.
///
/// MVP supports `literal://demo/*` (test fixtures) and `env://*`
/// (parent-env passthrough). Vault and external-secret-manager URIs are
/// in scope for later phases.
pub fn is_supported_source_uri(source: &str) -> bool {
    source.starts_with("literal://demo/")
        || source
            .strip_prefix("env://")
            .is_some_and(|name| !name.trim().is_empty())
}

fn validate_allowed_env(allowed_env: &[AttestAllowedEnv], errors: &mut Vec<String>) {
    let mut names = BTreeSet::new();
    for item in allowed_env {
        if item.name.trim().is_empty() {
            errors.push("allowed_env name must not be empty".to_string());
        }
        if !names.insert(item.name.clone()) {
            errors.push(format!("duplicate allowed_env {}", item.name));
        }
        if !is_supported_source_uri(&item.source) {
            errors.push(format!(
                "unsupported source for {}: {}; MVP supports literal://demo/* and env://*",
                item.name, item.source
            ));
        }
        if item.reason.trim().is_empty() {
            errors.push(format!(
                "allowed_env {} reason must not be empty",
                item.name
            ));
        }
    }
}

fn validate_denied_env(denied_env: &[AttestDeniedEnv], errors: &mut Vec<String>) {
    let mut names = BTreeSet::new();
    for item in denied_env {
        if item.name.trim().is_empty() {
            errors.push("denied_env name must not be empty".to_string());
        }
        if !names.insert(item.name.clone()) {
            errors.push(format!("duplicate denied_env {}", item.name));
        }
        if item.reason.trim().is_empty() {
            errors.push(format!("denied_env {} reason must not be empty", item.name));
        }
    }
}

fn validate_safe_baseline(safe_baseline_env: &[String], errors: &mut Vec<String>) {
    let mut names = BTreeSet::new();
    for name in safe_baseline_env {
        if name.trim().is_empty() {
            errors.push("safe_baseline_env name must not be empty".to_string());
        }
        if !names.insert(name.clone()) {
            errors.push(format!("duplicate safe_baseline_env {name}"));
        }
        if is_sensitive_baseline_name(name) {
            errors.push(format!(
                "safe_baseline_env {name} is sensitive and cannot be baseline-inherited"
            ));
        }
    }
}

fn is_sensitive_baseline_name(name: &str) -> bool {
    let upper = name.to_ascii_uppercase();
    upper.contains("SECRET")
        || upper.contains("TOKEN")
        || upper.contains("CREDENTIAL")
        || upper.contains("CREDS")
        || upper.contains("PASSWORD")
        || upper.contains("PASSWD")
        || upper.contains("PRIVATE_KEY")
        || upper.ends_with("_KEY")
        || upper.ends_with("_PWD")
}

#[cfg(test)]
mod tests {
    use super::*;

    fn valid_contract() -> AttestContract {
        AttestContract {
            schema: ATTEST_CONTRACT_SCHEMA.to_string(),
            repo_path: ".".to_string(),
            repo_commit: None,
            created_at: Utc::now(),
            created_by: "model-test".to_string(),
            command: vec!["true".to_string()],
            policy: AttestContractPolicy {
                default_env: "deny".to_string(),
                allow_safe_baseline: true,
                redaction: REDACTION_BLAKE3.to_string(),
            },
            allowed_env: vec![AttestAllowedEnv {
                name: "DATABASE_URL".to_string(),
                source: "literal://demo/DATABASE_URL".to_string(),
                required: true,
                reason: "test".to_string(),
            }],
            denied_env: vec![AttestDeniedEnv {
                name: "AWS_SECRET_ACCESS_KEY".to_string(),
                reason: "test".to_string(),
            }],
            safe_baseline_env: vec!["PATH".to_string()],
            source_scan: None,
        }
    }

    #[test]
    fn contract_rejects_empty_and_blank_command_entries() {
        let mut empty = valid_contract();
        empty.command.clear();
        assert!(empty
            .validation_errors()
            .iter()
            .any(|error| error.contains("command must contain")));

        let mut blank = valid_contract();
        blank.command = vec!["true".to_string(), " ".to_string()];
        assert!(blank
            .validation_errors()
            .iter()
            .any(|error| error.contains("command must contain")));
    }

    #[test]
    fn contract_rejects_invalid_denied_env_entries() {
        let mut contract = valid_contract();
        contract.denied_env = vec![
            AttestDeniedEnv {
                name: "".to_string(),
                reason: "test".to_string(),
            },
            AttestDeniedEnv {
                name: "GH_TOKEN".to_string(),
                reason: "".to_string(),
            },
            AttestDeniedEnv {
                name: "GH_TOKEN".to_string(),
                reason: "duplicate".to_string(),
            },
        ];

        let errors = contract.validation_errors().join("; ");

        assert!(errors.contains("denied_env name must not be empty"));
        assert!(errors.contains("denied_env GH_TOKEN reason must not be empty"));
        assert!(errors.contains("duplicate denied_env GH_TOKEN"));
    }

    #[test]
    fn contract_rejects_each_sensitive_baseline_name_pattern() {
        for name in [
            "APP_SECRET",
            "API_TOKEN",
            "DATABASE_PASSWORD",
            "DB_PASSWD",
            "SSH_PRIVATE_KEY",
            "GOOGLE_APPLICATION_CREDENTIALS",
            "GOOGLE_GHA_CREDS_PATH",
            "AWS_ACCESS_KEY",
            "DB_PWD",
        ] {
            let mut contract = valid_contract();
            contract.safe_baseline_env = vec![name.to_string()];

            let errors = contract.validation_errors().join("; ");

            assert!(
                errors.contains("is sensitive and cannot be baseline-inherited"),
                "{name} was not rejected: {errors}"
            );
        }
    }

    #[test]
    fn contract_rejects_unsupported_source_uri() {
        let mut contract = valid_contract();
        contract.allowed_env = vec![AttestAllowedEnv {
            name: "DATABASE_URL".to_string(),
            source: "https://example.com/secrets/db".to_string(),
            required: true,
            reason: "test".to_string(),
        }];
        let errors = contract.validation_errors().join("; ");
        assert!(errors.contains("unsupported source for DATABASE_URL"));
    }

    #[test]
    fn contract_rejects_unsupported_default_env_policy() {
        let mut contract = valid_contract();
        contract.policy.default_env = "allow".to_string();
        let errors = contract.validation_errors().join("; ");
        assert!(errors.contains("unsupported policy.default_env allow"));
    }

    #[test]
    fn contract_accepts_a_well_formed_artifact() {
        let contract = valid_contract();
        assert!(
            contract.ensure_valid().is_ok(),
            "valid_contract() should pass validation: {:?}",
            contract.validation_errors()
        );
    }

    #[test]
    fn contract_accepts_legacy_schema_and_redaction_during_compat() {
        let mut contract = valid_contract();
        contract.schema = LEGACY_ATTEST_CONTRACT_SCHEMA.to_string();
        contract.policy.redaction = REDACTION_SHA256_LEGACY.to_string();
        assert!(
            contract.ensure_valid().is_ok(),
            "legacy schema + sha256 redaction must remain valid: {:?}",
            contract.validation_errors()
        );
    }

    #[test]
    fn is_supported_source_uri_accepts_mvp_schemes_and_rejects_others() {
        assert!(is_supported_source_uri("literal://demo/FOO"));
        assert!(is_supported_source_uri("env://FOO"));
        assert!(!is_supported_source_uri("env://"));
        assert!(!is_supported_source_uri("env:// "));
        assert!(!is_supported_source_uri("https://example.com"));
        assert!(!is_supported_source_uri("vault://path"));
    }
}