kanade-shared 0.48.2

Shared wire types, NATS subject helpers, KV constants, YAML manifest schema, and teravars-backed config loader for the kanade endpoint-management system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
//! Which platform an agent binary targets, read from its own bytes — the
//! single source of truth for the `agent_releases` Object Store key scheme.
//!
//! Key scheme (backward compatible):
//!
//!   * Windows releases stay at the **bare `<version>`** key — agents in
//!     the field fetch exactly that today, so any change would be a
//!     migration. (Windows aarch64 also maps to the bare key: the fleet is
//!     x86_64 in practice, and distinguishing it is a future problem.)
//!   * Linux releases live at **`<version>-linux-<arch>`** (`x86_64` /
//!     `aarch64`), macOS releases at **`<version>-macos-aarch64`** (Apple
//!     Silicon only — Intel Macs are unsupported; macOS 27 dropped Intel).
//!     Semver prerelease dashes are fine because consumers always match
//!     the *suffix* (`-linux-x86_64` / `-macos-aarch64` / …), never a dash
//!     mid-version.
//!
//! Detection is by magic bytes, not filename: `MZ` → PE (arch from the
//! COFF header's Machine field), `\x7fELF` → ELF (arch from `e_machine`),
//! thin 64-bit Mach-O → macOS (arch from `cputype`; only arm64 is
//! accepted, an x86_64 Mach-O is a clear "Intel Macs unsupported" error).
//! Universal (fat) Mach-O and 32-bit Mach-O are clear errors: a release
//! key names exactly one arch, so the operator publishes one thin binary
//! per arch. Pure byte inspection, no parsing library — the PE path only
//! needs `e_lfanew` + the COFF Machine field, which `pelite` (used for
//! VERSIONINFO in `exe_version.rs`) doesn't surface as a bare number
//! without dragging in its full header model.

/// Object Store key suffix for Linux x86_64 releases.
pub const LINUX_SUFFIX_X86_64: &str = "-linux-x86_64";
/// Object Store key suffix for Linux aarch64 releases.
pub const LINUX_SUFFIX_AARCH64: &str = "-linux-aarch64";
/// Both Linux suffixes, for "any Linux key" scans (rollout checks).
pub const LINUX_SUFFIXES: [&str; 2] = [LINUX_SUFFIX_X86_64, LINUX_SUFFIX_AARCH64];

/// Object Store key suffix for macOS aarch64 (Apple Silicon) releases —
/// the only macOS platform kanade supports.
pub const MACOS_SUFFIX_AARCH64: &str = "-macos-aarch64";

/// Every platform key suffix (all non-Windows platforms), in the order
/// rollout existence checks probe them after the bare Windows key.
pub const PLATFORM_SUFFIXES: [&str; 3] = [
    LINUX_SUFFIX_X86_64,
    LINUX_SUFFIX_AARCH64,
    MACOS_SUFFIX_AARCH64,
];

/// The platform an uploaded agent binary runs on.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentPlatform {
    WindowsX86_64,
    WindowsAarch64,
    LinuxX86_64,
    LinuxAarch64,
    MacOSAarch64,
}

impl AgentPlatform {
    /// Identify the platform from the binary's leading bytes. Errors
    /// (rather than guesses) on universal / 32-bit Mach-O, on a recognized
    /// container with an unsupported architecture, and on anything
    /// unrecognized — a publish that can't name its platform must not
    /// silently land on the Windows key.
    pub fn detect(bytes: &[u8]) -> Result<AgentPlatform, String> {
        if bytes.starts_with(b"MZ") {
            return detect_pe(bytes);
        }
        if bytes.starts_with(b"\x7fELF") {
            return detect_elf(bytes);
        }
        if let Some(result) = detect_macho(bytes) {
            return result;
        }
        Err(
            "unrecognized binary format (not a Windows PE, Linux ELF, or macOS Mach-O) — is this \
             a kanade-agent build?"
                .to_string(),
        )
    }

    /// The `agent_releases` Object Store key for `version` on this
    /// platform. Windows → the bare version (full backward compatibility);
    /// Linux / macOS → the arch-suffixed key.
    pub fn release_key(&self, version: &str) -> String {
        match self.suffix() {
            Some(suffix) => format!("{version}{suffix}"),
            None => version.to_string(),
        }
    }

