Skill: y80163442-boop/naver-writer-acp (version 1.1.1)

--- name: naver-writer-acp description: Publish Naver Blog posts through the ACP marketplace flow (buyer-local thin-runner + sealed payload + offering execute). Use when a user asks to write/publish a Naver post, recover RUNNER_NOT_READY onboarding, run one-time thin-runner setup/login, or submit a publish job that should route through your ACP offering (`naver-blog-writer`) for paid service execution. --- # Naver Writer ACP (Marketplace-connected) Use this skill for the **real ACP commerce path**: 1. preflight local daemon 2. recover with setup_url when runner is not ready 3. one-time thin-runner setup/login 4. publish via ACP offering execute (paid path) ## Required runtime assumptions - Buyer machine: macOS + `@y80163442/naver-thin-runner` - Local daemon available on `127.0.0.1:${LOCAL_DAEMON_PORT:-19090}` - For paid marketplace path: `OPENCLAW_OFFERING_EXECUTE_URL` is configured ## Core commands ## 1) Preflight ```bash scripts/preflight.sh ``` If runner is not ready, it returns: ```json { "error": "RUNNER_NOT_READY", "setup_url": "https://...", "next_action": "RUN_SETUP" } ``` ## 2) Setup runner (one-time) ```bash scripts/setup_runner.sh --setup-url "<setup_url>" # then user login once: npx @y80163442/naver-thin-runner login ``` Or proof-based mode: ```bash scripts/setup_runner.sh --proof-token "<proof_token>" --setup-issue-url "https://<control-plane>/v3/onboarding/setup-url/issue" ``` ## 3) Publish ```bash scripts/publish.sh --title "제목" --body "본문" --tags "tag1,tag2" ``` Publish flow: - preflight - `GET /v1/local/identity` - `POST /v1/local/seal-job` - submit to `OPENCLAW_OFFERING_EXECUTE_URL` (preferred, paid path) - fallback: direct `/v2/jobs/dispatch-and-wait` (admin/internal only) ## Environment variables See `references/setup.md`. Key variables: - `X_LOCAL_TOKEN` - `LOCAL_DAEMON_PORT` (default `19090`) - `OPENCLAW_OFFERING_ID` (default `naver-blog-writer`) - `OPENCLAW_OFFERING_EXECUTE_URL` (required for paid path) - `PROOF_TOKEN`, `SETUP_ISSUE_URL` (for auto setup_url issue) ## Safety notes - Never commit real tokens/keys. - Keep `ACP_ADMIN_API_KEY` for internal debugging only. - For real commerce metrics/billing, use offering execute path (not direct dispatch fallback). ## References - `references/setup.md` - `references/ops-checklist.md`

path: references/ops-checklist.md
content: # Ops Checklist (OpenClaw ↔ ACP)

## Before go-live

- [ ] `scripts/smoke_test.sh` passes
- [ ] control-plane `/v2/version` returns 200
- [ ] ACP service `/v2/version` returns 200
- [ ] proof issue (`/v3/onboarding/proofs/issue`) returns 201
- [ ] setup-url issue (`/v3/onboarding/setup-url/issue`) returns 201
- [ ] offering execute URL is configured for paid path
- [ ] one buyer machine completed runner setup + login once

## Real-user test

1. OpenClaw installs skill from ClawHub
2. `scripts/preflight.sh`
3. If `RUNNER_NOT_READY`, run `scripts/setup_runner.sh` and login once
4. `scripts/publish.sh --title ... --body ...`
5. Verify deliverable has `published_url`
6. Verify ACP job appears as completed in marketplace records

## Incident quick actions

- `RUNNER_NOT_READY`: issue setup_url, run setup/login
- `AUTH_EXPIRED`: run thin-runner login again
- `ATTESTATION_ALREADY_USED`: ensure fresh `/v1/local/identity` for each job
- `PUBLISH_CONFIRM_TIMEOUT`: retry with new idempotency key


path: references/publish-checklist.md
content: # ClawHub Publish Checklist (ACP-connected skill)

## Content checks

