359 lines
8.2 KiB
Markdown
359 lines
8.2 KiB
Markdown
# Quick Reference: Action Parameter Handling
|
|
|
|
**Last Updated:** 2026-02-07
|
|
**Status:** Current standard for all actions
|
|
|
|
## TL;DR
|
|
|
|
- ✅ **DO:** Read action parameters from **stdin as JSON**
|
|
- ❌ **DON'T:** Use environment variables for action parameters
|
|
- 💡 **Environment variables** are for debug/config only (e.g., `DEBUG=1`)
|
|
|
|
## Secure Parameter Delivery
|
|
|
|
All action parameters are delivered via **stdin** in **JSON format** to prevent exposure in process listings.
|
|
|
|
### YAML Configuration
|
|
|
|
```yaml
|
|
name: my_action
|
|
ref: mypack.my_action
|
|
runner_type: shell # or python, nodejs
|
|
entry_point: my_action.sh
|
|
|
|
# Always specify stdin parameter delivery
|
|
parameter_delivery: stdin
|
|
parameter_format: json
|
|
|
|
parameters:
|
|
type: object
|
|
properties:
|
|
message:
|
|
type: string
|
|
default: "Hello"
|
|
api_key:
|
|
type: string
|
|
secret: true # Mark sensitive parameters
|
|
```
|
|
|
|
## Implementation Patterns
|
|
|
|
### Bash/Shell Actions
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
set -e
|
|
set -o pipefail
|
|
|
|
# Read JSON parameters from stdin
|
|
INPUT=$(cat)
|
|
|
|
# Parse parameters with jq (includes default values)
|
|
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Hello, World!"')
|
|
API_KEY=$(echo "$INPUT" | jq -r '.api_key // ""')
|
|
COUNT=$(echo "$INPUT" | jq -r '.count // 1')
|
|
ENABLED=$(echo "$INPUT" | jq -r '.enabled // false')
|
|
|
|
# Handle optional parameters (check for null)
|
|
if [ -n "$API_KEY" ] && [ "$API_KEY" != "null" ]; then
|
|
echo "API key provided"
|
|
fi
|
|
|
|
# Use parameters
|
|
echo "Message: $MESSAGE"
|
|
echo "Count: $COUNT"
|
|
```
|
|
|
|
### Python Actions
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
import json
|
|
import sys
|
|
from typing import Dict, Any
|
|
|
|
def read_parameters() -> Dict[str, Any]:
|
|
"""Read and parse JSON parameters from stdin."""
|
|
try:
|
|
input_data = sys.stdin.read()
|
|
if not input_data:
|
|
return {}
|
|
return json.loads(input_data)
|
|
except json.JSONDecodeError as e:
|
|
print(f"ERROR: Invalid JSON input: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def main():
|
|
# Read parameters
|
|
params = read_parameters()
|
|
|
|
# Access parameters with defaults
|
|
message = params.get('message', 'Hello, World!')
|
|
api_key = params.get('api_key')
|
|
count = params.get('count', 1)
|
|
enabled = params.get('enabled', False)
|
|
|
|
# Validate required parameters
|
|
if not params.get('url'):
|
|
print("ERROR: 'url' parameter is required", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Use parameters
|
|
print(f"Message: {message}")
|
|
print(f"Count: {count}")
|
|
|
|
# Output result as JSON
|
|
result = {"status": "success", "message": message}
|
|
print(json.dumps(result))
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
```
|
|
|
|
### Node.js Actions
|
|
|
|
```javascript
|
|
#!/usr/bin/env node
|
|
|
|
const readline = require('readline');
|
|
|
|
async function readParameters() {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
terminal: false
|
|
});
|
|
|
|
let input = '';
|
|
for await (const line of rl) {
|
|
input += line;
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(input || '{}');
|
|
} catch (err) {
|
|
console.error('ERROR: Invalid JSON input:', err.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
// Read parameters
|
|
const params = await readParameters();
|
|
|
|
// Access parameters with defaults
|
|
const message = params.message || 'Hello, World!';
|
|
const apiKey = params.api_key;
|
|
const count = params.count || 1;
|
|
const enabled = params.enabled || false;
|
|
|
|
// Use parameters
|
|
console.log(`Message: ${message}`);
|
|
console.log(`Count: ${count}`);
|
|
|
|
// Output result as JSON
|
|
const result = { status: 'success', message };
|
|
console.log(JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('ERROR:', err.message);
|
|
process.exit(1);
|
|
});
|
|
```
|
|
|
|
## Testing Actions Locally
|
|
|
|
```bash
|
|
# Test with specific parameters
|
|
echo '{"message": "Test", "count": 5}' | ./my_action.sh
|
|
|
|
# Test with defaults (empty JSON)
|
|
echo '{}' | ./my_action.sh
|
|
|
|
# Test with file input
|
|
cat test-params.json | ./my_action.sh
|
|
|
|
# Test Python action
|
|
echo '{"url": "https://api.example.com"}' | python3 my_action.py
|
|
|
|
# Test with multiple parameters including secrets
|
|
echo '{"url": "https://api.example.com", "api_key": "secret123"}' | ./my_action.sh
|
|
```
|
|
|
|
## Environment Variables Usage
|
|
|
|
### ✅ Correct Usage (Configuration/Debug)
|
|
|
|
```bash
|
|
# Debug logging control
|
|
DEBUG=1 ./my_action.sh
|
|
|
|
# Log level control
|
|
LOG_LEVEL=debug ./my_action.sh
|
|
|
|
# System configuration
|
|
PATH=/usr/local/bin:$PATH ./my_action.sh
|
|
```
|
|
|
|
### ❌ Incorrect Usage (Parameters)
|
|
|
|
```bash
|
|
# NEVER do this - parameters should come from stdin
|
|
ATTUNE_ACTION_MESSAGE="Hello" ./my_action.sh # ❌ WRONG
|
|
API_KEY="secret" ./my_action.sh # ❌ WRONG - exposed in ps!
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Required Parameters
|
|
|
|
```bash
|
|
# Bash
|
|
URL=$(echo "$INPUT" | jq -r '.url // ""')
|
|
if [ -z "$URL" ] || [ "$URL" == "null" ]; then
|
|
echo "ERROR: 'url' parameter is required" >&2
|
|
exit 1
|
|
fi
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
if not params.get('url'):
|
|
print("ERROR: 'url' parameter is required", file=sys.stderr)
|
|
sys.exit(1)
|
|
```
|
|
|
|
### Optional Parameters with Null Check
|
|
|
|
```bash
|
|
# Bash
|
|
API_KEY=$(echo "$INPUT" | jq -r '.api_key // ""')
|
|
if [ -n "$API_KEY" ] && [ "$API_KEY" != "null" ]; then
|
|
# Use API key
|
|
echo "Authenticated request"
|
|
fi
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
api_key = params.get('api_key')
|
|
if api_key:
|
|
# Use API key
|
|
print("Authenticated request")
|
|
```
|
|
|
|
### Boolean Parameters
|
|
|
|
```bash
|
|
# Bash - jq outputs lowercase 'true'/'false'
|
|
ENABLED=$(echo "$INPUT" | jq -r '.enabled // false')
|
|
if [ "$ENABLED" = "true" ]; then
|
|
echo "Feature enabled"
|
|
fi
|
|
```
|
|
|
|
```python
|
|
# Python - native boolean
|
|
enabled = params.get('enabled', False)
|
|
if enabled:
|
|
print("Feature enabled")
|
|
```
|
|
|
|
### Array Parameters
|
|
|
|
```bash
|
|
# Bash
|
|
ITEMS=$(echo "$INPUT" | jq -c '.items // []')
|
|
ITEM_COUNT=$(echo "$ITEMS" | jq 'length')
|
|
echo "Processing $ITEM_COUNT items"
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
items = params.get('items', [])
|
|
print(f"Processing {len(items)} items")
|
|
for item in items:
|
|
print(f" - {item}")
|
|
```
|
|
|
|
### Object Parameters
|
|
|
|
```bash
|
|
# Bash
|
|
HEADERS=$(echo "$INPUT" | jq -c '.headers // {}')
|
|
# Extract specific header
|
|
AUTH=$(echo "$HEADERS" | jq -r '.Authorization // ""')
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
headers = params.get('headers', {})
|
|
auth = headers.get('Authorization')
|
|
```
|
|
|
|
## Security Best Practices
|
|
|
|
1. **Never log sensitive parameters** - Avoid printing secrets to stdout/stderr
|
|
2. **Mark secrets in YAML** - Use `secret: true` for sensitive parameters
|
|
3. **No parameter echoing** - Don't echo input JSON back in error messages
|
|
4. **Clear error messages** - Don't include parameter values in errors
|
|
5. **Validate input** - Check parameter types and ranges
|
|
|
|
### Example: Safe Error Handling
|
|
|
|
```python
|
|
# ❌ BAD - exposes parameter value
|
|
if not valid_url(url):
|
|
print(f"ERROR: Invalid URL: {url}", file=sys.stderr)
|
|
|
|
# ✅ GOOD - generic error message
|
|
if not valid_url(url):
|
|
print("ERROR: 'url' parameter must be a valid HTTP/HTTPS URL", file=sys.stderr)
|
|
```
|
|
|
|
## Migration from Environment Variables
|
|
|
|
If you have existing actions using environment variables:
|
|
|
|
```bash
|
|
# OLD (environment variables)
|
|
MESSAGE="${ATTUNE_ACTION_MESSAGE:-Hello}"
|
|
COUNT="${ATTUNE_ACTION_COUNT:-1}"
|
|
|
|
# NEW (stdin JSON)
|
|
INPUT=$(cat)
|
|
MESSAGE=$(echo "$INPUT" | jq -r '.message // "Hello"')
|
|
COUNT=$(echo "$INPUT" | jq -r '.count // 1')
|
|
```
|
|
|
|
```python
|
|
# OLD (environment variables)
|
|
import os
|
|
message = os.environ.get('ATTUNE_ACTION_MESSAGE', 'Hello')
|
|
count = int(os.environ.get('ATTUNE_ACTION_COUNT', '1'))
|
|
|
|
# NEW (stdin JSON)
|
|
import json, sys
|
|
params = json.loads(sys.stdin.read() or '{}')
|
|
message = params.get('message', 'Hello')
|
|
count = params.get('count', 1)
|
|
```
|
|
|
|
## Dependencies
|
|
|
|
- **Bash**: Requires `jq` (installed in all Attune worker containers)
|
|
- **Python**: Standard library only (`json`, `sys`)
|
|
- **Node.js**: Built-in modules only (`readline`)
|
|
|
|
## References
|
|
|
|
- [Core Pack Actions README](../packs/core/actions/README.md) - Reference implementations
|
|
- [Secure Action Parameter Handling Formats](zed:///agent/thread/e68272e6-a5a2-4d88-aaca-a9009f33a812) - Design document
|
|
- [Worker Service Architecture](./architecture/worker-service.md) - Parameter delivery details
|
|
|
|
## See Also
|
|
|
|
- Environment variables via `execution.env_vars` (for runtime context)
|
|
- Secret management via `key` table (for encrypted storage)
|
|
- Parameter validation in action YAML schemas |