diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07bbb55..afd8ab6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,18 +70,19 @@ jobs: mode: firewall-free - run: sfw bun install --frozen-lockfile - name: Build workspace packages - run: bun run build + run: | + cd packages/agent && bun run build + cd ../cli && bun run build + (cd ../pi-tps-mail && bun run build) || true - name: Build ${TARGET} env: TARGET: ${{ matrix.target }} PKG: ${{ matrix.pkg }} ART: ${{ matrix.artifact }} run: | - PKG_VERSION=$(node -p "require('./packages/cli/package.json').version") - bun build --compile ./packages/cli/bin/tps.ts \ - --target=${TARGET} \ - --define "INJECTED_VERSION=\"$PKG_VERSION\"" \ - --outfile=packages/cli/dist/${ART} + bun run --filter @tpsdev-ai/cli build:binary:${TARGET#bun-} + # The build script outputs to dist/tps-, copy to artifact name + cp packages/cli/dist/tps-${TARGET#bun-} packages/cli/dist/${ART} sha256sum packages/cli/dist/${ART} > packages/cli/dist/${ART}.sha256 - name: Stage binary env: @@ -113,10 +114,15 @@ jobs: with: mode: firewall-free - run: sfw bun install --frozen-lockfile + - name: Build workspace packages + run: | + cd packages/agent && bun run build + cd ../cli && bun run build + (cd ../pi-tps-mail && bun run build) || true - name: Build linux-x64 release binary run: | - PKG_VERSION=$(node -p "require('./packages/cli/package.json').version") - bun build --compile ./packages/cli/bin/tps.ts --target=bun-linux-x64 --define "INJECTED_VERSION=\"$PKG_VERSION\"" --outfile=/tmp/tps-release-smoke + bun run --filter @tpsdev-ai/cli build:binary:linux-x64 + cp packages/cli/dist/tps-linux-x64 /tmp/tps-release-smoke chmod +x /tmp/tps-release-smoke - name: Smoke test --version run: | @@ -126,11 +132,36 @@ jobs: echo "❌ version mismatch: binary=$VERSION_OUTPUT package=$PKG_VERSION" exit 1 fi - - name: Smoke test office list + echo "✅ --version OK" + - name: Smoke test identity init + run: | + export HOME=$(mktemp -d) + export TPS_VAULT_KEY="ci-release-smoke" + /tmp/tps-release-smoke identity init --json > /tmp/tps-identity.json + FP=$(node -p "try { JSON.parse(require('fs').readFileSync('/tmp/tps-identity.json','utf8')).fingerprint } catch(e) { '' }") + if [ -z "$FP" ]; then + echo "❌ identity init failed" + exit 1 + fi + echo "✅ identity init OK (fingerprint: $FP)" + - name: Smoke test Noise handshake run: | export HOME=$(mktemp -d) - mkdir -p "$HOME/.tps/branch-office" - TPS_OFFICE_SKIP_VM=1 /tmp/tps-release-smoke office list >/tmp/tps-office-list.log 2>&1 + export TPS_VAULT_KEY="ci-release-smoke" + timeout 15 /tmp/tps-release-smoke branch init --name release-smoke 2>&1 | tee /tmp/tps-branch.log || true + if grep -qE "Branch identity created|Listening on" /tmp/tps-branch.log; then + echo "✅ Noise handshake OK" + else + echo "❌ Noise handshake failed" + exit 1 + fi + - name: Smoke test no baked paths + run: | + if grep -q "/home/runner" /tmp/tps-release-smoke; then + echo "❌ Found /home/runner references in binary" + exit 1 + fi + echo "✅ No baked CI paths" publish-packages: name: Publish npm packages diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..adbd0c4 --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,121 @@ +name: Smoke (compiled binaries) + +on: + push: + branches: [main] + paths: + - 'packages/cli/**' + - 'packages/agent/**' + - 'bun.lock' + - '.github/workflows/smoke.yml' + pull_request: + branches: [main] + paths: + - 'packages/cli/**' + - 'packages/agent/**' + - 'bun.lock' + - '.github/workflows/smoke.yml' + +permissions: + contents: read + +jobs: + smoke: + name: Smoke ${{ matrix.target }} + strategy: + fail-fast: false + matrix: + include: + - target: linux-x64 + runs-on: ubuntu-latest + - target: linux-arm64 + runs-on: ubuntu-24.04-arm + # GitHub retired Intel mac runners (macos-13 queues forever). + # This job runs on Apple Silicon under Rosetta 2 translation — + # bun-compiled x64 binaries auto-translate. Bare-metal Intel mac + # is untested and that is a documented gap, not a hidden one. + - target: darwin-x64 + runs-on: macos-latest + - target: darwin-arm64 + runs-on: macos-latest + runs-on: ${{ matrix.runs-on }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: "1.3.10" + - uses: socketdev/action@ba6de6cc0565af1f42295590380973573297e31f # v1.3.2 + with: + mode: firewall-free + - run: sfw bun install --frozen-lockfile + - name: Build workspace packages (agent → cli) + run: | + # Build agent first — it no longer depends on cli's build output + # (signEnvelope was moved to agent, breaking the circular dep). + cd packages/agent && bun run build + # Build cli with agent types now available + cd ../cli && bun run build + # Build pi-tps-mail if present + (cd ../pi-tps-mail && bun run build) || true + - name: Build portable binary (${{ matrix.target }}) + run: bun run --filter @tpsdev-ai/cli build:binary:${{ matrix.target }} + - name: Set up Rosetta 2 prefix (darwin-x64) + if: matrix.target == 'darwin-x64' + run: echo "ARCH_PREFIX=arch -x86_64" >> $GITHUB_ENV + - name: Acceptance — version + run: | + BIN="packages/cli/dist/tps-${{ matrix.target }}" + chmod +x "$BIN" + VERSION_OUTPUT=$($ARCH_PREFIX "$BIN" --version) + PKG_VERSION=$(node -p "require('./packages/cli/package.json').version") + echo "Binary version: $VERSION_OUTPUT" + echo "Package version: $PKG_VERSION" + if [ "$VERSION_OUTPUT" != "$PKG_VERSION" ]; then + echo "❌ version mismatch" + exit 1 + fi + echo "✅ --version OK" + - name: Acceptance — identity init (exercises sodium) + run: | + BIN="packages/cli/dist/tps-${{ matrix.target }}" + export HOME=$(mktemp -d) + export TPS_VAULT_KEY="ci-smoke-test-key" + # Capture stdout only — stderr (warnings) goes to CI log + $ARCH_PREFIX "$BIN" identity init --json > /tmp/tps-identity.json + echo "Identity output:" + cat /tmp/tps-identity.json + # Verify we got a valid fingerprint + FP=$(node -p "try { JSON.parse(require('fs').readFileSync('/tmp/tps-identity.json','utf8')).fingerprint } catch(e) { '' }") + if [ -z "$FP" ]; then + echo "❌ identity init failed — no fingerprint" + exit 1 + fi + echo "✅ identity init OK (fingerprint: $FP)" + - name: Acceptance — Noise handshake (branch init) + run: | + BIN="packages/cli/dist/tps-${{ matrix.target }}" + export HOME=$(mktemp -d) + export TPS_VAULT_KEY="ci-smoke-test-key" + # branch init exercises the full noise-handshake → sodium-javascript path + # portable timeout: GNU `timeout` does not exist on stock macOS runners (this exact line + # failed with "timeout: command not found" on darwin-arm64 while every linux lane passed) + perl -e 'alarm shift; exec @ARGV' 15 $ARCH_PREFIX "$BIN" branch init --name smoke-test 2>&1 | tee /tmp/tps-branch.log || true + if grep -q "Branch identity created" /tmp/tps-branch.log; then + echo "✅ Noise handshake OK" + elif grep -q "Listening on" /tmp/tps-branch.log; then + echo "✅ Noise handshake OK (listening)" + else + echo "Branch init output:" + cat /tmp/tps-branch.log + echo "❌ Noise handshake failed" + exit 1 + fi + - name: Acceptance — no baked paths + run: | + BIN="packages/cli/dist/tps-${{ matrix.target }}" + COUNT=$(grep -ac "/home/runner" "$BIN" || echo 0) + if [ "$COUNT" -ne 0 ]; then + echo "❌ Found $COUNT /home/runner references in binary" + exit 1 + fi + echo "✅ No baked CI paths" diff --git a/bin/tps.ts b/bin/tps.ts index 9022c6f..e776022 100755 --- a/bin/tps.ts +++ b/bin/tps.ts @@ -88,7 +88,7 @@ async function checkNono() { if (command === "office" && rest[0] === "relay") return; // relay runs in background const { findNono } = await import("../src/utils/nono.js"); if (!findNono()) { - console.warn( + console.error( "⚠️ nono not found. Host agents will run without process isolation.\n" + " Install nono for syscall filtering + filesystem boundaries.\n" + " Use --nonono to run anyway (not recommended).\n" diff --git a/bun.lock b/bun.lock index d19363b..6f4ffa9 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,17 @@ "": { "name": "@tpsdev-ai/cli", "dependencies": { + "blake2b": "^2.1.4", "canonicalize": "^3.0.0", + "chacha20-universal": "^1.0.4", "handlebars": "^4.7.9", + "nanoassert": "^3.1.0", + "sha256-universal": "^1.2.1", + "sha512-universal": "^1.2.1", + "siphash24": "^1.3.1", + "sodium-javascript": "^0.8.0", "ws": "^8.20.1", + "xsalsa20": "^1.2.0", }, "devDependencies": { "@biomejs/biome": "^2.4.4", @@ -24,7 +32,7 @@ "dependencies": { "@tpsdev-ai/cli": "workspace:*", "js-tiktoken": "^1.0.13", - "js-yaml": "^4.1.0", + "js-yaml": "^4.3.1", "zod": "^3.24.0", }, "devDependencies": { @@ -48,7 +56,7 @@ "handlebars": "^4.7.8", "ink": "^5.2.0", "ink-text-input": "^6.0.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.3.1", "meow": "^13.2.0", "msgpackr": "^1.11.8", "noise-handshake": "^4.2.0", @@ -65,8 +73,16 @@ "@types/node": "^22.0.0", "@types/react": "^18.3.0", "@types/safe-regex": "^1.1.6", + "blake2b": "^2.1.4", + "chacha20-universal": "^1.0.4", "fast-check": "^4.5.3", + "nanoassert": "^3.1.0", + "sha256-universal": "^1.2.1", + "sha512-universal": "^1.2.1", + "siphash24": "^1.3.1", + "sodium-javascript": "^0.8.0", "typescript": "^5.7.0", + "xsalsa20": "^1.2.0", }, "optionalDependencies": { "@tpsdev-ai/cli-darwin-arm64": "0.5.4", @@ -83,7 +99,6 @@ }, "dependencies": { "require-addon": "^1.2.0", - "sodium-native": "^5.0.10", }, }, "packages/cli-darwin-x64": { @@ -94,7 +109,6 @@ }, "dependencies": { "require-addon": "^1.2.0", - "sodium-native": "^5.0.10", }, }, "packages/cli-linux-arm64": { @@ -105,7 +119,6 @@ }, "dependencies": { "require-addon": "^1.2.0", - "sodium-native": "^5.0.10", }, }, "packages/cli-linux-x64": { @@ -116,7 +129,6 @@ }, "dependencies": { "require-addon": "^1.2.0", - "sodium-native": "^5.0.10", }, }, "packages/pi-tps-mail": { @@ -137,7 +149,7 @@ }, }, "overrides": { - "js-yaml": "^4.3.0", + "js-yaml": "^4.3.1", "shell-quote": "^1.9.0", "ws": "^8.21.0", }, @@ -228,8 +240,14 @@ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "blake2b": ["blake2b@2.1.4", "", { "dependencies": { "blake2b-wasm": "^2.4.0", "nanoassert": "^2.0.0" } }, "sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A=="], + + "blake2b-wasm": ["blake2b-wasm@2.4.0", "", { "dependencies": { "b4a": "^1.0.1", "nanoassert": "^2.0.0" } }, "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w=="], + "canonicalize": ["canonicalize@3.0.0", "", { "bin": { "canonicalize": "bin/canonicalize.js" } }, "sha512-yYLfHyDMIXRyRqsKBRLX023riFLpXY2YOfdtqKXZRZy9qsfOJ9U+4F9YZL7MEzL5+ziN2x2nlBvY/Voi3EBljA=="], + "chacha20-universal": ["chacha20-universal@1.0.4", "", { "dependencies": { "nanoassert": "^2.0.0" } }, "sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q=="], + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], @@ -274,7 +292,7 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="], + "js-yaml": ["js-yaml@4.3.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -288,7 +306,7 @@ "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + "nanoassert": ["nanoassert@3.1.0", "", {}, "sha512-pHCA6mU/H4BzVxHSFzfuhb6NJ0des9pPWsnTERnOQ1a1Ft/M15+b8+sNCsHNWgLN+NVr5QaVlwp0IE7NfST+xw=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -318,14 +336,26 @@ "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "sha256-universal": ["sha256-universal@1.2.1", "", { "dependencies": { "b4a": "^1.0.1", "sha256-wasm": "^2.2.1" } }, "sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw=="], + + "sha256-wasm": ["sha256-wasm@2.2.2", "", { "dependencies": { "b4a": "^1.0.1", "nanoassert": "^2.0.0" } }, "sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ=="], + + "sha512-universal": ["sha512-universal@1.2.1", "", { "dependencies": { "b4a": "^1.0.1", "sha512-wasm": "^2.3.1" } }, "sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ=="], + + "sha512-wasm": ["sha512-wasm@2.3.4", "", { "dependencies": { "b4a": "^1.0.1", "nanoassert": "^2.0.0" } }, "sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg=="], + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "siphash24": ["siphash24@1.3.1", "", { "dependencies": { "nanoassert": "^2.0.0" } }, "sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA=="], + "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], "snooplogg": ["snooplogg@6.1.1", "", {}, "sha512-ERUf0zDe+4/wdzoTlz9aj45PCXRO/jckRY6BwlGLl4dZVr8hanHEW13MSYLSqOz9aDbRR8PGnb4arak+Nh4VqQ=="], + "sodium-javascript": ["sodium-javascript@0.8.0", "", { "dependencies": { "blake2b": "^2.1.1", "chacha20-universal": "^1.0.4", "nanoassert": "^2.0.0", "sha256-universal": "^1.1.0", "sha512-universal": "^1.1.0", "siphash24": "^1.0.1", "xsalsa20": "^1.0.0" } }, "sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg=="], + "sodium-native": ["sodium-native@5.0.10", "", { "dependencies": { "require-addon": "^1.1.0", "which-runtime": "^1.2.1" } }, "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA=="], "sodium-universal": ["sodium-universal@5.0.1", "", { "dependencies": { "sodium-native": "^5.0.1" }, "peerDependencies": { "sodium-javascript": "~0.8.0" }, "optionalPeers": ["sodium-javascript"] }, "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q=="], @@ -356,12 +386,30 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "xsalsa20": ["xsalsa20@1.2.0", "", {}, "sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "blake2b/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + + "blake2b-wasm/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + + "chacha20-universal/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + "cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + "noise-handshake/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + + "sha256-wasm/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + + "sha512-wasm/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + + "siphash24/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "sodium-javascript/nanoassert": ["nanoassert@2.0.0", "", {}, "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA=="], } } diff --git a/package.json b/package.json index c5d68d4..73f3312 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@tpsdev-ai/tps", "private": true, - "description": "TPS Monorepo \u2014 CLI and Agent Runtime", + "description": "TPS Monorepo — CLI and Agent Runtime", "workspaces": [ "packages/*" ], @@ -20,14 +20,22 @@ "author": "tpsdev-ai", "license": "Apache-2.0", "dependencies": { + "blake2b": "^2.1.4", "canonicalize": "^3.0.0", + "chacha20-universal": "^1.0.4", "handlebars": "^4.7.9", - "ws": "^8.20.1" + "nanoassert": "^3.1.0", + "sha256-universal": "^1.2.1", + "sha512-universal": "^1.2.1", + "siphash24": "^1.3.1", + "sodium-javascript": "^0.8.0", + "ws": "^8.20.1", + "xsalsa20": "^1.2.0" }, "overrides": { "ws": "^8.21.0", "shell-quote": "^1.9.0", - "js-yaml": "^4.3.0" + "js-yaml": "^4.3.1" }, "packageManager": "bun@1.3.10" } diff --git a/packages/agent/package.json b/packages/agent/package.json index 5c89a13..a4f20a7 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -32,9 +32,15 @@ "mail-driven" ], "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/tpsdev-ai/cli.git", + "directory": "packages/agent" + }, + "homepage": "https://github.com/tpsdev-ai/cli/tree/main/packages/agent#readme", "dependencies": { "@tpsdev-ai/cli": "workspace:*", - "js-yaml": "^4.1.0", + "js-yaml": "^4.3.1", "js-tiktoken": "^1.0.13", "zod": "^3.24.0" }, diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index ca32b31..a2ef06e 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -40,3 +40,7 @@ export { loadAgentConfig } from "./config.js"; // Flair integration export { FlairContextProvider } from "./io/flair.js"; export type { FlairConfig } from "./runtime/types.js"; + +// Signing +export { signEnvelope, verifyEnvelope } from "./lib/signEnvelope.js"; +export type { Envelope, ChainEntry, FlairClient, VerifyOk, VerifyReject } from "./lib/signEnvelope.js"; diff --git a/packages/agent/src/io/mail.ts b/packages/agent/src/io/mail.ts index 983c1f5..0c12f8e 100644 --- a/packages/agent/src/io/mail.ts +++ b/packages/agent/src/io/mail.ts @@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, renameSync, readFileSync, writeFile import { join } from "node:path"; import type { EventLogger } from "../telemetry/events.js"; import { sanitizeError } from "../telemetry/events.js"; -import { verifyEnvelope, type FlairClient } from "@tpsdev-ai/cli/lib/signEnvelope"; +import { verifyEnvelope, type FlairClient } from "../lib/signEnvelope.js"; export interface MailMessage { filename: string; diff --git a/packages/cli/src/lib/signEnvelope.ts b/packages/agent/src/lib/signEnvelope.ts similarity index 100% rename from packages/cli/src/lib/signEnvelope.ts rename to packages/agent/src/lib/signEnvelope.ts diff --git a/packages/agent/src/runtime/agent.ts b/packages/agent/src/runtime/agent.ts index 99bb0a2..0481e30 100644 --- a/packages/agent/src/runtime/agent.ts +++ b/packages/agent/src/runtime/agent.ts @@ -10,7 +10,7 @@ import { BoundaryManager } from "../governance/boundary.js"; import { createDefaultToolset } from "../tools/index.js"; import { EventLogger } from "../telemetry/events.js"; import { FlairContextProvider } from "../io/flair.js"; -import type { FlairClient } from "@tpsdev-ai/cli/lib/signEnvelope"; +import type { FlairClient } from "../lib/signEnvelope.js"; export class AgentRuntime { private loop: EventLoop; diff --git a/packages/cli-darwin-arm64/package.json b/packages/cli-darwin-arm64/package.json index 5ba0895..a83f60f 100644 --- a/packages/cli-darwin-arm64/package.json +++ b/packages/cli-darwin-arm64/package.json @@ -16,9 +16,14 @@ "README.md" ], "dependencies": { - "sodium-native": "^5.0.10", "require-addon": "^1.2.0" }, + "repository": { + "type": "git", + "url": "git+https://github.com/tpsdev-ai/cli.git", + "directory": "packages/cli-darwin-arm64" + }, + "homepage": "https://github.com/tpsdev-ai/cli#readme", "license": "Apache-2.0", "publishConfig": { "access": "public" diff --git a/packages/cli-darwin-x64/package.json b/packages/cli-darwin-x64/package.json index 3ff63c5..3120174 100644 --- a/packages/cli-darwin-x64/package.json +++ b/packages/cli-darwin-x64/package.json @@ -16,9 +16,14 @@ "README.md" ], "dependencies": { - "sodium-native": "^5.0.10", "require-addon": "^1.2.0" }, + "repository": { + "type": "git", + "url": "git+https://github.com/tpsdev-ai/cli.git", + "directory": "packages/cli-darwin-x64" + }, + "homepage": "https://github.com/tpsdev-ai/cli#readme", "license": "Apache-2.0", "publishConfig": { "access": "public" diff --git a/packages/cli-linux-arm64/package.json b/packages/cli-linux-arm64/package.json index bfb238b..4919c93 100644 --- a/packages/cli-linux-arm64/package.json +++ b/packages/cli-linux-arm64/package.json @@ -16,9 +16,14 @@ "README.md" ], "dependencies": { - "sodium-native": "^5.0.10", "require-addon": "^1.2.0" }, + "repository": { + "type": "git", + "url": "git+https://github.com/tpsdev-ai/cli.git", + "directory": "packages/cli-linux-arm64" + }, + "homepage": "https://github.com/tpsdev-ai/cli#readme", "license": "Apache-2.0", "publishConfig": { "access": "public" diff --git a/packages/cli-linux-x64/package.json b/packages/cli-linux-x64/package.json index 2f1db8f..ce82773 100644 --- a/packages/cli-linux-x64/package.json +++ b/packages/cli-linux-x64/package.json @@ -16,9 +16,14 @@ "README.md" ], "dependencies": { - "sodium-native": "^5.0.10", "require-addon": "^1.2.0" }, + "repository": { + "type": "git", + "url": "git+https://github.com/tpsdev-ai/cli.git", + "directory": "packages/cli-linux-x64" + }, + "homepage": "https://github.com/tpsdev-ai/cli#readme", "license": "Apache-2.0", "publishConfig": { "access": "public" diff --git a/packages/cli/package.json b/packages/cli/package.json index eb6a9a7..c531071 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,10 +21,10 @@ "scripts": { "build": "tsc", "build:binary": "bun run build:binary:darwin-arm64 && bun run build:binary:darwin-x64 && bun run build:binary:linux-arm64 && bun run build:binary:linux-x64", - "build:binary:darwin-arm64": "bun build --compile bin/tps.ts --target=bun-darwin-arm64 --define \"INJECTED_VERSION=\\\"$npm_package_version\\\"\" -e sodium-native -e require-addon --outfile=dist/tps-darwin-arm64", - "build:binary:darwin-x64": "bun build --compile bin/tps.ts --target=bun-darwin-x64 --define \"INJECTED_VERSION=\\\"$npm_package_version\\\"\" -e sodium-native -e require-addon --outfile=dist/tps-darwin-x64", - "build:binary:linux-arm64": "bun build --compile bin/tps.ts --target=bun-linux-arm64 --define \"INJECTED_VERSION=\\\"$npm_package_version\\\"\" -e sodium-native -e require-addon --outfile=dist/tps-linux-arm64", - "build:binary:linux-x64": "bun build --compile bin/tps.ts --target=bun-linux-x64 --define \"INJECTED_VERSION=\\\"$npm_package_version\\\"\" -e sodium-native -e require-addon --outfile=dist/tps-linux-x64", + "build:binary:darwin-arm64": "bun run scripts/build-portable.ts bun-darwin-arm64", + "build:binary:darwin-x64": "bun run scripts/build-portable.ts bun-darwin-x64", + "build:binary:linux-arm64": "bun run scripts/build-portable.ts bun-linux-arm64", + "build:binary:linux-x64": "bun run scripts/build-portable.ts bun-linux-x64", "dev": "tsc --watch", "start": "node dist/bin/tps.js", "tps": "node dist/bin/tps.js", @@ -41,6 +41,12 @@ "tps" ], "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/tpsdev-ai/cli.git", + "directory": "packages/cli" + }, + "homepage": "https://github.com/tpsdev-ai/cli#readme", "dependencies": { "@noble/curves": "^2.0.1", "@noble/ed25519": "^3.0.0", @@ -49,7 +55,7 @@ "handlebars": "^4.7.8", "ink": "^5.2.0", "ink-text-input": "^6.0.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.3.1", "meow": "^13.2.0", "msgpackr": "^1.11.8", "noise-handshake": "^4.2.0", @@ -65,8 +71,16 @@ "@types/node": "^22.0.0", "@types/react": "^18.3.0", "@types/safe-regex": "^1.1.6", + "blake2b": "^2.1.4", + "chacha20-universal": "^1.0.4", "fast-check": "^4.5.3", + "nanoassert": "^3.1.0", + "sha256-universal": "^1.2.1", + "sha512-universal": "^1.2.1", + "siphash24": "^1.3.1", + "sodium-javascript": "^0.8.0", "typescript": "^5.7.0", + "xsalsa20": "^1.2.0", "@tpsdev-ai/agent": "workspace:*" }, "files": [ diff --git a/packages/cli/scripts/build-portable.ts b/packages/cli/scripts/build-portable.ts new file mode 100644 index 0000000..ac33602 --- /dev/null +++ b/packages/cli/scripts/build-portable.ts @@ -0,0 +1,93 @@ +#!/usr/bin/env bun +/** + * build-portable.ts — Build a standalone tps binary with sodium-javascript (pure JS) + * instead of sodium-native (native addon). This keeps the binary portable across + * machines — no baked-in CI paths, no missing .node addons. + * + * Usage: + * bun run scripts/build-portable.ts [outfile] + * + * Targets: bun-linux-x64, bun-linux-arm64, bun-darwin-x64, bun-darwin-arm64 + * + * Two-phase build: + * 1. Bun.build() with a plugin that aliases sodium-native → sodium-javascript + * 2. bun build --compile on the bundled output to produce the standalone binary + */ + +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkgRoot = resolve(__dirname, ".."); +const repoRoot = resolve(pkgRoot, "..", ".."); + +const target = (process.argv[2] as string) || "bun-linux-x64"; +const outfile = + process.argv[3] || resolve(pkgRoot, "dist", `tps-${target.replace("bun-", "")}`); + +// Read version from package.json +const pkgJson = JSON.parse( + await Bun.file(resolve(pkgRoot, "package.json")).text() +); +const version = pkgJson.version || "0.0.0"; + +// Phase 1: bundle with sodium-native → sodium-javascript alias +const sjPath = resolve(repoRoot, "node_modules", "sodium-javascript", "index.js"); + +const result = await Bun.build({ + entrypoints: [resolve(pkgRoot, "bin", "tps.ts")], + target: "bun", + outdir: resolve(pkgRoot, "dist"), + define: { + INJECTED_VERSION: JSON.stringify(version), + }, + external: ["require-addon"], + plugins: [ + { + name: "alias-sodium-native", + setup(build) { + build.onResolve({ filter: /^sodium-native$/ }, () => { + return { path: sjPath }; + }); + }, + }, + { + name: "stub-msgpackr-extract", + setup(build) { + // msgpackr-extract is an optional native addon for msgpackr. + // It bakes __dirname into the bundle (→ /home/runner on CI), + // which fails the binary-hygiene gate. Stub it out so msgpackr + // falls back to its pure-JS codec — the portable binary doesn't + // ship native addons anyway. + build.onResolve({ filter: /^msgpackr-extract$/ }, () => { + return { path: resolve(pkgRoot, "scripts", "stubs", "empty.js") }; + }); + }, + }, + ], + naming: "[dir]/tps-bundle.[ext]", +}); + +if (!result.success) { + for (const log of result.logs) { + console.error(log); + } + process.exit(1); +} + +const bundlePath = result.outputs[0].path; + +// Phase 2: compile the bundle to a standalone binary +const proc = Bun.spawnSync( + ["bun", "build", "--compile", bundlePath, "--target", target, "--outfile", outfile], + { + cwd: repoRoot, + stdio: ["inherit", "inherit", "inherit"], + } +); + +if (proc.exitCode !== 0) { + process.exit(proc.exitCode ?? 1); +} + +console.log(`Portable binary: ${outfile}`); diff --git a/packages/cli/scripts/stubs/empty.js b/packages/cli/scripts/stubs/empty.js new file mode 100644 index 0000000..a284570 --- /dev/null +++ b/packages/cli/scripts/stubs/empty.js @@ -0,0 +1,2 @@ +// Stub: msgpackr-extract is an optional native addon. The portable binary uses msgpackr's pure-JS fallback. +module.exports = null; diff --git a/packages/cli/src/commands/mail.ts b/packages/cli/src/commands/mail.ts index 2db6f34..60fd2de 100644 --- a/packages/cli/src/commands/mail.ts +++ b/packages/cli/src/commands/mail.ts @@ -9,7 +9,7 @@ import { loadHostIdentityId } from "../utils/identity.js"; import { queueOutboxMessage } from "../utils/outbox.js"; import { galLookup } from "../utils/gal.js"; import { parseTaskEnvelope, formatTaskEnvelope, createTaskEnvelope } from "../utils/task-envelope.js"; -import { signEnvelope, type Envelope, type ChainEntry } from "../lib/signEnvelope.js"; +import { signEnvelope, type Envelope, type ChainEntry } from "@tpsdev-ai/agent"; import { readAgentPrivateKey, parseInboundChain } from "../utils/agent-keys.js"; import { randomUUID } from "node:crypto"; diff --git a/packages/cli/src/utils/agent-keys.ts b/packages/cli/src/utils/agent-keys.ts index 11821d8..1a23a74 100644 --- a/packages/cli/src/utils/agent-keys.ts +++ b/packages/cli/src/utils/agent-keys.ts @@ -86,7 +86,7 @@ function seedFromPkcs8Der(der: Buffer): Buffer | null { * Parse TPS_INBOUND_CHAIN_JSON into a ChainEntry array. * Returns null if unset, empty, or invalid. */ -import type { ChainEntry } from "../lib/signEnvelope.js"; +import type { ChainEntry } from "@tpsdev-ai/agent"; export function parseInboundChain(raw: string | undefined): ChainEntry[] | null { if (!raw || raw.trim() === "") return null; diff --git a/packages/cli/src/utils/mail.ts b/packages/cli/src/utils/mail.ts index 914754c..0d2566b 100644 --- a/packages/cli/src/utils/mail.ts +++ b/packages/cli/src/utils/mail.ts @@ -4,7 +4,7 @@ import { homedir } from "node:os"; import { randomUUID } from "node:crypto"; import { sanitizeIdentifier } from "../schema/sanitizer.js"; import { logEvent } from "./archive.js"; -import { verifyEnvelope, type FlairClient } from "../lib/signEnvelope.js"; +import { verifyEnvelope, type FlairClient } from "@tpsdev-ai/agent"; export interface MailMessage { id: string; diff --git a/packages/cli/test/identity.test.ts b/packages/cli/test/identity.test.ts index ad6cd08..d00ec18 100644 --- a/packages/cli/test/identity.test.ts +++ b/packages/cli/test/identity.test.ts @@ -373,4 +373,29 @@ describe("CLI integration", () => { expect(e.status).toBe(1); } }); + + test("identity init --json emits parseable JSON on stdout (warnings on stderr)", () => { + const { execSync } = require("node:child_process"); + // Run WITHOUT --nonono so any warnings (nono not found, etc.) would fire. + // They must go to stderr — stdout must be clean parseable JSON. + const result = execSync( + `bun ${TPS_BIN} identity init --json`, + { + encoding: "utf-8", + env: { + ...process.env, + TPS_VAULT_KEY: "test-passphrase", + TPS_IDENTITY_DIR: join(tempDir, "cli-identity-stdout"), + TPS_REGISTRY_DIR: join(tempDir, "cli-registry-stdout"), + }, + // Capture stdout only — stderr is separate + stdio: ["pipe", "pipe", "pipe"], + } + ); + // Must be parseable JSON — no warning text mixed in + const parsed = JSON.parse(result); + expect(parsed.fingerprint).toMatch(/^[0-9a-f]{64}$/); + expect(parsed.signingPublicKey).toMatch(/^[0-9a-f]{64}$/); + expect(parsed.encryptionPublicKey).toMatch(/^[0-9a-f]{64}$/); + }); }); diff --git a/packages/cli/test/mail-consumer-verify.test.ts b/packages/cli/test/mail-consumer-verify.test.ts index 9113bde..7ecc3aa 100644 --- a/packages/cli/test/mail-consumer-verify.test.ts +++ b/packages/cli/test/mail-consumer-verify.test.ts @@ -13,7 +13,7 @@ import { tmpdir } from "node:os"; import * as ed from "@noble/ed25519"; import { createHash } from "node:crypto"; import canonicalize from "canonicalize"; -import { signEnvelope, type Envelope, verifyEnvelope } from "../src/lib/signEnvelope.js"; +import { signEnvelope, type Envelope, verifyEnvelope } from "@tpsdev-ai/agent"; import { sendMessage, checkMessages, getInbox } from "../src/utils/mail.js"; // Wire sha512 for sync sign operations. diff --git a/packages/cli/test/mail-send-sign.test.ts b/packages/cli/test/mail-send-sign.test.ts index 078a3af..e9e2992 100644 --- a/packages/cli/test/mail-send-sign.test.ts +++ b/packages/cli/test/mail-send-sign.test.ts @@ -15,7 +15,7 @@ import { signEnvelope, type Envelope, type ChainEntry, -} from "../src/lib/signEnvelope.js"; +} from "@tpsdev-ai/agent"; // Wire sha512 for sync sign operations. import { hashes } from "@noble/ed25519"; diff --git a/packages/cli/test/signEnvelope.test.ts b/packages/cli/test/signEnvelope.test.ts index b176cea..0e10259 100644 --- a/packages/cli/test/signEnvelope.test.ts +++ b/packages/cli/test/signEnvelope.test.ts @@ -6,7 +6,7 @@ import { describe, expect, test } from "bun:test"; import * as ed from "@noble/ed25519"; import { createHash } from "node:crypto"; import canonicalize from "canonicalize"; -import { signEnvelope, type Envelope, type ChainEntry } from "../src/lib/signEnvelope.js"; +import { signEnvelope, type Envelope, type ChainEntry } from "@tpsdev-ai/agent"; // Wire sha512 for sync sign operations (same as production). import { hashes } from "@noble/ed25519"; diff --git a/packages/cli/test/verifyEnvelope.test.ts b/packages/cli/test/verifyEnvelope.test.ts index 2220f2d..3adc409 100644 --- a/packages/cli/test/verifyEnvelope.test.ts +++ b/packages/cli/test/verifyEnvelope.test.ts @@ -12,7 +12,7 @@ import { type Envelope, type ChainEntry, type FlairClient, -} from "../src/lib/signEnvelope.js"; +} from "@tpsdev-ai/agent"; // Wire sha512 for sync sign operations (same as production). import { hashes } from "@noble/ed25519"; diff --git a/src/utils/github-webhook.ts b/src/utils/github-webhook.ts index 660f8d6..a7d64f3 100644 --- a/src/utils/github-webhook.ts +++ b/src/utils/github-webhook.ts @@ -76,7 +76,7 @@ function formatEvent(event: string, payload: Record): string { export async function handleGithubWebhook(req: IncomingMessage, res: ServerResponse): Promise { if (!webhookSecret()) { - console.warn("[webhook] GITHUB_WEBHOOK_SECRET not set — all webhook requests will be rejected"); + console.error("[webhook] GITHUB_WEBHOOK_SECRET not set — all webhook requests will be rejected"); res.statusCode = 503; res.end("Webhook not configured"); return; diff --git a/src/utils/nono.ts b/src/utils/nono.ts index 4dcb18b..f2176f3 100644 --- a/src/utils/nono.ts +++ b/src/utils/nono.ts @@ -121,7 +121,7 @@ export async function withNono( ); process.exit(1); } else { - console.warn( + console.error( `⚠️ nono not found — running ${profile} WITHOUT isolation. Install nono for security: https://nono.sh` ); return fn(); @@ -154,7 +154,7 @@ export function runCommandUnderNono( ); return 1; } - console.warn( + console.error( `⚠️ nono not found — running WITHOUT isolation: ${cmd.join(" ")}` ); const result = spawnSync(cmd[0]!, cmd.slice(1), { @@ -197,7 +197,7 @@ export function installNonoProfiles(targetDir?: string, silent?: boolean): void const bundledDir = findBundledProfilesDir(); if (!existsSync(bundledDir)) { - if (!silent) console.warn(`⚠️ No bundled nono profiles found at ${bundledDir}`); + if (!silent) console.error(`⚠️ No bundled nono profiles found at ${bundledDir}`); return; }