a4aa 0.4.1

Adapter-For-All-Agents — unified agent client adapter: process supervision, stdio/PTY transports, JSON-RPC and ACP lanes, CLI drivers, and a stdio sidecar for hosts in other languages.
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
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
//! `AgentCard` — the complete per-agent descriptor an `a4aa-<agent>` crate
//! exports.
//!
//! A [`Dialect`] describes where a host may *write* (hooks, rules, skills,
//! MCP config). The card carries everything else a host needs to know about
//! an agent as data:
//!
//! - [`DetectSurface`] — how to find the agent on a machine: binary and
//!   process names, bundled-binary directories, config markers, desktop
//!   bundles.
//! - [`AgentSurface`] — how to reach it: the lane-family declaration from
//!   which [`AgentCard::spec`] materializes a runtime [`AgentSpec`].
//! - [`FeatureSurface`] — what it natively supports: instruction channel,
//!   execution/permission modes, attachments, steering, model catalog.
//! - [`UsageSurface`] — where token accounting appears, live on the wire
//!   and in persisted session stores.
//! - [`HistoryRoot`] — where its conversation/session history lives.
//!
//! Cards are `const` data: a host reads them, a lane honors the declared
//! surface, and no secret or user path is ever stored in one (`{home}` and
//! `{sessionId}` are placeholders the consumer expands).

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::dialect::Dialect;
use crate::env::{EnvPolicy, EnvSource};
use crate::model::{
    AgentSpec, BindingReceiptShape, CliLaneSpec, CommandSpec, LaneSpec, PromptChannel, RpcCallSpec,
    RpcCompletionSpec, RpcEnvelopeSpec, RpcLaneSpec, RpcMcpConfig, RpcMcpToolApproval,
    RpcTransport, SessionScope, StreamMode,
};

/// The complete descriptor for one agent.
pub struct AgentCard {
    /// Catalog id — identical to `dialect.id`.
    pub id: &'static str,
    /// Display name.
    pub label: &'static str,
    /// Mechanism write surfaces (hooks, rules, skills, …).
    pub dialect: &'static Dialect,
    /// How to reach the agent.
    pub surface: AgentSurface,
    /// Further native interaction forms, in preference order after the
    /// primary `surface` — a verified ACP endpoint ahead of a stream-json
    /// CLI, and so on. `AgentCard::specs` materializes primary first,
    /// alternates in declaration order, for a host's lane fallback chain.
    pub alternates: &'static [AlternateLane],
    /// Machine-discovery facts.
    pub detect: DetectSurface,
    /// Natively supported features.
    pub features: FeatureSurface,
    /// Token-accounting locations.
    pub usage: UsageSurface,
    /// Session-store and history locations.
    pub history: &'static [HistoryRoot],
}

impl AgentCard {
    /// Materialize the runtime spec for this agent.
    ///
    /// `command.program` carries the canonical binary name; a host that
    /// resolved an absolute path during detection substitutes it before
    /// spawning. Session resume is request-driven: a host holding a binding
    /// sets `SessionRequest::resume` and the lane applies the declared
    /// `sessionResume`/`resumeArgs` surface — a spec that cannot express
    /// resume fails closed at open.
    pub fn spec(&self) -> AgentSpec {
        self.spec_for(&self.surface, self.detect.binaries.first().copied(), false)
    }

    /// The primary spec plus each declared alternate, in order — the
    /// launch chain a host walks when the preferred native form cannot
    /// establish or finish a turn.
    pub fn specs(&self) -> Vec<AgentSpec> {
        std::iter::once(self.spec())
            .chain(self.alternates.iter().map(|alternate| {
                self.spec_for(
                    &alternate.surface,
                    alternate
                        .binary
                        .or_else(|| self.detect.binaries.first().copied()),
                    false,
                )
            }))
            .collect()
    }

