objectiveai_cli/filesystem/tools/manifest.rs
1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4/// Per-OS exec command. Reused from the SDK so the exec shape stays
5/// identical across the on-disk manifest and the `tools get` wire
6/// response.
7pub use objectiveai_sdk::cli::command::tools::get::Exec;
8
9/// Per-CPU-architecture cli bundle filenames for a single OS. Each entry
10/// is the GitHub-release `.zip` asset filename for that arch (or absent
11/// when not built for it). Arch keys follow the repo-wide convention
12/// (`x86_64` / `aarch64`) — the same names the release workflow uses for
13/// its assets, which is exactly what these values reference.
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
15#[schemars(rename = "filesystem.tools.CliZipArch")]
16pub struct CliZipArch {
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 #[schemars(extend("omitempty" = true))]
19 pub x86_64: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 #[schemars(extend("omitempty" = true))]
22 pub aarch64: Option<String>,
23}
24
25impl CliZipArch {
26 /// True when no arch entry is present — used by [`CliZip`]'s
27 /// `skip_serializing_if` so an OS with no bundles is omitted (and a
28 /// fully-empty `cli_zip` still serializes as `{}`).
29 pub fn is_empty(&self) -> bool {
30 self.x86_64.is_none() && self.aarch64.is_none()
31 }
32}
33
34/// Per-OS cli bundle: the GitHub-release `.zip` asset filenames for each
35/// platform, split per CPU architecture (`x86_64` / `aarch64`). At
36/// install time the current platform's entry — matched on OS *and* arch —
37/// is downloaded and extracted into the `cli/` working directory. Local
38/// to the CLI's filesystem layer (the on-disk shape); shared by both the
39/// tool and plugin manifests. The `cli_zip` field itself is required;
40/// each per-OS and per-arch entry is optional — absent means nothing to
41/// fetch for that platform (e.g. a hand-placed source-run tool, or a
42/// viewer-only plugin).
43#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
44#[schemars(rename = "filesystem.tools.CliZip")]
45pub struct CliZip {
46 #[serde(default, skip_serializing_if = "CliZipArch::is_empty")]
47 pub windows: CliZipArch,
48 #[serde(default, skip_serializing_if = "CliZipArch::is_empty")]
49 pub linux: CliZipArch,
50 #[serde(default, skip_serializing_if = "CliZipArch::is_empty")]
51 pub macos: CliZipArch,
52}
53
54/// Declarative metadata a local tool ships with — the on-disk
55/// `objectiveai.json` at
56/// `<base_dir>/tools/<owner>/<name>/<version>/objectiveai.json`. The
57/// CLI reads and writes this shape verbatim; the `tools get` wire
58/// response is a lean projection of it (it drops `cli_zip`).
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
60#[schemars(rename = "filesystem.tools.Manifest")]
61pub struct Manifest {
62 /// GitHub-style owner (user or org) of the tool's source repo.
63 pub owner: String,
64 /// The tool's identifier (matches the `<name>` directory segment).
65 pub name: String,
66 /// Version string. Free-form; semver recommended, not enforced.
67 pub version: String,
68 /// One-line description of what the tool does. Surfaced to agents
69 /// that consume the tool.
70 pub description: String,
71 /// Per-OS exec command. The current platform's vector is the
72 /// program plus its leading arguments; the caller's `--args` are
73 /// appended and the whole thing runs with CWD = this version
74 /// folder's `cli/` subdir.
75 pub exec: Exec,
76 /// Per-OS cli bundle zip filenames (required field; each per-OS
77 /// entry optional). Hand-placed tools leave every entry absent.
78 pub cli_zip: CliZip,
79}
80
81impl Manifest {
82 /// Validate fields that serde alone can't enforce. Tools have no
83 /// cross-field invariants (unlike plugins' viewer rules), so this is
84 /// currently a no-op — it exists so the shared install/discovery
85 /// paths can call `validate()` uniformly across manifest kinds.
86 pub fn validate(&self) -> Result<(), &'static str> {
87 Ok(())
88 }
89}
90
91// Projection to the SDK's lean `tools get` wire response — drops the
92// on-disk-only `cli_zip`. `exec` is the same SDK `Exec` type, so it
93// moves across unchanged.
94impl From<Manifest> for objectiveai_sdk::cli::command::tools::get::ResponseManifest {
95 fn from(m: Manifest) -> Self {
96 Self {
97 owner: m.owner,
98 name: m.name,
99 version: m.version,
100 description: m.description,
101 exec: m.exec,
102 }
103 }
104}