    /// The key suffix for this platform, `None` for Windows (bare key).
    pub fn suffix(&self) -> Option<&'static str> {
        match self {
            AgentPlatform::WindowsX86_64 | AgentPlatform::WindowsAarch64 => None,
            AgentPlatform::LinuxX86_64 => Some(LINUX_SUFFIX_X86_64),
            AgentPlatform::LinuxAarch64 => Some(LINUX_SUFFIX_AARCH64),
            AgentPlatform::MacOSAarch64 => Some(MACOS_SUFFIX_AARCH64),
        }
    }

    /// Human label for audit records / logs (`"windows-x86_64"`, …).
    pub fn as_str(&self) -> &'static str {
        match self {
            AgentPlatform::WindowsX86_64 => "windows-x86_64",
            AgentPlatform::WindowsAarch64 => "windows-aarch64",
            AgentPlatform::LinuxX86_64 => "linux-x86_64",
            AgentPlatform::LinuxAarch64 => "linux-aarch64",
            AgentPlatform::MacOSAarch64 => "macos-aarch64",
        }
    }
}

/// The platform label for an existing store key, derived from its suffix:
/// `"linux-x86_64"` / `"linux-aarch64"` / `"macos-aarch64"` for suffixed
/// keys, `"windows"` for anything else (bare
/// keys are Windows by definition of the key scheme). Used by the releases
/// listing, which only has keys to look at.
pub fn platform_of_key(key: &str) -> &'static str {
    if key.ends_with(LINUX_SUFFIX_X86_64) {
        "linux-x86_64"
    } else if key.ends_with(LINUX_SUFFIX_AARCH64) {
        "linux-aarch64"
    } else if key.ends_with(MACOS_SUFFIX_AARCH64) {
        "macos-aarch64"
    } else {
        "windows"
    }
}

/// The rollout-visible version for a store key: the key with any platform
/// suffix (linux / macos) stripped. A scope's `target_version` always
/// names the BASE version (`0.46.0`, never `0.46.0-linux-x86_64`) — the
/// agent's own platform decides which suffixed binary it fetches — so
/// guards that compare a key against `target_version` must go through
/// this.
pub fn base_version_of_key(key: &str) -> &str {
    for suffix in PLATFORM_SUFFIXES {
        if let Some(base) = key.strip_suffix(suffix) {
            return base;
        }
    }
    key
}

/// Every store key a rollout's existence check should accept for
/// `version`: the bare (Windows) key plus each linux and macOS platform
/// key. A version is rollable-out when ANY of these exists — a
/// Linux-only or macOS-only publish never writes the bare key.
pub fn candidate_keys(version: &str) -> Vec<String> {
    let mut keys = Vec::with_capacity(1 + PLATFORM_SUFFIXES.len());
    keys.push(version.to_string());
    for suffix in PLATFORM_SUFFIXES {
        keys.push(format!("{version}{suffix}"));
    }
    keys
}

/// The charset every `agent_releases` key must fit: the key reaches a
/// quoted `Content-Disposition` filename, generated PowerShell / batch /
/// shell install scripts, and NATS subjects — restrict the charset
/// (semver-ish) rather than escaping four different formats. Shared by the
/// backend publish endpoint, the CLI publish, and the installer endpoint
/// (which used to carry its own copy).
pub fn check_release_key(key: &str) -> Result<(), String> {
    if key.is_empty()
        || !key
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '+' | '-'))
    {
        return Err(format!(
            "release key must be non-empty and contain only [A-Za-z0-9._+-], got {key:?}"
        ));
    }
    Ok(())
}

/// PE: `MZ` DOS stub, `e_lfanew` (u32 LE at 0x3C) → `"PE\0\0"` signature,
/// COFF Machine field (u16 LE right after). 0x8664 = x86_64, 0xAA64 =
/// aarch64 (ARM64EC uses the same field for our purposes — a native ARM64
/// agent build reports 0xAA64).
fn detect_pe(bytes: &[u8]) -> Result<AgentPlatform, String> {
    let truncated = || "truncated PE header".to_string();
    let pe_off = u32::from_le_bytes(
        bytes
            .get(0x3C..0x40)
            .ok_or_else(truncated)?
            .try_into()
            .map_err(|_| truncated())?,
    ) as usize;
    let coff = bytes.get(pe_off..pe_off + 6).ok_or_else(truncated)?;
    if coff[..4] != *b"PE\0\0" {
        return Err("MZ binary without a PE signature".to_string());
    }
    match u16::from_le_bytes([coff[4], coff[5]]) {
        0x8664 => Ok(AgentPlatform::WindowsX86_64),
        0xAA64 => Ok(AgentPlatform::WindowsAarch64),
        other => Err(format!(
            "unsupported PE machine type 0x{other:04X} (kanade-agent ships x86_64 and aarch64 only)"
        )),
    }
}

