Skip to main content

agent_profile/adapter/
kiro.rs

1// Copyright (c) 2026 Costas Kirgoussios
2//
3// This software is licensed under the PolyForm Noncommercial License 1.0.0.
4// You may obtain a copy of the License at:
5// https://polyformproject.org/licenses/noncommercial/1.0.0/
6
7//! Kiro CLI: `KIRO_HOME` per profile (SP4 design §8.5 — settings move, the data tree does not).
8
9use std::path::PathBuf;
10
11use super::{
12    Adapter, AdapterEvidence, AdapterMetadata, Capability, CapabilityClaim, CapabilityState,
13    EnvOverride, Mechanism, PathKind, PlanContext, PlannedLaunch, SupportLevel, profile_dir,
14};
15use crate::config::AppRoot;
16use crate::error::Result;
17use crate::name::ProfileName;
18
19const VAR: &str = "KIRO_HOME";
20
21static METADATA: AdapterMetadata = AdapterMetadata {
22    id: "kiro",
23    executable: "kiro-cli",
24    mechanism: Mechanism::Env(VAR),
25    support: SupportLevel::Proven,
26    evidence: AdapterEvidence {
27        mechanism_id: "kiro-home-v1",
28        verified_at: "2026-11-09",
29        upstream_version: "3.1.0",
30        source_url: "https://kiro.dev/cli/",
31        notes: "the settings subsystem obeyed the variable — `+ f \
32                /home/agent/probe-target/settings/cli.json` at the target against an \
33                `(absent)` default — while a data tree appeared \
34                under `.local/share/kiro-cli` (§9 outcome 5, Q3 answered partial); \
35                the probe installed by the vendor script, which refuses a version \
36                argument (§12), so upstream_version is the `version-extracted` value; \
37                KIRO_API_KEY is the documented bypass variable and AWS_ACCESS_KEY_ID, \
38                AWS_PROFILE are also strings-measured; no credential file appeared in \
39                the delta",
40    },
41    capabilities: &[
42        CapabilityClaim {
43            capability: Capability::ConfigIsolation,
44            state: CapabilityState::Supported,
45            basis: "measured: `+ f /home/agent/probe-target/settings/cli.json` — the settings \
46                    subsystem moved its file under KIRO_HOME, and the watched default `~/.kiro` \
47                    and `.local/share/kiro-cli` were both `(absent)` at baseline, so the \
48                    settings write at the target is the variable's",
49        },
50        CapabilityClaim {
51            capability: Capability::CredentialIsolation,
52            state: CapabilityState::Unknown,
53            basis: "unmeasured: naming the vendor session required — no credential file appeared \
54                    in the delta, `KIRO_API_KEY` (strings measured) is the documented bypass \
55                    variable, and `AWS_ACCESS_KEY_ID`, `AWS_PROFILE` are also strings-measured",
56        },
57        CapabilityClaim {
58            capability: Capability::StateIsolation,
59            state: CapabilityState::NotGuaranteed,
60            basis: "measured: `+ f /home/agent/.local/share/kiro-cli/data.sqlite3`, \
61                    `+ d .../run-receipts` (`.reaper.lock`) and \
62                    `+ f .../telemetry-export-drops.lock` — the data subsystem ignores KIRO_HOME \
63                    and wrote the whole `.local/share/kiro-cli` tree at the default (Q3)",
64        },
65    ],
66    env: &[EnvOverride { name: VAR, sensitive: false }],
67    conflicts: &[],
68    credential_env: &["KIRO_API_KEY", "AWS_ACCESS_KEY_ID", "AWS_PROFILE"],
69};
70
71/// The Kiro CLI adapter.
72#[derive(Debug, Clone, Copy)]
73pub struct Kiro;
74
75impl Adapter for Kiro {
76    fn metadata(&self) -> &'static AdapterMetadata {
77        &METADATA
78    }
79
80    fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
81        // `KIRO_HOME` names the directory the agent builds its tree under: the measured delta shows
82        // `settings/cli.json` directly inside it, so the profile directory IS the home. The data
83        // tree at `.local/share/kiro-cli` is the subsystem that ignores the variable (its basis) —
84        // declaring it would create a directory the agent refuses to relocate.
85        vec![(profile_dir(root, profile, METADATA.id), PathKind::Dir)]
86    }
87
88    fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
89        METADATA.mechanism.plan(self, ctx)
90    }
91}