# Parameter Delivery Methods

**Last Updated**: 2025-02-05  
**Status**: Active Feature

---

## Overview

Attune provides secure parameter passing for actions with two delivery methods: **stdin** (default) and **file** (for large payloads). This document describes parameter delivery, formats, and best practices.

**Key Design Principle**: Action parameters and environment variables are completely separate:
- **Parameters** - Data the action operates on (always secure: stdin or file)
- **Environment Variables** - Execution context/configuration (set as env vars, stored in `execution.env_vars`)

---

## Security by Design

### Parameters Are Always Secure

Action parameters are **never** passed as environment variables. They are always delivered via:
- **stdin** (default) - Secure, not visible in process listings
- **file** - Secure temporary file with restrictive permissions (0400)

This ensures parameters (including sensitive data like passwords, API keys, tokens) are never exposed in process listings.

### Environment Variables Are Separate

Environment variables provide execution context and configuration:
- Stored in `execution.env_vars` (JSONB key-value pairs)
- Set as environment variables by the worker
- Examples: `ATTUNE_EXECUTION_ID`, custom config values, feature flags
- Typically non-sensitive (visible in process environment)

---

## Parameter Delivery Methods

### 1. Standard Input (`stdin`)

**Security**: ✅ **High** - Not visible in process listings  
**Use Case**: Sensitive data, structured parameters, credentials

Parameters are serialized in the specified format and written to stdin as a single document (secrets are merged into the parameters), followed by a newline, then stdin is closed.

**Example** (this is the default):
```yaml
parameter_delivery: stdin
parameter_format: json
```

**Environment variables set**:
- `ATTUNE_PARAMETER_DELIVERY=stdin`
- `ATTUNE_PARAMETER_FORMAT=json`

**Stdin content (JSON format)**:
```
{"message":"Hello","count":42,"enabled":true,"api_key":"secret123","db_password":"pass456"}
```

**Python script example**:
```python
#!/usr/bin/env python3
import sys
import json

def read_stdin_params():
    """Read parameters from stdin. Secrets are already merged into parameters."""
    content = sys.stdin.read().strip()
    return json.loads(content) if content else {}

params = read_stdin_params()
message = params.get('message', 'default')
api_key = params.get('api_key')
print(f"Message: {message}")
```

**Shell script example**:
```bash
#!/bin/bash

# Read parameters from stdin (JSON format)
read -r PARAMS_JSON
# Parse JSON (requires jq)
MESSAGE=$(echo "$PARAMS_JSON" | jq -r '.message // "default"')
COUNT=$(echo "$PARAMS_JSON" | jq -r '.count // 0')

echo "Message: $MESSAGE, Count: $COUNT"
```

---

### 2. Temporary File (`file`)

**Security**: ✅ **High** - File has restrictive permissions (owner read-only)  
**Use Case**: Large parameter payloads, sensitive data, actions that need random access to parameters

Parameters are written to a temporary file with restrictive permissions (`0400` on Unix). The file path is provided via the `ATTUNE_PARAMETER_FILE` environment variable.

**Example**:
```yaml
# Explicitly set to file
parameter_delivery: file
parameter_format: yaml
```

**Environment variables set**:
- `ATTUNE_PARAMETER_DELIVERY=file`
- `ATTUNE_PARAMETER_FORMAT=yaml`
- `ATTUNE_PARAMETER_FILE=/tmp/attune-params-abc123.yaml`

**File content (YAML format)**:
```yaml
message: Hello
count: 42
enabled: true
```

**Python script example**:
```python
#!/usr/bin/env python3
import os
import yaml

def read_file_params():
    """Read parameters from temporary file."""
    param_file = os.environ.get('ATTUNE_PARAMETER_FILE')
    if not param_file:
        return {}
    
    with open(param_file, 'r') as f:
        return yaml.safe_load(f)

params = read_file_params()
message = params.get('message', 'default')
count = params.get('count', 0)
print(f"Message: {message}, Count: {count}")
```

**Shell script example**:
```bash
#!/bin/bash

# Read from parameter file
PARAM_FILE="${ATTUNE_PARAMETER_FILE}"
if [ -f "$PARAM_FILE" ]; then
    # Parse YAML (requires yq or similar)
    MESSAGE=$(yq eval '.message // "default"' "$PARAM_FILE")
    COUNT=$(yq eval '.count // 0' "$PARAM_FILE")
    echo "Message: $MESSAGE, Count: $COUNT"
fi
```

**Note**: The temporary file is automatically deleted after the action completes.

---

## Parameter Formats

### 1. JSON (`json`)

**Format**: JSON object  
**Best For**: Structured data, Python/Node.js actions, complex parameters  
**Type Preservation**: Yes (strings, numbers, booleans, arrays, objects)