- [ ] `SKILL.md` explains ACP commerce path clearly
- [ ] No secrets committed (`token`, `key`, `.env`)
- [ ] Scripts point to preflight/setup_runner/publish flow
- [ ] `references/setup.md` updated with real env names

## Validation checks

- [ ] `scripts/smoke_test.sh` passes
- [ ] `scripts/preflight.sh` returns RUNNER_NOT_READY contract on clean machine
- [ ] proof/setup-url issue works on production control-plane

## Publish

```bash
clawhub login
clawhub publish ./skills/naver-writer-acp \
  --slug naver-writer-acp \
  --name "Naver Writer ACP" \
  --version 1.1.0 \
  --changelog "Switch to ACP marketplace-connected flow (preflight/setup_runner/publish)"
```

## Post-publish

- [ ] `clawhub install naver-writer-acp --version 1.1.0` works in clean dir
- [ ] Installed scripts run (`scripts/smoke_test.sh`)


path: references/setup.md
content: # Setup (ACP marketplace path)

## Recommended path (paid offering execute)

```bash
# local daemon
export X_LOCAL_TOKEN="<thin-runner-local-token>"
export LOCAL_DAEMON_PORT="19090"

# onboarding helper (optional but recommended)
export PROOF_TOKEN="<proof_token>"
export SETUP_ISSUE_URL="https://<control-plane>/v3/onboarding/setup-url/issue"
# OR pre-issued setup URL
export SETUP_URL="https://<control-plane>/v2/onboarding/setup-sessions/start?setup_token=..."

# ACP offering execute path (for purchase/billing flow)
export OPENCLAW_OFFERING_ID="naver-blog-writer"
export OPENCLAW_OFFERING_EXECUTE_URL="https://<acp-service>/execute"
export OPENCLAW_CORE_API_KEY="<optional-if-required>"
export OPENCLAW_CORE_API_KEY_HEADER="x-api-key"
```

## Internal fallback only (direct dispatch)

Use only for debugging/ops, not for normal paid commerce tracking.

```bash
export CONTROL_PLANE_URL="https://control-plane-production-662b.up.railway.app"
export ACP_ADMIN_API_KEY="<admin-key>"
```

## Basic runbook

```bash
# 1) preflight
scripts/preflight.sh

# 2) if RUNNER_NOT_READY, setup once
scripts/setup_runner.sh --proof-token "$PROOF_TOKEN" --setup-issue-url "$SETUP_ISSUE_URL"
npx @y80163442/naver-thin-runner login

# 3) publish
scripts/publish.sh --title "제목" --body "본문" --tags "tag1,tag2"
```


path: scripts/preflight.sh
content: #!/usr/bin/env bash
set -euo pipefail

SETUP_URL="${SETUP_URL:-}"
PROOF_TOKEN="${PROOF_TOKEN:-}"
SETUP_ISSUE_URL="${SETUP_ISSUE_URL:-}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-$(hostname)-$(uname -s)-$(uname -m)}"
LOCAL_DAEMON_PORT="${LOCAL_DAEMON_PORT:-19090}"
X_LOCAL_TOKEN="${X_LOCAL_TOKEN:-${THIN_RUNNER_LOCAL_TOKEN:-}}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --setup-url)
      SETUP_URL="$2"
      shift 2
      ;;
    --proof-token)
      PROOF_TOKEN="$2"
      shift 2
      ;;
    --setup-issue-url)
      SETUP_ISSUE_URL="$2"
      shift 2
      ;;
    --device-fingerprint)
      DEVICE_FINGERPRINT="$2"
      shift 2
      ;;
    --local-daemon-port)
      LOCAL_DAEMON_PORT="$2"
      shift 2
      ;;
    --x-local-token)
      X_LOCAL_TOKEN="$2"
      shift 2
      ;;
    *)
      echo "unknown arg: $1" >&2
      exit 1
      ;;
  esac
done