    /// The materialized spec plus the card's `trusted_workspace_args`.
    /// Call only when the workspace this host will dispatch is one it is
    /// willing to grant: those arguments mark the dispatched directory
    /// trusted (for example cursor's `--trust`). `spec()` never includes
    /// them — trusting a workspace is the host's explicit decision, not
    /// the card's default.
    pub fn spec_trusting_workspace(&self) -> AgentSpec {
        self.spec_for(&self.surface, self.detect.binaries.first().copied(), true)
    }

    /// `spec_trusting_workspace()` plus each alternate.
    pub fn specs_trusting_workspace(&self) -> Vec<AgentSpec> {
        std::iter::once(self.spec_trusting_workspace())
            .chain(self.alternates.iter().map(|alternate| {
                self.spec_for(
                    &alternate.surface,
                    alternate
                        .binary
                        .or_else(|| self.detect.binaries.first().copied()),
                    true,
                )
            }))
            .collect()
    }

    /// The materialized spec with `EnvPolicy::InheritAll` — the fidelity
    /// path. `spec()` sanitizes the child environment; this opt-in hands
    /// the spawned agent every ambient host variable, so call it only
    /// from a custody path that deliberately trusts the launched agent
    /// (`va serve`'s local chain, AwP presets). Materializing a card is
    /// never, by itself, a reason to expose host credentials.
    pub fn spec_inheriting_host_env(&self) -> AgentSpec {
        let mut spec = self.spec();
        spec.command.env = EnvPolicy::InheritAll {
            set: BTreeMap::new(),
        };
        spec
    }

    /// `spec_inheriting_host_env()` plus each alternate.
    pub fn specs_inheriting_host_env(&self) -> Vec<AgentSpec> {
        self.specs()
            .into_iter()
            .map(|mut spec| {
                spec.command.env = EnvPolicy::InheritAll {
                    set: BTreeMap::new(),
                };
                spec
            })
            .collect()
    }