**Example**:
```json
{
  "message": "Hello, World!",
  "count": 42,
  "enabled": true,
  "tags": ["prod", "api"],
  "config": {
    "timeout": 30,
    "retries": 3
  }
}
```

---

### 2. Dotenv (`dotenv`)

**Format**: `KEY='VALUE'` (one per line)  
**Best For**: Simple key-value pairs when needed  
**Type Preservation**: No (all values are strings)

**Example**:
```
MESSAGE='Hello, World!'
COUNT='42'
ENABLED='true'
```

**Escaping**: Single quotes in values are escaped as `'\''`

---

### 3. YAML (`yaml`)

**Format**: YAML document  
**Best For**: Human-readable structured data, complex configurations  
**Type Preservation**: Yes (strings, numbers, booleans, arrays, objects)

**Example**:
```yaml
message: Hello, World!
count: 42
enabled: true
tags:
  - prod
  - api
config:
  timeout: 30
  retries: 3
```

---

## Configuration in Action YAML

Add these fields to your action metadata file:

```yaml
name: my_action
ref: mypack.my_action
description: "My secure action"
runner_type: python
entry_point: my_action.py

# Parameter delivery configuration (optional - these are the defaults)
# parameter_delivery: stdin   # Options: stdin, file (default: stdin)
# parameter_format: json      # Options: json, dotenv, yaml (default: json)

parameters:
  type: object
  properties:
    api_key:
      type: string
      description: "API key for authentication"
      secret: true          # Mark sensitive parameters
    message:
      type: string
      description: "Message to process"
```

---

## Best Practices

### 1. Choose the Right Delivery Method

| Scenario | Recommended Delivery | Recommended Format |
|----------|---------------------|-------------------|
| Most actions (default) | `stdin` | `json` |
| Sensitive credentials | `stdin` (default) | `json` (default) |
| Large parameter payloads (>1MB) | `file` | `json` or `yaml` |
| Complex structured data | `stdin` (default) | `json` (default) |
| Shell scripts | `stdin` (default) | `json` or `dotenv` |
| Python/Node.js actions | `stdin` (default) | `json` (default) |

### 2. Mark Sensitive Parameters

Always mark sensitive parameters with `secret: true` in the parameter schema:

```yaml
parameters:
  type: object
  properties:
    password:
      type: string
      secret: true
    api_token:
      type: string
      secret: true
```

### 3. Handle Missing Parameters Gracefully

```python
# Python example
params = read_params()
api_key = params.get('api_key')
if not api_key:
    print("ERROR: api_key parameter is required", file=sys.stderr)
    sys.exit(1)
```

```bash
# Shell example
if [ -z "$ATTUNE_ACTION_API_KEY" ]; then
    echo "ERROR: api_key parameter is required" >&2
    exit 1
fi
```

### 4. Validate Parameter Format

Check the `ATTUNE_PARAMETER_DELIVERY` environment variable to determine how parameters were delivered:

```python
import os

delivery_method = os.environ.get('ATTUNE_PARAMETER_DELIVERY', 'env')
param_format = os.environ.get('ATTUNE_PARAMETER_FORMAT', 'dotenv')

if delivery_method == 'env':
    # Read from environment variables
    params = read_env_params()
elif delivery_method == 'stdin':
    # Read from stdin
    params = read_stdin_params()
elif delivery_method == 'file':
    # Read from file
    params = read_file_params()
```

### 5. Clean Up Sensitive Data

For file-based delivery, the system automatically deletes the temporary file. For stdin/env, ensure sensitive data doesn't leak into logs:

```python
# Don't log sensitive parameters
logger.info(f"Processing request for user: {params['username']}")
# Don't do this:
# logger.debug(f"Full params: {params}")  # May contain secrets!
```

---

## Design Philosophy

### Parameters vs Environment Variables

**Action Parameters** (`stdin` or `file`):
- Data the action operates on
- Always secure (never in environment)
- Examples: API payloads, credentials, business data
- Stored in `execution.config` → `parameters`
- Passed via stdin or temporary file

**Environment Variables** (`execution.env_vars`):
- Execution context and configuration
- Set as environment variables by worker
- Examples: `ATTUNE_EXECUTION_ID`, custom config, feature flags
- Stored in `execution.env_vars` JSONB
- Typically non-sensitive

### Default Behavior (Secure by Default)

**As of 2025-02-05**: Parameters default to:
- `parameter_delivery: stdin`
- `parameter_format: json`

All action parameters are secure by design. There is no option to pass parameters as environment variables.

### Migration from Environment Variables

If you were previously passing data as environment variables, you now have two options:

**Option 1: Move to Parameters** (for action data):
```python
# Read from stdin (secrets are merged into parameters)
import sys, json
content = sys.stdin.read().strip()
params = json.loads(content) if content else {}
value = params.get('key')
```

