Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-kitty-backspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix Backspace handling in Kitty terminals.
31 changes: 24 additions & 7 deletions .github/actions/macos-notarize/action.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: macOS notarize
description: |
Submit a signed binary to Apple notary service via notarytool, wait for
result, and run Gatekeeper online check (spctl). Fails the job with the
notarytool log on rejection.
Submit a signed binary and sibling native helpers to Apple notary service via
notarytool, wait for result, and run Gatekeeper online check (spctl). Fails
the job with the notarytool log on rejection.

inputs:
binary-path:
Expand Down Expand Up @@ -47,10 +47,18 @@ runs:

# 2. Pack with ditto (--norsrc avoids ._AppleDouble files)
binary_name=$(basename "$BINARY_PATH")
binary_dir=$(dirname "$BINARY_PATH")
package_root="${RUNNER_TEMP}/${binary_name}-notarize-root"
zip_path="${RUNNER_TEMP}/${binary_name}.notarize.zip"
ditto -c -k --norsrc --keepParent "$BINARY_PATH" "$zip_path"
rm -rf "$package_root"
mkdir -p "$package_root"
cp "$BINARY_PATH" "$package_root/$binary_name"
if [[ -d "$binary_dir/native" ]]; then
cp -R "$binary_dir/native" "$package_root/native"
fi
ditto -c -k --norsrc "$package_root" "$zip_path"

echo "==> Submitting $binary_name for notarization..."
echo "==> Submitting $binary_name and sibling native helpers for notarization..."

# 3. Submit and capture log
log_path="${RUNNER_TEMP}/notarize-${binary_name}.log"
Expand All @@ -74,14 +82,14 @@ runs:
--key-id "$APPLE_NOTARIZATION_KEY_ID" \
--issuer "$APPLE_NOTARIZATION_ISSUER_ID" || true
fi
rm -f "$key_path" "$zip_path"
rm -rf "$key_path" "$zip_path" "$package_root"
exit 1
fi

echo "==> Notarization accepted"

# 5. Cleanup
rm -f "$key_path" "$zip_path"
rm -rf "$key_path" "$zip_path" "$package_root"

