diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0fc7928 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Verify release + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: Release tag to verify + required: true + type: string + +permissions: + contents: read + +jobs: + verify: + runs-on: macos-15 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 + persist-credentials: false + - name: Download and verify release assets + env: + RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }} + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "invalid release tag: $RELEASE_TAG" >&2 + exit 1 + fi + git fetch --force --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}" + if [ "$(git cat-file -t "refs/tags/${RELEASE_TAG}")" != "tag" ]; then + echo "release tag must be annotated" >&2 + exit 1 + fi + tag_object="$(git rev-parse "refs/tags/${RELEASE_TAG}")" + api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}" + ref_json="$(curl --fail --silent --show-error --location --retry 3 \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + --header 'Accept: application/vnd.github+json' \ + "${api}/git/ref/tags/${RELEASE_TAG}")" + if [ "$(jq -r '.object.type' <<<"$ref_json")" != "tag" ] || \ + [ "$(jq -r '.object.sha' <<<"$ref_json")" != "$tag_object" ]; then + echo "release tag does not match the annotated GitHub tag object" >&2 + exit 1 + fi + tag_json="$(curl --fail --silent --show-error --location --retry 3 \ + --header "Authorization: Bearer ${GITHUB_TOKEN}" \ + --header 'Accept: application/vnd.github+json' \ + "${api}/git/tags/${tag_object}")" + if [ "$(jq -r '.verification.verified' <<<"$tag_json")" != "true" ]; then + echo "release tag signature is not verified by GitHub" >&2 + exit 1 + fi + tag_commit="$(git rev-list -n 1 "refs/tags/${RELEASE_TAG}")" + if ! git merge-base --is-ancestor "$tag_commit" HEAD; then + echo "release tag is not reachable from protected default-branch history" >&2 + exit 1 + fi + version="${RELEASE_TAG#v}" + base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${RELEASE_TAG}" + archive="CrawlBar-v${version}-macos.zip" + checksum="${archive}.sha256" + curl --fail --location --retry 3 --output "$archive" "${base}/${archive}" + curl --fail --location --retry 3 --output "$checksum" "${base}/${checksum}" + expected="$(awk -v name="$archive" '$2 == name { print $1 }' "$checksum")" + if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then + echo "checksum asset does not bind exactly to $archive" >&2 + exit 1 + fi + actual="$(shasum -a 256 "$archive" | awk '{ print $1 }')" + test "$actual" = "$expected" + CRAWLBAR_EXPECTED_VERSION="$version" Scripts/verify_release.sh --require-notarized "$archive" diff --git a/.gitignore b/.gitignore index 641d657..baf2b14 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ DerivedData/ CrawlBar.app/ dist/ +.mac-release.env +.mac-release.local.env *.xcuserdata/ *.xcodeproj/ *.xcworkspace/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f831a16..26ebfb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog -## v0.4.1 - Unreleased +## v0.4.1 - 2026-07-09 + +### Changes + +- Distribute notarized universal macOS builds signed by the OpenClaw Foundation, including Homebrew installs, so the app identity persists across upgrades. + +### Fixes + +- Accept current `crawlkit/control` manifests when optional descriptions or branding metadata are omitted. ## v0.4.0 - 2026-06-19 diff --git a/README.md b/README.md index 40cc589..7323c7b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,13 @@ Scripts/package_app.sh The packaged `.app` is written to `dist/CrawlBar.app`. +Local and CI packages use ad-hoc signing and do not need release credentials. +Official release packages fail closed unless they use the OpenClaw Foundation +Developer ID identity. Runtime keychain and notarization profile locators belong +in the ignored `.mac-release.local.env`, never in committed configuration. + ## Releases Release notes are kept in [CHANGELOG.md](CHANGELOG.md). The packaged app bundle -version is generated by `Scripts/package_app.sh`. +version is read from `version.env`. Official artifacts are universal, hardened, +notarized, stapled, and verified by `Scripts/package_release.sh`. diff --git a/Scripts/package_app.sh b/Scripts/package_app.sh index eb3106c..804575c 100755 --- a/Scripts/package_app.sh +++ b/Scripts/package_app.sh @@ -5,35 +5,82 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DIST_DIR="$ROOT_DIR/dist" APP_DIR="$DIST_DIR/CrawlBar.app" STAGING_APP_DIR="$DIST_DIR/.CrawlBar.app.tmp.$$" +BUILD_DIR="${TMPDIR:-/tmp}/crawlbar-package.$$" CONTENTS_DIR="$STAGING_APP_DIR/Contents" MACOS_DIR="$CONTENTS_DIR/MacOS" HELPERS_DIR="$CONTENTS_DIR/Helpers" RESOURCES_DIR="$CONTENTS_DIR/Resources" +EXPECTED_IDENTITY="Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)" + +# shellcheck source=version.env +source "$ROOT_DIR/version.env" + +official_release="${CRAWLBAR_OFFICIAL_RELEASE:-0}" +universal_build="${CRAWLBAR_UNIVERSAL:-$official_release}" +signing_identity="${CRAWLBAR_CODESIGN_IDENTITY:-${MAC_RELEASE_CODESIGN_IDENTITY:-}}" + +if [ "$official_release" = "1" ] && [ "$signing_identity" != "$EXPECTED_IDENTITY" ]; then + echo "official release requires signing identity: $EXPECTED_IDENTITY" >&2 + exit 1 +fi cd "$ROOT_DIR" -swift build -c release --product CrawlBar >&2 -swift build -c release --product crawlbarctl >&2 +mkdir -p "$DIST_DIR" + +if [ "$universal_build" = "1" ]; then + for arch in arm64 x86_64; do + swift build \ + -c release \ + --triple "${arch}-apple-macosx14.0" \ + --scratch-path "$BUILD_DIR/$arch" \ + --product CrawlBar >&2 + swift build \ + -c release \ + --triple "${arch}-apple-macosx14.0" \ + --scratch-path "$BUILD_DIR/$arch" \ + --product crawlbarctl >&2 + done + arm_release="$BUILD_DIR/arm64/arm64-apple-macosx/release" + intel_release="$BUILD_DIR/x86_64/x86_64-apple-macosx/release" + resource_bundle="$arm_release/CrawlBar_CrawlBar.bundle" +else + swift build -c release --product CrawlBar >&2 + swift build -c release --product crawlbarctl >&2 + native_release="$ROOT_DIR/.build/release" + resource_bundle="$native_release/CrawlBar_CrawlBar.bundle" +fi rm -rf "$STAGING_APP_DIR" -trap 'rm -rf "$STAGING_APP_DIR"' EXIT +trap 'rm -rf "$STAGING_APP_DIR" "$BUILD_DIR"' EXIT mkdir -p "$MACOS_DIR" "$HELPERS_DIR" "$RESOURCES_DIR" -cp ".build/release/CrawlBar" "$MACOS_DIR/CrawlBar" -cp ".build/release/crawlbarctl" "$HELPERS_DIR/crawlbar" -RESOURCE_BUNDLE=".build/release/CrawlBar_CrawlBar.bundle" -if [ -d "$RESOURCE_BUNDLE" ]; then - cp -R "$RESOURCE_BUNDLE" "$RESOURCES_DIR/CrawlBar_CrawlBar.bundle" +if [ "$universal_build" = "1" ]; then + lipo -create \ + "$arm_release/CrawlBar" \ + "$intel_release/CrawlBar" \ + -output "$MACOS_DIR/CrawlBar" + lipo -create \ + "$arm_release/crawlbarctl" \ + "$intel_release/crawlbarctl" \ + -output "$HELPERS_DIR/crawlbar" +else + cp "$native_release/CrawlBar" "$MACOS_DIR/CrawlBar" + cp "$native_release/crawlbarctl" "$HELPERS_DIR/crawlbar" +fi + +if [ -d "$resource_bundle" ]; then + cp -R "$resource_bundle" "$RESOURCES_DIR/CrawlBar_CrawlBar.bundle" if ! find "$RESOURCES_DIR/CrawlBar_CrawlBar.bundle" -type f -print -quit | grep -q .; then - echo "SwiftPM resource bundle is empty: $RESOURCE_BUNDLE" >&2 + echo "SwiftPM resource bundle is empty: $resource_bundle" >&2 exit 1 fi else - echo "missing SwiftPM resource bundle: $RESOURCE_BUNDLE" >&2 + echo "missing SwiftPM resource bundle: $resource_bundle" >&2 exit 1 fi Scripts/generate_app_icon.swift "$RESOURCES_DIR/CrawlBar.icns" -cat > "$CONTENTS_DIR/Info.plist" <<'PLIST' +cat > "$CONTENTS_DIR/Info.plist" < @@ -49,9 +96,9 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST' CFBundlePackageType APPL CFBundleShortVersionString - 0.4.0 + $CRAWLBAR_VERSION CFBundleVersion - 1 + $CRAWLBAR_BUILD_NUMBER LSMinimumSystemVersion 14.0 LSUIElement @@ -62,7 +109,11 @@ cat > "$CONTENTS_DIR/Info.plist" <<'PLIST' PLIST -if command -v codesign >/dev/null 2>&1; then +if [ "$official_release" = "1" ]; then + codesign --force --options runtime --timestamp --sign "$signing_identity" "$HELPERS_DIR/crawlbar" + codesign --force --options runtime --timestamp --sign "$signing_identity" "$STAGING_APP_DIR" + "$ROOT_DIR/Scripts/verify_release.sh" "$STAGING_APP_DIR" +elif command -v codesign >/dev/null 2>&1; then codesign --force --deep --sign - "$STAGING_APP_DIR" >/dev/null fi diff --git a/Scripts/package_release.sh b/Scripts/package_release.sh new file mode 100755 index 0000000..c8257ae --- /dev/null +++ b/Scripts/package_release.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIST_DIR="$ROOT_DIR/dist" + +# shellcheck source=version.env +source "$ROOT_DIR/version.env" + +tag="v$CRAWLBAR_VERSION" +if [ "$(git -C "$ROOT_DIR" describe --tags --exact-match HEAD 2>/dev/null || true)" != "$tag" ]; then + echo "release must run from exact tag $tag" >&2 + exit 1 +fi +if [ -n "$(git -C "$ROOT_DIR" status --porcelain)" ]; then + echo "release requires a clean checkout" >&2 + exit 1 +fi + +if [ -z "${NOTARYTOOL_KEYCHAIN_PROFILE:-}" ]; then + echo "NOTARYTOOL_KEYCHAIN_PROFILE is required at runtime" >&2 + exit 1 +fi + +export CRAWLBAR_OFFICIAL_RELEASE=1 +"$ROOT_DIR/Scripts/package_app.sh" >/dev/null + +app="$DIST_DIR/CrawlBar.app" +archive="$DIST_DIR/CrawlBar-v$CRAWLBAR_VERSION-macos.zip" +checksum="$archive.sha256" + +create_archive() { + rm -f "$archive" + ( + cd "$DIST_DIR" + COPYFILE_DISABLE=1 /usr/bin/zip -q -r -X "$(basename "$archive")" "$(basename "$app")" + ) +} + +rm -f "$checksum" +create_archive +xcrun notarytool submit "$archive" --keychain-profile "$NOTARYTOOL_KEYCHAIN_PROFILE" --wait +xcrun stapler staple "$app" +xcrun stapler validate "$app" + +create_archive +"$ROOT_DIR/Scripts/verify_release.sh" --require-notarized "$archive" + +(cd "$DIST_DIR" && shasum -a 256 "$(basename "$archive")" > "$(basename "$checksum")") +echo "$archive" +echo "$checksum" diff --git a/Scripts/verify_release.sh b/Scripts/verify_release.sh new file mode 100755 index 0000000..36dafd5 --- /dev/null +++ b/Scripts/verify_release.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +EXPECTED_IDENTITY="Developer ID Application: OpenClaw Foundation (FWJYW4S8P8)" +EXPECTED_TEAM="FWJYW4S8P8" +EXPECTED_BUNDLE_ID="com.vincentkoc.CrawlBar" +require_notarized=0 + +if [ "${1:-}" = "--require-notarized" ]; then + require_notarized=1 + shift +fi + +artifact="${1:-}" +if [ -z "$artifact" ]; then + echo "usage: $0 [--require-notarized] " >&2 + exit 2 +fi + +work_dir="" +app_path="$artifact" +if [ "${artifact##*.}" = "zip" ]; then + archive_entries="$(zipinfo -1 "$artifact")" + if ! grep -Fqx 'CrawlBar.app/' <<<"$archive_entries"; then + echo "release archive is missing the CrawlBar.app root" >&2 + exit 1 + fi + while IFS= read -r entry; do + case "$entry" in + CrawlBar.app/ | CrawlBar.app/*) ;; + *) + echo "unexpected release archive member: $entry" >&2 + exit 1 + ;; + esac + entry_path="${entry%/}" + case "/$entry_path/" in + */../* | */./* | *//* | *\\*) + echo "unsafe release archive member: $entry" >&2 + exit 1 + ;; + esac + done <<<"$archive_entries" + duplicate_entry="$(LC_ALL=C sort <<<"$archive_entries" | uniq -d | head -n 1)" + if [ -n "$duplicate_entry" ]; then + echo "duplicate release archive member: $duplicate_entry" >&2 + exit 1 + fi + + work_dir="$(mktemp -d "${TMPDIR:-/tmp}/crawlbar-verify.XXXXXX")" + trap 'rm -rf "$work_dir"' EXIT + ditto -x -k "$artifact" "$work_dir" + app_path="$work_dir/CrawlBar.app" + unexpected_path="$(find "$work_dir" -mindepth 1 -maxdepth 1 ! -name 'CrawlBar.app' -print -quit)" + unsafe_path="$(find "$app_path" ! -type d ! -type f -print -quit 2>/dev/null || true)" + if [ -n "$unexpected_path" ]; then + echo "unexpected extracted release path: $unexpected_path" >&2 + exit 1 + fi + if [ -n "$unsafe_path" ]; then + echo "release archive contains an unsafe link or special file: $unsafe_path" >&2 + exit 1 + fi +fi + +if [ ! -d "$app_path" ]; then + echo "missing CrawlBar.app in artifact: $artifact" >&2 + exit 1 +fi + +# shellcheck source=version.env +source "$ROOT_DIR/version.env" +expected_version="${CRAWLBAR_EXPECTED_VERSION:-$CRAWLBAR_VERSION}" + +plist="$app_path/Contents/Info.plist" +helper="$app_path/Contents/Helpers/crawlbar" +if [ ! -f "$plist" ] || [ ! -f "$app_path/Contents/MacOS/CrawlBar" ] || [ ! -f "$helper" ]; then + echo "release archive does not contain the exact CrawlBar app layout" >&2 + exit 1 +fi +bundle_id="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$plist")" +version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist")" +if [ "$bundle_id" != "$EXPECTED_BUNDLE_ID" ]; then + echo "unexpected bundle identifier: $bundle_id" >&2 + exit 1 +fi +if [ "$version" != "$expected_version" ]; then + echo "unexpected bundle version: $version" >&2 + exit 1 +fi + +codesign --verify --deep --strict --verbose=2 "$app_path" +for signed_path in "$app_path" "$helper"; do + signature="$(codesign -d --verbose=4 "$signed_path" 2>&1)" + grep -Fqx "Authority=$EXPECTED_IDENTITY" <<<"$signature" + grep -Fqx "TeamIdentifier=$EXPECTED_TEAM" <<<"$signature" + grep -Eq '^CodeDirectory .* flags=.*\(runtime\)' <<<"$signature" +done + +app_signature="$(codesign -d --verbose=4 "$app_path" 2>&1)" +grep -Fqx "Identifier=$EXPECTED_BUNDLE_ID" <<<"$app_signature" + +for executable in "$app_path/Contents/MacOS/CrawlBar" "$helper"; do + architectures="$(lipo -archs "$executable")" + grep -qw arm64 <<<"$architectures" + grep -qw x86_64 <<<"$architectures" +done + +if [ "$require_notarized" = "1" ]; then + spctl --assess --type execute --verbose=4 "$app_path" + xcrun stapler validate "$app_path" + syspolicy_check distribution "$app_path" +fi + +echo "verified CrawlBar $version: $EXPECTED_BUNDLE_ID, $EXPECTED_TEAM, universal, hardened runtime" diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift index 7d34117..097ba26 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift @@ -79,11 +79,11 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { self.schemaVersion = Self.decodeSchemaVersion(from: container) self.id = try container.decode(CrawlAppID.self, forKey: .id) self.displayName = try container.decode(String.self, forKey: .displayName) - self.description = try container.decode(String.self, forKey: .description) + self.description = try container.decodeIfPresent(String.self, forKey: .description) ?? "" self.availability = try container.decodeIfPresent(CrawlAppManifest.Availability.self, forKey: .availability) ?? .available self.binary = try container.decode(CrawlAppManifest.Binary.self, forKey: .binary) self.execution = try container.decodeIfPresent(CrawlAppManifest.Execution.self, forKey: .execution) - self.branding = try container.decode(CrawlAppManifest.Branding.self, forKey: .branding) + self.branding = try container.decodeIfPresent(CrawlAppManifest.Branding.self, forKey: .branding) ?? .init() self.paths = try container.decode(CrawlAppManifest.Paths.self, forKey: .paths) self.commands = try Self.decodeCommands(from: container, binaryName: self.binary.name) self.capabilities = Self.decodeCapabilities(from: container, commands: self.commands) diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifestBranding.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifestBranding.swift index 4360ae2..4430ae8 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifestBranding.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifestBranding.swift @@ -8,8 +8,8 @@ public extension CrawlAppManifest { public var bundleIdentifier: String? public init( - symbolName: String, - accentColor: String, + symbolName: String = "", + accentColor: String = "#6E6E73", iconPath: String? = nil, bundleIdentifier: String? = nil) { @@ -25,5 +25,13 @@ public extension CrawlAppManifest { case iconPath = "icon_path" case bundleIdentifier = "bundle_identifier" } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.symbolName = try container.decodeIfPresent(String.self, forKey: .symbolName) ?? "" + self.accentColor = try container.decodeIfPresent(String.self, forKey: .accentColor) ?? "#6E6E73" + self.iconPath = try container.decodeIfPresent(String.self, forKey: .iconPath) + self.bundleIdentifier = try container.decodeIfPresent(String.self, forKey: .bundleIdentifier) + } } } diff --git a/Sources/CrawlBarSelfTest/SelfTestConfig.swift b/Sources/CrawlBarSelfTest/SelfTestConfig.swift index fdc2eb0..c713edf 100644 --- a/Sources/CrawlBarSelfTest/SelfTestConfig.swift +++ b/Sources/CrawlBarSelfTest/SelfTestConfig.swift @@ -84,9 +84,8 @@ extension CrawlBarSelfTest { "schema_version": "crawlkit.control.v1", "id": "objectcrawl", "display_name": "Object Crawl", - "description": "A crawlkit manifest", "binary": {"name": "objectcrawl"}, - "branding": {"symbol_name": "tray", "accent_color": "#123456"}, + "branding": {}, "paths": {"default_config": "~/.objectcrawl/config.toml"}, "commands": { "status": {"title": "Status", "argv": ["objectcrawl", "status", "--json"], "json": true}, @@ -127,6 +126,8 @@ extension CrawlBarSelfTest { throw SelfTestError.failed("crawlkit command-object manifests load from disk") } try Self.expect(objectManifest.commands["status"] == ["status", "--json"], "crawlkit command argv strips binary") + try Self.expect(objectManifest.description.isEmpty, "crawlkit optional description can be omitted") + try Self.expect(objectManifest.branding.accentColor == "#6E6E73", "crawlkit optional branding uses a neutral fallback") try Self.expect(objectManifest.commands["query"] == ["--json", "sql"], "crawlkit query sample SQL is stripped") try Self.expect(objectManifest.commands["refresh"] == ["sync", "--json"], "crawlkit sync command aliases to refresh") try Self.expect(objectManifest.commands["desktop-cache-import"] == ["tap", "--json"], "crawlkit tap command aliases to desktop cache import") diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 1f8b03e..b4077e4 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -1,19 +1,18 @@ # CrawlBar Control Protocol CrawlBar treats each crawler as a local CLI with a small control contract. -Today that contract lives in CrawlBar manifests and adapters. The cleaner -long-term home is `crawlkit`, because the Go CLIs already share config, -status, output, desktop-cache, pack, and git-share infrastructure there. +The canonical Go contract lives in `crawlkit/control`; CrawlBar also accepts +its legacy array-valued manifests for compatibility. ## Manifest A crawler can be built in or represented by a manifest JSON file under -`~/.crawlbar/apps`. Once `crawlkit` grows a control package, each crawler should -also expose the same payload through `metadata --json`. +`~/.crawlbar/apps`. CrawlKit-based crawlers should expose the same payload +through `metadata --json`. ```json { - "schema_version": 1, + "schema_version": "crawlkit.control.v1", "id": "examplecrawl", "display_name": "Example Crawl", "description": "Local archive for Example", @@ -27,47 +26,17 @@ also expose the same payload through `metadata --json`. "default_share": "~/.examplecrawl/share" }, "commands": { - "metadata": ["metadata", "--json"], - "status": ["status", "--json"], - "doctor": ["doctor", "--json"], - "refresh": ["sync", "--json"], - "publish": ["publish", "--json"], - "update": ["update", "--json"], - "remote-status": ["remote", "status", "--json"], - "remote-archives": ["remote", "archives", "--json"], - "cloud-publish": ["cloud", "publish", "--json"] + "metadata": {"argv": ["examplecrawl", "metadata", "--json"], "json": true}, + "status": {"argv": ["examplecrawl", "status", "--json"], "json": true}, + "doctor": {"argv": ["examplecrawl", "doctor", "--json"], "json": true}, + "sync": {"argv": ["examplecrawl", "sync", "--json"], "json": true, "mutates": true}, + "publish": {"argv": ["examplecrawl", "publish", "--json"], "json": true, "mutates": true}, + "update": {"argv": ["examplecrawl", "update", "--json"], "json": true, "mutates": true}, + "remote-status": {"argv": ["examplecrawl", "remote", "status", "--json"], "json": true}, + "remote-archives": {"argv": ["examplecrawl", "remote", "archives", "--json"], "json": true}, + "cloud-publish": {"argv": ["examplecrawl", "cloud", "publish", "--json"], "json": true, "mutates": true} }, - "capabilities": ["status", "doctor", "refresh", "publish", "update", "remote_archive", "cloud_publish"], - "config_options": [ - { - "id": "api_token", - "label": "API token", - "kind": "secret", - "env_var": "EXAMPLECRAWL_TOKEN", - "config_key": "example.token" - }, - { - "id": "embedding_model", - "label": "Embedding model", - "kind": "choice", - "default_value": "text-embedding-3-small", - "choices": ["text-embedding-3-small", "text-embedding-3-large"], - "env_var": "OPENAI_EMBEDDING_MODEL", - "config_key": "embeddings.model" - } - ], - "config_sections": [ - { - "id": "access", - "title": "Example Access", - "option_ids": ["api_token"] - }, - { - "id": "ai", - "title": "Embeddings", - "option_ids": ["embedding_model"] - } - ], + "capabilities": ["status", "doctor", "sync", "publish", "update", "remote_archive", "cloud_publish"], "privacy": { "contains_private_messages": false, "exports_secrets": false, @@ -89,7 +58,7 @@ CrawlBar accepts varied JSON, then normalizes known fields into one status model Unknown fields are allowed. The app should not break when a crawler adds extra data. -The preferred future shape is a `crawlkit`-owned status envelope with: +The canonical `crawlkit/control.Status` envelope has: - `app_id`, `schema_version`, `generated_at`, `state`, and `summary`. - normalized counters as `{id,label,value}` rows. @@ -104,9 +73,11 @@ The preferred future shape is a `crawlkit`-owned status envelope with: ## Configuration -`config_options` describe editable values. `config_sections` only arrange those -fields into native settings groups. Duplicate option IDs are ignored after the -first entry so a broken external manifest cannot crash the settings UI. +CrawlBar file manifests can additionally include `config_options` describing +editable values and `config_sections` arranging them into native settings +groups. These are CrawlBar extensions, not fields in the current +`crawlkit/control.Manifest`. Duplicate option IDs are ignored after the first +entry so a broken external manifest cannot crash the settings UI. Secrets must never be emitted by `metadata --json`, and config reads should redact them unless an explicit reveal flag is provided. Longer term, crawler @@ -115,7 +86,8 @@ editing TOML directly. ## Actions -Actions are manifest command arrays. CrawlBar does not shell-expand them. +Canonical actions are manifest command objects containing an `argv` array. +CrawlBar also accepts its legacy array-only form and never shell-expands either. - `status` should be fast and read-only. - `doctor` may inspect auth/config and should avoid writes unless the crawler already defines that behavior. diff --git a/version.env b/version.env new file mode 100644 index 0000000..2769400 --- /dev/null +++ b/version.env @@ -0,0 +1,2 @@ +CRAWLBAR_VERSION=0.4.1 +CRAWLBAR_BUILD_NUMBER=401