agent-tools-interface 0.7.8

Agent Tools Interface — secure CLI for AI agent tool execution
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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
#![allow(dead_code, clippy::too_many_arguments, clippy::type_complexity)]

use clap::{Parser, Subcommand, ValueEnum};
use std::process;

mod cli;
mod core;
mod output;
mod providers;
mod proxy;
mod security;

#[derive(Debug, Clone, ValueEnum)]
pub enum OutputFormat {
    Json,
    Table,
    Text,
}

#[derive(Parser, Debug)]
#[command(
    name = "ati",
    about = "Agent Tools Interface — secure CLI for AI agent tool execution",
    version,
    long_about = "ATI provides secure, scoped access to external tools for AI agents running in sandboxes.\n\
                   Keys are encrypted and never exposed to the agent or environment."
)]
pub struct Cli {
    #[arg(
        long,
        value_enum,
        default_value = "json",
        global = true,
        env = "ATI_OUTPUT",
        alias = "format"
    )]
    pub output: OutputFormat,

    #[arg(
        short = 'J',
        long = "json",
        global = true,
        help = "Shorthand for --output json"
    )]
    pub json: bool,

    #[arg(long, global = true, help = "Enable verbose/debug output")]
    pub verbose: bool,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// Execute a tool by name
    #[command(
        // disable_help_flag so `ati run bb --help` passes --help through to
        // the CLI tool instead of showing ATI's own help. Without this, clap
        // intercepts --help before trailing_var_arg gets a chance. Use
        // `ati help run` or `ati run -h` for ATI's own run help.
        disable_help_flag = true,
        after_help = "Examples:\n  ati run web_search --query \"rust async\"\n  ati run github:search_repositories --query \"ati\" -J\n  ati run get_stock_quote --symbol AAPL --output json\n\nTip: run 'ati tool info <name>' to see associated skills and usage guidance."
    )]
    Run {
        /// Tool name (e.g. web_search)
        tool_name: String,
        /// Tool arguments as --key value pairs
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },

    /// List, inspect, and discover tools
    #[command(subcommand, alias = "tools")]
    Tool(ToolCommands),

    /// Manage skill files (methodology docs for agents)
    #[command(subcommand, alias = "skills")]
    Skill(SkillCommands),

    /// Lazily read remote skills from the GCS registry without installing them
    #[command(name = "skillati", subcommand)]
    SkillAti(SkillAtiCommands),

    /// LLM-powered tool discovery — ask what tool to use
    #[command(name = "assist")]
    Assist {
        /// Optional tool/provider scope, followed by the query
        #[arg(trailing_var_arg = true, required = true)]
        args: Vec<String>,
        /// Return a structured plan of tool calls instead of prose
        #[arg(long)]
        plan: bool,
        /// Save the plan to a file (implies --plan)
        #[arg(long)]
        save: Option<String>,
        /// Use local LLM (ollama/llama.cpp) — no API key needed
        #[arg(long)]
        local: bool,
    },

    /// Execute a saved tool plan
    #[command(subcommand)]
    Plan(PlanCommands),

    /// Unified provider management — MCP, OpenAPI, and HTTP providers
    #[command(
        subcommand,
        name = "provider",
        after_help = "Examples:\n  ati provider list\n  ati provider add-mcp serpapi --transport http --url https://mcp.serpapi.com/mcp\n  ati provider import-openapi https://api.example.com/openapi.json --name example\n  ati provider remove old_provider"
    )]
    Provider(ProviderCommands),

    /// Authentication and scope information
    #[command(subcommand)]
    Auth(AuthCommands),

    /// JWT token management — keygen, issue, inspect, validate
    #[command(subcommand)]
    Token(TokenCommands),

    /// Initialize ~/.ati/ directory structure
    Init {
        /// Configure for proxy mode (generates JWT secret)
        #[arg(long)]
        proxy: bool,
        /// Use ES256 key pair instead of HS256 secret (requires --proxy)
        #[arg(long)]
        es256: bool,
    },

    /// Manage API keys in ~/.ati/credentials
    #[command(subcommand)]
    Key(KeyCommands),

    /// Query the audit log
    #[command(subcommand)]
    Audit(AuditCommands),

    /// Run ATI as a proxy server (holds keys, serves sandbox agents)
    Proxy {
        /// Port to listen on
        #[arg(long, default_value = "8090")]
        port: u16,
        /// Bind address (default: 127.0.0.1; use 0.0.0.0 to listen on all interfaces)
        #[arg(long)]
        bind: Option<String>,
        /// ATI directory (manifests, keyring, scopes)
        #[arg(long)]
        ati_dir: Option<String>,
        /// Load API keys from environment variables instead of keyring.enc
        #[arg(long)]
        env_keys: bool,
    },
}

