From 441d859afe1d74d036c39367dce7d60491737441 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Sat, 10 Oct 2026 17:21:03 +1100 Subject: [PATCH] Phase 1: file one Paperclip job from a script Add scripts/paperclip-roundtrip.sh. It files one Paperclip issue and prints the returned id and identifier. The key comes from the environment, then from environment.d. The script never prints the key. Fix the runbook: the create route is POST /api/companies/{companyId}/issues. The old POST /api/issues route returns 404. Record the first filed job: AUD-55 (d58891ce-9934-4710-9bba-d37974251ed9). Co-Authored-By: Paperclip --- .gitignore | 1 + docs/outline/04-agent-runbook.md | 19 +++- scripts/paperclip-roundtrip.sh | 182 +++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 6 deletions(-) create mode 100755 scripts/paperclip-roundtrip.sh diff --git a/.gitignore b/.gitignore index a82649d..bec5044 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env *.visual-check.* +.state/ diff --git a/docs/outline/04-agent-runbook.md b/docs/outline/04-agent-runbook.md index c6701f7..1db1db5 100644 --- a/docs/outline/04-agent-runbook.md +++ b/docs/outline/04-agent-runbook.md @@ -19,7 +19,11 @@ result that returns. `Hub`. - Note: `POST /api/agents/{id}/keys` is **board-only**. A run JWT gets `403 Board access required`. Mint keys with the local board key. -2. File one job with `curl`. Use the issue-create endpoint. +2. File one job with the round-trip script. The script uses the issue-create + endpoint: `POST /api/companies/{companyId}/issues`. + - The old `POST /api/issues` route does not exist. It returns `404`. + - Company id: `d4921013-e5da-4d88-8016-57d5746e2d03`. + - Script: `scripts/paperclip-roundtrip.sh`. 3. Confirm the job appears in the Paperclip board. 4. Wait for the agent to close the job. 5. Send the result to Jervis `POST /announce` so the house speaks it. @@ -28,11 +32,14 @@ result that returns. ### Commands ```bash -export PAPERCLIP_API_KEY= -curl -s -X POST http://192.168.20.13:3100/api/issues \ - -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"title":"round-trip test","description":"prove the return path"}' +# File one job with the script. It reads PAPERCLIP_API_KEY from the +# environment, then from ~/.config/environment.d/10-secrets.conf. +scripts/paperclip-roundtrip.sh \ + --title "Hub round-trip job: confirm the return path" \ + --description "Prove the outbound Paperclip path." \ + --assignee 403de1f7-645c-4d35-bc7e-19b01d77bacc \ + --priority high \ + --project 7ae791bb-8d9a-462f-99bf-013421d20623 # Speak the result. curl -s -X POST http://192.168.20.13:8501/announce \ diff --git a/scripts/paperclip-roundtrip.sh b/scripts/paperclip-roundtrip.sh new file mode 100755 index 0000000..466e3a1 --- /dev/null +++ b/scripts/paperclip-roundtrip.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# +# paperclip-roundtrip.sh - file one Paperclip job (issue) from the command line. +# +# This is the outbound half of the Central AI Hub Phase 1 round trip. +# It proves that a script can file a job to Paperclip and get an identifier back. +# See hub_brief.md (build order step 1) and docs/outline/04-agent-runbook.md. +# +# The script never prints or commits the API key. It reads the key from the +# environment, then from the environment.d secrets file. +# +# Usage: +# scripts/paperclip-roundtrip.sh [options] +# +# Options: +# -t, --title TITLE Job title. +# -d, --description TEXT Job description text. +# -f, --description-file F Read the description from file F ("-" is stdin). +# -a, --assignee AGENT_ID Assign the job to this agent id. +# -p, --priority LEVEL backlog|low|medium|high|critical (default: medium). +# -P, --project PROJECT_ID Put the job in this project. +# --parent ISSUE_ID Make the job a child of this issue id. +# --idempotency-key KEY Idempotency key. A retry with the same key is safe. +# --json Print the raw API response after the summary. +# -h, --help Show this help. +# +# Environment: +# PAPERCLIP_API_KEY Required. Read from the environment first, then from +# ${PAPERCLIP_SECRETS_FILE:-~/.config/environment.d/10-secrets.conf}. +# PAPERCLIP_BASE_URL Paperclip base URL. Default: http://192.168.20.13:3100 +# PAPERCLIP_COMPANY_ID Company id. Default: the Hub company. +# PAPERCLIP_STATE_DIR Where the filed-issue record is written. +# Default: /.state +# +# Exit codes: +# 0 job filed +# 1 usage or configuration error +# 2 Paperclip API error +# +set -euo pipefail + +readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly PROJECT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)" + +readonly DEFAULT_BASE_URL="http://192.168.20.13:3100" +readonly DEFAULT_COMPANY_ID="d4921013-e5da-4d88-8016-57d5746e2d03" +readonly DEFAULT_TITLE="Hub round-trip job: confirm the return path" +readonly DEFAULT_DESCRIPTION="Prove the outbound Paperclip path: a script files a job and reads back the identifier." + +BASE_URL="${PAPERCLIP_BASE_URL:-${PAPERCLIP_BOARD_URL:-$DEFAULT_BASE_URL}}" +COMPANY_ID="${PAPERCLIP_COMPANY_ID:-$DEFAULT_COMPANY_ID}" +SECRETS_FILE="${PAPERCLIP_SECRETS_FILE:-$HOME/.config/environment.d/10-secrets.conf}" +STATE_DIR="${PAPERCLIP_STATE_DIR:-$PROJECT_DIR/.state}" + +TITLE="$DEFAULT_TITLE" +DESCRIPTION="$DEFAULT_DESCRIPTION" +DESCRIPTION_FILE="" +ASSIGNEE="" +PRIORITY="medium" +PROJECT_ID="" +PARENT="" +IDEMPOTENCY_KEY="" +PRINT_JSON=0 + +usage() { + awk 'NR>2 && /^#/ { sub(/^# ?/, ""); print } NR>2 && !/^#/ { exit }' "${BASH_SOURCE[0]}" +} + +die() { + echo "error: $*" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--title) TITLE="${2:?missing value for $1}"; shift 2 ;; + -d|--description) DESCRIPTION="${2:?missing value for $1}"; shift 2 ;; + -f|--description-file) DESCRIPTION_FILE="${2:?missing value for $1}"; shift 2 ;; + -a|--assignee) ASSIGNEE="${2:?missing value for $1}"; shift 2 ;; + -p|--priority) PRIORITY="${2:?missing value for $1}"; shift 2 ;; + -P|--project) PROJECT_ID="${2:?missing value for $1}"; shift 2 ;; + --parent) PARENT="${2:?missing value for $1}"; shift 2 ;; + --idempotency-key) IDEMPOTENCY_KEY="${2:?missing value for $1}"; shift 2 ;; + --json) PRINT_JSON=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $1 (use --help)" ;; + esac +done + +case "$PRIORITY" in + backlog|low|medium|high|critical) ;; + *) die "invalid priority: $PRIORITY" ;; +esac + +# --- Read the description from a file when asked. ----------------------------- +if [[ -n "$DESCRIPTION_FILE" ]]; then + if [[ "$DESCRIPTION_FILE" == "-" ]]; then + DESCRIPTION="$(cat)" + else + [[ -r "$DESCRIPTION_FILE" ]] || die "cannot read description file: $DESCRIPTION_FILE" + DESCRIPTION="$(cat -- "$DESCRIPTION_FILE")" + fi +fi + +# --- Resolve the API key. Never print it. ------------------------------------- +if [[ -z "${PAPERCLIP_API_KEY:-}" ]]; then + if [[ -r "$SECRETS_FILE" ]]; then + PAPERCLIP_API_KEY="$(awk -F= '/^PAPERCLIP_API_KEY=/{sub(/^PAPERCLIP_API_KEY=/,""); print; exit}' "$SECRETS_FILE")" + fi +fi +[[ -n "${PAPERCLIP_API_KEY:-}" ]] || die "PAPERCLIP_API_KEY is not set and not in $SECRETS_FILE" + +command -v curl >/dev/null 2>&1 || die "curl is required" +command -v jq >/dev/null 2>&1 || die "jq is required" + +BASE_URL="${BASE_URL%/}" + +# --- Check the board is reachable. -------------------------------------------- +if ! curl -sS -m 10 -o /dev/null "$BASE_URL/health"; then + echo "error: Paperclip is not reachable at $BASE_URL/health" >&2 + exit 2 +fi + +# --- Build the request body. -------------------------------------------------- +BODY="$(jq -n \ + --arg title "$TITLE" \ + --arg description "$DESCRIPTION" \ + --arg priority "$PRIORITY" \ + --arg projectId "$PROJECT_ID" \ + --arg assigneeAgentId "$ASSIGNEE" \ + --arg parentId "$PARENT" \ + --arg idempotencyKey "$IDEMPOTENCY_KEY" \ + '{title: $title, description: $description, priority: $priority} + + (if $projectId != "" then {projectId: $projectId} else {} end) + + (if $assigneeAgentId != "" then {assigneeAgentId: $assigneeAgentId} else {} end) + + (if $parentId != "" then {parentId: $parentId} else {} end) + + (if $idempotencyKey != "" then {idempotencyKey: $idempotencyKey} else {} end)')" + +TMP="$(mktemp)" +trap 'rm -f "$TMP"' EXIT + +HEADERS=(-H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json") +if [[ -n "${PAPERCLIP_RUN_ID:-}" ]]; then + HEADERS+=(-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID") +fi + +HTTP_CODE="$(curl -sS -o "$TMP" -w '%{http_code}' -X POST \ + "${HEADERS[@]}" \ + --data "$BODY" \ + "$BASE_URL/api/companies/$COMPANY_ID/issues")" || die "curl failed to reach $BASE_URL" + +if [[ "$HTTP_CODE" != "200" && "$HTTP_CODE" != "201" ]]; then + echo "error: Paperclip returned HTTP $HTTP_CODE" >&2 + cat "$TMP" >&2 + echo >&2 + exit 2 +fi + +ISSUE_ID="$(jq -r '(.issue // .).id // empty' "$TMP")" +IDENTIFIER="$(jq -r '(.issue // .).identifier // empty' "$TMP")" +[[ -n "$ISSUE_ID" ]] || die "Paperclip response has no issue id" + +# --- Record the result. ------------------------------------------------------- +mkdir -p "$STATE_DIR" +printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$ISSUE_ID" "$IDENTIFIER" \ + >> "$STATE_DIR/paperclip-roundtrip-issues.log" + +PREFIX="${IDENTIFIER%%-*}" +echo "Filed Paperclip job" +echo " title: $TITLE" +echo " priority: $PRIORITY" +echo " issue id: $ISSUE_ID" +echo " identifier: $IDENTIFIER" +if [[ "$PREFIX" != "$IDENTIFIER" && -n "$PREFIX" ]]; then + echo " board url: $BASE_URL/$PREFIX/issues/$IDENTIFIER" +fi +echo " record: $STATE_DIR/paperclip-roundtrip-issues.log" + +if [[ "$PRINT_JSON" == "1" ]]; then + echo + jq . "$TMP" +fi