    fn spec_for(
        &self,
        surface: &AgentSurface,
        binary: Option<&'static str>,
        trust_workspace: bool,
    ) -> AgentSpec {
        let lane = match surface {
            AgentSurface::Acp {
                prefer_load,
                auth_key,
                ..
            } => LaneSpec::Acp {
                mcp_servers: Vec::new(),
                prefer_load: *prefer_load,
                auth_key: auth_key.map(AcpAuthKey::source),
            },
            AgentSurface::Cli(cli) => LaneSpec::Cli(Box::new(CliLaneSpec {
                args: {
                    let mut args: Vec<String> =
                        cli.args.iter().map(|arg| (*arg).to_owned()).collect();
                    if trust_workspace {
                        args.extend(
                            cli.trusted_workspace_args
                                .iter()
                                .map(|arg| (*arg).to_owned()),
                        );
                    }
                    args
                },
                prompt_channel: cli.prompt_channel,
                stream: cli.stream,
                resume_args: cli
                    .resume_args
                    .iter()
                    .map(|arg| (*arg).to_owned())
                    .collect(),
                resume_spawn_args: cli
                    .resume_spawn_args
                    .iter()
                    .map(|arg| (*arg).to_owned())
                    .collect(),
                stdin_template: cli.stdin_template.map(|text| {
                    serde_json::from_str(text).expect("a4aa card stdin template is json")
                }),
                json_lines: cli.json_lines,
                session_id_path: cli.session_id_path.map(str::to_owned),
                usage_path: cli
                    .usage_path
                    .map(str::to_owned)
                    .or_else(|| self.usage.live.as_ref().map(|usage| usage.path.to_owned())),
                open_args: cli.open_args.iter().map(|arg| (*arg).to_owned()).collect(),
                binding_file_env: cli.binding_file_env.map(str::to_owned),
                binding_receipt_shape: cli.binding_receipt_shape,
                result_selector: cli.result_selector.map(str::to_owned),
                result_selector_field: cli.result_selector_field.map(str::to_owned),
                result_path: cli.result_path.map(str::to_owned),
                execution_modes: self
                    .features
                    .execution_modes
                    .iter()
                    .map(|mode| (*mode).to_owned())
                    .collect(),
                permission_modes: self
                    .features
                    .permission_modes
                    .iter()
                    .map(|mode| (*mode).to_owned())
                    .collect(),
                effort_model_suffixes: cli
                    .effort_model_suffixes
                    .iter()
                    .map(|level| (*level).to_owned())
                    .collect(),
                mcp_servers: Vec::new(),
            })),
            AgentSurface::Rpc(rpc) => LaneSpec::Rpc(Box::new(RpcLaneSpec {
                initialize: rpc.initialize.map(RpcCallDecl::call),
                initialized: rpc.initialized.map(str::to_owned),
                session_open: rpc.session_open.map(RpcCallDecl::call),
                session_resume: rpc.session_resume.map(RpcCallDecl::call),
                prompt: rpc.prompt.call(),
                completion: rpc.completion.map(|done| RpcCompletionSpec {
                    method: done.method.to_owned(),
                    turn_id_path: done.turn_id_path.map(str::to_owned),
                    status_path: done.status_path.map(str::to_owned),
                    status_value: done.status_value.map(str::to_owned),
                }),
                cancel: rpc.cancel.map(RpcCallDecl::call),
                session_close: rpc.session_close.map(RpcCallDecl::call),
                text_event_method: rpc.text_event_method.map(str::to_owned),
                text_path: rpc.text_path.map(str::to_owned),
                result_text_path: rpc.result_text_path.map(str::to_owned),
                pre_turn: rpc.pre_turn.iter().map(|decl| decl.call()).collect(),
                session_scope: rpc.session_scope,
                kill_on_cancel: rpc.kill_on_cancel,
                envelope: rpc.envelope.map(|envelope| RpcEnvelopeSpec {
                    method_field: envelope.method_field.to_owned(),
                    string_ids: envelope.string_ids,
                    params_inline: envelope.params_inline,
                    response_method: envelope.response_method.map(str::to_owned),
                    data_field: envelope.data_field.map(str::to_owned),
                    result_field: envelope.result_field.map(str::to_owned),
                    error_field: envelope.error_field.map(str::to_owned),
                    success_field: envelope.success_field.map(str::to_owned),
                    command_field: envelope.command_field.map(str::to_owned),
                    discriminant_field: envelope.discriminant_field.map(str::to_owned),
                    request_marker: envelope.request_marker.map(str::to_owned),
                    notification_marker: envelope.notification_marker.map(str::to_owned),
                    const_fields: envelope
                        .const_fields
                        .iter()
                        .map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
                        .collect(),
                }),
                passthrough: rpc
                    .passthrough
                    .iter()
                    .map(|method| (*method).to_owned())
                    .collect(),
                transport: match rpc.transport {
                    RpcTransportDecl::Stdio => RpcTransport::Stdio,
                    RpcTransportDecl::WebSocket(announce) => RpcTransport::WebSocket {
                        announce: announce.to_owned(),
                    },
                },
                max_frame_bytes: rpc.max_frame_bytes,
                usage_path: rpc.usage_path.map(str::to_owned),
                failure_path: rpc.failure_path.map(str::to_owned),
                failure_value: rpc.failure_value.map(str::to_owned),
                failure_message_path: rpc.failure_message_path.map(str::to_owned),
                mcp_servers: Vec::new(),
                mcp_config: rpc.mcp.map(|mcp| RpcMcpConfig {
                    flag: mcp.flag.to_owned(),
                    root: mcp.root.to_owned(),
                    env_vars_key: mcp.env_vars_key.to_owned(),
                    tools_approval: match (mcp.tools_approval_key, mcp.tools_approval_value) {
                        (Some(key), Some(value)) => Some(RpcMcpToolApproval {
                            key: key.to_owned(),
                            value: value.to_owned(),
                        }),
                        _ => None,
                    },
                    // The card declares the mechanism only. Naming the
                    // trusted servers is host configuration (`mcpConfig`),
                    // so a published card never carries a private service
                    // name and no server is pre-approved by default.
                    tools_approval_servers: Vec::new(),
                }),
            })),
            AgentSurface::Serve { .. } => LaneSpec::Serve {
                mcp_servers: Vec::new(),
            },
        };
        AgentSpec {
            id: self.id.to_owned(),
            client_name: None,
            lane,
            command: CommandSpec {
                program: binary.unwrap_or(self.id).to_owned(),
                args: Self::surface_spawn_args(surface)
                    .iter()
                    .map(|arg| (*arg).to_owned())
                    .collect(),
                cwd: None,
                env: EnvPolicy::default(),
            },
            max_output_bytes: None,
        }
    }

