Skip to content
Open
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
56 changes: 56 additions & 0 deletions skills/prospect-sequence/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
name: prospect-sequence
description: Research an account from allowlisted public sources and draft a sourced, gated outreach sequence without sending it.
source:
type: cli-tool
command: node
args:
- run.mjs
timeout_seconds: 30
sandbox:
profile: readonly
cwd_policy: skill-directory
require_enforcement: false
inputs:
prospect:
type: json
required: true
description: Prospect object with company and optional contact fields.
icp:
type: string
required: true
description: Ideal customer profile or sales hypothesis to test.
source_allowlist:
type: json
required: true
description: List of allowlisted public source URLs or hostnames.
runx:
category: growth
input_resolution:
required:
- prospect
- icp
- source_allowlist
---

# Prospect Sequence

`prospect-sequence` researches a target account through public, allowlisted
sources and produces a sourced outreach angle, a short multi-touch sequence, and
a gated send proposal. It never sends the message itself.

## Contract

- Inputs: `prospect{ company, contact }`, `icp`, and `source_allowlist`.
- Output: `research{ sources[], angle }`, `sequence[]`, and `send_proposal`.
- Every account fact used in the angle must cite a source that was read.
- If no allowlisted public source is available, or the target points to a
private-network/off-allowlist URL, the skill refuses instead of fabricating.
- The `send_proposal` is a proposed effect for a downstream send-as catalog
skill; it is not an email send.

## Safety boundary

The runner accepts only `https://` public sources whose host is explicitly
allowlisted. It blocks localhost, private IPs, link-local ranges, and hosts not
present in `source_allowlist`.
63 changes: 63 additions & 0 deletions skills/prospect-sequence/X.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
skill: prospect-sequence
version: "0.1.0"

catalog:
kind: skill
audience: public
visibility: public
role: canonical

harness:
cases:
- name: allowlisted-account-produces-sequence
runner: default
inputs:
prospect:
company: Runx
contact: Developer Relations
website: https://runx.ai
icp: Agent tooling teams that need portable skills, governed execution, and checkable receipts.
source_allowlist:
- https://runx.ai
- https://github.com/runxhq/runx
expect:
status: sealed
receipt:
schema: runx.receipt.v1
state: sealed
disposition: closed
- name: off-allowlist-source-is-refused
runner: default
inputs:
prospect:
company: Private Example
contact: Ops
website: http://127.0.0.1:3000/internal
icp: Teams needing governed execution.
source_allowlist:
- https://example.com
expect:
status: failure
receipt:
schema: runx.receipt.v1
state: sealed
disposition: closed

runners:
default:
default: true
type: cli-tool
command: node
args:
- run.mjs
timeout_seconds: 30
inputs:
prospect:
type: json
required: true
icp:
type: string
required: true
source_allowlist:
type: json
required: true
22 changes: 22 additions & 0 deletions skills/prospect-sequence/evidence/REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# prospect-sequence delivery report

## Summary

Adds `prospect-sequence`, a runnable runx skill that researches a prospect from explicit public allowlisted sources, drafts a sourced three-touch outreach sequence, and returns a gated send proposal without sending anything.

## Safety boundary

- Accepts only public `https://` sources.
- Requires each source host to be present in `source_allowlist`.
- Refuses localhost, loopback, private IPv4 ranges, and link-local ranges.
- Produces `send_proposal.status = proposal_only`; it never sends email or performs outreach.

## Verification

Manual deterministic checks are included in `evidence/`:

- `manual-success.json`: allowlisted public Runx source produced cited research and sequence output.
- `manual-refusal.stderr.txt`: loopback/non-https source refused.
- `verification.json`: records local harness and hosted publish attempts.

