Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .claude/skills/swamp-extension-model/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,26 @@ resources: {
**Spec naming:** Resource spec keys must not contain hyphens (`-`). Use
camelCase or single words (e.g., `igw` not `internet-gateway`).

**Sensitive fields:** Mark fields containing secrets with
`z.meta({ sensitive: true })`. Values are stored in a vault and replaced with
vault references before persistence:

```typescript
resources: {
"keypair": {
schema: z.object({
keyId: z.string(),
keyMaterial: z.string().meta({ sensitive: true }),
}),
lifetime: "infinite",
garbageCollection: 10,
},
},
```

Set `sensitiveOutput: true` on the spec to treat all fields as sensitive. Set
`vaultName` on the spec to override which vault stores the values.

**Schema requirement:** If your resource will be referenced by other models via
CEL expressions, declare the referenced properties explicitly in the Zod schema:

Expand Down
35 changes: 35 additions & 0 deletions .claude/skills/swamp-vault/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,41 @@ prodToken: ${{ vault.get(prod-secrets, auth-token) }}
devToken: ${{ vault.get(dev-secrets, auth-token) }}
```

## Automatic Sensitive Field Storage

Model output schemas can mark fields as sensitive. When a method executes,
sensitive values are stored in a vault and replaced with vault references before
persistence — no manual `vault put` needed.

```typescript
// In an extension model's resource spec
resources: {
"keypair": {
schema: z.object({
keyId: z.string(),
keyMaterial: z.string().meta({ sensitive: true }),
}),
lifetime: "infinite",
garbageCollection: 10,
},
},
```

After execution, persisted data contains
`${{ vault.get('vault-name', 'auto-key') }}` instead of the plaintext secret.
The actual value is stored in the vault.

**Options:**

- `z.meta({ sensitive: true })` — mark individual fields
- `sensitiveOutput: true` on the spec — treat all fields as sensitive
- `vaultName` on the spec or field metadata — override which vault stores values
- `vaultKey` on field metadata — override the auto-generated vault key

A vault must be configured or an error is thrown at write time.

See the **swamp-extension-model** skill for full schema examples.

## Security Best Practices

1. **Environment separation**: Use different vaults for dev/staging/prod
Expand Down
135 changes: 92 additions & 43 deletions design/vaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,67 +104,116 @@ The expression syntax is:
- `key` - The secret identifier within that vault
- `value` - The value to store (for put operations)

## Sensitive Field Marking
## Sensitive Field Marking (Implemented)

Model schemas can mark fields as sensitive using Zod's `.meta()` method:
Model schemas mark fields as sensitive using Zod's `.meta()` method. When a
method executes, sensitive output fields are automatically stored in a vault and
replaced with vault reference expressions before persistence.

### Schema Metadata

Mark individual fields as sensitive in a resource output spec schema:

```typescript
// Input schema with sensitive field
export const ApiKeyInputAttributesSchema = z.object({
serviceName: z.string().min(1),
keyData: z.string().meta({
description: "Private key data for API authentication",
sensitive: true,
vault: true, // Indicates this should come from vault
}),
});

// Data/Resource schema with sensitive output
export const ApiKeyDataAttributesSchema = z.object({
serviceName: z.string(),
apiKey: z.string().meta({
description: "Generated API key",
sensitive: true,
vault: true, // Indicates this should be stored in vault
vaultKey: "generated-api-key", // Optional: specify vault key name
}),
keyId: z.string(),
createdAt: z.string().datetime(),
});
resources: {
result: {
schema: z.object({
keyId: z.string(),
keyMaterial: z.string().meta({ sensitive: true }),
publicKey: z.string(),
}),
lifetime: "infinite",
garbageCollection: 10,
},
},
```

### Sensitive Field Metadata
Supported metadata properties on `.meta()`:

Fields marked as sensitive support these metadata properties:
- `sensitive: boolean` - Marks the field as containing sensitive data (required)
- `vaultKey?: string` - Custom vault key (defaults to auto-generated path)
- `vaultName?: string` - Specific vault to use (overrides spec/default vault)

- `sensitive: boolean` - Marks the field as containing sensitive data
- `vault: boolean` - Indicates the field should interact with vault storage
- `vaultKey?: string` - Optional custom key name for vault storage (defaults to
field name)
- `vaultName?: string` - Optional specific vault to use (defaults to repository
default)
### Spec-Level `sensitiveOutput`

### Automatic Vault Integration
When an entire resource output is sensitive, set `sensitiveOutput: true` on the
`ResourceOutputSpec` instead of marking each field individually:

When a field is marked with `sensitive: true` and `vault: true`:
```typescript
resources: {
result: {
schema: z.object({ ... }),
lifetime: "infinite",
garbageCollection: 10,
sensitiveOutput: true, // All fields treated as sensitive
vaultName: "my-vault", // Optional: override vault for this spec
},
},
```

**Input Fields**: The swamp runtime automatically resolves vault expressions:
### Vault Key Naming

```yaml
# User writes this
keyData: ${{ vault.get(aws, machineKeyData) }}
Auto-generated vault keys are built from the model type, ID, method name, and
field path, then sanitized to replace characters that are invalid in vault secret
keys (`@` is removed, `/` and `\` are replaced with `-`):

# Runtime validates against schema and retrieves from vault
```
{sanitized modelType}-{modelId}-{methodName}-{fieldPath}
```

**Output Fields**: The swamp runtime automatically stores sensitive output:
For example: `@user/aws/ec2-keypair` with field `KeyMaterial` becomes
`user-aws-ec2-keypair-abc-123-createKeyPair-KeyMaterial`

Custom keys can be specified via `vaultKey` in field metadata:

```typescript
// After method execution, sensitive fields are automatically stored
// Field: apiKey with meta { sensitive: true, vault: true, vaultKey: "generated-api-key" }
// Result: vault.put(default-vault, "generated-api-key", resultData.attributes.apiKey)
apiKey: z.string().meta({ sensitive: true, vaultKey: "my-api-key" }),
```

### Vault Reference Format

Sensitive values are replaced with CEL-compatible vault reference expressions
using single-quoted string arguments:

```
${{ vault.get('vault-name', 'vault-key') }}
```

### Vault Resolution Order

The vault used for storing a sensitive field is resolved in this order:

1. Field-level `vaultName` from `.meta()` metadata
2. Spec-level `vaultName` from `ResourceOutputSpec`
3. First available vault from `VaultService`

### Processing Behavior

- Values are **snapshotted** before processing to prevent cross-contamination
when multiple fields are sensitive
- Non-string values are JSON-stringified before vault storage
- Fields with `null` or `undefined` values are skipped
- If sensitive fields exist but no vault is configured, an error is thrown with
guidance to create a vault
- Processing is injected inside `createResourceWriter()` before JSON
serialization, so it applies transparently to all resource writes

### Implementation

Processing is handled by `processSensitiveResourceData()` in
`src/domain/models/data_writer.ts`. Schema introspection is performed by
`extractSensitiveFields()` in `src/domain/models/sensitive_field_extractor.ts`.

### Input Fields

Input fields use vault expressions directly in YAML:

```yaml
keyData: ${{ vault.get('aws', 'machineKeyData') }}
```

The expression evaluation system resolves these at runtime.

## AWS Secrets Manager Provider

The AWS Secrets Manager provider is the initial implementation supporting:
Expand Down
Loading
Loading