    /// Process argv prefix before the lane's own arguments (`app-server`
    /// for a `codex app-server` launch). Empty for agents whose argv is the
    /// lane template itself.
    fn surface_spawn_args(surface: &AgentSurface) -> &'static [&'static str] {
        match surface {
            AgentSurface::Acp { spawn_args, .. }
            | AgentSurface::Cli(CliSurface { spawn_args, .. })
            | AgentSurface::Serve { spawn_args } => spawn_args,
            AgentSurface::Rpc(rpc) => rpc.spawn_args,
        }
    }
}

/// The lane family plus its agent-specific surface.
pub enum AgentSurface {
    /// Agent Client Protocol: the protocol supplies session semantics, so
    /// the only agent knowledge is the launch argv and `session/load`
    /// preference.
    Acp {
        prefer_load: bool,
        spawn_args: &'static [&'static str],
        /// Credential source for the `authenticate` retry: when session
        /// establishment is refused with auth-required, the lane sends the
        /// resolved value as `_meta.api_key` alongside the advertised
        /// method id (Devin's contract). `None` keeps the stock
        /// method-id-only attempt.
        auth_key: Option<AcpAuthKey>,
    },
    /// Argv/stdin CLI, one process per turn.
    Cli(CliSurface),
    /// Host-declared JSON-RPC methods over stdio. Cards are `const`, so the
    /// surface rides by reference — `Box` cannot be built in const context.
    Rpc(&'static RpcSurface),
    /// The agent's own first-party HTTP server (its `serve` family
    /// subcommand): spawned per session, discovered on stdout, driven over
    /// loopback REST. Preferred transport where the agent offers one.
    Serve { spawn_args: &'static [&'static str] },
}

/// One additional native form on a card — the surface plus the launch
/// program when it differs from `detect.binaries[0]` (an ACP server bundle
/// ahead of the general CLI binary, for example).
pub struct AlternateLane {
    /// The lane declaration for this form.
    pub surface: AgentSurface,
    /// Launch program override; `None` reuses the card's first binary.
    pub binary: Option<&'static str>,
}

