Validation harness for the apify-pi-plugin. Build, install, configure, and test the Apify plugin against a running pi agent. Uses pi -p one-shot mode and tmux for interactive commands. Used by the validator agent after the coder agent completes implementation. Triggers: "validate apify plugin", "test pi plugin", "verify apify integration".
npx skills add https://github.com/apify/apify-pi-plugin --skill pi-plugin-validation
The validator agent runs this skill after the coder agent has implemented
apify-pi-plugin per PI_PLUGIN_TICKET.md.
The validator uses the real pi binary, not headless SDK. Functional tests
use one-shot pi -p mode. Interactive commands use tmux. This validates the
plugin exactly as an end user would experience it.
apify-pi-plugin/ directory with anindex.ts entry, package.json with "pi": { "extensions": ["."] }, and
all required source files per the ticket's §8 File structure.
.env file exists in the plugin repo root containingAPIFY_API_KEY=apify_api_....
/Users/gokdenizkaymak/apify/pi passes npm run check.~/.pi/agent/auth.json orprovider-specific env var like ANTHROPIC_API_KEY). The validator reuses
whatever provider/model is already configured.
# Find the pi binary (built from monorepo)
PI_BIN=$(which pi 2>/dev/null || echo "/Users/gokdenizkaymak/apify/pi/packages/coding-agent/dist/cli.js")
echo "Using pi: $PI_BIN"
# Load Apify API key from .env
cd /path/to/apify-pi-plugin
export APIFY_API_KEY=$(grep APIFY_API_KEY .env | cut -d'=' -f2)
echo "Apify key loaded (first 12): ${APIFY_API_KEY:0:12}..."
cd /path/to/apify-pi-plugin
npm install --ignore-scripts
Verify it compiles:
npx tsc --noEmit
# If no tsconfig.json, verify module loads:
node --experimental-strip-types --no-warnings \
-e "import('./index.ts').then(m => console.log('OK: module loaded'))"
The validator writes the config file directly. This is equivalent to what
/apify login does after the user pastes their key, but scriptable.
mkdir -p ~/.pi/agent
cat > ~/.pi/agent/apify.json << APIFYEOF
{
"apiKey": "${APIFY_API_KEY}"
}
APIFYEOF
Verify the config file was written:
cat ~/.pi/agent/apify.json | python3 -c "import sys,json; c=json.load(sys.stdin); print(f'Key present: {bool(c.get(\"apiKey\"))}, length: {len(c.get(\"apiKey\",\"\"))}')"
All functional tests use one-shot pi -p mode. The plugin is loaded via -e
pointing at the plugin's index.ts. The apify tool is explicitly enabled via
--tools.
Common flags for all scenarios:
PI_CMD="$PI_BIN -e /path/to/apify-pi-plugin/index.ts --tools apify,read,bash,write -p"
The validator must wait for pi to exit (one-shot mode exits after the prompt
is fully processed) and capture stdout. Expect 5-60 seconds depending on the
prompt complexity and model speed.
Purpose: Verify the apify tool is registered and pi knows about it.
Command:
$PI_BIN -e /path/to/apify-pi-plugin/index.ts --list-tools 2>&1 | grep apify
Pass: Output includes apify. Does NOT return "no such tool" or empty result.
Purpose: Verify discover/search returns Actor listings with tilde slugs and a tip:.
Command (one-shot):
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=discover and query='instagram scraper' to search the Apify Store. Report the top 3 results including their slugs (must use ~ not /). Confirm the response ends with a 'tip:' suggestion."
Pass conditions (grep the output):
username~actor-name (tilde format)tip:apify/ (slash format)Purpose: Verify discover/schema returns input schema and README for a known Actor.
Command:
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=discover and actorId='apify~web-scraper-puppeteer' to fetch the input schema. Tell me what input fields it requires and confirm the response ends with a 'tip:' that mentions action=start."
Pass conditions:
startUrls or equivalent input fieldstip:action="start" or action=startPurpose: Verify start returns immediately with a run reference.
Command:
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=start, actorId='apify~web-scraper-puppeteer', and input={ startUrls: [{ url: 'https://example.com' }] } to launch a scrape. The start call must return immediately (NOT wait for the run to finish). Report the runId and datasetId from the response."
Pass conditions:
runIddatasetIdPurpose: Verify collect returns correct terminal/pending/error buckets.
Procedure:
Command:
# After Scenario 4 succeeds with runId=RUN_ID and datasetId=DS_ID:
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash,write \
-p "Use the apify tool with action=collect and runReferences=[{ runId: 'RUN_ID', actorId: 'apify~web-scraper-puppeteer', datasetId: 'DS_ID' }] to check the status of the run. Report whether it completed and how many items were scraped."
Note: if the run is still RUNNING, wait 10s and retry. The validator may need
to run this command 2-3 times until allDone: true.
Pass conditions:
<<<EXTERNAL_UNTRUSTED_CONTENT>>> wrapping markersPurpose: Verify untrusted-content markers are present and correctly formatted.
Command (combine with Scenario 5 collect output):
# Search the output of Scenario 5 for markers
echo "$COLLECT_OUTPUT" | grep -c "<<<EXTERNAL_UNTRUSTED_CONTENT>>>"
echo "$COLLECT_OUTPUT" | grep -c "<<<END_EXTERNAL_UNTRUSTED_CONTENT>>>"
echo "$COLLECT_OUTPUT" | grep -c "Source: apify:"
Pass conditions:
Source: apify:apify~web-scraper-puppeteer appearsPurpose: Verify structured error (not a crash) when no key is configured.
Command:
# Temporarily remove the key
mv ~/.pi/agent/apify.json ~/.pi/agent/apify.json.bak
unset APIFY_API_KEY
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=discover and query='test'." 2>&1 | head -50
# Restore
mv ~/.pi/agent/apify.json.bak ~/.pi/agent/apify.json
export APIFY_API_KEY=$(grep APIFY_API_KEY /path/to/apify-pi-plugin/.env | cut -d'=' -f2)
Pass conditions:
missing_credential or "not configured"Purpose: Verify slash-based slugs are rejected.
Command:
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=discover and actorId='apify/instagram-scraper' (using slash). Report the error." 2>&1
Pass conditions:
Purpose: Verify non-Apify baseUrl values are rejected.
Command:
# Write malicious config
cat > ~/.pi/agent/apify.json << BADEOF
{
"apiKey": "${APIFY_API_KEY}",
"baseUrl": "https://evil.example.com"
}
BADEOF
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=discover and query='test'." 2>&1 | head -30
# Restore valid config
cat > ~/.pi/agent/apify.json << GOODEOF
{
"apiKey": "${APIFY_API_KEY}"
}
GOODEOF
Pass conditions:
invalid_base_url or "baseUrl" errorevil.example.comenabled: falsePurpose: Verify the hard kill switch.
Command:
cat > ~/.pi/agent/apify.json << KILLEOF
{
"apiKey": "${APIFY_API_KEY}",
"enabled": false
}
KILLEOF
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Use the apify tool with action=discover and query='test'." 2>&1 | head -30
# Restore
cat > ~/.pi/agent/apify.json << GOODEOF
{
"apiKey": "${APIFY_API_KEY}"
}
GOODEOF
Pass conditions:
/apify status and /apify testPurpose: Verify /apify status and /apify test respond correctly via one-shot.
These commands are pi slash commands, which means they run inside an active
pi session. Use one-shot mode to invoke them:
# /apify status
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Run the /apify status command and report the output. Confirm it shows authentication info without exposing the full API key." 2>&1
# /apify test
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash \
-p "Run the /apify test command and report the output. Confirm connectivity to Apify is working." 2>&1
Pass conditions:
/apify status output mentions userId or plan, AND does NOT contain the fullAPI key string (max 12 chars visible)
/apify test output confirms connectivity/apify login — interactive test via tmuxPurpose: Verify the interactive /apify login command flow (masked input,
validates, writes config, never echoes full key).
This test uses tmux per the AGENTS.md testing pattern. The validator must:
/apify login.env) at the masked prompt# 1. Remove existing config
rm -f ~/.pi/agent/apify.json
# 2. Start tmux session with pi + extension loaded
tmux new-session -d -s apify-login-test -x 100 -y 30
tmux send-keys -t apify-login-test \
"$PI_BIN -e /path/to/apify-pi-plugin/index.ts --tools apify,read,bash" Enter
# 3. Wait for pi to start (check for prompt)
sleep 4
tmux capture-pane -t apify-login-test -p | tail -5
# 4. Send the /apify login command
tmux send-keys -t apify-login-test "/apify login" Enter
# 5. Wait for the key prompt, then send the key
sleep 2
tmux send-keys -t apify-login-test "${APIFY_API_KEY}" Enter
# 6. Wait for validation and config write
sleep 3
# 7. Capture full pane output
tmux capture-pane -t apify-login-test -p > /tmp/apify-login-output.txt
# 8. Kill tmux session
tmux kill-session -t apify-login-test
Pass conditions (check /tmp/apify-login-output.txt):
test -f ~/.pi/agent/apify.json && echo "PASS: config written"
cat ~/.pi/agent/apify.json | python3 -c "import sys,json; c=json.load(sys.stdin); print(f'Key stored: {bool(c.get(\"apiKey\"))}')"
Purpose: End-to-end test of the complete three-primitive workflow.
This is the most important scenario. Run it as a single one-shot prompt that
exercises all three actions:
RUN_ID_FILE=/tmp/apify-test-runid.txt
DS_ID_FILE=/tmp/apify-test-dsid.txt
$PI_BIN -e /path/to/apify-pi-plugin/index.ts \
--tools apify,read,bash,write \
-p "Do the following steps, reporting results after each:
1. Use apify with action=discover and actorId='apify~web-scraper-puppeteer' to see its input schema.
2. Based on the schema, use apify with action=start, actorId='apify~web-scraper-puppeteer', and input={ startUrls: [{ url: 'https://example.com' }] } to launch a scrape. Note the runId and datasetId.
3. Wait 15 seconds, then use apify with action=collect with the run reference to check and retrieve results.
4. Confirm the collected data is wrapped in untrusted-content markers." 2>&1
Pass conditions (all must be present in output):
startUrls or input fieldsrunId and datasetId<<<EXTERNAL_UNTRUSTED_CONTENT>>><<<END_EXTERNAL_UNTRUSTED_CONTENT>>>Source: apify:rm -f ~/.pi/agent/apify.json
rm -f /tmp/apify-login-output.txt /tmp/apify-test-runid.txt /tmp/apify-test-dsid.txt
Note: any test runs created on Apify during validation remain on the account
and are billed accordingly. Use cheap/small Actors (apify~web-scraper-puppeteer
with one URL to example.com) to minimize cost.
npm run check passes at/Users/gokdenizkaymak/apify/pi)
/Users/gokdenizkaymak/apify/pi/packages/coding-agent/dist/cli.js(or system pi if installed)
.env filebrew install tmux if needed)Take apify/pi-plugin-validation from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx, brew.
Without those the skill loads but fails at the first command.