working out the worker/execution interface
This commit is contained in:
359
docs/QUICKREF-action-parameters.md
Normal file
359
docs/QUICKREF-action-parameters.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user