/// Const-declared `api_key` source for the acp `authenticate` retry;
/// materializes to an [`EnvSource`] on the spec so secret resolution
/// stays host-side.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AcpAuthKey {
    /// Read this host environment variable at session open.
    HostEnv(&'static str),
    /// Resolve this reference through the host's `SecretResolver`.
    Secret(&'static str),
}

impl AcpAuthKey {
    fn source(self) -> EnvSource {
        match self {
            Self::HostEnv(name) => EnvSource::HostEnv {
                host_env: name.to_owned(),
            },
            Self::Secret(reference) => EnvSource::Secret {
                secret: reference.to_owned(),
            },
        }
    }
}

/// Process-per-turn CLI contract.
pub struct CliSurface {
    /// Fixed argv template; `{prompt}`/`{model}`/`{effort}`/`{mode}`/
    /// `{permission}`/`{instructions}` placeholders substitute per turn.
    pub args: &'static [&'static str],
    /// argv materialized only through `spec_trusting_workspace()` — flags
    /// that grant the dispatched workspace elevated trust (cursor's
    /// `--trust`). A card declares them separately so adopting the card
    /// never silently grants workspace trust; the custody host opts in
    /// for the workspace it dispatches.
    pub trusted_workspace_args: &'static [&'static str],
    pub prompt_channel: PromptChannel,
    pub stream: StreamMode,
    /// argv appended when a session binding exists (`{sessionId}`
    /// substitutes it). Empty: the CLI has no persisted-session resume.
    pub resume_args: &'static [&'static str],
    /// argv replacing `spawn_args` entirely when a session binding exists —
    /// for CLIs whose resume shape reorders argv (`codex exec resume <id>`).
    /// Empty: spawn args stay constant.
    pub resume_spawn_args: &'static [&'static str],
    /// stdin payload template as static JSON text; replaces the bare prompt
    /// line when the prompt channel resolves to stdin. A leaf exactly
    /// `{prompt}` receives the prompt as a JSON string.
    pub stdin_template: Option<&'static str>,
    /// stdout is newline-delimited JSON events.
    pub json_lines: bool,
    /// Dotted path to the session id on a stdout line.
    pub session_id_path: Option<&'static str>,
    /// argv run once at open when no resume binding exists; the first
    /// non-empty stdout line becomes the session binding.
    pub open_args: &'static [&'static str],
    /// Environment variable the lane fills with a per-turn scratch file
    /// path; the agent's installed hook writes the session receipt there
    /// and the lane reads `conversationId`-family keys after the turn.
    pub binding_file_env: Option<&'static str>,
    /// What the receipt may carry beyond the session id.
    pub binding_receipt_shape: BindingReceiptShape,
    /// On a `json_lines` stream, the discriminator value of the record
    /// carrying the turn's authoritative text.
    pub result_selector: Option<&'static str>,
    /// Discriminator field for `result_selector`; defaults to `type`.
    pub result_selector_field: Option<&'static str>,
    /// Dotted path to the final text inside the `result_selector` record.
    pub result_path: Option<&'static str>,
    /// Dotted path to token counters on a stdout JSON line for this lane.
    /// `None` inherits the card's `usage.live` path; set it when the
    /// lane's stream records usage under a different shape (an ACP
    /// primary reports `session/update`, a CLI alternate `result.usage`).
    pub usage_path: Option<&'static str>,
    /// argv prefix before the lane args (for example a subcommand).
    pub spawn_args: &'static [&'static str],
    /// Model ids encode reasoning effort as a `-<level>` suffix: a
    /// requested `effort` rewrites that segment of the effective model
    /// value (`cursor-grok-4.6` + `high` → `cursor-grok-4.6-high`). Lists
    /// the vocabulary's levels; empty means effort needs its own
    /// placeholder in `args`.
    pub effort_model_suffixes: &'static [&'static str],
}