**Option 2: Use execution.env_vars** (for execution context):
Store non-sensitive configuration in `execution.env_vars` when creating the execution:
```json
{
  "action_ref": "mypack.myaction",
  "parameters": {"data": "value"},
  "env_vars": {"CUSTOM_CONFIG": "value"}
}
```

Then read from environment in action:
```python
import os
config = os.environ.get('CUSTOM_CONFIG')
```

---

## Examples

### Complete Python Action with Stdin/JSON

**Action YAML** (`mypack/actions/secure_action.yaml`):
```yaml
name: secure_action
ref: mypack.secure_action
description: "Secure action with stdin parameter delivery"
runner_type: python
entry_point: secure_action.py
# Uses default stdin + json (no need to specify)

parameters:
  type: object
  properties:
    api_token:
      type: string
      secret: true
    endpoint:
      type: string
    data:
      type: object
  required:
    - api_token
    - endpoint
```

**Action Script** (`mypack/actions/secure_action.py`):
```python
#!/usr/bin/env python3
import sys
import json
import requests

def read_stdin_params():
    """Read parameters from stdin. Secrets are already merged into parameters."""
    content = sys.stdin.read().strip()
    return json.loads(content) if content else {}

def main():
    params = read_stdin_params()
    
    api_token = params.get('api_token')
    endpoint = params.get('endpoint')
    data = params.get('data', {})
    
    if not api_token or not endpoint:
        print(json.dumps({"error": "Missing required parameters"}))
        sys.exit(1)
    
    headers = {"Authorization": f"Bearer {api_token}"}
    response = requests.post(endpoint, json=data, headers=headers)
    
    result = {
        "status_code": response.status_code,
        "response": response.json() if response.ok else None,
        "success": response.ok
    }
    
    print(json.dumps(result))
    sys.exit(0 if response.ok else 1)

if __name__ == "__main__":
    main()
```

### Complete Shell Action with File/YAML

**Action YAML** (`mypack/actions/process_config.yaml`):
```yaml
name: process_config
ref: mypack.process_config
description: "Process configuration with file-based parameter delivery"
runner_type: shell
entry_point: process_config.sh
# Explicitly use file delivery for large configs
parameter_delivery: file
parameter_format: yaml

parameters:
  type: object
  properties:
    config:
      type: object
      description: "Configuration object"
    environment:
      type: string
      enum: [dev, staging, prod]
  required:
    - config
```

**Action Script** (`mypack/actions/process_config.sh`):
```bash
#!/bin/bash
set -e

# Check if parameter file exists
if [ -z "$ATTUNE_PARAMETER_FILE" ]; then
    echo "ERROR: No parameter file provided" >&2
    exit 1
fi

# Read configuration from YAML file (requires yq)
ENVIRONMENT=$(yq eval '.environment // "dev"' "$ATTUNE_PARAMETER_FILE")
CONFIG=$(yq eval '.config' "$ATTUNE_PARAMETER_FILE")

echo "Processing configuration for environment: $ENVIRONMENT"
echo "Config: $CONFIG"

# Process configuration...
# Your logic here

echo "Configuration processed successfully"
exit 0
```

---

## Environment Variables Reference

Actions automatically receive these environment variables:

**System Variables** (always set):
- `ATTUNE_EXECUTION_ID` - Current execution ID
- `ATTUNE_ACTION_REF` - Action reference (e.g., "mypack.myaction")
- `ATTUNE_PARAMETER_DELIVERY` - Delivery method (stdin/file)
- `ATTUNE_PARAMETER_FORMAT` - Format used (json/dotenv/yaml)
- `ATTUNE_PARAMETER_FILE` - File path (only for file delivery)

**Custom Variables** (from `execution.env_vars`):
Any key-value pairs in `execution.env_vars` are set as environment variables.

Example:
```json
{
  "env_vars": {
    "LOG_LEVEL": "debug",
    "RETRY_COUNT": "3"
  }
}
```

Action receives:
```bash
LOG_LEVEL=debug
RETRY_COUNT=3
```

---

## Related Documentation

- [Pack Structure](../packs/pack-structure.md)
- [Action Development Guide](./action-development-guide.md) (future)
- [Secrets Management](../authentication/secrets-management.md)
- [Security Best Practices](../authentication/security-review-2024-01-02.md)
- [Execution API](../api/api-executions.md)

---

## Support

For questions or issues related to parameter delivery:
1. Check the action logs for parameter delivery metadata
2. Verify the `ATTUNE_PARAMETER_DELIVERY` and `ATTUNE_PARAMETER_FORMAT` environment variables
3. Test with a simple action first before implementing complex parameter handling
4. Review the example actions in the `core` pack for reference implementations