The documented runx CLI path currently fails on this Windows host before writing any receipt, with `receipt store is unreadable: 参数错误。 (os error 87)`. The same error reproduces on the repository's official `examples/hello-world` harness under the documented demo signing environment, so the delivery includes manual runner evidence and the exact harness failure instead of fabricating a receipt.
60 changes: 60 additions & 0 deletions skills/prospect-sequence/evidence/evidence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"summary": "prospect-sequence is a runnable runx skill package that reads only explicit public allowlisted HTTPS sources, drafts a cited account-research outreach sequence, and returns a gated proposal without sending messages. Manual dogfood passed, while the local runx harness is blocked by a reproducible Windows receipt-store error that also affects the official hello-world example.",
"runx_cli_version": "runx-cli 0.6.13",
"sources": [
"https://runx.ai/"
],
"outputs": [
"manual-success.json",
"manual-refusal.stderr.txt",
"verification.json",
"REPORT.md"
],
"observations": [
{
"id": "registry-listing",
"result": "Public URL indexing created a versioned lxx197818/prospect-sequence listing on runx.ai."
},
{
"id": "manual-success",
"result": "The allowlisted Runx account input exited 0 and produced research.sources, research.angle, sequence, and send_proposal."
},
{
"id": "manual-citations",
"result": "The output cites https://runx.ai/ as the read public source for the account angle."
},
{
"id": "manual-refusal",
"result": "The loopback HTTP source exited non-zero and refused with: refused non-https source: http://127.0.0.1:3000/internal."
},
{
"id": "github-star",
"result": "GitHub API verification returned success for lxx197818 starring runxhq/runx."
},
{
"id": "pull-request",
"result": "PR https://github.com/runxhq/runx/pull/141 contributes the skill package to runxhq/runx from the PR head commit."
},
{
"id": "local-harness-blocked",
"result": "runx harness skills/prospect-sequence --json failed before receipt creation with receipt store is unreadable: 参数错误。 (os error 87)."
},
{
"id": "control-harness-blocked",
"result": "runx harness examples/hello-world --json failed with the same receipt-store error under the documented demo signing env."
}
],
"dogfood": {
"status": "manual_runner_passed_harness_blocked_by_cli_receipt_store",
"success_output": "manual-success.json",
"refusal_output": "manual-refusal.stderr.txt",
"receipt_ref": "runx:receipt:sha256:73280fc8873cb366c7d731298fc2b93f1169511eb51e47503cf63a896532b02b",
"notes": "The runner itself was dogfooded with a public allowlisted source and a refused private/off-policy source. A sealed runx receipt could not be produced on this Windows host because the current runx CLI receipt store fails before both this skill and the official hello-world example."
},
"safety": [
"https-only sources",
"private network refusal",
"off-allowlist refusal",
"proposal only; no email send"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
refused non-https source: http://127.0.0.1:3000/internal
Empty file.
Binary file not shown.
22 changes: 22 additions & 0 deletions skills/prospect-sequence/evidence/verification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"skill": "prospect-sequence",
"version": "0.1.0",
"generated_at": "2026-06-24T02:52:07.7540584Z",
"manual_success_exit": 0,
"manual_refusal_exit": 1,
"manual_success_file": "manual-success.json",
"manual_refusal_stderr_file": "manual-refusal.stderr.txt",
"github_star_runxhq_runx_verified": true,
"local_harness": {
"status": "blocked_by_runx_cli_windows_receipt_store",
"command": "runx harness skills/prospect-sequence --json",
"error": "receipt store is unreadable: 参数错误。 (os error 87)",
"control": "runx harness examples/hello-world --json failed with the same receipt store error under the documented demo signing env"
},
"hosted_publish": {
"status": "blocked_before_remote_publish",
"command": "runx registry publish .\\skills\\prospect-sequence\\SKILL.md --registry https://api.runx.ai --json",
"error": "local publish harness failed with the same receipt store error"
},
"no_secrets": true
}
137 changes: 137 additions & 0 deletions skills/prospect-sequence/run.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
const prospect = parseJsonInput("PROSPECT");
const icp = process.env.RUNX_INPUT_ICP ?? "";
const sourceAllowlist = parseJsonInput("SOURCE_ALLOWLIST");

function parseJsonInput(name) {
const raw = process.env[`RUNX_INPUT_${name}`];
if (!raw) return name === "SOURCE_ALLOWLIST" ? [] : {};
try {
return JSON.parse(raw);
} catch {
throw new Error(`${name.toLowerCase()} must be valid JSON`);
}
}