/// Declared JSON-RPC method surface over stdio.
pub struct RpcSurface {
    /// Handshake before any session call; `initialized` names the
    /// notification sent once it resolves.
    pub initialize: Option<RpcCallDecl>,
    pub initialized: Option<&'static str>,
    /// Opens the session when the agent is session-oriented.
    pub session_open: Option<RpcCallDecl>,
    /// Opens by resuming an existing binding (`{sessionId}`).
    pub session_resume: Option<RpcCallDecl>,
    /// Per-turn call; its result ends the turn unless `completion` names
    /// a notification that does.
    pub prompt: RpcCallDecl,
    /// Notification sent on explicit user cancellation.
    pub cancel: Option<RpcCallDecl>,
    /// Teardown call, where the agent has one.
    pub session_close: Option<RpcCallDecl>,
    /// Notification method carrying streamed text.
    pub text_event_method: Option<&'static str>,
    /// Dotted path inside that notification to the text payload — a string
    /// leaf or an array of `{type,text}` blocks whose text concatenates.
    pub text_path: Option<&'static str>,
    /// Async turn completion when `prompt` only acknowledges the turn.
    pub completion: Option<CompletionDecl>,
    /// Dotted path to the text inside a prompt result.
    pub result_text_path: Option<&'static str>,
    /// Per-turn configuration calls issued before every `prompt` (for
    /// example model/thinking-level selection on flat-envelope agents). A
    /// call whose params resolve to an empty object is skipped that turn.
    pub pre_turn: &'static [RpcCallDecl],
    /// Whether a resume binding maps verbatim or seeds a fresh
    /// process-scoped wire id.
    pub session_scope: SessionScope,
    /// Explicit cancellation kills the process rather than issuing a
    /// `cancel` call — for agents whose only cancel semantic is teardown.
    pub kill_on_cancel: bool,
    /// Non-stock framing conventions (flat `{type,id,…}` envelopes);
    /// `None` is plain JSON-RPC 2.0.
    pub envelope: Option<&'static EnvelopeDecl>,
    /// Methods a consumer may invoke verbatim through the lane.
    pub passthrough: &'static [&'static str],
    /// argv prefix before the lane args (for example a subcommand).
    /// A `WebSocket` transport substitutes `{port}` with a free loopback
    /// port the lane picks (for example `daemon --port {port}`).
    pub spawn_args: &'static [&'static str],
    /// Byte transport the protocol rides; `Stdio` is the process's own
    /// stdin/stdout, `WebSocket` spawns a first-party daemon and connects
    /// to its announced endpoint.
    pub transport: RpcTransportDecl,
    /// Largest inbound frame the lane accepts; `None` uses the shared
    /// default. Declare the agent's real frame size when its terminal
    /// events embed large payloads.
    pub max_frame_bytes: Option<usize>,
    /// Dotted path inside a notification's params to a token-usage
    /// object; each observation is retained and the last lands on
    /// `TurnOutcome::usage`. `None` scans the conventional carriers
    /// (`params.usage`, `params.message.usage`).
    pub usage_path: Option<&'static str>,
    /// Dotted path on any notification's params marking a failed turn —
    /// for agents that report an errored turn as a normal completion
    /// record (for example pi's `message.stopReason`). `None` disables
    /// stream-level failure detection.
    pub failure_path: Option<&'static str>,
    /// Marker value `failure_path` reports for a failed turn; `None`
    /// treats any extracted marker as failure.
    pub failure_value: Option<&'static str>,
    /// Dotted path to the failure's detail text, landed on the error.
    pub failure_message_path: Option<&'static str>,
    /// Native MCP registration surface; `None` means this rpc lane cannot
    /// install servers and must refuse a non-empty server list.
    pub mcp: Option<&'static RpcMcpDecl>,
}

/// Card-level MCP registration declaration for a dotted `key=value`
/// config-override surface (for example `codex app-server -c …`).
#[derive(Clone, Copy)]
pub struct RpcMcpDecl {
    /// Argv flag introducing one override (`-c`).
    pub flag: &'static str,
    /// Dotted config root under which servers are named (`mcp_servers`).
    pub root: &'static str,
    /// Config key that forwards environment variable names (`env_vars`).
    pub env_vars_key: &'static str,
    /// Optional key installed per server to pre-approve tool calls
    /// (`default_tools_approval_mode`). Which servers receive it is host
    /// configuration (`mcpConfig.toolsApprovalServers`), never a blanket
    /// card-wide default.
    pub tools_approval_key: Option<&'static str>,
    /// Value installed for `tools_approval_key` (`approve`).
    pub tools_approval_value: Option<&'static str>,
}