#[derive(Subcommand, Debug)]
pub enum ToolCommands {
    /// List available tools (filtered by your scopes)
    List {
        /// Filter by provider name
        #[arg(long)]
        provider: Option<String>,
    },
    /// Show detailed info about a specific tool
    Info {
        /// Tool name
        name: String,
    },
    /// Search tools by name, description, or tags
    Search {
        /// Search query (fuzzy matches on name, description, tags, category)
        query: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum SkillCommands {
    /// List available skills (with optional filters)
    List {
        /// Filter by category binding
        #[arg(long)]
        category: Option<String>,
        /// Filter by provider binding
        #[arg(long)]
        provider: Option<String>,
        /// Filter by tool binding
        #[arg(long)]
        tool: Option<String>,
    },
    /// Show a skill's content (prints SKILL.md)
    Show {
        /// Skill name
        name: String,
        /// Print only skill.toml metadata instead of SKILL.md
        #[arg(long)]
        meta: bool,
        /// Also print reference files
        #[arg(long)]
        refs: bool,
    },
    /// Search skills by name, description, keywords, or tools
    Search {
        /// Search query (fuzzy matches on name, description, keywords, tools)
        query: String,
    },
    /// Show skill.toml metadata and bindings
    Info {
        /// Skill name
        name: String,
    },
    /// Install a skill from a local directory, git URL, or HTTPS URL
    Install {
        /// Path or URL to skill (git URL, or local directory)
        source: String,
        /// Clone from a git repository URL (deprecated: URLs are auto-detected)
        #[arg(long)]
        from_git: Option<String>,
        /// Override skill name
        #[arg(long)]
        name: Option<String>,
        /// Install all skills from a multi-skill directory
        #[arg(long)]
        all: bool,
        /// Use local LLM (ollama) for manifest generation — zero network calls
        #[arg(long)]
        local: bool,
    },
    /// Remove an installed skill
    Remove {
        /// Skill name to remove
        name: String,
    },
    /// Scaffold a new skill directory
    Init {
        /// Skill name
        name: String,
        /// Pre-populate tool bindings (comma-separated)
        #[arg(long, value_delimiter = ',')]
        tools: Vec<String>,
        /// Pre-populate provider binding
        #[arg(long)]
        provider: Option<String>,
    },
    /// Validate a skill's skill.toml and check tool references
    Validate {
        /// Skill name
        name: String,
        /// Also verify tool references exist in manifests
        #[arg(long)]
        check_tools: bool,
    },
    /// Read skill content for agent consumption (no decoration)
    Read {
        /// Skill name (omit if using --tool)
        name: Option<String>,
        /// Read all skills bound to this tool
        #[arg(long)]
        tool: Option<String>,
        /// Inline reference file contents after SKILL.md
        #[arg(long)]
        with_refs: bool,
    },
    /// Show what skills auto-load for current scopes
    Resolve {
        /// Path to custom scopes.json
        #[arg(long)]
        scopes: Option<String>,
    },
    /// Verify integrity of an installed skill
    Verify {
        /// Skill name
        name: String,
    },
    /// Show diff between installed and source skill
    Diff {
        /// Skill source (URL or path, with optional @SHA)
        source: String,
    },
    /// Update an installed skill from its source
    Update {
        /// Skill name
        name: String,
        /// Force update even if content hash changed
        #[arg(long)]
        force: bool,
    },
    /// View remote skills via the lazy GCS registry
    Fetch {
        /// SkillATI-style subcommands (catalog, read, resources, cat, refs, ref, build-index)
        #[command(subcommand)]
        fetch: SkillAtiCommands,
    },
}

#[derive(Subcommand, Debug)]
pub enum SkillAtiCommands {
    /// List remote skills available from the GCS registry
    Catalog {
        /// Optional fuzzy search over remote skill name/description
        #[arg(long)]
        search: Option<String>,
    },
    /// Read SKILL.md for a remote skill from the GCS registry
    Read {
        /// Skill name
        name: String,
    },
    /// List bundled resources for a remote skill without reading file contents
    Resources {
        /// Skill name
        name: String,
        /// Optional resource prefix to filter on, e.g. references/ or scripts/
        #[arg(long)]
        prefix: Option<String>,
    },
    /// Read a skill-relative file path, including nested references, scripts, or assets
    Cat {
        /// Skill name
        name: String,
        /// Skill-relative path, e.g. references/foo.md or ../other-skill/SKILL.md
        path: String,
    },
    /// List available on-demand reference files for a remote skill
    Refs {
        /// Skill name
        name: String,
    },
    /// Read a single reference file for a remote skill
    Ref {
        /// Skill name
        name: String,
        /// Reference file name under references/
        reference: String,
    },
    /// Build a SkillATI catalog manifest from a local skills directory for GCS publishing
    #[command(name = "build-index")]
    BuildIndex {
        /// Directory containing one subdirectory per skill, or a single skill directory
        source_dir: String,
        /// Optional file path to write the manifest JSON to
        #[arg(long = "output-file")]
        output_file: Option<String>,
    },
}

#[derive(Subcommand, Debug)]
pub enum ProviderCommands {
    /// Add an MCP provider — generates a TOML manifest
    #[command(name = "add-mcp")]
    AddMcp {
        /// Provider name (used as manifest filename and tool prefix)
        name: String,
        /// Transport type: http or stdio
        #[arg(long)]
        transport: String,
        /// MCP server URL (required for http transport)
        #[arg(long)]
        url: Option<String>,
        /// Command to run (required for stdio transport)
        #[arg(long)]
        command: Option<String>,
        /// Arguments for the stdio command (repeatable)
        #[arg(long, allow_hyphen_values = true)]
        args: Vec<String>,
        /// Environment variables for stdio as KEY=VALUE (repeatable)
        #[arg(long)]
        env: Vec<String>,
        /// Auth type: none, bearer, header (default: none)
        #[arg(long)]
        auth: Option<String>,
        /// Keyring key name for auth (required for bearer/header auth)
        #[arg(long)]
        auth_key: Option<String>,
        /// Custom header name for header auth
        #[arg(long)]
        auth_header: Option<String>,
        /// Provider description (default: "{name} MCP provider")
        #[arg(long)]
        description: Option<String>,
        /// Provider category
        #[arg(long)]
        category: Option<String>,
    },

    /// Add a CLI provider — register a local CLI tool for use through ATI
    #[command(name = "add-cli")]
    AddCli {
        /// Provider name (becomes the tool name for `ati run <name>`)
        name: String,
        /// Path to CLI binary (or name to resolve via PATH)
        #[arg(long)]
        command: String,
        /// Default args prepended to every invocation
        #[arg(long)]
        default_args: Vec<String>,
        /// Environment variables as KEY=VALUE (use ${key} for keyring, @{key} for credential file)
        #[arg(long)]
        env: Vec<String>,
        /// Provider description
        #[arg(long)]
        description: Option<String>,
        /// Provider category
        #[arg(long)]
        category: Option<String>,
        /// Default timeout in seconds (default: 120)
        #[arg(long)]
        timeout: Option<u64>,
    },

    /// Import an OpenAPI spec — download to ~/.ati/specs/ and generate manifest
    #[command(name = "import-openapi")]
    ImportOpenapi {
        /// Path or URL to the OpenAPI spec (JSON or YAML)
        spec: String,
        /// Provider name (derived from spec URL/path if omitted)
        #[arg(long)]
        name: Option<String>,
        /// Keyring key name for the API key (default: {name}_api_key)
        #[arg(long)]
        auth_key: Option<String>,
        /// Only include operations with these tags
        #[arg(long)]
        include_tags: Vec<String>,
        /// Preview the generated manifest without saving
        #[arg(long)]
        dry_run: bool,
    },

    /// Inspect an OpenAPI spec — show operations, auth, base URL
    #[command(name = "inspect-openapi")]
    InspectOpenapi {
        /// Path or URL to the OpenAPI spec (JSON or YAML)
        spec: String,
        /// Only show operations with these tags
        #[arg(long)]
        include_tags: Vec<String>,
    },

    /// List all configured providers (HTTP, MCP, OpenAPI)
    List,

    /// Remove a provider manifest
    Remove {
        /// Provider name to remove
        name: String,
    },

    /// Show provider details
    Info {
        /// Provider name
        name: String,
    },

    /// Load a provider ephemerally — fetch spec, detect auth, cache for immediate use
    #[command(
        after_help = "Examples:\n  ati provider load https://petstore3.swagger.io/api/v3/openapi.json --name petstore\n  ati provider load --mcp --transport http --url https://mcp.serpapi.com/mcp --name serpapi\n  ati provider load spec.json --name myapi --save"
    )]
    Load {
        /// Path or URL to OpenAPI spec (omit for --mcp mode)
        spec: Option<String>,
        /// Provider name
        #[arg(long)]
        name: String,
        /// Load as MCP provider instead of OpenAPI
        #[arg(long)]
        mcp: bool,
        /// MCP transport: http or stdio
        #[arg(long)]
        transport: Option<String>,
        /// MCP server URL (required for http transport)
        #[arg(long)]
        url: Option<String>,
        /// Command to run (required for stdio transport)
        #[arg(long)]
        command: Option<String>,
        /// Arguments for the stdio command (repeatable)
        #[arg(long, allow_hyphen_values = true)]
        args: Vec<String>,
        /// Environment variables as KEY=VALUE (repeatable, use ${keyring_ref} for secrets)
        #[arg(long)]
        env: Vec<String>,
        /// Auth type override (auto-detected for OpenAPI)
        #[arg(long)]
        auth: Option<String>,
        /// Keyring key name for auth
        #[arg(long)]
        auth_key: Option<String>,
        /// Custom header name for auth (e.g., x-api-key)
        #[arg(long)]
        auth_header: Option<String>,
        /// Custom query parameter name for auth
        #[arg(long)]
        auth_query: Option<String>,
        /// Save permanently (write TOML manifest) instead of caching
        #[arg(long)]
        save: bool,
        /// Cache TTL in seconds (default: 3600 = 1 hour)
        #[arg(long, default_value = "3600")]
        ttl: u64,
    },