resolve_setup_url() {
  if [[ -n "$SETUP_URL" ]]; then
    printf '%s' "$SETUP_URL"
    return 0
  fi

  if [[ -z "$PROOF_TOKEN" || -z "$SETUP_ISSUE_URL" ]]; then
    printf ''
    return 0
  fi

  local tmp_body
  tmp_body="$(mktemp)"

  local payload
  payload="$(node - "$PROOF_TOKEN" "$DEVICE_FINGERPRINT" <<'NODE'
const proofToken = process.argv[2];
const deviceFingerprint = process.argv[3];
process.stdout.write(JSON.stringify({
  proof_token: proofToken,
  device_fingerprint: deviceFingerprint,
}));
NODE
)"

  local code
  code="$({
    curl -sS -o "$tmp_body" -w '%{http_code}' \
      -X POST \
      -H 'content-type: application/json' \
      -d "$payload" \
      "$SETUP_ISSUE_URL"
  } || true)"

  if [[ "$code" =~ ^2 ]]; then
    node - "$tmp_body" <<'NODE'
const fs = require('node:fs');
const body = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
process.stdout.write(typeof body.setup_url === 'string' ? body.setup_url : '');
NODE
    rm -f "$tmp_body"
    return 0
  fi

  rm -f "$tmp_body"
  printf ''
  return 0
}

emit_not_ready() {
  local resolved_setup_url
  resolved_setup_url="$(resolve_setup_url)"
  node - "$resolved_setup_url" <<'NODE'
const setupUrl = process.argv[2] || null;
console.log(JSON.stringify({
  error: 'RUNNER_NOT_READY',
  setup_url: setupUrl,
  next_action: 'RUN_SETUP',
}, null, 2));
NODE
}

if [[ -z "$X_LOCAL_TOKEN" ]]; then
  emit_not_ready
  exit 2
fi

tmp_body="$(mktemp)"
trap 'rm -f "$tmp_body"' EXIT

http_code="$({
  curl -sS --max-time 5 -o "$tmp_body" -w '%{http_code}' \
    -H "x-local-[REDACTED_SECRET]" \
    "http://127.0.0.1:${LOCAL_DAEMON_PORT}/v1/local/identity"
} || true)"

if [[ "$http_code" != "200" ]]; then
  emit_not_ready
  exit 2
fi

cat "$tmp_body"


path: scripts/publish.sh
content: #!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PRECHECK="$SCRIPT_DIR/preflight.sh"

TITLE="${TITLE:-}"
BODY="${BODY:-}"
TAGS="${TAGS:-}"
PUBLISH_AT="${PUBLISH_AT:-}"
SETUP_URL="${SETUP_URL:-}"
PROOF_TOKEN="${PROOF_TOKEN:-}"
SETUP_ISSUE_URL="${SETUP_ISSUE_URL:-}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-$(hostname)-$(uname -s)-$(uname -m)}"
LOCAL_DAEMON_PORT="${LOCAL_DAEMON_PORT:-19090}"
X_LOCAL_TOKEN="${X_LOCAL_TOKEN:-${THIN_RUNNER_LOCAL_TOKEN:-}}"
OFFERING_ID="${OPENCLAW_OFFERING_ID:-naver-blog-writer}"
OFFERING_EXECUTE_URL="${OPENCLAW_OFFERING_EXECUTE_URL:-}"
OFFERING_API_KEY="${OPENCLAW_CORE_API_KEY:-}"
OFFERING_API_KEY_HEADER="${OPENCLAW_CORE_API_KEY_HEADER:-x-api-key}"
CONTROL_PLANE_URL="${CONTROL_PLANE_URL:-${ACP_RUNNER_SERVER_URL:-}}"
ADMIN_KEY_RAW="${ACP_ADMIN_API_KEY:-${ACP_CONTROL_PLANE_API_KEYS:-}}"
ADMIN_KEY="${ADMIN_KEY_RAW%%,*}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --title)
      TITLE="$2"
      shift 2
      ;;
    --body)
      BODY="$2"
      shift 2
      ;;
    --tags)
      TAGS="$2"
      shift 2
      ;;
    --publish-at)
      PUBLISH_AT="$2"
      shift 2
      ;;
    --setup-url)
      SETUP_URL="$2"
      shift 2
      ;;
    --local-daemon-port)
      LOCAL_DAEMON_PORT="$2"
      shift 2
      ;;
    --x-local-token)
      X_LOCAL_TOKEN="$2"
      shift 2
      ;;
    *)
      echo "unknown arg: $1" >&2
      exit 1
      ;;
  esac