/// Card-level transport declaration — `&'static` friendly counterpart of
/// [`crate::model::RpcTransport`].
#[derive(Clone, Copy)]
pub enum RpcTransportDecl {
    /// The agent process speaks the protocol on its own stdio.
    Stdio,
    /// Spawn a first-party daemon; its stdout announces the `ws://`
    /// endpoint after this prefix (for example `WebSocket endpoint: `).
    WebSocket(&'static str),
}

/// Wire conventions for an rpc lane that is not stock JSON-RPC 2.0 — see
/// [`crate::model::RpcEnvelopeSpec`] for field semantics.
#[derive(Clone, Copy)]
pub struct EnvelopeDecl {
    pub method_field: &'static str,
    pub string_ids: bool,
    pub params_inline: bool,
    pub response_method: Option<&'static str>,
    pub data_field: Option<&'static str>,
    pub result_field: Option<&'static str>,
    pub error_field: Option<&'static str>,
    pub success_field: Option<&'static str>,
    pub command_field: Option<&'static str>,
    /// Field that classifies inbound frames by value; absent:
    /// `method_field` doubles as the classifier.
    pub discriminant_field: Option<&'static str>,
    /// Discriminant value stamped on outbound requests (for example
    /// `"request"`) — only with `discriminant_field`.
    pub request_marker: Option<&'static str>,
    /// Discriminant value stamped on outbound notifications (for example
    /// `"notification"`) — only with `discriminant_field`.
    pub notification_marker: Option<&'static str>,
    /// Constant members merged into every outbound frame — protocol
    /// identity tags only; frame-kind tagging uses the markers.
    pub const_fields: &'static [(&'static str, &'static str)],
}

/// One declared call: method plus a static-JSON params template.
#[derive(Clone, Copy)]
pub struct RpcCallDecl {
    pub method: &'static str,
    /// JSON text; string leaves may carry `{prompt}`/`{sessionId}`/
    /// `{turnId}`/`{model}`/`{effort}`/`{instructions}`/`{cwd}`
    /// placeholders, substituted at call time.
    pub params: &'static str,
}

impl RpcCallDecl {
    fn call(self) -> RpcCallSpec {
        RpcCallSpec {
            method: self.method.to_owned(),
            params: serde_json::from_str(self.params).expect("a4aa card rpc params are json"),
        }
    }
}

/// Async turn completion: the prompt call is an acknowledgement and the
/// named notification ends the turn.
#[derive(Clone, Copy)]
pub struct CompletionDecl {
    pub method: &'static str,
    /// Dotted path to the turn id inside the notification params.
    pub turn_id_path: Option<&'static str>,
    /// Dotted path to the terminal status string; becomes the outcome's
    /// `stopReason` verbatim.
    pub status_path: Option<&'static str>,
    /// Only a notification whose `status_path` equals this value ends the
    /// turn.
    pub status_value: Option<&'static str>,
}

/// Machine-discovery facts for hosts that scan for installed agents. Path
/// templates are `{home}`-relative; `oses` limits a path to the listed
/// platforms (`macos`, `linux`, `windows`), and an empty list means all.
pub struct DetectSurface {
    /// Executable names to resolve on `PATH`.
    pub binaries: &'static [&'static str],
    /// Process image names observed while the agent runs.
    pub processes: &'static [&'static str],
    /// Directories hosting a bundled executable outside `PATH`.
    pub binary_dirs: &'static [OsPath],
    /// Configuration files whose presence marks the agent configured.
    pub config: &'static [&'static str],
    /// Marker paths proving prior use.
    pub detection: &'static [&'static str],
    /// Desktop application executables that bundle this agent.
    pub apps: &'static [OsPath],
}

/// A path valid only on the listed platforms; an empty `oses` means all.
pub struct OsPath {
    pub path: &'static str,
    pub oses: &'static [&'static str],
}

