Skip to content
11 changes: 6 additions & 5 deletions RELEASE-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,14 @@ Method: full catalog copy extraction (names, summaries, activation hints, pack t
**Known limitations (release notes, no code):**
3. Launcher `activationMode=leaderSequence` keeps the Hyper hold path active (config-identical to holdHyper). Note in release docs until intent is decided.
4. HRL Toggles toggle-mode without companion layers: keys silently no-op (stub-deflayer safety net). Covered by sprint epic #865.
5. **Hand-written `(cmd ...)` kanata actions no longer work** — the bundled engine is now compiled without kanata's `cmd` feature ([#879](https://github.com/malpern/KeyPath/issues/879)): the root daemon cannot execute shell commands regardless of config contents. Configs merely carrying the legacy `danger-enable-cmd yes` header still load; configs *using* `(cmd ...)` fail validation with kanata's "cmd is not enabled for this executable" message. Supported alternative: KeyPath's consent-gated script actions, found under **Settings → Script Execution** (run as the user, not root; see the Running Scripts guide).

**Post-1.0 design backlog (umbrella issue):**
5. **Timing vocabulary chaos** — "tap window / hold delay / tap offset / hold offset / quick tap term / prior idle" used across editors without a shared mental model; raw ms fields leak implementation names (`requirePriorIdleMs`, `hrm-stats`).
6. **Two magic keys, no explanation** — "Leader" (most layers) vs "Hyper" (Quick Launcher, provided by Caps Lock Remap). Nothing at catalog level explains the difference or the dependency.
7. **Activation-hint format inconsistency** — "Leader → f → function keys" vs "Hold Hyper key" vs "11 keys · 180ms hold" (a spec, not an instruction); 9 families have no hint at all.
8. **"Ben Vallack" naming** — two families named after a YouTuber; meaningless to most users.
9. **Agent's top-10 string fixes** — e.g. "Raw values" → "Expert mode", "Favor tap when another key is pressed (quick tap)" → clearer phrasing, "Protect fast typing" → say what it does, Key Repeat "Speed" is actually an interval. Full list preserved in the umbrella issue.
6. **Timing vocabulary chaos** — "tap window / hold delay / tap offset / hold offset / quick tap term / prior idle" used across editors without a shared mental model; raw ms fields leak implementation names (`requirePriorIdleMs`, `hrm-stats`).
7. **Two magic keys, no explanation** — "Leader" (most layers) vs "Hyper" (Quick Launcher, provided by Caps Lock Remap). Nothing at catalog level explains the difference or the dependency.
8. **Activation-hint format inconsistency** — "Leader → f → function keys" vs "Hold Hyper key" vs "11 keys · 180ms hold" (a spec, not an instruction); 9 families have no hint at all.
9. **"Ben Vallack" naming** — two families named after a YouTuber; meaningless to most users.
10. **Agent's top-10 string fixes** — e.g. "Raw values" → "Expert mode", "Favor tap when another key is pressed (quick tap)" → clearer phrasing, "Protect fast typing" → say what it does, Key Repeat "Speed" is actually an interval. Full list preserved in the umbrella issue.

### Overall design verdict

Expand Down
26 changes: 21 additions & 5 deletions Scripts/build-kanata.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,29 @@ mkdir -p "$BUILD_DIR"
echo "📁 Kanata source: $KANATA_SOURCE"
echo "📁 Build directory: $BUILD_DIR"

# Cargo features for the production engine. Deliberately excludes `cmd` (#879):
# KeyPath actions are all push-msg/TCP dispatched in-app; compiling cmd out
# removes the root daemon's ability to spawn processes no matter what a
# (user-writable) config says. Participates in the cache key below — changing
# features must invalidate the cached binary or the old capability ships.
KANATA_FEATURES="tcp_server"

# TCC-Safe Caching Logic
function calculate_source_hash() {
# Generate hash based on kanata source files (excluding build artifacts).
# Includes C/C++ sources: the fork vendors the karabiner-driverkit crate
# (driverkit/c_src), and a .cpp/.hpp-only change must invalidate the cache
# or a stale engine silently ships (bit MAL-57 Layer 3).
# Also folds in the cargo feature set: a feature change (e.g. dropping
# `cmd`, #879) alters the binary without touching any source file.
cd "$KANATA_SOURCE"
find . \( -name "*.rs" -o -name "*.toml" -o -name "*.lock" \
-o -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) \
-not -path "./target/*" \
-exec shasum -a 256 {} + 2>/dev/null | shasum -a 256 | cut -d' ' -f1
{
find . \( -name "*.rs" -o -name "*.toml" -o -name "*.lock" \
-o -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) \
-not -path "./target/*" \
-exec shasum -a 256 {} + 2>/dev/null
echo "features=$KANATA_FEATURES"
} | shasum -a 256 | cut -d' ' -f1
}

function check_cache_validity() {
Expand Down Expand Up @@ -119,10 +131,14 @@ rustup target add x86_64-apple-darwin >/dev/null 2>&1 || true
# Build for ARM64 (Apple Silicon)
echo "🔨 Building for ARM64 (Apple Silicon)..."
cd "$KANATA_SOURCE"
# NOTE: features come from KANATA_FEATURES above — `cmd` is intentionally
# omitted (#879). Configs that merely carry `danger-enable-cmd yes` still load
# (the defcfg flag parses and is ignored); only actual `(cmd …)` actions fail
# at parse with a clear "cmd is not enabled for this executable" message.
MACOSX_DEPLOYMENT_TARGET=11.0 \
cargo build \
--release \
--features cmd,tcp_server \
--features "$KANATA_FEATURES" \
--target aarch64-apple-darwin

# Return to project root
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,6 @@
do {
let content = try await readFileAsync(path: configurationPath)

// One-time migration for the danger-enable-cmd default flip: a pre-existing
// config that uses (cmd ...) actions grandfathers the policy ON before any
// regeneration could strip the header line. No-op once decided.
KanataCommandActionsPolicy.grandfatherIfNeeded(configContent: content)

let contentHash = SHA256.hash(data: Data(content.utf8))
.map { String(format: "%02x", $0) }.joined()
if contentHash == lastContentHash, let cached = currentConfiguration {
Expand Down Expand Up @@ -277,22 +272,6 @@
ruleCollections: [RuleCollection],
customRules: [CustomRule] = []
) async throws -> KanataConfiguration {
// Grandfather the cmd-actions policy against the on-disk config BEFORE
// generating: startup bootstrap regenerates (and saves) without any prior
// reload(), so the reload() hook alone would run too late — after the
// hand-written (cmd ...) config this migration must inspect was already
// overwritten. Generation below evaluates the policy via the
// allowCommandActions default, so the decision must be recorded first.
// Check-then-act is safe here: grandfatherIfNeeded re-checks internally
// and always records the same value for the same config (idempotent), so
// concurrent callers can't disagree.
if !KanataCommandActionsPolicy.hasRecordedDecision(),
Foundation.FileManager.default.fileExists(atPath: configurationPath),
let existingContent = try? String(contentsOfFile: configurationPath, encoding: .utf8)
{
KanataCommandActionsPolicy.grandfatherIfNeeded(configContent: existingContent)
}

// Custom rules come first so they take priority over preset collections
let customRuleCollections = customRules.asRuleCollections()
AppLogger.shared.log("🔧 [ConfigService] Converting \(customRules.count) custom rules to \(customRuleCollections.count) collections")
Expand Down Expand Up @@ -483,9 +462,7 @@
if lowerError.contains("missing"), lowerError.contains("defcfg") {
// Add missing defcfg using the shared single-source-of-truth header.
if !repairedConfig.contains("(defcfg") {
let defcfgSection = KanataDefcfg.repairFallback(
allowCommandActions: KanataCommandActionsPolicy.isEnabled()
).render() + "\n\n"
let defcfgSection = KanataDefcfg.repairFallback.render() + "\n\n"
repairedConfig = defcfgSection + repairedConfig
}
}
Expand Down Expand Up @@ -546,22 +523,25 @@
}

/// Save a repaired config (from AI repair)
///
/// No `danger-enable-cmd` sanitization is performed here on purpose. The
/// bundled kanata engine is compiled without the `cmd` feature (#879), so a
/// legacy `danger-enable-cmd yes` header is inert — kanata loads the config
/// and logs "compiled to never allow cmd"; only configs that actually *use*
/// `(cmd ...)` fail validation. The runtime stripping that used to live here
/// (KanataCommandActionsPolicy.enforcingPolicy) is therefore redundant and
/// was removed with the policy. The repair prompt also instructs the model
/// not to emit the header.
public func saveRepairedConfig(_ repairedContent: String) async throws {
AppLogger.shared.log("💾 [Config] Saving AI-repaired config")

// The repair model is prompted to respect the command-actions policy but is
// never trusted with the safety header: with the policy OFF, any
// danger-enable-cmd grant in model output is stripped before it reaches the
// root daemon's config (#860).
let enforcedContent = KanataCommandActionsPolicy.enforcingPolicy(on: repairedContent)

let configURL = URL(fileURLWithPath: configurationPath)
try await writeFileURLAsync(string: enforcedContent, to: configURL)
try await writeFileURLAsync(string: repairedContent, to: configURL)

// Update current configuration
setCurrentConfiguration(
KanataConfiguration(
content: enforcedContent,
content: repairedContent,
keyMappings: [], // Will be re-parsed on next reload
lastModified: Date(),
path: configurationPath
Expand Down Expand Up @@ -734,7 +714,7 @@
let uppercasedInput = input.uppercased()

for prefix in modifierPrefixes {
if uppercasedInput.hasPrefix(prefix) {

Check warning on line 717 in Sources/KeyPathAppKit/Infrastructure/Config/ConfigurationService.swift

View workflow job for this annotation

GitHub Actions / code-quality

`where` clauses are preferred over a single `if` inside a `for` (for_where)
// Preserve canonical uppercase prefix, convert base key
let baseKey = String(input.dropFirst(prefix.count))
let convertedBase = convertToKanataKey(baseKey)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,13 @@

/// Generate configuration content from rule collections.
/// Flattens enabled collections to `defsrc`/`deflayer` for backward compatibility with Kanata config format.
/// `allowCommandActions` controls the `danger-enable-cmd` defcfg line; it defaults to the
/// user's recorded `KanataCommandActionsPolicy` (OFF unless opted in or grandfathered).
public static func generateFromCollections(
_ collections: [RuleCollection],
leaderKeyPreference: LeaderKeyPreference? = nil,
navActivationMode: ContextHUDTriggerMode = .tapToToggle,
navHoldDelayMs: Int = 200,
chordGroups: [ChordGroupConfig] = [],
sequences: [KanataDefseqParser.ParsedSequence] = [],
allowCommandActions: Bool = KanataCommandActionsPolicy.isEnabled()
sequences: [KanataDefseqParser.ParsedSequence] = []
) -> String {
var resolvedCollections = collections.isEmpty ? defaultSystemCollections : collections
if !resolvedCollections.contains(where: { $0.id == RuleCollectionIdentifier.macFunctionKeys }) {
Expand Down Expand Up @@ -93,7 +90,7 @@
let mergedAliasDefinitions = deduplicateAliases(aliasDefinitions)
AppLogger.shared
.log(
"📚 [KanataConfig] Total alias definitions: \(mergedAliasDefinitions.count) (before dedup: \(aliasDefinitions.count)), first 5: \(mergedAliasDefinitions.prefix(5).map(\.aliasName).joined(separator: ", "))"

Check warning on line 93 in Sources/KeyPathAppKit/Infrastructure/Config/KanataConfigurationGenerator.swift

View workflow job for this annotation

GitHub Actions / code-quality

Line should be 200 characters or less; currently it has 220 characters (line_length)
)
let blocks = deduplicateBlocks(rawBlocks)
let enabledNames = enabledCollections.map(\.name).joined(separator: ", ")
Expand All @@ -111,7 +108,6 @@
return (krc.globalDelayMs, krc.globalIntervalMs)
}()
let defcfg = KanataDefcfg.standard(
allowCommandActions: allowCommandActions,
managedRepeatTiming: repeatTiming,
requirePriorIdleMs: requirePriorIdleMs > 0 ? requirePriorIdleMs : nil,
hasChords: !chordMappings.isEmpty,
Expand Down
Loading
Loading