done

if [[ -z "$TITLE" || -z "$BODY" ]]; then
  echo '{"error":"title and body are required"}'
  exit 1
fi

set +e
PRECHECK_OUTPUT="$($PRECHECK \
  --setup-url "$SETUP_URL" \
  --proof-token "$PROOF_TOKEN" \
  --setup-issue-url "$SETUP_ISSUE_URL" \
  --device-fingerprint "$DEVICE_FINGERPRINT" \
  --local-daemon-port "$LOCAL_DAEMON_PORT" \
  --x-local-token "$X_LOCAL_TOKEN" 2>&1)"
PRECHECK_CODE=$?
set -e

if [[ $PRECHECK_CODE -ne 0 ]]; then
  printf '%s\n' "$PRECHECK_OUTPUT"
  exit $PRECHECK_CODE
fi

identity_tmp="$(mktemp)"
seal_tmp="$(mktemp)"
exec_tmp="$(mktemp)"
trap 'rm -f "$identity_tmp" "$seal_tmp" "$exec_tmp"' EXIT

identity_code="$({
  curl -sS -o "$identity_tmp" -w '%{http_code}' \
    -H "x-local-[REDACTED_SECRET]" \
    "http://127.0.0.1:${LOCAL_DAEMON_PORT}/v1/local/identity"
} || true)"

if [[ "$identity_code" != "200" ]]; then
  cat "$identity_tmp"
  exit 1
fi

seal_payload="$(node - "$TITLE" "$BODY" "$TAGS" "$PUBLISH_AT" <<'NODE'
const title = process.argv[2] || '';
const body = process.argv[3] || '';
const tagsRaw = process.argv[4] || '';
const publishAt = process.argv[5] || '';
const tags = tagsRaw
  .split(',')
  .map((v) => v.trim())
  .filter((v) => v.length > 0);
const payload = {
  title,
  body_markdown: body,
  tags,
  publish_at: publishAt || null,
};
process.stdout.write(JSON.stringify(payload));
NODE
)"

seal_code="$({
  curl -sS -o "$seal_tmp" -w '%{http_code}' \
    -X POST \
    -H 'content-type: application/json' \
    -H "x-local-[REDACTED_SECRET]" \
    -d "$seal_payload" \
    "http://127.0.0.1:${LOCAL_DAEMON_PORT}/v1/local/seal-job"
} || true)"

if [[ "$seal_code" != "200" ]]; then
  cat "$seal_tmp"
  exit 1
fi

requirement_payload="$(node - "$(cat "$identity_tmp")" "$(cat "$seal_tmp")" <<'NODE'
const identity = JSON.parse(process.argv[2]);
const sealed = JSON.parse(process.argv[3]);
const req = {
  runner_attestation: identity.runner_attestation,
  sealed_payload: sealed.sealed_payload,
  payload_digest: sealed.payload_digest,
  idempotency_key: sealed.idempotency_key,
};
process.stdout.write(JSON.stringify(req));
NODE
)"

if [[ -n "$OFFERING_EXECUTE_URL" ]]; then
  execute_body="$(node - "$OFFERING_ID" "$requirement_payload" <<'NODE'
const offeringId = process.argv[2];
const requirements = JSON.parse(process.argv[3]);
process.stdout.write(JSON.stringify({
  offering_id: offeringId,
  requirements,
}));
NODE
)"

  if [[ -n "$OFFERING_API_KEY" ]]; then
    execute_code="$({
      curl -sS -o "$exec_tmp" -w '%{http_code}' \
        -X POST \
        -H 'content-type: application/json' \
        -H "${OFFERING_API_KEY_HEADER}: ${OFFERING_API_KEY}" \
        -d "$execute_body" \
        "$OFFERING_EXECUTE_URL"
    } || true)"
  else
    execute_code="$({
      curl -sS -o "$exec_tmp" -w '%{http_code}' \
        -X POST \
        -H 'content-type: application/json' \
        -d "$execute_body" \
        "$OFFERING_EXECUTE_URL"
    } || true)"
  fi

  if [[ "$execute_code" =~ ^2 ]]; then
    cat "$exec_tmp"
    exit 0
  fi

  cat "$exec_tmp"
  exit 1