/// One session-store or history location.
pub struct HistoryRoot {
    /// `{home}`-relative path template.
    pub path: &'static str,
    /// Stable kind label a host reports (`"claude-project-transcripts"`,
    /// `"codex-session-store"`, …).
    pub kind: &'static str,
    pub store: StoreKind,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StoreKind {
    JsonlFile,
    JsonlDir,
    SqliteDb,
    MarkdownFile,
    Directory,
    Other,
}

/// Token-accounting locations. Declared facts only — a counter the agent
/// does not report stays absent everywhere downstream.
pub struct UsageSurface {
    /// Live stream: which event carries a usage object and where inside it.
    pub live: Option<LiveUsage>,
    /// Session store: how token counters appear in persisted records.
    pub transcript: Option<TranscriptUsage>,
    /// Vendors whose only token accounting is a hosted API: the canonical
    /// endpoint. The credential and request shape stay the consumer's —
    /// the card records where the authoritative numbers live.
    pub hosted: Option<&'static str>,
}

/// Usage on the wire.
pub struct LiveUsage {
    /// Event discriminator — a notification method on an rpc lane, or the
    /// value a stdout line's event/type field carries on a `jsonLines` cli
    /// lane.
    pub event: &'static str,
    /// Dotted path to the counter object inside the event.
    pub path: &'static str,
}

/// Usage inside a session-store record.
pub struct TranscriptUsage {
    /// `HistoryRoot::kind` holding token records.
    pub root: &'static str,
    /// Record discriminator `(dotted path, equals value)` — only matching
    /// records carry counters. `None`: any record bearing `container` is a
    /// usage record (for stores that mark usage by presence, not type).
    pub record: Option<(&'static str, &'static str)>,
    /// Dotted path to the counter object inside a matching record.
    pub container: &'static str,
    /// Field names read as input/prompt tokens; first present wins.
    pub input: &'static [&'static str],
    /// Field names read as cached input tokens.
    pub cached: &'static [&'static str],
    /// Field names read as output/completion tokens.
    pub output: &'static [&'static str],
    /// Field names read as a reported total; empty where the store has
    /// none — the host sums, the adapter never invents a total.
    pub total: &'static [&'static str],
}

/// Natively supported features — declared facts, not negotiated state.
/// Runtime-negotiated capabilities ride on [`crate::model::Capabilities`].
pub struct FeatureSurface {
    /// How session guidance reaches the agent.
    pub instructions: InstructionChannel,
    /// Accepted execution-mode selector values, distinct from permission
    /// presets (for example `plan` versus `request-review`).
    pub execution_modes: &'static [&'static str],
    /// Accepted permission/mode selector values.
    pub permission_modes: &'static [&'static str],
    /// Mode applied when the host selects none.
    pub permission_default: Option<&'static str>,
    /// Prompt attachments the agent accepts.
    pub attachments: AttachmentSupport,
    /// Reasoning-effort selection is supported.
    pub effort: bool,
    /// Mid-turn steering: follow-up input without ending the turn.
    pub steer: bool,
    /// Model enumeration route; `None` where the agent has no catalog API.
    pub models: ModelCatalog,
}

/// The channel session guidance takes.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InstructionChannel {
    /// Guidance composes into prompt text — the default for agents with no
    /// native channel.
    Compose,
    /// A session-open parameter carries it (for example
    /// `developerInstructions`); the surface's params template declares the
    /// `{instructions}` placeholder.
    Param(&'static str),
    /// An argv flag carries it (for example `--append-system-prompt`); the
    /// lane's args template declares the `{instructions}` placeholder.
    Flag(&'static str),
}

/// Prompt attachment forms the agent accepts.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttachmentSupport {
    None,
    /// Local file references (for example `localImage` input items).
    LocalImages,
}

/// How a host enumerates the agent's model list.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ModelCatalog {
    /// No catalog API — the host's own model knowledge applies.
    None,
    /// A JSON-RPC method lists models (rpc lane passthrough).
    RpcMethod(&'static str),
    /// argv prints the catalog.
    CliArgs(&'static [&'static str]),
}