- name: Verify with Gatekeeper online check
shell: bash
Expand All @@ -91,5 +99,14 @@ runs:
set -euo pipefail
echo "==> codesign -dv $BINARY_PATH"
codesign -dv --verbose=2 "$BINARY_PATH"
binary_dir=$(dirname "$BINARY_PATH")
if [[ -d "$binary_dir/native" ]]; then
while IFS= read -r -d '' helper; do
echo "==> codesign --verify $helper"
codesign --verify --strict --verbose=2 "$helper"
echo "==> codesign -dv $helper"
codesign -dv --verbose=2 "$helper"
done < <(find "$binary_dir/native" -type f -name '*.node' -print0)
fi
echo "==> spctl -a -vvv -t install $BINARY_PATH"
spctl -a -vvv -t install "$BINARY_PATH"
2 changes: 1 addition & 1 deletion apps/kimi-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"postinstall": "node scripts/postinstall.mjs"
},
"dependencies": {
"@earendil-works/pi-tui": "^0.74.0",
"@earendil-works/pi-tui": "^0.79.3",
"@mariozechner/clipboard": "^0.3.2",
"chalk": "^5.4.1",
"cli-highlight": "^2.1.11",
Expand Down
16 changes: 15 additions & 1 deletion apps/kimi-code/scripts/native/03-inject.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { copyFile, mkdir, stat } from 'node:fs/promises';
import { resolve } from 'node:path';
import { dirname, resolve } from 'node:path';

import { resolveExecutableNativeFiles } from './assets.mjs';
import { fail, run, tryRun } from './exec.mjs';
import {
appRoot,
Expand Down Expand Up @@ -52,12 +53,25 @@ async function injectSeaBlob(target) {
await run(postjectPath(), args);
}

async function copyExecutableNativeFiles(target) {
const files = resolveExecutableNativeFiles({ appRoot, target });
for (const file of files) {
const destination = resolve(nativeBinDir(target), file.relativePath);
await mkdir(dirname(destination), { recursive: true });
await copyFile(file.sourcePath, destination);
}
if (files.length > 0) {
console.log(`Copied ${files.length} native helper file(s) for ${target}`);
}
}

export async function runInjectStep() {
const target = targetTriple();
await ensureBlobExists();
await copyNodeExecutable(target);
await removeSignatureIfNeeded(target);
await injectSeaBlob(target);
await copyExecutableNativeFiles(target);
}

if (import.meta.url === `file://${process.argv[1]}`) {
Expand Down
30 changes: 28 additions & 2 deletions apps/kimi-code/scripts/native/04-sign.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { stat, writeFile } from 'node:fs/promises';
import { basename, resolve } from 'node:path';

import { run } from './exec.mjs';
import { nativeBinPath, targetTriple } from './paths.mjs';
import { resolveExecutableFileRelatives } from './native-deps.mjs';
import { nativeBinDir, nativeBinPath, targetTriple } from './paths.mjs';

const ENTITLEMENTS_PATH = resolve(import.meta.dirname, 'entitlements.plist');

Expand All @@ -28,6 +29,18 @@ export function buildCodesignArgs({ identity, executable, entitlementsPath, keyc
return args;
}

export function buildCodesignNativeHelperArgs({ identity, file, keychainPath }) {
if (identity === '-') {
return ['--sign', '-', file];
}
const args = ['--sign', identity, '--options', 'runtime', '--timestamp'];
if (keychainPath) {
args.push('--keychain', keychainPath);
}
args.push('--force', file);
return args;
}

async function sha256(path) {
return await new Promise((resolveHash, reject) => {
const hash = createHash('sha256');
Expand All @@ -48,6 +61,19 @@ export async function runSignStep({ identity = '-', keychainPath = null } = {})
const executable = nativeBinPath(target);

if (process.platform === 'darwin') {
for (const relativePath of resolveExecutableFileRelatives(target)) {
const file = resolve(nativeBinDir(target), relativePath);
await stat(file);
await run(
'codesign',
buildCodesignNativeHelperArgs({
identity,
file,
keychainPath,
}),
);
}

const args = buildCodesignArgs({
identity,
executable,
Expand Down
13 changes: 12 additions & 1 deletion apps/kimi-code/scripts/native/05-verify.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { resolve } from 'node:path';

import { run } from './exec.mjs';
import { nativeBinPath, targetTriple } from './paths.mjs';
import { resolveExecutableFileRelatives } from './native-deps.mjs';
import { nativeBinDir, nativeBinPath, targetTriple } from './paths.mjs';

export async function runVerifyStep({ requireGatekeeper = false } = {}) {
if (process.platform !== 'darwin') {
Expand All @@ -13,6 +16,14 @@ export async function runVerifyStep({ requireGatekeeper = false } = {}) {
console.log(`==> codesign -dv ${executable}`);
await run('codesign', ['-dv', '--verbose=2', executable]);

for (const relativePath of resolveExecutableFileRelatives(target)) {
const file = resolve(nativeBinDir(target), relativePath);
console.log(`==> codesign --verify ${file}`);
await run('codesign', ['--verify', '--strict', '--verbose=2', file]);
console.log(`==> codesign -dv ${file}`);
await run('codesign', ['-dv', '--verbose=2', file]);
}

if (requireGatekeeper) {
// spctl in 'install' mode simulates the Gatekeeper online check — only a
// fully notarized binary passes. Ad-hoc signed binaries fail, so this is
Expand Down
42 changes: 38 additions & 4 deletions apps/kimi-code/scripts/native/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path
import { pathToFileURL } from 'node:url';

import { NATIVE_ASSET_MANIFEST_VERSION, buildManifestKey } from './manifest.mjs';
import { resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs';
import { nativeDeps, resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs';

export { NATIVE_ASSET_MANIFEST_VERSION };

Expand All @@ -17,9 +17,7 @@ export const NATIVE_TARGETS = Object.freeze(
SUPPORTED_TARGETS.map((t) => {
const deps = resolveTargetDeps(t);
const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName;
const koffiNativeFile = deps.find((d) => d.id === 'koffi')?.nativeFileRelatives?.[0];
const koffiTriplet = koffiNativeFile?.match(/koffi\/([^/]+)\/koffi\.node$/)?.[1] ?? null;
return [t, { clipboardPackage: clipboardTarget, koffiTriplet }];
return [t, { clipboardPackage: clipboardTarget }];
}),
),
);
Expand Down Expand Up @@ -274,3 +272,39 @@ export async function collectNativeAssets({ appRoot, target }) {
assets,
};
}

export function resolveExecutableNativeFiles({ appRoot, target }) {
const requireFromApp = createRequire(pathToFileURL(resolve(appRoot, 'package.json')));
const files = [];

for (const dep of nativeDeps) {
const executableFileRelatives = dep.executableFileRelatives?.(target) ?? [];
if (executableFileRelatives.length === 0) continue;

const packageName = dep.name(target);
const parentName = dep.parent
? nativeDeps.find((p) => p.id === dep.parent)?.name(target) ?? null
: null;
const packageRoot = resolvePackageRootGeneric(
requireFromApp,
packageName,
parentName,
appRoot,
target,
);

for (const relativePath of executableFileRelatives) {
const sourcePath = resolve(packageRoot, relativePath);
if (!existsSync(sourcePath)) {
fail(`Native package ${packageName} does not contain ${relativePath} at ${packageRoot}`);
}
files.push({
packageName,
relativePath: toPosixPath(relativePath),
sourcePath,
});
}
}

return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
}
2 changes: 0 additions & 2 deletions apps/kimi-code/scripts/native/check-bundle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ const optionalRuntimeRequires = new Set([
'utf-8-validate',
]);
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
const handledNativeRuntimeRequires = new Set(['koffi']);

function isAllowedSpecifier(specifier) {
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;
if (optionalRuntimeRequires.has(specifier)) return true;
if (handledNativeRuntimeRequires.has(specifier)) return true;
return false;
}

Expand Down
4 changes: 2 additions & 2 deletions apps/kimi-code/scripts/native/entitlements.plist
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>

<!-- We dlopen koffi.node / clipboard.node at runtime; they're built by
third parties and aren't signed by our Team. Without this they'd be
<!-- We dlopen native helper modules at runtime; some are built by
third parties and are not signed by our Team. Without this they'd be
rejected by hardened runtime. -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
Expand Down
34 changes: 18 additions & 16 deletions apps/kimi-code/scripts/native/native-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ const clipboardSubpackageByTarget = Object.freeze({
'win32-x64': '@mariozechner/clipboard-win32-x64-msvc',
});

const koffiTripletByTarget = Object.freeze({
'darwin-arm64': 'darwin_arm64',
'darwin-x64': 'darwin_x64',
'linux-arm64': 'linux_arm64',
'linux-x64': 'linux_x64',
'win32-arm64': 'win32_arm64',
'win32-x64': 'win32_x64',
const piTuiExecutableFilesByTarget = Object.freeze({
'darwin-arm64': ['native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node'],
'darwin-x64': ['native/darwin/prebuilds/darwin-x64/darwin-modifiers.node'],
'win32-arm64': ['native/win32/prebuilds/win32-arm64/win32-console-mode.node'],
'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'],
});

export function isSupportedTarget(target) {
Expand All @@ -52,6 +50,9 @@ export function isSupportedTarget(target) {
* @property {(target: string) => string[]} [nativeFileRelatives]
* — explicit list of .node files relative to package root
* (used by 'js-and-native-file'; native-files mode auto-scans *.node)
* @property {(target: string) => string[]} [executableFileRelatives]
* — files copied next to the native executable, preserving the
* package-relative path
*/

/** @type {readonly NativeDepDescriptor[]} */
Expand All @@ -71,20 +72,21 @@ export const nativeDeps = Object.freeze([
{
id: 'pi-tui',
name: () => '@earendil-works/pi-tui',
// pi-tui is bundled into main.cjs at build time — we don't collect it as
// a native dep, only register it so koffi can declare it as parent.
// pi-tui is bundled into main.cjs. Its native helpers are loaded from
// native/... beside the executable instead of from the native asset cache.
collect: 'virtual',
parent: null,
},
{
id: 'koffi',
name: () => 'koffi',
collect: 'js-and-native-file',
parent: 'pi-tui',
nativeFileRelatives: (target) => [`build/koffi/${koffiTripletByTarget[target]}/koffi.node`],
executableFileRelatives: (target) => piTuiExecutableFilesByTarget[target] ?? [],
},
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Package pi-tui native helpers with native builds

When building the native SEA artifacts for Windows/macOS, the bumped @earendil-works/pi-tui@0.79.3 now loads its own .node helpers from native/win32/.../win32-console-mode.node and native/darwin/.../darwin-modifiers.node, but this registry now only collects the clipboard packages and scripts/native/package.mjs zips only the executable. Those helpers will be absent from the native release, so Windows VT input support for modified keys such as Shift+Tab, and Apple Terminal Shift+Enter detection, silently regress. Please either collect/copy pi-tui's native files or otherwise make them available next to the native executable.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in d45d7b0 — pi-tui helpers now get copied next to the native executable and included in the release zip.

also added macOS signing/verification for those helpers, included the sibling native/ dir in the notary submission, and made the native smoke check fail if the helper is missing.


export function resolveExecutableFileRelatives(target) {
if (!isSupportedTarget(target)) {
throw new Error(`Unsupported native executable target: ${target}`);
}
return nativeDeps.flatMap((d) => d.executableFileRelatives?.(target) ?? []);
}

/**
* Resolve which deps need collecting for a given build target, with concrete names.
*/
Expand Down
Loading