fi

if [[ -z "$CONTROL_PLANE_URL" || -z "$ADMIN_KEY" ]]; then
  echo '{"error":"OPENCLAW_OFFERING_EXECUTE_URL not set and direct dispatch creds missing"}'
  exit 1
fi

dispatch_code="$({
  curl -sS -o "$exec_tmp" -w '%{http_code}' \
    -X POST \
    -H 'content-type: application/json' \
    -H "x-[REDACTED_SECRET]" \
    -d "$requirement_payload" \
    "${CONTROL_PLANE_URL%/}/v2/jobs/dispatch-and-wait"
} || true)"

if [[ "$dispatch_code" =~ ^2 ]]; then
  cat "$exec_tmp"
  exit 0
fi

cat "$exec_tmp"
exit 1


path: scripts/setup_runner.sh
content: #!/usr/bin/env bash
set -euo pipefail

SETUP_URL="${SETUP_URL:-}"
PROOF_TOKEN="${PROOF_TOKEN:-}"
SETUP_ISSUE_URL="${SETUP_ISSUE_URL:-}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-$(hostname)-$(uname -s)-$(uname -m)}"
AUTO_SERVICE="${AUTO_SERVICE:-both}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --setup-url)
      SETUP_URL="$2"
      shift 2
      ;;
    --proof-token)
      PROOF_TOKEN="$2"
      shift 2
      ;;
    --setup-issue-url)
      SETUP_ISSUE_URL="$2"
      shift 2
      ;;
    --device-fingerprint)
      DEVICE_FINGERPRINT="$2"
      shift 2
      ;;
    --auto-service)
      AUTO_SERVICE="$2"
      shift 2
      ;;
    *)
      echo "unknown arg: $1" >&2
      exit 1
      ;;
  esac
done

if [[ -z "$SETUP_URL" && ( -z "$PROOF_TOKEN" || -z "$SETUP_ISSUE_URL" ) ]]; then
  echo '{"error":"setup_url required or proof_token+setup_issue_url required"}'
  exit 1
fi

runner_cmd=(npx @y80163442/naver-thin-runner setup)

if [[ -n "$SETUP_URL" ]]; then
  runner_cmd+=(--setup-url "$SETUP_URL")
else
  runner_cmd+=(--proof-token "$PROOF_TOKEN" --setup-issue-url "$SETUP_ISSUE_URL" --device-fingerprint "$DEVICE_FINGERPRINT")
fi

runner_cmd+=(--auto-service "$AUTO_SERVICE")
"${runner_cmd[@]}"

echo '{"ok":true,"next_action":"RUN_LOGIN_ONCE","hint":"npx @y80163442/naver-thin-runner login"}'


path: scripts/smoke_test.sh
content: #!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"

echo "[1/3] preflight should return RUNNER_NOT_READY without local token"
set +e
OUTPUT="$(bash "$ROOT_DIR/scripts/preflight.sh" 2>&1)"
CODE=$?
set -e

if [[ $CODE -ne 2 ]]; then
  echo "[FAIL] expected exit code 2, got $CODE"
  echo "$OUTPUT"
  exit 1
fi

echo "$OUTPUT" | grep -q '"error": "RUNNER_NOT_READY"'

echo "[2/3] publish input validation"
set +e
OUT2="$(bash "$ROOT_DIR/scripts/publish.sh" --title "" --body "" 2>&1)"
CODE2=$?
set -e
if [[ $CODE2 -eq 0 ]]; then
  echo "[FAIL] publish should fail on empty title/body"
  exit 1
fi
echo "$OUT2" | grep -q 'title and body are required'

echo "[3/3] script syntax check"
bash -n "$ROOT_DIR/scripts/preflight.sh"
bash -n "$ROOT_DIR/scripts/setup_runner.sh"
bash -n "$ROOT_DIR/scripts/publish.sh"

echo "[OK] Smoke test passed"


Security scan verdicts — ClawScan: suspicious | static analysis:  | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)