/// ELF: magic, then `e_machine` (u16 at offset 18) read with the
/// endianness `EI_DATA` (offset 5) declares. 62 = EM_X86_64, 183 =
/// EM_AARCH64.
fn detect_elf(bytes: &[u8]) -> Result<AgentPlatform, String> {
    let truncated = || "truncated ELF header".to_string();
    let ei_data = bytes.get(5).ok_or_else(truncated)?;
    let em = bytes.get(18..20).ok_or_else(truncated)?;
    let machine = match ei_data {
        1 => u16::from_le_bytes([em[0], em[1]]),
        2 => u16::from_be_bytes([em[0], em[1]]),
        other => return Err(format!("unknown ELF endianness {other}")),
    };
    match machine {
        62 => Ok(AgentPlatform::LinuxX86_64),
        183 => Ok(AgentPlatform::LinuxAarch64),
        other => Err(format!(
            "unsupported ELF machine {other} (kanade-agent ships x86_64 (62) and aarch64 (183) \
             only)"
        )),
    }
}

/// Mach-O: `None` when the bytes carry no Mach-O magic at all (so the
/// caller falls through to "unrecognized"), otherwise the verdict. Thin
/// 64-bit images (`MH_MAGIC_64` in either byte order) are identified by
/// `cputype` (i32 at offset 4, in the header's byte order):
/// `CPU_TYPE_ARM64` = 0x0100000C is accepted; `CPU_TYPE_X86_64` =
/// 0x01000007 is rejected (Intel Macs are unsupported — Apple Silicon
/// only). Universal (fat) wrappers are rejected — a release key names one
/// arch, so the operator must `lipo -thin` — as is 32-bit Mach-O.
fn detect_macho(bytes: &[u8]) -> Option<Result<AgentPlatform, String>> {
    const MH_MAGIC_64_BE: [u8; 4] = [0xFE, 0xED, 0xFA, 0xCF];
    const MH_MAGIC_64_LE: [u8; 4] = [0xCF, 0xFA, 0xED, 0xFE];
    const MH_MAGIC_32: [[u8; 4]; 2] = [
        [0xFE, 0xED, 0xFA, 0xCE], // MH_MAGIC (BE)
        [0xCE, 0xFA, 0xED, 0xFE], // MH_CIGAM (LE)
    ];
    // 0xCAFEBABE is also the Java class-file magic; either way it is not
    // a publishable agent, so it lands on the same rejection.
    const FAT_MAGICS: [[u8; 4]; 4] = [
        [0xCA, 0xFE, 0xBA, 0xBE], // FAT_MAGIC
        [0xBE, 0xBA, 0xFE, 0xCA], // FAT_CIGAM
        [0xCA, 0xFE, 0xBA, 0xBF], // FAT_MAGIC_64
        [0xBF, 0xBA, 0xFE, 0xCA], // FAT_CIGAM_64
    ];

    let little_endian = if bytes.starts_with(&MH_MAGIC_64_LE) {
        true
    } else if bytes.starts_with(&MH_MAGIC_64_BE) {
        false
    } else if MH_MAGIC_32.iter().any(|m| bytes.starts_with(m)) {
        return Some(Err(
            "32-bit Mach-O is not supported — kanade-agent ships 64-bit Apple Silicon \
             (arm64) macOS builds only"
                .to_string(),
        ));
    } else if FAT_MAGICS.iter().any(|m| bytes.starts_with(m)) {
        return Some(Err(
            "universal (fat) Mach-O binaries are not supported — publish one thin binary per \
             arch (e.g. `lipo -thin arm64 kanade-agent -output kanade-agent-arm64`)"
                .to_string(),
        ));
    } else {
        return None;
    };

    let Some(cpu) = bytes.get(4..8) else {
        return Some(Err("truncated Mach-O header".to_string()));
    };
    let cpu = [cpu[0], cpu[1], cpu[2], cpu[3]];
    let cputype = if little_endian {
        u32::from_le_bytes(cpu)
    } else {
        u32::from_be_bytes(cpu)
    };
    Some(match cputype {
        0x0100_000C => Ok(AgentPlatform::MacOSAarch64),
        0x0100_0007 => Err(
            "Intel (x86_64) macOS agents are not supported — kanade supports Apple Silicon \
             (arm64) Macs only"
                .to_string(),
        ),
        other => Err(format!(
            "unsupported Mach-O cputype 0x{other:08X} (kanade-agent ships Apple Silicon arm64 \
             macOS builds only)"
        )),
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Minimal MZ+PE: DOS stub with e_lfanew at 0x80, signature + Machine.
    fn fake_pe(machine: u16) -> Vec<u8> {
        let mut b = vec![0u8; 0x80 + 6];
        b[0] = b'M';
        b[1] = b'Z';
        b[0x3C..0x40].copy_from_slice(&0x80u32.to_le_bytes());
        b[0x80..0x84].copy_from_slice(b"PE\0\0");
        b[0x84..0x86].copy_from_slice(&machine.to_le_bytes());
        b
    }

    /// Minimal ELF ident + e_machine.
    fn fake_elf(machine: u16, little_endian: bool) -> Vec<u8> {
        let mut b = vec![0u8; 20];
        b[..4].copy_from_slice(b"\x7fELF");
        b[4] = 2; // ELFCLASS64
        b[5] = if little_endian { 1 } else { 2 };
        if little_endian {
            b[18..20].copy_from_slice(&machine.to_le_bytes());
        } else {
            b[18..20].copy_from_slice(&machine.to_be_bytes());
        }
        b
    }

    #[test]
    fn detects_pe_architectures() {
        assert_eq!(
            AgentPlatform::detect(&fake_pe(0x8664)).unwrap(),
            AgentPlatform::WindowsX86_64
        );
        assert_eq!(
            AgentPlatform::detect(&fake_pe(0xAA64)).unwrap(),
            AgentPlatform::WindowsAarch64
        );
        // An MZ with a machine we don't ship is an error, not a guess.
        assert!(AgentPlatform::detect(&fake_pe(0x14C)).is_err()); // i386
        // MZ but no PE signature (a DOS binary) is an error too.
        let mut b = fake_pe(0x8664);
        b[0x80] = b'X';
        assert!(AgentPlatform::detect(&b).is_err());
        // Truncated just past MZ.
        assert!(AgentPlatform::detect(b"MZ").is_err());
    }

    #[test]
    fn detects_elf_architectures_and_endianness() {
        assert_eq!(
            AgentPlatform::detect(&fake_elf(62, true)).unwrap(),
            AgentPlatform::LinuxX86_64
        );
        assert_eq!(
            AgentPlatform::detect(&fake_elf(183, true)).unwrap(),
            AgentPlatform::LinuxAarch64
        );
        // e_machine honors EI_DATA — a big-endian-encoded aarch64 still
        // reads as aarch64.
        assert_eq!(
            AgentPlatform::detect(&fake_elf(183, false)).unwrap(),
            AgentPlatform::LinuxAarch64
        );
        assert!(AgentPlatform::detect(&fake_elf(40, true)).is_err()); // ARM 32
        assert!(AgentPlatform::detect(b"\x7fELF").is_err()); // truncated
    }

    /// Minimal thin 64-bit Mach-O header: magic + cputype.
    fn fake_macho(cputype: u32, little_endian: bool) -> Vec<u8> {
        let mut b = vec![0u8; 8];
        if little_endian {
            b[..4].copy_from_slice(&[0xCF, 0xFA, 0xED, 0xFE]);
            b[4..8].copy_from_slice(&cputype.to_le_bytes());
        } else {
            b[..4].copy_from_slice(&[0xFE, 0xED, 0xFA, 0xCF]);
            b[4..8].copy_from_slice(&cputype.to_be_bytes());
        }
        b
    }

    #[test]
    fn detects_macho_architectures_by_cputype() {
        // Intel Macs are unsupported: an x86_64 Mach-O is a clear error in
        // either byte order, never a platform.
        for le in [true, false] {
            let err = AgentPlatform::detect(&fake_macho(0x0100_0007, le)).unwrap_err();
            assert!(
                err.contains("Intel (x86_64) macOS agents are not supported"),
                "{err}"
            );
            assert!(err.contains("Apple Silicon"), "{err}");
        }
        assert_eq!(
            AgentPlatform::detect(&fake_macho(0x0100_000C, true)).unwrap(),
            AgentPlatform::MacOSAarch64
        );
        // cputype honors the magic's byte order.
        assert_eq!(
            AgentPlatform::detect(&fake_macho(0x0100_000C, false)).unwrap(),
            AgentPlatform::MacOSAarch64
        );
        // 32-bit ARM (CPU_TYPE_ARM = 12) in a 64-bit header: error, not a guess.
        assert!(AgentPlatform::detect(&fake_macho(12, true)).is_err());
        // Truncated right after the magic.
        assert!(AgentPlatform::detect(&[0xCF, 0xFA, 0xED, 0xFE]).is_err());
    }

    #[test]
    fn fat_and_32bit_macho_are_clear_errors() {
        for magic in [[0xCA, 0xFE, 0xBA, 0xBE], [0xBF, 0xBA, 0xFE, 0xCA]] {
            let err = AgentPlatform::detect(&magic).unwrap_err();
            assert!(err.contains("thin binary per arch"), "{err}");
        }
        for magic in [[0xFE, 0xED, 0xFA, 0xCE], [0xCE, 0xFA, 0xED, 0xFE]] {
            let err = AgentPlatform::detect(&magic).unwrap_err();
            assert!(err.contains("32-bit Mach-O"), "{err}");
        }
    }

    #[test]
    fn unknown_bytes_are_an_error_not_a_windows_guess() {
        assert!(AgentPlatform::detect(b"#!/bin/sh\necho hi").is_err());
        assert!(AgentPlatform::detect(b"").is_err());
    }

    #[test]
    fn release_keys_follow_the_scheme() {
        // Windows stays bare — the whole backward-compatibility point.
        assert_eq!(AgentPlatform::WindowsX86_64.release_key("0.45.4"), "0.45.4");
        assert_eq!(
            AgentPlatform::WindowsAarch64.release_key("0.45.4"),
            "0.45.4"
        );
        assert_eq!(
            AgentPlatform::LinuxX86_64.release_key("0.45.4"),
            "0.45.4-linux-x86_64"
        );
        assert_eq!(
            AgentPlatform::LinuxAarch64.release_key("0.45.4"),
            "0.45.4-linux-aarch64"
        );
        assert_eq!(
            AgentPlatform::MacOSAarch64.release_key("0.45.4"),
            "0.45.4-macos-aarch64"
        );
        // Semver prerelease dashes are untouched — only the suffix matters.
        assert_eq!(
            AgentPlatform::LinuxX86_64.release_key("0.46.0-rc.1"),
            "0.46.0-rc.1-linux-x86_64"
        );
        assert!(check_release_key(&AgentPlatform::LinuxAarch64.release_key("0.46.0-rc.1")).is_ok());
    }

    #[test]
    fn platform_of_key_reads_the_suffix_only() {
        assert_eq!(platform_of_key("0.45.4"), "windows");
        assert_eq!(platform_of_key("0.45.4-linux-x86_64"), "linux-x86_64");
        assert_eq!(platform_of_key("0.45.4-linux-aarch64"), "linux-aarch64");
        assert_eq!(platform_of_key("0.45.4-macos-aarch64"), "macos-aarch64");
        // A version whose PRERELEASE mentions linux still parses by suffix:
        // `-linux-x86_64` wins, but a bare `1.0.0-linux` is a Windows key
        // (no arch suffix — odd, but the suffix rule is the contract).
        assert_eq!(platform_of_key("1.0.0-linux"), "windows");
        assert_eq!(platform_of_key("1.0.0-rc-linux-x86_64"), "linux-x86_64");
    }

    #[test]
    fn base_version_strips_only_the_platform_suffix() {
        assert_eq!(base_version_of_key("0.45.4"), "0.45.4");
        assert_eq!(base_version_of_key("0.45.4-linux-x86_64"), "0.45.4");
        assert_eq!(base_version_of_key("0.45.4-linux-aarch64"), "0.45.4");
        assert_eq!(base_version_of_key("0.45.4-macos-aarch64"), "0.45.4");
        // Prerelease dashes are not platform suffixes.
        assert_eq!(base_version_of_key("0.46.0-rc.1"), "0.46.0-rc.1");
        assert_eq!(
            base_version_of_key("0.46.0-rc.1-linux-x86_64"),
            "0.46.0-rc.1"
        );
    }

    #[test]
    fn candidate_keys_cover_bare_then_linux_then_macos() {
        assert_eq!(
            candidate_keys("0.46.0"),
            vec![
                "0.46.0".to_string(),
                "0.46.0-linux-x86_64".to_string(),
                "0.46.0-linux-aarch64".to_string(),
                "0.46.0-macos-aarch64".to_string(),
            ]
        );
    }

    #[test]
    fn release_key_charset_is_restricted() {
        for bad in ["", "0.43.99\n evil", "0.43.99\"x", "0.43.99'x", "a b"] {
            assert!(check_release_key(bad).is_err(), "{bad:?}");
        }
        for good in [
            "0.43.99",
            "0.43.99-rc.1+build.5",
            "1.0.0_alpha",
            "0.45.4-linux-x86_64",
        ] {
            check_release_key(good).unwrap();
        }
    }
}