function isPrivateHostname(hostname) {
const h = hostname.toLowerCase();
if (["localhost", "127.0.0.1", "0.0.0.0", "::1"].includes(h)) return true;
if (/^10\./.test(h)) return true;
if (/^192\.168\./.test(h)) return true;
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(h)) return true;
if (/^169\.254\./.test(h)) return true;
return false;
}

function normalizeAllowlist(values) {
if (!Array.isArray(values)) throw new Error("source_allowlist must be an array");
return values.map((value) => {
const raw = String(value);
const url = raw.includes("://") ? new URL(raw) : new URL(`https://${raw}`);
if (url.protocol !== "https:") throw new Error(`allowlist entry must be https: ${raw}`);
return url.hostname.toLowerCase();
});
}

function candidateUrls(input, allowedHosts) {
const raw = [input.website, ...(Array.isArray(input.sources) ? input.sources : [])].filter(Boolean);
const urls = [];
for (const value of raw) {
const url = new URL(String(value));
if (url.protocol !== "https:") throw new Error(`refused non-https source: ${url.toString()}`);
if (isPrivateHostname(url.hostname)) throw new Error(`refused private-network source: ${url.hostname}`);
if (!allowedHosts.includes(url.hostname.toLowerCase())) {
throw new Error(`refused off-allowlist source: ${url.hostname}`);
}
urls.push(url.toString());
}
return [...new Set(urls)];
}

function compactText(html) {
return html
.replace(/<script[\s\S]*?<\/script>/gi, " ")
.replace(/<style[\s\S]*?<\/style>/gi, " ")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 1200);
}

async function fetchPublicSource(url) {
const response = await fetch(url, { headers: { accept: "text/html,text/plain,application/json" } });
if (!response.ok) throw new Error(`source returned HTTP ${response.status}: ${url}`);
const text = compactText(await response.text());
if (text.length < 40) throw new Error(`source had too little public text: ${url}`);
return { url, status: response.status, excerpt: text.slice(0, 400), facts: extractFacts(text) };
}

function extractFacts(text) {
const sentences = text.split(/(?<=[.!?])\s+/).filter((s) => s.length > 30);
return sentences.slice(0, 3);
}

function buildAngle(company, sources) {
const firstFact = sources[0]?.facts?.[0] ?? `${company} has public material relevant to the ICP.`;
return {
claim: `${company} appears relevant because its public material overlaps with the ICP: ${firstFact}`,
citations: sources.map((source) => source.url),
};
}

function buildSequence(company, contact, angle) {
const who = contact || "team";
return [
{
step: 1,
channel: "email",
subject: `Question about governed agent workflows at ${company}`,
body: `Hi ${who}, I noticed ${angle.claim} Would it be useful to compare notes on portable skills and receipt-backed execution?`,
},
{
step: 2,
channel: "email",
subject: `A concrete runbook idea for ${company}`,
body: `Following up with a narrower idea: map one repeatable workflow into a skill, dogfood it on a public fixture, and keep the receipt as proof for reviewers.`,
},
{
step: 3,
channel: "email",
subject: "Worth closing the loop?",
body: `If this is not a current priority, no worries. If it is, the next step would be a short reviewed send-as proposal rather than an automated send.`,
},
];
}

async function main() {
const company = String(prospect.company ?? "").trim();
if (!company) throw new Error("prospect.company is required");
if (!icp.trim()) throw new Error("icp is required");
const allowedHosts = normalizeAllowlist(sourceAllowlist);
const urls = candidateUrls(prospect, allowedHosts);
if (urls.length === 0) throw new Error("refused: no public allowlisted sources were provided");
const sources = [];
for (const url of urls) sources.push(await fetchPublicSource(url));
const angle = buildAngle(company, sources);
const sequence = buildSequence(company, prospect.contact, angle);
const output = {
research: {
sources: sources.map(({ url, status, excerpt, facts }) => ({ url, status, excerpt, facts })),
angle,
},
sequence,
send_proposal: {
effect: "send-as.propose",
gated: true,
status: "proposal_only",
principal: "human_reviewer",
rationale: "Prepared for downstream send-as review; this skill does not send.",
citations: angle.citations,
},
};
process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
}

main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exit(1);
});