#!/bin/bash # macOS installer. No hook trust changes, shared credentials, or local hook runtime. set -euo pipefail umask 077 version=1.2.0 origin=https://knowledge.cyberflo.ai codex_bin=/Applications/ChatGPT.app/Contents/Resources/codex install_root='' upgrade=0 old_root='' switched=0 access_origin=https://access.cyberflo.ai connect_access=0 login=0 release_dir='' while [ "$#" -gt 0 ]; do case "$1" in --codex) codex_bin="$2"; shift 2;; --install-root) install_root="$2"; shift 2;; --origin) origin="$2"; shift 2;; --no-login) login=0; shift;; --login) login=1; shift;; --connect-access) connect_access=1; shift;; --upgrade) upgrade=1; shift;; --version) version="$2"; shift 2;; --access-origin) access_origin="$2"; shift 2;; --release-dir) release_dir="$2"; shift 2;; *) echo 'Unknown option. Supported: --codex PATH --install-root PATH --origin HTTPS_ORIGIN --no-login --release-dir TRUSTED_LOCAL_RELEASE' >&2; exit 2;; esac done case "$version" in 1.0.0|1.1.0|1.2.0) ;; *) echo "Unknown published version" >&2; exit 2;; esac [ -n "$install_root" ] || install_root="$HOME/.codex/company-knowledge/$version" [ -x "$codex_bin" ] || { echo 'Select the desktop-bundled Codex executable with --codex. The shell alias is not sufficient.' >&2; exit 2; } client_version=$("$codex_bin" --version) [[ "$client_version" =~ ^codex-cli\ [0-9]+\.[0-9]+\.[0-9]+([+-][a-zA-Z0-9.-]+)?$ ]] || { echo 'The selected executable did not identify itself as Codex CLI. Nothing was configured.' >&2; exit 2; } case "$origin" in https://*) ;; *) echo 'HTTPS origin required.' >&2; exit 2;; esac authority=${origin#https://} case "$authority" in ''|*/*|*@*|*\?*|*\#*|*[[:space:]]*) echo 'Use an HTTPS origin without credentials or a path.' >&2; exit 2;; esac case "$access_origin" in https://*) ;; *) echo 'HTTPS access origin required.' >&2; exit 2;; esac access_authority=${access_origin#https://} case "$access_authority" in ''|*/*|*@*|*\?*|*\#*|*[[:space:]]*) echo 'Use a canonical Access HTTPS origin.' >&2; exit 2;; esac temp_dir=$(mktemp -d) cleanup() { result=$? if [ "$switched" = 1 ] && [ "$result" != 0 ] && [ -n "$old_root" ]; then "$codex_bin" plugin remove company-knowledge@company-knowledge-team --json > /dev/null 2>&1 || true "$codex_bin" plugin marketplace remove company-knowledge-team --json > /dev/null 2>&1 || true if "$codex_bin" plugin marketplace add "$old_root" --json > /dev/null && "$codex_bin" plugin add company-knowledge@company-knowledge-team --json > /dev/null; then echo 'Upgrade failed; the previous verified package was restored. Connections were retained.' >&2 else echo 'Upgrade failed and automatic package restoration needs attention. The previous verified directory was retained.' >&2 fi fi rm -rf "$temp_dir" } trap cleanup EXIT "$codex_bin" features list > "$temp_dir/features" hook_support=$(/usr/bin/awk '$1 == "hooks" { print $3 }' "$temp_dir/features") [ "$hook_support" = true ] || { echo 'This desktop client has no enabled hooks feature. Update the desktop or review its existing hook setting; the installer will not override it.' >&2; exit 3; } cat > "$temp_dir/inspect.js" <<'JXA' ObjC.import('Foundation'); function run(a) { function read(p) { return ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError(p, $.NSUTF8StringEncoding, null)); } var v = JSON.parse(read(a[1])); if (a[0] === 'connections' || a[0] === 'access-connections') { var connectionName = a[0] === 'connections' ? 'hive-read' : 'autopilot-access'; if (!Array.isArray(v)) throw Error('Unsupported MCP configuration response'); var named = v.filter(x => x.name === connectionName); var same = v.filter(x => x.transport && x.transport.url && x.transport.url.replace(/\/$/, '') === a[2]); if (named.length && (named[0].transport.url !== a[2] || named[0].enabled === false)) return 'conflict'; if (same.some(x => x.name !== connectionName)) return 'duplicate'; return named.length ? 'reuse' : 'add'; } if (a[0] === 'manifest') { if (v.version !== a[2] || v.workflow_version !== a[2] || !/^[a-f0-9]{64}$/.test(v.archive_sha256)) throw Error('Invalid release manifest'); return v.archive_sha256; } if (a[0] === 'marketplace') { var rows = (v.marketplaces || []).filter(x => x.name === 'company-knowledge-team'); return rows.length && rows[0].root !== a[2] ? rows[0].root : 'ok'; } } JXA # Keep configuration output private: it can contain unrelated credential headers. "$codex_bin" mcp list --json > "$temp_dir/mcp.json" connection=$(/usr/bin/osascript -l JavaScript "$temp_dir/inspect.js" connections "$temp_dir/mcp.json" "$origin/mcp") case "$connection" in conflict) echo 'The existing hive-read connection is disabled or points elsewhere. Resolve it in Codex settings; this installer did not overwrite it.' >&2; exit 3;; duplicate) echo 'This service already has a different connection name. Resolve that alias to hive-read before installing; no duplicate was added.' >&2; exit 3;; esac if [ "$connect_access" = 1 ]; then access_connection=$(/usr/bin/osascript -l JavaScript "$temp_dir/inspect.js" access-connections "$temp_dir/mcp.json" "$access_origin/mcp") case "$access_connection" in add|reuse) ;; *) echo 'Resolve the existing Access connection conflict; nothing was overwritten.' >&2; exit 3;; esac fi mkdir -p "$(dirname "$install_root")" install_root="$(cd "$(dirname "$install_root")" && pwd -P)/$(basename "$install_root")" "$codex_bin" plugin marketplace list --json > "$temp_dir/markets.json" market=$(/usr/bin/osascript -l JavaScript "$temp_dir/inspect.js" marketplace "$temp_dir/markets.json" "$install_root") if [ "$market" != ok ]; then [ "$upgrade" = 1 ] || { echo 'Another package is installed. Use --upgrade for a verified replacement; nothing was overwritten.' >&2; exit 3; } old_root="$market" [ -d "$old_root" ] && [ ! -L "$old_root" ] && [ -f "$old_root/.release-sha256" ] || { echo 'The previous package is not a verified release directory.' >&2; exit 4; } [ -z "$(/usr/bin/find "$old_root" -type l -print -quit)" ] || { echo 'Previous package contains a symbolic link; preserved.' >&2; exit 4; } fi if [ -n "$release_dir" ]; then cp "$release_dir/manifest.json" "$temp_dir/manifest.json" else /usr/bin/curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-time 30 "$origin/client/$version/manifest.json" -o "$temp_dir/manifest.json" fi expected=$(/usr/bin/osascript -l JavaScript "$temp_dir/inspect.js" manifest "$temp_dir/manifest.json" "$version") if [ -n "$release_dir" ]; then cp "$release_dir/company-knowledge.zip" "$temp_dir/package.zip" else /usr/bin/curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-time 60 "$origin/client/$version/company-knowledge.zip" -o "$temp_dir/package.zip" fi actual=$(/usr/bin/shasum -a 256 "$temp_dir/package.zip" | /usr/bin/awk '{print $1}') [ "$actual" = "$expected" ] || { echo 'Release integrity check failed; nothing was installed.' >&2; exit 4; } /usr/bin/unzip -Z1 "$temp_dir/package.zip" > "$temp_dir/entries" while IFS= read -r entry; do case "$entry" in /*|*../*|*\\*) echo 'Unsafe archive entry.' >&2; exit 4;; esac done < "$temp_dir/entries" mkdir "$temp_dir/unpacked" /usr/bin/unzip -q "$temp_dir/package.zip" -d "$temp_dir/unpacked" printf '%s\n' "$expected" > "$temp_dir/unpacked/.release-sha256" if [ -e "$install_root" ]; then [ ! -L "$install_root" ] && [ -z "$(/usr/bin/find "$install_root" -type l -print -quit)" ] || { echo 'Package contains a symbolic link; preserved.' >&2; exit 4; } [ -f "$install_root/.release-sha256" ] && [ "$(cat "$install_root/.release-sha256")" = "$expected" ] || { echo 'The installation directory contains a different release. It was preserved.' >&2; exit 4; } /usr/bin/diff -qr "$temp_dir/unpacked" "$install_root" > /dev/null || { echo 'The installed package differs from this release. It was preserved; review the difference before updating.' >&2; exit 4; } else mv "$temp_dir/unpacked" "$install_root" fi if [ -n "$old_root" ]; then old_version=$(/usr/bin/osascript -l JavaScript -e 'ObjC.import("Foundation"); function run(a) { return JSON.parse(ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError(a[0], $.NSUTF8StringEncoding, null))).version; }' "$old_root/plugins/company-knowledge/.codex-plugin/plugin.json") case "$old_version" in 1.0.0|1.1.0|1.2.0) ;; *) echo 'Unknown existing version; preserve it for review.' >&2; exit 4;; esac if [ -n "$release_dir" ]; then cp "$release_dir/../$old_version/manifest.json" "$temp_dir/old-manifest.json" cp "$release_dir/../$old_version/company-knowledge.zip" "$temp_dir/old-package.zip" else /usr/bin/curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-time 30 "$origin/client/$old_version/manifest.json" -o "$temp_dir/old-manifest.json" /usr/bin/curl --fail --silent --show-error --proto '=https' --tlsv1.2 --max-time 60 "$origin/client/$old_version/company-knowledge.zip" -o "$temp_dir/old-package.zip" fi old_expected=$(/usr/bin/osascript -l JavaScript "$temp_dir/inspect.js" manifest "$temp_dir/old-manifest.json" "$old_version") [ "$(/usr/bin/shasum -a 256 "$temp_dir/old-package.zip" | /usr/bin/awk '{print $1}')" = "$old_expected" ] || { echo 'Previous release verification failed.' >&2; exit 4; } mkdir "$temp_dir/old-verified" /usr/bin/unzip -q "$temp_dir/old-package.zip" -d "$temp_dir/old-verified" printf '%s\n' "$old_expected" > "$temp_dir/old-verified/.release-sha256" /usr/bin/diff -qr "$temp_dir/old-verified" "$old_root" > /dev/null || { echo 'The existing package was changed; it was preserved for review.' >&2; exit 4; } switched=1 "$codex_bin" plugin remove company-knowledge@company-knowledge-team --json > "$temp_dir/remove.json" "$codex_bin" plugin marketplace remove company-knowledge-team --json > "$temp_dir/remove-market.json" fi "$codex_bin" plugin marketplace add "$install_root" --json > "$temp_dir/add.json" "$codex_bin" plugin add company-knowledge@company-knowledge-team --json > "$temp_dir/plugin.json" if [ "$connection" = add ]; then "$codex_bin" mcp add hive-read --url "$origin/mcp" fi if [ "$login" = 1 ]; then "$codex_bin" mcp login hive-read --scopes hive.read,hive.work.append,hive.guidance.propose,hive.sources.import fi switched=0 if [ "$connect_access" = 1 ]; then case "$access_connection" in add) "$codex_bin" mcp add autopilot-access --url "$access_origin/mcp";; reuse) ;; *) echo 'Resolve the existing Autopilot Access alias/URL conflict; it was preserved.' >&2; exit 3;; esac fi echo 'Package installed. Setup is still pending: restart/open a fresh desktop task, review and trust the exact Autopilot hooks in /hooks, and complete the access_info + actual lifecycle check in the setup guide. Installation is not proof of readiness.' echo 'Existing authorization was reused. Verify access_info on hive-read and autopilot-access; log in only for missing capabilities or expired/revoked authorization. No database polling or business actions were enabled.'