    /// Install skills declared in a provider's manifest
    #[command(name = "install-skills")]
    InstallSkills {
        /// Provider name
        name: String,
    },

    /// Remove a cached (ephemeral) provider
    Unload {
        /// Provider name to unload
        name: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum KeyCommands {
    /// Store an API key
    Set {
        /// Key name (e.g. myapi_api_key)
        name: String,
        /// Key value (e.g. sk-xxx)
        value: String,
    },
    /// List stored API keys (values masked)
    List,
    /// Remove an API key
    Remove {
        /// Key name to remove
        name: String,
    },
}

#[derive(Subcommand, Debug)]
pub enum AuthCommands {
    /// Show current scopes, agent info, and expiry
    Status,
}

#[derive(Subcommand, Debug)]
pub enum TokenCommands {
    /// Generate an ES256 key pair (or HS256 secret)
    Keygen {
        /// Algorithm: ES256 (default) or HS256
        #[arg(long, default_value = "ES256")]
        algorithm: String,
    },
    /// Issue (sign) a JWT with given claims
    Issue {
        /// Agent identity (JWT sub claim)
        #[arg(long)]
        sub: String,
        /// Space-delimited scopes (JWT scope claim)
        #[arg(long)]
        scope: String,
        /// Time-to-live in seconds (default: 1800 = 30 minutes)
        #[arg(long, default_value = "1800")]
        ttl: u64,
        /// Audience (default: ati-proxy)
        #[arg(long)]
        aud: Option<String>,
        /// Issuer
        #[arg(long)]
        iss: Option<String>,
        /// Path to ES256 private key PEM file
        #[arg(long)]
        key: Option<String>,
        /// HS256 shared secret (hex string)
        #[arg(long)]
        secret: Option<String>,
        /// Rate limits as pattern=spec (e.g. "tool:github:*=10/hour")
        #[arg(long)]
        rate: Vec<String>,
    },
    /// Decode a JWT without verification (show claims)
    Inspect {
        /// JWT token string
        token: String,
    },
    /// Fully verify a JWT (signature + expiry + audience + issuer)
    Validate {
        /// JWT token string
        token: String,
        /// Path to ES256 public key PEM file
        #[arg(long)]
        key: Option<String>,
        /// HS256 shared secret (hex string)
        #[arg(long)]
        secret: Option<String>,
    },
}

#[derive(Subcommand, Debug)]
pub enum AuditCommands {
    /// Show recent audit entries
    Tail {
        /// Number of entries to show (default: 20)
        #[arg(short, long, default_value = "20")]
        n: usize,
    },
    /// Search audit entries
    Search {
        /// Filter by tool name (supports trailing wildcard, e.g. github:*)
        #[arg(long)]
        tool: Option<String>,
        /// Show entries since duration ago (e.g. 1h, 30m, 7d)
        #[arg(long)]
        since: Option<String>,
    },
}

#[derive(Subcommand, Debug)]
pub enum PlanCommands {
    /// Execute a saved plan file
    Execute {
        /// Path to the plan JSON file
        file: String,
        /// Confirm each step before executing
        #[arg(long)]
        confirm_each: bool,
    },
}

#[tokio::main]
async fn main() {
    let mut cli = Cli::parse();

    // Resolve -J shorthand: if --json flag is set, override output to JSON
    if cli.json {
        cli.output = OutputFormat::Json;
    }

    cli::common::ensure_ati_dir();

    // Initialize structured logging (and optionally Sentry when compiled with --features sentry).
    let log_mode = match &cli.command {
        Commands::Proxy { .. } => core::logging::LogMode::Proxy,
        _ => core::logging::LogMode::Cli,
    };
    let _sentry_guard = core::logging::init(log_mode, cli.verbose);

    let result = match &cli.command {
        Commands::Run { tool_name, args } => cli::call::execute(&cli, tool_name, args).await,
        Commands::Tool(subcmd) => cli::tools::execute(&cli, subcmd).await,
        Commands::Skill(subcmd) => cli::skills::execute(&cli, subcmd).await,
        Commands::SkillAti(subcmd) => cli::skillati::execute(&cli, subcmd).await,
        Commands::Assist {
            args,
            plan,
            save,
            local,
        } => cli::help::execute_with_plan(&cli, args, *plan, save.as_deref(), *local).await,
        Commands::Plan(subcmd) => cli::plan::execute(&cli, subcmd).await,
        Commands::Provider(subcmd) => cli::provider::execute(&cli, subcmd).await,
        Commands::Auth(subcmd) => cli::auth::execute(&cli, subcmd).await,
        Commands::Token(subcmd) => {
            cli::token::execute(subcmd).map_err(|e| e as Box<dyn std::error::Error>)
        }
        Commands::Init { proxy, es256 } => cli::init::execute(*proxy, *es256),
        Commands::Key(subcmd) => cli::keys::execute(subcmd),
        Commands::Audit(subcmd) => cli::audit::execute(&cli, subcmd),
        Commands::Proxy {
            port,
            bind,
            ati_dir,
            env_keys,
        } => {
            let dir = ati_dir
                .as_deref()
                .map(std::path::PathBuf::from)
                .unwrap_or_else(cli::common::ati_dir);
            proxy::server::run(*port, bind.clone(), dir, cli.verbose, *env_keys).await
        }
    };

    if let Err(e) = result {
        // Always route errors through tracing so they reach Sentry when the
        // sentry feature is enabled. Structured JSON output is rendered
        // separately to stderr for machine consumers.
        tracing::error!(error = %e, "ati command failed");
        let is_json = matches!(cli.output, OutputFormat::Json);
        if is_json {
            let error_json = core::error::format_structured_error(e.as_ref(), cli.verbose);
            eprintln!("{error_json}");
        } else if cli.verbose {
            let mut source = std::error::Error::source(e.as_ref());
            while let Some(cause) = source {
                tracing::debug!("  caused by: {cause}");
                source = std::error::Error::source(cause);
            }
        }
        let exit_code = core::error::exit_code_for_error(e.as_ref());
        // Flush the sentry transport queue before process::exit (which
        // bypasses destructors). Without this, queued error events are
        // lost on CLI exit.
        core::logging::shutdown(_sentry_guard);
        process::exit(exit_code);
    }
}